Repository navigation
fix(ts): a local annotated as one of a type's properties has that property's type - #2640
Merged
Merged
Conversation
…nks to nothing `groupIds: ExcalidrawElement["groupIds"]` was read as the type it indexes, so `groupIds.filter(…)` would reach `ExcalidrawElement.filter` had the class one, and on vscode `onCommandExecuted.fire(…)` reached an `Emitter.fire` through an `Emitter<E>['event']`. An indexed type is one the annotation names without saying: like an outside type's, it links to nothing and is not matched by the method's name, which for a `Parameters<typeof f>[0]` local would only guess. The sync parking doesn't wait for the indexed type's module either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…perty's type `editor: IChatWidget['inputEditor']` now types `editor` as the `ICodeEditor` the property is declared with, read off the indexed class or interface's own lines as `this.field` calls already are, and an array property follows the array rules. One whose type isn't written in the project (`Parameters<typeof f>[0]`, a getter) names no type to call or to guess by the method's name. The sync parking is main's again: an indexed type's call waits for the indexed type's module, whose declaration resolution reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`visibility: ISessionChatPillVisibilityService['pullRequests']` is
declared as `ISessionChatPillFilter & { isVisible(…) }`; its head names
no `isVisible`, and the call then went on to a name guess that picked
the service class's own `isVisible`. Like a `this.<field>` call, a call
on an indexed property links to its type's method or to nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The indexed type is the one the call's file imports, and the property's type the class the declaring file imports or declares, passed to resolveMethodOnType like a typed local's import. vscode's `ComponentFixtureContext['disposableStore']` reached copilot's vendored `DisposableStore`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An indexed type whose property's type isn't written in the indexed type,
a getter's or a `typeof`, or whose key isn't a name, is read as the type
it indexes, as it was before. The nearest line declaring a name may be a
member of another variable's type: vscode's `let mockPty: {
onCommandExecuted: Emitter<E>['event'] }` sits above calls on the local
`onCommandExecuted`, an `Emitter<E>`, whose `fire` they reach.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as before Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The field reader takes `Emitter<T>`'s `fire(event: T)` for its `event`, so `Emitter<E>['event']` read as `T` and decided alone, and vscode's `onCommandExecuted.fire(...)` lost its link to `Emitter.fire`. A type parameter of the indexed class names no type, so the annotation reads as the type it indexes, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
What
This is the follow-up #2639 left open. TypeScript/JavaScript receiver inference reads a local or parameter annotated as one of a type's properties (
editor: IChatWidget['inputEditor']) as the type it indexes (IChatWidget). The call then links to that type's method of the same name. If the type has no such method, the call falls through to a guess by the method's name.On a fresh vscode index (main 10e316d), each case checked against its declaration:
visibility.isVisible(…)withvisibility: ISessionChatPillVisibilityService['pullRequests']links to the service's ownisVisible.raw.disconnect()throughCopilotSessionWrapper['session']links to the wrapper'sdisconnect.chatModel.onDidChange(…)withchatModel: IInlineChatSession['chatModel']reachesHiddenRangeModel.onDidChange(a folding model), by the method's name alone.editor.executeEdits(…)andeditor.getValue()witheditor: IChatWidget['inputEditor'](×11) reachICodeEditoronly by a 0.65 name guess.Decision
The options I measured:
editor.*guesses that happen to be right.(d) reads the property with
tsFieldDeclaration, the same readerthis.<field>.<method>()calls use (#1496). It treats the result the waymatchTsThisFieldCalldoes: the call links to that type's method, or to nothing. It is not matched by the method's name. A first version that fell through to the name strategies gave a wrong 0.65 guess for a property declared as an intersection.Fix
In
matchMethodCall's bracket-suffix block (#2639), an indexed typeT['p']/T["p"]goes throughtsIndexedPropertyType:Tis the class or interface the call's file imports, or else declares (tsImportedTypeFile, fix(ts): a typed call links to the method of the class the file imports #2641). Its propertyp's declared type is read withtsFieldDeclaration.pimports, or else declares, passed toresolveMethodOnTypeas fix(ts): a typed call links to the method of the class the file imports #2641's hint. This takes vscode'sdisposables: ComponentFixtureContext['disposableStore']to vscode'sDisposableStore, where index order took copilot's vendored copy.groupIds: string[]) follows fix(ts): an array method on a local annotated as an array of a class links to nothing, not the class's method #2639's array rules.tsFieldDeclarationnow reports a[]after the type.typeof, a type parameter of the class, or a key that isn't a name (Parameters<typeof f>[0]).Why the fallback matters: vscode's terminal test has
let onCommandExecuted: Emitter<E>. Inference reads the nearer type-literal memberlet mockPty: { onCommandExecuted: Emitter<E>['event'] }instead.eventis a getter ofEmitter, and the field reader takes theevent: Toffire(event: T)for it. Reading that as the class's type parameterT, and so deciding alone, cost 18onCommandExecuted.fire/onCommandFinished.firelinks that #2641 made correct.I also tried skipping such type-literal members as declarations. On vscode that retargeted 888 calls and added and removed about 140 more, many of them wrong. Member lines are often what types a destructured local (
const { service } = createService(…)), and the backward scan isn't scoped, so skipping them reaches unrelated declarations further up a test file. It's reverted.Sync parking is unchanged from main. A failed indexed-type call still waits under the module of the type it indexes, where the property and its type are declared, so a change there re-opens it. One case isn't covered: a method added later to the property's type, in another module, doesn't re-open the call.
Validation
Fresh-index A/B vs main 10e316d
vscode (15,358 files): nodes and files are identical, and no
name_tailchanges. 14 edges removed and 12 added, allcalls instance-method:editor.*calls go from 0.65 name guesses to 0.9 typed links, with the sameICodeEditortargets.visibility.isVisible,raw.disconnectandchatModel.onDidChange, the wrong links above, are removed.disposables.addnow links to vscode'svs/base/common/lifecycleDisposableStore.onCommand*.firelinks are unchanged.10 JS/TS repos (folo, vben, milo-ui, ghostfolio, infrahub, bitwarden, medusa, react-native, refine-examples, excalidraw): byte-identical.
Tests
__tests__/ts-array-annotated-receivers.test.ts's indexed-type describe has 8 tests.Failing on main (run in a detached worktree):
ed: Widget['inputEditor']linksed.executeEditstoCodeEditor.executeEdits, with aDiffEditor.executeEditsdecoy;visibility: VisibilityService['pullRequests']links toVisibility.isVisible, not the service's ownisVisible;Filter & { isVisible(): boolean }gets no edge, neither the service's method nor a guess;groupIds: Element['groupIds']on astring[]property gets noElement.filteredge;CodeEditors each reach their own;Widgets, imported (once asimport type) by two callers, each read their own property and reach theCodeEditordeclared beside them.Guards, which pass on main:
logger: Parameters<typeof make>[0]gets noLogger.infoname guess;onDone: Emitter<number>['event'], a getter's, withfire(event: T), still reachesEmitter.fire. This one fails without the fallback.Mutations:
Parametersguard;Full suite (Windows), run while another heavy program was loading the machine: 573 files passed. 50 tests in 22 files failed, all 5 s timeouts except:
daemon-pid-reuseEBUSYteardown;mcp-writer-lockrace;EEXISTon a symlinkroot-identity.test.tscreates.All 22 files pass when rerun serially (1,113 tests).
🤖 Generated with Claude Code