All files / TRANSPILE/1-Analyze UndeclaredTypeAnalyzer.ts

97.77% Statements 44/45
93.75% Branches 15/16
100% Functions 15/15
97.77% Lines 44/45

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                                                                                                            385x     385x 385x       131x 131x       813x 1x     130x     69x     69x     69x     69x     131x 131x 131x                 131x 1x     130x 130x 120x     10x         10x 3x 3x     7x         497x               497x     497x                                   497x 112x     385x 385x                       130x           134x                                             10x             10x                                                 140x 15x 15x 11x       129x       7x                                             3x                               10x          
/**
 * UndeclaredTypeAnalyzer — rejects a name in a type position that does not name
 * a type: one that denotes nothing this file can see (E0426), or one that
 * denotes a register, which is not a type (E0429, #1336).
 *
 * Issue #1312. `CodeGenerator.getTypeName` ends in `resolved ?? ctx.getText()`,
 * so a name that resolves to no type was emitted as the raw C-Next source text
 * and the run exited 0. `Mode borrowedFromSibling <- Mode.MODE_A;` produced
 * `Mode borrowedFromSibling = Mode.MODE_A;`, which gcc rejects with
 * "unknown type name 'Mode'". The report framed this as a cross-file sibling
 * defect; measured on 0b0bd133 a lone file with no includes anywhere in the run
 * behaves identically, so the hole is the type position itself.
 *
 * ## Why an analyzer and not codegen
 *
 * Codegen-thrown errors report the synthetic position `1:0` -- 129 of 282
 * `.expected.error` fixtures begin that way (#1316's measurement, tracked as
 * #1184/#1318). #1312 asks for the type to be named *at its use site*, which
 * only a pass holding the parse tree can do. Analyzers already run with
 * `CodeGenState.symbols` populated (`Transpiler.ts:509`), so the position and
 * the symbol view are available in the same place.
 *
 * ## Why `enterUserType` and not a list of type positions
 *
 * A type appears in variable declarations, parameters, struct fields, return
 * types and for-init declarations. Enumerating them is the shape that leaves a
 * hole -- the project has been bitten by rules enforced in one context and
 * silently absent in another (#1219). `userType` is the single grammar rule for
 * a bare named type, so hooking it covers every position by construction, and a
 * new type position added to the grammar is covered without anyone remembering
 * to update this file.
 *
 * `this.T`, `global.T` and `Scope.T` are separate grammar branches that state
 * their scope outright; they are not `userType` and are deliberately not
 * checked here.
 */
 
import { ParseTreeWalker } from "antlr4ng";
import { CNextListener } from "../../PARSE/2-Parse/grammar/CNextListener";
import * as Parser from "../../PARSE/2-Parse/grammar/CNextParser";
import BUILTIN_TYPE_NAMES from "./BUILTIN_TYPE_NAMES";
import EnclosingScope from "./helpers/EnclosingScope";
import ICodeGenSymbols from "../../types/ICodeGenSymbols";
import IUndeclaredTypeError from "./types/IUndeclaredTypeError";
import NameExistence from "../../PARSE/3-Declare/NameExistence";
import SymbolTable from "../../PARSE/3-Declare/SymbolTable";
import ParserUtils from "../../utils/ParserUtils";
import ScopeUtils from "../../utils/ScopeUtils";
import type IAnalysisContext from "./types/IAnalysisContext";
 
class UndeclaredTypeListener extends CNextListener {
  private readonly analyzer: UndeclaredTypeAnalyzer;
 
  // eslint-disable-next-line @typescript-eslint/lines-between-class-members
  private readonly enclosing = new EnclosingScope();
 
  constructor(analyzer: UndeclaredTypeAnalyzer) {
    super();
    this.analyzer = analyzer;
  }
 
  private static _isSizeofOperand(ctx: Parser.UserTypeContext): boolean {
    for (
      let node: Parser.UserTypeContext["parent"] = ctx.parent;
      node;
      node = node.parent
    ) {
      if (node instanceof Parser.SizeofExpressionContext) {
        return true;
      }
    }
    return false;
  }
 
  override enterScopeDeclaration = (
    ctx: Parser.ScopeDeclarationContext,
  ): void => {
    this.enclosing.enter(ctx.IDENTIFIER().getText());
  };
 
  override exitScopeDeclaration = (
    _ctx: Parser.ScopeDeclarationContext,
  ): void => {
    this.enclosing.exit();
  };
 
  override enterUserType = (ctx: Parser.UserTypeContext): void => {
    const typeName = ctx.IDENTIFIER().getText();
    Iif (BUILTIN_TYPE_NAMES.has(typeName)) {
      return;
    }
 
    // ADR-023: `sizeof ( type | expression )`. The two alternatives are
    // ambiguous for a bare identifier and ANTLR takes the `type` branch, so
    // `sizeof(myArray)` arrives here as a `userType` naming a VARIABLE. The
    // operand is genuinely undecidable from the grammar alone, so this is the
    // one position where an unknown name is not evidence of a missing type.
    if (UndeclaredTypeListener._isSizeofOperand(ctx)) {
      return;
    }
 
    const scope = this.enclosing.current();
    if (this.analyzer.isVisibleType(typeName, scope)) {
      return;
    }
 
    const { line, column } = ParserUtils.getPosition(ctx);
 
    // A register IS declared -- just not as a type. Reporting "not defined" of
    // a name declared a few lines up reads as a transpiler fault rather than a
    // mistake in the source, so name what it actually is (#1336).
    if (this.analyzer.isRegister(typeName, scope)) {
      this.analyzer.addRegisterInTypePositionError(typeName, line, column);
      return;
    }
 
    this.analyzer.addError(typeName, line, column);
  };
}
 
