All files / TRANSPILE/1-Analyze NestedTernaryAnalyzer.ts

100% Statements 25/25
100% Branches 14/14
100% Functions 5/5
100% Lines 21/21

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                                                                                                        46x 711x     46x 711x 707x 736x 736x 56x     651x       473x     473x     1583x     1583x   14x         42x 42x 4x 4x                           473x 473x 473x          
/**
 * E0710: a ternary may not appear inside another ternary's branches (ADR-022).
 *
 * #1322. The check this replaces was a SUBSTRING TEST on the branch's source
 * text:
 *
 *     const text = ctx.getText();
 *     if (text.includes("?") && text.includes(":")) { throw ... }
 *
 * which rejects `(n = 1) ? "a?b:c" : "plain"`. That is a legal ternary whose
 * true branch is a string literal containing both characters, and it was
 * reported as a nested ternary -- verified by probe before this existed.
 *
 * A rule about syntax was asking about characters. The parse tree already knows
 * whether a branch contains a ternary, so it is asked.
 *
 * ## The traversal, and the trap CLAUDE.md records
 *
 * `ternaryExpression` carries THREE `orExpression` children: `[0]` condition,
 * `[1]` true value, `[2]` false value. They must be addressed through
 * `orExpression(i)` and never `getChild(i)` -- the condition is parenthesised,
 * so `getChild(0)` is the `(` token and an index-based skip silently does
 * nothing. That is a documented bug in this project.
 *
 * ## All three are searched, and the codegen check searched two
 *
 * `validateNoNestedTernary` was called on `orExprs[1]` and `orExprs[2]` only.
 * ADR-022 says "no nesting allowed" without qualification, so the CONDITION was
 * a hole: `(((n = 1) ? 2 : 3) = 2) ? 4 : 5` parses, compiles, and emits C --
 * verified by probe, and the whole nesting sits inside a comparison, so E0701
 * (a condition must be a comparison) is satisfied and does not fire either.
 *
 * The first version of this analyzer reproduced the hole and justified it in
 * this comment by asserting E0701 owned the case. That was never probed and was
 * false. Searching all three children closes it; a corpus scan of 1276 `.cnx`
 * files found zero conditions containing a ternary, so nothing legal regresses.
 */
 
import { ParserRuleContext, 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 INestedTernaryError from "./types/INestedTernaryError";
 
/**
 * A `ternaryExpression` node that is really a ternary.
 *
 * The grammar threads every expression through this rule, so most instances
 * carry a single `orExpression` and no `?`. The `COLON` token is what marks a
 * real one -- a structural fact, not a character search of the text.
 */
const isTernary = (ctx: ParserRuleContext): boolean =>
  ctx instanceof Parser.TernaryExpressionContext && ctx.COLON() !== null;
 
/** Whether `node`'s subtree contains a real ternary. */
const containsTernary = (node: ParserRuleContext): boolean => {
  if (isTernary(node)) return true;
  for (let index = 0; index < node.getChildCount(); index += 1) {
    const child = node.getChild(index);
    if (child instanceof ParserRuleContext && containsTernary(child)) {
      return true;
    }
  }
  return false;
};
 
class NestedTernaryListener extends CNextListener {
  private readonly found: INestedTernaryError[] = [];
 
  public errors(): INestedTernaryError[] {
    return this.found;
  }
 
  override enterTernaryExpression = (
    ctx: Parser.TernaryExpressionContext,
  ): void => {
    if (ctx.COLON() === null) return;
 
    for (const [index, branch] of [
      [0, "condition"],
      [1, "true branch"],
      [2, "false branch"],
    ] as const) {
      const value = ctx.orExpression(index);
      if (value === null || !containsTernary(value)) continue;
      const { line, column } = ParserUtils.getPosition(value);
      this.found.push({
        code: "E0710",
        line,
        column,
        message: `Nested ternary not allowed in ${branch}`,
        helpText:
          "A ternary inside a ternary is hard to read and hard to certify (ADR-022). Use if/else, or lift the inner expression into a named variable first.",
      });
    }
  };
}
 
class NestedTernaryAnalyzer {
  public analyze(tree: Parser.ProgramContext): INestedTernaryError[] {
    const listener = new NestedTernaryListener();
    ParseTreeWalker.DEFAULT.walk(listener, tree);
    return listener.errors();
  }
}
 
export default NestedTernaryAnalyzer;