All files / transpiler/data IncludeResolver.ts

95% Statements 95/100
86% Branches 43/50
92.85% Functions 13/14
96.84% Lines 92/95

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 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400                      14x                                                                                             14x   253x       253x     253x                     255x             255x   255x 83x     255x                     83x           83x 5x         5x     78x                     78x     78x 3x   75x   75x 75x   75x                       75x 42x   42x     42x 42x     33x 33x                         5x   4x 5x                   1x                           1x 1x               4x                               30x 30x           30x 1x 1x     29x                                                           145x 145x 145x 145x 145x   145x 32x   32x 30x   30x           32x   29x 29x   29x 29x   29x 32x   32x 5x           5x       5x 1x 1x         1x     4x 4x   4x 4x   4x     5x       145x 28x     145x 145x   145x 145x 30x 30x 29x       145x                                                         239x     239x     239x 239x 80x 80x 240x 240x 83x           239x          
import { dirname, join, resolve } from "node:path";
 
import IncludeDiscovery from "./IncludeDiscovery";
import FileDiscovery from "./FileDiscovery";
import IDiscoveredFile from "./types/IDiscoveredFile";
import EFileType from "./types/EFileType";
import DependencyGraph from "./DependencyGraph";
import IFileSystem from "../types/IFileSystem";
import NodeFileSystem from "../NodeFileSystem";
 
/** Default file system instance (singleton for performance) */
const defaultFs = NodeFileSystem.instance;
 
/**
 * Result of resolving includes from source content
 */
interface IResolvedIncludes {
  /** C/C++ headers to parse for symbol collection */
  headers: IDiscoveredFile[];
 
  /** C-Next files to parse for symbol collection */
  cnextIncludes: IDiscoveredFile[];
 
  /** Warnings for unresolved local includes */
  warnings: string[];
 
  /**
   * Issue #497: Map from resolved header path to original include directive.
   * Used to include C headers (instead of forward declarations) when their
   * types are used in public interfaces.
   * Example: "/abs/path/data-types.h" => '#include "data-types.h"'
   */
  headerIncludeDirectives: Map<string, string>;
}
 
/**
 * Unified include resolution for the C-Next Pipeline
 *
 * This class encapsulates the complete include resolution workflow,
 * used by both `run()` (CLI) and `transpileSource()` (API) paths.
 *
 * Key responsibilities:
 * - Extract #include directives from source content
 * - Resolve include paths using search directories
 * - Categorize resolved files into headers vs C-Next includes
 * - Track warnings for unresolved local includes
 * - Deduplicate resolved files by path
 *
 * @example
 * const resolver = new IncludeResolver(['/path/to/includes']);
 * const result = resolver.resolve('#include "header.h"');
 * // result.headers contains resolved header files
 */
class IncludeResolver {
  /**
   * Type helper for accessing IResolvedIncludes externally.
   * Use: `type IResolvedIncludes = ReturnType<InstanceType<typeof IncludeResolver>["resolve"]>`
   */
  static readonly _resolvedIncludesType: IResolvedIncludes = undefined as never;
 
  private readonly resolvedPaths: Set<string> = new Set();
  private readonly fs: IFileSystem;
 
  constructor(
    private readonly searchPaths: string[],
    fs: IFileSystem = defaultFs,
  ) {
    this.fs = fs;
  }
 
  /**
   * Extract includes from source content and resolve them to files
   *
   * @param content - Source file content
   * @param sourceFilePath - Optional path to source file (for error messages)
   * @returns Resolved includes categorized by type, plus warnings
   */
  resolve(content: string, sourceFilePath?: string): IResolvedIncludes {
    const result: IResolvedIncludes = {
      headers: [],
      cnextIncludes: [],
      warnings: [],
      headerIncludeDirectives: new Map<string, string>(),
    };
 
    const includes = IncludeDiscovery.extractIncludesWithInfo(content);
 
    for (const includeInfo of includes) {
      this._processInclude(includeInfo, sourceFilePath, result);
    }
 
    return result;
  }
 
  /**
   * Process a single include directive
   */
  private _processInclude(
    includeInfo: { path: string; isLocal: boolean },
    sourceFilePath: string | undefined,
    result: IResolvedIncludes,
  ): void {
    const resolved = IncludeDiscovery.resolveInclude(
      includeInfo.path,
      this.searchPaths,
      this.fs,
    );
 
    if (!resolved) {
      this._handleUnresolvedInclude(
        includeInfo,
        sourceFilePath,
        result.warnings,
      );
      return;
    }
 
    this._handleResolvedInclude(resolved, includeInfo, result);
  }
 
