All files / PARSE/1-Discover RunAnchor.ts

96.42% Statements 27/28
90% Branches 27/30
100% Functions 4/4
100% Lines 26/26

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                                                                                                    1011x 1011x 250x   761x   761x     1011x 1011x                                                                           568x           568x 568x 568x 3x 3x 3x     568x 568x                                         524x 524x 3x                         761x 256x     505x       505x   306x         199x       505x          
import { dirname, join, resolve } from "node:path";
 
import IncludeDiscovery from "./IncludeDiscovery";
import PathResolver from "./PathResolver";
import CompileCommandsReader from "./preprocessor/CompileCommandsReader";
import Preprocessor from "./preprocessor/Preprocessor";
import ToolchainDetector from "./preprocessor/ToolchainDetector";
import type ICompileCommandsResult from "./preprocessor/types/ICompileCommandsResult";
import type IToolchain from "./preprocessor/types/IToolchain";
import type IFileSystem from "../../types/IFileSystem";
import type IRunAnchor from "./types/IRunAnchor";
import type ITranspilerConfig from "../../types/ITranspilerConfig";
 
/** The configuration an anchor is decided from. */
type TAnchorSettings = Pick<
  Required<ITranspilerConfig>,
  "includeDirs" | "defines" | "outDir" | "headerOutDir"
>;
 
/**
 * Where a run is anchored (#1719): the one decision of everything that follows
 * from the location of the run's own root.
 *
 * #1444: this was four private methods of `Transpiler`, which anchored at
 * construction (to find the cache's project root) and again at the start of
 * every run's discovery. Both now ask this.
 */
class RunAnchor {
  /**
   * Anchor a run at `anchorPath`.
   *
   * Adopt the compiler's own view from the project's compile_commands.json, if
   * present. Every build system (CMake, PlatformIO, Meson, Zephyr, bear-wrapped
   * Make) emits this database; reading it — rather than mirroring framework
   * include paths in cnext.config.json — lets cnext resolve external headers
   * exactly as the compiler will, which is the same reason clangd reads it.
   * The include paths + defines + compiler are the contract every build system
   * converges on. (Issue #985 external-symbol recovery; unblocks ADR-062.)
   *
   * `previous` is kept for what the move does not change: the same anchor is
   * the same answer, the same project root reads the same database, and the
   * same compiler needs no second toolchain probe -- which shells out, so
   * redoing it per editor request would be the costly part.
   */
  static at(
    anchorPath: string,
    previous: IRunAnchor | null,
    settings: TAnchorSettings,
    fs: IFileSystem,
  ): IRunAnchor {
    const path = resolve(anchorPath);
    if (previous?.path === path) {
      return previous;
    }
    const projectRoot = RunAnchor._projectRootFrom(anchorPath, fs);
    const settled =
      previous !== null && previous.projectRoot === projectRoot
        ? previous
        : RunAnchor._compileDatabaseAt(projectRoot, previous, settings, fs);
    const directory = dirname(path);
    return {
      path,
      directory,
      projectRoot,
      includeDirs: settled.includeDirs,
      defines: settled.defines,
      compiler: settled.compiler,
      preprocessor: settled.preprocessor,
      // Issue #586: path resolution for output files
      pathResolver: new PathResolver(
        {
          inputs: [directory],
          outDir: settings.outDir,
          headerOutDir: settings.headerOutDir,
          // Issue #1547: the stable base for files outside the entry's
          // directory. Same value `_guardIdentity` already measures
          // include-guard identity against, so a file's guard and its header
          // path can no longer disagree about where it sits purely because the
          // shell moved.
          projectRoot,
        },
        fs,
      ),
    };
  }
 
  /**
   * What `projectRoot`'s compile_commands.json contributes: its include search
   * paths after the caller's (so both preprocessing and include-tree resolution
   * see what the compiler sees), its defines beneath the caller's, which win on
   * conflict, and the preprocessor its compiler picks.
   */
  private static _compileDatabaseAt(
    projectRoot: string | undefined,
    previous: IRunAnchor | null,
    settings: TAnchorSettings,
    fs: IFileSystem,
  ): Pick<IRunAnchor, "includeDirs" | "defines" | "compiler" | "preprocessor"> {
    const db = projectRoot
      ? CompileCommandsReader.load(
          join(projectRoot, "compile_commands.json"),
          fs,
        )
      : null;
    const includeDirs = [...settings.includeDirs];
    const seen = new Set(includeDirs);
    for (const path of db?.includePaths ?? []) {
      Eif (!seen.has(path)) {
        seen.add(path);
        includeDirs.push(path);
      }
    }
    const compiler = db?.compiler ?? null;
    return {
      includeDirs,
      defines: { ...db?.defines, ...settings.defines },
      compiler,
      preprocessor:
        previous?.compiler === compiler
          ? previous.preprocessor
          : new Preprocessor(fs, RunAnchor._toolchainForCompileDb(db, fs)),
    };
  }
 
  /**
   * The toolchain to preprocess with, given a discovered compile database. An
   * explicit CNEXT_CROSS_COMPILER override always wins (deferred to Preprocessor's
   * own detection); otherwise adopt the database's compiler if it resolves, else
   * fall back to auto-detection.
   */
  private static _toolchainForCompileDb(
    db: ICompileCommandsResult | null,
    fs: IFileSystem,
  ): IToolchain | undefined {
    Iif (process.env.CNEXT_CROSS_COMPILER) return undefined;
    if (!db?.compiler) return undefined;
    return ToolchainDetector.fromPath(db.compiler, fs) ?? undefined;
  }
 
  /**
   * Determine the project root by walking up from a run's anchor looking for
   * project markers. Returns undefined if no project root can be established,
   * which disables caching to avoid polluting the filesystem with .cnx directories.
   */
  private static _projectRootFrom(
    anchorPath: string,
    fs: IFileSystem,
  ): string | undefined {
    // Start from the anchor: the input, or where a source run's text lives
    if (!anchorPath) {
      return undefined;
    }
 
    const resolvedInput = resolve(anchorPath);
    let startDir: string;
 
    // Determine starting directory based on whether input exists
    if (fs.exists(resolvedInput)) {
      // Input exists - use its directory if file, or itself if directory
      startDir = fs.isFile(resolvedInput)
        ? dirname(resolvedInput)
        : resolvedInput;
    } else {
      // Input doesn't exist - assume it's a file path, use parent directory
      startDir = dirname(resolvedInput);
    }
 
    // No project root disables caching, so no .cnx directory is left behind
    return IncludeDiscovery.findProjectRoot(startDir, fs) ?? undefined;
  }
}
 
export default RunAnchor;