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.
- package/README.md +18 -2
- package/dist/index.js +8897 -6260
- package/dist/index.js.map +4 -4
- package/grammar/CNext.g4 +12 -0
- package/package.json +4 -2
- package/src/transpiler/Transpiler.ts +376 -48
- package/src/transpiler/__tests__/DualCodePaths.test.ts +1 -1
- package/src/transpiler/__tests__/compileCommandsDiscovery.integration.test.ts +94 -0
- package/src/transpiler/__tests__/externalSymbolRecovery.integration.test.ts +215 -0
- package/src/transpiler/logic/__tests__/detectAssemblySyntax.test.ts +116 -0
- package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +65 -10
- package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +186 -14
- package/src/transpiler/logic/analysis/MixedTypeCategoryAnalyzer.ts +408 -0
- package/src/transpiler/logic/analysis/PassByValueAnalyzer.ts +16 -97
- package/src/transpiler/logic/analysis/ReturnPathAnalyzer.ts +171 -0
- package/src/transpiler/logic/analysis/SignedShiftAnalyzer.ts +124 -12
- package/src/transpiler/logic/analysis/__tests__/FunctionCallAnalyzer.test.ts +200 -0
- package/src/transpiler/logic/analysis/__tests__/InitializationAnalyzer.test.ts +386 -1
- package/src/transpiler/logic/analysis/__tests__/MixedTypeCategoryAnalyzer.test.ts +346 -0
- package/src/transpiler/logic/analysis/__tests__/ReturnPathAnalyzer.test.ts +232 -0
- package/src/transpiler/logic/analysis/__tests__/SignedShiftAnalyzer.test.ts +211 -0
- package/src/transpiler/logic/analysis/runAnalyzers.ts +23 -2
- package/src/transpiler/logic/analysis/types/IMixedTypeCategoryError.ts +17 -0
- package/src/transpiler/logic/analysis/types/IReturnPathError.ts +12 -0
- package/src/transpiler/logic/detectAssemblySyntax.ts +80 -0
- package/src/transpiler/logic/parser/grammar/CNext.interp +4 -1
- package/src/transpiler/logic/parser/grammar/CNext.tokens +169 -167
- package/src/transpiler/logic/parser/grammar/CNextLexer.interp +4 -1
- package/src/transpiler/logic/parser/grammar/CNextLexer.tokens +169 -167
- package/src/transpiler/logic/parser/grammar/CNextLexer.ts +545 -541
- package/src/transpiler/logic/parser/grammar/CNextListener.ts +11 -0
- package/src/transpiler/logic/parser/grammar/CNextParser.ts +1318 -1178
- package/src/transpiler/logic/parser/grammar/CNextVisitor.ts +7 -0
- package/src/transpiler/logic/preprocessor/CompileCommandsReader.ts +332 -0
- package/src/transpiler/logic/preprocessor/ExternalDeclarationOracle.ts +202 -0
- package/src/transpiler/logic/preprocessor/Preprocessor.ts +24 -6
- package/src/transpiler/logic/preprocessor/ToolchainDetector.ts +42 -1
- package/src/transpiler/logic/preprocessor/__tests__/CompileCommandsReader.test.ts +216 -0
- package/src/transpiler/logic/preprocessor/__tests__/ExternalDeclarationOracle.test.ts +153 -0
- package/src/transpiler/logic/preprocessor/__tests__/Preprocessor.test.ts +43 -18
- package/src/transpiler/logic/preprocessor/__tests__/ToolchainDetector.crossCompiler.test.ts +75 -0
- package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/dependent.h +7 -0
- package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/predecessor.h +5 -0
- package/src/transpiler/logic/preprocessor/types/ICompileCommandsResult.ts +14 -0
- package/src/transpiler/logic/preprocessor/types/IPreprocessOptions.ts +17 -0
- package/src/transpiler/logic/symbols/SymbolTable.ts +62 -2
- package/src/transpiler/logic/symbols/__tests__/SymbolTable.test.ts +55 -4
- package/src/transpiler/logic/symbols/cnext/collectors/VariableCollector.ts +15 -2
- package/src/transpiler/logic/symbols/cnext/utils/TypeUtils.ts +64 -50
- package/src/transpiler/output/MisraSuppressionUtils.ts +52 -0
- package/src/transpiler/output/__tests__/MisraSuppressionUtils.test.ts +67 -0
- package/src/transpiler/output/codegen/CodeGenerator.ts +196 -98
- package/src/transpiler/output/codegen/TypeResolver.ts +148 -0
- package/src/transpiler/output/codegen/TypeValidator.ts +179 -81
- package/src/transpiler/output/codegen/__tests__/CodeGenerator.coverage.test.ts +165 -17
- package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +163 -35
- package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +2 -2
- package/src/transpiler/output/codegen/__tests__/TypeResolver.test.ts +93 -0
- package/src/transpiler/output/codegen/__tests__/TypeValidator.alwaysTrueLoop.test.ts +91 -0
- package/src/transpiler/output/codegen/__tests__/TypeValidator.test.ts +97 -14
- package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +13 -0
- package/src/transpiler/output/codegen/assignment/AssignmentKind.ts +1 -1
- package/src/transpiler/output/codegen/assignment/handlers/AccessPatternHandlers.ts +20 -12
- package/src/transpiler/output/codegen/assignment/handlers/ArrayHandlers.ts +424 -22
- package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +31 -18
- package/src/transpiler/output/codegen/assignment/handlers/BitmapHandlers.ts +7 -8
- package/src/transpiler/output/codegen/assignment/handlers/RegisterHandlers.ts +6 -8
- package/src/transpiler/output/codegen/assignment/handlers/RegisterUtils.ts +12 -10
- package/src/transpiler/output/codegen/assignment/handlers/SimpleHandler.ts +3 -7
- package/src/transpiler/output/codegen/assignment/handlers/SpecialHandlers.ts +7 -9
- package/src/transpiler/output/codegen/assignment/handlers/StringHandlers.ts +12 -10
- package/src/transpiler/output/codegen/assignment/handlers/__tests__/ArrayHandlers.test.ts +479 -11
- package/src/transpiler/output/codegen/generators/IOrchestrator.ts +3 -0
- package/src/transpiler/output/codegen/generators/declarationGenerators/ScopeGenerator.ts +26 -7
- package/src/transpiler/output/codegen/generators/declarationGenerators/__tests__/ScopeGenerator.test.ts +86 -0
- package/src/transpiler/output/codegen/generators/expressions/BinaryExprGenerator.ts +43 -24
- package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +76 -58
- package/src/transpiler/output/codegen/generators/expressions/ExpressionGenerator.ts +9 -2
- package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +26 -13
- package/src/transpiler/output/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts +44 -0
- package/src/transpiler/output/codegen/generators/expressions/__tests__/ExpressionGenerator.test.ts +82 -1
- package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +129 -1
- package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +64 -8
- package/src/transpiler/output/codegen/generators/statements/__tests__/ControlFlowGenerator.test.ts +8 -5
- package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +30 -2
- package/src/transpiler/output/codegen/helpers/ParameterInputAdapter.ts +17 -3
- package/src/transpiler/output/codegen/helpers/ParameterSignatureBuilder.ts +17 -4
- package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +240 -42
- package/src/transpiler/output/codegen/helpers/SymbolLookupHelper.ts +0 -21
- package/src/transpiler/output/codegen/helpers/TypeGenerationHelper.ts +60 -39
- package/src/transpiler/output/codegen/helpers/TypeRegistrationEngine.ts +170 -36
- package/src/transpiler/output/codegen/helpers/VariableDeclHelper.ts +37 -39
- package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +19 -0
- package/src/transpiler/output/codegen/helpers/__tests__/ParameterInputAdapter.test.ts +117 -0
- package/src/transpiler/output/codegen/helpers/__tests__/ParameterSignatureBuilder.test.ts +94 -2
- package/src/transpiler/output/codegen/helpers/__tests__/StringDeclHelper.test.ts +322 -9
- package/src/transpiler/output/codegen/helpers/__tests__/SymbolLookupHelper.test.ts +0 -64
- package/src/transpiler/output/codegen/helpers/__tests__/TypeRegistrationEngine.test.ts +101 -0
- package/src/transpiler/output/codegen/helpers/__tests__/VariableDeclHelper.test.ts +29 -5
- package/src/transpiler/output/codegen/subscript/SubscriptClassifier.ts +30 -11
- package/src/transpiler/output/codegen/subscript/TSubscriptKind.ts +1 -1
- package/src/transpiler/output/codegen/subscript/__tests__/SubscriptClassifier.test.ts +38 -6
- package/src/transpiler/output/codegen/types/ICallbackTypeInfo.ts +2 -1
- package/src/transpiler/output/codegen/types/IParameterInput.ts +7 -0
- package/src/transpiler/output/headers/BaseHeaderGenerator.ts +8 -0
- package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +140 -24
- package/src/transpiler/output/headers/__tests__/HeaderGeneratorUtils.test.ts +280 -0
- package/src/transpiler/output/headers/generators/IHeaderTypeInput.ts +13 -0
- package/src/transpiler/output/headers/generators/generateStructHeader.ts +48 -28
- package/src/transpiler/state/CodeGenState.ts +91 -22
- package/src/transpiler/state/__tests__/CodeGenState.test.ts +253 -11
- package/src/transpiler/types/ICachedFileEntry.ts +7 -0
- package/src/utils/LiteralUtils.ts +23 -0
- package/src/utils/__tests__/LiteralUtils.test.ts +101 -0
- package/src/utils/cache/CacheManager.ts +13 -2
- package/src/utils/cache/__tests__/CacheManager.test.ts +18 -1
- package/src/utils/constants/TypeConstants.ts +13 -0
- 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 -
|
|
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
|
-
|
|
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(
|
|
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
|
|
174
|
-
|
|
175
|
-
expect(
|
|
176
|
-
expect(
|
|
177
|
-
expect(
|
|
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
|
|
192
|
-
expect(
|
|
193
|
-
expect(
|
|
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
|
|
212
|
-
expect(
|
|
213
|
-
expect(
|
|
214
|
-
expect(
|
|
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
|
|
294
|
-
expect(
|
|
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,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;
|