All files / TRANSPILE/1-Analyze MixedTypeCategoryAnalyzer.ts

95.38% Statements 124/130
92.3% Branches 108/117
95.65% Functions 22/23
96.22% Lines 102/106

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 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391                                                                                                                                        46x             46x                   541x     541x 541x   541x               926x   922x     920x   916x 916x 893x                   925x 925x 33x   892x 892x 896x 896x 656x 652x 4x       892x                             222x 222x 222x 222x 225x 225x 225x 218x 31x 31x 31x     225x   222x                 225x 220x                                     225x 119x 117x       2x   115x                           225x 83x                             250x                         1958x 1958x 4x 4x 4x                     283x 78x   78x     283x       165x 165x   30x 30x     30x 30x 29x 8x 8x             541x 541x     1958x     1958x         541x   541x     541x 541x       541x 541x   224x 222x       541x 541x 222x     541x 165x     541x     541x               39x     39x 8x   31x         78x 73x                   88x   39x   39x 39x 39x 39x   39x 39x 39x     12x         39x 19x   39x                      
/**
 * Mixed Type Category Analyzer
 *
 * Detects operators whose two operands have different essential type
 * categories at compile time: signed, unsigned, floating, and -- per MISRA
 * C:2012 Rule 10.4, for C-Next and header types alike (#1668, ruling 2) --
 * character, Boolean, and each named enum as a category of its own.
 *
 * MISRA C:2012 Rule 10.4: "Both operands of an operator in which the usual
 * arithmetic conversions are performed shall have the same essential type
 * category." Combining a signed and an unsigned value (e.g. `u32 + i32`) relies
 * on C's usual arithmetic conversions, whose result can be surprising (ADR-024).
 *
 * To combine a signed and an unsigned value, the developer reinterprets one
 * operand's bits to the other's category with bit indexing (ADR-007), e.g.
 * `a + b[0, 32]`, making the conversion explicit. To combine an integer and a
 * floating value, the developer casts the integer, e.g. `(f32)i * k` (#1668).
 *
 * UNSUFFIXED integer literals are exempt: such a literal has no fixed
 * essential category — it is contextually typed to the other operand
 * (ADR-052), so `a + 5` and `k * 3` are fine. A suffixed literal has its
 * suffix's category (#1668, R2): `a + 5i32` is signed against unsigned. A
 * float literal is NOT exempt: no integer operand can adopt it, so `i * 2.5`
 * is floating against unsigned. MISRA's one exception is kept: `+` and `+<-`
 * may combine a character with a signed or unsigned operand. The rule fires
 * only when BOTH operands resolve to a category, and the two differ.
 *
 * Compound assignments (`+<-`, `*<-`, ...) are the same operators, so the
 * target and the value are compared the same way (#1668). `y *<- 2.5` is
 * `y <- y * 2.5`; checking only the binary form rejected the second spelling
 * and accepted the first.
 *
 * Each operand is typed by the one operand typer (#1668), which binds a name
 * through Program's lexical frames -- so a same-named variable of a different
 * category in another function or a nested block never poisons the lookup
 * (Issue #1085 review) -- and types every shape of operand: fields, elements,
 * calls, casts, C and C++ operands. This analyzer applies Rule 10.4's policy
 * to the facts (`rule104Category`); the typer decides no policy.
 *
 * Note: shift operators (<< / >>) are intentionally NOT checked here — MISRA
 * Rule 10.4 only governs operators subject to the usual arithmetic conversions,
 * and a shift count is promoted independently. A signed shift count is a Rule
 * 10.1 concern handled elsewhere (Issue #1085 review).
 */
 
import { ParseTreeWalker, ParserRuleContext } from "antlr4ng";
import { CNextListener } from "../../PARSE/2-Parse/grammar/CNextListener";
import * as Parser from "../../PARSE/2-Parse/grammar/CNextParser";
import IMixedTypeCategoryError from "./types/IMixedTypeCategoryError";
import BinaryOperatorLevelListener from "./BinaryOperatorLevelListener";
import AssignmentSiteListener from "./AssignmentSiteListener";
import BooleanOperandAnalyzer from "./BooleanOperandAnalyzer";
import EnumValueResolver from "./EnumValueResolver";
import ParserUtils from "../../utils/ParserUtils";
import OperandTyper from "../../utils/OperandTyper";
import type IAnalysisContext from "./types/IAnalysisContext";
import type TBinaryOperatorLevel from "./types/TBinaryOperatorLevel";
import type TAssignmentSite from "./types/TAssignmentSite";
import type IOperandType from "../../types/IOperandType";
 
