c-next 0.2.11 → 0.2.13
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 +80 -649
- package/dist/index.js +8273 -6357
- package/dist/index.js.map +4 -4
- package/grammar/C.g4 +17 -4
- package/package.json +3 -3
- package/src/__tests__/index.test.ts +1 -1
- package/src/cli/CleanCommand.ts +8 -12
- package/src/cli/Cli.ts +29 -6
- package/src/cli/Runner.ts +42 -62
- package/src/cli/__tests__/CleanCommand.test.ts +10 -10
- package/src/cli/__tests__/Cli.test.ts +59 -7
- package/src/cli/__tests__/ConfigPrinter.test.ts +12 -12
- package/src/cli/__tests__/PathNormalizer.test.ts +5 -5
- package/src/cli/__tests__/Runner.test.ts +108 -82
- package/src/cli/serve/ServeCommand.ts +1 -1
- package/src/cli/types/ICliConfig.ts +2 -2
- package/src/lib/parseWithSymbols.ts +21 -21
- package/src/transpiler/Transpiler.ts +99 -46
- package/src/transpiler/__tests__/DualCodePaths.test.ts +29 -29
- package/src/transpiler/__tests__/Transpiler.coverage.test.ts +244 -72
- package/src/transpiler/__tests__/Transpiler.test.ts +32 -72
- package/src/transpiler/__tests__/determineProjectRoot.test.ts +30 -28
- package/src/transpiler/__tests__/needsConditionalPreprocessing.test.ts +1 -1
- package/src/transpiler/data/CNextMarkerDetector.ts +34 -0
- package/src/transpiler/data/CppEntryPointScanner.ts +174 -0
- package/src/transpiler/data/FileDiscovery.ts +2 -105
- package/src/transpiler/data/InputExpansion.ts +37 -81
- package/src/transpiler/data/__tests__/CNextMarkerDetector.test.ts +62 -0
- package/src/transpiler/data/__tests__/CppEntryPointScanner.test.ts +239 -0
- package/src/transpiler/data/__tests__/FileDiscovery.test.ts +45 -191
- package/src/transpiler/data/__tests__/InputExpansion.test.ts +36 -204
- package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +2 -2
- package/src/transpiler/logic/analysis/PassByValueAnalyzer.ts +4 -5
- package/src/transpiler/logic/parser/c/grammar/C.interp +33 -3
- package/src/transpiler/logic/parser/c/grammar/C.tokens +237 -207
- package/src/transpiler/logic/parser/c/grammar/CLexer.interp +48 -3
- package/src/transpiler/logic/parser/c/grammar/CLexer.tokens +237 -207
- package/src/transpiler/logic/parser/c/grammar/CLexer.ts +702 -611
- package/src/transpiler/logic/parser/c/grammar/CParser.ts +1221 -1107
- package/src/transpiler/logic/symbols/SymbolTable.ts +147 -73
- package/src/transpiler/logic/symbols/__tests__/SymbolTable.test.ts +157 -14
- package/src/transpiler/logic/symbols/c/__tests__/CResolver.integration.test.ts +3 -3
- package/src/transpiler/logic/symbols/c/collectors/StructCollector.ts +7 -37
- package/src/transpiler/logic/symbols/cnext/__tests__/TSymbolInfoAdapter.test.ts +6 -6
- package/src/transpiler/logic/symbols/cnext/adapters/TSymbolInfoAdapter.ts +28 -27
- package/src/transpiler/logic/symbols/cnext/index.ts +4 -4
- package/src/transpiler/logic/symbols/cnext/utils/SymbolNameUtils.ts +5 -5
- package/src/transpiler/output/codegen/CodeGenerator.ts +16 -1
- package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +15 -0
- package/src/transpiler/output/codegen/__tests__/ExpressionWalker.test.ts +3 -3
- package/src/transpiler/output/codegen/__tests__/RequireInclude.test.ts +14 -14
- package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +2 -2
- package/src/transpiler/output/codegen/helpers/FunctionContextManager.ts +15 -3
- package/src/transpiler/output/codegen/helpers/ParameterInputAdapter.ts +14 -6
- package/src/transpiler/output/codegen/helpers/__tests__/ParameterInputAdapter.test.ts +1 -0
- package/src/transpiler/output/codegen/types/IFunctionContextCallbacks.ts +2 -0
- package/src/transpiler/output/codegen/utils/QualifiedNameGenerator.ts +7 -7
- package/src/transpiler/output/codegen/utils/__tests__/QualifiedNameGenerator.test.ts +3 -3
- package/src/transpiler/output/headers/BaseHeaderGenerator.ts +10 -1
- package/src/transpiler/output/headers/HeaderGenerator.ts +3 -0
- package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +6 -2
- package/src/transpiler/output/headers/__tests__/HeaderGeneratorUtils.test.ts +16 -0
- package/src/transpiler/output/headers/adapters/HeaderSymbolAdapter.ts +19 -19
- package/src/transpiler/output/headers/adapters/__tests__/HeaderSymbolAdapter.test.ts +5 -5
- package/src/transpiler/state/SymbolRegistry.ts +10 -12
- package/src/transpiler/state/__tests__/SymbolRegistry.test.ts +11 -13
- package/src/transpiler/types/ICachedFileEntry.ts +4 -0
- package/src/transpiler/types/IPipelineFile.ts +3 -0
- package/src/transpiler/types/ITranspilerConfig.ts +2 -2
- package/src/transpiler/types/symbols/IScopeSymbol.ts +1 -1
- package/src/utils/FunctionUtils.ts +3 -3
- package/src/utils/__tests__/FunctionUtils.test.ts +6 -4
- package/src/utils/cache/CacheManager.ts +28 -15
- package/src/utils/cache/__tests__/CacheManager.test.ts +6 -4
- package/src/transpiler/data/types/IDiscoveryOptions.ts +0 -15
package/grammar/C.g4
CHANGED
|
@@ -74,7 +74,7 @@ unaryExpression
|
|
|
74
74
|
: ('++' | '--' | 'sizeof')* (
|
|
75
75
|
postfixExpression
|
|
76
76
|
| unaryOperator castExpression
|
|
77
|
-
| ('sizeof' | '_Alignof') '(' typeName ')'
|
|
77
|
+
| ('sizeof' | '_Alignof' | '__alignof__' | '__alignof') '(' typeName ')'
|
|
78
78
|
| '&&' Identifier // GCC extension address of label
|
|
79
79
|
)
|
|
80
80
|
;
|
|
@@ -200,6 +200,7 @@ storageClassSpecifier
|
|
|
200
200
|
| 'extern'
|
|
201
201
|
| 'static'
|
|
202
202
|
| '_Thread_local'
|
|
203
|
+
| '__thread' // GCC extension
|
|
203
204
|
| 'auto'
|
|
204
205
|
| 'register'
|
|
205
206
|
;
|
|
@@ -213,18 +214,23 @@ typeSpecifier
|
|
|
213
214
|
| 'float'
|
|
214
215
|
| 'double'
|
|
215
216
|
| 'signed'
|
|
217
|
+
| '__signed' // GCC extension
|
|
218
|
+
| '__signed__' // GCC extension
|
|
216
219
|
| 'unsigned'
|
|
217
220
|
| '_Bool'
|
|
218
221
|
| '_Complex'
|
|
219
222
|
| '__m128'
|
|
220
223
|
| '__m128d'
|
|
221
224
|
| '__m128i'
|
|
225
|
+
| '__int128' // GCC 128-bit integer
|
|
226
|
+
| '__int128_t' // GCC 128-bit signed
|
|
227
|
+
| '__uint128_t' // GCC 128-bit unsigned
|
|
222
228
|
| '__extension__' '(' ('__m128' | '__m128d' | '__m128i') ')'
|
|
223
229
|
| atomicTypeSpecifier
|
|
224
230
|
| structOrUnionSpecifier
|
|
225
231
|
| enumSpecifier
|
|
226
232
|
| typedefName
|
|
227
|
-
| '__typeof__' '(' constantExpression ')' // GCC extension
|
|
233
|
+
| ('__typeof__' | '__typeof') '(' constantExpression ')' // GCC extension
|
|
228
234
|
;
|
|
229
235
|
|
|
230
236
|
structOrUnionSpecifier
|
|
@@ -283,14 +289,21 @@ atomicTypeSpecifier
|
|
|
283
289
|
|
|
284
290
|
typeQualifier
|
|
285
291
|
: 'const'
|
|
292
|
+
| '__const' // GCC extension
|
|
293
|
+
| '__const__' // GCC extension
|
|
286
294
|
| 'restrict'
|
|
295
|
+
| '__restrict' // GCC extension
|
|
296
|
+
| '__restrict__' // GCC extension
|
|
287
297
|
| 'volatile'
|
|
298
|
+
| '__volatile' // GCC extension
|
|
299
|
+
| '__volatile__' // GCC extension
|
|
288
300
|
| '_Atomic'
|
|
289
301
|
;
|
|
290
302
|
|
|
291
303
|
functionSpecifier
|
|
292
304
|
: 'inline'
|
|
293
305
|
| '_Noreturn'
|
|
306
|
+
| '__inline' // GCC extension
|
|
294
307
|
| '__inline__' // GCC extension
|
|
295
308
|
| '__stdcall'
|
|
296
309
|
| gccAttributeSpecifier
|
|
@@ -329,7 +342,7 @@ vcSpecificModifer
|
|
|
329
342
|
;
|
|
330
343
|
|
|
331
344
|
gccDeclaratorExtension
|
|
332
|
-
: '__asm' '(' StringLiteral+ ')'
|
|
345
|
+
: ('__asm' | '__asm__') '(' StringLiteral+ ')' // GCC asm in declarators
|
|
333
346
|
| gccAttributeSpecifier
|
|
334
347
|
;
|
|
335
348
|
|
|
@@ -431,7 +444,7 @@ statement
|
|
|
431
444
|
| selectionStatement
|
|
432
445
|
| iterationStatement
|
|
433
446
|
| jumpStatement
|
|
434
|
-
| ('__asm' | '__asm__') ('volatile' | '__volatile__') '(' (
|
|
447
|
+
| ('__asm' | '__asm__') ('volatile' | '__volatile' | '__volatile__') '(' (
|
|
435
448
|
logicalOrExpression (',' logicalOrExpression)*
|
|
436
449
|
)? (':' (logicalOrExpression (',' logicalOrExpression)*)?)* ')' ';'
|
|
437
450
|
;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c-next",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"description": "A safer C for embedded systems development. Transpiles to clean, readable C.",
|
|
5
5
|
"packageManager": "npm@11.9.0",
|
|
6
6
|
"type": "module",
|
|
@@ -92,12 +92,12 @@
|
|
|
92
92
|
"devDependencies": {
|
|
93
93
|
"@types/node": "^25.2.1",
|
|
94
94
|
"@types/yargs": "^17.0.35",
|
|
95
|
-
"esbuild": "^0.27.3",
|
|
96
95
|
"@vitest/coverage-v8": "^4.0.18",
|
|
97
96
|
"antlr4ng-cli": "^2.0.0",
|
|
98
97
|
"chalk": "^5.6.2",
|
|
99
98
|
"cspell": "^9.6.4",
|
|
100
99
|
"dependency-cruiser": "^17.3.7",
|
|
100
|
+
"esbuild": "^0.27.3",
|
|
101
101
|
"husky": "^9.1.7",
|
|
102
102
|
"knip": "^5.83.1",
|
|
103
103
|
"lint-staged": "^16.2.7",
|
|
@@ -111,8 +111,8 @@
|
|
|
111
111
|
"@n1ru4l/toposort": "^0.0.1",
|
|
112
112
|
"antlr4ng": "^3.0.16",
|
|
113
113
|
"cosmiconfig": "^9.0.0",
|
|
114
|
-
"fast-glob": "^3.3.3",
|
|
115
114
|
"flat-cache": "^6.1.20",
|
|
115
|
+
"immer": "^11.1.4",
|
|
116
116
|
"tsx": "^4.21.0",
|
|
117
117
|
"typescript": "^5.9.3",
|
|
118
118
|
"yargs": "^18.0.0"
|
package/src/cli/CleanCommand.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Deletes generated files (.c, .cpp, .h, .hpp) that have matching .cnx sources
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { basename, join, resolve } from "node:path";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
7
|
import { unlinkSync } from "node:fs";
|
|
8
8
|
import InputExpansion from "../transpiler/data/InputExpansion";
|
|
9
9
|
import PathResolver from "../transpiler/data/PathResolver";
|
|
@@ -13,12 +13,12 @@ import PathResolver from "../transpiler/data/PathResolver";
|
|
|
13
13
|
*/
|
|
14
14
|
class CleanCommand {
|
|
15
15
|
/**
|
|
16
|
-
* Discover CNX files from
|
|
16
|
+
* Discover CNX files from entry point.
|
|
17
17
|
* Returns null if none found or on error.
|
|
18
18
|
*/
|
|
19
|
-
private static discoverCnxFiles(
|
|
19
|
+
private static discoverCnxFiles(input: string): string[] | null {
|
|
20
20
|
try {
|
|
21
|
-
const cnxFiles = InputExpansion.expandInputs(
|
|
21
|
+
const cnxFiles = InputExpansion.expandInputs([input]);
|
|
22
22
|
if (cnxFiles.length === 0) {
|
|
23
23
|
console.log("No .cnx files found. Nothing to clean.");
|
|
24
24
|
return null;
|
|
@@ -54,21 +54,17 @@ class CleanCommand {
|
|
|
54
54
|
/**
|
|
55
55
|
* Execute the clean command
|
|
56
56
|
*
|
|
57
|
-
* @param
|
|
57
|
+
* @param input - Entry point .cnx file
|
|
58
58
|
* @param outDir - Output directory for code files
|
|
59
59
|
* @param headerOutDir - Optional separate output directory for headers
|
|
60
60
|
*/
|
|
61
|
-
static execute(
|
|
62
|
-
inputs: string[],
|
|
63
|
-
outDir: string,
|
|
64
|
-
headerOutDir?: string,
|
|
65
|
-
): void {
|
|
61
|
+
static execute(input: string, outDir: string, headerOutDir?: string): void {
|
|
66
62
|
if (!outDir) {
|
|
67
63
|
console.log("No output directory specified. Nothing to clean.");
|
|
68
64
|
return;
|
|
69
65
|
}
|
|
70
66
|
|
|
71
|
-
const cnxFiles = this.discoverCnxFiles(
|
|
67
|
+
const cnxFiles = this.discoverCnxFiles(input);
|
|
72
68
|
if (!cnxFiles) return;
|
|
73
69
|
|
|
74
70
|
const resolvedOutDir = resolve(outDir);
|
|
@@ -77,7 +73,7 @@ class CleanCommand {
|
|
|
77
73
|
: resolvedOutDir;
|
|
78
74
|
|
|
79
75
|
const pathResolver = new PathResolver({
|
|
80
|
-
inputs,
|
|
76
|
+
inputs: [dirname(resolve(input))],
|
|
81
77
|
outDir,
|
|
82
78
|
headerOutDir,
|
|
83
79
|
});
|
package/src/cli/Cli.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { dirname, resolve } from "node:path";
|
|
8
|
+
import { existsSync, statSync } from "node:fs";
|
|
8
9
|
import ArgParser from "./ArgParser";
|
|
9
10
|
import ConfigLoader from "./ConfigLoader";
|
|
10
11
|
import ConfigPrinter from "./ConfigPrinter";
|
|
@@ -65,17 +66,39 @@ class Cli {
|
|
|
65
66
|
return { shouldRun: false, exitCode: 0 };
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
// Validate
|
|
69
|
-
if (
|
|
70
|
-
console.error("Error:
|
|
71
|
-
console.error(
|
|
69
|
+
// Validate single entry point
|
|
70
|
+
if (args.inputFiles.length > 1) {
|
|
71
|
+
console.error("Error: Only one entry point file is supported");
|
|
72
|
+
console.error(
|
|
73
|
+
"Other files are discovered automatically via #include directives",
|
|
74
|
+
);
|
|
75
|
+
return { shouldRun: false, exitCode: 1 };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!config.input) {
|
|
79
|
+
console.error("Error: No input file specified");
|
|
80
|
+
console.error("Usage: cnext <file.cnx>");
|
|
81
|
+
return { shouldRun: false, exitCode: 1 };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resolvedInput = resolve(config.input);
|
|
85
|
+
if (!existsSync(resolvedInput)) {
|
|
86
|
+
console.error(`Error: Input not found: ${config.input}`);
|
|
87
|
+
return { shouldRun: false, exitCode: 1 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (statSync(resolvedInput).isDirectory()) {
|
|
91
|
+
console.error(
|
|
92
|
+
"Error: Directory input not supported. Specify an entry point file.",
|
|
93
|
+
);
|
|
94
|
+
console.error(`Example: cnext ${config.input}/main.cnx`);
|
|
72
95
|
return { shouldRun: false, exitCode: 1 };
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
// Handle --clean: delete generated files
|
|
76
99
|
if (args.cleanMode) {
|
|
77
100
|
CleanCommand.execute(
|
|
78
|
-
config.
|
|
101
|
+
config.input,
|
|
79
102
|
config.outputPath,
|
|
80
103
|
config.headerOutDir,
|
|
81
104
|
);
|
|
@@ -94,7 +117,7 @@ class Cli {
|
|
|
94
117
|
fileConfig: IFileConfig,
|
|
95
118
|
): ICliConfig {
|
|
96
119
|
const rawConfig: ICliConfig = {
|
|
97
|
-
|
|
120
|
+
input: args.inputFiles[0] ?? "",
|
|
98
121
|
outputPath: args.outputPath || fileConfig.output || "",
|
|
99
122
|
// Merge include dirs: config includes come first, CLI includes override/append
|
|
100
123
|
includeDirs: [...(fileConfig.include ?? []), ...args.includeDirs],
|
package/src/cli/Runner.ts
CHANGED
|
@@ -3,19 +3,13 @@
|
|
|
3
3
|
* Executes the transpiler with the given configuration
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { basename, dirname, resolve } from "node:path";
|
|
7
7
|
import { existsSync, statSync, renameSync } from "node:fs";
|
|
8
|
-
import InputExpansion from "../transpiler/data/InputExpansion";
|
|
9
8
|
import Transpiler from "../transpiler/Transpiler";
|
|
10
9
|
import ICliConfig from "./types/ICliConfig";
|
|
11
10
|
import ResultPrinter from "./ResultPrinter";
|
|
12
11
|
import ITranspilerResult from "../transpiler/types/ITranspilerResult";
|
|
13
|
-
|
|
14
|
-
/** Result of categorizing inputs into directories and files */
|
|
15
|
-
interface ICategorizedInputs {
|
|
16
|
-
srcDirs: string[];
|
|
17
|
-
explicitFiles: string[];
|
|
18
|
-
}
|
|
12
|
+
import InputExpansion from "../transpiler/data/InputExpansion";
|
|
19
13
|
|
|
20
14
|
/** Result of determining output path */
|
|
21
15
|
interface IOutputPathResult {
|
|
@@ -32,19 +26,21 @@ class Runner {
|
|
|
32
26
|
* @param config - CLI configuration
|
|
33
27
|
*/
|
|
34
28
|
static async execute(config: ICliConfig): Promise<void> {
|
|
35
|
-
const
|
|
36
|
-
const files = this._expandInputFiles(config.inputs);
|
|
29
|
+
const resolvedInput = resolve(config.input);
|
|
37
30
|
const { outDir, explicitOutputFile } = this._determineOutputPath(
|
|
38
31
|
config,
|
|
39
|
-
|
|
32
|
+
resolvedInput,
|
|
40
33
|
);
|
|
41
34
|
|
|
35
|
+
// Infer basePath from entry file's parent directory if not set
|
|
36
|
+
const basePath = config.basePath || dirname(resolvedInput);
|
|
37
|
+
|
|
42
38
|
const pipeline = new Transpiler({
|
|
43
|
-
|
|
39
|
+
input: resolvedInput,
|
|
44
40
|
includeDirs: config.includeDirs,
|
|
45
41
|
outDir,
|
|
46
42
|
headerOutDir: config.headerOutDir,
|
|
47
|
-
basePath
|
|
43
|
+
basePath,
|
|
48
44
|
preprocess: config.preprocess,
|
|
49
45
|
defines: config.defines,
|
|
50
46
|
cppRequired: config.cppRequired,
|
|
@@ -54,52 +50,20 @@ class Runner {
|
|
|
54
50
|
debugMode: config.debugMode,
|
|
55
51
|
});
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
ResultPrinter.print(result);
|
|
61
|
-
process.exit(result.success ? 0 : 1);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Categorize inputs into directories and explicit files.
|
|
66
|
-
*/
|
|
67
|
-
private static _categorizeInputs(inputs: string[]): ICategorizedInputs {
|
|
68
|
-
const srcDirs: string[] = [];
|
|
69
|
-
const explicitFiles: string[] = [];
|
|
70
|
-
|
|
71
|
-
for (const input of inputs) {
|
|
72
|
-
const resolvedPath = resolve(input);
|
|
73
|
-
const isDir =
|
|
74
|
-
existsSync(resolvedPath) && statSync(resolvedPath).isDirectory();
|
|
75
|
-
if (isDir) {
|
|
76
|
-
srcDirs.push(resolvedPath);
|
|
77
|
-
} else {
|
|
78
|
-
explicitFiles.push(resolvedPath);
|
|
79
|
-
}
|
|
53
|
+
if (InputExpansion.isCppEntryPoint(resolvedInput)) {
|
|
54
|
+
console.log(`Scanning ${basename(resolvedInput)} for C-Next includes...`);
|
|
80
55
|
}
|
|
81
56
|
|
|
82
|
-
|
|
83
|
-
}
|
|
57
|
+
const result = await pipeline.transpile({ kind: "files" });
|
|
84
58
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*/
|
|
88
|
-
private static _expandInputFiles(inputs: string[]): string[] {
|
|
89
|
-
let files: string[];
|
|
90
|
-
try {
|
|
91
|
-
files = InputExpansion.expandInputs(inputs);
|
|
92
|
-
} catch (error) {
|
|
93
|
-
console.error(`Error: ${error}`);
|
|
94
|
-
process.exit(1);
|
|
59
|
+
if (InputExpansion.isCppEntryPoint(resolvedInput)) {
|
|
60
|
+
this._printCppEntryPointResult(result, resolvedInput);
|
|
95
61
|
}
|
|
96
62
|
|
|
97
|
-
|
|
98
|
-
console.error("Error: No .cnx files found");
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
63
|
+
this._renameOutputIfNeeded(result, explicitOutputFile);
|
|
101
64
|
|
|
102
|
-
|
|
65
|
+
ResultPrinter.print(result);
|
|
66
|
+
process.exit(result.success ? 0 : 1);
|
|
103
67
|
}
|
|
104
68
|
|
|
105
69
|
/**
|
|
@@ -107,10 +71,10 @@ class Runner {
|
|
|
107
71
|
*/
|
|
108
72
|
private static _determineOutputPath(
|
|
109
73
|
config: ICliConfig,
|
|
110
|
-
|
|
74
|
+
resolvedInput: string,
|
|
111
75
|
): IOutputPathResult {
|
|
112
76
|
if (!config.outputPath) {
|
|
113
|
-
return { outDir: dirname(
|
|
77
|
+
return { outDir: dirname(resolvedInput), explicitOutputFile: null };
|
|
114
78
|
}
|
|
115
79
|
|
|
116
80
|
const isExplicitFile =
|
|
@@ -127,13 +91,6 @@ class Runner {
|
|
|
127
91
|
|
|
128
92
|
// Explicit output file
|
|
129
93
|
if (isExplicitFile) {
|
|
130
|
-
if (files.length > 1) {
|
|
131
|
-
console.error(
|
|
132
|
-
"Error: Cannot use explicit output filename with multiple input files",
|
|
133
|
-
);
|
|
134
|
-
console.error("Use a directory path instead: -o <directory>/");
|
|
135
|
-
process.exit(1);
|
|
136
|
-
}
|
|
137
94
|
return {
|
|
138
95
|
outDir: dirname(config.outputPath),
|
|
139
96
|
explicitOutputFile: resolve(config.outputPath),
|
|
@@ -165,6 +122,29 @@ class Runner {
|
|
|
165
122
|
result.outputFiles[0] = explicitOutputFile;
|
|
166
123
|
}
|
|
167
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Print result message for C/C++ entry point scanning.
|
|
128
|
+
*/
|
|
129
|
+
private static _printCppEntryPointResult(
|
|
130
|
+
result: ITranspilerResult,
|
|
131
|
+
resolvedInput: string,
|
|
132
|
+
): void {
|
|
133
|
+
if (result.filesProcessed === 0) {
|
|
134
|
+
console.log("No C-Next files found in include tree. To get started:");
|
|
135
|
+
console.log(" 1. Create a .cnx file (e.g., led.cnx)");
|
|
136
|
+
console.log(" 2. Run: cnext led.cnx");
|
|
137
|
+
console.log(" 3. Include the generated header in your C/C++ code");
|
|
138
|
+
console.log(` 4. Re-run: cnext ${basename(resolvedInput)}`);
|
|
139
|
+
} else {
|
|
140
|
+
const fileNames = result.files
|
|
141
|
+
.map((f) => basename(f.sourcePath))
|
|
142
|
+
.join(", ");
|
|
143
|
+
console.log(
|
|
144
|
+
`Found ${result.filesProcessed} C-Next source file(s): ${fileNames}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
168
148
|
}
|
|
169
149
|
|
|
170
150
|
export default Runner;
|
|
@@ -35,7 +35,7 @@ describe("CleanCommand", () => {
|
|
|
35
35
|
|
|
36
36
|
describe("execute", () => {
|
|
37
37
|
it("reports no output directory when outDir is empty", () => {
|
|
38
|
-
CleanCommand.execute(
|
|
38
|
+
CleanCommand.execute("src/", "", undefined);
|
|
39
39
|
|
|
40
40
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
41
41
|
"No output directory specified. Nothing to clean.",
|
|
@@ -45,7 +45,7 @@ describe("CleanCommand", () => {
|
|
|
45
45
|
it("reports no files found when no .cnx files exist", () => {
|
|
46
46
|
vi.mocked(InputExpansion.expandInputs).mockReturnValue([]);
|
|
47
47
|
|
|
48
|
-
CleanCommand.execute(
|
|
48
|
+
CleanCommand.execute("src/", "build/", undefined);
|
|
49
49
|
|
|
50
50
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
51
51
|
"No .cnx files found. Nothing to clean.",
|
|
@@ -57,7 +57,7 @@ describe("CleanCommand", () => {
|
|
|
57
57
|
throw new Error("Path not found");
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
CleanCommand.execute(
|
|
60
|
+
CleanCommand.execute("nonexistent/", "build/", undefined);
|
|
61
61
|
|
|
62
62
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
63
63
|
"Error: Error: Path not found",
|
|
@@ -72,7 +72,7 @@ describe("CleanCommand", () => {
|
|
|
72
72
|
// Mock unlinkSync to succeed for all calls
|
|
73
73
|
vi.mocked(fs.unlinkSync).mockImplementation(() => {});
|
|
74
74
|
|
|
75
|
-
CleanCommand.execute(
|
|
75
|
+
CleanCommand.execute("/project/src/", "/project/build/", undefined);
|
|
76
76
|
|
|
77
77
|
// Should try to delete .c, .cpp, .h, .hpp files
|
|
78
78
|
expect(fs.unlinkSync).toHaveBeenCalledTimes(4);
|
|
@@ -89,7 +89,7 @@ describe("CleanCommand", () => {
|
|
|
89
89
|
vi.mocked(fs.unlinkSync).mockImplementation(() => {});
|
|
90
90
|
|
|
91
91
|
CleanCommand.execute(
|
|
92
|
-
|
|
92
|
+
"/project/src/",
|
|
93
93
|
"/project/build/",
|
|
94
94
|
"/project/include/",
|
|
95
95
|
);
|
|
@@ -127,7 +127,7 @@ describe("CleanCommand", () => {
|
|
|
127
127
|
throw enoentError;
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
CleanCommand.execute(
|
|
130
|
+
CleanCommand.execute("/project/src/", "/project/build/", undefined);
|
|
131
131
|
|
|
132
132
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
133
133
|
"No generated files found to delete.",
|
|
@@ -146,7 +146,7 @@ describe("CleanCommand", () => {
|
|
|
146
146
|
throw permError;
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
CleanCommand.execute(
|
|
149
|
+
CleanCommand.execute("/project/src/", "/project/build/", undefined);
|
|
150
150
|
|
|
151
151
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
152
152
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
@@ -168,7 +168,7 @@ describe("CleanCommand", () => {
|
|
|
168
168
|
vi.mocked(fs.unlinkSync).mockImplementation(() => {});
|
|
169
169
|
|
|
170
170
|
CleanCommand.execute(
|
|
171
|
-
|
|
171
|
+
"/project/src/main.cnx",
|
|
172
172
|
"/project/build/",
|
|
173
173
|
undefined,
|
|
174
174
|
);
|
|
@@ -187,7 +187,7 @@ describe("CleanCommand", () => {
|
|
|
187
187
|
|
|
188
188
|
vi.mocked(fs.unlinkSync).mockImplementation(() => {});
|
|
189
189
|
|
|
190
|
-
CleanCommand.execute(
|
|
190
|
+
CleanCommand.execute("/project/src/", "/project/build/", undefined);
|
|
191
191
|
|
|
192
192
|
const unlinkCalls = vi.mocked(fs.unlinkSync).mock.calls;
|
|
193
193
|
const paths = unlinkCalls.map((call) => call[0] as string);
|
|
@@ -204,7 +204,7 @@ describe("CleanCommand", () => {
|
|
|
204
204
|
|
|
205
205
|
vi.mocked(fs.unlinkSync).mockImplementation(() => {});
|
|
206
206
|
|
|
207
|
-
CleanCommand.execute(
|
|
207
|
+
CleanCommand.execute("/project/src/", "/project/build/", undefined);
|
|
208
208
|
|
|
209
209
|
const unlinkCalls = vi.mocked(fs.unlinkSync).mock.calls;
|
|
210
210
|
const paths = unlinkCalls.map((call) => call[0] as string);
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
13
|
+
import { existsSync, statSync } from "node:fs";
|
|
13
14
|
import Cli from "../Cli";
|
|
14
15
|
import ArgParser from "../ArgParser";
|
|
15
16
|
import ConfigLoader from "../ConfigLoader";
|
|
@@ -31,6 +32,14 @@ vi.mock("../PathNormalizer", () => ({
|
|
|
31
32
|
normalizeConfig: (config: unknown) => config,
|
|
32
33
|
},
|
|
33
34
|
}));
|
|
35
|
+
vi.mock("node:fs", async (importOriginal) => {
|
|
36
|
+
const actual = await importOriginal<typeof import("node:fs")>();
|
|
37
|
+
return {
|
|
38
|
+
...actual,
|
|
39
|
+
existsSync: vi.fn(() => true),
|
|
40
|
+
statSync: vi.fn(() => ({ isDirectory: () => false })),
|
|
41
|
+
};
|
|
42
|
+
});
|
|
34
43
|
|
|
35
44
|
describe("Cli", () => {
|
|
36
45
|
let mockParsedArgs: IParsedArgs;
|
|
@@ -62,6 +71,10 @@ describe("Cli", () => {
|
|
|
62
71
|
// Default mocks
|
|
63
72
|
vi.mocked(ArgParser.parse).mockReturnValue(mockParsedArgs);
|
|
64
73
|
vi.mocked(ConfigLoader.load).mockReturnValue({});
|
|
74
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
75
|
+
vi.mocked(statSync).mockReturnValue({
|
|
76
|
+
isDirectory: () => false,
|
|
77
|
+
} as ReturnType<typeof statSync>);
|
|
65
78
|
|
|
66
79
|
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
67
80
|
});
|
|
@@ -71,13 +84,13 @@ describe("Cli", () => {
|
|
|
71
84
|
});
|
|
72
85
|
|
|
73
86
|
describe("run", () => {
|
|
74
|
-
it("returns shouldRun: true with config when
|
|
87
|
+
it("returns shouldRun: true with config when input is provided", () => {
|
|
75
88
|
const result = Cli.run();
|
|
76
89
|
|
|
77
90
|
expect(result.shouldRun).toBe(true);
|
|
78
91
|
expect(result.exitCode).toBe(0);
|
|
79
92
|
expect(result.config).toBeDefined();
|
|
80
|
-
expect(result.config?.
|
|
93
|
+
expect(result.config?.input).toBe("input.cnx");
|
|
81
94
|
});
|
|
82
95
|
|
|
83
96
|
it("handles --pio-install flag", () => {
|
|
@@ -135,7 +148,46 @@ describe("Cli", () => {
|
|
|
135
148
|
expect(result.exitCode).toBe(0);
|
|
136
149
|
});
|
|
137
150
|
|
|
138
|
-
it("returns error when
|
|
151
|
+
it("returns error when multiple input files specified", () => {
|
|
152
|
+
mockParsedArgs.inputFiles = ["a.cnx", "b.cnx"];
|
|
153
|
+
vi.mocked(ArgParser.parse).mockReturnValue(mockParsedArgs);
|
|
154
|
+
|
|
155
|
+
const result = Cli.run();
|
|
156
|
+
|
|
157
|
+
expect(result.shouldRun).toBe(false);
|
|
158
|
+
expect(result.exitCode).toBe(1);
|
|
159
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
160
|
+
"Error: Only one entry point file is supported",
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("returns error when input file does not exist", () => {
|
|
165
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
166
|
+
|
|
167
|
+
const result = Cli.run();
|
|
168
|
+
|
|
169
|
+
expect(result.shouldRun).toBe(false);
|
|
170
|
+
expect(result.exitCode).toBe(1);
|
|
171
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
172
|
+
expect.stringContaining("Input not found"),
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("returns error when input is a directory", () => {
|
|
177
|
+
vi.mocked(statSync).mockReturnValue({
|
|
178
|
+
isDirectory: () => true,
|
|
179
|
+
} as ReturnType<typeof statSync>);
|
|
180
|
+
|
|
181
|
+
const result = Cli.run();
|
|
182
|
+
|
|
183
|
+
expect(result.shouldRun).toBe(false);
|
|
184
|
+
expect(result.exitCode).toBe(1);
|
|
185
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
186
|
+
"Error: Directory input not supported. Specify an entry point file.",
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("returns error when no input file specified", () => {
|
|
139
191
|
mockParsedArgs.inputFiles = [];
|
|
140
192
|
vi.mocked(ArgParser.parse).mockReturnValue(mockParsedArgs);
|
|
141
193
|
|
|
@@ -144,7 +196,7 @@ describe("Cli", () => {
|
|
|
144
196
|
expect(result.shouldRun).toBe(false);
|
|
145
197
|
expect(result.exitCode).toBe(1);
|
|
146
198
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
147
|
-
"Error: No input
|
|
199
|
+
"Error: No input file specified",
|
|
148
200
|
);
|
|
149
201
|
});
|
|
150
202
|
|
|
@@ -157,7 +209,7 @@ describe("Cli", () => {
|
|
|
157
209
|
expect(ConfigLoader.load).toHaveBeenCalledWith("/path/to/src");
|
|
158
210
|
});
|
|
159
211
|
|
|
160
|
-
it("loads config from cwd when no input
|
|
212
|
+
it("loads config from cwd when no input file", () => {
|
|
161
213
|
mockParsedArgs.inputFiles = [];
|
|
162
214
|
mockParsedArgs.showConfig = true; // To avoid error path
|
|
163
215
|
vi.mocked(ArgParser.parse).mockReturnValue(mockParsedArgs);
|
|
@@ -294,7 +346,7 @@ describe("Cli", () => {
|
|
|
294
346
|
|
|
295
347
|
it("passes through all config fields", () => {
|
|
296
348
|
mockParsedArgs = {
|
|
297
|
-
inputFiles: ["a.cnx"
|
|
349
|
+
inputFiles: ["a.cnx"],
|
|
298
350
|
outputPath: "build/",
|
|
299
351
|
includeDirs: ["inc/"],
|
|
300
352
|
defines: { DEBUG: true },
|
|
@@ -318,7 +370,7 @@ describe("Cli", () => {
|
|
|
318
370
|
const result = Cli.run();
|
|
319
371
|
|
|
320
372
|
expect(result.config).toEqual({
|
|
321
|
-
|
|
373
|
+
input: "a.cnx",
|
|
322
374
|
outputPath: "build/",
|
|
323
375
|
includeDirs: ["inc/"],
|
|
324
376
|
defines: { DEBUG: true },
|