All files / TRANSPILE/1-Analyze CompoundAssignmentAnalyzer.ts

100% Statements 25/25
100% Branches 13/13
100% Functions 10/10
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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127                                                                                          477x   477x     477x               163x   28x 28x   7x 7x                                                   28x 2x   26x 26x 4x           22x 22x           477x       22x       477x 477x 163x     477x          
/**
 * E0857: a compound operator needs a whole storage location.
 *
 * `+<-` and friends are read-modify-write. C-Next implements them where the
 * target names one location it can read and write back, and rejects them on a
 * bit index, a bit range, a slice or a string.
 *
 * #1322. This was the audit's largest duplicate group: **six** throws with four
 * message variants, across `AccessPatternHandlers`, `BitAccessHandlers`,
 * `AssignmentHandlerUtils`, `BitmapHandlers`, `StringHandlers` and
 * `ArrayHandlers` -- and `validateNotCompound` was defined twice, verbatim, in
 * two of them. Six sites deciding one thing, which is the shape CLAUDE.md calls
 * the project's worst anti-pattern.
 *
 * ## Why it is a 2.1 rule and not a syntactic one
 *
 * `arr[0] +<- 2` is accepted; `flags[0] +<- 1` is rejected. Same production.
 * What differs is whether the base was declared as an array -- on a scalar, a
 * subscript is a BIT index (ADR-007), and a bit is not a location the compound
 * lowering can write back to.
 *
 * That is why this family could not move before `IDeclaredVar.dimensions`
 * existed: the only place that knew `flags` was scalar and `arr` was not, was
 * codegen's type registry, which is populated after the analyzers run.
 *
 * A two-expression subscript needs no such lookup -- a range or a slice is not
 * one location whatever the base -- so the array-ness question is asked only
 * where it decides something.
 */
 
import { ParseTreeWalker } from "antlr4ng";
 
import * as Parser from "../../PARSE/2-Parse/grammar/CNextParser";
import ParserUtils from "../../utils/ParserUtils";
import OperandTyper from "../../utils/OperandTyper";
import AssignmentSiteListener from "./AssignmentSiteListener";
import ICompoundAssignmentError from "./types/ICompoundAssignmentError";
import type IOperandType from "../../types/IOperandType";
import type TAssignmentSite from "./types/TAssignmentSite";
import type IAnalysisContext from "./types/IAnalysisContext";
 
/** What made a target unusable, in words the message can name. */
type TRejection = "bit index" | "bit range or slice" | "string";
 
class CompoundAssignmentCheck {
  private readonly found: ICompoundAssignmentError[] = [];
 
  public constructor(private readonly context: IAnalysisContext) {}
 
  public errors(): ICompoundAssignmentError[] {
    return this.found;
  }
 
  /** A compound assignment, in a statement or a `for` header (#1726) */
  public checkSite(site: TAssignmentSite): void {
    // `ASSIGN` is the plain `<-`; every other operator in the rule is compound.
    // Asking what it is NOT keeps this from listing the operators, which is the
    // enumeration that let E0853 miss `switch`.
    if (site.assignmentOperator().ASSIGN()) return;
 
    const reason = this.rejectionFor(site.assignmentTarget());
    if (reason === null) return;
 
    const { line, column } = ParserUtils.getPosition(site);
    this.found.push({
      code: "E0857",
      line,
      column,
      message: `Compound assignment operators are not supported on a ${reason}`,
      helpText:
        "A compound operator reads, modifies and writes back one storage location. Write the read and the write out separately.",
    });
  }
 
  /**
   * Why this target cannot take a compound operator, or null.
   *
   * Each subscript is what the one operand typer classified it as, against
   * the shape of what it indexes (#1668). That is what makes
   * `bytes.data[0] +<- 5` legal while `flags[0] +<- 1` is not: the first
   * indexes a struct field declared an array, the second a scalar, which is a
   * bit index (ADR-007). A shape the typer cannot establish is array access,
   * and never rejects: a name this pass cannot resolve is another
   * diagnostic's to report, not this one's to guess at.
   */
  private rejectionFor(
    target: Parser.AssignmentTargetContext,
  ): TRejection | null {
    // A range or a slice is never one storage location, whatever it
    // indexes, so this needs no resolved shape.
    if (target.postfixTargetOp().some((op) => op.expression().length >= 2)) {
      return "bit range or slice";
    }
    const typing = OperandTyper.chainOf(target, this.context);
    if (typing.steps.some((step) => step.subscript === "bit_single")) {
      return "bit index";
    }
    // The chain may END on a string -- `config.name +<- " suffix"` where
    // `name` is a `string<32>` field. A string is a buffer copied by
    // `strncpy`, not a value `+` can be applied to, wherever it is reached
    // from.
    const last = OperandTyper.typeOfTarget(target, this.context);
    return CompoundAssignmentAnalyzer.isString(last) ? "string" : null;
  }
}
 
class CompoundAssignmentAnalyzer {
  /** #1456: handed in rather than read off shared state. */
  constructor(private readonly context: IAnalysisContext) {}
 
  /** A bounded string value -- not an element of one, which is a char */
  public static isString(t: IOperandType | null): boolean {
    return t !== null && t.dimensions.length === 0 && OperandTyper.isString(t);
  }
 
  public analyze(tree: Parser.ProgramContext): ICompoundAssignmentError[] {
    const check = new CompoundAssignmentCheck(this.context);
    ParseTreeWalker.DEFAULT.walk(
      new AssignmentSiteListener((site) => check.checkSite(site)),
      tree,
    );
    return check.errors();
  }
}
 
export default CompoundAssignmentAnalyzer;