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.
- package/dist/index.js +617 -474
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/src/transpiler/Transpiler.ts +50 -29
- package/src/transpiler/__tests__/Transpiler.coverage.test.ts +2 -1
- package/src/transpiler/data/PathResolver.ts +10 -6
- package/src/transpiler/data/__tests__/PathResolver.test.ts +32 -0
- package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +82 -7
- package/src/transpiler/logic/analysis/PassByValueAnalyzer.ts +1 -12
- package/src/transpiler/logic/analysis/SignedShiftAnalyzer.ts +239 -0
- package/src/transpiler/logic/analysis/__tests__/FunctionCallAnalyzer.test.ts +92 -0
- package/src/transpiler/logic/analysis/__tests__/SignedShiftAnalyzer.test.ts +414 -0
- package/src/transpiler/logic/analysis/__tests__/StructFieldAnalyzer.test.ts +15 -75
- package/src/transpiler/logic/analysis/__tests__/runAnalyzers.test.ts +3 -15
- package/src/transpiler/logic/analysis/runAnalyzers.ts +11 -2
- package/src/transpiler/logic/analysis/types/ISignedShiftError.ts +15 -0
- package/src/transpiler/logic/symbols/SymbolUtils.ts +4 -1
- package/src/transpiler/logic/symbols/__tests__/SymbolUtils.test.ts +10 -11
- package/src/transpiler/logic/symbols/c/__tests__/CResolver.integration.test.ts +27 -4
- package/src/transpiler/logic/symbols/c/collectors/FunctionCollector.ts +11 -5
- package/src/transpiler/logic/symbols/cpp/__tests__/CppResolver.integration.test.ts +4 -4
- package/src/transpiler/output/codegen/CodeGenerator.ts +169 -11
- package/src/transpiler/output/codegen/__tests__/CodeGenerator.coverage.test.ts +132 -3
- package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +56 -31
- package/src/transpiler/output/codegen/analysis/StringLengthCounter.ts +12 -11
- package/src/transpiler/output/codegen/analysis/__tests__/StringLengthCounter.test.ts +21 -21
- package/src/transpiler/output/codegen/assignment/handlers/AccessPatternHandlers.ts +2 -2
- package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +2 -2
- package/src/transpiler/output/codegen/assignment/handlers/BitmapHandlers.ts +2 -2
- package/src/transpiler/output/codegen/assignment/handlers/RegisterHandlers.ts +4 -4
- package/src/transpiler/output/codegen/assignment/handlers/__tests__/AccessPatternHandlers.test.ts +4 -4
- package/src/transpiler/output/codegen/assignment/handlers/__tests__/BitAccessHandlers.test.ts +8 -8
- package/src/transpiler/output/codegen/assignment/handlers/__tests__/BitmapHandlers.test.ts +5 -5
- package/src/transpiler/output/codegen/assignment/handlers/__tests__/RegisterHandlers.test.ts +4 -4
- package/src/transpiler/output/codegen/generators/expressions/AccessExprGenerator.ts +7 -326
- package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +8 -1
- package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +14 -275
- package/src/transpiler/output/codegen/generators/expressions/UnaryExprGenerator.ts +13 -1
- package/src/transpiler/output/codegen/generators/expressions/__tests__/AccessExprGenerator.test.ts +0 -573
- package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +8 -439
- package/src/transpiler/output/codegen/generators/expressions/__tests__/UnaryExprGenerator.test.ts +196 -0
- package/src/transpiler/output/codegen/helpers/ArgumentGenerator.ts +5 -0
- package/src/transpiler/output/codegen/helpers/ParameterInputAdapter.ts +7 -2
- package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +8 -1
- package/src/transpiler/output/codegen/helpers/TypedefParamParser.ts +34 -0
- package/src/transpiler/output/codegen/helpers/VariableDeclHelper.ts +11 -0
- package/src/transpiler/output/codegen/helpers/VariableModifierBuilder.ts +16 -1
- package/src/transpiler/output/codegen/helpers/__tests__/ParameterInputAdapter.test.ts +48 -0
- package/src/transpiler/output/codegen/helpers/__tests__/TypedefParamParser.test.ts +88 -0
- package/src/transpiler/output/codegen/helpers/__tests__/VariableModifierBuilder.test.ts +34 -2
- package/src/transpiler/output/codegen/types/TTypeInfo.ts +1 -0
- package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +15 -2
- package/src/transpiler/output/headers/__tests__/BaseHeaderGenerator.test.ts +87 -0
- package/src/utils/BitUtils.ts +17 -13
- package/src/utils/ExpressionUtils.ts +51 -0
- package/src/utils/__tests__/BitUtils.test.ts +56 -56
- package/src/utils/types/IParameterSymbol.ts +2 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
1333
|
-
headerSymbol.parameters
|
|
1334
|
-
headerSymbol.parameters.length
|
|
1336
|
+
symbol.kind !== "function" ||
|
|
1337
|
+
!headerSymbol.parameters ||
|
|
1338
|
+
headerSymbol.parameters.length === 0
|
|
1335
1339
|
) {
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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(".
|
|
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$/,
|
|
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$/,
|
|
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$/,
|
|
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$/,
|
|
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 (
|
|
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
|
-
|
|
588
|
-
|
|
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
|
-
|
|
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
|
|
706
|
-
*
|
|
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
|
-
|
|
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;
|