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:
co-authored by
Claude Opus 4.7
Colby McHenry
parent
893256b88e
commit
48eebe1e3e
+66
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user