Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | 46x 478x 478x 478x 478x 478x 12x 12x 12x 12x 12x 1x 1x 11x 1x 1x 10x 8x 8x 12x 8x 5x 12x 12x 12x 12x 8x 8x 12x 12x 8x 10x 10x 16x 16x 16x 2x 2x 2x 14x 8x 5x 5x 5x 5x 8x 5x 3x 3x 2x 2x 2x 1x 2x 2x 1x 16x 16x 8x 16x 8x 8x 8x 16x 16x 8x 8x 4x 4x 478x 478x 478x 478x | /**
* ADR-025 switch statements: E0711-E0714.
*
* #1322. Five throws in `TypeValidator.validateSwitchStatement`, every one of
* them reaching the user as `1:0` -- seven fixtures under `tests/switch/` all
* assert that position, which is the defect this card exists to remove.
*
* ## Every one of these is a whole-statement question
*
* Unlike most relocations, nothing here needs a type the analyzers could not
* already see. `knownEnums` and `enumMembers` are on the per-file symbol view,
* populated before `runAnalyzers`; the clause count, the case labels and the
* `default(N)` count are all in the parse tree. The checks lived in codegen
* because that is where the switch was being WRITTEN, not because that is where
* the facts were.
*
* ## Why the coverage rules are one code and not two
*
* `switch covers 3 of 4` and `Non-exhaustive switch ... missing 1` were two
* throws, and they are the same decision: the clauses must account for the
* enum's variants exactly. What differs is only whether a `default(N)` is
* present to contribute a count. Two codes would make a reader think the fix
* differs; it does not.
*/
import { ParseTreeWalker } from "antlr4ng";
import { CNextListener } from "../../PARSE/2-Parse/grammar/CNextListener";
import * as Parser from "../../PARSE/2-Parse/grammar/CNextParser";
import ParserUtils from "../../utils/ParserUtils";
import OperandTyper from "../../utils/OperandTyper";
import EnumValueResolver from "./EnumValueResolver";
import ISwitchStatementError from "./types/ISwitchStatementError";
import EnumMemberSuggestion from "./helpers/EnumMemberSuggestion";
import type IAnalysisContext from "./types/IAnalysisContext";
/** MISRA C:2012 Rule 16.6: a switch needs at least two clauses. */
const MINIMUM_CLAUSES = 2;
class SwitchStatementListener extends CNextListener {
private readonly found: ISwitchStatementError[] = [];
private readonly values: EnumValueResolver;
public constructor(private readonly context: IAnalysisContext) {
super();
this.values = new EnumValueResolver(context);
}
public errors(): ISwitchStatementError[] {
return this.found;
}
override enterSwitchStatement = (
ctx: Parser.SwitchStatementContext,
): void => {
const switchExpr = ctx.expression();
const cases = ctx.switchCase();
const defaultCase = ctx.defaultCase();
if (OperandTyper.isBoolean(OperandTyper.typeOf(switchExpr, this.context))) {
this.report(
switchExpr,
"E0711",
"Cannot switch on a boolean",
"MISRA C:2012 Rule 16.7: a bool has two states and a switch implies more. Use if/else.",
);
return;
}
if (cases.length + (defaultCase ? 1 : 0) < MINIMUM_CLAUSES) {
this.report(
switchExpr,
"E0712",
"Switch requires at least 2 clauses",
"MISRA C:2012 Rule 16.6: a one-clause switch is an if statement written the long way.",
);
return;
}
if (this.reportDuplicateCase(cases)) return;
const verdict = this.values.classify(switchExpr);
const switchEnum = verdict.kind === "enum" ? verdict.typeName : null;
Iif (this.reportBareMemberLabels(cases, switchEnum)) return;
if (switchEnum !== null) {
this.checkExhaustiveness(switchExpr, switchEnum, cases, defaultCase);
}
};
/**
* E0424 on a case label (ADR-017, #1322): a bare identifier label resolves
* against the switch's enum, and against nothing when the switch is not on
* an enum. A label naming a member of some OTHER enum is the bare-member
* mistake with the enum spelled out for it. True when one was reported.
*/
/**
* One label. True when it named a bare enum member that the switch's own
* enum does not declare -- a label naming nothing at all is a const, and not
* this rule's to judge.
*/
private reportBareMemberLabel(
label: Parser.CaseLabelContext,
switchEnum: string | null,
): boolean {
const symbols = this.context.symbols;
const name = label.IDENTIFIER()?.getText();
Eif (name === undefined) return false;
if (switchEnum !== null && symbols.enumMembers.get(switchEnum)?.has(name)) {
return false;
}
const declaring = EnumMemberSuggestion.enumsDeclaring(name, symbols);
if (declaring.length === 0) return false; // a const label
const { line, column } = ParserUtils.getPosition(label);
this.found.push({
code: "E0424",
line,
column,
message: EnumMemberSuggestion.message(name, declaring),
helpText:
switchEnum === null
? "The switch is not on an enum, so a bare member names nothing here; qualify it, or switch on a value of the enum's type (ADR-017)."
: `The switch is on ${switchEnum}, which declares no such member; qualify the label with the enum it belongs to (ADR-017).`,
});
return true;
}
private reportBareMemberLabels(
cases: readonly Parser.SwitchCaseContext[],
switchEnum: string | null,
): boolean {
let reported = false;
for (const caseCtx of cases) {
for (const label of caseCtx.caseLabel()) {
Iif (this.reportBareMemberLabel(label, switchEnum)) reported = true;
}
}
return reported;
}
/** True when a duplicate was reported, so the later checks are skipped. */
private reportDuplicateCase(
cases: readonly Parser.SwitchCaseContext[],
): boolean {
const seen = new Set<string>();
for (const caseCtx of cases) {
for (const label of caseCtx.caseLabel()) {
const value = SwitchStatementListener.caseLabelValue(label);
if (seen.has(value)) {
const { line, column } = ParserUtils.getPosition(label);
this.found.push({
code: "E0713",
line,
column,
message: `Duplicate case value '${value}' in switch statement`,
helpText:
"Two clauses cannot claim the same value; one of them is unreachable.",
});
return true;
}
seen.add(value);
}
}
return false;
}
/**
* The clauses must account for the enum's variants exactly.
*
* A `default(N)` states how many variants it absorbs, so the total is the
* explicit labels plus that count; without one, the explicit labels alone
* have to cover every variant. A `default` with no count says nothing about
* how many it covers, so nothing can be checked.
*/
private checkExhaustiveness(
at: Parser.ExpressionContext,
enumTypeName: string,
cases: readonly Parser.SwitchCaseContext[],
defaultCase: Parser.DefaultCaseContext | null,
): void {
const variants = this.context.symbols.enumMembers.get(enumTypeName);
Iif (!variants) return;
const total = variants.size;
const explicit = cases.reduce(
(sum, caseCtx) => sum + caseCtx.caseLabel().length,
0,
);
if (defaultCase) {
const declared = defaultCase.INTEGER_LITERAL();
if (declared === null) return;
const absorbed = Number.parseInt(declared.getText(), 10);
const covered = explicit + absorbed;
if (covered !== total) {
this.report(
at,
"E0714",
`Switch covers ${covered} of ${total} ${enumTypeName} variants (${explicit} explicit + default(${absorbed}))`,
`Every variant has to be accounted for exactly once. Expected ${total}.`,
);
}
return;
}
if (explicit !== total) {
this.report(
at,
"E0714",
`Non-exhaustive switch on ${enumTypeName}: covers ${explicit} of ${total} variants, missing ${total - explicit}`,
"Add the missing cases, or a `default(N)` stating how many variants it absorbs.",
);
}
}
/**
* A case label's value, as a string that compares equal for equal values.
*
* The spellings have to normalize or the duplicate check misses: `-1` and
* `-0x1` are the same case written two ways, and `tests/switch` has a fixture
* for each. Moved wholesale from codegen, where it had no other caller.
*/
private static caseLabelValue(ctx: Parser.CaseLabelContext): string {
const qualified = ctx.qualifiedType();
if (qualified) {
return qualified
.IDENTIFIER()
.map((id) => id.getText())
.join(".");
}
const identifier = ctx.IDENTIFIER();
Iif (identifier) return identifier.getText();
const negated = ctx.children?.[0]?.getText() === "-";
const numeric =
ctx.INTEGER_LITERAL() ?? ctx.HEX_LITERAL() ?? ctx.BINARY_LITERAL();
if (numeric) {
const value = BigInt(numeric.getText());
return String(negated ? -value : value);
}
const character = ctx.CHAR_LITERAL();
return character ? character.getText() : "";
}
private report(
at: Parser.ExpressionContext,
code: string,
message: string,
helpText: string,
): void {
const { line, column } = ParserUtils.getPosition(at);
this.found.push({ code, line, column, message, helpText });
}
}
class SwitchStatementAnalyzer {
/** #1456: handed in rather than read off shared state. */
constructor(private readonly context: IAnalysisContext) {}
public analyze(tree: Parser.ProgramContext): ISwitchStatementError[] {
const listener = new SwitchStatementListener(this.context);
ParseTreeWalker.DEFAULT.walk(listener, tree);
return listener.errors();
}
}
export default SwitchStatementAnalyzer;
|