Repository navigation
fix(js,ts): a call on an element of a class field reaches the element's type - #2852
Merged
Merged
Conversation
…'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 was referenced Oct 23, 2026
Merged
colbymchenry
pushed a commit
that referenced
this pull request
Oct 23, 2026
A TypeScript or JavaScript call on a constructed object, new C(args).m(), reached the resolver as the bare method name in both engines, so exact matching bound it to any project method called m: new RegExp(p).exec(s) to a database wrapper's exec, new Array(n).fill(0) to a text class's fill, new TextEncoder().encode(s) to a project encode function. Both engines now emit new C().m (new ns.C().m for a namespaced class, and nothing for a constructor that is not a plain name or member chain). The resolver looks m up on C with resolveMethodOnType, inherited methods on the conformance pass once extends edges exist, an aliased named import mapped back to its class, and a JS built-in (RegExp, Map, Date...) never a project class unless the file imports or declares one. An ES5 constructor function keeps its prototype method. Our changes: the lookup passes the file the call's file imports the class from (tsImportedTypeFile), so of two classes sharing a name the imported one is the one constructed (the PR as written took the first indexed copy, at 0.9); new this.constructor() and a static new this() keep the bare method name, as a call on this does (the PR dropped three.js's clone idiom, 29 links to a class's own copy); rebased onto PR #2852, which edits the same keepsBareTsJsReceiver and test lists; resolved the import list conflict with rustChainedCallByName from PR #2846; CHANGELOG bullet moved next to the JS/TS receiver bullets. Validation: the new tests fail 4 of 6 on main 449eefa with both engines and pass here. A/B against main, kernel per arm: flask, gin and vite byte-identical. codegraph 48 links added (new ToolHandler(cg).execute), 2 retargeted and 14 typed, all right, 58 removed, 57 wrong (RegExp exec and test, Array fill, Set add) and 1 right one lost to the grammar's parse of new nodeSqlite!.DatabaseSync(). excalidraw 22 wrong removed (TextEncoder, TextDecoder, URLSearchParams, Array fill), 3 typed. pixijs 14 retargeted and 278 typed, all right, 7 wrong removed. pdf.js 5 added, 47 retargeted and 68 typed, all right, 76 wrong removed (Array keys and fill, typed arrays, Response). three.js 33 added, 404 retargeted (Capsule::set to Matrix4::set and Matrix3::set, NURBSCurve::copy to the class's own copy) and 429 typed, all right; 135 removed, 133 wrong (UINumber onChange, which three.js attaches at run time, TextDecoder, typed arrays, URL) and 2 right only by name (Loader methods inherited through a base imported from the three package). No default-imported class under another name lost an edge. Kernel parity holds on all six TS/JS repos and this repository, 0 diffs. Index CPU time unchanged or lower: codegraph 81.5 s to 79.5 s, three.js 72.0 s to 71.2 s (medians of 3, interleaved). Original PR #1945 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A TypeScript or JavaScript method called on an element of a class field, like
this.handlers[type].handle()orthis.items[0].save(), now links to the method of the type the field is declared to hold (Item[],Array<Item>,ReadonlyArray<Item>,Record<string, Handler>), or to nothing. On main it kept the bare method name, which exact-matched whichever project method shared it, often another class's:this.listeners[type].push(h)reached an unrelatedUndoStack.push.Root cause
A member chain rooted at
thiscollapses to the bare method name, so the resolver can read the owner off the enclosing class (keepsBareTsJsReceiver, and the kernel'skeeps_bare_receiver). A subscript in that chain leaves the resolver nothing to read the receiver from, so the bare name went to name matching.The fix (from the contributor)
this.<field>[k].<m>()is emitted asthis.<field>[].<m>, andthis['svc'].m()asthis.svc.m, the field itself (tsThisSubscriptReceiver/this_subscript_of). Any other subscript in athis-rooted chain (this.a.b[k].m(),this[k].m()), and one underwindow(window['App'].init()), now emits nothing, asarr[0].run()already did.matchTsThisFieldCallreads the field's declaration on the calling class or a base, as it does forthis.<field>.<m>(). The field reader also records the element type ofX[],Array<X>,ReadonlyArray<X>andRecord<K, X>, and the method resolves on that type or nowhere. There is no element type for a type parameter of the declaring class, a union or intersection with another type (| nulland| undefinedkeep it, also when the union goes on below), an array of arrays, or a non-plain element.tsClassTypeParameterreads a class's type-parameter list from its keyword to the list's own>, over several lines and through braces in constraints (<T extends { id: string }>).What we changed on top
escapeRegExpintsClassTypeParameter.tsClassTypeParameteralso accepts thetypekeyword. The PR's keyword match (class|interface) stopped seeing a generic type alias's parameters, and theT['p']path from fix(ts): a local annotated as one of a type's properties has that property's type #2640 passes type-alias owners to it. SoState<Item = unknown>['current']readItemas the project classItem, where main reads it as the type parameter. New case ints-array-annotated-receivers.test.ts: it fails on the PR as written (the alias half) and on main (the brace-constraint half), and passes here.Validation
Tests.
ts-this-field-element.test.ts(the contributor's) fails 10 of 11 on main with both the kernel and wasm (CODEGRAPH_KERNEL=0), each case linking to the decoy class, and passes here on both. The newts-array-annotated-receiverscase fails on the PR as written (the type-alias half) and on main (the brace-constraint half), and passes here. The contributor's extraction cases inexpression-receiver-callsandkernel-tsjs-paritypass.A/B against main 9c4794d (graph dumps by natural keys, kernel built per arm; the main commits after it touch only the viewer and tests):
src/vs/base+src/vs/editorEvery changed edge was read at its call site:
this.modelLineProjections[i].getViewLineCount()moves from the class's own method toIModelLineProjection(17 sites),this._lenses[i].dispose()fromCodeLensContribution::disposetoCodeLensWidget::dispose, andthis._chunks[0].slice()from a projectslicefunction toVSBuffer::slice. pixijs'sthis.buffers[0].update()moves fromRopeGeometry::updatetoBuffer::update. typeorm'sthis.connectedQueryRunners[0].release()now reaches the declaredQueryRunnerinterface rather than one driver's runner.this._lines[i].substring()→OffsetRange::substring×13,.focus()on DOM elements, and pixijs'sthis._gpuData[key]?.destroy()→ the class's owndestroy. The 15 right ones lost were right only by name, in shapes the PR leaves unresolved by design. Those are a getter (this.viewItems), an index-signature field, a cast ((this.filters[0] as MaskFilter)), a mixin object'sthis, aT[]type parameter, a union type alias, and deep chains likethis.replacements[0].range.getStartPosition()..set()→Capsule::set×41 and.copy()→NURBSCurve::copy×22, playcanvas SVGsetAttribute→StandardMaterial::setAttribute×27, pdf.jsclassList.add→Word64::add. About 58 were right by luck (Color::setHex,GraphNode::setLocalPosition,PDFThumbnailView::toggleSelected). This is the deliberate trade the PR describes, the same onearr[0].run()already makes.window['App'].init(): no such call in any of the 9 repos. The onewindow-rooted subscript chain changed was excalidraw'swindow.visualDebug.data[…].push(element), which had linked toBinaryHeap::push(wrong, removed).Kernel parity (
scripts/kernel-parity.mjs) holds on every TS/JS repo above and on this repository'ssrc/and__tests__/, with the same deferral counts as main.Cost: the index CPU time is within noise. Medians of 3 interleaved runs: pixijs 32.4 s → 33.3 s and three.js 64.7 s → 67.9 s, against a spread of 28.6–33.8 s and 60.1–72.6 s between runs of the same arm.
Suite (
CODEGRAPH_KERNEL_EXPECT=1 npx vitest run --maxWorkers=4 --minWorkers=1, on this commit): 756 files, 9,028 tests, 8,921 passed, 82 skipped. The 25 that failed were all in the four daemon suites (mcp-daemon,daemon-older-version,daemon-silent-holder,daemon-pid-reuse), because the run's environment hadCODEGRAPH_NO_DAEMON=1, which their spawned servers inherit. Run again with the daemon allowed, those four files pass: 58 passed, 1 skipped. Main then moved by one PHP-only commit (5c5bfd4,import-resolver.ts). After rebasing onto it, with the changed lines identical, this PR's test files, the PHP alias test and the resolution suites pass: 8 files, 401 tests.Lands #2766 by @inth3shadows.
🤖 Generated with Claude Code