/**
 * A Rule 10.4 category: `signed`, `unsigned`, `floating`, `character`,
 * `boolean`, or `enum:<C name>` -- each named enum its own -- or null for an
 * operand with none (an unsuffixed literal, a bit, a struct, an unknown).
 */
type Category = string | null;
 
/** Assignments that are not arithmetic, so not Rule 10.4 operands */
const NOT_RULE_10_4_ASSIGNMENTS: ReadonlySet<string> = new Set([
  "<-",
  "<<<-",
  ">><-",
]);
 
/** The operators MISRA lets combine a character with an integer */
const CHARACTER_ARITHMETIC: ReadonlySet<string> = new Set(["+", "+<-"]);
 
class MixedCategoryCheck {
  private readonly enums: EnumValueResolver;
 
  /**
   * Each checked level's own category, for the level that contains it: the
   * running category it folded to, or null when a pair in it differed --
   * already reported there, once. Levels are checked innermost first.
   */
  private readonly levelCategories = new Map<ParserRuleContext, Category>();
 
  constructor(
    private readonly analyzer: MixedTypeCategoryAnalyzer,
    private readonly context: IAnalysisContext,
  ) {
    this.enums = new EnumValueResolver(context);
  }
 
  /**
   * An operand's Rule 10.4 category, or null when it has none. The policy
   * over the typer's facts, decided here and only here.
   */
  static rule104Category(t: IOperandType | null): Category {
    if (t === null) return null;
    // An array is not an arithmetic operand of any category (#1191)
    if (t.dimensions.length > 0) return null;
    // A subscript into a scalar is a bit index or range, not arithmetic
    // on the scalar's category (ADR-024)
    if (t.form.kind === "bitIndex" || t.form.kind === "bitRange") return null;
    // Which overload a call selects is C++'s decision, not ours (C03)
    Iif (t.form.kind === "foreign" && t.form.indeterminate) return null;
    if (t.category === "enum") return `enum:${t.enumTypeName ?? t.typeName}`;
    return t.category === "none" ? null : t.category;
  }
 
  /**
   * One category for an operand. An operand that is itself an operator level
   * has the category that level folded to; any other has the category its
   * value leaves share, or null when they have none -- or disagree, which is
   * reported at the inner operator, once.
   */
  private operandCategory(ctx: ParserRuleContext): Category {
    const level = OperandTyper.compositeLevelOf(ctx);
    if (level !== null && this.levelCategories.has(level)) {
      return this.levelCategories.get(level) ?? null;
    }
    let resolved: Category = null;
    for (const leaf of OperandTyper.valueLeaves(ctx, this.context)) {
      const category = MixedCategoryCheck.rule104Category(leaf);
      if (category === null) continue;
      if (resolved === null) {
        resolved = category;
      I} else if (resolved !== category) {
        return null;
      }
    }
    return resolved;
  }
 
  /**
   * Checks one level, folding a running category left to right (#1760
   * review). Only adjacent operands were compared, so a category-less
   * literal between two operands broke the chain: `a * 2 * k` passed. A
   * category-less operand now adopts the running category; an exempt
   * character + integer gives character (MISRA C:2012 D.7); after a pair
   * that differs -- reported there -- the right operand's category runs on.
   */
  public checkLevel(
    operands: ParserRuleContext[],
    level: TBinaryOperatorLevel,
  ): void {
    const parent = operands[0]?.parent;
    let running = this.operandCategory(operands[0]);
    let mixed = false;
    for (let i = 1; i < operands.length; i += 1) {
      const right = this.operandCategory(operands[i]);
      const operator = parent?.getChild(i * 2 - 1)?.getText() ?? "";
      if (this.reportsPair(operands[i - 1], operands[i], level)) {
        if (MixedCategoryCheck.differ(running, right, operator)) {
          const { line, column } = ParserUtils.getPosition(operands[i]);
          this.analyzer.addError(line, column, running!, right!);
          mixed = true;
        }
      }
      running = MixedCategoryCheck.fold(running, right, operator);
    }
    Eif (parent) this.levelCategories.set(parent, mixed ? null : running);
  }
 
