feat(SM-18): Delete lookupFuzzy, lookupFuzzyCallable, globalIndex, callableIndex (#769)
* Initial plan
* Update test files for SymbolTable interface changes
Remove lookupFuzzy, lookupFuzzyCallable, globalIndex, and callableIndex
references from all test files. Replace lookupFuzzyCallable with
lookupCallableByName. Update getStats assertions to only expect
{ fileCount }. Remove tests that exclusively tested removed methods.
Files updated:
- symbol-table.test.ts: Remove lookupFuzzy describe block and all
globalIndex/callableIndex tests, update callable method references
- symbol-resolver.test.ts: Remove SM-16 lookupFuzzy test block,
update Tier 3 describe title
- type-env.test.ts: Update all mock SymbolTable objects and spy
variable names
- call-form.test.ts: Update ownerId propagation test
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(SM-18): Remove lookupFuzzy, lookupFuzzyCallable, globalIndex, callableIndex
Remove from SymbolTable interface and implementation:
- lookupFuzzy method
- lookupFuzzyCallable method
- globalIndex Map
- callableIndex Map (renamed to callableByName, backing lookupCallableByName)
Add lookupCallableByName as the targeted replacement for fuzzy callable
lookups. Migrate all production callers:
- resolution-context.ts: lookupFuzzyCallable → lookupCallableByName
- type-env.ts: lookupFuzzyCallable → lookupCallableByName
- call-processor.ts: lookupFuzzy → lookupCallableByName (D2 widen paths)
Remove fuzzyCallCount/fuzzyCallableCallCount stats and globalSymbolCount
from getStats(). Update pipeline.ts logging accordingly.
Memory savings: globalIndex stored every non-Property symbol (typically
the largest index by entry count). Removing it eliminates one Map plus
all its per-name arrays — net savings proportional to unique symbol
count in the project.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/4a658c69-41a9-4d57-8527-50ca544ca967
* fix(SM-18): address all PR #769 review findings
1. type-env.test.ts mock: add missing lookupImplByName + getFiles methods.
2. Macro/Delegate tests: 2 new tests confirm C/C++ Macro and C# Delegate
are indexed in callableByName.
3. D2 widen path test: module-alias scenario verifying lookupCallableByName
resolves methods in aliased files that shadow same-file definitions.
4. CALLABLE_TYPES unified: exported from symbol-table.ts (single source of
truth), imported in call-processor.ts. Removed duplicate
CALLABLE_SYMBOL_TYPES constant.
5. getStats() observability restored: tier hit counters (tierSameFile,
tierImportScoped, tierGlobal, tierMiss) replace the removed
fuzzyCallCount diagnostic.
* chore(SM-18): remove unnecessary `as any` casts on valid NodeLabel types
Macro, Delegate, TypeAlias, Const, and Variable are all valid NodeLabel
values in gitnexus-shared. The casts suppressed type checking without
purpose and signaled false uncertainty.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
This commit is contained in:
co-authored by
Copilot
Gergo Magyar
parent
e5dafce9f2
commit
100858f8c8
@@ -1,7 +1,7 @@
|
||||
import { KnowledgeGraph } from '../graph/types.js';
|
||||
import { ASTCache } from './ast-cache.js';
|
||||
import type { SymbolDefinition, SymbolTable } from './symbol-table.js';
|
||||
import { CLASS_TYPES } from './symbol-table.js';
|
||||
import { CLASS_TYPES, CALLABLE_TYPES } from './symbol-table.js';
|
||||
import Parser from 'tree-sitter';
|
||||
import type { ResolutionContext } from './resolution-context.js';
|
||||
import { TIER_CONFIDENCE, type ResolutionTier } from './resolution-context.js';
|
||||
@@ -1299,7 +1299,7 @@ export const processCalls = async (
|
||||
return collectedHeritage;
|
||||
};
|
||||
|
||||
const CALLABLE_SYMBOL_TYPES = new Set(['Function', 'Method', 'Constructor', 'Macro', 'Delegate']);
|
||||
// CALLABLE_TYPES imported from symbol-table.ts — single source of truth.
|
||||
|
||||
const CONSTRUCTOR_TARGET_TYPES = new Set(['Constructor', 'Class', 'Struct', 'Record']);
|
||||
|
||||
@@ -1317,10 +1317,10 @@ const filterCallableCandidates = (
|
||||
} else {
|
||||
const types = candidates.filter((c) => CONSTRUCTOR_TARGET_TYPES.has(c.type));
|
||||
kindFiltered =
|
||||
types.length > 0 ? types : candidates.filter((c) => CALLABLE_SYMBOL_TYPES.has(c.type));
|
||||
types.length > 0 ? types : candidates.filter((c) => CALLABLE_TYPES.has(c.type));
|
||||
}
|
||||
} else {
|
||||
kindFiltered = candidates.filter((c) => CALLABLE_SYMBOL_TYPES.has(c.type));
|
||||
kindFiltered = candidates.filter((c) => CALLABLE_TYPES.has(c.type));
|
||||
}
|
||||
|
||||
if (kindFiltered.length === 0) return [];
|
||||
@@ -1476,7 +1476,7 @@ const dedupSwiftExtensionCandidates = (
|
||||
*
|
||||
* If filtering still leaves multiple candidates, refuse to emit a CALLS edge.
|
||||
*/
|
||||
/** Per-file cache for the widen path's lookupFuzzy calls. Cleared between files. */
|
||||
/** Per-file cache for the widen path's lookupCallableByName calls. Cleared between files. */
|
||||
type WidenCache = Map<string, readonly SymbolDefinition[]>;
|
||||
|
||||
/** @internal Exported for unit tests of D0 skip conditions (SM-11). Do not use outside tests. */
|
||||
@@ -1577,7 +1577,7 @@ const resolveCallTarget = (
|
||||
// the caller defines a function with the same name as the callee (Issue #417).
|
||||
//
|
||||
// Tracks `aliasNarrowed` so the D2 widening step below does NOT undo the alias filtering
|
||||
// by calling lookupFuzzy again (which would re-introduce homonym candidates from other files).
|
||||
// by calling lookupCallableByName again (which would re-introduce homonym candidates from other files).
|
||||
let aliasNarrowed = false;
|
||||
if (call.callForm === 'member' && call.receiverName) {
|
||||
const aliasMap = ctx.moduleAliasMap?.get(currentFile);
|
||||
@@ -1591,12 +1591,12 @@ const resolveCallTarget = (
|
||||
} else {
|
||||
// Same-file tier returned a local match, but the alias points elsewhere.
|
||||
// Widen to global candidates and filter to the aliased module's file.
|
||||
// Use per-file widenCache to avoid repeated lookupFuzzy for the same
|
||||
// Use per-file widenCache to avoid repeated lookupCallableByName for the same
|
||||
// calledName+moduleFile from multiple call sites in the same file.
|
||||
const cacheKey = `${call.calledName}\0${moduleFile}`;
|
||||
let fuzzyDefs = widenCache?.get(cacheKey);
|
||||
if (!fuzzyDefs) {
|
||||
fuzzyDefs = ctx.symbols.lookupFuzzy(call.calledName);
|
||||
fuzzyDefs = ctx.symbols.lookupCallableByName(call.calledName);
|
||||
widenCache?.set(cacheKey, fuzzyDefs);
|
||||
}
|
||||
const widened = filterCallableCandidates(fuzzyDefs, call.argCount, call.callForm).filter(
|
||||
@@ -1668,17 +1668,17 @@ const resolveCallTarget = (
|
||||
const typeFiles = new Set(typeResolved.candidates.map((d) => d.filePath));
|
||||
|
||||
// D2. Widen candidates: same-file tier may miss the parent's method when
|
||||
// it lives in another file. Query the symbol table directly for all
|
||||
// it lives in another file. Query the callable index directly for all
|
||||
// global methods with this name, then apply arity/kind filtering.
|
||||
//
|
||||
// When the candidate set was already narrowed by module-alias
|
||||
// disambiguation, do NOT widen back to the full fuzzy pool — that
|
||||
// disambiguation, do NOT widen back to the full callable pool — that
|
||||
// would undo the alias narrowing and reintroduce homonym candidates
|
||||
// from other files.
|
||||
const methodPool =
|
||||
filteredCandidates.length <= 1 && !aliasNarrowed
|
||||
? filterCallableCandidates(
|
||||
ctx.symbols.lookupFuzzy(call.calledName),
|
||||
ctx.symbols.lookupCallableByName(call.calledName),
|
||||
call.argCount,
|
||||
call.callForm,
|
||||
)
|
||||
@@ -1715,7 +1715,7 @@ const resolveCallTarget = (
|
||||
// through to the permissive single-candidate tail return.
|
||||
//
|
||||
// Addresses Codex review finding R3 (PR #744): member calls where
|
||||
// fuzzy fallback picked a globally-matching symbol that has no
|
||||
// widening picked a globally-matching symbol that has no
|
||||
// relationship to the receiver's class hierarchy were silently
|
||||
// producing false-positive edges. Example: Rust `c.trait_only()` where
|
||||
// `trait_only` is captured as a Function node with no ownerId — it
|
||||
@@ -1920,12 +1920,12 @@ const resolveFieldOwnership = (
|
||||
*
|
||||
* After deduplication:
|
||||
*
|
||||
* - 0 unique matches → `undefined` (owner-scoped path has no answer; D1-D4 fuzzy
|
||||
* fallback in `resolveCallTarget` may still find something via lookupFuzzy)
|
||||
* - 0 unique matches → `undefined` (owner-scoped path has no answer; D1-D4
|
||||
* fallback in `resolveCallTarget` may still find something via callable index)
|
||||
* - 1 unique match → return it
|
||||
* - ≥2 unique matches → `undefined` (genuine homonym ambiguity; don't silently pick one)
|
||||
*
|
||||
* This absorbs what was previously D4's job inside `resolveCallTarget` — "filter fuzzy
|
||||
* This absorbs what was previously D4's job inside `resolveCallTarget` — "filter
|
||||
* candidates to those whose ownerId is in the receiver type's nodeId set" — into the
|
||||
* owner-scoped path, aligning with the plan's target:
|
||||
*
|
||||
@@ -1933,8 +1933,7 @@ const resolveFieldOwnership = (
|
||||
*
|
||||
* The returned `tier` reflects how the owner TYPE was resolved (not the method name).
|
||||
* Threaded out here so callers don't need a second `ctx.resolve(ownerType, ...)` call —
|
||||
* this decouples callers from `ctx.resolve`'s per-file caching contract, which SM-16
|
||||
* will restructure when it replaces the `lookupFuzzy` data source.
|
||||
* this decouples callers from `ctx.resolve`'s per-file caching contract.
|
||||
*/
|
||||
const resolveMethodByOwner = (
|
||||
receiverTypeName: string,
|
||||
@@ -2064,17 +2063,12 @@ export const resolveMemberCall = (
|
||||
* {@link resolveCallTarget} delegates here for `callForm === 'free'` before
|
||||
* processing constructor and member calls.
|
||||
*
|
||||
* **Design note (SM-13):** This path still falls through to Tier 3 (global)
|
||||
* via `ctx.resolve()`. Fuzzy global resolution remains until Phase 5 replaces
|
||||
* `lookupFuzzy` with a scoped data source.
|
||||
*
|
||||
* **Asymmetry vs `resolveCallTarget`:** `resolveFreeCall` intentionally does
|
||||
* NOT take a `widenCache` parameter and does NOT run a D2 fuzzy-widening
|
||||
* NOT take a `widenCache` parameter and does NOT run a D2 widening
|
||||
* pass. Member calls (`resolveCallTarget`'s main body) widen via
|
||||
* `lookupFuzzy` to reach parent-class methods defined in different files;
|
||||
* `lookupCallableByName` to reach parent-class methods defined in different files;
|
||||
* free calls have no receiver type and rely exclusively on the tiered pool
|
||||
* from `ctx.resolve()`. Phase 5 will revisit whether free calls need a
|
||||
* scoped widening pass once `lookupFuzzy` is retired.
|
||||
* from `ctx.resolve()`.
|
||||
*
|
||||
* @param calledName - The called function name (e.g. 'doStuff')
|
||||
* @param filePath - File path of the call site
|
||||
|
||||
@@ -1106,9 +1106,6 @@ async function runChunkedParseAndResolve(
|
||||
console.log(
|
||||
`🔍 Resolution cache: ${rcStats.cacheHits} hits, ${rcStats.cacheMisses} misses (${hitRate}% hit rate)`,
|
||||
);
|
||||
console.log(
|
||||
`🔍 Fuzzy Lookups: ${rcStats.fuzzyCallCount} total, ${rcStats.fuzzyCallableCallCount} callable`,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Finalize the accumulator before the read phase begins. All worker-path
|
||||
|
||||
@@ -10,15 +10,14 @@
|
||||
* 2a-named. Named binding chain (walkBindingChain via NamedImportMap)
|
||||
* 2a. Import-scoped (iterate importedFiles with lookupExactAll per file)
|
||||
* 2b. Package-scoped (iterate indexed files matching package dir with lookupExactAll)
|
||||
* 3. Global (lookupClassByName + lookupImplByName + lookupFuzzyCallable — consumers must check count)
|
||||
* 3. Global (lookupClassByName + lookupImplByName + lookupCallableByName — consumers must check count)
|
||||
*
|
||||
* SM-16: resolveUncached no longer calls lookupFuzzy. Each tier queries the
|
||||
* minimum necessary scope directly:
|
||||
* Each tier queries the minimum necessary scope directly:
|
||||
* - Tier 2a iterates the caller's import set (O(imports) × O(1) lookupExactAll).
|
||||
* - Tier 2b iterates all indexed files filtered by package dir
|
||||
* (O(files) × O(1) lookupExactAll — avoids a global name scan).
|
||||
* - Tier 3 combines lookupClassByName + lookupImplByName + lookupFuzzyCallable
|
||||
* (three O(1) index lookups vs one O(1) lookupFuzzy, with a narrower result set).
|
||||
* - Tier 3 combines lookupClassByName + lookupImplByName + lookupCallableByName
|
||||
* (three O(1) index lookups with a narrow, type-specific result set).
|
||||
*/
|
||||
|
||||
import type { SymbolTable, SymbolDefinition } from './symbol-table.js';
|
||||
@@ -76,11 +75,12 @@ export interface ResolutionContext {
|
||||
// --- Operational ---
|
||||
getStats(): {
|
||||
fileCount: number;
|
||||
globalSymbolCount: number;
|
||||
fuzzyCallCount: number;
|
||||
fuzzyCallableCallCount: number;
|
||||
cacheHits: number;
|
||||
cacheMisses: number;
|
||||
tierSameFile: number;
|
||||
tierImportScoped: number;
|
||||
tierGlobal: number;
|
||||
tierMiss: number;
|
||||
};
|
||||
clear(): void;
|
||||
}
|
||||
@@ -104,6 +104,11 @@ export const createResolutionContext = (): ResolutionContext => {
|
||||
let cache: Map<string, TieredCandidates | null> | null = null;
|
||||
let cacheHits = 0;
|
||||
let cacheMisses = 0;
|
||||
// Tier hit counters — replaces the lost fuzzyCallCount diagnostic
|
||||
let tierSameFile = 0;
|
||||
let tierImportScoped = 0;
|
||||
let tierGlobal = 0;
|
||||
let tierMiss = 0;
|
||||
|
||||
// --- Core resolution (single implementation of tier logic) ---
|
||||
|
||||
@@ -111,6 +116,7 @@ export const createResolutionContext = (): ResolutionContext => {
|
||||
// Tier 1: Same file — authoritative match (returns all overloads)
|
||||
const localDefs = symbols.lookupExactAll(fromFile, name);
|
||||
if (localDefs.length > 0) {
|
||||
tierSameFile++;
|
||||
return { candidates: localDefs, tier: 'same-file' };
|
||||
}
|
||||
|
||||
@@ -119,6 +125,7 @@ export const createResolutionContext = (): ResolutionContext => {
|
||||
// correctly even when lookupExactAll on the alias name returns nothing.
|
||||
const chainResult = walkBindingChain(name, fromFile, symbols, namedImportMap);
|
||||
if (chainResult && chainResult.length > 0) {
|
||||
tierImportScoped++;
|
||||
return { candidates: chainResult, tier: 'import-scoped' };
|
||||
}
|
||||
|
||||
@@ -131,6 +138,7 @@ export const createResolutionContext = (): ResolutionContext => {
|
||||
importedDefs.push(...symbols.lookupExactAll(file, name));
|
||||
}
|
||||
if (importedDefs.length > 0) {
|
||||
tierImportScoped++;
|
||||
return { candidates: importedDefs, tier: 'import-scoped' };
|
||||
}
|
||||
}
|
||||
@@ -178,37 +186,36 @@ export const createResolutionContext = (): ResolutionContext => {
|
||||
}
|
||||
}
|
||||
if (packageDefs.length > 0) {
|
||||
tierImportScoped++;
|
||||
return { candidates: packageDefs, tier: 'import-scoped' };
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 3: Global — three targeted O(1) index lookups replace the single
|
||||
// lookupFuzzy global scan. Class-like symbols (Class, Struct, Interface,
|
||||
// Enum, Record, Trait) are covered by lookupClassByName; Rust impl blocks
|
||||
// by lookupImplByName (separate to avoid polluting heritage resolution);
|
||||
// callables (Function, Method, Constructor) by lookupFuzzyCallable.
|
||||
// Tier 3: Global — targeted O(1) index lookups for each symbol category.
|
||||
// Class-like symbols (Class, Struct, Interface, Enum, Record, Trait) are
|
||||
// covered by lookupClassByName; Rust impl blocks by lookupImplByName
|
||||
// (separate to avoid polluting heritage resolution); callables (Function,
|
||||
// Method, Constructor, Macro, Delegate) by lookupCallableByName.
|
||||
// The three indexes cover disjoint symbol types so no dedup is needed.
|
||||
// Consumers must check candidates.length and refuse ambiguous matches.
|
||||
//
|
||||
// Known exclusion: TypeAlias, Const, and Variable are NOT reachable at
|
||||
// Tier 3 — they don't belong to any of the three indexes. The old
|
||||
// lookupFuzzy returned them, but in practice they were never useful as
|
||||
// Tier 3 candidates: TypeAlias is not a call target, Const/Variable
|
||||
// are resolved via import or same-file tiers. If a future language
|
||||
// needs them at Tier 3, add a dedicated index.
|
||||
// Macro (C/C++) and Delegate (C#) ARE included in callableIndex
|
||||
// Tier 3 — they don't belong to any of the three indexes. In practice
|
||||
// they were never useful as Tier 3 candidates: TypeAlias is not a call
|
||||
// target, Const/Variable are resolved via import or same-file tiers.
|
||||
// If a future language needs them at Tier 3, add a dedicated index.
|
||||
// Macro (C/C++) and Delegate (C#) ARE included in the callable index
|
||||
// since call-processor.ts treats them as callable targets.
|
||||
//
|
||||
// Note: lookupFuzzy is still called directly in call-processor.ts
|
||||
// (D2 module-alias widen path at ~line 1506/1588). Those callers
|
||||
// bypass resolveUncached entirely and are tracked for separate removal
|
||||
// in the roadmap. fuzzyCallCount only reflects resolveUncached usage.
|
||||
const classDefs = symbols.lookupClassByName(name);
|
||||
const implDefs = symbols.lookupImplByName(name);
|
||||
const callableDefs = symbols.lookupFuzzyCallable(name);
|
||||
const callableDefs = symbols.lookupCallableByName(name);
|
||||
|
||||
if (classDefs.length === 0 && implDefs.length === 0 && callableDefs.length === 0) return null;
|
||||
if (classDefs.length === 0 && implDefs.length === 0 && callableDefs.length === 0) {
|
||||
tierMiss++;
|
||||
return null;
|
||||
}
|
||||
const globalDefs = [...classDefs, ...implDefs, ...callableDefs];
|
||||
tierGlobal++;
|
||||
return { candidates: globalDefs, tier: 'global' };
|
||||
};
|
||||
|
||||
@@ -257,6 +264,10 @@ export const createResolutionContext = (): ResolutionContext => {
|
||||
...symbols.getStats(),
|
||||
cacheHits,
|
||||
cacheMisses,
|
||||
tierSameFile,
|
||||
tierImportScoped,
|
||||
tierGlobal,
|
||||
tierMiss,
|
||||
});
|
||||
|
||||
const clear = (): void => {
|
||||
@@ -269,6 +280,10 @@ export const createResolutionContext = (): ResolutionContext => {
|
||||
clearCache();
|
||||
cacheHits = 0;
|
||||
cacheMisses = 0;
|
||||
tierSameFile = 0;
|
||||
tierImportScoped = 0;
|
||||
tierGlobal = 0;
|
||||
tierMiss = 0;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -14,6 +14,17 @@ export const CLASS_TYPES = new Set([
|
||||
'Trait',
|
||||
]);
|
||||
|
||||
/** Callable symbol types indexed in callableByName for Tier 3 resolution
|
||||
* and D2 widen in call-processor.ts. Single source of truth — do not
|
||||
* duplicate this set elsewhere. */
|
||||
export const CALLABLE_TYPES = new Set([
|
||||
'Function',
|
||||
'Method',
|
||||
'Constructor',
|
||||
'Macro', // C/C++
|
||||
'Delegate', // C#
|
||||
]);
|
||||
|
||||
export interface SymbolDefinition {
|
||||
nodeId: string;
|
||||
filePath: string;
|
||||
@@ -79,17 +90,11 @@ export interface SymbolTable {
|
||||
lookupExactAll: (filePath: string, name: string) => SymbolDefinition[];
|
||||
|
||||
/**
|
||||
* Low Confidence: Look for a symbol anywhere in the project
|
||||
* Used when imports are missing or for framework magic
|
||||
* Look up callable symbols (Function, Method, Constructor, Macro, Delegate) by name.
|
||||
* O(1) via dedicated eagerly-populated index keyed by symbol name.
|
||||
* Used by Tier 3 resolution and ReturnTypeLookup to resolve callee → return type.
|
||||
*/
|
||||
lookupFuzzy: (name: string) => SymbolDefinition[];
|
||||
|
||||
/**
|
||||
* Low Confidence: Look for callable symbols (Function/Method/Constructor) by name.
|
||||
* Faster than `lookupFuzzy` + filter — backed by a lazy callable-only index.
|
||||
* Used by ReturnTypeLookup to resolve callee → return type.
|
||||
*/
|
||||
lookupFuzzyCallable: (name: string) => SymbolDefinition[];
|
||||
lookupCallableByName: (name: string) => SymbolDefinition[];
|
||||
|
||||
/**
|
||||
* Look up a field/property by its owning class nodeId and field name.
|
||||
@@ -128,7 +133,7 @@ export interface SymbolTable {
|
||||
* Look up class-like definitions (Class, Struct, Interface, Enum, Record) by name.
|
||||
* O(1) via dedicated eagerly-populated index keyed by symbol name.
|
||||
* Returns all matching definitions across files (e.g. partial classes).
|
||||
* Used by Phase 1 semantic-model tasks to replace filtered lookupFuzzy calls.
|
||||
* Used by Phase 1 semantic-model tasks to replace filtered global lookups.
|
||||
*/
|
||||
lookupClassByName: (name: string) => SymbolDefinition[];
|
||||
|
||||
@@ -161,9 +166,6 @@ export interface SymbolTable {
|
||||
*/
|
||||
getStats: () => {
|
||||
fileCount: number;
|
||||
globalSymbolCount: number;
|
||||
fuzzyCallCount: number;
|
||||
fuzzyCallableCallCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -178,40 +180,31 @@ export const createSymbolTable = (): SymbolTable => {
|
||||
// Array allows overloaded methods (same name, different signatures) to coexist.
|
||||
const fileIndex = new Map<string, Map<string, SymbolDefinition[]>>();
|
||||
|
||||
// 2. Global Reverse Index (The "Backup")
|
||||
// Structure: SymbolName -> [List of Definitions]
|
||||
const globalIndex = new Map<string, SymbolDefinition[]>();
|
||||
|
||||
// 3. Eagerly-populated Callable Index — maintained on add().
|
||||
// 2. Eagerly-populated Callable Index — maintained on add().
|
||||
// Structure: SymbolName -> [Callable Definitions]
|
||||
// Only Function, Method, Constructor symbols are indexed.
|
||||
const callableIndex = new Map<string, SymbolDefinition[]>();
|
||||
// Only Function, Method, Constructor, Macro, Delegate symbols are indexed.
|
||||
const callableByName = new Map<string, SymbolDefinition[]>();
|
||||
|
||||
// 4. Eagerly-populated Field/Property Index — keyed by "ownerNodeId\0fieldName".
|
||||
// 3. Eagerly-populated Field/Property Index — keyed by "ownerNodeId\0fieldName".
|
||||
// Only Property symbols with ownerId and declaredType are indexed.
|
||||
const fieldByOwner = new Map<string, SymbolDefinition>();
|
||||
|
||||
// 5. Eagerly-populated Method Index — keyed by "ownerNodeId\0methodName".
|
||||
// 4. Eagerly-populated Method Index — keyed by "ownerNodeId\0methodName".
|
||||
// Method symbols with ownerId are indexed. Supports overloads (array values).
|
||||
const methodByOwner = new Map<string, SymbolDefinition[]>();
|
||||
|
||||
// 6. Eagerly-populated Class-type Index — keyed by symbol name.
|
||||
// 5. Eagerly-populated Class-type Index — keyed by symbol name.
|
||||
// Only Class, Struct, Interface, Enum, Record symbols are indexed.
|
||||
const classByName = new Map<string, SymbolDefinition[]>();
|
||||
const classByQualifiedName = new Map<string, SymbolDefinition[]>();
|
||||
|
||||
// 7. Eagerly-populated Impl Index — keyed by symbol name.
|
||||
// 6. Eagerly-populated Impl Index — keyed by symbol name.
|
||||
// Rust impl blocks (type 'Impl') are stored here to keep them out of
|
||||
// classByName (which drives heritage resolution) while still being
|
||||
// reachable from Tier 3 resolution for method lookup.
|
||||
const implByName = new Map<string, SymbolDefinition[]>();
|
||||
let fuzzyCallCount = 0;
|
||||
|
||||
let fuzzyCallableCallCount = 0;
|
||||
|
||||
// Must match CALLABLE_SYMBOL_TYPES in call-processor.ts — Macro (C/C++)
|
||||
// and Delegate (C#) are callable targets that Tier 3 must surface.
|
||||
const CALLABLE_TYPES = new Set(['Function', 'Method', 'Constructor', 'Macro', 'Delegate']);
|
||||
// Use the module-level CALLABLE_TYPES constant (exported for call-processor.ts).
|
||||
|
||||
const add = (
|
||||
filePath: string,
|
||||
@@ -261,31 +254,25 @@ export const createSymbolTable = (): SymbolTable => {
|
||||
fileMap.get(name)!.push(def);
|
||||
}
|
||||
|
||||
// B. Properties go to fieldByOwner index only — skip globalIndex to prevent
|
||||
// B. Properties go to fieldByOwner index only — skip other indexes to prevent
|
||||
// namespace pollution for common names like 'id', 'name', 'type'.
|
||||
// Index ALL properties (even without declaredType) so write-access tracking
|
||||
// can resolve field ownership for dynamically-typed languages (Ruby, JS).
|
||||
if (type === 'Property' && metadata?.ownerId) {
|
||||
fieldByOwner.set(`${metadata.ownerId}\0${name}`, def);
|
||||
// Still add to fileIndex above (for lookupExact), but skip globalIndex
|
||||
// Still add to fileIndex above (for lookupExact), but skip other indexes
|
||||
return;
|
||||
}
|
||||
|
||||
// C. Add to Global Index (same object reference)
|
||||
if (!globalIndex.has(name)) {
|
||||
globalIndex.set(name, []);
|
||||
}
|
||||
globalIndex.get(name)!.push(def);
|
||||
|
||||
// C2. Methods, constructors, and ownerId-bound Functions go to
|
||||
// methodByOwner index (in addition to globalIndex).
|
||||
// C. Methods, constructors, and ownerId-bound Functions go to
|
||||
// methodByOwner index.
|
||||
//
|
||||
// Some language extractors emit class methods as `Function` with an
|
||||
// `ownerId` — notably Python (`def method(self):` inside a class body),
|
||||
// Rust trait methods, and Kotlin object/companion methods. Treating
|
||||
// `Function` with ownerId the same as `Method` here makes D0
|
||||
// (`resolveMemberCall`) work uniformly across all supported languages
|
||||
// instead of silently falling through to D1-D4 fuzzy widening.
|
||||
// instead of silently falling through to D1-D4 widening.
|
||||
if ((type === 'Method' || type === 'Constructor' || type === 'Function') && metadata?.ownerId) {
|
||||
const key = `${metadata.ownerId}\0${name}`;
|
||||
const existing = methodByOwner.get(key);
|
||||
@@ -296,7 +283,7 @@ export const createSymbolTable = (): SymbolTable => {
|
||||
}
|
||||
}
|
||||
|
||||
// C3. Class-like types go to classByName index (in addition to globalIndex).
|
||||
// C2. Class-like types go to classByName index.
|
||||
if (CLASS_TYPES.has(type)) {
|
||||
const existing = classByName.get(name);
|
||||
if (existing) {
|
||||
@@ -314,7 +301,7 @@ export const createSymbolTable = (): SymbolTable => {
|
||||
}
|
||||
}
|
||||
|
||||
// C4. Rust Impl blocks go to implByName (separate from classByName to avoid
|
||||
// C3. Rust Impl blocks go to implByName (separate from classByName to avoid
|
||||
// polluting heritage resolution with Impl nodes as parent candidates).
|
||||
if (type === 'Impl') {
|
||||
const existing = implByName.get(name);
|
||||
@@ -327,11 +314,11 @@ export const createSymbolTable = (): SymbolTable => {
|
||||
|
||||
// D. Eagerly maintain callable index (like classByName, implByName).
|
||||
if (CALLABLE_TYPES.has(type)) {
|
||||
const existing = callableIndex.get(name);
|
||||
const existing = callableByName.get(name);
|
||||
if (existing) {
|
||||
existing.push(def);
|
||||
} else {
|
||||
callableIndex.set(name, [def]);
|
||||
callableByName.set(name, [def]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -350,14 +337,8 @@ export const createSymbolTable = (): SymbolTable => {
|
||||
return fileIndex.get(filePath)?.get(name) ?? [];
|
||||
};
|
||||
|
||||
const lookupFuzzy = (name: string): SymbolDefinition[] => {
|
||||
fuzzyCallCount++;
|
||||
return globalIndex.get(name) || [];
|
||||
};
|
||||
|
||||
const lookupFuzzyCallable = (name: string): SymbolDefinition[] => {
|
||||
fuzzyCallableCallCount++;
|
||||
return callableIndex.get(name) ?? [];
|
||||
const lookupCallableByName = (name: string): SymbolDefinition[] => {
|
||||
return callableByName.get(name) ?? [];
|
||||
};
|
||||
|
||||
const lookupFieldByOwner = (
|
||||
@@ -428,22 +409,16 @@ export const createSymbolTable = (): SymbolTable => {
|
||||
|
||||
const getStats = () => ({
|
||||
fileCount: fileIndex.size,
|
||||
globalSymbolCount: globalIndex.size,
|
||||
fuzzyCallableCallCount: fuzzyCallableCallCount,
|
||||
fuzzyCallCount: fuzzyCallCount,
|
||||
});
|
||||
|
||||
const clear = () => {
|
||||
fileIndex.clear();
|
||||
globalIndex.clear();
|
||||
callableIndex.clear();
|
||||
callableByName.clear();
|
||||
fieldByOwner.clear();
|
||||
methodByOwner.clear();
|
||||
classByName.clear();
|
||||
classByQualifiedName.clear();
|
||||
implByName.clear();
|
||||
fuzzyCallCount = 0;
|
||||
fuzzyCallableCallCount = 0;
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -451,8 +426,7 @@ export const createSymbolTable = (): SymbolTable => {
|
||||
lookupExact,
|
||||
lookupExactFull,
|
||||
lookupExactAll,
|
||||
lookupFuzzy,
|
||||
lookupFuzzyCallable,
|
||||
lookupCallableByName,
|
||||
lookupFieldByOwner,
|
||||
lookupMethodByOwner,
|
||||
lookupClassByName,
|
||||
|
||||
@@ -861,7 +861,7 @@ export const buildTypeEnv = (
|
||||
// SymbolTable is authoritative when it has an unambiguous match
|
||||
if (symbolTable) {
|
||||
if (provider.isBuiltInName(callee)) return undefined;
|
||||
const callables = symbolTable.lookupFuzzyCallable(callee);
|
||||
const callables = symbolTable.lookupCallableByName(callee);
|
||||
if (callables.length === 1) {
|
||||
const rawReturn = callables[0].returnType;
|
||||
if (rawReturn) return extractReturnTypeName(rawReturn);
|
||||
@@ -875,7 +875,7 @@ export const buildTypeEnv = (
|
||||
lookupRawReturnType(callee: string): string | undefined {
|
||||
if (symbolTable) {
|
||||
if (provider.isBuiltInName(callee)) return undefined;
|
||||
const callables = symbolTable.lookupFuzzyCallable(callee);
|
||||
const callables = symbolTable.lookupCallableByName(callee);
|
||||
if (callables.length === 1) return callables[0].returnType;
|
||||
// Ambiguous (2+) → return undefined (conservative, no cross-file fallback)
|
||||
if (callables.length > 1) return undefined;
|
||||
|
||||
@@ -54,7 +54,7 @@ export type DeclaredTypeUnwrapper = (
|
||||
) => string | undefined;
|
||||
|
||||
/** Narrow lookup interface for resolving a callee name → return type name.
|
||||
* Backed by SymbolTable.lookupFuzzyCallable; passed via ForLoopExtractorContext.
|
||||
* Backed by SymbolTable.lookupCallableByName; passed via ForLoopExtractorContext.
|
||||
* Conservative: returns undefined when the callee is ambiguous (0 or 2+ matches). */
|
||||
export interface ReturnTypeLookup {
|
||||
/** Processed type name after stripping wrappers (e.g., 'User' from 'Promise<User>').
|
||||
|
||||
@@ -452,13 +452,13 @@ describe('ownerId on SymbolDefinition', () => {
|
||||
expect(def!.ownerId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('propagates ownerId through lookupFuzzy', () => {
|
||||
it('propagates ownerId through lookupCallableByName', () => {
|
||||
const st = createSymbolTable();
|
||||
st.add('src/foo.ts', 'save', 'Method:src/foo.ts:save', 'Method', {
|
||||
ownerId: 'Class:src/foo.ts:User',
|
||||
});
|
||||
|
||||
const defs = st.lookupFuzzy('save');
|
||||
const defs = st.lookupCallableByName('save');
|
||||
expect(defs).toHaveLength(1);
|
||||
expect(defs[0].ownerId).toBe('Class:src/foo.ts:User');
|
||||
});
|
||||
|
||||
@@ -2218,7 +2218,7 @@ describe('processCalls — D0 MRO fast path (SM-10)', () => {
|
||||
// D0: lookupMethodByOwner(classId, 'doWork') → undefined
|
||||
// heritageMap.getAncestors(classId) → []
|
||||
// lookupMethodByOwnerWithMRO returns undefined → D0 miss
|
||||
// D1-D4: receiver type resolves to Obj; D2 widens via lookupFuzzy;
|
||||
// D1-D4: receiver type resolves to Obj; D2 widens via lookupCallableByName;
|
||||
// D3 file-filter picks the only candidate in Obj's file.
|
||||
// Guarantees D0 miss does not swallow the call — D1-D4 still runs.
|
||||
const classFile = 'src/models/Obj.java';
|
||||
@@ -2550,3 +2550,44 @@ describe('processAssignmentsFromExtracted', () => {
|
||||
expect(accesses[0].targetId).toBe('Property:src/models.ts:address');
|
||||
});
|
||||
});
|
||||
|
||||
// ---- D2 widen: module-alias + lookupCallableByName resolves method in aliased file ----
|
||||
|
||||
describe('D2 widen path: lookupCallableByName via module alias', () => {
|
||||
let graph: ReturnType<typeof createKnowledgeGraph>;
|
||||
let ctx: ResolutionContext;
|
||||
|
||||
beforeEach(() => {
|
||||
graph = createKnowledgeGraph();
|
||||
ctx = createResolutionContext();
|
||||
});
|
||||
|
||||
it('resolves method via module alias widen using lookupCallableByName', async () => {
|
||||
// Python pattern: `import auth; auth.login()` — auth is a module alias
|
||||
// pointing to auth.py. login() is defined only in auth.py (not imported
|
||||
// by consumer.py). The D2 widen path should find login via the global
|
||||
// callable index filtered to the aliased module file.
|
||||
ctx.symbols.add('src/auth.py', 'login', 'Function:src/auth.py:login', 'Function');
|
||||
// Consumer has a same-file function that shadows 'login' at Tier 1
|
||||
ctx.symbols.add('src/consumer.py', 'login', 'Function:src/consumer.py:login', 'Function');
|
||||
// Module alias: consumer.py → auth → src/auth.py
|
||||
ctx.moduleAliasMap.set('src/consumer.py', new Map([['auth', 'src/auth.py']]));
|
||||
|
||||
const calls: ExtractedCall[] = [
|
||||
{
|
||||
filePath: 'src/consumer.py',
|
||||
calledName: 'login',
|
||||
sourceId: 'Function:src/consumer.py:main',
|
||||
receiverName: 'auth',
|
||||
callForm: 'member',
|
||||
},
|
||||
];
|
||||
|
||||
await processCallsFromExtracted(graph, calls, ctx);
|
||||
|
||||
const rels = graph.relationships.filter((r) => r.type === 'CALLS');
|
||||
expect(rels).toHaveLength(1);
|
||||
// Should resolve to auth.py's login, NOT consumer.py's same-file shadow
|
||||
expect(rels[0].targetId).toBe('Function:src/auth.py:login');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -417,16 +417,6 @@ describe('lookupExactFull', () => {
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('shares same object reference between fileIndex and globalIndex', () => {
|
||||
const symbolTable = createSymbolTable();
|
||||
symbolTable.add('src/x.ts', 'Bar', 'Class:src/x.ts:Bar', 'Class');
|
||||
|
||||
const fromExact = symbolTable.lookupExactFull('src/x.ts', 'Bar');
|
||||
const fromFuzzy = symbolTable.lookupFuzzy('Bar')[0];
|
||||
|
||||
expect(fromExact).toBe(fromFuzzy);
|
||||
});
|
||||
|
||||
it('preserves optional callable metadata on stored definitions', () => {
|
||||
const symbolTable = createSymbolTable();
|
||||
symbolTable.add('src/math.ts', 'sum', 'Function:src/math.ts:sum', 'Function', {
|
||||
@@ -434,11 +424,10 @@ describe('lookupExactFull', () => {
|
||||
});
|
||||
|
||||
const fromExact = symbolTable.lookupExactFull('src/math.ts', 'sum');
|
||||
const fromFuzzy = symbolTable.lookupFuzzy('sum')[0];
|
||||
const fromCallable = symbolTable.lookupCallableByName('sum')[0];
|
||||
|
||||
expect(fromExact?.parameterCount).toBe(2);
|
||||
expect(fromFuzzy.parameterCount).toBe(2);
|
||||
expect(fromExact).toBe(fromFuzzy);
|
||||
expect(fromCallable.parameterCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -626,32 +615,6 @@ describe('per-file cache', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SM-16: resolveUncached no longer calls lookupFuzzy
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Note: fuzzyCallCount tracks ALL lookupFuzzy calls on the SymbolTable, including
|
||||
// the D2 module-alias widen path in call-processor.ts which still calls lookupFuzzy
|
||||
// directly. This test only exercises resolveUncached (via ctx.resolve), so the stat
|
||||
// is 0 here. In a full pipeline integration test, fuzzyCallCount would be non-zero
|
||||
// due to D2 callers.
|
||||
describe('SM-16: resolveUncached does not call lookupFuzzy', () => {
|
||||
it('lookupFuzzy is never called during resolve — fuzzyCallCount stays at 0', () => {
|
||||
const ctx = createResolutionContext();
|
||||
ctx.symbols.add('src/user.ts', 'User', 'Class:src/user.ts:User', 'Class');
|
||||
ctx.symbols.add('src/service.ts', 'UserService', 'Class:src/service.ts:UserService', 'Class');
|
||||
ctx.importMap.set('src/app.ts', new Set(['src/user.ts']));
|
||||
ctx.packageMap.set('cmd/main.go', new Set(['/internal/']));
|
||||
|
||||
// Exercise all tiers
|
||||
ctx.resolve('User', 'src/user.ts'); // Tier 1 same-file
|
||||
ctx.resolve('User', 'src/app.ts'); // Tier 2a import-scoped
|
||||
ctx.resolve('UserService', 'src/other.ts'); // Tier 3 global
|
||||
|
||||
expect(ctx.getStats().fuzzyCallCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Tier 2a uses importMap (file-level imports). Go resolves cross-package symbols
|
||||
// via packageMap (Tier 2b) instead, so no Go Tier 2a test is needed. Kotlin and
|
||||
// PHP support file-level imports but the importMap path is language-agnostic —
|
||||
@@ -769,7 +732,7 @@ describe('SM-16: Tier 2b — iterate getFiles() + isFileInPackageDir', () => {
|
||||
ctx = createResolutionContext();
|
||||
});
|
||||
|
||||
it('Go: resolves symbol in package dir via file iteration (no lookupFuzzy)', () => {
|
||||
it('Go: resolves symbol in package dir via file iteration', () => {
|
||||
ctx.symbols.add(
|
||||
'internal/auth/handler.go',
|
||||
'Authenticate',
|
||||
@@ -826,7 +789,7 @@ describe('SM-16: Tier 2b — iterate getFiles() + isFileInPackageDir', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('SM-16: Tier 3 global — lookupClassByName + lookupImplByName + lookupFuzzyCallable', () => {
|
||||
describe('SM-16: Tier 3 global — lookupClassByName + lookupImplByName + lookupCallableByName', () => {
|
||||
let ctx: ResolutionContext;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -1048,29 +1011,19 @@ describe('SM-16: Tier 3 — TypeAlias, Const, Variable are NOT returned', () =>
|
||||
});
|
||||
|
||||
it('TypeAlias is not reachable at Tier 3', () => {
|
||||
ctx.symbols.add(
|
||||
'src/types.ts',
|
||||
'Handler',
|
||||
'TypeAlias:src/types.ts:Handler',
|
||||
'TypeAlias' as any,
|
||||
);
|
||||
ctx.symbols.add('src/types.ts', 'Handler', 'TypeAlias:src/types.ts:Handler', 'TypeAlias');
|
||||
const result = ctx.resolve('Handler', 'src/app.ts');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('Const is not reachable at Tier 3', () => {
|
||||
ctx.symbols.add(
|
||||
'src/config.ts',
|
||||
'MAX_RETRIES',
|
||||
'Const:src/config.ts:MAX_RETRIES',
|
||||
'Const' as any,
|
||||
);
|
||||
ctx.symbols.add('src/config.ts', 'MAX_RETRIES', 'Const:src/config.ts:MAX_RETRIES', 'Const');
|
||||
const result = ctx.resolve('MAX_RETRIES', 'src/app.ts');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('Variable is not reachable at Tier 3', () => {
|
||||
ctx.symbols.add('src/state.ts', 'counter', 'Variable:src/state.ts:counter', 'Variable' as any);
|
||||
ctx.symbols.add('src/state.ts', 'counter', 'Variable:src/state.ts:counter', 'Variable');
|
||||
const result = ctx.resolve('counter', 'src/app.ts');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
@@ -1088,21 +1041,16 @@ describe('SM-16: Tier 3 — TypeAlias, Const, Variable are NOT returned', () =>
|
||||
expect(funcResult!.tier).toBe('global');
|
||||
});
|
||||
|
||||
it('Macro (C/C++) is reachable at Tier 3 via callableIndex', () => {
|
||||
ctx.symbols.add('src/macros.h', 'ASSERT', 'Macro:src/macros.h:ASSERT', 'Macro' as any);
|
||||
it('Macro (C/C++) is reachable at Tier 3 via callable index', () => {
|
||||
ctx.symbols.add('src/macros.h', 'ASSERT', 'Macro:src/macros.h:ASSERT', 'Macro');
|
||||
const result = ctx.resolve('ASSERT', 'src/main.c');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.tier).toBe('global');
|
||||
expect(result!.candidates[0].type).toBe('Macro');
|
||||
});
|
||||
|
||||
it('Delegate (C#) is reachable at Tier 3 via callableIndex', () => {
|
||||
ctx.symbols.add(
|
||||
'src/Events.cs',
|
||||
'OnClick',
|
||||
'Delegate:src/Events.cs:OnClick',
|
||||
'Delegate' as any,
|
||||
);
|
||||
it('Delegate (C#) is reachable at Tier 3 via callable index', () => {
|
||||
ctx.symbols.add('src/Events.cs', 'OnClick', 'Delegate:src/Events.cs:OnClick', 'Delegate');
|
||||
const result = ctx.resolve('OnClick', 'src/App.cs');
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.tier).toBe('global');
|
||||
|
||||
@@ -11,7 +11,6 @@ describe('SymbolTable', () => {
|
||||
describe('add', () => {
|
||||
it('registers a symbol in the table', () => {
|
||||
table.add('src/index.ts', 'main', 'func:main', 'Function');
|
||||
expect(table.getStats().globalSymbolCount).toBe(1);
|
||||
expect(table.getStats().fileCount).toBe(1);
|
||||
});
|
||||
|
||||
@@ -19,15 +18,12 @@ describe('SymbolTable', () => {
|
||||
table.add('src/index.ts', 'main', 'func:main', 'Function');
|
||||
table.add('src/index.ts', 'helper', 'func:helper', 'Function');
|
||||
expect(table.getStats().fileCount).toBe(1);
|
||||
expect(table.getStats().globalSymbolCount).toBe(2);
|
||||
});
|
||||
|
||||
it('handles same name in different files', () => {
|
||||
table.add('src/a.ts', 'init', 'func:a:init', 'Function');
|
||||
table.add('src/b.ts', 'init', 'func:b:init', 'Function');
|
||||
expect(table.getStats().fileCount).toBe(2);
|
||||
// Global index groups by name, so 'init' has one entry with two definitions
|
||||
expect(table.getStats().globalSymbolCount).toBe(1);
|
||||
});
|
||||
|
||||
it('allows duplicate adds for same file and name (overloads preserved)', () => {
|
||||
@@ -37,8 +33,6 @@ describe('SymbolTable', () => {
|
||||
expect(table.lookupExact('src/a.ts', 'foo')).toBe('func:foo:1');
|
||||
// lookupExactAll returns all overloads
|
||||
expect(table.lookupExactAll('src/a.ts', 'foo')).toHaveLength(2);
|
||||
// Global index appends
|
||||
expect(table.lookupFuzzy('foo')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,36 +57,10 @@ describe('SymbolTable', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('lookupFuzzy', () => {
|
||||
it('finds all definitions of a symbol across files', () => {
|
||||
table.add('src/a.ts', 'render', 'func:a:render', 'Function');
|
||||
table.add('src/b.ts', 'render', 'func:b:render', 'Method');
|
||||
const results = table.lookupFuzzy('render');
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0]).toEqual({
|
||||
nodeId: 'func:a:render',
|
||||
filePath: 'src/a.ts',
|
||||
type: 'Function',
|
||||
});
|
||||
expect(results[1]).toEqual({ nodeId: 'func:b:render', filePath: 'src/b.ts', type: 'Method' });
|
||||
});
|
||||
|
||||
it('returns empty array for unknown symbol', () => {
|
||||
expect(table.lookupFuzzy('nonexistent')).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array for empty table', () => {
|
||||
expect(table.lookupFuzzy('anything')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
it('returns zero counts for empty table', () => {
|
||||
expect(table.getStats()).toEqual({
|
||||
fileCount: 0,
|
||||
globalSymbolCount: 0,
|
||||
fuzzyCallCount: 0,
|
||||
fuzzyCallableCallCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,14 +70,6 @@ describe('SymbolTable', () => {
|
||||
table.add('src/b.ts', 'baz', 'func:baz', 'Function');
|
||||
expect(table.getStats().fileCount).toBe(2);
|
||||
});
|
||||
|
||||
it('tracks unique global symbol names', () => {
|
||||
table.add('src/a.ts', 'foo', 'func:a:foo', 'Function');
|
||||
table.add('src/b.ts', 'foo', 'func:b:foo', 'Function');
|
||||
table.add('src/a.ts', 'bar', 'func:a:bar', 'Function');
|
||||
// 'foo' and 'bar' are 2 unique global names
|
||||
expect(table.getStats().globalSymbolCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('returnType metadata', () => {
|
||||
@@ -120,13 +80,13 @@ describe('SymbolTable', () => {
|
||||
expect(def!.returnType).toBe('User');
|
||||
});
|
||||
|
||||
it('returnType is available via lookupFuzzy', () => {
|
||||
it('returnType is available via lookupExactFull', () => {
|
||||
table.add('src/utils.ts', 'getUser', 'func:getUser', 'Function', {
|
||||
returnType: 'Promise<User>',
|
||||
});
|
||||
const results = table.lookupFuzzy('getUser');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].returnType).toBe('Promise<User>');
|
||||
const result = table.lookupExactFull('src/utils.ts', 'getUser');
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.returnType).toBe('Promise<User>');
|
||||
});
|
||||
|
||||
it('omits returnType when not provided', () => {
|
||||
@@ -169,28 +129,28 @@ describe('SymbolTable', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Property exclusion from globalIndex', () => {
|
||||
it('Property with ownerId is NOT added to globalIndex', () => {
|
||||
describe('Property exclusion from callable index', () => {
|
||||
it('Property with ownerId is NOT in callable index', () => {
|
||||
table.add('src/models.ts', 'name', 'prop:name', 'Property', {
|
||||
declaredType: 'string',
|
||||
ownerId: 'class:User',
|
||||
});
|
||||
// Should not appear in fuzzy lookup
|
||||
expect(table.lookupFuzzy('name')).toEqual([]);
|
||||
// Should not appear in callable lookup
|
||||
expect(table.lookupCallableByName('name')).toEqual([]);
|
||||
// But should still be in fileIndex
|
||||
expect(table.lookupExact('src/models.ts', 'name')).toBe('prop:name');
|
||||
});
|
||||
|
||||
it('Property without ownerId IS added to globalIndex', () => {
|
||||
it('Property without ownerId is NOT in callable index', () => {
|
||||
table.add('src/models.ts', 'name', 'prop:name', 'Property');
|
||||
expect(table.lookupFuzzy('name')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('name')).toEqual([]);
|
||||
});
|
||||
|
||||
it('Property without declaredType is still added to fieldByOwner index only (not globalIndex)', () => {
|
||||
it('Property without declaredType is still added to fieldByOwner index only', () => {
|
||||
table.add('src/models.ts', 'name', 'prop:name', 'Property', { ownerId: 'class:User' });
|
||||
// No declaredType → still indexed in fieldByOwner (for write-access tracking
|
||||
// in dynamically-typed languages like Ruby/JS), but excluded from globalIndex
|
||||
expect(table.lookupFuzzy('name')).toEqual([]);
|
||||
// in dynamically-typed languages like Ruby/JS), but excluded from callable index
|
||||
expect(table.lookupCallableByName('name')).toEqual([]);
|
||||
expect(table.lookupFieldByOwner('class:User', 'name')).toEqual({
|
||||
nodeId: 'prop:name',
|
||||
filePath: 'src/models.ts',
|
||||
@@ -199,40 +159,50 @@ describe('SymbolTable', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('non-Property types are always added to globalIndex', () => {
|
||||
it('non-Property callable types are in callable index', () => {
|
||||
table.add('src/models.ts', 'save', 'method:save', 'Method', { ownerId: 'class:User' });
|
||||
expect(table.lookupFuzzy('save')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('save')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('conditional callableIndex invalidation', () => {
|
||||
it('adding a Function invalidates callableIndex', () => {
|
||||
describe('conditional callable index behaviour', () => {
|
||||
it('adding a Function makes it available in callable index', () => {
|
||||
table.add('src/a.ts', 'foo', 'func:foo', 'Function', { returnType: 'void' });
|
||||
// First call builds the index
|
||||
expect(table.lookupFuzzyCallable('foo')).toHaveLength(1);
|
||||
// Add another callable — should invalidate and rebuild
|
||||
expect(table.lookupCallableByName('foo')).toHaveLength(1);
|
||||
// Add another callable
|
||||
table.add('src/a.ts', 'bar', 'func:bar', 'Method');
|
||||
expect(table.lookupFuzzyCallable('bar')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('bar')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('adding a Property does NOT invalidate callableIndex', () => {
|
||||
it('adding a Property does NOT add it to callable index', () => {
|
||||
table.add('src/a.ts', 'foo', 'func:foo', 'Function');
|
||||
// Build callable index
|
||||
expect(table.lookupFuzzyCallable('foo')).toHaveLength(1);
|
||||
// Add a Property — callable index should still be valid (foo still found)
|
||||
expect(table.lookupCallableByName('foo')).toHaveLength(1);
|
||||
// Add a Property — callable index should still only contain foo
|
||||
table.add('src/models.ts', 'name', 'prop:name', 'Property', {
|
||||
declaredType: 'string',
|
||||
ownerId: 'class:User',
|
||||
});
|
||||
expect(table.lookupFuzzyCallable('foo')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('foo')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('adding a Class does NOT invalidate callableIndex', () => {
|
||||
it('adding a Class does NOT add it to callable index', () => {
|
||||
table.add('src/a.ts', 'foo', 'func:foo', 'Function');
|
||||
expect(table.lookupFuzzyCallable('foo')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('foo')).toHaveLength(1);
|
||||
table.add('src/models.ts', 'User', 'class:User', 'Class');
|
||||
// Class is not callable, should not trigger rebuild
|
||||
expect(table.lookupFuzzyCallable('foo')).toHaveLength(1);
|
||||
// Class is not callable, should not appear
|
||||
expect(table.lookupCallableByName('foo')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('Macro (C/C++) is indexed in callable index', () => {
|
||||
table.add('src/macros.h', 'ASSERT', 'macro:ASSERT', 'Macro');
|
||||
expect(table.lookupCallableByName('ASSERT')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('ASSERT')[0].type).toBe('Macro');
|
||||
});
|
||||
|
||||
it('Delegate (C#) is indexed in callable index', () => {
|
||||
table.add('src/Events.cs', 'OnClick', 'delegate:OnClick', 'Delegate');
|
||||
expect(table.lookupCallableByName('OnClick')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('OnClick')[0].type).toBe('Delegate');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -355,8 +325,8 @@ describe('SymbolTable', () => {
|
||||
it('does NOT index Method without ownerId', () => {
|
||||
table.add('src/utils.ts', 'helper', 'method:helper', 'Method');
|
||||
expect(table.lookupMethodByOwner('', 'helper')).toBeUndefined();
|
||||
// But it should still be in lookupFuzzy
|
||||
expect(table.lookupFuzzy('helper')).toHaveLength(1);
|
||||
// But it should still be in lookupCallableByName
|
||||
expect(table.lookupCallableByName('helper')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns first match for overloads with same returnType (unambiguous)', () => {
|
||||
@@ -400,8 +370,8 @@ describe('SymbolTable', () => {
|
||||
parameterCount: 0,
|
||||
ownerId: 'class:User',
|
||||
});
|
||||
// But it should be in lookupFuzzyCallable
|
||||
expect(table.lookupFuzzyCallable('User')).toHaveLength(1);
|
||||
// But it should be in lookupCallableByName
|
||||
expect(table.lookupCallableByName('User')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns undefined for overloads with different returnTypes (ambiguous)', () => {
|
||||
@@ -418,14 +388,12 @@ describe('SymbolTable', () => {
|
||||
expect(table.lookupMethodByOwner('class:Converter', 'convert')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Method with ownerId is still available via lookupFuzzy and lookupFuzzyCallable', () => {
|
||||
it('Method with ownerId is still available via lookupCallableByName', () => {
|
||||
table.add('src/models.ts', 'save', 'method:save', 'Method', {
|
||||
returnType: 'void',
|
||||
ownerId: 'class:User',
|
||||
});
|
||||
// Methods stay in globalIndex (unlike Properties)
|
||||
expect(table.lookupFuzzy('save')).toHaveLength(1);
|
||||
expect(table.lookupFuzzyCallable('save')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('save')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('after clear(), lookupMethodByOwner returns undefined', () => {
|
||||
@@ -439,37 +407,37 @@ describe('SymbolTable', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('lookupFuzzyCallable', () => {
|
||||
describe('lookupCallableByName', () => {
|
||||
it('returns only callable types (Function, Method, Constructor)', () => {
|
||||
table.add('src/a.ts', 'foo', 'func:foo', 'Function');
|
||||
table.add('src/a.ts', 'bar', 'method:bar', 'Method');
|
||||
table.add('src/a.ts', 'Baz', 'ctor:Baz', 'Constructor');
|
||||
table.add('src/a.ts', 'User', 'class:User', 'Class');
|
||||
table.add('src/a.ts', 'IUser', 'iface:IUser', 'Interface');
|
||||
expect(table.lookupFuzzyCallable('foo')).toHaveLength(1);
|
||||
expect(table.lookupFuzzyCallable('bar')).toHaveLength(1);
|
||||
expect(table.lookupFuzzyCallable('Baz')).toHaveLength(1);
|
||||
expect(table.lookupFuzzyCallable('User')).toEqual([]);
|
||||
expect(table.lookupFuzzyCallable('IUser')).toEqual([]);
|
||||
expect(table.lookupCallableByName('foo')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('bar')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('Baz')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('User')).toEqual([]);
|
||||
expect(table.lookupCallableByName('IUser')).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array for unknown name', () => {
|
||||
table.add('src/a.ts', 'foo', 'func:foo', 'Function');
|
||||
expect(table.lookupFuzzyCallable('unknown')).toEqual([]);
|
||||
expect(table.lookupCallableByName('unknown')).toEqual([]);
|
||||
});
|
||||
|
||||
it('rebuilds index after adding new callable', () => {
|
||||
it('includes newly added callable', () => {
|
||||
table.add('src/a.ts', 'foo', 'func:foo', 'Function');
|
||||
expect(table.lookupFuzzyCallable('foo')).toHaveLength(1);
|
||||
expect(table.lookupFuzzyCallable('bar')).toEqual([]);
|
||||
expect(table.lookupCallableByName('foo')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('bar')).toEqual([]);
|
||||
table.add('src/a.ts', 'bar', 'func:bar', 'Function');
|
||||
expect(table.lookupFuzzyCallable('bar')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('bar')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('filters non-callable types from mixed name entries', () => {
|
||||
table.add('src/a.ts', 'save', 'func:save', 'Function');
|
||||
table.add('src/b.ts', 'save', 'class:save', 'Class');
|
||||
const callables = table.lookupFuzzyCallable('save');
|
||||
const callables = table.lookupCallableByName('save');
|
||||
expect(callables).toHaveLength(1);
|
||||
expect(callables[0].type).toBe('Function');
|
||||
});
|
||||
@@ -491,15 +459,11 @@ describe('SymbolTable', () => {
|
||||
table.clear();
|
||||
expect(table.getStats()).toEqual({
|
||||
fileCount: 0,
|
||||
globalSymbolCount: 0,
|
||||
fuzzyCallCount: 0,
|
||||
fuzzyCallableCallCount: 0,
|
||||
});
|
||||
expect(table.lookupExact('src/a.ts', 'foo')).toBeUndefined();
|
||||
expect(table.lookupFuzzy('foo')).toEqual([]);
|
||||
expect(table.lookupFieldByOwner('class:User', 'address')).toBeUndefined();
|
||||
expect(table.lookupMethodByOwner('class:User', 'save')).toBeUndefined();
|
||||
expect(table.lookupFuzzyCallable('foo')).toEqual([]);
|
||||
expect(table.lookupCallableByName('foo')).toEqual([]);
|
||||
expect(table.lookupClassByName('User')).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -509,23 +473,20 @@ describe('SymbolTable', () => {
|
||||
table.add('src/b.ts', 'bar', 'func:bar', 'Function');
|
||||
expect(table.getStats()).toEqual({
|
||||
fileCount: 1,
|
||||
globalSymbolCount: 1,
|
||||
fuzzyCallCount: 0,
|
||||
fuzzyCallableCallCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('resets callableIndex so first lookup after clear rebuilds from scratch', () => {
|
||||
it('resets callable index so first lookup after clear rebuilds from scratch', () => {
|
||||
table.add('src/a.ts', 'foo', 'func:foo', 'Function');
|
||||
// Populate the lazy callable index
|
||||
expect(table.lookupFuzzyCallable('foo')).toHaveLength(1);
|
||||
// Verify callable is found
|
||||
expect(table.lookupCallableByName('foo')).toHaveLength(1);
|
||||
table.clear();
|
||||
// After clear the callable index must be gone — empty table returns nothing
|
||||
expect(table.lookupFuzzyCallable('foo')).toEqual([]);
|
||||
// Re-adding and looking up rebuilds successfully
|
||||
expect(table.lookupCallableByName('foo')).toEqual([]);
|
||||
// Re-adding and looking up works correctly
|
||||
table.add('src/a.ts', 'foo', 'func:foo2', 'Function');
|
||||
expect(table.lookupFuzzyCallable('foo')).toHaveLength(1);
|
||||
expect(table.lookupFuzzyCallable('foo')[0].nodeId).toBe('func:foo2');
|
||||
expect(table.lookupCallableByName('foo')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('foo')[0].nodeId).toBe('func:foo2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -540,7 +501,7 @@ describe('SymbolTable', () => {
|
||||
expect(def!.ownerId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('stores only ownerId on a Method (non-Property) — still added to globalIndex', () => {
|
||||
it('stores only ownerId on a Method (non-Property) — still in callable index', () => {
|
||||
table.add('src/models.ts', 'save', 'method:save', 'Method', { ownerId: 'class:Repo' });
|
||||
const def = table.lookupExactFull('src/models.ts', 'save');
|
||||
expect(def).toBeDefined();
|
||||
@@ -548,12 +509,12 @@ describe('SymbolTable', () => {
|
||||
expect(def!.parameterCount).toBeUndefined();
|
||||
expect(def!.returnType).toBeUndefined();
|
||||
expect(def!.declaredType).toBeUndefined();
|
||||
// Non-Property with ownerId must still appear in globalIndex
|
||||
expect(table.lookupFuzzy('save')).toHaveLength(1);
|
||||
// Non-Property with ownerId must still appear in callable index
|
||||
expect(table.lookupCallableByName('save')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('stores declaredType alone (no ownerId) — symbol goes to globalIndex', () => {
|
||||
// A Variable/Property without an owner should still be globally visible
|
||||
it('stores declaredType alone (no ownerId) — symbol in file index', () => {
|
||||
// A Variable/Property without an owner should still be accessible via file index
|
||||
table.add('src/config.ts', 'DEFAULT_TIMEOUT', 'var:DEFAULT_TIMEOUT', 'Variable', {
|
||||
declaredType: 'number',
|
||||
});
|
||||
@@ -561,9 +522,6 @@ describe('SymbolTable', () => {
|
||||
expect(def).toBeDefined();
|
||||
expect(def!.declaredType).toBe('number');
|
||||
expect(def!.ownerId).toBeUndefined();
|
||||
// No ownerId → not a Property exclusion path → must be in globalIndex
|
||||
expect(table.lookupFuzzy('DEFAULT_TIMEOUT')).toHaveLength(1);
|
||||
expect(table.lookupFuzzy('DEFAULT_TIMEOUT')[0].declaredType).toBe('number');
|
||||
});
|
||||
|
||||
it('stores all four optional metadata fields simultaneously on a Method', () => {
|
||||
@@ -600,46 +558,40 @@ describe('SymbolTable', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('lookupFuzzyCallable — lazy index behaviour', () => {
|
||||
describe('lookupCallableByName — eager index behavior', () => {
|
||||
it('returns empty array when table has no callables', () => {
|
||||
table.add('src/models.ts', 'User', 'class:User', 'Class');
|
||||
table.add('src/models.ts', 'IUser', 'iface:IUser', 'Interface');
|
||||
expect(table.lookupFuzzyCallable('User')).toEqual([]);
|
||||
expect(table.lookupFuzzyCallable('IUser')).toEqual([]);
|
||||
expect(table.lookupCallableByName('User')).toEqual([]);
|
||||
expect(table.lookupCallableByName('IUser')).toEqual([]);
|
||||
});
|
||||
|
||||
it('uses cached index on second call without adding new symbols', () => {
|
||||
it('returns consistent result on repeated calls', () => {
|
||||
table.add('src/a.ts', 'fetch', 'func:fetch', 'Function', { returnType: 'Response' });
|
||||
// First call — builds the lazy index
|
||||
const first = table.lookupFuzzyCallable('fetch');
|
||||
const first = table.lookupCallableByName('fetch');
|
||||
expect(first).toHaveLength(1);
|
||||
// Second call — must return equivalent result from cache
|
||||
const second = table.lookupFuzzyCallable('fetch');
|
||||
const second = table.lookupCallableByName('fetch');
|
||||
expect(second).toHaveLength(1);
|
||||
expect(second[0].nodeId).toBe('func:fetch');
|
||||
// Both calls return the same array reference (same cache entry)
|
||||
expect(first).toBe(second);
|
||||
});
|
||||
|
||||
it('invalidated cache is rebuilt correctly after adding a Method', () => {
|
||||
it('includes newly added Method', () => {
|
||||
table.add('src/a.ts', 'alpha', 'func:alpha', 'Function');
|
||||
// Warm the cache
|
||||
expect(table.lookupFuzzyCallable('alpha')).toHaveLength(1);
|
||||
expect(table.lookupFuzzyCallable('beta')).toEqual([]);
|
||||
// Add a Method — must invalidate cache
|
||||
expect(table.lookupCallableByName('alpha')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('beta')).toEqual([]);
|
||||
// Add a Method
|
||||
table.add('src/a.ts', 'beta', 'method:beta', 'Method');
|
||||
// Rebuilt cache must now include beta
|
||||
const result = table.lookupFuzzyCallable('beta');
|
||||
const result = table.lookupCallableByName('beta');
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].type).toBe('Method');
|
||||
});
|
||||
|
||||
it('invalidated cache is rebuilt correctly after adding a Constructor', () => {
|
||||
it('includes newly added Constructor', () => {
|
||||
table.add('src/a.ts', 'existing', 'func:existing', 'Function');
|
||||
expect(table.lookupFuzzyCallable('existing')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('existing')).toHaveLength(1);
|
||||
table.add('src/models.ts', 'MyClass', 'ctor:MyClass', 'Constructor');
|
||||
expect(table.lookupFuzzyCallable('MyClass')).toHaveLength(1);
|
||||
expect(table.lookupFuzzyCallable('MyClass')[0].type).toBe('Constructor');
|
||||
expect(table.lookupCallableByName('MyClass')).toHaveLength(1);
|
||||
expect(table.lookupCallableByName('MyClass')[0].type).toBe('Constructor');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -859,10 +811,9 @@ describe('SymbolTable', () => {
|
||||
expect(results[0].ownerId).toBe('module:models');
|
||||
});
|
||||
|
||||
it('class-like symbols are still available via lookupFuzzy', () => {
|
||||
it('class-like symbols are available via lookupClassByName', () => {
|
||||
table.add('src/models.ts', 'User', 'class:User', 'Class');
|
||||
// classByName is an additional index, not a replacement for globalIndex
|
||||
expect(table.lookupFuzzy('User')).toHaveLength(1);
|
||||
// classByName is the dedicated index for class-like lookups
|
||||
expect(table.lookupClassByName('User')).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -83,17 +83,15 @@ const createMockSymbolTable = (overrides: Partial<SymbolTable> = {}): SymbolTabl
|
||||
lookupExact: () => undefined,
|
||||
lookupExactFull: () => undefined,
|
||||
lookupExactAll: () => [],
|
||||
lookupFuzzy: () => [],
|
||||
lookupFuzzyCallable: () => [],
|
||||
lookupCallableByName: () => [],
|
||||
lookupFieldByOwner: () => undefined,
|
||||
lookupMethodByOwner: () => undefined,
|
||||
lookupClassByName: () => [],
|
||||
lookupClassByQualifiedName: () => [],
|
||||
lookupImplByName: () => [],
|
||||
getFiles: () => [][Symbol.iterator](),
|
||||
getStats: () => ({
|
||||
fileCount: 0,
|
||||
globalSymbolCount: 0,
|
||||
fuzzyCallCount: 0,
|
||||
fuzzyCallableCallCount: 0,
|
||||
}),
|
||||
clear: () => {},
|
||||
...overrides,
|
||||
@@ -1195,7 +1193,7 @@ class RepoService {
|
||||
describe('destructured call results', () => {
|
||||
// Minimal mock SymbolTable for call-result return type lookup
|
||||
const makeSymbolTable = (callables: Array<{ name: string; returnType?: string }>) => ({
|
||||
lookupFuzzyCallable: (name: string) =>
|
||||
lookupCallableByName: (name: string) =>
|
||||
callables
|
||||
.filter((c) => c.name === name)
|
||||
.map((c) => ({
|
||||
@@ -1205,11 +1203,10 @@ class RepoService {
|
||||
returnType: c.returnType,
|
||||
})),
|
||||
lookupClassByName: () => [],
|
||||
lookupFuzzy: () => [],
|
||||
lookupExact: () => undefined,
|
||||
lookupExactFull: () => undefined,
|
||||
add: () => {},
|
||||
getStats: () => ({ fileCount: 0, globalSymbolCount: 0 }),
|
||||
getStats: () => ({ fileCount: 0 }),
|
||||
clear: () => {},
|
||||
});
|
||||
|
||||
@@ -2055,7 +2052,7 @@ class RepoService {
|
||||
lookupExact: () => undefined,
|
||||
lookupExactFull: () => undefined,
|
||||
add: () => {},
|
||||
getStats: () => ({ fileCount: 0, globalSymbolCount: 0 }),
|
||||
getStats: () => ({ fileCount: 0 }),
|
||||
clear: () => {},
|
||||
};
|
||||
const typeEnv = buildTypeEnv(tree, 'kotlin', { symbolTable: mockSymbolTable as any });
|
||||
@@ -2073,14 +2070,12 @@ class RepoService {
|
||||
);
|
||||
const mockSymbolTable = {
|
||||
lookupClassByName: () => [],
|
||||
lookupFuzzy: (name: string) =>
|
||||
name === 'doStuff' ? [{ nodeId: 'n1', filePath: 'utils.kt', type: 'Function' }] : [],
|
||||
lookupFuzzyCallable: () => [],
|
||||
lookupCallableByName: () => [],
|
||||
lookupFieldByOwner: () => undefined,
|
||||
lookupExact: () => undefined,
|
||||
lookupExactFull: () => undefined,
|
||||
add: () => {},
|
||||
getStats: () => ({ fileCount: 0, globalSymbolCount: 0 }),
|
||||
getStats: () => ({ fileCount: 0 }),
|
||||
clear: () => {},
|
||||
};
|
||||
const typeEnv = buildTypeEnv(tree, 'kotlin', { symbolTable: mockSymbolTable as any });
|
||||
@@ -2460,7 +2455,7 @@ function process(repo: Repo) {
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const lookupCallableByName = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) =>
|
||||
name === 'Repo' ? [createClassDef('Repo', 'Class', 'models.ts')] : [],
|
||||
@@ -2474,11 +2469,11 @@ function process(repo: Repo) {
|
||||
returnType: 'Profile',
|
||||
}
|
||||
: undefined,
|
||||
lookupFuzzyCallable,
|
||||
lookupCallableByName,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', { symbolTable });
|
||||
expect(flatGet(typeEnv, 'profile')).toBe('Profile');
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
expect(lookupCallableByName).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('inherited method return type resolution uses lookupMethodByOwner on parent owners', () => {
|
||||
@@ -2490,7 +2485,7 @@ function process(repo: Repo) {
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const lookupCallableByName = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) => {
|
||||
if (name === 'Repo') return [createClassDef('Repo', 'Class', 'models.ts')];
|
||||
@@ -2507,14 +2502,14 @@ function process(repo: Repo) {
|
||||
returnType: 'Profile',
|
||||
}
|
||||
: undefined,
|
||||
lookupFuzzyCallable,
|
||||
lookupCallableByName,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', {
|
||||
symbolTable,
|
||||
parentMap: new Map([['Repo', ['BaseRepo']]]),
|
||||
});
|
||||
expect(flatGet(typeEnv, 'profile')).toBe('Profile');
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
expect(lookupCallableByName).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('method return type resolution handles multiple class defs when only one owner has the method', () => {
|
||||
@@ -2526,7 +2521,7 @@ function process(repo: Repo) {
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const lookupCallableByName = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) =>
|
||||
name === 'Repo'
|
||||
@@ -2549,11 +2544,11 @@ function process(repo: Repo) {
|
||||
}
|
||||
: undefined,
|
||||
lookupExactAll: () => [],
|
||||
lookupFuzzyCallable,
|
||||
lookupCallableByName,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', { symbolTable });
|
||||
expect(flatGet(typeEnv, 'profile')).toBe('Profile');
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
expect(lookupCallableByName).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('method return type resolution with multiple class defs falls back to MRO when direct owners miss', () => {
|
||||
@@ -2565,7 +2560,7 @@ function process(repo: Repo) {
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const lookupCallableByName = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) => {
|
||||
if (name === 'Repo') {
|
||||
@@ -2588,14 +2583,14 @@ function process(repo: Repo) {
|
||||
}
|
||||
: undefined,
|
||||
lookupExactAll: () => [],
|
||||
lookupFuzzyCallable,
|
||||
lookupCallableByName,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', {
|
||||
symbolTable,
|
||||
parentMap: new Map([['Repo', ['BaseRepo']]]),
|
||||
});
|
||||
expect(flatGet(typeEnv, 'profile')).toBe('Profile');
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
expect(lookupCallableByName).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('method return type resolution stays unresolved when multiple class defs each define the method', () => {
|
||||
@@ -2607,7 +2602,7 @@ function process(repo: Repo) {
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const lookupCallableByName = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) =>
|
||||
name === 'Repo'
|
||||
@@ -2642,11 +2637,11 @@ function process(repo: Repo) {
|
||||
return undefined;
|
||||
},
|
||||
lookupExactAll: () => [],
|
||||
lookupFuzzyCallable,
|
||||
lookupCallableByName,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', { symbolTable });
|
||||
expect(flatGet(typeEnv, 'profile')).toBeUndefined();
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
expect(lookupCallableByName).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('method return type resolution preserves same-return overload success', () => {
|
||||
@@ -2658,7 +2653,7 @@ function process(repo: Repo) {
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const lookupCallableByName = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) =>
|
||||
name === 'Repo' ? [createClassDef('Repo', 'Class', 'models.ts')] : [],
|
||||
@@ -2691,11 +2686,11 @@ function process(repo: Repo) {
|
||||
},
|
||||
]
|
||||
: [],
|
||||
lookupFuzzyCallable,
|
||||
lookupCallableByName,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', { symbolTable });
|
||||
expect(flatGet(typeEnv, 'profile')).toBe('Profile');
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
expect(lookupCallableByName).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('method return type resolution stays unresolved for ambiguous overloads with differing returns', () => {
|
||||
@@ -2707,7 +2702,7 @@ function process(repo: Repo) {
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const lookupCallableByName = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) => {
|
||||
if (name === 'Repo') return [createClassDef('Repo', 'Class', 'models.ts')];
|
||||
@@ -2743,14 +2738,14 @@ function process(repo: Repo) {
|
||||
},
|
||||
]
|
||||
: [],
|
||||
lookupFuzzyCallable,
|
||||
lookupCallableByName,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', {
|
||||
symbolTable,
|
||||
parentMap: new Map([['Repo', ['BaseRepo']]]),
|
||||
});
|
||||
expect(flatGet(typeEnv, 'profile')).toBeUndefined();
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
expect(lookupCallableByName).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('inherited method return type resolution preserves same-return overload success on parent owners', () => {
|
||||
@@ -2762,7 +2757,7 @@ function process(repo: Repo) {
|
||||
`,
|
||||
TypeScript.typescript,
|
||||
);
|
||||
const lookupFuzzyCallable = vi.fn(() => []);
|
||||
const lookupCallableByName = vi.fn(() => []);
|
||||
const symbolTable = createMockSymbolTable({
|
||||
lookupClassByName: (name: string) => {
|
||||
if (name === 'Repo') return [createClassDef('Repo', 'Class', 'models.ts')];
|
||||
@@ -2798,14 +2793,14 @@ function process(repo: Repo) {
|
||||
},
|
||||
]
|
||||
: [],
|
||||
lookupFuzzyCallable,
|
||||
lookupCallableByName,
|
||||
});
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', {
|
||||
symbolTable,
|
||||
parentMap: new Map([['Repo', ['BaseRepo']]]),
|
||||
});
|
||||
expect(flatGet(typeEnv, 'profile')).toBe('Profile');
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalledWith('getProfile');
|
||||
expect(lookupCallableByName).not.toHaveBeenCalledWith('getProfile');
|
||||
});
|
||||
|
||||
it('inherited method return type resolution stays unresolved for ambiguous overloads on parent owners', () => {
|
||||
@@ -2830,13 +2825,13 @@ function process(repo: Repo) {
|
||||
parameterCount: 2,
|
||||
returnType: 'Admin',
|
||||
});
|
||||
const lookupFuzzyCallable = vi.spyOn(symbolTable, 'lookupFuzzyCallable');
|
||||
const lookupCallableByName = vi.spyOn(symbolTable, 'lookupCallableByName');
|
||||
const typeEnv = buildTypeEnv(tree, 'typescript', {
|
||||
symbolTable,
|
||||
parentMap: new Map([['Repo', ['BaseRepo']]]),
|
||||
});
|
||||
expect(flatGet(typeEnv, 'profile')).toBeUndefined();
|
||||
expect(lookupFuzzyCallable).not.toHaveBeenCalled();
|
||||
expect(lookupCallableByName).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5747,7 +5742,7 @@ function process() {
|
||||
describe('importedReturnTypes (Phase 14 E3)', () => {
|
||||
// Minimal mock SymbolTable that returns a known callable
|
||||
const makeSymbolTable = (callables: Array<{ name: string; returnType?: string }>) => ({
|
||||
lookupFuzzyCallable: (name: string) =>
|
||||
lookupCallableByName: (name: string) =>
|
||||
callables
|
||||
.filter((c) => c.name === name)
|
||||
.map((c) => ({
|
||||
@@ -5757,11 +5752,10 @@ function process() {
|
||||
returnType: c.returnType,
|
||||
})),
|
||||
lookupClassByName: () => [],
|
||||
lookupFuzzy: () => [],
|
||||
lookupExact: () => undefined,
|
||||
lookupExactFull: () => undefined,
|
||||
add: () => {},
|
||||
getStats: () => ({ fileCount: 0, globalSymbolCount: 0 }),
|
||||
getStats: () => ({ fileCount: 0 }),
|
||||
clear: () => {},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user