class UndeclaredTypeAnalyzer {
  private readonly errors: IUndeclaredTypeError[] = [];
 
  /**
   * #1456: what the program declares, handed in rather than read off
   * `CodeGenState`. Constructor rather than a parameter on `analyze`, because
   * `isVisibleType` and `isRegister` are called by the listener and need it
   * too -- a parameter would have to reach them through the walk.
   */
  constructor(private readonly context: IAnalysisContext) {}
 
  analyze(tree: Parser.ProgramContext): IUndeclaredTypeError[] {
    this.errors.length = 0;
 
    // Only diagnose where the transpiler actually knows the whole name
    // universe of the file. A C/C++ header is not parsed into the symbol table
    // -- `FILE` from <stdio.h> is the standing example -- so an unresolved name
    // in a file that can see one is indistinguishable from a type the compiler
    // will supply, and emitting it verbatim is the CORRECT behavior for C
    // interop (#985 external-symbol recovery).
    //
    // Rejecting valid interop code is a regression; failing to diagnose is the
    // status quo, so this declines rather than guesses.
    //
    // #1399 review: the answer is computed during discovery from the
    // resolver's own categorization and is TRANSITIVE. An earlier version
    // walked this file's own `#include` token text, which made a third
    // spelling of "is this a C-Next include?" (it missed `.cnext`) and stopped
    // at one hop -- so a macro reached through a `.cnx` include was REJECTED,
    // code that main compiles.
    if (this.context.reachesForeignHeader) {
      return this.errors;
    }
 
    ParseTreeWalker.DEFAULT.walk(new UndeclaredTypeListener(this), tree);
    return this.errors;
  }
 
  /**
   * ADR-057: inside a scope a bare `T` may name a scope-declared type, which is
   * recorded under its qualified name. The bare spelling is still accepted,
   * because a scope may also reference a global type.
   */
  isVisibleType(
    typeName: string,
    scope: ReturnType<EnclosingScope["current"]>,
  ): boolean {
    return UndeclaredTypeAnalyzer._eitherSpelling(
      typeName,
      scope,
      this.context.symbols,
      this.context.symbolTable,
      (name, fileSymbols, table) =>
        NameExistence.isTypeName(name, fileSymbols, table),
    );
  }
 
  /**
   * Whether the name denotes a register (ADR-004). Asked only after
   * `isVisibleType` has already answered no, to tell E0429 from E0426.
   *
   * Cross-file this answers no, and the name falls through to E0426. That is
   * currently TRUE rather than a gap: a register declared in an included file is
   * not visible in the consumer at all (#1453 -- its `#define` is written to the
   * implementation file and the header exports nothing), so "not defined" is the
   * accurate report. When #1453 makes such a register visible it lands in the
   * same `knownRegisters` set this reads, and E0429 begins firing there with no
   * change here.
   *
   * ADR-111: retire this with E0429 when a register becomes a type -- at which
   * point `isVisibleType` answers yes and this is never reached.
   */
  isRegister(
    typeName: string,
    scope: ReturnType<EnclosingScope["current"]>,
  ): boolean {
    return UndeclaredTypeAnalyzer._eitherSpelling(
      typeName,
      scope,
      // Reached only after `isVisibleType` returned false, which requires a
      // symbol view -- see its guard.
      this.context.symbols,
      this.context.symbolTable,
      (name, fileSymbols) => NameExistence.isRegisterName(name, fileSymbols),
    );
  }
 
  /**
   * ADR-057: inside a scope a bare `T` may name a scope-declared type, recorded
   * under its qualified name. The bare spelling stays acceptable because a
   * scope may also reference a global type.
   *
   * Both questions must accept the SAME pair of spellings. A scoped register
   * that only the qualified spelling finds would otherwise fail the type test
   * and then fail the register test too, and be reported as undefined rather
   * than as a register -- so the walk lives here once and is asked twice.
   */
  private static _eitherSpelling(
    typeName: string,
    scope: ReturnType<EnclosingScope["current"]>,
    symbols: ICodeGenSymbols,
    symbolTable: SymbolTable,
    test: (
      name: string,
      symbols: ICodeGenSymbols,
      symbolTable: SymbolTable,
    ) => boolean,
  ): boolean {
    if (scope) {
      const qualified = ScopeUtils.qualifyInScope(typeName, scope);
      if (test(qualified, symbols, symbolTable)) {
        return true;
      }
    }
 
    return test(typeName, symbols, symbolTable);
  }
 
  addError(typeName: string, line: number, column: number): void {
    this._push(
      "E0426",
      typeName,
      line,
      column,
      `type '${typeName}' is not defined`,
    );
  }
 
  /**
   * E0429: the name is declared, but as a register, and a register is not a
   * type (ADR-004).
   *
   * ADR-111: this code exists only for as long as that is true. When ADR-111 is
   * IMPLEMENTED, `Control c;` becomes the instantiation form it designs and
   * this diagnostic is retired outright -- delete the method, its call site,
   * `NameExistence.isRegisterName`, the fixture, and the E0429 registry row.
   */
  addRegisterInTypePositionError(
    typeName: string,
    line: number,
    column: number,
  ): void {
    this._push(
      "E0429",
      typeName,
      line,
      column,
      `'${typeName}' is a register, not a type`,
    );
  }
 
  private _push(
    code: string,
    typeName: string,
    line: number,
    column: number,
    message: string,
  ): void {
    this.errors.push({ code, typeName, line, column, message });
  }
}
 
export default UndeclaredTypeAnalyzer;