  /**
   * Handle a resolved include path
   */
  private _handleResolvedInclude(
    resolved: string,
    includeInfo: { path: string; isLocal: boolean },
    result: IResolvedIncludes,
  ): void {
    const absolutePath = resolve(resolved);
 
    // Deduplicate by absolute path
    if (this.resolvedPaths.has(absolutePath)) {
      return;
    }
    this.resolvedPaths.add(absolutePath);
 
    const file = FileDiscovery.discoverFile(resolved, this.fs);
    Iif (!file) return;
 
    this._categorizeFile(file, absolutePath, includeInfo, result);
  }
 
  /**
   * Categorize a discovered file into headers or cnext includes
   */
  private _categorizeFile(
    file: IDiscoveredFile,
    absolutePath: string,
    includeInfo: { path: string; isLocal: boolean },
    result: IResolvedIncludes,
  ): void {
    if (file.type === EFileType.CHeader || file.type === EFileType.CppHeader) {
      result.headers.push(file);
      // Issue #497: Track the original include directive for this header
      const directive = includeInfo.isLocal
        ? `#include "${includeInfo.path}"`
        : `#include <${includeInfo.path}>`;
      result.headerIncludeDirectives.set(absolutePath, directive);
      return;
    }
 
    Eif (file.type === EFileType.CNext) {
      result.cnextIncludes.push(file);
    }
  }
 
  /**
   * Handle an unresolved include (warn for local includes only)
   */
  private _handleUnresolvedInclude(
    includeInfo: { path: string; isLocal: boolean },
    sourceFilePath: string | undefined,
    warnings: string[],
  ): void {
    // System includes (<...>) that aren't found are silently ignored
    if (!includeInfo.isLocal) return;
 
    const fromFile = sourceFilePath ? ` (from ${sourceFilePath})` : "";
    warnings.push(
      `Warning: #include "${includeInfo.path}" not found${fromFile}. ` +
        `Struct field types from this header will not be detected.`,
    );
  }
 
  /**
   * Reset the resolved paths set (for reuse across multiple files)
   */
  reset(): void {
    this.resolvedPaths.clear();
  }
 
  /**
   * Get the set of resolved paths (for deduplication across resolver instances)
   */
  getResolvedPaths(): ReadonlySet<string> {
    return this.resolvedPaths;
  }
 
  /**
   * Add already-resolved paths to prevent re-resolution
   */
  addResolvedPaths(paths: Iterable<string>): void {
    for (const path of paths) {
      this.resolvedPaths.add(path);
    }
  }
 
  /**
   * Check if a resolved include is a header file to process.
   */
  private static isProcessableHeader(file: IDiscoveredFile | null): boolean {
    return (
      file !== null &&
      (file.type === EFileType.CHeader || file.type === EFileType.CppHeader)
    );
  }
 
  /**
   * Read header content, returning null if not readable or if generated by C-Next.
   */
  private static readHeaderContent(
    file: IDiscoveredFile,
    fs: IFileSystem,
    warnings: string[],
    onDebug?: (message: string) => void,
  ): string | null {
    let content: string;
    try {
      content = fs.readFile(file.path);
    } catch {
      warnings.push(`Warning: Could not read header ${file.path}`);
      return null;
    }
 
    if (content.includes("Generated by C-Next Transpiler")) {
      onDebug?.(`Skipping C-Next generated header: ${file.path}`);
      return null;
    }
 
    return content;
  }
 
