c-next 0.2.17 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +7645 -5941
  3. package/dist/index.js.map +4 -4
  4. package/grammar/CNext.g4 +8 -0
  5. package/package.json +1 -3
  6. package/src/transpiler/Transpiler.ts +286 -26
  7. package/src/transpiler/__tests__/compileCommandsDiscovery.integration.test.ts +94 -0
  8. package/src/transpiler/__tests__/externalSymbolRecovery.integration.test.ts +215 -0
  9. package/src/transpiler/logic/__tests__/detectAssemblySyntax.test.ts +116 -0
  10. package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +8 -0
  11. package/src/transpiler/logic/analysis/MixedTypeCategoryAnalyzer.ts +408 -0
  12. package/src/transpiler/logic/analysis/PassByValueAnalyzer.ts +16 -97
  13. package/src/transpiler/logic/analysis/ReturnPathAnalyzer.ts +171 -0
  14. package/src/transpiler/logic/analysis/__tests__/MixedTypeCategoryAnalyzer.test.ts +346 -0
  15. package/src/transpiler/logic/analysis/__tests__/ReturnPathAnalyzer.test.ts +232 -0
  16. package/src/transpiler/logic/analysis/runAnalyzers.ts +23 -2
  17. package/src/transpiler/logic/analysis/types/IMixedTypeCategoryError.ts +17 -0
  18. package/src/transpiler/logic/analysis/types/IReturnPathError.ts +12 -0
  19. package/src/transpiler/logic/detectAssemblySyntax.ts +80 -0
  20. package/src/transpiler/logic/parser/grammar/CNext.interp +4 -1
  21. package/src/transpiler/logic/parser/grammar/CNext.tokens +169 -167
  22. package/src/transpiler/logic/parser/grammar/CNextLexer.interp +4 -1
  23. package/src/transpiler/logic/parser/grammar/CNextLexer.tokens +169 -167
  24. package/src/transpiler/logic/parser/grammar/CNextLexer.ts +545 -541
  25. package/src/transpiler/logic/parser/grammar/CNextListener.ts +11 -0
  26. package/src/transpiler/logic/parser/grammar/CNextParser.ts +1257 -1185
  27. package/src/transpiler/logic/parser/grammar/CNextVisitor.ts +7 -0
  28. package/src/transpiler/logic/preprocessor/CompileCommandsReader.ts +332 -0
  29. package/src/transpiler/logic/preprocessor/ExternalDeclarationOracle.ts +202 -0
  30. package/src/transpiler/logic/preprocessor/Preprocessor.ts +24 -6
  31. package/src/transpiler/logic/preprocessor/ToolchainDetector.ts +42 -1
  32. package/src/transpiler/logic/preprocessor/__tests__/CompileCommandsReader.test.ts +216 -0
  33. package/src/transpiler/logic/preprocessor/__tests__/ExternalDeclarationOracle.test.ts +153 -0
  34. package/src/transpiler/logic/preprocessor/__tests__/Preprocessor.test.ts +43 -18
  35. package/src/transpiler/logic/preprocessor/__tests__/ToolchainDetector.crossCompiler.test.ts +75 -0
  36. package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/dependent.h +7 -0
  37. package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/predecessor.h +5 -0
  38. package/src/transpiler/logic/preprocessor/types/ICompileCommandsResult.ts +14 -0
  39. package/src/transpiler/logic/preprocessor/types/IPreprocessOptions.ts +17 -0
  40. package/src/transpiler/logic/symbols/SymbolTable.ts +45 -0
  41. package/src/transpiler/logic/symbols/__tests__/SymbolTable.test.ts +49 -0
  42. package/src/transpiler/output/MisraSuppressionUtils.ts +52 -0
  43. package/src/transpiler/output/__tests__/MisraSuppressionUtils.test.ts +67 -0
  44. package/src/transpiler/output/codegen/CodeGenerator.ts +45 -4
  45. package/src/transpiler/output/codegen/TypeResolver.ts +148 -0
  46. package/src/transpiler/output/codegen/TypeValidator.ts +179 -81
  47. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +13 -1
  48. package/src/transpiler/output/codegen/__tests__/TypeResolver.test.ts +93 -0
  49. package/src/transpiler/output/codegen/__tests__/TypeValidator.alwaysTrueLoop.test.ts +91 -0
  50. package/src/transpiler/output/codegen/__tests__/TypeValidator.test.ts +86 -14
  51. package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +13 -0
  52. package/src/transpiler/output/codegen/assignment/AssignmentKind.ts +1 -1
  53. package/src/transpiler/output/codegen/assignment/handlers/AccessPatternHandlers.ts +20 -12
  54. package/src/transpiler/output/codegen/assignment/handlers/ArrayHandlers.ts +424 -22
  55. package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +31 -18
  56. package/src/transpiler/output/codegen/assignment/handlers/BitmapHandlers.ts +7 -8
  57. package/src/transpiler/output/codegen/assignment/handlers/RegisterHandlers.ts +6 -8
  58. package/src/transpiler/output/codegen/assignment/handlers/RegisterUtils.ts +12 -10
  59. package/src/transpiler/output/codegen/assignment/handlers/SimpleHandler.ts +3 -7
  60. package/src/transpiler/output/codegen/assignment/handlers/SpecialHandlers.ts +7 -9
  61. package/src/transpiler/output/codegen/assignment/handlers/StringHandlers.ts +12 -10
  62. package/src/transpiler/output/codegen/assignment/handlers/__tests__/ArrayHandlers.test.ts +479 -11
  63. package/src/transpiler/output/codegen/generators/IOrchestrator.ts +3 -0
  64. package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +28 -15
  65. package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +26 -13
  66. package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +129 -1
  67. package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +64 -8
  68. package/src/transpiler/output/codegen/generators/statements/__tests__/ControlFlowGenerator.test.ts +8 -5
  69. package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +30 -2
  70. package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +16 -13
  71. package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +19 -0
  72. package/src/transpiler/output/codegen/helpers/__tests__/StringDeclHelper.test.ts +57 -11
  73. package/src/transpiler/output/codegen/subscript/SubscriptClassifier.ts +30 -11
  74. package/src/transpiler/output/codegen/subscript/TSubscriptKind.ts +1 -1
  75. package/src/transpiler/output/codegen/subscript/__tests__/SubscriptClassifier.test.ts +38 -6
  76. package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +65 -24
  77. package/src/transpiler/state/CodeGenState.ts +20 -16
  78. package/src/transpiler/types/ICachedFileEntry.ts +7 -0
  79. package/src/utils/cache/CacheManager.ts +13 -2
  80. package/src/utils/cache/__tests__/CacheManager.test.ts +18 -1
  81. package/src/utils/constants/TypeConstants.ts +13 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Unit tests for CompileCommandsReader.
