c-next 0.2.1 → 0.2.2

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 (31) hide show
  1. package/bin/cnext.js +17 -7
  2. package/dist/index.js +139747 -0
  3. package/dist/index.js.map +7 -0
  4. package/package.json +8 -4
  5. package/src/cli/Cli.ts +5 -2
  6. package/src/cli/PathNormalizer.ts +170 -0
  7. package/src/cli/PlatformIOCommand.ts +51 -3
  8. package/src/cli/__tests__/Cli.integration.test.ts +100 -0
  9. package/src/cli/__tests__/Cli.test.ts +17 -12
  10. package/src/cli/__tests__/PathNormalizer.test.ts +411 -0
  11. package/src/cli/__tests__/PlatformIOCommand.test.ts +156 -0
  12. package/src/cli/serve/__tests__/ServeCommand.test.ts +1 -1
  13. package/src/transpiler/NodeFileSystem.ts +5 -0
  14. package/src/transpiler/logic/symbols/SymbolTable.ts +17 -0
  15. package/src/transpiler/output/codegen/CodeGenerator.ts +27 -32
  16. package/src/transpiler/output/codegen/TypeResolver.ts +12 -24
  17. package/src/transpiler/output/codegen/__tests__/CodeGenerator.coverage.test.ts +130 -5
  18. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +67 -57
  19. package/src/transpiler/output/codegen/analysis/MemberChainAnalyzer.ts +9 -13
  20. package/src/transpiler/output/codegen/analysis/__tests__/MemberChainAnalyzer.test.ts +20 -10
  21. package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +5 -2
  22. package/src/transpiler/output/codegen/assignment/__tests__/AssignmentClassifier.test.ts +18 -0
  23. package/src/transpiler/output/codegen/assignment/handlers/StringHandlers.ts +25 -4
  24. package/src/transpiler/output/codegen/assignment/handlers/__tests__/handlerTestUtils.ts +18 -0
  25. package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +51 -2
  26. package/src/transpiler/output/codegen/generators/expressions/LiteralGenerator.ts +76 -8
  27. package/src/transpiler/output/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts +147 -0
  28. package/src/transpiler/output/codegen/generators/expressions/__tests__/LiteralGenerator.test.ts +116 -0
  29. package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +6 -5
  30. package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +14 -5
  31. package/src/transpiler/types/IFileSystem.ts +8 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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",
@@ -23,7 +23,7 @@
23
23
  "test:q": "tsx scripts/test.ts -q",
24
24
  "test:update": "tsx scripts/test.ts --update",
25
25
  "analyze": "./scripts/static-analysis.sh",
26
- "clean": "rm -rf src/transpiler/logic/parser/grammar src/transpiler/logic/parser/c/grammar src/transpiler/logic/parser/cpp/grammar",
26
+ "clean": "rm -rf dist src/transpiler/logic/parser/grammar src/transpiler/logic/parser/c/grammar src/transpiler/logic/parser/cpp/grammar",
27
27
  "prettier:check": "prettier --check .",
28
28
  "prettier:fix": "prettier --write .",
29
29
  "cspell:check": "cspell .",
@@ -33,7 +33,7 @@
33
33
  "depcruise": "depcruise src/transpiler --config .dependency-cruiser.cjs",
34
34
  "depcruise:graph": "depcruise src/transpiler --config .dependency-cruiser.cjs --output-type dot | dot -T svg > dependency-graph.svg",
35
35
  "prepare": "husky",
36
- "prepublishOnly": "npm run prettier:check && npm run oxlint:check && npm test",
36
+ "prepublishOnly": "npm run prettier:check && npm run oxlint:check && npm run build && npm test",
37
37
  "coverage:check": "tsx scripts/coverage-checker.ts check",
38
38
  "coverage:report": "tsx scripts/coverage-checker.ts report",
39
39
  "coverage:gaps": "tsx scripts/coverage-checker.ts gaps",
@@ -45,7 +45,9 @@
45
45
  "unit:watch": "vitest",
46
46
  "unit:coverage": "vitest run --coverage",
47
47
  "unit:coverage:html": "vitest run --coverage && echo 'Coverage report: coverage/index.html'",
