c-next 0.2.16 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/dist/index.js +8897 -6260
- package/dist/index.js.map +4 -4
- package/grammar/CNext.g4 +12 -0
- package/package.json +4 -2
- package/src/transpiler/Transpiler.ts +376 -48
- package/src/transpiler/__tests__/DualCodePaths.test.ts +1 -1
- package/src/transpiler/__tests__/compileCommandsDiscovery.integration.test.ts +94 -0
- package/src/transpiler/__tests__/externalSymbolRecovery.integration.test.ts +215 -0
- package/src/transpiler/logic/__tests__/detectAssemblySyntax.test.ts +116 -0
- package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +65 -10
- package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +186 -14
- package/src/transpiler/logic/analysis/MixedTypeCategoryAnalyzer.ts +408 -0
- package/src/transpiler/logic/analysis/PassByValueAnalyzer.ts +16 -97
- package/src/transpiler/logic/analysis/ReturnPathAnalyzer.ts +171 -0
- package/src/transpiler/logic/analysis/SignedShiftAnalyzer.ts +124 -12
- package/src/transpiler/logic/analysis/__tests__/FunctionCallAnalyzer.test.ts +200 -0
- package/src/transpiler/logic/analysis/__tests__/InitializationAnalyzer.test.ts +386 -1
- package/src/transpiler/logic/analysis/__tests__/MixedTypeCategoryAnalyzer.test.ts +346 -0
- package/src/transpiler/logic/analysis/__tests__/ReturnPathAnalyzer.test.ts +232 -0
- package/src/transpiler/logic/analysis/__tests__/SignedShiftAnalyzer.test.ts +211 -0
- package/src/transpiler/logic/analysis/runAnalyzers.ts +23 -2
- package/src/transpiler/logic/analysis/types/IMixedTypeCategoryError.ts +17 -0
- package/src/transpiler/logic/analysis/types/IReturnPathError.ts +12 -0
- package/src/transpiler/logic/detectAssemblySyntax.ts +80 -0
- package/src/transpiler/logic/parser/grammar/CNext.interp +4 -1
- package/src/transpiler/logic/parser/grammar/CNext.tokens +169 -167
- package/src/transpiler/logic/parser/grammar/CNextLexer.interp +4 -1
- package/src/transpiler/logic/parser/grammar/CNextLexer.tokens +169 -167
- package/src/transpiler/logic/parser/grammar/CNextLexer.ts +545 -541
- package/src/transpiler/logic/parser/grammar/CNextListener.ts +11 -0
- package/src/transpiler/logic/parser/grammar/CNextParser.ts +1318 -1178
- package/src/transpiler/logic/parser/grammar/CNextVisitor.ts +7 -0
- package/src/transpiler/logic/preprocessor/CompileCommandsReader.ts +332 -0
- package/src/transpiler/logic/preprocessor/ExternalDeclarationOracle.ts +202 -0
- package/src/transpiler/logic/preprocessor/Preprocessor.ts +24 -6
- package/src/transpiler/logic/preprocessor/ToolchainDetector.ts +42 -1
- package/src/transpiler/logic/preprocessor/__tests__/CompileCommandsReader.test.ts +216 -0
- package/src/transpiler/logic/preprocessor/__tests__/ExternalDeclarationOracle.test.ts +153 -0
- package/src/transpiler/logic/preprocessor/__tests__/Preprocessor.test.ts +43 -18
- package/src/transpiler/logic/preprocessor/__tests__/ToolchainDetector.crossCompiler.test.ts +75 -0
- package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/dependent.h +7 -0
- package/src/transpiler/logic/preprocessor/__tests__/fixtures/oracle/predecessor.h +5 -0
- package/src/transpiler/logic/preprocessor/types/ICompileCommandsResult.ts +14 -0
- package/src/transpiler/logic/preprocessor/types/IPreprocessOptions.ts +17 -0
- package/src/transpiler/logic/symbols/SymbolTable.ts +62 -2
- package/src/transpiler/logic/symbols/__tests__/SymbolTable.test.ts +55 -4
- package/src/transpiler/logic/symbols/cnext/collectors/VariableCollector.ts +15 -2
- package/src/transpiler/logic/symbols/cnext/utils/TypeUtils.ts +64 -50
- package/src/transpiler/output/MisraSuppressionUtils.ts +52 -0
- package/src/transpiler/output/__tests__/MisraSuppressionUtils.test.ts +67 -0
- package/src/transpiler/output/codegen/CodeGenerator.ts +196 -98
- package/src/transpiler/output/codegen/TypeResolver.ts +148 -0
- package/src/transpiler/output/codegen/TypeValidator.ts +179 -81
- package/src/transpiler/output/codegen/__tests__/CodeGenerator.coverage.test.ts +165 -17
- package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +163 -35
- package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +2 -2
- package/src/transpiler/output/codegen/__tests__/TypeResolver.test.ts +93 -0
- package/src/transpiler/output/codegen/__tests__/TypeValidator.alwaysTrueLoop.test.ts +91 -0
- package/src/transpiler/output/codegen/__tests__/TypeValidator.test.ts +97 -14
- package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +13 -0
- package/src/transpiler/output/codegen/assignment/AssignmentKind.ts +1 -1
- package/src/transpiler/output/codegen/assignment/handlers/AccessPatternHandlers.ts +20 -12
- package/src/transpiler/output/codegen/assignment/handlers/ArrayHandlers.ts +424 -22
- package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +31 -18
- package/src/transpiler/output/codegen/assignment/handlers/BitmapHandlers.ts +7 -8
- package/src/transpiler/output/codegen/assignment/handlers/RegisterHandlers.ts +6 -8
- package/src/transpiler/output/codegen/assignment/handlers/RegisterUtils.ts +12 -10
- package/src/transpiler/output/codegen/assignment/handlers/SimpleHandler.ts +3 -7
- package/src/transpiler/output/codegen/assignment/handlers/SpecialHandlers.ts +7 -9
- package/src/transpiler/output/codegen/assignment/handlers/StringHandlers.ts +12 -10
- package/src/transpiler/output/codegen/assignment/handlers/__tests__/ArrayHandlers.test.ts +479 -11
- package/src/transpiler/output/codegen/generators/IOrchestrator.ts +3 -0
- package/src/transpiler/output/codegen/generators/declarationGenerators/ScopeGenerator.ts +26 -7
- package/src/transpiler/output/codegen/generators/declarationGenerators/__tests__/ScopeGenerator.test.ts +86 -0
- package/src/transpiler/output/codegen/generators/expressions/BinaryExprGenerator.ts +43 -24
- package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +76 -58
- package/src/transpiler/output/codegen/generators/expressions/ExpressionGenerator.ts +9 -2
- package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +26 -13
- package/src/transpiler/output/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts +44 -0
- package/src/transpiler/output/codegen/generators/expressions/__tests__/ExpressionGenerator.test.ts +82 -1
- package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +129 -1
- package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +64 -8
- package/src/transpiler/output/codegen/generators/statements/__tests__/ControlFlowGenerator.test.ts +8 -5
- package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +30 -2
- package/src/transpiler/output/codegen/helpers/ParameterInputAdapter.ts +17 -3
- package/src/transpiler/output/codegen/helpers/ParameterSignatureBuilder.ts +17 -4
- package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +240 -42
- package/src/transpiler/output/codegen/helpers/SymbolLookupHelper.ts +0 -21
- package/src/transpiler/output/codegen/helpers/TypeGenerationHelper.ts +60 -39
- package/src/transpiler/output/codegen/helpers/TypeRegistrationEngine.ts +170 -36
- package/src/transpiler/output/codegen/helpers/VariableDeclHelper.ts +37 -39
- package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +19 -0
- package/src/transpiler/output/codegen/helpers/__tests__/ParameterInputAdapter.test.ts +117 -0
- package/src/transpiler/output/codegen/helpers/__tests__/ParameterSignatureBuilder.test.ts +94 -2
- package/src/transpiler/output/codegen/helpers/__tests__/StringDeclHelper.test.ts +322 -9
- package/src/transpiler/output/codegen/helpers/__tests__/SymbolLookupHelper.test.ts +0 -64
- package/src/transpiler/output/codegen/helpers/__tests__/TypeRegistrationEngine.test.ts +101 -0
- package/src/transpiler/output/codegen/helpers/__tests__/VariableDeclHelper.test.ts +29 -5
- package/src/transpiler/output/codegen/subscript/SubscriptClassifier.ts +30 -11
- package/src/transpiler/output/codegen/subscript/TSubscriptKind.ts +1 -1
- package/src/transpiler/output/codegen/subscript/__tests__/SubscriptClassifier.test.ts +38 -6
- package/src/transpiler/output/codegen/types/ICallbackTypeInfo.ts +2 -1
- package/src/transpiler/output/codegen/types/IParameterInput.ts +7 -0
- package/src/transpiler/output/headers/BaseHeaderGenerator.ts +8 -0
- package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +140 -24
- package/src/transpiler/output/headers/__tests__/HeaderGeneratorUtils.test.ts +280 -0
- package/src/transpiler/output/headers/generators/IHeaderTypeInput.ts +13 -0
- package/src/transpiler/output/headers/generators/generateStructHeader.ts +48 -28
- package/src/transpiler/state/CodeGenState.ts +91 -22
- package/src/transpiler/state/__tests__/CodeGenState.test.ts +253 -11
- package/src/transpiler/types/ICachedFileEntry.ts +7 -0
- package/src/utils/LiteralUtils.ts +23 -0
- package/src/utils/__tests__/LiteralUtils.test.ts +101 -0
- package/src/utils/cache/CacheManager.ts +13 -2
- package/src/utils/cache/__tests__/CacheManager.test.ts +18 -1
- package/src/utils/constants/TypeConstants.ts +13 -0
- package/src/utils/types/IParameterSymbol.ts +1 -0
|
@@ -157,10 +157,32 @@ class InitializationListener extends CNextListener {
|
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
//
|
|
160
|
+
// Resolve the assignment target ONCE (single source of truth)
|
|
161
|
+
const target = this._resolveAssignmentTarget(baseId, postfixOps);
|
|
162
|
+
|
|
163
|
+
// Issue #1012: Compound assignment reads the LHS before writing.
|
|
164
|
+
// Check if the assignment operator is a compound operator (not simple `<-`).
|
|
165
|
+
const isCompoundAssignment = ctx.assignmentOperator().ASSIGN() === null;
|
|
166
|
+
if (isCompoundAssignment) {
|
|
167
|
+
const { line, column } = ParserUtils.getPosition(ctx);
|
|
168
|
+
this.analyzer.checkRead(target.varName, line, column, target.fieldName);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Record the assignment to the resolved target
|
|
172
|
+
this.analyzer.recordAssignment(target.varName, target.fieldName);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resolve an assignment target to its variable name and optional field name.
|
|
177
|
+
* Single classification path used by both read checks and assignment recording.
|
|
178
|
+
*/
|
|
179
|
+
private _resolveAssignmentTarget(
|
|
180
|
+
baseId: string,
|
|
181
|
+
postfixOps: Parser.PostfixTargetOpContext[],
|
|
182
|
+
): { varName: string; fieldName?: string } {
|
|
183
|
+
// Simple variable: x <- value (no postfix ops)
|
|
161
184
|
if (postfixOps.length === 0) {
|
|
162
|
-
|
|
163
|
-
return;
|
|
185
|
+
return { varName: baseId };
|
|
164
186
|
}
|
|
165
187
|
|
|
166
188
|
// Analyze postfix operations
|
|
@@ -168,15 +190,13 @@ class InitializationListener extends CNextListener {
|
|
|
168
190
|
|
|
169
191
|
// Member access: p.x <- value (struct field)
|
|
170
192
|
if (identifiers.length >= 2 && !hasSubscript) {
|
|
171
|
-
|
|
172
|
-
const fieldName = identifiers[1];
|
|
173
|
-
this.analyzer.recordAssignment(varName, fieldName);
|
|
174
|
-
} else {
|
|
175
|
-
// Array access or mixed: arr[i] <- value or arr[i].field <- value
|
|
176
|
-
// Consider the array/base as a whole initialized
|
|
177
|
-
this.analyzer.recordAssignment(baseId);
|
|
193
|
+
return { varName: identifiers[0], fieldName: identifiers[1] };
|
|
178
194
|
}
|
|
179
|
-
|
|
195
|
+
|
|
196
|
+
// Array access or mixed: arr[i] <- value or arr[i].field <- value
|
|
197
|
+
// Consider the array/base as a whole
|
|
198
|
+
return { varName: baseId };
|
|
199
|
+
}
|
|
180
200
|
|
|
181
201
|
// ========================================================================
|
|
182
202
|
// Function Call Arguments (ADR-006: pass-by-reference may initialize)
|
|
@@ -526,14 +546,159 @@ class InitializationAnalyzer {
|
|
|
526
546
|
|
|
527
547
|
/**
|
|
528
548
|
* Process scope member variable declarations (ADR-016)
|
|
549
|
+
* Issue #1019: Scope members require explicit initialization like locals
|
|
529
550
|
*/
|
|
530
551
|
private _processScopeMembers(decl: Parser.DeclarationContext): void {
|
|
531
552
|
const scopeDecl = decl.scopeDeclaration();
|
|
532
553
|
if (!scopeDecl) return;
|
|
533
554
|
|
|
534
555
|
const scopeName = scopeDecl.IDENTIFIER().getText();
|
|
556
|
+
|
|
557
|
+
// Phase 1: Find all members assigned in any scope function
|
|
558
|
+
const assignedMembers = this._findAssignedScopeMembers(scopeDecl);
|
|
559
|
+
|
|
560
|
+
// Phase 2: Process each member with assignment info
|
|
561
|
+
for (const member of scopeDecl.scopeMember()) {
|
|
562
|
+
this._processScopeMemberVariable(member, scopeName, assignedMembers);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Scan all functions in a scope to find which members are assigned.
|
|
568
|
+
* Issue #1019: A member assigned in ANY function is considered initialized
|
|
569
|
+
* for reads in other functions within the same scope.
|
|
570
|
+
*/
|
|
571
|
+
private _findAssignedScopeMembers(
|
|
572
|
+
scopeDecl: Parser.ScopeDeclarationContext,
|
|
573
|
+
): Set<string> {
|
|
574
|
+
const assigned = new Set<string>();
|
|
575
|
+
|
|
535
576
|
for (const member of scopeDecl.scopeMember()) {
|
|
536
|
-
|
|
577
|
+
const funcDecl = member.functionDeclaration();
|
|
578
|
+
if (!funcDecl) continue;
|
|
579
|
+
|
|
580
|
+
const body = funcDecl.block();
|
|
581
|
+
if (!body) continue;
|
|
582
|
+
|
|
583
|
+
// Scan the function body for assignments to scope members
|
|
584
|
+
this._collectAssignmentsInBlock(body, assigned);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return assigned;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Recursively collect variable names that are assigned in a block.
|
|
592
|
+
* Looks for assignment statements targeting bare identifiers or this.member.
|
|
593
|
+
*/
|
|
594
|
+
private _collectAssignmentsInBlock(
|
|
595
|
+
block: Parser.BlockContext,
|
|
596
|
+
assigned: Set<string>,
|
|
597
|
+
): void {
|
|
598
|
+
for (const stmt of block.statement()) {
|
|
599
|
+
this._collectAssignmentsInStatement(stmt, assigned);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Collect assignments from a single statement, recursing into nested blocks.
|
|
605
|
+
*/
|
|
606
|
+
private _collectAssignmentsInStatement(
|
|
607
|
+
stmt: Parser.StatementContext,
|
|
608
|
+
assigned: Set<string>,
|
|
609
|
+
): void {
|
|
610
|
+
this._collectDirectAssignment(stmt, assigned);
|
|
611
|
+
this._collectFromControlFlow(stmt, assigned);
|
|
612
|
+
this._collectFromSwitch(stmt, assigned);
|
|
613
|
+
this._collectFromBlock(stmt, assigned);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Collect assignment from the statement itself (if it's an assignment).
|
|
618
|
+
*/
|
|
619
|
+
private _collectDirectAssignment(
|
|
620
|
+
stmt: Parser.StatementContext,
|
|
621
|
+
assigned: Set<string>,
|
|
622
|
+
): void {
|
|
623
|
+
const assignStmt = stmt.assignmentStatement();
|
|
624
|
+
if (!assignStmt) return;
|
|
625
|
+
|
|
626
|
+
const target = assignStmt.assignmentTarget();
|
|
627
|
+
if (!target) return;
|
|
628
|
+
|
|
629
|
+
// Both bare identifier and this.member use the same IDENTIFIER token
|
|
630
|
+
const id = target.IDENTIFIER()?.getText();
|
|
631
|
+
if (id) {
|
|
632
|
+
assigned.add(id);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Recurse into control flow statements (if, while, do-while, for).
|
|
638
|
+
*/
|
|
639
|
+
private _collectFromControlFlow(
|
|
640
|
+
stmt: Parser.StatementContext,
|
|
641
|
+
assigned: Set<string>,
|
|
642
|
+
): void {
|
|
643
|
+
// if statement
|
|
644
|
+
const ifStmt = stmt.ifStatement();
|
|
645
|
+
if (ifStmt) {
|
|
646
|
+
for (const childStmt of ifStmt.statement()) {
|
|
647
|
+
this._collectAssignmentsInStatement(childStmt, assigned);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// while statement
|
|
652
|
+
const whileBody = stmt.whileStatement()?.statement();
|
|
653
|
+
if (whileBody) {
|
|
654
|
+
this._collectAssignmentsInStatement(whileBody, assigned);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// do-while statement (Issue #1019 review feedback)
|
|
658
|
+
const doWhileBody = stmt.doWhileStatement()?.block();
|
|
659
|
+
if (doWhileBody) {
|
|
660
|
+
this._collectAssignmentsInBlock(doWhileBody, assigned);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// for statement
|
|
664
|
+
const forBody = stmt.forStatement()?.statement();
|
|
665
|
+
if (forBody) {
|
|
666
|
+
this._collectAssignmentsInStatement(forBody, assigned);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Recurse into switch statement cases.
|
|
672
|
+
*/
|
|
673
|
+
private _collectFromSwitch(
|
|
674
|
+
stmt: Parser.StatementContext,
|
|
675
|
+
assigned: Set<string>,
|
|
676
|
+
): void {
|
|
677
|
+
const switchStmt = stmt.switchStatement();
|
|
678
|
+
if (!switchStmt) return;
|
|
679
|
+
|
|
680
|
+
for (const switchCase of switchStmt.switchCase()) {
|
|
681
|
+
const caseBlock = switchCase.block();
|
|
682
|
+
if (caseBlock) {
|
|
683
|
+
this._collectAssignmentsInBlock(caseBlock, assigned);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const defaultBlock = switchStmt.defaultCase()?.block();
|
|
687
|
+
if (defaultBlock) {
|
|
688
|
+
this._collectAssignmentsInBlock(defaultBlock, assigned);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Recurse into standalone block statement.
|
|
694
|
+
*/
|
|
695
|
+
private _collectFromBlock(
|
|
696
|
+
stmt: Parser.StatementContext,
|
|
697
|
+
assigned: Set<string>,
|
|
698
|
+
): void {
|
|
699
|
+
const block = stmt.block();
|
|
700
|
+
if (block) {
|
|
701
|
+
this._collectAssignmentsInBlock(block, assigned);
|
|
537
702
|
}
|
|
538
703
|
}
|
|
539
704
|
|
|
@@ -543,6 +708,7 @@ class InitializationAnalyzer {
|
|
|
543
708
|
private _processScopeMemberVariable(
|
|
544
709
|
member: Parser.ScopeMemberContext,
|
|
545
710
|
scopeName: string,
|
|
711
|
+
assignedMembers: Set<string>,
|
|
546
712
|
): void {
|
|
547
713
|
const memberVar = member.variableDeclaration();
|
|
548
714
|
if (!memberVar) return;
|
|
@@ -552,9 +718,15 @@ class InitializationAnalyzer {
|
|
|
552
718
|
const { line, column } = ParserUtils.getPosition(memberVar);
|
|
553
719
|
const typeName = this._extractUserTypeName(memberVar.type());
|
|
554
720
|
|
|
721
|
+
// Issue #1019: Scope members are initialized if they have an inline
|
|
722
|
+
// initializer OR are assigned in any function within the scope
|
|
723
|
+
const hasInlineInit = memberVar.expression() !== null;
|
|
724
|
+
const isAssignedInScope = assignedMembers.has(varName);
|
|
725
|
+
const hasInitializer = hasInlineInit || isAssignedInScope;
|
|
726
|
+
|
|
555
727
|
// Register with both raw name and transpiled C name for scope resolution
|
|
556
|
-
this.declareVariable(varName, line, column,
|
|
557
|
-
this.declareVariable(fullName, line, column,
|
|
728
|
+
this.declareVariable(varName, line, column, hasInitializer, typeName);
|
|
729
|
+
this.declareVariable(fullName, line, column, hasInitializer, typeName);
|
|
558
730
|
}
|
|
559
731
|
|
|
560
732
|
/**
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mixed Type Category Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Detects binary operators whose two operands have different essential type
|
|
5
|
+
* categories (signed vs unsigned) at compile time.
|
|
6
|
+
*
|
|
7
|
+
* MISRA C:2012 Rule 10.4: "Both operands of an operator in which the usual
|
|
8
|
+
* arithmetic conversions are performed shall have the same essential type
|
|
9
|
+
* category." Combining a signed and an unsigned value (e.g. `u32 + i32`) relies
|
|
10
|
+
* on C's usual arithmetic conversions, whose result can be surprising (ADR-024).
|
|
11
|
+
*
|
|
12
|
+
* To combine values of different categories, the developer reinterprets one
|
|
13
|
+
* operand's bits to the other's category with bit indexing (ADR-007), e.g.
|
|
14
|
+
* `a + b[0, 32]`, making the conversion explicit.
|
|
15
|
+
*
|
|
16
|
+
* Integer literals are exempt: a bare literal has no fixed essential category —
|
|
17
|
+
* it is contextually typed to the other operand (ADR-052), so `a + 5` is fine.
|
|
18
|
+
* The rule fires only when BOTH operands resolve to concrete, fixed-width
|
|
19
|
+
* integer types of different category.
|
|
20
|
+
*
|
|
21
|
+
* Two-pass analysis:
|
|
22
|
+
* 1. Collect declarations into per-scope frames (function, named scope, block,
|
|
23
|
+
* and for-loop header), so a name is resolved against ITS scope — a same-named
|
|
24
|
+
* variable of a different category in another function OR a nested block never
|
|
25
|
+
* poisons the lookup (Issue #1085 review).
|
|
26
|
+
* 2. Walk each binary-operator level and compare adjacent operand categories,
|
|
27
|
+
* resolving each operand within its enclosing scope frame.
|
|
28
|
+
*
|
|
29
|
+
* Note: shift operators (<< / >>) are intentionally NOT checked here — MISRA
|
|
30
|
+
* Rule 10.4 only governs operators subject to the usual arithmetic conversions,
|
|
31
|
+
* and a shift count is promoted independently. A signed shift count is a Rule
|
|
32
|
+
* 10.1 concern handled elsewhere (Issue #1085 review).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { ParseTreeWalker, ParserRuleContext } from "antlr4ng";
|
|
36
|
+
import { CNextListener } from "../parser/grammar/CNextListener";
|
|
37
|
+
import * as Parser from "../parser/grammar/CNextParser";
|
|
38
|
+
import IMixedTypeCategoryError from "./types/IMixedTypeCategoryError";
|
|
39
|
+
import ParserUtils from "../../../utils/ParserUtils";
|
|
40
|
+
import TypeConstants from "../../../utils/constants/TypeConstants";
|
|
41
|
+
|
|
42
|
+
/** Essential type category of an operand, or null when it cannot be resolved. */
|
|
43
|
+
type Category = "signed" | "unsigned" | null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Declarations directly in one lexical scope (a function, named scope, block, or
|
|
47
|
+
* for-loop header), with a link to the enclosing scope. Resolution searches
|
|
48
|
+
* outward to the global frame, so inner declarations shadow outer ones.
|
|
49
|
+
*/
|
|
50
|
+
interface ScopeFrame {
|
|
51
|
+
readonly vars: Map<string, string>;
|
|
52
|
+
readonly parent: ScopeFrame | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* First pass: build per-scope frames. Frames are anchored to the function /
|
|
57
|
+
* scope context node so the second pass can find an operand's frame by walking
|
|
58
|
+
* up its parent chain — no shared walk state between the passes.
|
|
59
|
+
*/
|
|
60
|
+
class ScopeCollector extends CNextListener {
|
|
61
|
+
private readonly globalFrame: ScopeFrame = { vars: new Map(), parent: null };
|
|
62
|
+
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/lines-between-class-members
|
|
64
|
+
private readonly frameOf: Map<ParserRuleContext, ScopeFrame> = new Map();
|
|
65
|
+
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/lines-between-class-members
|
|
67
|
+
private readonly stack: ScopeFrame[] = [this.globalFrame];
|
|
68
|
+
|
|
69
|
+
public getGlobalFrame(): ScopeFrame {
|
|
70
|
+
return this.globalFrame;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public getFrameOf(): Map<ParserRuleContext, ScopeFrame> {
|
|
74
|
+
return this.frameOf;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private top(): ScopeFrame {
|
|
78
|
+
return this.stack.at(-1) ?? this.globalFrame;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private pushFrame(node: ParserRuleContext): void {
|
|
82
|
+
const frame: ScopeFrame = { vars: new Map(), parent: this.top() };
|
|
83
|
+
this.frameOf.set(node, frame);
|
|
84
|
+
this.stack.push(frame);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private popFrame(): void {
|
|
88
|
+
this.stack.pop();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private record(
|
|
92
|
+
typeCtx: Parser.TypeContext | null,
|
|
93
|
+
identifier: { getText(): string } | null,
|
|
94
|
+
): void {
|
|
95
|
+
if (!typeCtx || !identifier) return;
|
|
96
|
+
this.top().vars.set(identifier.getText(), typeCtx.getText());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
override enterFunctionDeclaration = (
|
|
100
|
+
ctx: Parser.FunctionDeclarationContext,
|
|
101
|
+
): void => {
|
|
102
|
+
this.pushFrame(ctx);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
override exitFunctionDeclaration = (): void => {
|
|
106
|
+
this.popFrame();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
override enterScopeDeclaration = (
|
|
110
|
+
ctx: Parser.ScopeDeclarationContext,
|
|
111
|
+
): void => {
|
|
112
|
+
this.pushFrame(ctx);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
override exitScopeDeclaration = (): void => {
|
|
116
|
+
this.popFrame();
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
override enterVariableDeclaration = (
|
|
120
|
+
ctx: Parser.VariableDeclarationContext,
|
|
121
|
+
): void => {
|
|
122
|
+
this.record(ctx.type(), ctx.IDENTIFIER());
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
override enterParameter = (ctx: Parser.ParameterContext): void => {
|
|
126
|
+
this.record(ctx.type(), ctx.IDENTIFIER());
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
override enterForVarDecl = (ctx: Parser.ForVarDeclContext): void => {
|
|
130
|
+
this.record(ctx.type(), ctx.IDENTIFIER());
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Each braced block (if/while/for body, and a function/scope body) is its own
|
|
134
|
+
// lexical scope, so a different-category redeclaration shadows only within the
|
|
135
|
+
// block instead of poisoning the name function-wide (Issue #1085 review).
|
|
136
|
+
override enterBlock = (ctx: Parser.BlockContext): void => {
|
|
137
|
+
this.pushFrame(ctx);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
override exitBlock = (): void => {
|
|
141
|
+
this.popFrame();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// The for-loop header is its own scope so the loop variable is confined to the
|
|
145
|
+
// loop (header + body) and never overwrites an outer same-named variable.
|
|
146
|
+
override enterForStatement = (ctx: Parser.ForStatementContext): void => {
|
|
147
|
+
this.pushFrame(ctx);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
override exitForStatement = (): void => {
|
|
151
|
+
this.popFrame();
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Second pass: detect binary operators combining mixed essential categories.
|
|
157
|
+
*/
|
|
158
|
+
class MixedCategoryListener extends CNextListener {
|
|
159
|
+
private readonly analyzer: MixedTypeCategoryAnalyzer;
|
|
160
|
+
|
|
161
|
+
// eslint-disable-next-line @typescript-eslint/lines-between-class-members
|
|
162
|
+
private readonly globalFrame: ScopeFrame;
|
|
163
|
+
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/lines-between-class-members
|
|
165
|
+
private readonly frameOf: Map<ParserRuleContext, ScopeFrame>;
|
|
166
|
+
|
|
167
|
+
constructor(
|
|
168
|
+
analyzer: MixedTypeCategoryAnalyzer,
|
|
169
|
+
globalFrame: ScopeFrame,
|
|
170
|
+
frameOf: Map<ParserRuleContext, ScopeFrame>,
|
|
171
|
+
) {
|
|
172
|
+
super();
|
|
173
|
+
this.analyzer = analyzer;
|
|
174
|
+
this.globalFrame = globalFrame;
|
|
175
|
+
this.frameOf = frameOf;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The scope frame enclosing an operand: nearest function/scope ancestor. */
|
|
179
|
+
private frameFor(ctx: ParserRuleContext): ScopeFrame {
|
|
180
|
+
let node: ParserRuleContext | null = ctx;
|
|
181
|
+
while (node) {
|
|
182
|
+
const frame = this.frameOf.get(node);
|
|
183
|
+
if (frame) return frame;
|
|
184
|
+
node = node.parent;
|
|
185
|
+
}
|
|
186
|
+
return this.globalFrame;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Map a known variable name to its essential type category within a scope. */
|
|
190
|
+
private categoryOfName(name: string, frame: ScopeFrame): Category {
|
|
191
|
+
let current: ScopeFrame | null = frame;
|
|
192
|
+
while (current) {
|
|
193
|
+
const typeName = current.vars.get(name);
|
|
194
|
+
if (typeName) {
|
|
195
|
+
if (TypeConstants.SIGNED_TYPES.includes(typeName)) return "signed";
|
|
196
|
+
if (TypeConstants.UNSIGNED_INT_TYPES.includes(typeName)) {
|
|
197
|
+
return "unsigned";
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
current = current.parent;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Collect the essential category of every classifiable VALUE leaf under one
|
|
208
|
+
* binary-operator operand, descending through nested operator levels and
|
|
209
|
+
* parentheses but never into a postfix suffix (an array index, bit-range
|
|
210
|
+
* argument, or call argument is not a value operand of THIS operator).
|
|
211
|
+
*
|
|
212
|
+
* A unary expression is a grammar leaf of the operator levels:
|
|
213
|
+
* - prefix `-`/`~` preserve the operand's category, so descend through them;
|
|
214
|
+
* - prefix `!` (essentially-Boolean result) and `&` (address-of, ADR-006)
|
|
215
|
+
* carry no signed/unsigned category — contribute null, so a mix like
|
|
216
|
+
* `!a = !b` is not falsely rejected (Issue #1085 review);
|
|
217
|
+
* - a postfix WITH a suffix (member/call/indexing/bit-extraction) cannot be
|
|
218
|
+
* positively classified — contribute null, which exempts the sanctioned
|
|
219
|
+
* cross-category form `x[0, 32]`;
|
|
220
|
+
* - a parenthesized expression contributes ALL of its own leaves (not merely
|
|
221
|
+
* the leftmost), so a compound operand is judged by its whole content.
|
|
222
|
+
*/
|
|
223
|
+
private collectOperandCategories(
|
|
224
|
+
ctx: ParserRuleContext,
|
|
225
|
+
frame: ScopeFrame,
|
|
226
|
+
out: Category[],
|
|
227
|
+
): void {
|
|
228
|
+
if (ctx instanceof Parser.UnaryExpressionContext) {
|
|
229
|
+
const inner = ctx.unaryExpression();
|
|
230
|
+
if (inner) {
|
|
231
|
+
const op = ctx.getChild(0)?.getText();
|
|
232
|
+
if (op === "!" || op === "&") {
|
|
233
|
+
out.push(null);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
this.collectOperandCategories(inner, frame, out);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const postfix = ctx.postfixExpression();
|
|
241
|
+
if (!postfix || postfix.getChildCount() > 1) {
|
|
242
|
+
out.push(null);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const primary = postfix.primaryExpression();
|
|
247
|
+
const parenthesized = primary?.expression();
|
|
248
|
+
if (parenthesized) {
|
|
249
|
+
this.collectOperandCategories(parenthesized, frame, out);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const identifier = primary?.IDENTIFIER();
|
|
254
|
+
out.push(
|
|
255
|
+
identifier ? this.categoryOfName(identifier.getText(), frame) : null,
|
|
256
|
+
);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (let i = 0; i < ctx.getChildCount(); i += 1) {
|
|
261
|
+
const child = ctx.getChild(i);
|
|
262
|
+
if (child instanceof ParserRuleContext) {
|
|
263
|
+
this.collectOperandCategories(child, frame, out);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The essential category of one operand of a binary-operator level: the single
|
|
270
|
+
* category shared by all its classifiable value leaves, or null when it has
|
|
271
|
+
* none OR when its own leaves are themselves mixed.
|
|
272
|
+
*
|
|
273
|
+
* Returning null for an internally-mixed operand prevents a CASCADE of
|
|
274
|
+
* duplicate errors: `a * b + c` (with `i32 a`, `u32 b`, `u32 c`) is reported
|
|
275
|
+
* once — at the `a * b` level — instead of again at the `+ c` level, where the
|
|
276
|
+
* product's category is genuinely ambiguous rather than `a`'s leftmost
|
|
277
|
+
* (Issue #1085 review). An internally-mixed operand is always reported at its
|
|
278
|
+
* own level, so nothing is missed. Because a resolved (non-null) category
|
|
279
|
+
* means every classifiable leaf agrees, comparing two resolved-but-differing
|
|
280
|
+
* operands always reflects a real signed/unsigned combination — no false
|
|
281
|
+
* positive on uniform code.
|
|
282
|
+
*/
|
|
283
|
+
private operandCategory(ctx: ParserRuleContext, frame: ScopeFrame): Category {
|
|
284
|
+
const leaves: Category[] = [];
|
|
285
|
+
this.collectOperandCategories(ctx, frame, leaves);
|
|
286
|
+
|
|
287
|
+
let resolved: Category = null;
|
|
288
|
+
for (const leaf of leaves) {
|
|
289
|
+
if (leaf === null) continue;
|
|
290
|
+
if (resolved === null) {
|
|
291
|
+
resolved = leaf;
|
|
292
|
+
} else if (resolved !== leaf) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return resolved;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Compare adjacent operands at one binary-operator level and report any pair
|
|
301
|
+
* whose categories are both resolved and differ.
|
|
302
|
+
*/
|
|
303
|
+
private checkLevel(operands: ParserRuleContext[]): void {
|
|
304
|
+
if (operands.length < 2) return;
|
|
305
|
+
const frame = this.frameFor(operands[0]);
|
|
306
|
+
for (let i = 0; i < operands.length - 1; i += 1) {
|
|
307
|
+
const left = this.operandCategory(operands[i], frame);
|
|
308
|
+
const right = this.operandCategory(operands[i + 1], frame);
|
|
309
|
+
if (left && right && left !== right) {
|
|
310
|
+
const { line, column } = ParserUtils.getPosition(operands[i + 1]);
|
|
311
|
+
this.analyzer.addError(line, column);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
override enterMultiplicativeExpression = (
|
|
317
|
+
ctx: Parser.MultiplicativeExpressionContext,
|
|
318
|
+
): void => {
|
|
319
|
+
this.checkLevel(ctx.unaryExpression());
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
override enterAdditiveExpression = (
|
|
323
|
+
ctx: Parser.AdditiveExpressionContext,
|
|
324
|
+
): void => {
|
|
325
|
+
this.checkLevel(ctx.multiplicativeExpression());
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
override enterBitwiseAndExpression = (
|
|
329
|
+
ctx: Parser.BitwiseAndExpressionContext,
|
|
330
|
+
): void => {
|
|
331
|
+
this.checkLevel(ctx.shiftExpression());
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
override enterBitwiseXorExpression = (
|
|
335
|
+
ctx: Parser.BitwiseXorExpressionContext,
|
|
336
|
+
): void => {
|
|
337
|
+
this.checkLevel(ctx.bitwiseAndExpression());
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
override enterBitwiseOrExpression = (
|
|
341
|
+
ctx: Parser.BitwiseOrExpressionContext,
|
|
342
|
+
): void => {
|
|
343
|
+
this.checkLevel(ctx.bitwiseXorExpression());
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
override enterRelationalExpression = (
|
|
347
|
+
ctx: Parser.RelationalExpressionContext,
|
|
348
|
+
): void => {
|
|
349
|
+
this.checkLevel(ctx.bitwiseOrExpression());
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
override enterEqualityExpression = (
|
|
353
|
+
ctx: Parser.EqualityExpressionContext,
|
|
354
|
+
): void => {
|
|
355
|
+
this.checkLevel(ctx.relationalExpression());
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Analyzer that detects binary operations mixing essential type categories.
|
|
361
|
+
*/
|
|
362
|
+
class MixedTypeCategoryAnalyzer {
|
|
363
|
+
private errors: IMixedTypeCategoryError[] = [];
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Analyze the parse tree for mixed-category binary operations.
|
|
367
|
+
*/
|
|
368
|
+
public analyze(tree: Parser.ProgramContext): IMixedTypeCategoryError[] {
|
|
369
|
+
this.errors = [];
|
|
370
|
+
|
|
371
|
+
const collector = new ScopeCollector();
|
|
372
|
+
ParseTreeWalker.DEFAULT.walk(collector, tree);
|
|
373
|
+
|
|
374
|
+
const listener = new MixedCategoryListener(
|
|
375
|
+
this,
|
|
376
|
+
collector.getGlobalFrame(),
|
|
377
|
+
collector.getFrameOf(),
|
|
378
|
+
);
|
|
379
|
+
ParseTreeWalker.DEFAULT.walk(listener, tree);
|
|
380
|
+
|
|
381
|
+
return this.errors;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Add a mixed-category error.
|
|
386
|
+
*/
|
|
387
|
+
public addError(line: number, column: number): void {
|
|
388
|
+
this.errors.push({
|
|
389
|
+
code: "E0810",
|
|
390
|
+
line,
|
|
391
|
+
column,
|
|
392
|
+
message:
|
|
393
|
+
"Binary operator combines operands of different essential type categories (signed and unsigned)",
|
|
394
|
+
helpText:
|
|
395
|
+
"MISRA C:2012 Rule 10.4: both operands must share an essential type category. " +
|
|
396
|
+
"Reinterpret one operand's bits to match the other with bit indexing, e.g. value[0, 32] (ADR-007/ADR-024).",
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Get all detected errors.
|
|
402
|
+
*/
|
|
403
|
+
public getErrors(): IMixedTypeCategoryError[] {
|
|
404
|
+
return this.errors;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export default MixedTypeCategoryAnalyzer;
|