mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
fix(scope): remove non-null assertions from scope resolution
Made-with: Cursor
This commit is contained in:
parent
fc919ad6de
commit
eeacd57822
5 changed files with 93 additions and 62 deletions
|
|
@ -62,6 +62,14 @@ const CALL_TAGS = [
|
|||
'@reference.call.constructor',
|
||||
] as const;
|
||||
|
||||
function pickFirstDefined(grouped: CaptureMatch, tags: readonly string[]): Capture | undefined {
|
||||
for (const tag of tags) {
|
||||
const cap = grouped[tag];
|
||||
if (cap !== undefined) return cap;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop `@reference.read.member` matches whose underlying `member_expression`
|
||||
* is NOT actually a read context:
|
||||
|
|
@ -189,10 +197,9 @@ export function emitTsScopeCaptures(
|
|||
// overloads — TypeScript supports overload signatures via
|
||||
// function_signature, so `parameterTypes` is populated when
|
||||
// available.
|
||||
const declTag = FUNCTION_DECL_TAGS.find((t) => grouped[t] !== undefined);
|
||||
if (declTag !== undefined) {
|
||||
const anchor = grouped[declTag]!;
|
||||
const fnNode = findFunctionNode(tree.rootNode, anchor.range);
|
||||
const declAnchor = pickFirstDefined(grouped, FUNCTION_DECL_TAGS);
|
||||
if (declAnchor !== undefined) {
|
||||
const fnNode = findFunctionNode(tree.rootNode, declAnchor.range);
|
||||
if (fnNode !== null) {
|
||||
const arity = computeTsArityMetadata(fnNode);
|
||||
if (arity.parameterCount !== undefined) {
|
||||
|
|
@ -223,25 +230,26 @@ export function emitTsScopeCaptures(
|
|||
// arity filter can narrow overloads. Count the `argument` named
|
||||
// children of the backing `arguments` node. TypeScript constructor
|
||||
// calls use `new_expression`; regular calls use `call_expression`.
|
||||
const callTag = CALL_TAGS.find((t) => grouped[t] !== undefined);
|
||||
if (callTag !== undefined && grouped['@reference.arity'] === undefined) {
|
||||
const anchor = grouped[callTag]!;
|
||||
const callAnchor = pickFirstDefined(grouped, CALL_TAGS);
|
||||
if (callAnchor !== undefined && grouped['@reference.arity'] === undefined) {
|
||||
const callNode =
|
||||
findNodeAtRange(tree.rootNode, anchor.range, 'call_expression') ??
|
||||
findNodeAtRange(tree.rootNode, anchor.range, 'new_expression');
|
||||
findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression') ??
|
||||
findNodeAtRange(tree.rootNode, callAnchor.range, 'new_expression');
|
||||
if (callNode !== null) {
|
||||
const argList = callNode.childForFieldName('arguments');
|
||||
const args =
|
||||
const args: SyntaxNode[] =
|
||||
argList === null
|
||||
? []
|
||||
: argList.namedChildren.filter((c) => c !== null && c.type !== 'comment');
|
||||
: argList.namedChildren.filter(
|
||||
(c): c is SyntaxNode => c !== null && c.type !== 'comment',
|
||||
);
|
||||
grouped['@reference.arity'] = syntheticCapture(
|
||||
'@reference.arity',
|
||||
callNode,
|
||||
String(args.length),
|
||||
);
|
||||
|
||||
const argTypes = args.map((arg) => inferArgType(arg!));
|
||||
const argTypes = args.map((arg) => inferArgType(arg));
|
||||
grouped['@reference.parameter-types'] = syntheticCapture(
|
||||
'@reference.parameter-types',
|
||||
callNode,
|
||||
|
|
@ -258,9 +266,9 @@ export function emitTsScopeCaptures(
|
|||
// signature, arrow/function-expression assigned to a class field).
|
||||
// Arrow functions nested inside method bodies rely on scope-chain
|
||||
// lookup instead of synthesis — covered by `tsReceiverBinding`.
|
||||
if (grouped['@scope.function'] !== undefined) {
|
||||
const anchor = grouped['@scope.function']!;
|
||||
const fnNode = findFunctionNode(tree.rootNode, anchor.range);
|
||||
const scopeFnAnchor = grouped['@scope.function'];
|
||||
if (scopeFnAnchor !== undefined) {
|
||||
const fnNode = findFunctionNode(tree.rootNode, scopeFnAnchor.range);
|
||||
if (fnNode !== null) {
|
||||
const synth = synthesizeTsReceiverBinding(fnNode);
|
||||
if (synth !== null) out.push(synth);
|
||||
|
|
@ -301,8 +309,9 @@ export function emitTsScopeCaptures(
|
|||
*/
|
||||
function synthesizeDestructuringBindings(root: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const stack: SyntaxNode[] = [root];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
for (;;) {
|
||||
const node = stack.pop();
|
||||
if (node === undefined) break;
|
||||
for (const child of node.namedChildren) {
|
||||
if (child !== null) stack.push(child);
|
||||
}
|
||||
|
|
@ -364,8 +373,9 @@ function synthesizeDestructuringBindings(root: SyntaxNode, out: CaptureMatch[]):
|
|||
*/
|
||||
function synthesizeForOfMapTupleBindings(root: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const stack: SyntaxNode[] = [root];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
for (;;) {
|
||||
const node = stack.pop();
|
||||
if (node === undefined) break;
|
||||
for (const child of node.namedChildren) {
|
||||
if (child !== null) stack.push(child);
|
||||
}
|
||||
|
|
@ -412,8 +422,9 @@ function synthesizeForOfMapTupleBindings(root: SyntaxNode, out: CaptureMatch[]):
|
|||
*/
|
||||
function synthesizeInstanceofNarrowings(root: SyntaxNode, out: CaptureMatch[]): void {
|
||||
const stack: SyntaxNode[] = [root];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
for (;;) {
|
||||
const node = stack.pop();
|
||||
if (node === undefined) break;
|
||||
for (const child of node.namedChildren) {
|
||||
if (child !== null) stack.push(child);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,28 +45,34 @@ interface TypescriptResolutionConfig {
|
|||
* total work for what should be O(N_files + N_imports).
|
||||
*/
|
||||
function makeTsResolveImportTarget(): ScopeResolver['resolveImportTarget'] {
|
||||
let cachedAllFilePaths: ReadonlySet<string> | null = null;
|
||||
let cachedSet: Set<string> | null = null;
|
||||
let cachedAllFileList: readonly string[] | null = null;
|
||||
let cachedNormalizedFileList: readonly string[] | null = null;
|
||||
let cachedResolveCache: Map<string, string | null> | null = null;
|
||||
interface PassCache {
|
||||
readonly key: ReadonlySet<string>;
|
||||
readonly allFilePaths: Set<string>;
|
||||
readonly allFileList: readonly string[];
|
||||
readonly normalizedFileList: readonly string[];
|
||||
readonly resolveCache: Map<string, string | null>;
|
||||
}
|
||||
let cached: PassCache | null = null;
|
||||
|
||||
return (targetRaw, fromFile, allFilePaths, resolutionConfig) => {
|
||||
if (cachedAllFilePaths !== allFilePaths) {
|
||||
cachedAllFilePaths = allFilePaths;
|
||||
cachedSet = new Set(allFilePaths);
|
||||
cachedAllFileList = Array.from(allFilePaths);
|
||||
cachedNormalizedFileList = cachedAllFileList.map((f) => f.toLowerCase());
|
||||
cachedResolveCache = new Map();
|
||||
if (cached === null || cached.key !== allFilePaths) {
|
||||
const allFileList = Array.from(allFilePaths);
|
||||
cached = {
|
||||
key: allFilePaths,
|
||||
allFilePaths: new Set(allFilePaths),
|
||||
allFileList,
|
||||
normalizedFileList: allFileList.map((f) => f.toLowerCase()),
|
||||
resolveCache: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
const cfg = resolutionConfig as TypescriptResolutionConfig | undefined;
|
||||
const ws: TsResolveContext = {
|
||||
fromFile,
|
||||
allFilePaths: cachedSet!,
|
||||
allFileList: cachedAllFileList!,
|
||||
normalizedFileList: cachedNormalizedFileList!,
|
||||
resolveCache: cachedResolveCache!,
|
||||
allFilePaths: cached.allFilePaths,
|
||||
allFileList: cached.allFileList,
|
||||
normalizedFileList: cached.normalizedFileList,
|
||||
resolveCache: cached.resolveCache,
|
||||
tsconfigPaths: cfg?.tsconfigPaths ?? null,
|
||||
};
|
||||
return resolveTsTarget(targetRaw, ws);
|
||||
|
|
|
|||
|
|
@ -34,6 +34,16 @@ import {
|
|||
* pathological recursion if the receiver text is malformed. */
|
||||
const COMPOUND_RECEIVER_MAX_DEPTH = 4;
|
||||
|
||||
const MAP_TUPLE_SENTINEL_RE = /^__MAP_TUPLE_(\d+)__:(.+)$/;
|
||||
|
||||
function parseMapTupleSentinel(text: string): { tupleIdx: number; rhs: string } | null {
|
||||
const match = MAP_TUPLE_SENTINEL_RE.exec(text);
|
||||
if (match === null) return null;
|
||||
const [, idxStr, rhs] = match;
|
||||
if (idxStr === undefined || rhs === undefined) return null;
|
||||
return { tupleIdx: Number(idxStr), rhs };
|
||||
}
|
||||
|
||||
interface ResolveCompoundReceiverOptions {
|
||||
/** When true (default), if method lookup fails on the receiver's
|
||||
* class, walk its fields and try the lookup on each field's class.
|
||||
|
|
@ -73,13 +83,11 @@ export function resolveCompoundReceiverClass(
|
|||
// "static receiver" shapes like `UserService.findUser()` where
|
||||
// `UserService` isn't a variable but a class imported into scope.
|
||||
if (!text.includes('.') && !text.includes('(')) {
|
||||
const mapTuple = /^__MAP_TUPLE_(\d+)__:(.+)$/.exec(text);
|
||||
const mapTuple = parseMapTupleSentinel(text);
|
||||
if (mapTuple !== null) {
|
||||
const tupleIdx = Number(mapTuple[1]);
|
||||
const rhs = mapTuple[2]!;
|
||||
const rhsTb = findReceiverTypeBinding(inScope, rhs, scopes);
|
||||
const rhsTb = findReceiverTypeBinding(inScope, mapTuple.rhs, scopes);
|
||||
if (rhsTb === undefined) return undefined;
|
||||
const arg = extractShallowMapTypeArgByIndex(rhsTb.rawName, tupleIdx);
|
||||
const arg = extractShallowMapTypeArgByIndex(rhsTb.rawName, mapTuple.tupleIdx);
|
||||
if (arg === undefined) return undefined;
|
||||
return findClassBindingInScope(rhsTb.declaredAtScope, arg, scopes);
|
||||
}
|
||||
|
|
@ -89,13 +97,11 @@ export function resolveCompoundReceiverClass(
|
|||
// Map for-of: binding name is `user` but rawType is
|
||||
// `__MAP_TUPLE_i__:entries` (see captures.ts) — same extraction as
|
||||
// the literal-sentinel branch above.
|
||||
const boundMapTuple = /^__MAP_TUPLE_(\d+)__:(.+)$/.exec(tb.rawName);
|
||||
const boundMapTuple = parseMapTupleSentinel(tb.rawName);
|
||||
if (boundMapTuple !== null) {
|
||||
const tupleIdx = Number(boundMapTuple[1]);
|
||||
const rhs = boundMapTuple[2]!;
|
||||
const rhsTb = findReceiverTypeBinding(inScope, rhs, scopes);
|
||||
const rhsTb = findReceiverTypeBinding(inScope, boundMapTuple.rhs, scopes);
|
||||
if (rhsTb === undefined) return undefined;
|
||||
const arg = extractShallowMapTypeArgByIndex(rhsTb.rawName, tupleIdx);
|
||||
const arg = extractShallowMapTypeArgByIndex(rhsTb.rawName, boundMapTuple.tupleIdx);
|
||||
if (arg === undefined) return undefined;
|
||||
return findClassBindingInScope(rhsTb.declaredAtScope, arg, scopes);
|
||||
}
|
||||
|
|
@ -266,7 +272,9 @@ export function resolveCompoundReceiverClass(
|
|||
// the element class directly. Resolved before the field-walk
|
||||
// because Dictionary-family types aren't local class defs.
|
||||
if (options.unwrapCollectionAccessor !== undefined && parts.length >= 2) {
|
||||
const last = parts[parts.length - 1]!;
|
||||
const last = parts[parts.length - 1];
|
||||
const headInner = parts[0];
|
||||
if (last === undefined || headInner === undefined) return undefined;
|
||||
const prefix = parts.slice(0, -1).join('.');
|
||||
let prefixType: TypeRef | undefined;
|
||||
if (parts.length === 2) {
|
||||
|
|
@ -276,16 +284,17 @@ export function resolveCompoundReceiverClass(
|
|||
// to find its typeRef. We need the TypeRef (not the class def)
|
||||
// because the hook inspects the raw generic args (e.g.
|
||||
// `Dictionary<string, User>`).
|
||||
const headInner = parts[0]!;
|
||||
let cur = findReceiverTypeBinding(inScope, headInner, scopes);
|
||||
for (let i = 1; i < parts.length - 1 && cur !== undefined; i++) {
|
||||
const segment = parts[i];
|
||||
if (segment === undefined) break;
|
||||
const cls = findClassBindingInScope(cur.declaredAtScope, cur.rawName, scopes);
|
||||
if (cls === undefined) {
|
||||
cur = undefined;
|
||||
break;
|
||||
}
|
||||
const cs = classScopeByDefId.get(cls.nodeId);
|
||||
cur = cs?.typeBindings.get(parts[i]!);
|
||||
cur = cs?.typeBindings.get(segment);
|
||||
}
|
||||
prefixType = cur;
|
||||
}
|
||||
|
|
@ -297,7 +306,8 @@ export function resolveCompoundReceiverClass(
|
|||
}
|
||||
}
|
||||
|
||||
const head = parts[0]!;
|
||||
const head = parts[0];
|
||||
if (head === undefined) return undefined;
|
||||
const headMemberName = stripCallParens(head);
|
||||
const headType = findReceiverTypeBinding(inScope, headMemberName, scopes);
|
||||
let currentClass: SymbolDefinition | undefined = headType
|
||||
|
|
@ -323,7 +333,8 @@ export function resolveCompoundReceiverClass(
|
|||
);
|
||||
}
|
||||
for (let i = 1; i < parts.length && currentClass !== undefined; i++) {
|
||||
const segment = parts[i]!;
|
||||
const segment = parts[i];
|
||||
if (segment === undefined) break;
|
||||
const memberName = stripCallParens(segment);
|
||||
const cs = classScopeByDefId.get(currentClass.nodeId);
|
||||
let memberType = cs?.typeBindings.get(memberName);
|
||||
|
|
@ -478,8 +489,9 @@ function resolveMapValueTypeNameFromPrefix(
|
|||
): string | undefined {
|
||||
const classScopeByDefId = index.classScopeByDefId;
|
||||
const parts = splitChainAtTopLevel(objExpr);
|
||||
if (parts.length === 0) return undefined;
|
||||
const headMemberName = stripCallParens(parts[0]!);
|
||||
const head = parts[0];
|
||||
if (head === undefined) return undefined;
|
||||
const headMemberName = stripCallParens(head);
|
||||
const headType = findReceiverTypeBinding(inScope, headMemberName, scopes);
|
||||
let currentClass: SymbolDefinition | undefined = headType
|
||||
? findClassBindingInScope(headType.declaredAtScope, headType.rawName, scopes)
|
||||
|
|
@ -501,7 +513,9 @@ function resolveMapValueTypeNameFromPrefix(
|
|||
}
|
||||
let lastMemberType: TypeRef | undefined;
|
||||
for (let i = 1; i < parts.length && currentClass !== undefined; i++) {
|
||||
const memberName = stripCallParens(parts[i]!);
|
||||
const segment = parts[i];
|
||||
if (segment === undefined) break;
|
||||
const memberName = stripCallParens(segment);
|
||||
const cs = classScopeByDefId.get(currentClass.nodeId);
|
||||
if (cs === undefined) return undefined;
|
||||
let memberType = cs.typeBindings.get(memberName);
|
||||
|
|
|
|||
|
|
@ -97,8 +97,9 @@ export const defaultLinearize: LinearizeStrategy = (_classDefId, directParents,
|
|||
const ancestors: string[] = [];
|
||||
const visited = new Set<string>();
|
||||
const queue: string[] = [...directParents];
|
||||
while (queue.length > 0) {
|
||||
const cur = queue.shift()!;
|
||||
for (;;) {
|
||||
const cur = queue.shift();
|
||||
if (cur === undefined) break;
|
||||
if (visited.has(cur)) continue;
|
||||
visited.add(cur);
|
||||
ancestors.push(cur);
|
||||
|
|
|
|||
|
|
@ -335,12 +335,11 @@ export function emitReceiverBoundCalls(
|
|||
}
|
||||
|
||||
// ── Case 3b: chain-typebinding (`city → user.get_city`) ──────
|
||||
if (
|
||||
typeRef !== undefined &&
|
||||
typeRef.rawName.includes('.') &&
|
||||
!typeRef.rawName.includes('(') &&
|
||||
!namespaceTargets.has(typeRef.rawName.split('.')[0]!)
|
||||
) {
|
||||
const chainHead =
|
||||
typeRef !== undefined && typeRef.rawName.includes('.') && !typeRef.rawName.includes('(')
|
||||
? (typeRef.rawName.split('.', 1)[0] ?? '')
|
||||
: undefined;
|
||||
if (typeRef !== undefined && chainHead !== undefined && !namespaceTargets.has(chainHead)) {
|
||||
// Try the plain dotted-field walk first — covers property /
|
||||
// collection-accessor shapes (`.Values`, Kotlin `.size`) and
|
||||
// field chains. Fall back to call-form (`x()`) which treats
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue