All files / PARSE/1-Discover CppEntryPointScanner.ts

92% Statements 46/50
95.23% Branches 20/21
100% Functions 5/5
93.75% Lines 45/48

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                                                                27x 27x 27x 27x                           27x 27x 27x                   27x 27x   27x                             57x 56x     56x 56x               56x 56x 40x                           40x             40x     3x 2x       3x     37x 37x     37x 37x           37x 37x   32x 5x     1x     4x                       32x 32x     32x 8x 9x 9x 2x 2x         32x 6x     6x     26x     26x          
import { dirname, join, resolve } from "node:path";
import IncludeDiscovery from "./IncludeDiscovery";
import CNextMarkerDetector from "./CNextMarkerDetector";
import IFileSystem from "../../types/IFileSystem";
 
/**
 * Result of scanning a C/C++ entry point for C-Next sources.
 */
interface IScanResult {
  /** Absolute paths to discovered .cnx source files */
  cnextSources: string[];
  /** Errors encountered (e.g., missing .cnx files referenced by markers) */
  errors: string[];
  /** Warnings encountered (e.g., includes that couldn't be resolved) */
  warnings: string[];
  /** True if no C-Next markers were found (not an error, just informational) */
  noCNextFound: boolean;
}
 
/**
 * Scans C/C++ entry point files to discover C-Next source dependencies.
 *
 * Walks the include tree starting from a C/C++ file, looks for C-Next
 * generation markers in headers, and extracts the source .cnx paths.
 * Follows includes transitively through discovered .cnx files.
 *
 * Used by the C/C++ entry point feature (ADR-010) to enable compilation
 * where the entry point is a C/C++ file that #includes C-Next generated headers.
 */
class CppEntryPointScanner {
  private readonly fs: IFileSystem;
  private readonly searchPaths: string[];
  private readonly visited = new Set<string>();
  private readonly cnextSources = new Set<string>();
  private readonly errors: string[] = [];
  private readonly warnings: string[] = [];
 
  /**
   * #1706: the search path a discovered `.cnx` file's own includes resolve
   * along -- the one `.cnx` discovery computes for it, discovered tiers
   * included, not the entry point's list with the file's directory in front.
   */
  private readonly cnxSearchPaths: (cnxPath: string) => readonly string[];
 
  constructor(
    searchPaths: string[],
    fs: IFileSystem,
    cnxSearchPaths: (cnxPath: string) => readonly string[],
  ) {
    this.searchPaths = searchPaths;
    this.fs = fs;
    this.cnxSearchPaths = cnxSearchPaths;
  }
 
  /**
   * Scan an entry point file for C-Next source dependencies.
   *
   * @param entryPath - Path to the C/C++ entry point file
   * @returns Scan result with discovered sources, errors, and warnings
   */
  scan(entryPath: string): IScanResult {
    const absolutePath = resolve(entryPath);
    this._scanFile(absolutePath);
 
    return {
      cnextSources: Array.from(this.cnextSources),
      errors: this.errors,
      warnings: this.warnings,
      noCNextFound: this.cnextSources.size === 0 && this.errors.length === 0,
    };
  }
 
  /**
   * Scan a file for includes and process them.
   */
  private _scanFile(
    filePath: string,
    searchPaths: readonly string[] = [dirname(filePath), ...this.searchPaths],
  ): void {
    if (this.visited.has(filePath)) return;
    this.visited.add(filePath);
 
    let content: string;
    try {
      content = this.fs.readFile(filePath);
    } catch {
      this.warnings.push(`Could not read ${filePath}`);
      return;
    }
 
    // #1830 review: a `.cnx` file this reaches is read as the grammar reads
    // it, so a commented-out include does not join the run through this route.
    const includes = IncludeDiscovery.directivesOf(filePath, content);
    for (const includeInfo of includes) {
      this._processInclude(includeInfo, searchPaths, filePath);
    }
  }
 
  /**
   * Process a single include directive.
   */
  private _processInclude(
    includeInfo: { path: string; isLocal: boolean },
    searchPaths: readonly string[],
    fromFile: string,
  ): void {
    // #1672: the one decision 1.1 resolves a .cnx file's includes with, so a
    // quoted C-Next include is found beside `fromFile` and nowhere else.
    const resolved = IncludeDiscovery.resolveSpelling(
      includeInfo,
      dirname(fromFile),
      [...searchPaths],
      this.fs,
    );
 
    if (!resolved) {
      // Only warn for local includes that couldn't be resolved
      // System includes (like <stdio.h>) are expected to not be found
      if (includeInfo.isLocal) {
        this.warnings.push(
          `#include "${includeInfo.path}" not found (from ${fromFile})`,
        );
      }
      return;
    }
 
    const absolutePath = resolve(resolved);
    Iif (this.visited.has(absolutePath)) return;
 
    let headerContent: string;
    try {
      headerContent = this.fs.readFile(absolutePath);
    } catch {
      return;
    }
 
    // Check if this is a C-Next generated header
    const sourcePath = CNextMarkerDetector.extractSourcePath(headerContent);
    if (sourcePath) {
      // New-style marker with source path
      this._handleCNextMarker(sourcePath, absolutePath);
    } else if (CNextMarkerDetector.isCNextGenerated(headerContent)) {
      // Old-style marker without source path - treat as leaf node
      // (we know it's C-Next generated but can't determine the source)
      this.visited.add(absolutePath);
    } else {
      // Not a C-Next generated header - scan it for nested includes
      this._scanFile(absolutePath);
    }
  }
 
  /**
   * Handle a C-Next generation marker found in a header.
   *
   * Resolves the source path by first checking relative to the header location,
   * then searching the include paths. This supports both default behavior (header
   * next to source) and --header-out (header in separate directory).
   */
  private _handleCNextMarker(sourcePath: string, headerPath: string): void {
    const headerDir = dirname(headerPath);
    let absoluteSourcePath = resolve(join(headerDir, sourcePath));
 
    // If not found next to header, search include paths (supports --header-out)
    if (!this.fs.exists(absoluteSourcePath)) {
      for (const searchPath of this.searchPaths) {
        const candidate = resolve(join(searchPath, sourcePath));
        if (this.fs.exists(candidate)) {
          absoluteSourcePath = candidate;
          break;
        }
      }
    }
 
    if (!this.fs.exists(absoluteSourcePath)) {
      this.errors.push(
        `C-Next source not found: ${sourcePath} (referenced by ${headerPath})`,
      );
      return;
    }
 
    this.cnextSources.add(absoluteSourcePath);
    // Scan the .cnx file for its own includes (transitive discovery), along
    // the search path .cnx discovery gives it (#1706)
    this._scanFile(absoluteSourcePath, this.cnxSearchPaths(absoluteSourcePath));
  }
}
 
export default CppEntryPointScanner;