  /** Whether this rule, not another, reports a mix between two operands */
  private reportsPair(
    left: ParserRuleContext,
    right: ParserRuleContext,
    level: TBinaryOperatorLevel,
  ): boolean {
    if (this.enumComparison(left, right, level)) return false;
    return !MixedCategoryCheck.ownedElsewhere(
      this.operandCategory(left),
      this.operandCategory(right),
      level,
    );
  }
 
  /**
   * The category after `running operator right`: character for MISRA's
   * exempt character + integer (D.7), the running one when `right` has
   * none, and `right`'s otherwise. Owner ruling (2026-09-28, #1760 review):
   * D.7 stands, so `u32 a + 'A' + i32 s` is legal here; computing its exact
   * value into the destination is #1809.
   */
  private static fold(
    running: Category,
    right: Category,
    operator: string,
  ): Category {
    if (right === null) return running;
    if (running === null) return right;
    if (
      running !== right &&
      !MixedCategoryCheck.differ(running, right, operator)
    ) {
      return "character";
    }
    return right;
  }
 
  /**
   * Whether ADR-017 reports this pair (E0434, with its own message), so that
   * one defect has one code. Asked of E0434's own reading, so the two cannot
   * disagree about what is a C-Next enum: a header's enum is not one there,
   * and its comparison with an integer is this rule's mix.
   */
  private enumComparison(
    left: ParserRuleContext,
    right: ParserRuleContext,
    level: TBinaryOperatorLevel,
  ): boolean {
    if (level !== "equality" && level !== "relational") return false;
    return (
      this.enums.classify(left).kind === "enum" ||
      this.enums.classify(right).kind === "enum"
    );
  }
 
  /**
   * Whether Rule 10.1 reports this pair: a Boolean operand anywhere it admits
   * none (E0806/E0807), so that one defect has one code.
   */
  private static ownedElsewhere(
    left: Category,
    right: Category,
    level: TBinaryOperatorLevel | "compound",
  ): boolean {
    return (
      !BooleanOperandAnalyzer.admitsBoolean(level) &&
      (left === "boolean" || right === "boolean")
    );
  }
 
  /**
   * A conditional's two value arms are Rule 10.4 operands of each other
   * (ruling R3); its condition is not an operand. An arm whose own leaves
   * are mixed has no category, so that defect is reported once, at its own
   * operator, and never again here.
   */
  public checkTernary(ctx: Parser.TernaryExpressionContext): void {
    const arms = ParserUtils.ternaryValueArms(ctx);
    if (arms === null) return;
    const left = this.operandCategory(arms[0]);
    const right = this.operandCategory(arms[1]);
    Eif (left === null || right === null || left === right) return;
    const { line, column } = ParserUtils.getPosition(arms[1]);
    this.analyzer.addError(line, column, left, right, "conditional");
  }
 
  /** Whether two categories may not be combined by `operator` */
  private static differ(
    left: Category,
    right: Category,
    operator: string,
  ): boolean {
    if (left === null || right === null || left === right) return false;
    const integer = (c: string) => c === "signed" || c === "unsigned";
    const characterExempt =
      CHARACTER_ARITHMETIC.has(operator) &&
      ((left === "character" && integer(right)) ||
        (right === "character" && integer(left)));
    return !characterExempt;
  }
 
  public checkCompound(site: TAssignmentSite): void {
    const text = site.assignmentOperator().getText();
    if (NOT_RULE_10_4_ASSIGNMENTS.has(text)) return;
 
    const value = site.expression();
    const left = MixedCategoryCheck.rule104Category(
      OperandTyper.typeOfTarget(site.assignmentTarget(), this.context),
    );
    const right = this.operandCategory(value);
    if (MixedCategoryCheck.ownedElsewhere(left, right, "compound")) return;
    if (MixedCategoryCheck.differ(left, right, text)) {
      const { line, column } = ParserUtils.getPosition(value);
      this.analyzer.addError(line, column, left!, right!, "compound");
    }
  }
}
 