48
- "test:all": "npm run unit && npm run test:q",
48
+ "build": "node scripts/build.mjs",
49
+ "test:all": "npm run build && npm run unit && npm run test:q && npm run validate:c",
50
+ "validate:c": "node scripts/batch-validate.mjs",
49
51
  "duplication": "npx jscpd src/ scripts/ --reporters console",
50
52
  "duplication:json": "npx jscpd src/ scripts/ --reporters json --output .jscpd",
51
53
  "duplication:sonar": "curl -s 'https://sonarcloud.io/api/measures/component_tree?component=jlaustill_c-next&metricKeys=duplicated_blocks,duplicated_lines&s=metric&metricSort=duplicated_blocks&metricSortFilter=withMeasuresOnly&asc=false&ps=20' | jq -r '.components[] | \"\\(.path)\\t\\(.measures[0].value) blocks\\t\\(.measures[1].value) lines\"'",
@@ -79,6 +81,7 @@
79
81
  },
80
82
  "files": [
81
83
  "bin/",
84
+ "dist/",
82
85
  "src/",
83
86
  "grammar/",
84
87
  "README.md",
@@ -87,6 +90,7 @@
87
90
  "devDependencies": {
88
91
  "@types/node": "^25.2.1",
89
92
  "@types/yargs": "^17.0.35",
93
+ "esbuild": "^0.27.3",
90
94
  "@vitest/coverage-v8": "^4.0.18",
91
95
  "antlr4ng-cli": "^2.0.0",
92
96
  "chalk": "^5.6.2",
package/src/cli/Cli.ts CHANGED
@@ -10,6 +10,7 @@ import ConfigLoader from "./ConfigLoader";
10
10
  import ConfigPrinter from "./ConfigPrinter";
11
11
  import PlatformIOCommand from "./PlatformIOCommand";
12
12
  import CleanCommand from "./CleanCommand";
13
+ import PathNormalizer from "./PathNormalizer";
13
14
  import ICliResult from "./types/ICliResult";
14
15
  import ICliConfig from "./types/ICliConfig";
15
16
  import IParsedArgs from "./types/IParsedArgs";
@@ -92,7 +93,7 @@ class Cli {
92
93
  args: IParsedArgs,
93
94
  fileConfig: IFileConfig,
94
95
  ): ICliConfig {
95
- return {
96
+ const rawConfig: ICliConfig = {
96
97
  inputs: args.inputFiles,
97
98
  outputPath: args.outputPath || fileConfig.output || "",
98
99
  // Merge include dirs: config includes come first, CLI includes override/append
@@ -100,7 +101,7 @@ class Cli {
100
101
  defines: args.defines,
101
102
  preprocess: args.preprocess,
102
103
  verbose: args.verbose,
103
- cppRequired: args.cppRequired ?? fileConfig.cppRequired ?? false,
104
+ cppRequired: args.cppRequired || fileConfig.cppRequired || false,
104
105
  noCache: args.noCache || fileConfig.noCache === true,
105
106
  parseOnly: args.parseOnly,
106
107
  headerOutDir: args.headerOutDir ?? fileConfig.headerOut,
@@ -108,6 +109,8 @@ class Cli {
108
109
  target: args.target ?? fileConfig.target,
109
110
  debugMode: args.debugMode || fileConfig.debugMode,
110
111
  };
112
+
113
+ return PathNormalizer.normalizeConfig(rawConfig);
111
114
  }
112
115
  }
113
116
 
@@ -0,0 +1,170 @@
1
+ /**
2
+ * PathNormalizer
3
+ * Centralized path normalization for all config paths.
4
+ * Handles tilde expansion and recursive directory search.
5
+ */
6
+
7
+ import { join } from "node:path";
8
+ import IFileSystem from "../transpiler/types/IFileSystem";
9
+ import NodeFileSystem from "../transpiler/NodeFileSystem";
10
+ import ICliConfig from "./types/ICliConfig";
11
+
12
+ /** Default file system instance */
13
+ const defaultFs = NodeFileSystem.instance;
14
+
15
+ class PathNormalizer {
16
+ /**
17
+ * Expand ~ at the start of a path to the home directory.
18
+ * Only expands leading tilde (~/path or bare ~).
19
+ * @param path - Path that may start with ~
20
+ * @returns Path with ~ expanded to home directory
21
+ */
22
+ static expandTilde(path: string): string {
23
+ if (!path.startsWith("~")) {
24
+ return path;
25
+ }
26
+
27
+ const home = process.env.HOME || process.env.USERPROFILE;
28
+ if (!home) {
29
+ return path;
30
+ }
31
+
32
+ if (path === "~") {
33
+ return home;
34
+ }
35
+
36
+ if (path.startsWith("~/")) {
37
+ return home + path.slice(1);
38
+ }
39
+
40
+ return path;
41
+ }
42
+
43
+ /**
44
+ * Expand path/** to include all subdirectories recursively.
45
+ * If path doesn't end with /**, returns the path as single-element array
46
+ * (if it exists) or empty array (if it doesn't exist).
47
+ * @param path - Path that may end with /**
48
+ * @param fs - File system abstraction for testing
49
+ * @returns Array of all directories found
50
+ */
51
+ static expandRecursive(path: string, fs: IFileSystem = defaultFs): string[] {
52
+ const hasRecursiveSuffix = path.endsWith("/**");
53
+ const basePath = hasRecursiveSuffix ? path.slice(0, -3) : path;
54
+
55
+ if (!fs.exists(basePath)) {
56
+ return [];
57
+ }
58
+
59
+ if (!fs.isDirectory(basePath)) {
60
+ return [basePath];
61
+ }
62
+
63
+ if (!hasRecursiveSuffix) {
64
+ return [basePath];
65
+ }
66
+
67
+ // Recursively collect all subdirectories
68
+ const dirs: string[] = [basePath];
69
+ this.collectSubdirectories(basePath, dirs, fs);
70
+ return dirs;
71
+ }
72
+
73
+ /**
74
+ * Recursively collect all subdirectories into the dirs array.
75
+ * Uses a visited set to prevent symlink loops.
76
+ */
77
+ private static collectSubdirectories(
78
+ dir: string,
79
+ dirs: string[],
80
+ fs: IFileSystem,
81
+ visited: Set<string> = new Set(),
82
+ ): void {
83
+ // Get real path to detect symlink loops
84
+ const realPath = fs.realpath ? fs.realpath(dir) : dir;
85
+ if (visited.has(realPath)) {
86
+ return; // Skip already-visited directories (symlink loop protection)
87
+ }
88
+ visited.add(realPath);
89
+
90
+ const entries = fs.readdir(dir);
91
+ for (const entry of entries) {
92
+ const fullPath = join(dir, entry);
93
+ if (fs.isDirectory(fullPath)) {
94
+ // Check if this subdirectory's real path was already visited
95
+ const subRealPath = fs.realpath ? fs.realpath(fullPath) : fullPath;
96
+ if (!visited.has(subRealPath)) {
97
+ dirs.push(fullPath);
98
+ this.collectSubdirectories(fullPath, dirs, fs, visited);
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Normalize a single path (tilde expansion only).
106
+ * Used for output, headerOut, basePath.
107
+ * @param path - Path to normalize
108
+ * @returns Normalized path
109
+ */
110
+ static normalizePath(path: string): string {
111
+ if (!path) {
112
+ return path;
113
+ }
114
+ return this.expandTilde(path);
115
+ }
116
+
117
+ /**
118
+ * Normalize include paths (tilde + recursive expansion).
119
+ * Deduplicates paths to avoid redundant includes.
120
+ * @param paths - Array of paths to normalize
121
+ * @param fs - File system abstraction for testing
122
+ * @returns Flattened array of all resolved directories (deduplicated)
123
+ */
124
+ static normalizeIncludePaths(
125
+ paths: string[],
126
+ fs: IFileSystem = defaultFs,
127
+ ): string[] {
128
+ const seen = new Set<string>();
129
+ const result: string[] = [];
130
+
131
+ for (const path of paths) {
132
+ const expanded = this.expandTilde(path);
133
+ const dirs = this.expandRecursive(expanded, fs);
134
+ for (const dir of dirs) {
135
+ if (!seen.has(dir)) {
136
+ seen.add(dir);
137
+ result.push(dir);
138
+ }
139
+ }
140
+ }
141
+
142
+ return result;
143
+ }
144
+
145
+ /**
146
+ * Normalize all paths in a CLI config.
147
+ * Single entry point for all path normalization.
148
+ * @param config - CLI config with potentially unnormalized paths
149
+ * @param fs - File system abstraction for testing
150
+ * @returns New config with all paths normalized
151
+ */
152
+ static normalizeConfig(
153
+ config: ICliConfig,
154
+ fs: IFileSystem = defaultFs,
155
+ ): ICliConfig {
156
+ return {
157
+ ...config,
158
+ outputPath: this.normalizePath(config.outputPath),
159
+ headerOutDir: config.headerOutDir
160
+ ? this.normalizePath(config.headerOutDir)
161
+ : undefined,
162
+ basePath: config.basePath
163
+ ? this.normalizePath(config.basePath)
164
+ : undefined,
165
+ includeDirs: this.normalizeIncludePaths(config.includeDirs, fs),
166
+ };
167
+ }
168
+ }
169
+
170
+ export default PathNormalizer;
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { resolve } from "node:path";
7
7
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
8
+ import IFileConfig from "./types/IFileConfig";
8
9
 
9
10
  /**
10
11
  * Resolved paths for PlatformIO project.
@@ -43,11 +44,14 @@ class PlatformIOCommand {
43
44
  const { pioIniPath, scriptPath } = getPioProjectPaths();
44
45
 
45
46
  // Create cnext_build.py script
47
+ // Issue #833: Run transpilation at import time (before compilation),
48
+ // not as a pre-action on buildprog (which runs after compilation)
46
49
  const buildScript = `Import("env")
47
50
  import subprocess
51
+ import sys
48
52
  from pathlib import Path
49
53
 
50
- def transpile_cnext(source, target, env):
54
+ def transpile_cnext():
51
55
  """Transpile all .cnx files before build"""
52
56
  # Find all .cnx files in src directory
53
57
  src_dir = Path("src")
@@ -72,9 +76,10 @@ def transpile_cnext(source, target, env):
72
76
  except subprocess.CalledProcessError as e:
73
77
  print(f" ✗ Error: {cnx_file.name}")
74
78
  print(e.stderr)
75
- env.Exit(1)
79
+ sys.exit(1)
76
80
 
77
- env.AddPreAction("buildprog", transpile_cnext)
81
+ # Run transpilation at import time (before compilation starts)
82
+ transpile_cnext()
78
83
  `;
79
84
 
80
85
  writeFileSync(scriptPath, buildScript, "utf-8");
@@ -107,6 +112,9 @@ env.AddPreAction("buildprog", transpile_cnext)
107
112
  writeFileSync(pioIniPath, pioIni, "utf-8");
108
113
  console.log(`✓ Modified: ${pioIniPath}`);
109
114
 
115
+ // Setup cnext.config.json for PlatformIO
116
+ this.setupConfig();
117
+
110
118
  console.log("");
111
119
  console.log("✓ PlatformIO integration configured!");
112
120
  console.log("");
@@ -122,6 +130,46 @@ env.AddPreAction("buildprog", transpile_cnext)
122
130
  console.log("Commit both .cnx and generated .c files to version control.");
123
131
  }
124
132
 
133
+ /**
134
+ * Setup cnext.config.json with PlatformIO-appropriate defaults
135
+ * - Appends .pio/libdeps to include array
136
+ * - Sets headerOut to "include" if not already set
137
+ */
138
+ private static setupConfig(): void {
139
+ const configPath = resolve(process.cwd(), "cnext.config.json");
140
+ const pioInclude = ".pio/libdeps";
141
+
142
+ let config: IFileConfig = {};
143
+
144
+ if (existsSync(configPath)) {
145
+ try {
146
+ const content = readFileSync(configPath, "utf-8");
147
+ config = JSON.parse(content) as IFileConfig;
148
+ } catch (e) {
149
+ const msg = e instanceof Error ? e.message : String(e);
150
+ console.log(
151
+ `⚠ Could not parse existing cnext.config.json (${msg}), creating new one`,
152
+ );
153
+ config = {};
154
+ }
155
+ }
156
+
157
+ // Append .pio/libdeps to include array if not already present
158
+ const includes = config.include ?? [];
159
+ if (!includes.includes(pioInclude)) {
160
+ config.include = [...includes, pioInclude];
161
+ }
162
+
163
+ // Set headerOut only if not already set
164
+ if (!config.headerOut) {
165
+ config.headerOut = "include";
166
+ }
167
+
168
+ // Write config (with pretty formatting)
169
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
170
+ console.log(`✓ Updated: ${configPath}`);
171
+ }
172
+
125
173
  /**
126
174
  * Remove PlatformIO integration
127
175
  * Deletes cnext_build.py and removes extra_scripts from platformio.ini
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Integration tests for Cli path normalization.
3
+ * These tests do NOT mock dependencies - they test real behavior.
4
+ *
5
+ * Separate from Cli.test.ts to avoid vi.mock() hoisting issues.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
9
+ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+ import Cli from "../Cli";
13
+
14
+ describe("Cli path normalization (integration)", () => {
15
+ let tempDir: string;
16
+ const originalHome = process.env.HOME;
17
+ const originalArgv = process.argv;
18
+
19
+ beforeEach(() => {
20
+ tempDir = mkdtempSync(join(tmpdir(), "cli-paths-"));
21
+ process.env.HOME = "/home/testuser";
22
+ });
23
+
24
+ afterEach(() => {
25
+ rmSync(tempDir, { recursive: true, force: true });
26
+ process.env.HOME = originalHome;
27
+ process.argv = originalArgv;
28
+ });
29
+
30
+ it("expands tilde in config include paths", () => {
31
+ // Create a real home directory structure to verify tilde expansion
32
+ const homeDir = mkdtempSync(join(tmpdir(), "home-"));
33
+ mkdirSync(join(homeDir, "sdk", "include"), { recursive: true });
34
+
35
+ process.env.HOME = homeDir;
36
+
37
+ writeFileSync(
38
+ join(tempDir, "cnext.config.json"),
39
+ JSON.stringify({ include: ["~/sdk/include"] }),
40
+ );
41
+ writeFileSync(join(tempDir, "test.cnx"), "void main() {}");
42
+
43
+ process.argv = ["node", "cnext", join(tempDir, "test.cnx")];
44
+
45
+ const result = Cli.run();
46
+
47
+ // Verify tilde was expanded to actual home directory path
48
+ expect(result.config?.includeDirs).toContain(join(homeDir, "sdk/include"));
49
+ // Verify the unexpanded tilde path is NOT present
50
+ expect(result.config?.includeDirs).not.toContain("~/sdk/include");
51
+
52
+ rmSync(homeDir, { recursive: true, force: true });
53
+ });
54
+
55
+ it("expands ** in config include paths", () => {
56
+ // Create directory structure
57
+ mkdirSync(join(tempDir, "include", "sub"), { recursive: true });
58
+ writeFileSync(
59
+ join(tempDir, "cnext.config.json"),
60
+ JSON.stringify({ include: [`${tempDir}/include/**`] }),
61
+ );
62
+ writeFileSync(join(tempDir, "test.cnx"), "void main() {}");
63
+
64
+ process.argv = ["node", "cnext", join(tempDir, "test.cnx")];
65
+
66
+ const result = Cli.run();
67
+
68
+ expect(result.config?.includeDirs).toContain(join(tempDir, "include"));
69
+ expect(result.config?.includeDirs).toContain(
70
+ join(tempDir, "include", "sub"),
71
+ );
72
+ });
73
+
74
+ it("expands tilde in CLI --include paths", () => {
75
+ // Create a real home directory structure to verify tilde expansion
76
+ const homeDir = mkdtempSync(join(tmpdir(), "home-"));
77
+ mkdirSync(join(homeDir, "my-libs"), { recursive: true });
78
+
79
+ process.env.HOME = homeDir;
80
+
81
+ writeFileSync(join(tempDir, "test.cnx"), "void main() {}");
82
+
83
+ process.argv = [
84
+ "node",
85
+ "cnext",
86
+ join(tempDir, "test.cnx"),
87
+ "--include",
88
+ "~/my-libs",
89
+ ];
90
+
91
+ const result = Cli.run();
92
+
93
+ // Verify tilde was expanded to actual home directory path
94
+ expect(result.config?.includeDirs).toContain(join(homeDir, "my-libs"));
95
+ // Verify the unexpanded tilde path is NOT present
96
+ expect(result.config?.includeDirs).not.toContain("~/my-libs");
97
+
98
+ rmSync(homeDir, { recursive: true, force: true });
99
+ });
100
+ });
@@ -25,6 +25,12 @@ vi.mock("../ConfigLoader");
25
25
  vi.mock("../ConfigPrinter");
26
26
  vi.mock("../PlatformIOCommand");
27
27
  vi.mock("../CleanCommand");
28
+ vi.mock("../PathNormalizer", () => ({
29
+ default: {
30
+ // Pass through the config unchanged for unit tests
31
+ normalizeConfig: (config: unknown) => config,
32
+ },
33
+ }));
28
34
 
29
35
  describe("Cli", () => {
30
36
  let mockParsedArgs: IParsedArgs;
@@ -180,40 +186,39 @@ describe("Cli", () => {
180
186
  expect(result.config?.includeDirs).toContain("lib/");
181
187
  });
182
188
 
183
- it("uses file config cppRequired when CLI is undefined", () => {
184
- // This test documents that with yargs, cppRequired is always defined,
185
- // so file config cppRequired only applies if we explicitly set CLI to undefined
189
+ it("honors config file cppRequired when CLI does not specify --cpp (issue #827)", () => {
190
+ // Issue #827: When user doesn't pass --cpp, config file cppRequired should be used
191
+ // yargs returns cppRequired: false as the default, but config file should override
186
192
  const fileConfig: IFileConfig = {
187
193
  cppRequired: true,
188
194
  };
189
195
  vi.mocked(ConfigLoader.load).mockReturnValue(fileConfig);
190
196
 
191
- // Simulate undefined cppRequired (not possible with real yargs, but tests the merge logic)
192
- mockParsedArgs.cppRequired = undefined as unknown as boolean;
197
+ // CLI has cppRequired: false (yargs default when --cpp not specified)
198
+ mockParsedArgs.cppRequired = false;
193
199
  vi.mocked(ArgParser.parse).mockReturnValue(mockParsedArgs);
194
200
 
195
201
  const result = Cli.run();
196
202
 
203
+ // Config file's cppRequired: true should be honored
197
204
  expect(result.config?.cppRequired).toBe(true);
198
205
  });
199
206
 
200
- it("CLI args take precedence over file config", () => {
201
- mockParsedArgs.cppRequired = false;
207
+ it("CLI --cpp flag takes precedence over file config", () => {
208
+ mockParsedArgs.cppRequired = true;
202
209
  mockParsedArgs.target = "cortex-m0";
203
210
  vi.mocked(ArgParser.parse).mockReturnValue(mockParsedArgs);
204
211
 
205
212
  const fileConfig: IFileConfig = {
206
- cppRequired: true,
213
+ cppRequired: false,
207
214
  target: "teensy41",
208
215
  };
209
216
  vi.mocked(ConfigLoader.load).mockReturnValue(fileConfig);
210
217
 
211
218
  const result = Cli.run();
212
219
 
213
- // CLI explicitly set cppRequired to false, but the merge uses ?? so undefined would fall through
214
- // Actually, looking at the code: args.cppRequired ?? fileConfig.cppRequired ?? false
215
- // So if args.cppRequired is false (not undefined), it should use false
216
- expect(result.config?.cppRequired).toBe(false);
220
+ // CLI --cpp flag should take precedence
221
+ expect(result.config?.cppRequired).toBe(true);
217
222
  expect(result.config?.target).toBe("cortex-m0");
218
223
  });
219
224