3
+ *
4
+ * cnext must resolve external C/C++ headers exactly as the compiler will. Every
5
+ * build system (CMake, PlatformIO, Meson, Zephyr, bear-wrapped Make) converges
6
+ * on the same artifact — a `compile_commands.json` compilation database listing,
7
+ * per translation unit, the compiler binary and its `-I` / `-D` flags. Reading
8
+ * that database is how clangd/clang-tidy stay build-system-agnostic; cnext reads
9
+ * it for the same reason. This is the pure parse layer (no filesystem): a DB
10
+ * string in, `{ includePaths, defines, compiler }` out.
11
+ */
12
+ import { describe, it, expect } from "vitest";
13
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import CompileCommandsReader from "../CompileCommandsReader";
17
+
18
+ describe("CompileCommandsReader.parse", () => {
19
+ it("extracts include paths from -I arguments, resolving relative to the entry directory", () => {
20
+ const db = JSON.stringify([
21
+ {
22
+ directory: "/project/build",
23
+ file: "/project/src/main.cpp",
24
+ arguments: [
25
+ "xtensa-esp32s3-elf-g++",
26
+ "-I/abs/include",
27
+ "-I../rel/include",
28
+ "-c",
29
+ "/project/src/main.cpp",
30
+ ],
31
+ },
32
+ ]);
33
+
34
+ const result = CompileCommandsReader.parse(db);
35
+
36
+ // Absolute -I kept verbatim.
37
+ expect(result.includePaths).toContain("/abs/include");
38
+ // Relative -I resolved against the entry's `directory` (/project/build).
39
+ expect(result.includePaths).toContain("/project/rel/include");
40
+ });
41
+
42
+ it("handles space-separated -I and -isystem forms", () => {
43
+ const db = JSON.stringify([
44
+ {
45
+ directory: "/project",
46
+ file: "/project/src/main.cpp",
47
+ arguments: [
48
+ "g++",
49
+ "-I",
50
+ "sep/include",
51
+ "-isystem",
52
+ "/opt/toolchain/include",
53
+ "-isystem/attached/system",
54
+ "-c",
55
+ "main.cpp",
56
+ ],
57
+ },
58
+ ]);
59
+
60
+ const result = CompileCommandsReader.parse(db);
61
+
62
+ expect(result.includePaths).toContain("/project/sep/include"); // -I <path>
63
+ expect(result.includePaths).toContain("/opt/toolchain/include"); // -isystem <path>
64
+ expect(result.includePaths).toContain("/attached/system"); // -isystem<path>
65
+ });
66
+
67
+ it("extracts defines: KEY=VAL, bare KEY, and space-separated forms", () => {
68
+ const db = JSON.stringify([
69
+ {
70
+ directory: "/project",
71
+ file: "/project/src/main.cpp",
72
+ arguments: [
73
+ "g++",
74
+ "-DARDUINO=10812",
75
+ "-DCORE_DEBUG_LEVEL",
76
+ "-D",
77
+ "F_CPU=240000000L",
78
+ "-c",
79
+ "main.cpp",
80
+ ],
81
+ },
82
+ ]);
83
+
84
+ const result = CompileCommandsReader.parse(db);
85
+
86
+ expect(result.defines.ARDUINO).toBe("10812"); // -DKEY=VAL
87
+ expect(result.defines.CORE_DEBUG_LEVEL).toBe(true); // bare -DKEY
88
+ expect(result.defines.F_CPU).toBe("240000000L"); // -D KEY=VAL (separated)
89
+ });
90
+
91
+ it("parses the `command` string form with POSIX word-splitting", () => {
92
+ // PlatformIO emits a `command` string (not `arguments`). Word-splitting must
93
+ // match the shell the compiler was actually invoked through: real quotes are
94
+ // syntax (consumed), backslash-escaped quotes are literal (kept), and spaces
95
+ // inside quotes are preserved. The ARDUINO_BOARD case is the real gnarly one
96
+ // — outer real quotes wrapping backslash-escaped inner quotes, with spaces.
97
+ const db = JSON.stringify([
98
+ {
99
+ directory: "/project",
100
+ file: "/project/src/main.cpp",
101
+ command:
102
+ "xtensa-esp32s3-elf-g++ -Iinclude -I/abs/inc -DARDUINO=10812 " +
103
+ '-DARDUINO_BOARD="\\"Espressif ESP32-S3 (8 MB QD, No PSRAM)\\"" ' +
104
+ "'-DSINGLE=a b' -c main.cpp",
105
+ },
106
+ ]);
107
+
108
+ const result = CompileCommandsReader.parse(db);
109
+
110
+ expect(result.compiler).toBe("xtensa-esp32s3-elf-g++"); // argv[0]
111
+ expect(result.includePaths).toContain("/project/include"); // -Iinclude, rel
112
+ expect(result.includePaths).toContain("/abs/inc");
113
+ expect(result.defines.ARDUINO).toBe("10812");
114
+ // Escaped inner quotes kept, spaces preserved inside the real outer quotes.
115
+ expect(result.defines.ARDUINO_BOARD).toBe(
116
+ '"Espressif ESP32-S3 (8 MB QD, No PSRAM)"',
117
+ );
118
+ // Single-quoted operand: literal, spaces preserved, quotes consumed.
119
+ expect(result.defines.SINGLE).toBe("a b");
120
+ });
121
+
122
+ it("matches shlex on double-quote escapes and escaped whitespace", () => {
123
+ // Verified against `shlex.split`: inside "" only \" and \\ are escapes, so a
124
+ // backslash is KEPT before other characters (e.g. $); \<space> outside quotes
125
+ // is a literal space that joins one token.
126
+ const command = String.raw`cc -DA="x\\y" -DB="p\$q" -DD="lit\z" -DE=v\ w -c f.c`;
127
+ const db = JSON.stringify([{ directory: "/p", file: "/p/f.c", command }]);
128
+
129
+ const result = CompileCommandsReader.parse(db);
130
+
131
+ expect(result.defines.A).toBe("x\\y"); // "\\" -> one literal backslash
132
+ expect(result.defines.B).toBe("p\\$q"); // "\$" -> backslash kept
133
+ expect(result.defines.D).toBe("lit\\z"); // "\z" -> backslash kept
134
+ expect(result.defines.E).toBe("v w"); // v\ w -> single token "v w"
135
+ });
136
+
137
+ it("ignores trailing -I / -D flags that have no operand", () => {
138
+ const dbI = JSON.stringify([
139
+ { directory: "/p", file: "a.c", arguments: ["cc", "-I"] },
140
+ ]);
141
+ const dbD = JSON.stringify([
142
+ { directory: "/p", file: "a.c", arguments: ["cc", "-D"] },
143
+ ]);
144
+
145
+ expect(CompileCommandsReader.parse(dbI).includePaths).toEqual([]);
146
+ expect(CompileCommandsReader.parse(dbD).defines).toEqual({});
147
+ });
148
+
149
+ it("drops a trailing backslash without crashing", () => {
150
+ const db = JSON.stringify([
151
+ { directory: "/p", file: "/p/f.c", command: "cc -DA=1 -c f.c\\" },
152
+ ]);
153
+
154
+ const result = CompileCommandsReader.parse(db);
155
+
156
+ expect(result.defines.A).toBe("1");
157
+ expect(result.compiler).toBe("cc");
158
+ });
159
+
160
+ it("tolerates an entry with neither command nor arguments", () => {
161
+ const db = JSON.stringify([{ directory: "/p", file: "/p/a.c" }]);
162
+
163
+ const result = CompileCommandsReader.parse(db);
164
+
165
+ expect(result.includePaths).toEqual([]);
166
+ expect(result.defines).toEqual({});
167
+ expect(result.compiler).toBeNull();
168
+ });
169
+ });
170
+
171
+ describe("CompileCommandsReader.load", () => {
172
+ it("reads and parses a compile_commands.json file from disk", () => {
173
+ const dir = mkdtempSync(join(tmpdir(), "cnext-cc-"));
174
+ try {
175
+ writeFileSync(
176
+ join(dir, "compile_commands.json"),
177
+ JSON.stringify([
178
+ {
179
+ directory: dir,
180
+ file: "a.cpp",
181
+ command: "g++ -I/x/inc -DFOO=1 -c a.cpp",
182
+ },
183
+ ]),
184
+ );
185
+
186
+ const result = CompileCommandsReader.load(
187
+ join(dir, "compile_commands.json"),
188
+ );
189
+
190
+ expect(result).not.toBeNull();
191
+ expect(result?.includePaths).toContain("/x/inc");
192
+ expect(result?.defines.FOO).toBe("1");
193
+ expect(result?.compiler).toBe("g++");
194
+ } finally {
195
+ rmSync(dir, { recursive: true, force: true });
196
+ }
197
+ });
198
+
199
+ it("returns null for a missing file (caller falls back to config)", () => {
200
+ expect(
201
+ CompileCommandsReader.load("/no/such/dir/compile_commands.json"),
202
+ ).toBeNull();
203
+ });
204
+
205
+ it("returns null for malformed JSON rather than throwing", () => {
206
+ const dir = mkdtempSync(join(tmpdir(), "cnext-cc-"));
207
+ try {
208
+ writeFileSync(join(dir, "compile_commands.json"), "{ not valid json ");
209
+ expect(
210
+ CompileCommandsReader.load(join(dir, "compile_commands.json")),
211
+ ).toBeNull();
212
+ } finally {
213
+ rmSync(dir, { recursive: true, force: true });
214
+ }
215
+ });
216
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Unit tests for ExternalDeclarationOracle.
3
+ *
4
+ * cnext collects header symbols by preprocessing each header standalone, which
5
+ * fails for include-order-dependent framework headers (e.g. FreeRTOS task.h's
6
+ * `#ifndef INC_FREERTOS_H / #error`). The oracle recovers by preprocessing a
7
+ * header set AS A TRANSLATION UNIT (in order, keeping `#line` directives), then
8
+ * splitting the output back into per-file slices (for the caller to parse into
9
+ * full symbols) plus the names of function-like macros (no declaration to parse).
10
+ */
11
+ import { describe, it, expect } from "vitest";
12
+ import { join, dirname } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import ExternalDeclarationOracle from "../ExternalDeclarationOracle";
15
+ import Preprocessor from "../Preprocessor";
16
+
17
+ const currentDir = dirname(fileURLToPath(import.meta.url));
18
+ const FIXTURES = join(currentDir, "fixtures", "oracle");
19
+
20
+ function allContent(perFile: Map<string, string>): string {
21
+ return [...perFile.values()].join("\n");
22
+ }
23
+
24
+ describe("ExternalDeclarationOracle", () => {
25
+ it("recovers a declaration only valid after its predecessor header", async () => {
26
+ const preprocessor = new Preprocessor();
27
+ if (!preprocessor.isAvailable()) return; // no toolchain in this env
28
+
29
+ const recovery = await ExternalDeclarationOracle.recover(
30
+ ['"predecessor.h"', '"dependent.h"'],
31
+ preprocessor,
32
+ { includePaths: [FIXTURES] },
33
+ );
34
+
35
+ // dependent.h's #error passes only because predecessor.h ran first in the
36
+ // TU; oracle_fn is emitted (by a predecessor-defined macro) into its slice.
37
+ expect(recovery).not.toBeNull();
38
+ expect(allContent(recovery!.perFileContent)).toContain("oracle_fn");
39
+ });
40
+
41
+ it("buckets each header's declarations under its own path", async () => {
42
+ const preprocessor = new Preprocessor();
43
+ if (!preprocessor.isAvailable()) return;
44
+
45
+ const recovery = await ExternalDeclarationOracle.recover(
46
+ ['"predecessor.h"', '"dependent.h"'],
47
+ preprocessor,
48
+ { includePaths: [FIXTURES] },
49
+ );
50
+
51
+ const dependentKey = [...recovery!.perFileContent.keys()].find((k) =>
52
+ k.endsWith("dependent.h"),
53
+ );
54
+ expect(dependentKey).toBeDefined();
55
+ expect(recovery!.perFileContent.get(dependentKey!)).toContain("oracle_fn");
56
+ });
57
+
58
+ it("collects function-like macro names consumed by a normal preprocess", async () => {
59
+ const preprocessor = new Preprocessor();
60
+ if (!preprocessor.isAvailable()) return;
61
+
62
+ const recovery = await ExternalDeclarationOracle.recover(
63
+ ['"predecessor.h"'],
64
+ preprocessor,
65
+ { includePaths: [FIXTURES] },
66
+ );
67
+
68
+ expect(recovery!.macroNames.has("ORACLE_TICKS")).toBe(true);
69
+ });
70
+
71
+ it("does not surface names that are not declared anywhere", async () => {
72
+ const preprocessor = new Preprocessor();
73
+ if (!preprocessor.isAvailable()) return;
74
+
75
+ const recovery = await ExternalDeclarationOracle.recover(
76
+ ['"predecessor.h"'],
77
+ preprocessor,
78
+ { includePaths: [FIXTURES] },
79
+ );
80
+
81
+ expect(allContent(recovery!.perFileContent)).not.toContain(
82
+ "totally_undeclared_symbol",
83
+ );
84
+ expect(recovery!.macroNames.has("totally_undeclared_symbol")).toBe(false);
85
+ });
86
+
87
+ it("drops a header that cannot preprocess and keeps the rest", async () => {
88
+ const preprocessor = new Preprocessor();
89
+ if (!preprocessor.isAvailable()) return;
90
+
91
+ const recovery = await ExternalDeclarationOracle.recover(
92
+ // the middle include does not exist; it must be dropped, not fatal
93
+ [
94
+ '"predecessor.h"',
95
+ "<this_header_does_not_exist_xyzzy.h>",
96
+ '"dependent.h"',
97
+ ],
98
+ preprocessor,
99
+ { includePaths: [FIXTURES] },
100
+ );
101
+
102
+ expect(allContent(recovery!.perFileContent)).toContain("oracle_fn");
103
+ expect(recovery!.macroNames.has("ORACLE_TICKS")).toBe(true);
104
+ });
105
+
106
+ it("keeps searching after dropping more than half the includes", async () => {
107
+ const preprocessor = new Preprocessor();
108
+ if (!preprocessor.isAvailable()) return;
109
+
110
+ // Three of five includes are individually unpreprocessable. Dropping them
111
+ // one per iteration (>half) must not exhaust the loop before the two good
112
+ // includes are tried together. Regression for the aliased loop bound that
113
+ // re-read the shrinking directive count and bailed once >half were dropped.
114
+ const recovery = await ExternalDeclarationOracle.recover(
115
+ [
116
+ "<cnext_no_such_header_a.h>",
117
+ "<cnext_no_such_header_b.h>",
118
+ "<cnext_no_such_header_c.h>",
119
+ '"predecessor.h"',
120
+ '"dependent.h"',
121
+ ],
122
+ preprocessor,
123
+ { includePaths: [FIXTURES] },
124
+ );
125
+
126
+ expect(recovery).not.toBeNull();
127
+ expect(allContent(recovery!.perFileContent)).toContain("oracle_fn");
128
+ });
129
+
130
+ it("returns null for no includes", async () => {
131
+ const preprocessor = new Preprocessor();
132
+ const recovery = await ExternalDeclarationOracle.recover(
133
+ [],
134
+ preprocessor,
135
+ {},
136
+ );
137
+ expect(recovery).toBeNull();
138
+ });
139
+
140
+ it("returns null when no header can preprocess", async () => {
141
+ const preprocessor = new Preprocessor();
142
+ if (!preprocessor.isAvailable()) return;
143
+
144
+ // The only include does not exist; after it is dropped nothing remains, so
145
+ // the largest-working-TU search exhausts and recovery yields null.
146
+ const recovery = await ExternalDeclarationOracle.recover(
147
+ ["<cnext_no_such_header_xyzzy.h>"],
148
+ preprocessor,
149
+ { includePaths: [FIXTURES] },
150
+ );
151
+ expect(recovery).toBeNull();
152
+ });
153
+ });
@@ -20,16 +20,21 @@ type ExecCallback = (
20
20
  result: { stdout: string; stderr: string },
21
21
  ) => void;
22
22
 
23
- // Mock child_process - exec is promisified, so we mock it to work with promisify
23
+ // Mock child_process - execFile is promisified, so we mock it to work with
24
+ // promisify. execFile is called as (file, args, options, callback).
24
25
  vi.mock("node:child_process", () => ({
25
- exec: (cmd: string, opts: unknown, cb?: ExecCallback) => {
26
+ execFile: (
27
+ file: string,
28
+ args: string[],
29
+ opts: unknown,
30
+ cb?: ExecCallback,
31
+ ) => {
26
32
  // promisify converts callback-based to promise-based
27
- // We handle both forms (2-arg and 3-arg)
28
33
  let callback: ExecCallback | undefined = cb;
29
34
  if (typeof opts === "function") {
30
35
  callback = opts as ExecCallback;
31
36
  }
32
- const result = mockExec(cmd, opts);
37
+ const result = mockExec(file, args, opts);
33
38
  if (result instanceof Promise) {
34
39
  result
35
40
  .then((r: { stdout: string; stderr: string }) => callback?.(null, r))
@@ -170,11 +175,12 @@ describe("Preprocessor", () => {
170
175
  await preprocessor.preprocess("/path/to/file.h");
171
176
 
172
177
  expect(mockExec).toHaveBeenCalled();
173
- const command = mockExec.mock.calls[0][0];
174
- expect(command).toContain("/usr/bin/gcc");
175
- expect(command).toContain("-E");
176
- expect(command).toContain("/path/to/file.h");
177
- expect(command).toContain("-I/path/to");
178
+ const file = mockExec.mock.calls[0][0];
179
+ const args = mockExec.mock.calls[0][1];
180
+ expect(file).toBe("/usr/bin/gcc");
181
+ expect(args).toContain("-E");
182
+ expect(args).toContain("/path/to/file.h");
183
+ expect(args).toContain("-I/path/to");
178
184
  });
179
185
 
180
186
  it("includes custom include paths", async () => {
@@ -188,9 +194,9 @@ describe("Preprocessor", () => {
188
194
  includePaths: ["/custom/include", "/another/path"],
189
195
  });
190
196
 
191
- const command = mockExec.mock.calls[0][0];
192
- expect(command).toContain("-I/custom/include");
193
- expect(command).toContain("-I/another/path");
197
+ const args = mockExec.mock.calls[0][1];
198
+ expect(args).toContain("-I/custom/include");
199
+ expect(args).toContain("-I/another/path");
194
200
  });
195
201
 
196
202
  it("includes defines", async () => {
@@ -208,10 +214,29 @@ describe("Preprocessor", () => {
208
214
  },
209
215
  });
210
216
 
211
- const command = mockExec.mock.calls[0][0];
212
- expect(command).toContain("-DDEBUG");
213
- expect(command).toContain("-DVERSION=1.0");
214
- expect(command).not.toContain("-DDISABLED");
217
+ const args = mockExec.mock.calls[0][1];
218
+ expect(args).toContain("-DDEBUG");
219
+ expect(args).toContain("-DVERSION=1.0");
220
+ expect(args).not.toContain("-DDISABLED");
221
+ });
222
+
223
+ it("passes defines with shell metacharacters as a single argv element", async () => {
224
+ mockExec.mockReturnValue({ stdout: "content", stderr: "" });
225
+
226
+ const preprocessor = new Preprocessor(mockToolchain);
227
+ // A board-name macro whose value contains spaces and parentheses — valid
228
+ // for the compiler, but would break /bin/sh if the command were routed
229
+ // through a shell (Issue: cnext preprocessor must invoke via argv).
230
+ await preprocessor.preprocess("/path/to/file.h", {
231
+ defines: {
232
+ ARDUINO_BOARD: '"Espressif ESP32-S3 (8 MB QD, No PSRAM)"',
233
+ },
234
+ });
235
+
236
+ const args = mockExec.mock.calls[0][1];
237
+ expect(args).toContain(
238
+ '-DARDUINO_BOARD="Espressif ESP32-S3 (8 MB QD, No PSRAM)"',
239
+ );
215
240
  });
216
241
 
217
242
  it("returns success result with content", async () => {
@@ -290,8 +315,8 @@ int y = 10;
290
315
  keepLineDirectives: false,
291
316
  });
292
317
 
293
- const command = mockExec.mock.calls[0][0];
294
- expect(command).toContain("-P");
318
+ const args = mockExec.mock.calls[0][1];
319
+ expect(args).toContain("-P");
295
320
  });
296
321
 
297
322
  it("handles preprocessor errors", async () => {
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Unit tests for ToolchainDetector.fromPath and the CNEXT_CROSS_COMPILER override.
3
+ *
4
+ * Cross-compilation targets (e.g. ESP32/xtensa) ship framework headers that
5
+ * only preprocess with the target compiler — host gcc lacks their system
6
+ * headers and predefined macros. CNEXT_CROSS_COMPILER lets a project point cnext's
7
+ * preprocessor at that compiler (e.g. xtensa-esp32s3-elf-gcc).
8
+ */
9
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
10
+ import ToolchainDetector from "../ToolchainDetector";
11
+
12
+ vi.mock("node:child_process", () => ({ execSync: vi.fn() }));
13
+ vi.mock("node:fs", () => ({ existsSync: vi.fn() }));
14
+
15
+ import { execSync } from "node:child_process";
16
+ import { existsSync } from "node:fs";
17
+
18
+ describe("ToolchainDetector.fromPath", () => {
19
+ beforeEach(() => vi.clearAllMocks());
20
+ afterEach(() => vi.restoreAllMocks());
21
+
22
+ it("builds a cross-compiler toolchain from an executable name on PATH", () => {
23
+ vi.mocked(execSync).mockImplementation((cmd: string) => {
24
+ if (cmd === "which xtensa-esp32s3-elf-gcc") {
25
+ return "/opt/xt/bin/xtensa-esp32s3-elf-gcc\n";
26
+ }
27
+ return "xtensa-esp32s3-elf-gcc (crosstool-NG) 12.2.0\n";
28
+ });
29
+ vi.mocked(existsSync).mockReturnValue(true);
30
+
31
+ const tc = ToolchainDetector.fromPath("xtensa-esp32s3-elf-gcc");
32
+
33
+ expect(tc).not.toBeNull();
34
+ expect(tc!.cpp).toBe("/opt/xt/bin/xtensa-esp32s3-elf-gcc");
35
+ expect(tc!.name).toBe("xtensa-esp32s3-elf-gcc");
36
+ expect(tc!.isCrossCompiler).toBe(true);
37
+ });
38
+
39
+ it("accepts an absolute compiler path directly", () => {
40
+ vi.mocked(existsSync).mockReturnValue(true);
41
+ vi.mocked(execSync).mockReturnValue("gcc 13\n");
42
+
43
+ const tc = ToolchainDetector.fromPath("/usr/bin/xtensa-esp32s3-elf-gcc");
44
+
45
+ expect(tc!.cpp).toBe("/usr/bin/xtensa-esp32s3-elf-gcc");
46
+ });
47
+
48
+ it("returns null when the compiler path does not exist", () => {
49
+ vi.mocked(existsSync).mockReturnValue(false);
50
+
51
+ expect(ToolchainDetector.fromPath("/nope/xtensa-gcc")).toBeNull();
52
+ });
53
+ });
54
+
55
+ describe("ToolchainDetector.detect with CNEXT_CROSS_COMPILER", () => {
56
+ const original = process.env.CNEXT_CROSS_COMPILER;
57
+ beforeEach(() => vi.clearAllMocks());
58
+ afterEach(() => {
59
+ vi.restoreAllMocks();
60
+ if (original === undefined) delete process.env.CNEXT_CROSS_COMPILER;
61
+ else process.env.CNEXT_CROSS_COMPILER = original;
62
+ });
63
+
64
+ it("prefers CNEXT_CROSS_COMPILER over auto-detected toolchains", () => {
65
+ process.env.CNEXT_CROSS_COMPILER = "/opt/xt/bin/xtensa-esp32s3-elf-gcc";
66
+ vi.mocked(existsSync).mockReturnValue(true);
67
+ vi.mocked(execSync).mockReturnValue("xtensa gcc 12.2\n");
68
+
69
+ const tc = ToolchainDetector.detect();
70
+
71
+ expect(tc).not.toBeNull();
72
+ expect(tc!.cpp).toBe("/opt/xt/bin/xtensa-esp32s3-elf-gcc");
73
+ expect(tc!.isCrossCompiler).toBe(true);
74
+ });
75
+ });
@@ -0,0 +1,7 @@
1
+ /* Dependent header (mirrors FreeRTOS task.h): refuses standalone inclusion and
2
+ * emits its declaration via a predecessor-defined macro. Only resolves when
3
+ * predecessor.h is included first in the same translation unit. */
4
+ #ifndef PRED_GUARD
5
+ #error "include predecessor.h before dependent.h"
6
+ #endif
7
+ DECLARE_ORACLE_FN
@@ -0,0 +1,5 @@
1
+ /* Predecessor header (mirrors FreeRTOS.h): defines the guard, a declaration
2
+ * macro, and a function-like macro that dependent.h / callers rely on. */
3
+ #define PRED_GUARD 1
4
+ #define DECLARE_ORACLE_FN void oracle_fn(int x);
5
+ #define ORACLE_TICKS(ms) ((ms) / 10)
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The compiler's own view, derived from a `compile_commands.json` compilation
3
+ * database: the include search path, preprocessor defines, and compiler binary.
4
+ */
5
+ interface ICompileCommandsResult {
6
+ /** Include search paths (absolute), in first-seen order, de-duplicated. */
7
+ includePaths: string[];
8
+ /** Preprocessor defines: `-D KEY=VAL` -> `VAL`, bare `-D KEY` -> `true`. */
9
+ defines: Record<string, string | boolean>;
10
+ /** The compiler binary (argv[0]) of the entries, or null if none found. */
11
+ compiler: string | null;
12
+ }
13
+
14
+ export default ICompileCommandsResult;
@@ -15,6 +15,23 @@ interface IPreprocessOptions {
15
15
 
16
16
  /** Keep #line directives for source mapping (default: true) */
17
17
  keepLineDirectives?: boolean;
18
+
19
+ /**
20
+ * Headers whose macros to import before preprocessing the target file
21
+ * (gcc/clang `-imacros`), in order. Supplies include-order macro context so a
22
+ * header that requires a predecessor can preprocess — e.g. FreeRTOS `task.h`
23
+ * needs `INC_FREERTOS_H` and attribute macros defined by `FreeRTOS.h` first.
24
+ * Unlike `-include`, `-imacros` keeps only the predecessors' macros, not their
25
+ * declarations, so the output stays scoped to the target file.
26
+ */
27
+ imacros?: string[];
28
+
29
+ /**
30
+ * Dump macro definitions (gcc/clang `-dM`) instead of preprocessed source.
31
+ * Output is the `#define` list, used to discover function-like macros (e.g.
32
+ * FreeRTOS `pdMS_TO_TICKS`) that a plain preprocess consumes at use sites.
33
+ */
34
+ dumpMacros?: boolean;
18
35
  }
19
36
 
20
37
  export default IPreprocessOptions;
@@ -110,6 +110,16 @@ class SymbolTable {
110
110
  */
111
111
  private readonly needsStructKeyword: Set<string> = new Set();
112
112
 
113
+ /**
114
+ * Issue #985 recovery: names of external function-like MACROS recovered by
115
+ * translation-unit preprocessing (ExternalDeclarationOracle) for headers cnext
116
+ * could not preprocess standalone. Macros have no declaration to parse, so
117
+ * only their names are known; consulted by the undeclared-`global.`-call check
118
+ * so it does not false-positive on a real macro (e.g. FreeRTOS pdMS_TO_TICKS).
119
+ * Recovered FUNCTIONS are registered as full symbols (with signatures) instead.
120
+ */
121
+ private readonly externalDeclarationNames: Set<string> = new Set();
122
+
113
123
  /**
114
124
  * Issue #958: Immutable struct symbol state — additive only, query-time resolution.
115
125
  * Replaces separate opaqueTypes, typedefStructTypes, structTagAliases fields.
@@ -355,6 +365,21 @@ class SymbolTable {
355
365
  }
356
366
  }
357
367
 
368
+ /**
369
+ * Register external declaration names recovered via TU preprocessing.
370
+ * @see externalDeclarationNames
371
+ */
372
+ addExternalDeclarationNames(names: ReadonlySet<string>): void {
373
+ for (const name of names) {
374
+ this.externalDeclarationNames.add(name);
375
+ }
376
+ }
377
+
378
+ /** Whether a name was recovered as an external function / function-like macro. */
379
+ hasExternalDeclaration(name: string): boolean {
380
+ return this.externalDeclarationNames.has(name);
381
+ }
382
+
358
383
  /**
359
384
  * Get a C symbol by name (returns first match, or undefined)
360
385
  */
@@ -1016,6 +1041,26 @@ class SymbolTable {
1016
1041
  });
1017
1042
  }
1018
1043
 
1044
+ /**
1045
+ * Issue #985: Clear a struct tag's recorded body. Used by external-declaration
1046
+ * recovery to undo a PHANTOM body — one fabricated by ANTLR error-recovery when
1047
+ * the normal pass parsed a header's huge preprocessed blob — so a type the
1048
+ * clean per-file re-parse proves opaque (e.g. lvgl `lv_obj_t`) resolves as
1049
+ * opaque again (and codegen uses a pointer).
1050
+ * @param structTag The struct tag name (e.g., "_lv_obj_t")
1051
+ */
1052
+ clearStructTagHasBody(structTag: string): void {
1053
+ if (!this.structState.structTagsWithBodies.has(structTag)) return;
1054
+ this.structState = produce(this.structState, (draft) => {
1055
+ draft.structTagsWithBodies.delete(structTag);
1056
+ });
1057
+ }
1058
+
1059
+ /** Issue #985: The struct tag a typedef aliases, if any (e.g. lv_obj_t -> _lv_obj_t). */
1060
+ getStructTagForTypedef(typedefName: string): string | undefined {
1061
+ return this.structState.typedefToTag.get(typedefName);
1062
+ }
1063
+
1019
1064
  /**
1020
1065
  * Issue #958: Get all struct tags with bodies for cache serialization.
1021
1066
  * @returns Array of struct tag names