mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-04 08:45:36 +08:00
`export * as core from '../core/index.js'` was recorded as a plain wildcard,
so a barrel walk for `z.number()` went into core, on through core's own
`export * as regexes`, and landed on the `number` regex constant - ahead of
the `export * from './schemas.js'` that holds the schema function. zod's
`z.string()` (262 calls), `z.number()` (141), `z.email()`, `z.boolean()`...
all went to regex constants.
- A namespace re-export is now its own kind: it supplies only `ns`, and a
member path through it (`z.coerce.number()`, `z.core.safeParse()`)
continues in that module. A 3-segment chain is resolved when its root is an
`import * as` binding and only through the import resolver - untyped chains
stay unresolved as before.
- A named re-export (`export { safeParse } from '../core'`) is found for a
namespace import's member; the walk compared against `'*'` and only reached
those through the misread wildcard.
zod -471 +571 (every regex-constant edge now reaches the schema function;
`z.lt` -> core `_lt`, `z.coerce.string` -> coerce.ts), trpc +4 and kit +1
(namespace-import members through named re-exports). typeorm, hono,
excalidraw, element-plus, starlight unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
404 lines
17 KiB
TypeScript
404 lines
17 KiB
TypeScript
/**
|
||
* Reference Resolution Types
|
||
*
|
||
* Types for the reference resolution system.
|
||
*/
|
||
|
||
import { EdgeKind, Language, Node, ReferenceKind } from '../types';
|
||
|
||
/**
|
||
* An unresolved reference from extraction
|
||
*/
|
||
export interface UnresolvedRef {
|
||
/** ID of the source node containing the reference */
|
||
fromNodeId: string;
|
||
/** The name being referenced */
|
||
referenceName: string;
|
||
/** Type of reference */
|
||
referenceKind: ReferenceKind;
|
||
/** Line where reference occurs */
|
||
line: number;
|
||
/** Column where reference occurs */
|
||
column: number;
|
||
/** File path where reference occurs */
|
||
filePath: string;
|
||
/** Language of the source file */
|
||
language: Language;
|
||
/** Possible qualified names it might resolve to */
|
||
candidates?: string[];
|
||
/** `unresolved_refs.id` when loaded from the database — post-pass cleanup
|
||
* targets exactly this row instead of every same-key sibling (#1269). */
|
||
rowId?: number;
|
||
}
|
||
|
||
/**
|
||
* A resolved reference
|
||
*/
|
||
export interface ResolvedRef {
|
||
/** Original unresolved reference */
|
||
original: UnresolvedRef;
|
||
/** ID of the target node */
|
||
targetNodeId: string;
|
||
/** Confidence score (0-1) */
|
||
confidence: number;
|
||
/** How it was resolved */
|
||
resolvedBy: 'exact-match' | 'import' | 'qualified-name' | 'framework' | 'fuzzy' | 'instance-method' | 'file-path' | 'function-ref';
|
||
/**
|
||
* Edge kind the edge should carry when it is NOT the ref's own kind — a
|
||
* framework that turns a `calls` ref into a `navigates` edge, for example.
|
||
* The original kind is still recorded on the edge as `metadata.refKind`, so
|
||
* re-resolution after a target is removed reconstructs the ref faithfully.
|
||
*/
|
||
edgeKind?: EdgeKind;
|
||
/** Extra metadata the strategy wants persisted on the edge (`href`, …). */
|
||
metadata?: Record<string, unknown>;
|
||
/**
|
||
* The OTHER targets, when one reference names several.
|
||
*
|
||
* A navigation whose destination is a conditional reaches every arm —
|
||
* `!isAdmin ? keyword ? '/search/…' : '/page/…' : '/admin/…'` is one call
|
||
* and three screens — and drawing only the first would hide two places the
|
||
* code goes. `createEdges` fans these out into an edge apiece, sharing this
|
||
* resolution's kind and confidence; each carries its own metadata.
|
||
*
|
||
* The reference itself still resolves ONCE, so the resolution pipeline's
|
||
* bookkeeping — cleanup by row id, counts, re-resolution — is unchanged.
|
||
*/
|
||
alsoTargets?: { targetNodeId: string; metadata?: Record<string, unknown> }[];
|
||
}
|
||
|
||
/**
|
||
* Result of resolution attempt
|
||
*/
|
||
export interface ResolutionResult {
|
||
/** Successfully resolved references */
|
||
resolved: ResolvedRef[];
|
||
/** References that couldn't be resolved */
|
||
unresolved: UnresolvedRef[];
|
||
/** Statistics */
|
||
stats: {
|
||
total: number;
|
||
resolved: number;
|
||
unresolved: number;
|
||
byMethod: Record<string, number>;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Context for resolution - provides access to the graph
|
||
*/
|
||
export interface ResolutionContext {
|
||
/** Get all nodes in a file */
|
||
getNodesInFile(filePath: string): Node[];
|
||
/** Whether any node in the file is exported (`getNodesInFile(f).some(n => n.isExported)`), as one indexed probe. */
|
||
fileHasExportedNode?(filePath: string): boolean;
|
||
/** `getNodesInFile(f).filter(n => n.isExported)`, without decoding the rest of the file. */
|
||
getExportedNodesInFile?(filePath: string): Node[];
|
||
/** `getNodesInFile(f).filter(n => n.name === name)`, without decoding the rest of the file. */
|
||
getNodesInFileNamed?(filePath: string, name: string): Node[];
|
||
/** Get all nodes by name */
|
||
getNodesByName(name: string): Node[];
|
||
/** Get all nodes by qualified name */
|
||
getNodesByQualifiedName(qualifiedName: string): Node[];
|
||
/** Get all nodes of a kind */
|
||
getNodesByKind(kind: Node['kind']): Node[];
|
||
/**
|
||
* Stream nodes of a kind one at a time instead of materializing (and, unlike
|
||
* `getNodesByKind`, without populating the resolver's per-kind array cache).
|
||
* For unbounded kinds (`function`, `method`, `struct`) on a symbol-dense
|
||
* project the full array is gigabytes — the dynamic-edge synthesizers must
|
||
* use this so their memory stays O(1) in node count (#610, #1212). Optional
|
||
* so minimal test contexts compile; callers fall back to getNodesByKind.
|
||
*/
|
||
iterateNodesByKind?(kind: Node['kind']): IterableIterator<Node>;
|
||
/** Check if a file exists */
|
||
fileExists(filePath: string): boolean;
|
||
/** Read file content */
|
||
readFile(filePath: string): string | null;
|
||
/** `readFile(filePath)?.includes(needle) ?? false` for an ASCII `needle`, without decoding a file that lacks it. */
|
||
fileContains?(filePath: string, needle: string): boolean;
|
||
/**
|
||
* `readFile(filePath)` split into lines, LRU-cached per file. Receiver-type
|
||
* inference scans source lines for EVERY `receiver.method()` ref; splitting
|
||
* the whole file per ref made that O(refs-in-file × file-size) — ~20% of
|
||
* total index CPU on a Java-heavy repo and a driver of the #1122 watchdog
|
||
* kill on large ones. Optional so external/test contexts compile without it;
|
||
* callers fall back to splitting `readFile` themselves.
|
||
*/
|
||
getFileLines?(filePath: string): string[] | null;
|
||
/**
|
||
* The method-definition nodes matching `typeName::methodName` in the language family —
|
||
* exactly `resolveMethodOnType`'s kind/language/qualifiedName-suffix filter,
|
||
* LRU-cached per (language, type, method). The uncached path re-fetches every
|
||
* node sharing the METHOD name (unbounded — tens of thousands on a collision-
|
||
* heavy Java repo) and re-scans it per ref, the dominant term in the #1122
|
||
* watchdog kill. Cached entries hold only the small filtered result; per-ref
|
||
* disambiguation (import FQN, call-site file) stays in the caller so a cached
|
||
* entry is valid from any call site. Optional for external/test contexts.
|
||
*/
|
||
getMethodMatches?(typeName: string, methodName: string, language: Language): Node[];
|
||
/** Get project root */
|
||
getProjectRoot(): string;
|
||
/** Get all files */
|
||
getAllFiles(): string[];
|
||
/** Get nodes by lowercase name (O(1) lookup for fuzzy matching) */
|
||
getNodesByLowerName(lowerName: string): Node[];
|
||
/**
|
||
* Direct supertypes of the type named `typeName` (same language): the classes
|
||
* it extends and the interfaces / protocols / traits it implements/conforms to,
|
||
* by simple name. Backed by the resolved `implements`/`extends` edges, so it is
|
||
* EMPTY during the first resolution pass (edges aren't built yet) and populated
|
||
* afterward — the conformance pass uses it to resolve a chained method defined
|
||
* on a supertype the receiver type conforms to (e.g. a protocol-extension
|
||
* method). Optional so external/test contexts compile without it.
|
||
*/
|
||
getSupertypes?(typeName: string, language: Language): string[];
|
||
/**
|
||
* Look up a node by its id. Lets matchers derive the FROM-symbol's
|
||
* enclosing-class scope (Swift implicit-self method scoping, `this.X`
|
||
* member resolution). Optional so external/test contexts compile
|
||
* without it.
|
||
*/
|
||
getNodeById?(id: string): Node | null;
|
||
/** Get cached import mappings for a file */
|
||
getImportMappings(filePath: string, language: Language): ImportMapping[];
|
||
/** Import lookup supplied by the coordinator, keeping name matching from
|
||
* importing the import resolver (which itself uses name-matching helpers).
|
||
* Minimal contexts without import resolution may omit this capability. */
|
||
resolveImport?(ref: UnresolvedRef): ResolvedRef | null;
|
||
/**
|
||
* Whether an import specifier, written in `fromFile`, names a module outside
|
||
* the repository: a package that is not a relative path, a tsconfig alias, a
|
||
* workspace member or the repository's own package name, and that resolves
|
||
* to no project file. Supplied by the coordinator, like `resolveImport`.
|
||
*/
|
||
isOutOfRepoImport?(source: string, fromFile: string, language: Language): boolean;
|
||
/**
|
||
* Project import-path aliases (tsconfig/jsconfig `paths`). Returns
|
||
* `null` when the project doesn't define any. Cached per resolver
|
||
* instance — safe to call from any resolver code path. Optional so
|
||
* existing test fixtures and external context implementations
|
||
* compile without modification; production resolver implements it.
|
||
*/
|
||
getProjectAliases?(): import('./path-aliases').AliasMap | null;
|
||
/**
|
||
* The aliases of the tsconfig / jsconfig nearest `fromFile` below the
|
||
* project root that declares `paths` — a monorepo app's own `@/*` — or null.
|
||
*/
|
||
getNearestAliases?(fromFile: string): import('./path-aliases').AliasMap | null;
|
||
/**
|
||
* Go module info from `go.mod` at the project root. Returns `null`
|
||
* when the project has no `go.mod` (non-Go projects, pre-modules
|
||
* Go code, or projects whose modules live in subdirectories). Used
|
||
* by the Go branch of import resolution to distinguish in-module
|
||
* cross-package imports from third-party packages.
|
||
*/
|
||
getGoModule?(): import('./go-module').GoModule | null;
|
||
/**
|
||
* Monorepo workspace member packages, keyed by declared package name.
|
||
* Returns `null` for single-package repos (no `workspaces` field).
|
||
* Lets the resolver treat `@scope/ui/sub` as a local import into the
|
||
* member's directory instead of an external npm package (#629).
|
||
*/
|
||
getWorkspacePackages?(): import('./workspace-packages').WorkspacePackages | null;
|
||
/**
|
||
* Re-exports declared by a file (`export { x } from './other'`,
|
||
* `export * from './other'`). Empty array when the file has none.
|
||
* Optional so older callers compile; the import resolver follows
|
||
* re-export chains when this is provided.
|
||
*/
|
||
getReExports?(filePath: string, language: Language): ReExport[];
|
||
/**
|
||
* List immediate subdirectories of `relativePath` (relative to the
|
||
* project root). Returns an empty array when the path doesn't exist
|
||
* or isn't a directory. Used by framework resolvers that need to
|
||
* walk build-system metadata (e.g. Cargo workspace globs). Optional
|
||
* so external context implementations and test fixtures compile
|
||
* without modification.
|
||
*/
|
||
listDirectories?(relativePath: string): string[];
|
||
/**
|
||
* C/C++ include search directories (relative to project root),
|
||
* extracted from compile_commands.json or discovered by heuristic.
|
||
* Used by resolveCppIncludePath to search -I directories when
|
||
* relative resolution fails. Optional so existing callers compile.
|
||
*/
|
||
getCppIncludeDirs?(): string[];
|
||
}
|
||
|
||
/**
|
||
* Result of framework-specific file extraction.
|
||
*/
|
||
export interface FrameworkExtractionResult {
|
||
/** Framework-specific nodes (e.g. routes) */
|
||
nodes: Node[];
|
||
/** Framework-specific unresolved references (e.g. route -> handler) */
|
||
references: UnresolvedRef[];
|
||
}
|
||
|
||
/**
|
||
* Framework-specific resolver
|
||
*/
|
||
export interface FrameworkResolver {
|
||
/** Framework name */
|
||
name: string;
|
||
/** Languages this framework applies to. If omitted, applies to all languages. */
|
||
languages?: Language[];
|
||
/**
|
||
* Packages an app declares when it is built on this framework. When set,
|
||
* `extract()` runs only on files of an app whose package.json — the file's
|
||
* own or an enclosing one — declares one of them: in a monorepo with an Expo
|
||
* app beside a Next.js app, Expo Router must not read the Next app's
|
||
* `app/layout.tsx` as a `/layout` screen. When no manifest in the project
|
||
* declares any, detection found the framework by other evidence and the
|
||
* extractor runs on every file, as before.
|
||
*/
|
||
appDependencies?: readonly string[];
|
||
/** Detect if project uses this framework (project-level, called once at startup) */
|
||
detect(context: ResolutionContext): boolean;
|
||
/** Resolve a reference using framework-specific patterns */
|
||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null;
|
||
/**
|
||
* Opt a reference NAME through the resolver's name-exists pre-filter, even when
|
||
* no node is named that. Needed for dynamic dispatch where the call target is
|
||
* an attribute/descriptor, not a declared symbol (e.g. Django's
|
||
* `self._iterable_class(...)`, React effect callbacks). Returning true lets the
|
||
* ref reach `resolve()` instead of being dropped for having no name match.
|
||
*/
|
||
claimsReference?(name: string): boolean;
|
||
/**
|
||
* Extract framework-specific nodes and references from a file.
|
||
*
|
||
* Returns route nodes, middleware nodes, etc., plus unresolved references
|
||
* that link those nodes to handlers (view classes, controller methods,
|
||
* included modules). Unresolved references flow into the normal resolution
|
||
* pipeline; the framework's own `resolve()` is one of the strategies tried.
|
||
*/
|
||
extract?(filePath: string, content: string): FrameworkExtractionResult;
|
||
/**
|
||
* Cross-file finalization pass, called once after all per-file extraction
|
||
* completes (and again on every incremental sync). Used by frameworks where
|
||
* a symbol's final representation depends on a sibling file the per-file
|
||
* `extract()` never saw — e.g. NestJS's `RouterModule.register([...])`
|
||
* sets route prefixes for controllers declared elsewhere.
|
||
*
|
||
* Implementations return route/etc. nodes with mutated fields (typically
|
||
* `name`); the orchestrator persists each via `updateNode`. The node `id`
|
||
* MUST be preserved so existing edges (route → handler, etc.) stay intact;
|
||
* `qualifiedName` SHOULD be preserved so the pass stays idempotent — a
|
||
* second run can recover the original in-file form from `qualifiedName`.
|
||
*/
|
||
postExtract?(context: ResolutionContext): Node[];
|
||
}
|
||
|
||
/**
|
||
* Import mapping from a file
|
||
*/
|
||
export interface ImportMapping {
|
||
/** Local name used in the file */
|
||
localName: string;
|
||
/** Original exported name (may differ due to aliasing) */
|
||
exportedName: string;
|
||
/** Source module/path */
|
||
source: string;
|
||
/** Whether it's a default import */
|
||
isDefault: boolean;
|
||
/** Whether it's a namespace import (import * as X) */
|
||
isNamespace: boolean;
|
||
/** Resolved file path (if local) */
|
||
resolvedPath?: string;
|
||
}
|
||
|
||
/**
|
||
* Re-export from a file: `export { x } from './other'` or
|
||
* `export * from './other'`. Used by the resolver to chase
|
||
* symbols through barrel files.
|
||
*/
|
||
export type ReExport =
|
||
| {
|
||
kind: 'named';
|
||
/** Name as exported by THIS file. */
|
||
exportedName: string;
|
||
/** Name in the upstream module (differs when renamed: `as`). */
|
||
originalName: string;
|
||
/** Module specifier of the upstream module. */
|
||
source: string;
|
||
}
|
||
| {
|
||
kind: 'wildcard';
|
||
/** Module specifier of the upstream module. */
|
||
source: string;
|
||
}
|
||
| {
|
||
/** `export * as ns from './other'`: only `ns` is exported, the module's members through it. */
|
||
kind: 'namespace';
|
||
exportedName: string;
|
||
source: string;
|
||
};
|
||
|
||
/**
|
||
* Node kinds an `extends`/`implements` edge may legally TARGET — the things a
|
||
* type can actually inherit from or conform to.
|
||
*
|
||
* Kept deliberately wide: `type_alias` because TS `class X implements
|
||
* SomeAliasedObjectType` is valid, `component` because a framework component
|
||
* node stands in for a class, and `module`/`namespace` because whole
|
||
* languages inherit from one — Ruby `include Trackable` targets a `module`,
|
||
* Erlang `-behaviour(gen_server)` targets the behaviour module, which Erlang
|
||
* extraction indexes as a `namespace` (the conformance pass in
|
||
* `resolution/index.ts` makes the same `module` allowance).
|
||
*
|
||
* Everything omitted (`enum_member`, `method`, `field`, `property`,
|
||
* `variable`, `constant`, `function`, `parameter`, `import`, `export`,
|
||
* `file`, `route`) can never be a supertype in any supported language, so an
|
||
* inheritance edge pointing at one is false data.
|
||
*
|
||
* Why this is needed: the name-matcher scores node kind as a BONUS,
|
||
* never a filter, and awards no bonus at all for inheritance refs — so a
|
||
* same-named non-type outranked (or, as the sole candidate, was adopted
|
||
* outright as) the real supertype. Rust `use std::error::Error;` + `impl Error
|
||
* for MapperError {}` bound to the local `MapperError::Error` VARIANT. The
|
||
* supertype is out-of-repo and simply unresolvable; a failed ref is correct.
|
||
*/
|
||
export const SUPERTYPE_TARGET_KINDS = new Set<Node['kind']>([
|
||
'class', 'struct', 'interface', 'trait', 'protocol', 'enum', 'union',
|
||
'type_alias', 'component', 'module', 'namespace',
|
||
]);
|
||
|
||
/** Scala singleton objects are values, unlike inheritable Ruby modules. */
|
||
export function isSupertypeTarget(node: Node): boolean {
|
||
return SUPERTYPE_TARGET_KINDS.has(node.kind) &&
|
||
!(node.language === 'scala' && node.kind === 'module');
|
||
}
|
||
|
||
/** True for the reference kinds that assert an inheritance/conformance relation. */
|
||
export function isInheritanceRef(ref: UnresolvedRef): boolean {
|
||
return ref.referenceKind === 'extends' || ref.referenceKind === 'implements';
|
||
}
|
||
|
||
/**
|
||
* Node kinds an `imports` edge may never TARGET: members that only exist
|
||
* INSIDE a type. No language lets you import a class's property, an
|
||
* interface's method or an enum's variant — you import the type that
|
||
* contains it. The name-matcher has no kind filter, so a bare
|
||
* `import path from 'node:path'` (unresolvable, since the module is external)
|
||
* name-matched an interface property called `path` in an unrelated file.
|
||
*/
|
||
const NON_IMPORTABLE_KINDS = new Set<Node['kind']>([
|
||
'property', 'field', 'method', 'enum_member', 'parameter',
|
||
]);
|
||
|
||
/** Can an `imports` reference legally resolve to this node kind? */
|
||
export function isImportableKind(kind: Node['kind']): boolean {
|
||
return !NON_IMPORTABLE_KINDS.has(kind);
|
||
}
|
||
|
||
/**
|
||
* The signature extraction gives a C/C++ `constant` minted from a
|
||
* function-like `preproc_function_def` (`#define NAME(args) …`, #1838). A
|
||
* macro is a value: it is never a `calls` target, and its presence in a
|
||
* translation unit is what makes `NAME(x)` a macro expansion rather than a
|
||
* call.
|
||
*/
|
||
export const CPP_DEFINE_SIGNATURE = /^\s*#\s*define\b/;
|