All files / TRANSPILE/1-Analyze IncludeDirectiveAnalyzer.ts

97.56% Statements 40/41
93.75% Branches 15/16
100% Functions 10/10
100% Lines 35/35

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                                                                                                            46x           523x   523x 523x       521x     261x     261x 261x   261x 261x 261x 252x     8x   243x                         261x 261x       261x         252x 252x       252x                 260x 8x           8x                               251x 108x             8x           8x                           243x   7x       7x                           23x 23x                 523x 523x 523x          
/**
 * ADR-010 include directives: E0503, E0504, E0506.
 *
 * #1322. Three throws -- two in `TypeValidator` and one in `IncludeGenerator`
 * -- all reported as `1:0` with the real line appended to the message as
 * `Line N`, and the third with no error code at all.
 *
 * ## What this pass reads: discovery's answer, not discovery's inputs
 *
 * Whether an include resolves, and whether a header's C-Next source is where
 * the same include would find it, are decided once, by 1.1 Discover, while it
 * resolves the file's includes. This pass reads that answer per directive and
 * asks the file system nothing (#1672).
 *
 * #1322 and #1435 handed it discovery's INPUTS instead -- the search path and
 * the quoted-include directory -- because re-deriving them here had already
 * gone wrong twice: codegen's search path lacked the `--include` directories,
 * and its quoted directory started from `dirname(sourcePath)` where discovery
 * started from `workingDir`. With the inputs in hand this pass still made its
 * own decision, along its own branch between the two forms and its own copy
 * of the C-Next extensions. It joined an absolute angle include onto each
 * search directory, where discovery resolves it by its path, so E0504 missed
 * a header whose C-Next source sat beside it; and it asked the file system a
 * second time, so a file that appeared between the two asks was accepted
 * while the run never discovered it.
 *
 * ## E0506 was uncoded
 *
 * `Error: Included C-Next file not found` was a bare `throw new Error`, so it
 * reached the user as `1:0 Code generation failed: Error: …` with no code to
 * look up. It is the code the #1321 audit reserved for it. Discovery already
 * WARNS about the same missing file; the warning is not a diagnostic and the
 * run would exit 0 without this rule, which is why it is relocated rather than
 * deleted as subsumed.
 */
 
import { ParseTreeWalker } from "antlr4ng";
import { CNextListener } from "../../PARSE/2-Parse/grammar/CNextListener";
import * as Parser from "../../PARSE/2-Parse/grammar/CNextParser";
import ParserUtils from "../../utils/ParserUtils";
import invariant from "../../utils/invariant";
import IncludeDirectiveText from "../../utils/IncludeDirectiveText";
import EFileType from "../../PARSE/1-Discover/types/EFileType";
import IncludeDirective from "./helpers/IncludeDirective";
import IIncludeContext from "./types/IIncludeContext";
import IIncludeDirectiveError from "./types/IIncludeDirectiveError";
 
/**
 * Files that carry definitions; including one duplicates every symbol in it.
 *
 * #1840: kinds, not extensions. This was a list of its own (`.c`, `.cpp`,
 * `.cc`, `.cxx`, `.c++`) beside discovery's, and the two had diverged on
 * `.c++`. 1.1 Discover classifies every directive, and this reads its answer.
 */
const IMPLEMENTATION_KINDS: ReadonlySet<EFileType> = new Set([
  EFileType.CSource,
  EFileType.CppSource,
]);
 
class IncludeDirectiveListener extends CNextListener {
  private readonly found: IIncludeDirectiveError[] = [];
 
  public constructor(private readonly context: IIncludeContext) {
    super();
  }
 
  public errors(): IIncludeDirectiveError[] {
    return this.found;
  }
 
  override enterIncludeDirective = (
    ctx: Parser.IncludeDirectiveContext,
  ): void => {
    const spec = IncludeDirective.of(ctx);
    Iif (spec === null) return;
 
    const directive = IncludeDirectiveText.join(spec);
    const kind = this.kindOf(directive);
    if (this.checkImplementationFile(ctx, spec, kind)) return;
    if (
      this.checkMissingCnextFile(ctx, spec, kind, this.resolutionOf(directive))
    ) {
      return;
    }
    this.checkCnextAlternative(
      ctx,
      spec,
      this.context.cnextAlternatives.get(directive),
    );
  };
 
