mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-04 02:31:36 +00:00
feat(csharp-scope): parity Unit 6e — overload disambig + interface dispatch + FLAG FLIP
Closes the final 4 parity failures (4 → 0). C# now runs the
registry-primary scope-resolution path by default — added to
MIGRATED_LANGUAGES.
Changes:
- `scope-resolution/scope/walkers.ts`: was already extended in
Unit 6a to recognize Interface/Struct/Record/Enum as class-like
owners (interface default methods get ownerIds).
- `scope-resolution/passes/receiver-bound-calls.ts`: build
IMPLEMENTS edge index → emit secondary `interface-dispatch`
CALLS edges to every implementor's same-named member when the
primary receiver-typed edge targets an Interface method (closes
heritage CreateUser CALLS-count test).
- `scope-resolution/passes/receiver-bound-calls.ts`: new
`pickOverload` helper narrows multi-valued
`membersByOwner.get(owner).get(name)` candidates by arity then
argument types. Replaces the first-seen `findOwnedMember` lookup
in Case 4 so receiver-typed overloaded calls pick the right def.
- `scope-resolution/passes/free-call-fallback.ts`: new
`pickImplicitThisOverload` walks up to the enclosing class scope
and applies the same arity + argument-type narrowing for free
calls inside a class body (`Lookup("alice")` → `Lookup(string)`).
- `scope-resolution/workspace-index.ts`: new `membersByOwner`
multi-valued index (`Map<owner, Map<name, Def[]>>`) preserves
every overload alongside the existing first-seen `memberByOwner`.
- `scope-resolution/graph-bridge/node-lookup.ts` +
`scope-resolution/graph-bridge/ids.ts`: include parameter-types
suffix in the qualified lookup key for Method nodes. Legacy
parse-phase encodes the type tag into the node id (`Method:f.cs:
UserService.Lookup#1~int`); without this two same-arity overloads
collapsed to one lookup entry and routed to the wrong graph node.
- `scope-resolution/contract/scope-resolver.ts`: new
`collapseMemberCallsByCallerTarget` opt-in flag (was added in
Unit 6b for member-call dedup; documented here).
- `gitnexus-shared/src/scope-resolution/reference-site.ts`: new
`argumentTypes` field carrying inferred per-arg types.
- `scope-extractor.ts`: read @reference.parameter-types capture into
`site.argumentTypes` and add it + the declaration-arity tags to
KNOWN_SUB_TAGS so the anchor-detection picks the right anchor.
- `languages/csharp/captures.ts`: synthesize @reference.parameter-types
by inferring arg types from literal AST nodes (integer_literal →
'int', string_literal → 'string', constructor_expression →
type-name, etc).
- `languages/csharp/scope-resolver.ts`: opt in to
`collapseMemberCallsByCallerTarget`.
- `registry-primary-flag.ts`: **add CSharp to MIGRATED_LANGUAGES**.
Final state:
- C# parity: 175/175 green on flag-on AND flag-off.
- Python parity: 204/204 green on both flag paths (no regression).
- TypeScript clean.
51 → 0 failures across 18 commits on `feat/csharp-scope-resolution`.
This commit is contained in:
parent
8828fcacf6
commit
788c10aacd
9 changed files with 333 additions and 7 deletions
|
|
@ -71,4 +71,12 @@ export interface ReferenceSite {
|
|||
readonly explicitReceiver?: { readonly name: string };
|
||||
/** Argument count at the call site; used by `provider.arityCompatibility`. */
|
||||
readonly arity?: number;
|
||||
/**
|
||||
* Inferred argument types at the call site, one per argument. An
|
||||
* empty-string entry means "unknown" — consumers narrowing overload
|
||||
* candidates treat unknown as any-match. Populated by languages
|
||||
* that can derive types from literals / constructor expressions
|
||||
* (C#: `42` → `'int'`, `"alice"` → `'string'`).
|
||||
*/
|
||||
readonly argumentTypes?: readonly string[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,11 +159,26 @@ export function emitCsharpScopeCaptures(
|
|||
findNodeAtRange(tree.rootNode, anchor.range, 'object_creation_expression');
|
||||
if (callNode !== null) {
|
||||
const argList = callNode.childForFieldName('arguments');
|
||||
const n =
|
||||
const args =
|
||||
argList === null
|
||||
? 0
|
||||
: argList.namedChildren.filter((c) => c !== null && c.type === 'argument').length;
|
||||
grouped['@reference.arity'] = syntheticCapture('@reference.arity', callNode, String(n));
|
||||
? []
|
||||
: argList.namedChildren.filter((c) => c !== null && c.type === 'argument');
|
||||
grouped['@reference.arity'] = syntheticCapture(
|
||||
'@reference.arity',
|
||||
callNode,
|
||||
String(args.length),
|
||||
);
|
||||
|
||||
// Infer argument types from literal nodes so overload
|
||||
// disambiguation can narrow same-arity candidates by param
|
||||
// type. Non-literal arguments emit empty string to indicate
|
||||
// "unknown" — consumers treat unknown as any-match.
|
||||
const argTypes = args.map((arg) => inferArgType(arg!));
|
||||
grouped['@reference.parameter-types'] = syntheticCapture(
|
||||
'@reference.parameter-types',
|
||||
callNode,
|
||||
JSON.stringify(argTypes),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,6 +261,38 @@ function synthesizePrimaryConstructor(typeNode: SyntaxNode): CaptureMatch | null
|
|||
|
||||
type SyntaxNode = ReturnType<ReturnType<typeof getCsharpParser>['parse']>['rootNode'];
|
||||
|
||||
/** Infer a C# argument's static type from literal / constructor
|
||||
* patterns. Returns `''` when the arg has no statically-derivable
|
||||
* type (e.g. identifier — would require full type inference). */
|
||||
function inferArgType(argNode: SyntaxNode): string {
|
||||
// `argument > expression` — tree-sitter-c-sharp wraps the value.
|
||||
const expr = argNode.namedChild(0);
|
||||
if (expr === null) return '';
|
||||
switch (expr.type) {
|
||||
case 'integer_literal':
|
||||
return 'int';
|
||||
case 'real_literal':
|
||||
return 'double';
|
||||
case 'string_literal':
|
||||
case 'verbatim_string_literal':
|
||||
case 'interpolated_string_expression':
|
||||
case 'raw_string_literal':
|
||||
return 'string';
|
||||
case 'character_literal':
|
||||
return 'char';
|
||||
case 'boolean_literal':
|
||||
return 'bool';
|
||||
case 'null_literal':
|
||||
return 'null';
|
||||
case 'object_creation_expression': {
|
||||
const typeNode = expr.childForFieldName('type');
|
||||
return typeNode?.text ?? '';
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** Find the first C# function-like node at the given range. The
|
||||
* declaration anchor range covers the whole method/constructor/etc.
|
||||
* node, but the tag alone doesn't tell us which node type. */
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ import { SupportedLanguages } from 'gitnexus-shared';
|
|||
*/
|
||||
export const MIGRATED_LANGUAGES: ReadonlySet<SupportedLanguages> = new Set<SupportedLanguages>([
|
||||
SupportedLanguages.Python,
|
||||
SupportedLanguages.CSharp,
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -800,6 +800,7 @@ function pass5CollectReferences(
|
|||
: undefined;
|
||||
const explicitReceiver = extractExplicitReceiver(match);
|
||||
const arity = extractArity(match);
|
||||
const argumentTypes = extractArgumentTypes(match);
|
||||
|
||||
const site: ReferenceSite = {
|
||||
name: nameCap.text,
|
||||
|
|
@ -809,6 +810,7 @@ function pass5CollectReferences(
|
|||
...(callForm !== undefined ? { callForm } : {}),
|
||||
...(explicitReceiver !== undefined ? { explicitReceiver } : {}),
|
||||
...(arity !== undefined ? { arity } : {}),
|
||||
...(argumentTypes !== undefined ? { argumentTypes } : {}),
|
||||
};
|
||||
referenceSites.push(site);
|
||||
}
|
||||
|
|
@ -882,6 +884,18 @@ function extractArity(match: CaptureMatch): number | undefined {
|
|||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
|
||||
function extractArgumentTypes(match: CaptureMatch): readonly string[] | undefined {
|
||||
const cap = match['@reference.parameter-types'];
|
||||
if (cap === undefined) return undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(cap.text);
|
||||
if (Array.isArray(parsed) && parsed.every((x) => typeof x === 'string')) return parsed;
|
||||
} catch {
|
||||
/* malformed — fall through */
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ─── Internal: range + capture utilities ───────────────────────────────────
|
||||
|
||||
function rangesEqual(a: Range, b: Range): boolean {
|
||||
|
|
@ -931,6 +945,10 @@ const KNOWN_SUB_TAGS: ReadonlySet<string> = new Set<string>([
|
|||
'@reference.name',
|
||||
'@reference.receiver',
|
||||
'@reference.arity',
|
||||
'@reference.parameter-types',
|
||||
'@declaration.parameter-count',
|
||||
'@declaration.required-parameter-count',
|
||||
'@declaration.parameter-types',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -46,12 +46,24 @@ import {
|
|||
*/
|
||||
export function resolveDefGraphId(
|
||||
filePath: string,
|
||||
def: { qualifiedName?: string; type?: NodeLabel },
|
||||
def: { qualifiedName?: string; type?: NodeLabel; parameterTypes?: readonly string[] },
|
||||
nodeLookup: GraphNodeLookup,
|
||||
): string | undefined {
|
||||
const qn = def.qualifiedName;
|
||||
if (qn === undefined || qn.length === 0) return undefined;
|
||||
if (def.type !== undefined) {
|
||||
// Overload disambiguation: when the def carries parameter types,
|
||||
// try the parameter-typed key first so same-name same-arity
|
||||
// overloads route to their distinct graph nodes.
|
||||
if (
|
||||
def.type === 'Method' &&
|
||||
def.parameterTypes !== undefined &&
|
||||
def.parameterTypes.length > 0
|
||||
) {
|
||||
const pKey = qualifiedKey(filePath, def.type, `${qn}~${def.parameterTypes.join(',')}`);
|
||||
const pHit = nodeLookup.get(pKey);
|
||||
if (pHit !== undefined) return pHit;
|
||||
}
|
||||
const qualifiedHit = nodeLookup.get(qualifiedKey(filePath, def.type, qn));
|
||||
if (qualifiedHit !== undefined) return qualifiedHit;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,18 @@ export function buildGraphNodeLookup(graph: KnowledgeGraph): GraphNodeLookup {
|
|||
if (qualified !== undefined && qualified.length > 0) {
|
||||
const qKey = qualifiedKey(props.filePath, node.label, qualified);
|
||||
if (!lookup.has(qKey)) lookup.set(qKey, node.id);
|
||||
// Overload-disambiguating key: include parameter types so two
|
||||
// same-arity overloads (e.g. `Lookup(int)` vs `Lookup(string)`)
|
||||
// map to distinct graph nodes. Legacy parse-phase encodes the
|
||||
// type tag into the node id; we register both that node id and
|
||||
// a parameter-types-suffixed key so resolveDefGraphId can find
|
||||
// the right overload by matching its def's parameterTypes.
|
||||
const pTypes = (props as { parameterTypes?: readonly string[] }).parameterTypes;
|
||||
if (pTypes !== undefined && pTypes.length > 0 && node.label === 'Method') {
|
||||
const pKey = qualifiedKey(props.filePath, node.label, `${qualified}~${pTypes.join(',')}`);
|
||||
// Each overload is unique — set unconditionally.
|
||||
lookup.set(pKey, node.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback key: simple name. First-wins within a file — used when
|
||||
|
|
|
|||
|
|
@ -53,11 +53,18 @@ export function emitFreeCallFallback(
|
|||
fnDef = pickConstructorOrClass(classDef, workspaceIndex);
|
||||
}
|
||||
}
|
||||
// Implicit-this overload narrowing: an unqualified call inside
|
||||
// a method body might be calling a sibling overload on the
|
||||
// enclosing class. When the workspace has multiple methods of
|
||||
// the same name in a single class, choose the best match by
|
||||
// arity + argument types.
|
||||
if (fnDef === undefined && workspaceIndex !== undefined) {
|
||||
fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex);
|
||||
}
|
||||
if (fnDef === undefined) {
|
||||
fnDef = findCallableBindingInScope(site.inScope, site.name, scopes);
|
||||
}
|
||||
if (fnDef === undefined) continue;
|
||||
|
||||
const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup);
|
||||
if (callerGraphId === undefined) continue;
|
||||
const tgtGraphId = resolveDefGraphId(fnDef.filePath, fnDef, nodeLookup);
|
||||
|
|
@ -102,3 +109,82 @@ function pickConstructorOrClass(
|
|||
}
|
||||
return classDef;
|
||||
}
|
||||
|
||||
/** Walk up from the call-site scope to the enclosing class scope,
|
||||
* pick a method member by name with overload narrowing on arity +
|
||||
* argument types. Returns undefined if there's no enclosing class
|
||||
* or no matching method. Used for implicit-this calls inside a
|
||||
* class body where multiple overloads share the call name. */
|
||||
function pickImplicitThisOverload(
|
||||
site: {
|
||||
readonly inScope: ScopeId;
|
||||
readonly name: string;
|
||||
readonly arity?: number;
|
||||
readonly argumentTypes?: readonly string[];
|
||||
},
|
||||
scopes: ScopeResolutionIndexes,
|
||||
workspaceIndex: WorkspaceResolutionIndex,
|
||||
): SymbolDefinition | undefined {
|
||||
// Find the enclosing Class scope by walking parents.
|
||||
let curId: ScopeId | null = site.inScope;
|
||||
let classScopeId: ScopeId | undefined;
|
||||
while (curId !== null) {
|
||||
const sc = scopes.scopeTree.getScope(curId);
|
||||
if (sc === undefined) break;
|
||||
if (sc.kind === 'Class') {
|
||||
classScopeId = sc.id;
|
||||
break;
|
||||
}
|
||||
curId = sc.parent;
|
||||
}
|
||||
if (classScopeId === undefined) return undefined;
|
||||
|
||||
// Find the Class def for that scope by reverse-lookup in
|
||||
// classScopeByDefId.
|
||||
let classDefId: string | undefined;
|
||||
for (const [defId, scope] of workspaceIndex.classScopeByDefId) {
|
||||
if (scope.id === classScopeId) {
|
||||
classDefId = defId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (classDefId === undefined) return undefined;
|
||||
|
||||
const overloads = workspaceIndex.membersByOwner.get(classDefId)?.get(site.name);
|
||||
if (overloads === undefined || overloads.length === 0) return undefined;
|
||||
if (overloads.length === 1) return overloads[0];
|
||||
|
||||
const argTypes = site.argumentTypes;
|
||||
const argCount = site.arity;
|
||||
// Filter by arity (same logic as pickOverload in receiver-bound-calls).
|
||||
const arityMatches =
|
||||
argCount === undefined
|
||||
? overloads
|
||||
: overloads.filter((d) => {
|
||||
const max = d.parameterCount;
|
||||
const min = d.requiredParameterCount;
|
||||
if (max !== undefined && argCount > max) {
|
||||
const variadic =
|
||||
d.parameterTypes !== undefined &&
|
||||
d.parameterTypes.some((t) => t === 'params' || t.startsWith('params '));
|
||||
if (!variadic) return false;
|
||||
}
|
||||
if (min !== undefined && argCount < min) return false;
|
||||
return true;
|
||||
});
|
||||
const candidates = arityMatches.length > 0 ? arityMatches : overloads;
|
||||
|
||||
if (argTypes !== undefined && argTypes.length > 0) {
|
||||
const typed = candidates.filter((d) => {
|
||||
const params = d.parameterTypes;
|
||||
if (params === undefined) return false;
|
||||
for (let i = 0; i < argTypes.length && i < params.length; i++) {
|
||||
if (argTypes[i] === '') continue;
|
||||
if (argTypes[i] !== params[i]) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (typed.length >= 1) return typed[0];
|
||||
}
|
||||
return candidates[0];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import {
|
|||
} from '../scope/walkers.js';
|
||||
import { tryEmitEdge } from '../graph-bridge/edges.js';
|
||||
import { resolveCompoundReceiverClass } from '../passes/compound-receiver.js';
|
||||
import { resolveDefGraphId } from '../graph-bridge/ids.js';
|
||||
|
||||
/** Subset of `ScopeResolver` consumed by this pass. Accepting the
|
||||
* subset rather than the full provider keeps tests and partial
|
||||
|
|
@ -71,6 +72,67 @@ export function emitReceiverBoundCalls(
|
|||
const fieldFallback = provider.fieldFallbackOnMethodLookup ?? true;
|
||||
const collapse = provider.collapseMemberCallsByCallerTarget === true;
|
||||
|
||||
// Build an interface → implementors map from IMPLEMENTS edges.
|
||||
// Maps Interface graph-id → list of implementor class scope-def-ids.
|
||||
// We translate graph-ids back to scope-resolution DefIds via
|
||||
// `parsedFiles.localDefs` lookup so downstream `findOwnedMember`
|
||||
// (which keys by DefId) can find the implementor's members.
|
||||
const graphIdToClassDef = new Map<string, SymbolDefinition>();
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const def of parsed.localDefs) {
|
||||
if (def.type !== 'Class' && def.type !== 'Interface') continue;
|
||||
const graphId = resolveDefGraphId(parsed.filePath, def, nodeLookup);
|
||||
if (graphId !== undefined) graphIdToClassDef.set(graphId, def);
|
||||
}
|
||||
}
|
||||
const implementorsByInterfaceDefId = new Map<string, SymbolDefinition[]>();
|
||||
for (const rel of graph.iterRelationshipsByType('IMPLEMENTS')) {
|
||||
const ifaceDef = graphIdToClassDef.get(rel.targetId);
|
||||
const implDef = graphIdToClassDef.get(rel.sourceId);
|
||||
if (ifaceDef === undefined || implDef === undefined) continue;
|
||||
let list = implementorsByInterfaceDefId.get(ifaceDef.nodeId);
|
||||
if (list === undefined) {
|
||||
list = [];
|
||||
implementorsByInterfaceDefId.set(ifaceDef.nodeId, list);
|
||||
}
|
||||
list.push(implDef);
|
||||
}
|
||||
|
||||
/** Emit secondary CALLS edges with reason='interface-dispatch'
|
||||
* when the primary receiver-typed edge targeted an Interface's
|
||||
* method. Each implementing class's same-named method gets a
|
||||
* secondary edge (excluding the primary target itself). */
|
||||
const emitInterfaceDispatchFor = (
|
||||
ownerDef: SymbolDefinition,
|
||||
memberName: string,
|
||||
primaryMemberDef: SymbolDefinition,
|
||||
site: ParsedFile['referenceSites'][number],
|
||||
confidence: number,
|
||||
): number => {
|
||||
if (ownerDef.type !== 'Interface') return 0;
|
||||
const impls = implementorsByInterfaceDefId.get(ownerDef.nodeId);
|
||||
if (impls === undefined) return 0;
|
||||
let n = 0;
|
||||
for (const implDef of impls) {
|
||||
const implMember = findOwnedMember(implDef.nodeId, memberName, index);
|
||||
if (implMember === undefined) continue;
|
||||
if (implMember.nodeId === primaryMemberDef.nodeId) continue;
|
||||
const ok = tryEmitEdge(
|
||||
graph,
|
||||
scopes,
|
||||
nodeLookup,
|
||||
site,
|
||||
implMember,
|
||||
'interface-dispatch',
|
||||
seen,
|
||||
confidence,
|
||||
collapse,
|
||||
);
|
||||
if (ok) n++;
|
||||
}
|
||||
return n;
|
||||
};
|
||||
|
||||
for (const parsed of parsedFiles) {
|
||||
const namespaceTargets = collectNamespaceTargets(parsed, scopes);
|
||||
|
||||
|
|
@ -291,7 +353,7 @@ export function emitReceiverBoundCalls(
|
|||
const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)];
|
||||
let memberDef: SymbolDefinition | undefined;
|
||||
for (const ownerId of chain) {
|
||||
memberDef = findOwnedMember(ownerId, memberName, index);
|
||||
memberDef = pickOverload(ownerId, memberName, site, index);
|
||||
if (memberDef !== undefined) break;
|
||||
}
|
||||
if (memberDef !== undefined) {
|
||||
|
|
@ -317,6 +379,10 @@ export function emitReceiverBoundCalls(
|
|||
collapse,
|
||||
);
|
||||
if (ok) emitted++;
|
||||
// Interface dispatch: when the primary owner is an
|
||||
// Interface, emit secondary CALLS edges to every
|
||||
// implementing class's same-named method.
|
||||
emitted += emitInterfaceDispatchFor(ownerDef, memberName, memberDef, site, confidence);
|
||||
// Always mark handled when the site was resolved, even
|
||||
// if the edge was deduplicated (collapse mode), so
|
||||
// `emitReferencesViaLookup` doesn't re-emit from the
|
||||
|
|
@ -371,3 +437,61 @@ export function emitReceiverBoundCalls(
|
|||
|
||||
return emitted;
|
||||
}
|
||||
|
||||
/** Resolve a member by name on a class def, narrowing by argument
|
||||
* types when multiple overloads share the name. Falls back to the
|
||||
* first-seen def (legacy `findOwnedMember` semantics) when there's
|
||||
* no narrowing signal or when `argumentTypes` is unavailable. */
|
||||
function pickOverload(
|
||||
ownerId: string,
|
||||
memberName: string,
|
||||
site: ParsedFile['referenceSites'][number],
|
||||
index: WorkspaceResolutionIndex,
|
||||
): SymbolDefinition | undefined {
|
||||
const overloads = index.membersByOwner.get(ownerId)?.get(memberName);
|
||||
if (overloads === undefined || overloads.length === 0) {
|
||||
return findOwnedMember(ownerId, memberName, index);
|
||||
}
|
||||
if (overloads.length === 1) return overloads[0];
|
||||
|
||||
const argTypes = site.argumentTypes;
|
||||
const argCount = site.arity;
|
||||
|
||||
// First filter by arity: exact-required-match wins over variadic.
|
||||
const arityMatches =
|
||||
argCount === undefined
|
||||
? overloads
|
||||
: overloads.filter((d) => {
|
||||
const max = d.parameterCount;
|
||||
const min = d.requiredParameterCount;
|
||||
if (max !== undefined && argCount > max) {
|
||||
const variadic =
|
||||
d.parameterTypes !== undefined &&
|
||||
d.parameterTypes.some((t) => t === 'params' || t.startsWith('params '));
|
||||
if (!variadic) return false;
|
||||
}
|
||||
if (min !== undefined && argCount < min) return false;
|
||||
return true;
|
||||
});
|
||||
const candidates = arityMatches.length > 0 ? arityMatches : overloads;
|
||||
|
||||
// Then narrow by argument-type alignment when both sides are known.
|
||||
if (argTypes !== undefined && argTypes.length > 0) {
|
||||
const typed = candidates.filter((d) => {
|
||||
const params = d.parameterTypes;
|
||||
if (params === undefined) return false;
|
||||
// Compare each arg-type slot against the corresponding param.
|
||||
// Empty arg-type means "unknown" — counts as match. Mismatches
|
||||
// disqualify.
|
||||
for (let i = 0; i < argTypes.length && i < params.length; i++) {
|
||||
if (argTypes[i] === '') continue;
|
||||
if (argTypes[i] !== params[i]) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (typed.length === 1) return typed[0];
|
||||
if (typed.length > 0) return typed[0];
|
||||
}
|
||||
|
||||
return candidates[0];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ export interface WorkspaceResolutionIndex {
|
|||
* Built from `parsed.localDefs` so class-owned members land in the
|
||||
* right bucket via their `ownerId`. */
|
||||
readonly memberByOwner: ReadonlyMap<string, ReadonlyMap<string, SymbolDefinition>>;
|
||||
/** Multi-valued variant of `memberByOwner` so consumers narrowing
|
||||
* by parameter types (overload resolution) can see every candidate.
|
||||
* `memberByOwner` continues to return the first-seen def to
|
||||
* preserve existing consumers. */
|
||||
readonly membersByOwner: ReadonlyMap<string, ReadonlyMap<string, readonly SymbolDefinition[]>>;
|
||||
|
||||
/** File path → (simple-name → first matching module-scope-owned
|
||||
* `SymbolDefinition`). Backs `findExportedDef` — the lookup for
|
||||
|
|
@ -57,6 +62,7 @@ export function buildWorkspaceResolutionIndex(
|
|||
const classScopeByDefId = new Map<string, Scope>();
|
||||
const moduleScopeByFile = new Map<string, Scope>();
|
||||
const memberByOwner = new Map<string, Map<string, SymbolDefinition>>();
|
||||
const membersByOwner = new Map<string, Map<string, SymbolDefinition[]>>();
|
||||
const defsByFileAndName = new Map<string, Map<string, SymbolDefinition>>();
|
||||
const callablesBySimpleName = new Map<string, SymbolDefinition[]>();
|
||||
|
||||
|
|
@ -127,11 +133,23 @@ export function buildWorkspaceResolutionIndex(
|
|||
}
|
||||
// First-seen wins to match `findOwnedMember` semantics.
|
||||
if (!memberBucket.has(simple)) memberBucket.set(simple, def);
|
||||
|
||||
// Multi-valued variant — keeps every overload for
|
||||
// parameter-type narrowing.
|
||||
let membersBucket = membersByOwner.get(ownerId);
|
||||
if (membersBucket === undefined) {
|
||||
membersBucket = new Map();
|
||||
membersByOwner.set(ownerId, membersBucket);
|
||||
}
|
||||
const overloads = membersBucket.get(simple);
|
||||
if (overloads === undefined) membersBucket.set(simple, [def]);
|
||||
else overloads.push(def);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
classScopeByDefId,
|
||||
membersByOwner,
|
||||
memberByOwner,
|
||||
defsByFileAndName,
|
||||
callablesBySimpleName,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue