All files / transpiler/logic/analysis runAnalyzers.ts

97.05% Statements 33/34
90% Branches 9/10
95.65% Functions 22/23
100% Lines 33/33

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                                                                                                                              5171x 5171x 17x             5171x                                                                       314x 314x 15x       314x   314x       314x       312x   1x       311x       311x             309x           308x       308x       307x       306x       299x       298x       298x             298x         298x       298x         298x           298x 1x         314x 5171x                 5171x 16x       314x        
/**
 * Run all semantic analyzers on a parsed C-Next program
 *
 * Extracted from transpiler.ts for reuse in the unified pipeline.
 * All 14 analyzers (plus comment validation) run in sequence, each returning
 * errors that block compilation.
 */
 
import { CommonTokenStream } from "antlr4ng";
import { ProgramContext } from "../parser/grammar/CNextParser";
import IdentifierSyntaxAnalyzer from "./IdentifierSyntaxAnalyzer";
import ParameterNamingAnalyzer from "./ParameterNamingAnalyzer";
import StructFieldAnalyzer from "./StructFieldAnalyzer";
import InitializationAnalyzer from "./InitializationAnalyzer";
import FunctionCallAnalyzer from "./FunctionCallAnalyzer";
import UndeclaredTypeAnalyzer from "./UndeclaredTypeAnalyzer";
import UndeclaredValueAnalyzer from "./UndeclaredValueAnalyzer";
import NullCheckAnalyzer from "./NullCheckAnalyzer";
import DivisionByZeroAnalyzer from "./DivisionByZeroAnalyzer";
import FloatModuloAnalyzer from "./FloatModuloAnalyzer";
import ArrayIndexTypeAnalyzer from "./ArrayIndexTypeAnalyzer";
import SignedShiftAnalyzer from "./SignedShiftAnalyzer";
import BooleanOperandAnalyzer from "./BooleanOperandAnalyzer";
import MixedTypeCategoryAnalyzer from "./MixedTypeCategoryAnalyzer";
import ReturnPathAnalyzer from "./ReturnPathAnalyzer";
import ReturnValueUseAnalyzer from "./ReturnValueUseAnalyzer";
import CommentExtractor from "./CommentExtractor";
import ITranspileError from "../../../lib/types/ITranspileError";
import SymbolTable from "../symbols/SymbolTable";
import CodeGenState from "../../state/CodeGenState";
 
/**
 * Options for running analyzers
 */
interface IAnalyzerOptions {
  /**
   * Symbol table containing external function definitions from C/C++ headers
   * Used by FunctionCallAnalyzer to recognize external functions.
   * Falls back to CodeGenState.symbolTable if not provided.
   */
  symbolTable?: SymbolTable;
}
 
/**
 * Generic analyzer error with common fields
 */
interface IAnalyzerError {
  line: number;
  column: number;
  message: string;
  code?: string;
  rule?: string;
}
 
/**
 * Convert analyzer errors to ITranspileError format and add to accumulator.
 * Returns true if any errors were added (for early return logic).
 */
function collectErrors(
  analyzerErrors: IAnalyzerError[],
  target: ITranspileError[],
  formatMessage?: (err: IAnalyzerError) => string,
): boolean {
  const formatter = formatMessage ?? ((e) => e.message);
  for (const err of analyzerErrors) {
    target.push({
      line: err.line,
      column: err.column,
      message: formatter(err),
      severity: "error",
    });
  }
  return analyzerErrors.length > 0;
}
 
/**
 * One analysis step.
 *
 * #1399 review: the body was fifteen repetitions of
 * `if (collectErrors(x.analyze(tree), errors, fmt)) return errors;`, which is a
 * table written as control flow -- cognitive complexity 16, over the 15 limit,
 * and growing by one with every analyzer added. The ordering constraints were
 * real but survived only as prose between the blocks; as entries they are data
 * that moves with the step.
 */
interface IAnalyzerStep {
  /** Why this step sits here, when its position matters. */
  readonly label: string;
  readonly run: () => IAnalyzerError[];
  /** Defaults to the `error[CODE]: message` form. */
  readonly format?: (err: IAnalyzerError) => string;
  /** When true, findings are reported and later steps still run. */
  readonly advisory?: boolean;
}
 
/**
 * Run all semantic analyzers on a parsed program.
 *
 * @param tree - The parsed program AST
 * @param tokenStream - Token stream for comment validation
 * @param options - Optional configuration including external struct info
 * @returns Array of errors (empty if all pass)
 */
