mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 22:35:47 +08:00
A C call now links to the function of its name the call's own file can see:
its own, or one a header it includes defines, else one with external
linkage, the best placed of them (cppFunctionOverload's C branch). Before,
the name strategies took the function nearest the call; when that was
another source file's `static`, isVisibleAcrossFiles dropped it and the
call was left unlinked, so no call reached redis's `server.c` `mstime()`
or `ustime()`.
- A source file's `static` function is visible to a file that `#include`s
that source file: hiredis's async.c includes dict.c, redis's ae.c its
event-loop backend.
- The coordinator's `resolveImport` hook falls back to matchByFilePath for
a C/C++ `#include` the import resolver can't place, as resolveOneInner
already does, so cppIncluders sees includes only the end of their path
names: protobuf's `google/protobuf/descriptor.upb.h`, which two generated
copies end in, and jemalloc's `jemalloc/internal/*.h`. Standard headers
are left out: otherwise `#include <math.h>` makes jemalloc's
test/math.h visible to its own include through isIncludedCppTestSuite.
A/B on the cgstd kit against main 4a591aee: leveldb, fmt and rocksdb
identical; redis +194 calls linked and 115 retargeted (114 to hiredis
alloc.h's POSIX `static inline`s, 1 known miss: lua-cjson's fpconv_init to
an inline only `#ifdef USE_INTERNAL_FPCONV` builds use); protobuf 412 upb
calls retargeted from the unincluded php-upb.h amalgamation to the headers
the calling file includes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
5.1 KiB
TypeScript
127 lines
5.1 KiB
TypeScript
/**
|
|
* Which files include a C or C++ file: the files whose `#include` names it,
|
|
* then the files that include those, out to the translation units.
|
|
*
|
|
* Read from the `import` node extraction mints for every `#include`, each
|
|
* resolved by the coordinator's import lookup as the graph's include edge is
|
|
* (by the end of its path when nothing else places it, and never to a project
|
|
* file for a standard header like `<math.h>`), so "includes" here is the
|
|
* include edge a reader sees. The name matcher asks it
|
|
* whether a C or C++ file named like a test is part of a reference's
|
|
* translation unit: protobuf's conformance framework lives in
|
|
* `conformance/conformance_test.h` and `test_runner.h`, which the suites and
|
|
* runners that use it include.
|
|
*
|
|
* The walk starts from the included file, not from the reference's: a handful
|
|
* of such files are asked about, by references from many files, and a file
|
|
* nothing includes — a project's own tests, mostly — costs one map lookup.
|
|
*/
|
|
import type { Language, Node } from '../types';
|
|
import type { ResolutionContext } from './types';
|
|
|
|
/** One `#include` directive, as its import node records it. */
|
|
interface Include {
|
|
id: string;
|
|
/** The path as written: `conformance/conformance_test.h`. */
|
|
spec: string;
|
|
file: string;
|
|
language: Language;
|
|
line: number;
|
|
column: number;
|
|
}
|
|
|
|
interface Memo {
|
|
/** Every C / C++ `#include`, by the file name it spells (`a/b.h` → `b.h`). */
|
|
byName: Map<string, Include[]> | null;
|
|
/** Import node id → the indexed file its `#include` names (null: none). */
|
|
targets: Map<string, string | null>;
|
|
/** File → the files that include it, directly or through other includes. */
|
|
includers: Map<string, ReadonlySet<string>>;
|
|
}
|
|
|
|
const MEMO = new WeakMap<ResolutionContext, Memo>();
|
|
|
|
export function clearCppIncluderMemos(context: ResolutionContext): void {
|
|
MEMO.delete(context);
|
|
}
|
|
|
|
function memoFor(context: ResolutionContext): Memo {
|
|
let memo = MEMO.get(context);
|
|
if (!memo) MEMO.set(context, (memo = { byName: null, targets: new Map(), includers: new Map() }));
|
|
return memo;
|
|
}
|
|
|
|
const isCFamily = (language: string): boolean => language === 'c' || language === 'cpp';
|
|
const fileName = (filePath: string): string => filePath.slice(Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')) + 1);
|
|
|
|
const toInclude = (n: Pick<Node, 'id' | 'name' | 'filePath' | 'language' | 'startLine' | 'startColumn'>): Include =>
|
|
({ id: n.id, spec: n.name, file: n.filePath, language: n.language, line: n.startLine, column: n.startColumn });
|
|
|
|
/** The indexed file an `#include` names, the way its include edge resolves it. */
|
|
function includedFile(include: Include, context: ResolutionContext, memo: Memo): string | null {
|
|
const hit = memo.targets.get(include.id);
|
|
if (hit !== undefined) return hit;
|
|
const resolved = context.resolveImport?.({
|
|
fromNodeId: include.id,
|
|
referenceName: include.spec,
|
|
referenceKind: 'imports',
|
|
line: include.line,
|
|
column: include.column,
|
|
filePath: include.file,
|
|
language: include.language,
|
|
});
|
|
const target = resolved ? context.getNodeById?.(resolved.targetNodeId) : null;
|
|
const file = target?.kind === 'file' ? target.filePath : null;
|
|
memo.targets.set(include.id, file);
|
|
return file;
|
|
}
|
|
|
|
function includesByName(context: ResolutionContext, memo: Memo): Map<string, Include[]> {
|
|
if (memo.byName) return memo.byName;
|
|
const byName = new Map<string, Include[]>();
|
|
const nodes = context.getCppIncludeNodes?.() ?? context.iterateNodesByKind?.('import') ?? context.getNodesByKind('import');
|
|
for (const n of nodes) {
|
|
if (!isCFamily(n.language)) continue;
|
|
const name = fileName(n.name);
|
|
const list = byName.get(name);
|
|
if (list) list.push(toInclude(n));
|
|
else byName.set(name, [toInclude(n)]);
|
|
}
|
|
memo.byName = byName;
|
|
return byName;
|
|
}
|
|
|
|
/** The indexed file the `#include` an import node records names, or null. */
|
|
export function cppIncludedFile(include: Node, context: ResolutionContext): string | null {
|
|
return include.kind === 'import' && isCFamily(include.language) ? includedFile(toInclude(include), context, memoFor(context)) : null;
|
|
}
|
|
|
|
/**
|
|
* Every file that includes `filePath`, directly or through the files that
|
|
* include it: the translation units (and headers) it is part of.
|
|
*/
|
|
export function cppIncluders(filePath: string, context: ResolutionContext): ReadonlySet<string> {
|
|
const memo = memoFor(context);
|
|
const hit = memo.includers.get(filePath);
|
|
if (hit) return hit;
|
|
const byName = includesByName(context, memo);
|
|
const found = new Set<string>();
|
|
const queue = [filePath];
|
|
for (let i = 0; i < queue.length; i++) {
|
|
const target = queue[i]!;
|
|
const name = fileName(target);
|
|
const dot = name.lastIndexOf('.');
|
|
// `#include "foo"` can name `foo.h`: the include resolver tries extensions.
|
|
for (const spelled of dot > 0 ? [name, name.slice(0, dot)] : [name]) {
|
|
for (const include of byName.get(spelled) ?? []) {
|
|
if (include.file === filePath || found.has(include.file)) continue;
|
|
if (includedFile(include, context, memo) !== target) continue;
|
|
found.add(include.file);
|
|
queue.push(include.file);
|
|
}
|
|
}
|
|
}
|
|
memo.includers.set(filePath, found);
|
|
return found;
|
|
}
|