c-next 0.2.5 → 0.2.7

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 (57) hide show
  1. package/dist/index.js +617 -474
  2. package/dist/index.js.map +4 -4
  3. package/package.json +1 -1
  4. package/src/transpiler/Transpiler.ts +50 -29
  5. package/src/transpiler/__tests__/Transpiler.coverage.test.ts +2 -1
  6. package/src/transpiler/data/PathResolver.ts +10 -6
  7. package/src/transpiler/data/__tests__/PathResolver.test.ts +32 -0
  8. package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +82 -7
  9. package/src/transpiler/logic/analysis/PassByValueAnalyzer.ts +1 -12
  10. package/src/transpiler/logic/analysis/SignedShiftAnalyzer.ts +239 -0
  11. package/src/transpiler/logic/analysis/__tests__/FunctionCallAnalyzer.test.ts +92 -0
  12. package/src/transpiler/logic/analysis/__tests__/SignedShiftAnalyzer.test.ts +414 -0
  13. package/src/transpiler/logic/analysis/__tests__/StructFieldAnalyzer.test.ts +15 -75
  14. package/src/transpiler/logic/analysis/__tests__/runAnalyzers.test.ts +3 -15
  15. package/src/transpiler/logic/analysis/runAnalyzers.ts +11 -2
  16. package/src/transpiler/logic/analysis/types/ISignedShiftError.ts +15 -0
  17. package/src/transpiler/logic/symbols/SymbolUtils.ts +4 -1
  18. package/src/transpiler/logic/symbols/__tests__/SymbolUtils.test.ts +10 -11
  19. package/src/transpiler/logic/symbols/c/__tests__/CResolver.integration.test.ts +27 -4
  20. package/src/transpiler/logic/symbols/c/collectors/FunctionCollector.ts +11 -5
  21. package/src/transpiler/logic/symbols/cpp/__tests__/CppResolver.integration.test.ts +4 -4
  22. package/src/transpiler/output/codegen/CodeGenerator.ts +169 -11
  23. package/src/transpiler/output/codegen/__tests__/CodeGenerator.coverage.test.ts +132 -3
  24. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +56 -31
  25. package/src/transpiler/output/codegen/analysis/StringLengthCounter.ts +12 -11
  26. package/src/transpiler/output/codegen/analysis/__tests__/StringLengthCounter.test.ts +21 -21
  27. package/src/transpiler/output/codegen/assignment/handlers/AccessPatternHandlers.ts +2 -2
  28. package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +2 -2
  29. package/src/transpiler/output/codegen/assignment/handlers/BitmapHandlers.ts +2 -2
  30. package/src/transpiler/output/codegen/assignment/handlers/RegisterHandlers.ts +4 -4
  31. package/src/transpiler/output/codegen/assignment/handlers/__tests__/AccessPatternHandlers.test.ts +4 -4
  32. package/src/transpiler/output/codegen/assignment/handlers/__tests__/BitAccessHandlers.test.ts +8 -8
  33. package/src/transpiler/output/codegen/assignment/handlers/__tests__/BitmapHandlers.test.ts +5 -5
  34. package/src/transpiler/output/codegen/assignment/handlers/__tests__/RegisterHandlers.test.ts +4 -4
  35. package/src/transpiler/output/codegen/generators/expressions/AccessExprGenerator.ts +7 -326
  36. package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +8 -1
  37. package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +14 -275
  38. package/src/transpiler/output/codegen/generators/expressions/UnaryExprGenerator.ts +13 -1
  39. package/src/transpiler/output/codegen/generators/expressions/__tests__/AccessExprGenerator.test.ts +0 -573
  40. package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +8 -439
  41. package/src/transpiler/output/codegen/generators/expressions/__tests__/UnaryExprGenerator.test.ts +196 -0
  42. package/src/transpiler/output/codegen/helpers/ArgumentGenerator.ts +5 -0
  43. package/src/transpiler/output/codegen/helpers/ParameterInputAdapter.ts +7 -2
  44. package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +8 -1
  45. package/src/transpiler/output/codegen/helpers/TypedefParamParser.ts +34 -0
  46. package/src/transpiler/output/codegen/helpers/VariableDeclHelper.ts +11 -0
  47. package/src/transpiler/output/codegen/helpers/VariableModifierBuilder.ts +16 -1
  48. package/src/transpiler/output/codegen/helpers/__tests__/ParameterInputAdapter.test.ts +48 -0
  49. package/src/transpiler/output/codegen/helpers/__tests__/TypedefParamParser.test.ts +88 -0
  50. package/src/transpiler/output/codegen/helpers/__tests__/VariableModifierBuilder.test.ts +34 -2
  51. package/src/transpiler/output/codegen/types/TTypeInfo.ts +1 -0
  52. package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +15 -2
  53. package/src/transpiler/output/headers/__tests__/BaseHeaderGenerator.test.ts +87 -0
  54. package/src/utils/BitUtils.ts +17 -13
  55. package/src/utils/ExpressionUtils.ts +51 -0
  56. package/src/utils/__tests__/BitUtils.test.ts +56 -56
  57. package/src/utils/types/IParameterSymbol.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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",
