All files / TRANSPILE/1-Analyze ControllingExpressionAnalyzer.ts

88.23% Statements 60/68
82.14% Branches 23/28
100% Functions 15/15
87.3% Lines 55/63

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                                                                            46x     490x   490x 490x       490x           72x 72x     3x 3x     1x     1x     4x     4x 4x     1618x     1618x 4x     4x           79x 4x 4x     75x 75x   1x 1x               1x     74x             4x       4x                           78x 78x         78x 80x 80x         80x   82x   27x 27x         27x   13x 13x                 13x 13x 13x                         4x 4x                                     13x 13x 13x     5x   8x           490x     490x 490x 490x          
/**
 * ADR-022's controlling-expression rule: E0701 and E0702.
 *
 * #1322. Four throws in `TypeValidator`, all reaching the user as `1:0`, across
 * nineteen fixtures that assert that position.
 *
 * The rule itself is purely SYNTACTIC -- every leaf operand of a controlling
 * expression, after decomposing `||` and `&&`, must be an equality or
 * relational comparison. It reads the parse tree and nothing else, which makes
 * this the second family in a row that needed no capability 2.1 did not already
 * have. Only the help text asks a type question, and it asks it of the lexical
 * frames rather than of codegen's registry.
 *
 * ## The enumeration, named because it is the risk
 *
 * Five productions carry a controlling expression: `if`, `while`, `do-while`,
 * `for` and the ternary. There is no way to ask the grammar "which rules have
 * one" -- they are five listener methods, and a sixth production added later
 * would be silently unchecked. That is exactly how E0853 came to miss `switch`.
 *
 * Two things are done about it rather than hoped about it. The five delegate to
 * ONE check, so only the collection is enumerated and never the decision. And
 * `controlling-expression-every-kind-error` exercises all five in one file, so
 * its expected output carries five diagnostics: a kind that stops being visited
 * drops the count and fails the fixture. `forever` is deliberately absent from
 * the list -- it has no controlling expression, which is the point of it.
 */
 
import { ParserRuleContext, ParseTreeWalker } from "antlr4ng";
 
import { CNextListener } from "../../PARSE/2-Parse/grammar/CNextListener";
import * as Parser from "../../PARSE/2-Parse/grammar/CNextParser";
import ExpressionUtils from "../../utils/ExpressionUtils";
import ParserUtils from "../../utils/ParserUtils";
import OperandTyper from "../../utils/OperandTyper";
import IControllingExpressionError from "./types/IControllingExpressionError";
import type IAnalysisContext from "./types/IAnalysisContext";
 
const CALL_HELP = "Store the function result in a variable first.";
 
class ControllingExpressionListener extends CNextListener {
  private readonly found: IControllingExpressionError[] = [];
 
  public constructor(private readonly context: IAnalysisContext) {
    super();
  }
 
  public errors(): IControllingExpressionError[] {
    return this.found;
  }
 
  // --- The five productions that carry a controlling expression -------------
  // Each one only NAMES its expression; the rule itself lives in `check`.
 
  override enterIfStatement = (ctx: Parser.IfStatementContext): void => {
    this.check(ctx.expression(), "if");
  };
 
  override enterWhileStatement = (ctx: Parser.WhileStatementContext): void => {
    this.check(ctx.expression(), "while");
  };
 
  override enterDoWhileStatement = (
    ctx: Parser.DoWhileStatementContext,
  ): void => {
    this.check(ctx.expression(), "do-while");
  };
 
  override enterForStatement = (ctx: Parser.ForStatementContext): void => {
    // `for (;;)` has no controlling expression at all -- that is E0707's case
    // (a disguised infinite loop), not this rule's.
    const condition = ctx.expression();
    if (condition) this.check(condition, "for");
  };
 
  override enterTernaryExpression = (
    ctx: Parser.TernaryExpressionContext,
  ): void => {
    if (ctx.COLON() === null) return;
    const condition = ctx.orExpression(0);
    // Addressed through `orExpression(0)`, never `getChild(0)`: the condition
    // is parenthesised, so child 0 is the `(` (CLAUDE.md).
    Eif (condition) this.checkOrExpression(condition, "ternary");
  };
 
  // --- The one decision -----------------------------------------------------
 