/** Hands each conditional expression to the check */
class TernaryListener extends CNextListener {
  constructor(private readonly check: MixedCategoryCheck) {
    super();
  }
 
  override enterTernaryExpression = (
    ctx: Parser.TernaryExpressionContext,
  ): void => {
    this.check.checkTernary(ctx);
  };
}
 
class MixedTypeCategoryAnalyzer {
  constructor(private readonly context: IAnalysisContext) {}
 
  private errors: IMixedTypeCategoryError[] = [];
 
  public analyze(tree: Parser.ProgramContext): IMixedTypeCategoryError[] {
    this.errors = [];
    const check = new MixedCategoryCheck(this, this.context);
 
    // Levels are met outermost first; checked innermost first, so a level
    // that is an operand of another has its category when that one asks
    const levels: Array<[ParserRuleContext[], TBinaryOperatorLevel]> = [];
    ParseTreeWalker.DEFAULT.walk(
      new BinaryOperatorLevelListener((operands, level) => {
        if (level === "shift") return;
        levels.push([operands, level]);
      }),
      tree,
    );
    levels.reverse();
    for (const [operands, level] of levels) {
      check.checkLevel(operands, level);
    }
 
    ParseTreeWalker.DEFAULT.walk(
      new AssignmentSiteListener((site) => check.checkCompound(site)),
      tree,
    );
    ParseTreeWalker.DEFAULT.walk(new TernaryListener(check), tree);
 
    // Three walks; reported in source order
    return this.errors.sort((a, b) => a.line - b.line || a.column - b.column);
  }
 
  /** The diagnostic's text for each place two categories meet */
  private static message(
    what: "binary" | "conditional" | "compound",
    pair: string,
  ): string {
    Iif (what === "conditional") {
      return `Conditional operator's value arms have different essential type categories (${pair})`;
    }
    if (what === "compound") {
      return `Compound assignment combines a target and a value of different essential type categories (${pair})`;
    }
    return `Binary operator combines operands of different essential type categories (${pair})`;
  }
 
  /** How a category reads in a message */
  private static label(category: string): string {
    if (category.startsWith("enum:")) return `enum ${category.slice(5)}`;
    return category === "boolean" ? "Boolean" : category;
  }
 
  public addError(
    line: number,
    column: number,
    left: string,
    right: string,
    what: "binary" | "conditional" | "compound" = "binary",
  ): void {
    const integer = (c: string) => c === "signed" || c === "unsigned";
    const floating =
      (left === "floating" && integer(right)) ||
      (right === "floating" && integer(left));
    const signedness = integer(left) && integer(right);
    let pair = `${MixedTypeCategoryAnalyzer.label(left)} and ${MixedTypeCategoryAnalyzer.label(right)}`;
    if (floating) pair = "integer and floating";
    if (signedness) pair = "signed and unsigned";
    const rule =
      "MISRA C:2012 Rule 10.4: both operands must share an essential type category. ";
    let helpText = `${rule}Convert one operand explicitly, e.g. with a cast (ADR-024).`;
    if (floating) {
      // A compound assignment's target is its left operand, and a target
      // cannot be cast (#1760 review)
      helpText =
        what === "compound" && integer(left)
          ? `${rule}The target cannot be converted: write the assignment out with explicit conversions, e.g. x <- (u32)((f32)x * k) (ADR-024).`
          : `${rule}Convert the integer operand with an explicit cast, e.g. (f32)value (ADR-024).`;
    }
    if (signedness) {
      helpText = `${rule}Reinterpret one operand's bits to match the other with bit indexing, e.g. value[0, 32] (ADR-007/ADR-024).`;
    }
    this.errors.push({
      code: "E0810",
      line,
      column,
      message: MixedTypeCategoryAnalyzer.message(what, pair),
      helpText,
    });
  }
}
 
export default MixedTypeCategoryAnalyzer;