@@ -61,6 +61,7 @@ import CacheManager from "../utils/cache/CacheManager";
61
61
  import MapUtils from "../utils/MapUtils";
62
62
  import detectCppSyntax from "./logic/detectCppSyntax";
63
63
  import TransitiveEnumCollector from "./logic/symbols/TransitiveEnumCollector";
64
+ import TypedefParamParser from "./output/codegen/helpers/TypedefParamParser";
64
65
 
65
66
  /**
66
67
  * Unified transpiler
@@ -680,8 +681,10 @@ class Transpiler {
680
681
  }
681
682
  const headerContent = this.generateHeaderForFile(file);
682
683
  if (headerContent) {
684
+ // Issue #933: Pass cppDetected to generate .hpp in C++ mode
683
685
  const headerPath = this.pathResolver.getHeaderOutputPath(
684
686
  file.discoveredFile,
687
+ this.cppDetected,
685
688
  );
686
689
  this.fs.writeFile(headerPath, headerContent);
687
690
  result.outputFiles.push(headerPath);
@@ -1230,7 +1233,9 @@ class Transpiler {
1230
1233
  return null;
1231
1234
  }
1232
1235
 
1233
- const headerName = basename(sourcePath).replace(/\.cnx$|\.cnext$/, ".h");
1236
+ // Issue #933: Use .hpp extension for include guard in C++ mode
1237
+ const ext = this.cppDetected ? ".hpp" : ".h";
1238
+ const headerName = basename(sourcePath).replace(/\.cnx$|\.cnext$/, ext);
1234
1239
 
1235
1240
  const typeInput = this.state.getSymbolInfo(sourcePath);
1236
1241
  const passByValueParams =
@@ -1327,36 +1332,52 @@ class Transpiler {
1327
1332
  return symbols.map((symbol) => {
1328
1333
  const headerSymbol = HeaderSymbolAdapter.fromTSymbol(symbol);
1329
1334
 
1330
- // Apply auto-const to function parameters
1331
1335
  if (
1332
- symbol.kind === "function" &&
1333
- headerSymbol.parameters &&
1334
- headerSymbol.parameters.length > 0
1336
+ symbol.kind !== "function" ||
1337
+ !headerSymbol.parameters ||
1338
+ headerSymbol.parameters.length === 0
1335
1339
  ) {
1336
- const unmodified = unmodifiedParams.get(headerSymbol.name);
1337
- if (unmodified) {
1338
- // Create a mutable copy of parameters with auto-const applied
1339
- const updatedParams = headerSymbol.parameters.map((param) => {
1340
- const isPointerParam =
1341
- !param.isConst &&
1342
- !param.isArray &&
1343
- param.type !== "f32" &&
1344
- param.type !== "f64" &&
1345
- param.type !== "ISR" &&
1346
- !knownEnums.has(param.type ?? "");
1347
- const isArrayParam = param.isArray && !param.isConst;
1348
-
1349
- if (
1350
- (isPointerParam || isArrayParam) &&
1351
- unmodified.has(param.name)
1352
- ) {
1353
- return { ...param, isAutoConst: true };
1354
- }
1355
- return param;
1356
- });
1357
-
1358
- return { ...headerSymbol, parameters: updatedParams };
1359
- }
1340
+ return headerSymbol;
1341
+ }
1342
+
1343
+ // Issue #914: Resolve callback typedef type for callback-compatible functions
1344
+ const typedefName = CodeGenState.callbackCompatibleFunctions.get(
1345
+ headerSymbol.name,
1346
+ );
1347
+ const callbackTypedefType = typedefName
1348
+ ? CodeGenState.getTypedefType(typedefName)
1349
+ : undefined;
1350
+
1351
+ // Issue #914: For callback-compatible functions, bake pointer/const overrides
1352
+ // onto each parameter. Skip auto-const (matches CodeGenerator path).
1353
+ if (callbackTypedefType) {
1354
+ const updatedParams = TypedefParamParser.resolveCallbackParams(
1355
+ headerSymbol.parameters,
1356
+ callbackTypedefType,
1357
+ );
1358
+ return { ...headerSymbol, parameters: updatedParams };
1359
+ }
1360
+
1361
+ // Apply auto-const to non-callback function parameters
1362
+ const unmodified = unmodifiedParams.get(headerSymbol.name);
1363
+ if (unmodified) {
1364
+ const updatedParams = headerSymbol.parameters.map((param) => {
1365
+ const isPointerParam =
1366
+ !param.isConst &&
1367
+ !param.isArray &&
1368
+ param.type !== "f32" &&
1369
+ param.type !== "f64" &&
1370
+ param.type !== "ISR" &&
1371
+ !knownEnums.has(param.type ?? "");
1372
+ const isArrayParam = param.isArray && !param.isConst;
1373
+
1374
+ if ((isPointerParam || isArrayParam) && unmodified.has(param.name)) {
1375
+ return { ...param, isAutoConst: true };
1376
+ }
1377
+ return param;
1378
+ });
1379
+
1380
+ return { ...headerSymbol, parameters: updatedParams };
1360
1381
  }
1361
1382
 
1362
1383
  return headerSymbol;
@@ -556,8 +556,9 @@ describe("Transpiler coverage tests", () => {
556
556
 
557
557
  expect(result.success).toBe(true);
558
558
  // Header should be generated
559
+ // Issue #933: C++ mode generates .hpp extension
559
560
  const writeCalls = mockFs.getWriteLog();
560
- const headerWrites = writeCalls.filter((w) => w.path.endsWith(".h"));
561
+ const headerWrites = writeCalls.filter((w) => w.path.endsWith(".hpp"));
561
562
  expect(headerWrites.length).toBe(1);
562
563
  });
563
564
  });
@@ -109,13 +109,17 @@ class PathResolver {
109
109
  }
110
110
 
111
111
  /**
112
- * Get output path for a header file (.h)
112
+ * Get output path for a header file (.h or .hpp)
113
113
  * Uses headerOutDir if specified, otherwise falls back to outDir
114
114
  *
115
115
  * @param file - The discovered file to get header path for
116
+ * @param cppMode - If true, output .hpp; otherwise .h (Issue #933)
116
117
  * @returns The full header output path
117
118
  */
