All files / transpiler/logic/analysis UndeclaredValueAnalyzer.ts

77.77% Statements 35/45
54.83% Branches 17/31
85.71% Functions 6/7
77.77% Lines 35/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                                                                                                          254x     254x 254x 254x     53x     53x     53x     53x     850x     850x 850x 850x 608x         242x 242x 31x     211x         211x       211x               211x                 308x     308x         308x 54x     254x 254x   254x       254x                     211x 178x     33x 33x           33x 33x         33x                                                          
/**
 * UndeclaredValueAnalyzer — rejects a bare identifier in a value position that
 * denotes nothing this file can see (E0427).
 *
 * Issue #1353. `#985` closed this hole for a CALL (`E0422`); the value
 * reference was never covered, so `u32 v <- notDeclaredAnywhere;` exited 0 and
 * emitted `uint32_t v = notDeclaredAnywhere;`. The cause is the same shape as
 * #1312's: `TypeValidator.resolveBareIdentifier` returns `string | null` where
 * `null` means BOTH "emit it unchanged, it is fine" (a local needing no rename,
 * or a known global at file scope) and "no idea what this is", so no caller can
 * tell a resolved name from an unresolved one.
 *
 * Three positions can hold an undeclared name and they are one question asked
 * three ways -- "is this name visible here, as kind K?":
 *
 *   | position | owner                              |
 *   | -------- | ---------------------------------- |
 *   | call     | `FunctionCallAnalyzer` (E0422)     |
 *   | type     | `UndeclaredTypeAnalyzer` (E0426)   |
 *   | value    | this analyzer (E0427)              |
 *
 * The shared half is the lookup, not the policy: existence goes through
 * `NameExistence` and `ScopeFrameResolver`, while each position keeps its own
 * rules. E0422's are substantial and specific to calls -- ADR-030 ordering,
 * ADR-040 callable variables, ADR-057 implicit scope calls, stdlib header
 * hints -- and folding them in here would delete working behavior across ten
 * fixtures rather than remove a duplicate decision.
 *
 * A call target is therefore skipped outright: E0422 already owns it, and two
 * diagnostics for one name is worse than one.
 */
 
import { ParseTreeWalker } from "antlr4ng";
import { CNextListener } from "../parser/grammar/CNextListener";
import * as Parser from "../parser/grammar/CNextParser";
import BUILTIN_TYPE_NAMES from "../../constants/BUILTIN_TYPE_NAMES";
import CodeGenState from "../../state/CodeGenState";
import DeclarationScopeCollector from "./DeclarationScopeCollector";
import EnclosingScope from "./helpers/EnclosingScope";
import IUndeclaredValueError from "./types/IUndeclaredValueError";
import NameExistence from "../symbols/NameExistence";
import ParserUtils from "../../../utils/ParserUtils";
import REJECTED_KEYWORDS from "../../constants/REJECTED_KEYWORDS";
import ScopeFrameResolver from "./ScopeFrameResolver";
import ScopeUtils from "../../../utils/ScopeUtils";
 
class UndeclaredValueListener extends CNextListener {
  private readonly analyzer: UndeclaredValueAnalyzer;
 
  // eslint-disable-next-line @typescript-eslint/lines-between-class-members
  private readonly scopes: ScopeFrameResolver;
 
  // eslint-disable-next-line @typescript-eslint/lines-between-class-members
  private readonly enclosing = new EnclosingScope();
 
  constructor(analyzer: UndeclaredValueAnalyzer, scopes: ScopeFrameResolver) {
    super();
    this.analyzer = analyzer;
    this.scopes = scopes;
  }
 
  override enterScopeDeclaration = (
    ctx: Parser.ScopeDeclarationContext,
  ): void => {
    this.enclosing.enter(ctx.IDENTIFIER().getText());
  };
 
  override exitScopeDeclaration = (
    _ctx: Parser.ScopeDeclarationContext,
  ): void => {
    this.enclosing.exit();
  };
 
  override enterPostfixExpression = (
    ctx: Parser.PostfixExpressionContext,
  ): void => {
    const primary = ctx.primaryExpression();
    const identifier = primary?.IDENTIFIER();
    if (!identifier) {
      return;
    }
 
    // `name(...)` is a call. E0422 owns undefined calls, with ADR-030/040/057
    // rules this analyzer deliberately does not reimplement.
    const ops = ctx.postfixOp();
    if (ops.length > 0 && ops[0].getText().startsWith("(")) {
      return;
    }
 
    const name = identifier.getText();
 
    // ADR-026: `break`/`continue` parse as identifiers and are rejected by
    // E0703, which names the structured alternative. Reporting them as
    // undefined would be true and useless.
    Iif (REJECTED_KEYWORDS.has(name) || BUILTIN_TYPE_NAMES.has(name)) {
      return;
    }
 
    Eif (
      this.analyzer.isVisible(
        name,
        this.scopes.frameFor(ctx),
        this.enclosing.current(),
        this.scopes,
      )
    ) {
      return;
    }
 
    const { line, column } = ParserUtils.getPosition(primary);
    this.analyzer.addError(name, line, column);
  };
}
 
class UndeclaredValueAnalyzer {
  private readonly errors: IUndeclaredValueError[] = [];
 
  analyze(tree: Parser.ProgramContext): IUndeclaredValueError[] {
    this.errors.length = 0;
 
    // Same precondition as E0426, and the value axis needs it MORE: a `#define`
    // never reaches the symbol table at all, so `_isKnownForeignName` -- which
    // does catch a header typedef -- has nothing to fall back on for a macro.
    if (CodeGenState.currentFileReachesForeignHeader) {
      return this.errors;
    }
 
    const declarations = new DeclarationScopeCollector();
    ParseTreeWalker.DEFAULT.walk(declarations, tree);
 
    ParseTreeWalker.DEFAULT.walk(
      new UndeclaredValueListener(this, new ScopeFrameResolver(declarations)),
      tree,
    );
    return this.errors;
  }
 
  isVisible(
    name: string,
    frame: Parameters<ScopeFrameResolver["typeOfName"]>[1],
    scope: ReturnType<EnclosingScope["current"]>,
    scopes: ScopeFrameResolver,
  ): boolean {
    // A declared variable -- lexical frames first, then the symbol table for a
    // declaration that arrived through a .cnx include (#1220).
    if (scopes.typeOfName(name, frame) !== null) {
      return true;
    }
 
    const symbols = CodeGenState.symbols;
    Iif (!symbols) {
      return true;
    }
 
    // A function referenced as a value (ADR-029 function-as-type), and a type
    // used as the base of `Type.MEMBER`.
    const symbolTable = CodeGenState.symbolTable;
    Eif (
      NameExistence.isKnownType(name, symbols, symbolTable) ||
      NameExistence.isKnownEnumMember(name, symbols) ||
      CodeGenState.knownFunctions.has(name)
    ) {
      return true;
    }
 
    if (scope) {
      const qualified = ScopeUtils.qualifyInScope(name, scope);
      if (
        NameExistence.isKnownType(qualified, symbols, symbolTable) ||
        CodeGenState.knownFunctions.has(qualified) ||
        (symbols.scopeMembers.get(scope.name)?.has(name) ?? false)
      ) {
        return true;
      }
    }
 
    return false;
  }
 
  addError(identifier: string, line: number, column: number): void {
    this.errors.push({
      code: "E0427",
      identifier,
      line,
      column,
      message: `'${identifier}' is not defined`,
    });
  }
}
 
export default UndeclaredValueAnalyzer;