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 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 | 14x 14x 265x 265x 265x 265x 267x 267x 267x 97x 267x 97x 97x 5x 5x 92x 92x 92x 3x 89x 89x 89x 89x 89x 51x 51x 51x 51x 38x 38x 38x 38x 38x 38x 5x 4x 5x 1x 1x 1x 5x 35x 35x 35x 1x 1x 34x 153x 153x 153x 153x 153x 153x 37x 37x 35x 35x 37x 34x 34x 34x 34x 34x 37x 37x 7x 7x 7x 2x 2x 2x 5x 5x 5x 5x 5x 7x 153x 32x 153x 153x 153x 153x 35x 35x 34x 153x 252x 252x 252x 252x 83x 83x 249x 249x 86x 252x | 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 the unified `transpile()` entry point for both file and source modes.
*
* 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;
private readonly cppMode: boolean;
/**
* @param cppMode Controls .h vs .hpp extension for .cnx include directives.
* Note: In the Transpiler, cppDetected may change after IncludeResolver runs
* (e.g., when a .hpp header is discovered during Stage 2). HeaderGeneratorUtils
* uses stem-based dedup to handle any resulting .h/.hpp mismatch.
*/
constructor(
private readonly searchPaths: string[],
fs: IFileSystem = defaultFs,
cppMode: boolean = false,
) {
this.fs = fs;
this.cppMode = cppMode;
}
/**
* 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);
// Issue #854: Track header directive for cnext includes so their types
// can be mapped by ExternalTypeHeaderBuilder, preventing duplicate
// forward declarations (MISRA Rule 5.6)
const ext = this.cppMode ? ".hpp" : ".h";
const headerPath = includeInfo.path.replace(/\.cnx$|\.cnext$/, ext);
const directive = includeInfo.isLocal
? `#include "${headerPath}"`
: `#include <${headerPath}>`;
result.headerIncludeDirectives.set(absolutePath, directive);
}
}
/**
* 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 the unified
* transpile() entry point.
*
* 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;
|