  private check(ctx: Parser.ExpressionContext, kind: string): void {
    if (ExpressionUtils.hasFunctionCall(ctx)) {
      this.reportCall(ctx, kind);
      return;
    }
 
    const orExprs = ctx.ternaryExpression().orExpression();
    if (orExprs.length !== 1) {
      // The controlling expression is itself a ternary.
      const { line, column } = ParserUtils.getPosition(ctx);
      this.found.push({
        code: "E0701",
        line,
        column,
        message: `${kind} condition must be a comparison, not a ternary`,
        helpText:
          "MISRA C:2012 Rule 14.4: lift the ternary into a variable and compare that.",
      });
      return;
    }
 
    this.checkComparison(orExprs[0], kind);
  }
 
  private checkOrExpression(
    ctx: Parser.OrExpressionContext,
    kind: string,
  ): void {
    Iif (ExpressionUtils.hasFunctionCallInOr(ctx)) {
      this.reportCall(ctx, kind);
      return;
    }
    this.checkComparison(ctx, kind);
  }
 
  /**
   * Every leaf operand, after decomposing `||` and `&&`, has to carry a
   * comparison operator of its own.
   *
   * A bare value, a bare bool, a literal or a negation is rejected: `!ready` is
   * not a comparison, and MISRA C:2012 Rule 14.4 wants the intent written out.
   */
  private checkComparison(
    orExpr: Parser.OrExpressionContext,
    kind: string,
  ): void {
    const andExprs = orExpr.andExpression();
    Iif (andExprs.length === 0) {
      this.reportNotComparison(orExpr, kind);
      return;
    }
 
    for (const andExpr of andExprs) {
      const equalityExprs = andExpr.equalityExpression();
      Iif (equalityExprs.length === 0) {
        this.reportNotComparison(orExpr, kind);
        return;
      }
 
      for (const equalityExpr of equalityExprs) {
        // An equality operator (`=`, `!=`) makes this operand a comparison.
        if (equalityExpr.relationalExpression().length > 1) continue;
 
        const relational = equalityExpr.relationalExpression(0);
        Iif (!relational) {
          this.reportNotComparison(orExpr, kind);
          return;
        }
        // A relational operator (`<`, `>`, `<=`, `>=`) does too.
        if (relational.bitwiseOrExpression().length > 1) continue;
 
        this.reportNotComparison(equalityExpr, kind);
        return;
      }
    }
  }
 
  private reportNotComparison(
    node: Parser.OrExpressionContext | Parser.EqualityExpressionContext,
    kind: string,
  ): void {
    const text = node.getText();
    const { line, column } = ParserUtils.getPosition(node);
    this.found.push({
      code: "E0701",
      line,
      column,
      message: `${kind} condition must be a comparison, not '${text}'`,
      helpText: `MISRA C:2012 Rule 14.4: ${this.suggestionFor(node, text)}`,
    });
  }
 
  private reportCall(
    node: Parser.ExpressionContext | Parser.OrExpressionContext,
    kind: string,
  ): void {
    const { line, column } = ParserUtils.getPosition(node);
    this.found.push({
      code: "E0702",
      line,
      column,
      message: `${kind} condition may not call a function: '${node.getText()}'`,
      helpText: `MISRA C:2012 Rule 13.5: ${CALL_HELP}`,
    });
  }
 
  /**
   * What to write instead.
   *
   * A `bool` gets `x = true` and anything else gets `x > 0`, so the suggestion
   * is asked of the operand's DECLARED type. It reads the lexical frames rather
   * than codegen's type registry -- the registry does not exist yet when this
   * pass runs, and the frames also honour shadowing, which the registry's flat
   * name lookup does not.
   */
  private suggestionFor(node: ParserRuleContext, text: string): string {
    const negated = text.startsWith("!");
    const base = negated ? text.slice(1) : text;
    if (
      OperandTyper.isBoolean(OperandTyper.typeOfName(base, node, this.context))
    ) {
      return `write it out, e.g. ${base} = ${negated ? "false" : "true"}`;
    }
    return `write it out, e.g. ${text} > 0 or ${text} != 0`;
  }
}
 
class ControllingExpressionAnalyzer {
  /** #1456: handed in rather than read off shared state. */
  constructor(private readonly context: IAnalysisContext) {}
 
  public analyze(tree: Parser.ProgramContext): IControllingExpressionError[] {
    const listener = new ControllingExpressionListener(this.context);
    ParseTreeWalker.DEFAULT.walk(listener, tree);
    return listener.errors();
  }
}
 
export default ControllingExpressionAnalyzer;