c-next 0.2.16 → 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 (118) hide show
  1. package/README.md +18 -2
  2. package/dist/index.js +8897 -6260
  3. package/dist/index.js.map +4 -4
  4. package/grammar/CNext.g4 +12 -0
  5. package/package.json +4 -2
  6. package/src/transpiler/Transpiler.ts +376 -48
  7. package/src/transpiler/__tests__/DualCodePaths.test.ts +1 -1
  8. package/src/transpiler/__tests__/compileCommandsDiscovery.integration.test.ts +94 -0
  9. package/src/transpiler/__tests__/externalSymbolRecovery.integration.test.ts +215 -0
  10. package/src/transpiler/logic/__tests__/detectAssemblySyntax.test.ts +116 -0
  11. package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +65 -10
  12. package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +186 -14
  13. package/src/transpiler/logic/analysis/MixedTypeCategoryAnalyzer.ts +408 -0
  14. package/src/transpiler/logic/analysis/PassByValueAnalyzer.ts +16 -97
  15. package/src/transpiler/logic/analysis/ReturnPathAnalyzer.ts +171 -0
  16. package/src/transpiler/logic/analysis/SignedShiftAnalyzer.ts +124 -12
  17. package/src/transpiler/logic/analysis/__tests__/FunctionCallAnalyzer.test.ts +200 -0
  18. package/src/transpiler/logic/analysis/__tests__/InitializationAnalyzer.test.ts +386 -1
  19. package/src/transpiler/logic/analysis/__tests__/MixedTypeCategoryAnalyzer.test.ts +346 -0
  20. package/src/transpiler/logic/analysis/__tests__/ReturnPathAnalyzer.test.ts +232 -0
  21. package/src/transpiler/logic/analysis/__tests__/SignedShiftAnalyzer.test.ts +211 -0
  22. package/src/transpiler/logic/analysis/runAnalyzers.ts +23 -2
  23. package/src/transpiler/logic/analysis/types/IMixedTypeCategoryError.ts +17 -0
  24. package/src/transpiler/logic/analysis/types/IReturnPathError.ts +12 -0
  25. package/src/transpiler/logic/detectAssemblySyntax.ts +80 -0
  26. package/src/transpiler/logic/parser/grammar/CNext.interp +4 -1
  27. package/src/transpiler/logic/parser/grammar/CNext.tokens +169 -167
  28. package/src/transpiler/logic/parser/grammar/CNextLexer.interp +4 -1
  29. package/src/transpiler/logic/parser/grammar/CNextLexer.tokens +169 -167
  30. package/src/transpiler/logic/parser/grammar/CNextLexer.ts +545 -541
  31. package/src/transpiler/logic/parser/grammar/CNextListener.ts +11 -0
  32. package/src/transpiler/logic/parser/grammar/CNextParser.ts +1318 -1178
  33. package/src/transpiler/logic/parser/grammar/CNextVisitor.ts +7 -0
  34. package/src/transpiler/logic/preprocessor/CompileCommandsReader.ts +332 -0
  35. package/src/transpiler/logic/preprocessor/ExternalDeclarationOracle.ts +202 -0
  36. package/src/transpiler/logic/preprocessor/Preprocessor.ts +24 -6
  37. package/src/transpiler/logic/preprocessor/ToolchainDetector.ts +42 -1
  38. package/src/transpiler/logic/preprocessor/__tests__/CompileCommandsReader.test.ts +216 -0
  39. package/src/transpiler/logic/preprocessor/__tests__/ExternalDeclarationOracle.test.ts +153 -0
  40. package/src/transpiler/logic/preprocessor/__tests__/Preprocessor.test.ts +43 -18
  41. package/src/transpiler/logic/preprocessor/__tests__/ToolchainDetector.crossCompiler.test.ts +75 -0
  42. package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/dependent.h +7 -0
  43. package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/predecessor.h +5 -0
  44. package/src/transpiler/logic/preprocessor/types/ICompileCommandsResult.ts +14 -0
  45. package/src/transpiler/logic/preprocessor/types/IPreprocessOptions.ts +17 -0
  46. package/src/transpiler/logic/symbols/SymbolTable.ts +62 -2
  47. package/src/transpiler/logic/symbols/__tests__/SymbolTable.test.ts +55 -4
  48. package/src/transpiler/logic/symbols/cnext/collectors/VariableCollector.ts +15 -2
  49. package/src/transpiler/logic/symbols/cnext/utils/TypeUtils.ts +64 -50
  50. package/src/transpiler/output/MisraSuppressionUtils.ts +52 -0
  51. package/src/transpiler/output/__tests__/MisraSuppressionUtils.test.ts +67 -0
  52. package/src/transpiler/output/codegen/CodeGenerator.ts +196 -98
  53. package/src/transpiler/output/codegen/TypeResolver.ts +148 -0
  54. package/src/transpiler/output/codegen/TypeValidator.ts +179 -81
  55. package/src/transpiler/output/codegen/__tests__/CodeGenerator.coverage.test.ts +165 -17
  56. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +163 -35
  57. package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +2 -2
  58. package/src/transpiler/output/codegen/__tests__/TypeResolver.test.ts +93 -0
  59. package/src/transpiler/output/codegen/__tests__/TypeValidator.alwaysTrueLoop.test.ts +91 -0
  60. package/src/transpiler/output/codegen/__tests__/TypeValidator.test.ts +97 -14
  61. package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +13 -0
  62. package/src/transpiler/output/codegen/assignment/AssignmentKind.ts +1 -1
  63. package/src/transpiler/output/codegen/assignment/handlers/AccessPatternHandlers.ts +20 -12
  64. package/src/transpiler/output/codegen/assignment/handlers/ArrayHandlers.ts +424 -22
  65. package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +31 -18
  66. package/src/transpiler/output/codegen/assignment/handlers/BitmapHandlers.ts +7 -8
  67. package/src/transpiler/output/codegen/assignment/handlers/RegisterHandlers.ts +6 -8
  68. package/src/transpiler/output/codegen/assignment/handlers/RegisterUtils.ts +12 -10
  69. package/src/transpiler/output/codegen/assignment/handlers/SimpleHandler.ts +3 -7
  70. package/src/transpiler/output/codegen/assignment/handlers/SpecialHandlers.ts +7 -9
  71. package/src/transpiler/output/codegen/assignment/handlers/StringHandlers.ts +12 -10
  72. package/src/transpiler/output/codegen/assignment/handlers/__tests__/ArrayHandlers.test.ts +479 -11
  73. package/src/transpiler/output/codegen/generators/IOrchestrator.ts +3 -0
  74. package/src/transpiler/output/codegen/generators/declarationGenerators/ScopeGenerator.ts +26 -7
  75. package/src/transpiler/output/codegen/generators/declarationGenerators/__tests__/ScopeGenerator.test.ts +86 -0
  76. package/src/transpiler/output/codegen/generators/expressions/BinaryExprGenerator.ts +43 -24
  77. package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +76 -58
  78. package/src/transpiler/output/codegen/generators/expressions/ExpressionGenerator.ts +9 -2
  79. package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +26 -13
  80. package/src/transpiler/output/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts +44 -0
  81. package/src/transpiler/output/codegen/generators/expressions/__tests__/ExpressionGenerator.test.ts +82 -1
  82. package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +129 -1
  83. package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +64 -8
  84. package/src/transpiler/output/codegen/generators/statements/__tests__/ControlFlowGenerator.test.ts +8 -5
  85. package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +30 -2
  86. package/src/transpiler/output/codegen/helpers/ParameterInputAdapter.ts +17 -3
  87. package/src/transpiler/output/codegen/helpers/ParameterSignatureBuilder.ts +17 -4
  88. package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +240 -42
  89. package/src/transpiler/output/codegen/helpers/SymbolLookupHelper.ts +0 -21
  90. package/src/transpiler/output/codegen/helpers/TypeGenerationHelper.ts +60 -39
  91. package/src/transpiler/output/codegen/helpers/TypeRegistrationEngine.ts +170 -36
  92. package/src/transpiler/output/codegen/helpers/VariableDeclHelper.ts +37 -39
  93. package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +19 -0
  94. package/src/transpiler/output/codegen/helpers/__tests__/ParameterInputAdapter.test.ts +117 -0
  95. package/src/transpiler/output/codegen/helpers/__tests__/ParameterSignatureBuilder.test.ts +94 -2
  96. package/src/transpiler/output/codegen/helpers/__tests__/StringDeclHelper.test.ts +322 -9
  97. package/src/transpiler/output/codegen/helpers/__tests__/SymbolLookupHelper.test.ts +0 -64
  98. package/src/transpiler/output/codegen/helpers/__tests__/TypeRegistrationEngine.test.ts +101 -0
  99. package/src/transpiler/output/codegen/helpers/__tests__/VariableDeclHelper.test.ts +29 -5
  100. package/src/transpiler/output/codegen/subscript/SubscriptClassifier.ts +30 -11
  101. package/src/transpiler/output/codegen/subscript/TSubscriptKind.ts +1 -1
  102. package/src/transpiler/output/codegen/subscript/__tests__/SubscriptClassifier.test.ts +38 -6
  103. package/src/transpiler/output/codegen/types/ICallbackTypeInfo.ts +2 -1
  104. package/src/transpiler/output/codegen/types/IParameterInput.ts +7 -0
  105. package/src/transpiler/output/headers/BaseHeaderGenerator.ts +8 -0
  106. package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +140 -24
  107. package/src/transpiler/output/headers/__tests__/HeaderGeneratorUtils.test.ts +280 -0
  108. package/src/transpiler/output/headers/generators/IHeaderTypeInput.ts +13 -0
  109. package/src/transpiler/output/headers/generators/generateStructHeader.ts +48 -28
  110. package/src/transpiler/state/CodeGenState.ts +91 -22
  111. package/src/transpiler/state/__tests__/CodeGenState.test.ts +253 -11
  112. package/src/transpiler/types/ICachedFileEntry.ts +7 -0
  113. package/src/utils/LiteralUtils.ts +23 -0
  114. package/src/utils/__tests__/LiteralUtils.test.ts +101 -0
  115. package/src/utils/cache/CacheManager.ts +13 -2
  116. package/src/utils/cache/__tests__/CacheManager.test.ts +18 -1
  117. package/src/utils/constants/TypeConstants.ts +13 -0
  118. package/src/utils/types/IParameterSymbol.ts +1 -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;