mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-26 04:19:33 +08:00
fix(js,ts): a call on an element of a class field reaches the element's type
A TypeScript or JavaScript method called on an element of a class field, like this.handlers[type].handle() or this.items[0].save(), kept the bare method name: a member chain rooted at this collapses to it so the resolver can read the owner off the enclosing class. A subscript in the chain leaves the resolver nothing to read, so the bare name exact-matched any project method of that name, often another class's (this.listeners[type].push(h) reached an unrelated UndoStack.push). Both engines now emit this.<field>[k].<m>() as this.<field>[].<m>, and this['svc'].m() as this.svc.m; any other subscript in a this-rooted chain, or under window, emits nothing, as arr[0].run() already did. The resolver reads the element type the field's declaration writes (X[], Array<X>, ReadonlyArray<X>, Record<K, X>), on the calling class or a base, and resolves the method on it or leaves the call unresolved: a type parameter of the declaring class, a union with another type, an array of arrays or a non-plain element hold no one type. The class type-parameter reader now reads from the keyword to the list's own >, over several lines and through braces in constraints. Our changes: kept escapeRegExp from PR #2769 in the conflict; the type-parameter reader also accepts the type keyword, since the T['p'] path passes type-alias owners and the PR as written read State<Item>['current'] as the project class Item (new case in ts-array-annotated-receivers); CHANGELOG covers JavaScript and asks for a re-index. Validation: the element tests fail 10 of 11 on main with both engines; the type-alias case fails on the PR as written and its brace half on main. A/B against main, kernel built per arm: flask and gin byte-identical. TS: pixijs 7 links added, 5 retargeted and 5 typed, all right, 15 removed (9 wrong, 6 right only by name: a cast, a mixin's this, a T[] parameter, a merged mixin member, an index-signature type, an any field); typeorm 3 wrong removed, 1 right lost, 2 retargeted to the declared interface; excalidraw 1 wrong removed; vscode's base and editor 19 added, 24 retargeted and 35 typed, all right, 45 removed (37 wrong, 8 right only by name: a getter, an index-signature field, deep chains, a T[] parameter, a union alias). JS: three.js, playcanvas and pdf.js lose 288 bare-name guesses, about 230 of them wrong (array, Map and DOM calls on project classes). Kernel parity holds on every repo with deferrals unchanged. Index CPU time within noise (pixijs 32.4 s to 33.3 s, three.js 64.7 s to 67.9 s, medians of 3 interleaved). Original PR #2766 by @inth3shadows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Colby Mchenry
co-authored by
Claude Opus 5.5
parent
5c5bfd4051
commit
449eefa7da
@@ -684,16 +684,42 @@ function tsJsCalleeName(func: SyntaxNode, source: string): { name: string; at: S
|
||||
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(chain) ? { name: chain, at: cur } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A TS/JS receiver that subscripts the enclosing class's object: an element
|
||||
* of a field, `this.<field>[k]` → `this.<field>[]`, or a field named by a
|
||||
* string key, `this['svc']` → `this.svc`. Null for any other subscript.
|
||||
* Mirrored in the kernel's this_subscript_of.
|
||||
*/
|
||||
function tsThisSubscriptReceiver(node: SyntaxNode, source: string): string | null {
|
||||
if (node.type !== 'subscript_expression') return null;
|
||||
const objectNode = getChildByField(node, 'object');
|
||||
if (!objectNode) return null;
|
||||
const object = peelTsJsReceiver(objectNode);
|
||||
if (object.type === 'this') {
|
||||
const index = getChildByField(node, 'index');
|
||||
const key = index?.type === 'string' ? /^(['"])([A-Za-z_$][\w$]*)\1$/.exec(getNodeText(index, source))?.[2] : undefined;
|
||||
return key ? `this.${key}` : null;
|
||||
}
|
||||
if (object.type !== 'member_expression' || getChildByField(object, 'object')?.type !== 'this') return null;
|
||||
const property = getChildByField(object, 'property');
|
||||
if (!property || !THIS_FIELD_PROPERTY_TYPES.has(property.type)) return null;
|
||||
return `this.${getNodeText(property, source)}[]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a TS/JS receiver still collapses to the bare method name: `this` /
|
||||
* `super` (the resolver reads the owner off the enclosing class), a member
|
||||
* chain rooted at either or at `window` (the project-global escape of
|
||||
* {@link isUnresolvedTsJsChain}), and `new C()` (its class is written at the
|
||||
* call).
|
||||
* call). A subscript anywhere in the chain does not: the resolver cannot
|
||||
* read the receiver back from it, so the bare name would exact-match any
|
||||
* project method of that name. (An element of a this-field is typed instead,
|
||||
* see {@link tsThisSubscriptReceiver}.)
|
||||
*/
|
||||
function keepsBareTsJsReceiver(node: SyntaxNode, source: string): boolean {
|
||||
let cur: SyntaxNode | null = node;
|
||||
while (cur && TS_JS_CHAIN_RECEIVER_TYPES.has(cur.type)) {
|
||||
if (cur.type === 'subscript_expression') return false;
|
||||
const object = getChildByField(cur, 'object');
|
||||
cur = object ? peelTsJsReceiver(object) : null;
|
||||
}
|
||||
@@ -6022,6 +6048,8 @@ export class TreeSitterExtractor {
|
||||
: rawReceiver && this.language === 'swift'
|
||||
? peelSwiftCallWrappers(rawReceiver, this.source)
|
||||
: rawReceiver;
|
||||
// `this.<field>[]` / `this.<name>` of a subscript receiver, set by its branch below.
|
||||
let thisSubscript: string | null = null;
|
||||
// A LITERAL receiver — `", ".join(...)`, `"x".toUpperCase()`,
|
||||
// `5.times`, `[].concat(...)` — calls a builtin of the literal's
|
||||
// type, never a project symbol. The bare-name fallback below let
|
||||
@@ -6215,6 +6243,18 @@ export class TreeSitterExtractor {
|
||||
// its name keeps the `#` (#1987).
|
||||
const fieldName = getNodeText(getChildByField(receiver, 'property')!, this.source);
|
||||
calleeName = `this.${fieldName}.${methodName}`;
|
||||
} else if (
|
||||
TS_JS_CHAIN_LANGUAGES.has(this.language) &&
|
||||
receiver &&
|
||||
(thisSubscript = tsThisSubscriptReceiver(receiver, this.source)) !== null
|
||||
) {
|
||||
// An element of a field of the enclosing class —
|
||||
// `this.listeners[type].push()`, `this.items[0].save()` — kept
|
||||
// as `this.<field>[].<method>`: the resolver reads the element
|
||||
// type off the field's declaration (`Item[]`, `Record<K, V>`)
|
||||
// or leaves it unresolved. `this['svc'].start()` is the field
|
||||
// itself. Mirrored in the kernel's extract_call.
|
||||
calleeName = `${thisSubscript}.${methodName}`;
|
||||
} else if (
|
||||
(this.language === 'typescript' ||
|
||||
this.language === 'javascript' ||
|
||||
@@ -6276,7 +6316,7 @@ export class TreeSitterExtractor {
|
||||
) {
|
||||
// Any other TS/JS receiver is an expression with no static
|
||||
// type here — `(a ?? b).map()`, `f().list.map()`,
|
||||
// `arr[0].run()`, `(() => {}).call()`. The bare method name this
|
||||
// `arr[0].run()`, `this.a.b[k].run()`, `(() => {}).call()`. The bare method name this
|
||||
// used to emit exact-matched whichever project method shared the
|
||||
// name (`(await list()).map()` onto an adapter class's `map`).
|
||||
// Emit nothing: a silent miss, never a wrong edge. Mirrored in
|
||||
|
||||
Reference in New Issue
Block a user