function runAnalyzers(
  tree: ProgramContext,
  tokenStream: CommonTokenStream,
  options?: IAnalyzerOptions,
): ITranspileError[] {
  const errors: ITranspileError[] = [];
  const formatWithCode = (e: IAnalyzerError) =>
    `error[${e.code}]: ${e.message}`;
 
  // External function definitions from C/C++ headers, for the two steps that
  // need them. Read from CodeGenState unless the caller supplied one.
  const symbolTable = options?.symbolTable ?? CodeGenState.symbolTable;
 
  const steps: readonly IAnalyzerStep[] = [
    {
      // First: a malformed identifier feeds a bad name into every later analysis.
      label: "identifier syntax (ADR-063: no trailing or consecutive '_')",
      run: () => new IdentifierSyntaxAnalyzer().analyze(tree),
    },
    {
      label: "parameter naming (Issue #227: reserved naming patterns)",
      run: () => new ParameterNamingAnalyzer().analyze(tree),
      // Carries its own message text rather than a code.
      format: (e) => e.message,
    },
    {
      label: "struct fields (reserved field names like 'length')",
      run: () => new StructFieldAnalyzer().analyze(tree),
    },
    {
      label: "initialization (Rust-style use-before-init)",
      run: () => new InitializationAnalyzer().analyze(tree, symbolTable),
    },
    {
      // Before the call and essential-type analyses: a type that denotes
      // nothing feeds an unknown type into every later question, so the
      // diagnostics after it would name a consequence rather than the cause.
      label: "undefined type references (#1312)",
      run: () => new UndeclaredTypeAnalyzer().analyze(tree),
    },
    {
      // After the type check, so a file whose type is undefined reports the
      // type rather than every use of it.
      label: "undefined value references (#1353)",
      run: () => new UndeclaredValueAnalyzer().analyze(tree),
    },
    {
      label: "call analysis (ADR-030: define-before-use)",
      run: () => new FunctionCallAnalyzer().analyze(tree, symbolTable),
    },
    {
      label: "NULL checks (ADR-047: C library interop)",
      run: () => new NullCheckAnalyzer().analyze(tree),
    },
    {
      label: "division by zero (ADR-051: compile-time detection)",
      run: () => new DivisionByZeroAnalyzer().analyze(tree),
    },
    {
      label: "float modulo (% with f32/f64)",
      run: () => new FloatModuloAnalyzer().analyze(tree),
    },
    {
      label: "array index type (ADR-054: unsigned indexes only)",
      run: () => new ArrayIndexTypeAnalyzer().analyze(tree),
    },
    {
      label: "signed shift (MISRA C:2012 Rule 10.1)",
      run: () => new SignedShiftAnalyzer().analyze(tree),
    },
    {
      // Before the Rule 10.4 check, so a bool in an arithmetic expression is
      // reported as "not a number" rather than as a category mismatch with
      // whatever it was combined with.
      label: "boolean operands (MISRA C:2012 Rule 10.1, Issue #1183)",
      run: () => new BooleanOperandAnalyzer().analyze(tree),
    },
    {
      label:
        "mixed essential type category (MISRA C:2012 Rule 10.4, ADR-024 / Issue #1091)",
      run: () => new MixedTypeCategoryAnalyzer().analyze(tree),
    },
    {
      label: "return paths (ADR-067: non-void must return on all paths)",
      run: () => new ReturnPathAnalyzer().analyze(tree),
    },
    {
      label:
        "return-value use (ADR-070 / MISRA C:2012 Rule 17.7 at source level)",
      run: () => ReturnValueUseAnalyzer.analyze(tree),
    },
    {
      // Last, and does not halt: comment findings are reported alongside
      // whatever else the file produced.
      label: "comment validation (MISRA C:2012 Rules 3.1, 3.2 -- ADR-043)",
      run: () => new CommentExtractor(tokenStream).validate(),
      format: (e) => `error[MISRA-${e.rule}]: ${e.message}`,
      advisory: true,
    },
  ];
 
  for (const step of steps) {
    const found = collectErrors(
      step.run(),
      errors,
      step.format ?? formatWithCode,
    );
    // `break`, not an early `return`: both exits hand back the same `errors`
    // array, so returning from inside the loop reads as two exits with one
    // value (S3516) when it is really one exit and a stopping condition. What
    // varies is what `errors` CONTAINS, which no return statement expresses.
    if (found && !step.advisory) {
      break;
    }
  }
 
  return errors;
}
 
export default runAnalyzers;