mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-20 16:05:00 +08:00
e871c49a31
Post-batch cleanup deleted resolved refs (and parked failed ones) by (from_node_id, reference_name, reference_kind) — no line/col. When one caller had several call sites to the same callee and a batch boundary split them, the first batch's cleanup removed every row with that key, including later-batch siblings that were never attempted — their edges were silently never created. On nlohmann/json this ate 422 real call edges (write_cbor's 38 to_char_type calls indexed as 11). Refs loaded from unresolved_refs now carry their row id through resolution, and all three persist paths (sync resolveAndPersist, the yielding retry pass, the batched drain loop) delete / mark-failed by exactly that id. The key-tuple methods remain only as the fallback for hand-built refs from the public API. Failed-parking gains the same precision: outcome can differ per call site (receiver inference reads the ref's line), so a sibling must not inherit another row's failure. Also untracks the zz-scratch local test files that slipped into #1268 and gitignores the pattern. Validation: red-green regression test (5 sites, batch size 2 — old code kept 2 edges, fix keeps 5); nlohmann/json re-index is a strict superset of the previous edge set (0 lost, 422 recovered, spot-checked against source). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
276 lines
11 KiB
TypeScript
276 lines
11 KiB
TypeScript
/**
|
||
* Reference Resolution Types
|
||
*
|
||
* Types for the reference resolution system.
|
||
*/
|
||
|
||
import { 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';
|
||
}
|
||
|
||
/**
|
||
* 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[];
|
||
/** 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)` 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 `language` —
|
||
* 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[];
|
||
/**
|
||
* 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;
|
||
/**
|
||
* 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[];
|
||
/** 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;
|
||
};
|