118
- getHeaderOutputPath(file: IDiscoveredFile): string {
119
+ getHeaderOutputPath(file: IDiscoveredFile, cppMode = false): string {
120
+ // Issue #933: Use .hpp extension in C++ mode so C and C++ headers don't overwrite
121
+ const ext = cppMode ? ".hpp" : ".h";
122
+
119
123
  // Use headerOutDir if specified, otherwise fall back to outDir
120
124
  const headerDir = this.config.headerOutDir || this.config.outDir;
121
125
 
@@ -123,7 +127,7 @@ class PathResolver {
123
127
  if (relativePath) {
124
128
  // File is under an input directory - preserve structure (minus basePath)
125
129
  const strippedPath = this.stripBasePath(relativePath);
126
- const outputRelative = strippedPath.replace(/\.cnx$|\.cnext$/, ".h");
130
+ const outputRelative = strippedPath.replace(/\.cnx$|\.cnext$/, ext);
127
131
  const outputPath = join(headerDir, outputRelative);
128
132
 
129
133
  const outputDir = dirname(outputPath);
@@ -142,7 +146,7 @@ class PathResolver {
142
146
  // Only use CWD-relative path if file is under CWD (not starting with ..)
143
147
  if (relativeFromCwd && !relativeFromCwd.startsWith("..")) {
144
148
  const strippedPath = this.stripBasePath(relativeFromCwd);
145
- const outputRelative = strippedPath.replace(/\.cnx$|\.cnext$/, ".h");
149
+ const outputRelative = strippedPath.replace(/\.cnx$|\.cnext$/, ext);
146
150
  const outputPath = join(this.config.headerOutDir, outputRelative);
147
151
 
148
152
  const outputDir = dirname(outputPath);
@@ -154,7 +158,7 @@ class PathResolver {
154
158
  }
155
159
 
156
160
  // File outside CWD: put in headerOutDir with just basename
157
- const headerName = basename(file.path).replace(/\.cnx$|\.cnext$/, ".h");
161
+ const headerName = basename(file.path).replace(/\.cnx$|\.cnext$/, ext);
158
162
  const outputPath = join(this.config.headerOutDir, headerName);
159
163
 
160
164
  if (!this.fs.exists(this.config.headerOutDir)) {
@@ -166,7 +170,7 @@ class PathResolver {
166
170
 
167
171
  // Fallback: output next to the source file (no headerDir specified)
168
172
  // This handles included files that aren't under any input directory
169
- const headerName = basename(file.path).replace(/\.cnx$|\.cnext$/, ".h");
173
+ const headerName = basename(file.path).replace(/\.cnx$|\.cnext$/, ext);
170
174
  return join(dirname(file.path), headerName);
171
175
  }
172
176
 
@@ -324,5 +324,37 @@ describe("PathResolver", () => {
324
324
  // src/main.cnx with basePath "src" -> main.h in headerDir
325
325
  expect(result).toBe(join(headerDir, "main.h"));
326
326
  });
327
+
328
+ // Issue #933: Test C++ mode header extension
329
+ it("generates .hpp path in C++ mode", () => {
330
+ const resolver = new PathResolver({
331
+ inputs: [srcDir],
332
+ outDir,
333
+ });
334
+
335
+ const filePath = join(srcDir, "main.cnx");
336
+ writeFileSync(filePath, "");
337
+ const file = createFile(filePath);
338
+
339
+ const result = resolver.getHeaderOutputPath(file, true);
340
+
341
+ expect(result).toBe(join(outDir, "main.hpp"));
342
+ });
343
+
344
+ it("generates .hpp path in headerOutDir in C++ mode", () => {
345
+ const resolver = new PathResolver({
346
+ inputs: [srcDir],
347
+ outDir,
348
+ headerOutDir: headerDir,
349
+ });
350
+
351
+ const filePath = join(srcDir, "main.cnx");
352
+ writeFileSync(filePath, "");
353
+ const file = createFile(filePath);
354
+
355
+ const result = resolver.getHeaderOutputPath(file, true);
356
+
357
+ expect(result).toBe(join(headerDir, "main.hpp"));
358
+ });
327
359
  });
328
360
  });
@@ -572,6 +572,9 @@ class FunctionCallAnalyzer {
572
572
  );
573
573
  }
574
574
 
575
+ /** Current scope name during callback assignment scanning */
576
+ private scanCurrentScope: string | null = null;
577
+
575
578
  /**
576
579
  * Detect functions assigned to C function pointer typedefs.
577
580
  * When `PointCallback cb <- my_handler;` is found and PointCallback
@@ -582,13 +585,61 @@ class FunctionCallAnalyzer {
582
585
  ): void {
583
586
  for (const decl of tree.declaration()) {
584
587
  const funcDecl = decl.functionDeclaration();
585
- if (!funcDecl) continue;
588
+ if (funcDecl) {
589
+ this.scanStandaloneFunctionForCallbacks(funcDecl);
590
+ continue;
591
+ }
592
+
593
+ const scopeDecl = decl.scopeDeclaration();
594
+ if (scopeDecl) {
595
+ this.scanScopeForCallbacks(scopeDecl);
596
+ }
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Scan a standalone function declaration for callback assignments.
602
+ */
603
+ private scanStandaloneFunctionForCallbacks(
604
+ funcDecl: Parser.FunctionDeclarationContext,
605
+ ): void {
606
+ const block = funcDecl.block();
607
+ if (!block) return;
608
+
609
+ this.scanCurrentScope = null;
610
+ this.scanBlockForCallbackAssignments(block);
611
+ }
586
612
 
587
- const block = funcDecl.block();
588
- if (!block) continue;
613
+ /**
614
+ * Scan all member functions in a scope for callback assignments (Issue #895).
615
+ */
616
+ private scanScopeForCallbacks(
617
+ scopeDecl: Parser.ScopeDeclarationContext,
618
+ ): void {
619
+ const scopeName = scopeDecl.IDENTIFIER().getText();
589
620
 
590
- this.scanBlockForCallbackAssignments(block);
621
+ for (const member of scopeDecl.scopeMember()) {
622
+ this.scanScopeMemberForCallbacks(member, scopeName);
591
623
  }
624
+
625
+ this.scanCurrentScope = null;
626
+ }
627
+
628
+ /**
629
+ * Scan a single scope member for callback assignments.
630
+ */
631
+ private scanScopeMemberForCallbacks(
632
+ member: Parser.ScopeMemberContext,
633
+ scopeName: string,
634
+ ): void {
635
+ const memberFunc = member.functionDeclaration();
636
+ if (!memberFunc) return;
637
+
638
+ const block = memberFunc.block();
639
+ if (!block) return;
640
+
641
+ this.scanCurrentScope = scopeName;
642
+ this.scanBlockForCallbackAssignments(block);
592
643
  }
593
644
 
594
645
  /**
@@ -702,17 +753,41 @@ class FunctionCallAnalyzer {
702
753
 
703
754
  /**
704
755
  * Extract a function reference from an expression context.
705
- * Matches bare identifiers (e.g., "my_handler") and qualified scope
706
- * names (e.g., "MyScope.handler").
756
+ * Matches:
757
+ * - Bare identifiers: "my_handler"
758
+ * - Qualified scope names: "MyScope.handler"
759
+ * - Self-scope reference: "this.handler" (resolved using scanCurrentScope)
760
+ * - Global scope reference: "global.ScopeName.handler"
707
761
  * Returns null if the expression is not a function reference.
708
762
  */
709
763
  private extractFunctionReference(
710
764
  expr: Parser.ExpressionContext,
711
765
  ): string | null {
712
766
  const text = expr.getText();
713
- if (/^\w+(\.\w+)?$/.test(text)) {
767
+
768
+ // Pattern 1: this.member -> CurrentScope.member (Issue #895)
769
+ const thisPattern = /^this\.(\w+)$/;
770
+ const thisMatch = thisPattern.exec(text);
771
+ if (thisMatch) {
772
+ if (!this.scanCurrentScope) {
773
+ return null; // this.member outside scope context
774
+ }
775
+ return this.scanCurrentScope + "." + thisMatch[1];
776
+ }
777
+
778
+ // Pattern 2: global.Scope.member -> Scope.member (Issue #895)
779
+ const globalPattern = /^global\.(\w+)\.(\w+)$/;
780
+ const globalMatch = globalPattern.exec(text);
781
+ if (globalMatch) {
782
+ return globalMatch[1] + "." + globalMatch[2];
783
+ }
784
+
785
+ // Pattern 3: Bare identifier or simple Scope.member
786
+ const simplePattern = /^\w+(\.\w+)?$/;
787
+ if (simplePattern.test(text)) {
714
788
  return text;
715
789
  }
790
+
716
791
  return null;
717
792
  }
718
793
 
@@ -375,18 +375,7 @@ class PassByValueAnalyzer {
375
375
  orExpr: Parser.OrExpressionContext,
376
376
  handler: (unaryExpr: Parser.UnaryExpressionContext) => void,
377
377
  ): void {
378
- orExpr
379
- .andExpression()
380
- .flatMap((and) => and.equalityExpression())
381
- .flatMap((eq) => eq.relationalExpression())
382
- .flatMap((rel) => rel.bitwiseOrExpression())
383
- .flatMap((bor) => bor.bitwiseXorExpression())
384
- .flatMap((bxor) => bxor.bitwiseAndExpression())
385
- .flatMap((band) => band.shiftExpression())
386
- .flatMap((shift) => shift.additiveExpression())
387
- .flatMap((add) => add.multiplicativeExpression())
388
- .flatMap((mul) => mul.unaryExpression())
389
- .forEach(handler);
378
+ ExpressionUtils.collectUnaryFromOrExpr(orExpr).forEach(handler);
390
379
  }
391
380
 
392
381
  /**
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Signed Shift Analyzer
3
+ * Detects shift operators used with signed integer types at compile time
4
+ *
5
+ * MISRA C:2012 Rule 10.1: Operands shall not be of an inappropriate essential type
6
+ * - Left-shifting negative signed values is undefined behavior in C
7
+ * - Right-shifting negative signed values is implementation-defined in C
8
+ *
9
+ * C-Next rejects all shift operations on signed types (i8, i16, i32, i64) at
10
+ * compile time to ensure defined, portable behavior.
11
+ *
12
+ * Two-pass analysis:
13
+ * 1. Collect variable declarations with their types
14
+ * 2. Detect shift operations with signed operands
15
+ */
16
+
17
+ import { ParseTreeWalker } from "antlr4ng";
18
+ import { CNextListener } from "../parser/grammar/CNextListener";
19
+ import * as Parser from "../parser/grammar/CNextParser";
20
+ import ISignedShiftError from "./types/ISignedShiftError";
21
+ import ParserUtils from "../../../utils/ParserUtils";
22
+ import TypeConstants from "../../../utils/constants/TypeConstants";
23
+ import ExpressionUtils from "../../../utils/ExpressionUtils";
24
+
25
+ /**
26
+ * First pass: Collect variable declarations with their types
27
+ */
28
+ class SignedVariableCollector extends CNextListener {
29
+ private readonly signedVars: Set<string> = new Set();
30
+
31
+ public getSignedVars(): Set<string> {
32
+ return this.signedVars;
33
+ }
34
+
35
+ /**
36
+ * Track a typed identifier if it has a signed type
37
+ */
38
+ private trackIfSigned(
39
+ typeCtx: Parser.TypeContext | null,
40
+ identifier: { getText(): string } | null,
41
+ ): void {
42
+ if (!typeCtx) return;
43
+
44
+ const typeName = typeCtx.getText();
45
+ if (!TypeConstants.SIGNED_TYPES.includes(typeName)) return;
46
+
47
+ if (!identifier) return;
48
+
49
+ this.signedVars.add(identifier.getText());
50
+ }
51
+
52
+ /**
53
+ * Track variable declarations with signed types
54
+ */
55
+ override enterVariableDeclaration = (
56
+ ctx: Parser.VariableDeclarationContext,
57
+ ): void => {
58
+ this.trackIfSigned(ctx.type(), ctx.IDENTIFIER());
59
+ };
60
+
61
+ /**
62
+ * Track function parameters with signed types
63
+ */
64
+ override enterParameter = (ctx: Parser.ParameterContext): void => {
65
+ this.trackIfSigned(ctx.type(), ctx.IDENTIFIER());
66
+ };
67
+
68
+ /**
69
+ * Track for-loop variable declarations with signed types
70
+ */
71
+ override enterForVarDecl = (ctx: Parser.ForVarDeclContext): void => {
72
+ this.trackIfSigned(ctx.type(), ctx.IDENTIFIER());
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Second pass: Detect shift operations with signed operands
78
+ */
79
+ class SignedShiftListener extends CNextListener {
80
+ private readonly analyzer: SignedShiftAnalyzer;
81
+
82
+ // eslint-disable-next-line @typescript-eslint/lines-between-class-members
83
+ private readonly signedVars: Set<string>;
84
+
85
+ constructor(analyzer: SignedShiftAnalyzer, signedVars: Set<string>) {
86
+ super();
87
+ this.analyzer = analyzer;
88
+ this.signedVars = signedVars;
89
+ }
90
+
91
+ /**
92
+ * Check shift expressions for signed operands
93
+ * shiftExpression: additiveExpression (('<<' | '>>') additiveExpression)*
94
+ */
95
+ override enterShiftExpression = (
96
+ ctx: Parser.ShiftExpressionContext,
97
+ ): void => {
98
+ const operands = ctx.additiveExpression();
99
+ if (operands.length < 2) return;
100
+
101
+ // Check each operator between additive expressions
102
+ for (let i = 0; i < operands.length - 1; i++) {
103
+ const operatorToken = ctx.getChild(i * 2 + 1);
104
+ if (!operatorToken) continue;
105
+
106
+ const operator = operatorToken.getText();
107
+ if (operator !== "<<" && operator !== ">>") continue;
108
+
109
+ const leftOperand = operands[i];
110
+
111
+ // Check left operand (the value being shifted)
112
+ if (this.isSignedOperand(leftOperand)) {
113
+ const { line, column } = ParserUtils.getPosition(leftOperand);
114
+ this.analyzer.addError(line, column, operator);
115
+ }
116
+ }
117
+ };
118
+
119
+ /**
120
+ * Check if an additive expression contains a signed type operand
121
+ */
122
+ private isSignedOperand(ctx: Parser.AdditiveExpressionContext): boolean {
123
+ // Walk down to unary expressions
124
+ const multExprs = ctx.multiplicativeExpression();
125
+ for (const multExpr of multExprs) {
126
+ const unaryExprs = multExpr.unaryExpression();
127
+ for (const unaryExpr of unaryExprs) {
128
+ if (this.isSignedUnaryExpression(unaryExpr)) {
129
+ return true;
130
+ }
131
+ }
132
+ }
133
+ return false;
134
+ }
135
+
136
+ /**
137
+ * Check if a unary expression is a signed type
138
+ */
139
+ private isSignedUnaryExpression(ctx: Parser.UnaryExpressionContext): boolean {
140
+ // Check for MINUS prefix (negation) - indicates signed context
141
+ // Grammar: unaryExpression: MINUS unaryExpression | ...
142
+ if (ctx.MINUS()) {
143
+ const nestedUnary = ctx.unaryExpression();
144
+ if (nestedUnary) {
145
+ // If negating a literal, it's a negative number (signed)
146
+ const nestedPostfix = nestedUnary.postfixExpression();
147
+ if (nestedPostfix) {
148
+ const nestedPrimary = nestedPostfix.primaryExpression();
149
+ if (nestedPrimary?.literal()) {
150
+ return true;
151
+ }
152
+ }
153
+ // If negating a variable, check if it's signed
154
+ return this.isSignedUnaryExpression(nestedUnary);
155
+ }
156
+ return false;
157
+ }
158
+
159
+ const postfixExpr = ctx.postfixExpression();
160
+ if (!postfixExpr) return false;
161
+
162
+ const primaryExpr = postfixExpr.primaryExpression();
163
+ if (!primaryExpr) return false;
164
+
165
+ // Check for parenthesized expression
166
+ const parenExpr = primaryExpr.expression();
167
+ if (parenExpr) {
168
+ return this.isSignedExpression(parenExpr);
169
+ }
170
+
171
+ // Check for identifier that's a signed variable
172
+ const identifier = primaryExpr.IDENTIFIER();
173
+ if (identifier) {
174
+ return this.signedVars.has(identifier.getText());
175
+ }
176
+
177
+ // Positive integer literals are treated as unsigned
178
+ return false;
179
+ }
180
+
181
+ /**
182
+ * Check if a full expression contains signed operands
183
+ */
184
+ private isSignedExpression(ctx: Parser.ExpressionContext): boolean {
185
+ const ternary = ctx.ternaryExpression();
186
+ if (!ternary) return false;
187
+
188
+ const additiveExprs = ExpressionUtils.collectAdditiveExpressions(ternary);
189
+ return additiveExprs.some((addExpr) => this.isSignedOperand(addExpr));
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Analyzer that detects shift operations on signed integer types
195
+ */
196
+ class SignedShiftAnalyzer {
197
+ private errors: ISignedShiftError[] = [];
198
+
199
+ /**
200
+ * Analyze the parse tree for signed shift operations
201
+ */
202
+ public analyze(tree: Parser.ProgramContext): ISignedShiftError[] {
203
+ this.errors = [];
204
+
205
+ // First pass: collect signed variables
206
+ const collector = new SignedVariableCollector();
207
+ ParseTreeWalker.DEFAULT.walk(collector, tree);
208
+ const signedVars = collector.getSignedVars();
209
+
210
+ // Second pass: detect shift with signed operands
211
+ const listener = new SignedShiftListener(this, signedVars);
212
+ ParseTreeWalker.DEFAULT.walk(listener, tree);
213
+
214
+ return this.errors;
215
+ }
216
+
217
+ /**
218
+ * Add a signed shift error
219
+ */
220
+ public addError(line: number, column: number, operator: string): void {
221
+ this.errors.push({
222
+ code: "E0805",
223
+ line,
224
+ column,
225
+ message: `Shift operator '${operator}' not allowed on signed integer types`,
226
+ helpText:
227
+ "Shift operations on signed integers have undefined (<<) or implementation-defined (>>) behavior. Use unsigned types (u8, u16, u32, u64) for bit manipulation.",
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Get all detected errors
233
+ */
234
+ public getErrors(): ISignedShiftError[] {
235
+ return this.errors;
236
+ }
237
+ }
238
+
239
+ export default SignedShiftAnalyzer;