  /**
   * Issue #592: Recursively resolve all headers from a set of root headers.
   *
   * This method handles the recursive include graph traversal that was
   * previously in Transpiler.doCollectHeaderSymbols(). It:
   * - Discovers all nested #include directives
   * - Tracks visited paths to avoid cycles
   * - Returns headers in dependency order (dependencies first)
   * - Skips headers generated by C-Next Transpiler
   *
   * @param rootHeaders - Initial set of headers to resolve from
   * @param includeDirs - Include directories for resolving nested includes
   * @param options - Optional configuration
   * @returns All headers (root + nested) in dependency order
   */
  static resolveHeadersTransitively(
    rootHeaders: IDiscoveredFile[],
    includeDirs: string[],
    options?: {
      /** Callback for debug logging */
      onDebug?: (message: string) => void;
      /** Set of already-processed paths to skip */
      processedPaths?: Set<string>;
      /** File system abstraction (defaults to NodeFileSystem) */
      fs?: IFileSystem;
    },
  ): { headers: IDiscoveredFile[]; warnings: string[] } {
    const fs = options?.fs ?? defaultFs;
    const visited = new Set<string>(options?.processedPaths);
    const warnings: string[] = [];
    const depGraph = new DependencyGraph();
    const fileByPath = new Map<string, IDiscoveredFile>();
 
    const processHeader = (file: IDiscoveredFile): void => {
      const absolutePath = resolve(file.path);
 
      if (visited.has(absolutePath)) return;
      visited.add(absolutePath);
 
      const content = IncludeResolver.readHeaderContent(
        file,
        fs,
        warnings,
        options?.onDebug,
      );
      if (!content) return;
 
      depGraph.addFile(absolutePath);
      fileByPath.set(absolutePath, file);
 
      const includes = IncludeDiscovery.extractIncludesWithInfo(content);
      const searchPaths = [dirname(absolutePath), ...includeDirs];
 
      options?.onDebug?.(`Processing includes in ${file.path}:`);
      options?.onDebug?.(`  Search paths: ${searchPaths.join(", ")}`);
 
      for (const includeInfo of includes) {
        const resolved = IncludeDiscovery.resolveInclude(
          includeInfo.path,
          searchPaths,
          fs,
        );
 
        options?.onDebug?.(
          `  #include "${includeInfo.path}" → ${resolved ?? "NOT FOUND"}`,
        );
 
        if (!resolved) {
          Eif (includeInfo.isLocal) {
            warnings.push(
              `Warning: #include "${includeInfo.path}" not found (from ${file.path}). ` +
                `Struct field types from this header will not be detected.`,
            );
          }
          continue;
        }
 
        const includedFile = FileDiscovery.discoverFile(resolved, fs);
        Iif (!IncludeResolver.isProcessableHeader(includedFile)) continue;
 
        const includedPath = resolve(includedFile!.path);
        depGraph.addDependency(absolutePath, includedPath);
 
        options?.onDebug?.(
          `    → Recursively processing ${includedFile!.path}`,
        );
        processHeader(includedFile!);
      }
    };
 
    for (const header of rootHeaders) {
      processHeader(header);
    }
 
    const sortedPaths = depGraph.getSortedFiles();
    warnings.push(...depGraph.getWarnings());
 
    const sortedHeaders: IDiscoveredFile[] = [];
    for (const path of sortedPaths) {
      const file = fileByPath.get(path);
      if (file) {
        sortedHeaders.push(file);
      }
    }
 
    return { headers: sortedHeaders, warnings };
  }
 
  /**
   * Build search paths from a source file location
   *
   * Consolidates the search path building logic used by both `run()` and
   * `transpileSource()` code paths.
   *
   * Search order (highest to lowest priority):
   * 1. Source file's directory (for relative includes)
   * 2. Additional include directories (e.g., from --include flag)
   * 3. Config include directories
   * 4. Project-level common directories (include/, src/, lib/)
   *
   * @param sourceDir - Directory containing the source file
   * @param includeDirs - Include directories from config
   * @param additionalIncludeDirs - Extra include directories (e.g., from API options)
   * @param projectRoot - Optional project root for common directory discovery
   * @param fs - File system abstraction (defaults to NodeFileSystem)
   * @returns Array of search paths in priority order
   */
  static buildSearchPaths(
    sourceDir: string,
    includeDirs: string[],
    additionalIncludeDirs: string[] = [],
    projectRoot?: string,
    fs: IFileSystem = defaultFs,
  ): string[] {
    const paths: string[] = [];
 
    // Search path priority: 1) source dir, 2) additional dirs, 3) config dirs
    paths.push(sourceDir, ...additionalIncludeDirs, ...includeDirs);
 
    // 4. Project-level common directories
    const root = projectRoot ?? IncludeDiscovery.findProjectRoot(sourceDir, fs);
    if (root) {
      const commonDirs = ["include", "src", "lib"];
      for (const dir of commonDirs) {
        const includePath = join(root, dir);
        if (fs.exists(includePath) && fs.isDirectory(includePath)) {
          paths.push(includePath);
        }
      }
    }
 
    // Remove duplicates while preserving order
    return Array.from(new Set(paths));
  }
}
 
export default IncludeResolver;