feat(resolution): add C/C++ include path resolution (#453)

* feat(resolution): add C/C++ include path resolution

Add full import resolution pipeline for C and C++ #include directives,
connecting extracted import nodes to actual header files in the project.

- Add C/C++ extension resolution (.h, .hpp, .hxx, .cpp, .cc, .cxx)
- Add system header filtering with ~80 C and ~80 C++ stdlib headers
- Add extractCppImports() for #include import mapping extraction
- Add compile_commands.json parsing for -I/-isystem include directories
- Add heuristic include dir discovery (include/, src/, lib/, api/)
- Add resolveCppIncludePath() for include directory search
- Add C/C++ built-in symbol filtering (printf, malloc, std::*, etc.)
- Wire getCppIncludeDirs into ResolutionContext
- Add 13 new tests for C/C++ import resolution and extraction

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* review: wire #include resolution into pipeline + fix builtin filter

The PR landed the include-dir scan logic (loadCppIncludeDirs +
resolveCppIncludePath) but the indexer never reached it: imports
references with referenceName='X.h' fell into resolveViaImport's
symbol-lookup branch (matched extractCppImports' basename-without-ext
localName via .startsWith, then tried to find a symbol named like the
extension and failed). End result on bitcoin-core: 0 new file→file
imports vs main, despite the include-dir scan resolving paths correctly
when probed directly. resolveViaImport now has a C/C++ imports branch
that resolves the include path to the actual file node and returns
that — skipping the irrelevant symbol scan. Measured on bitcoin-core:
+2,059 newly resolved file→file imports (6,027 → 8,086, +34%).

The unconditional CPP_BUILT_INS / C_BUILT_INS filter also misfired:
C/C++ codebases routinely shadow stdlib names (bitcoin's mp::move,
custom allocators with free/malloc, stream classes with read/write/
close/open, logging libs wrapping printf). Filtering those names
killed legitimate edges — 1,179 → 0 for move(), 33 → 0 for free(),
149 → 7 for write() on bitcoin. The filter now defers to
hasAnyPossibleMatch: only filter when no user-defined symbol with the
name exists. std:: prefix stays unconditional (never user-shadowed in
practice). After: printf/free/open/close/read/write/swap all preserved
at main's counts; the std::move-binds-to-mp::move false-positives still
drop (correctly: −2,154 C/C++ calls).

Also: drop the duplicate 'FILE' in C_BUILT_INS; add an end-to-end test
that asserts `#include "X.h"` produces a file→file imports edge in the
real indexing pipeline (not just direct resolver probes); add a test
documenting the cross-language `.h` heuristic claim (Obj-C dirs are
intentionally allowed as C/C++ include dirs); add CHANGELOG entry
under [Unreleased] with measured numbers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Colby McHenry <me@colbymchenry.com>
This commit is contained in:
g122622
2026-05-26 19:56:25 -05:00
committed by GitHub
co-authored by Claude Opus 4.7 Colby McHenry
parent 893256b88e
commit 48eebe1e3e
6 changed files with 799 additions and 4 deletions
+66 -1
View File
@@ -17,7 +17,7 @@ import {
ImportMapping,
} from './types';
import { matchReference } from './name-matcher';
import { resolveViaImport, extractImportMappings, extractReExports } from './import-resolver';
import { resolveViaImport, extractImportMappings, extractReExports, loadCppIncludeDirs } from './import-resolver';
import { detectFrameworks } from './frameworks';
import { synthesizeCallbackEdges } from './callback-synthesizer';
import { loadProjectAliases, type AliasMap } from './path-aliases';
@@ -131,6 +131,49 @@ const PASCAL_BUILT_INS = new Set([
'IInterface', 'IUnknown',
]);
const C_BUILT_INS = new Set([
// Standard C library functions
'printf', 'fprintf', 'sprintf', 'snprintf', 'scanf', 'fscanf', 'sscanf',
'malloc', 'calloc', 'realloc', 'free',
'memcpy', 'memmove', 'memset', 'memcmp', 'memchr',
'strlen', 'strcpy', 'strncpy', 'strcat', 'strncat', 'strcmp', 'strncmp',
'strstr', 'strchr', 'strrchr', 'strtok', 'strdup',
'fopen', 'fclose', 'fread', 'fwrite', 'fgets', 'fputs', 'fputc', 'fgetc',
'feof', 'ferror', 'fflush', 'fseek', 'ftell', 'rewind',
'exit', 'abort', 'atexit', 'atoi', 'atol', 'atof', 'strtol', 'strtoul', 'strtod',
'qsort', 'bsearch',
'abs', 'labs', 'rand', 'srand',
'sin', 'cos', 'tan', 'sqrt', 'pow', 'log', 'log10', 'exp', 'ceil', 'floor', 'fabs',
'time', 'clock', 'difftime', 'mktime', 'localtime', 'gmtime', 'strftime', 'asctime',
'assert', 'errno',
'perror', 'remove', 'rename', 'tmpfile', 'tmpnam',
'getenv', 'system',
'signal', 'raise',
'setjmp', 'longjmp',
'va_start', 'va_end', 'va_arg', 'va_copy',
'NULL', 'EOF', 'BUFSIZ', 'FILENAME_MAX', 'RAND_MAX', 'EXIT_SUCCESS', 'EXIT_FAILURE',
'size_t', 'ptrdiff_t', 'wchar_t', 'intptr_t', 'uintptr_t',
'int8_t', 'int16_t', 'int32_t', 'int64_t',
'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t',
'FILE',
// POSIX additions commonly seen
'stat', 'lstat', 'fstat', 'open', 'close', 'read', 'write', 'pipe',
'fork', 'exec', 'waitpid', 'getpid', 'getppid', 'kill', 'sleep', 'usleep',
'pthread_create', 'pthread_join', 'pthread_mutex_lock', 'pthread_mutex_unlock',
'dlopen', 'dlsym', 'dlclose',
]);
const CPP_BUILT_INS = new Set([
// iostream objects (often used without std:: prefix via using)
'cout', 'cin', 'cerr', 'clog', 'endl', 'flush', 'ws',
'std', // the namespace itself when used as std::something
// Common C++ keywords that leak as references
'nullptr', 'true', 'false', 'this', 'sizeof', 'alignof', 'typeid',
'static_cast', 'dynamic_cast', 'reinterpret_cast', 'const_cast',
'make_unique', 'make_shared', 'make_pair',
'move', 'forward', 'swap',
]);
/**
* Reference Resolver
*
@@ -392,6 +435,10 @@ export class ReferenceResolver {
this.reExportCache.set(filePath, reExports);
return reExports;
},
getCppIncludeDirs: () => {
return loadCppIncludeDirs(this.projectRoot);
},
};
}
@@ -832,6 +879,24 @@ export class ReferenceResolver {
}
}
// C/C++ standard library symbols (printf, malloc, std::vector, etc.).
// Names that collide with user-defined symbols are NOT filtered —
// C and C++ projects routinely shadow stdlib names (custom allocators
// define `malloc`/`free`, stream wrappers define `read`/`write`/`open`,
// containers define `move`/`swap`, logging libs wrap `printf`). Killing
// those resolutions makes the graph wrong, not cleaner. We only filter
// when there's no user node with this name — then name-matching would
// produce zero edges anyway and the filter just short-circuits work.
if (ref.language === 'c' || ref.language === 'cpp') {
// C++ std:: namespace prefix — safe to filter unconditionally,
// since `std::foo` is never a user-defined qualified name in
// tree-sitter output.
if (name.startsWith('std::')) return true;
if (C_BUILT_INS.has(name) || CPP_BUILT_INS.has(name)) {
return !this.hasAnyPossibleMatch(name);
}
}
return false;
}