  /**
   * Discovery's answer for this directive. It reads a `.cnx` file's
   * directives with the grammar's own lexer (#1745), so every directive the
   * parser found has one.
   */
  private kindOf(directive: string): EFileType {
    const kind = this.context.kinds.get(directive);
    invariant(
      kind !== undefined,
      `1.1 Discover classified every directive 1.2 parsed (missing ${directive})`,
    );
    return kind;
  }
 
  /** Discovery's resolution of this directive (see `kindOf`). */
  private resolutionOf(directive: string): string | null {
    const resolved = this.context.resolutions.get(directive);
    invariant(
      resolved !== undefined,
      `1.1 Discover resolved every directive 1.2 parsed (missing ${directive})`,
    );
    return resolved;
  }
 
  /** E0503: `#include "helper.c"` -- a definition, not an interface. */
  private checkImplementationFile(
    ctx: Parser.IncludeDirectiveContext,
    spec: { path: string; isLocal: boolean },
    kind: EFileType,
  ): boolean {
    if (!IMPLEMENTATION_KINDS.has(kind)) return false;
    this.report(
      ctx,
      "E0503",
      `Cannot #include implementation file '${spec.path}'. Only header files (.h, .hpp) are allowed.`,
      "An implementation file defines its symbols, so including it defines them again in every includer (ADR-010); include the header instead.",
    );
    return true;
  }
 
  /**
   * E0506: `#include "helper.cnx"` naming a file that is not there.
   *
   * Quoted only, and resolved relative to the including file, because that is
   * what ADR-010 says a quoted include means. An angle include is searched
   * along the run's paths and its absence is discovery's warning to give.
   */
  private checkMissingCnextFile(
    ctx: Parser.IncludeDirectiveContext,
    spec: { path: string; isLocal: boolean },
    kind: EFileType,
    resolved: string | null,
  ): boolean {
    if (!spec.isLocal || kind !== EFileType.CNext) return false;
    if (resolved !== null) return false;
    // The help names no absolute path on purpose. The throw this replaces put
    // the resolved path in its message; it had no fixture, and the first one
    // written for it embedded this machine's checkout directory in an
    // `.expected.error`. A quoted include resolves relative to the including
    // file, so the spelling IS the relative answer and the directory is the
    // one the reader already has open.
    this.report(
      ctx,
      "E0506",
      `Included C-Next file not found: ${spec.path}`,
      "A quoted include is resolved relative to the file it appears in (ADR-010); check the spelling and the path from this file's own directory.",
    );
    return true;
  }
 
  /**
   * E0504: a header is included where its C-Next source sits beside it.
   *
   * The generated header and the `.cnx` describe the same interface, but only
   * the `.cnx` carries what the transpiler needs to check the call.
   */
  private checkCnextAlternative(
    ctx: Parser.IncludeDirectiveContext,
    spec: { path: string; isLocal: boolean },
    cnxPath: string | undefined,
  ): void {
    if (cnxPath === undefined) return;
 
    const instead = IncludeDirectiveText.join({
      path: cnxPath,
      isLocal: spec.isLocal,
    });
    this.report(
      ctx,
      "E0504",
      `Found ${IncludeDirectiveText.join(spec)} but '${cnxPath}' exists at the same location.\n       Use ${instead} instead to use the C-Next version.`,
      "The generated header describes the interface; the C-Next source is what the transpiler can check calls against (ADR-010).",
    );
  }
 
  private report(
    at: Parser.IncludeDirectiveContext,
    code: string,
    message: string,
    helpText: string,
  ): void {
    const { line, column } = ParserUtils.getPosition(at);
    this.found.push({ code, line, column, message, helpText });
  }
}
 
class IncludeDirectiveAnalyzer {
  public analyze(
    tree: Parser.ProgramContext,
    context: IIncludeContext,
  ): IIncludeDirectiveError[] {
    const listener = new IncludeDirectiveListener(context);
    ParseTreeWalker.DEFAULT.walk(listener, tree);
    return listener.errors();
  }
}
 
export default IncludeDirectiveAnalyzer;