Skip to content

fix(ts): an array method on a local annotated as an array of a class links to nothing, not the class's method - #2639

Merged
colbymchenry merged 8 commits into
mainfrom
claude/ts-array-annotation-receivers
Oct 18, 2026
Merged

colbymchenry merged 8 commits into
mainfrom
claude/ts-array-annotation-receivers

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

What

TypeScript/JavaScript receiver inference read a local or parameter annotated items: Item[] as an Item. The annotation pattern \b${r}\b\s*:\s*([A-Z][\w.$]*) in buildLocalReceiverTypePatterns stops its capture at [. So items.find(…) linked, at 0.9 instance-method, to Item.find whenever the class declares a method of that name.

On a fresh vscode index (main 53ad0d4) that is 16 wrong edges, each checked against its declaration:

  • distinctElements.find(…) on an ExplorerItem[] → ExplorerItem.find (×2);
  • ranges.map(…) / selection.map(…) on an OffsetRange[] → OffsetRange.map (×8);
  • sortedRanges.slice → OffsetRange.slice (×3);
  • flatList.find → OutlineEntry.find (×2);
  • diffToRemoves.reverse() on a DetailedLineRangeMapping[] → LineRangeMapping.reverse.

This turned up while measuring #2637.

Fix

The annotation pattern now captures a bracket suffix with the type (Item[], Item<K>[]), and matchMethodCall checks it:

  • On an array of a type, a method arrays have resolves to nothing, with no name guess. The methods are the new JS_ARRAY_METHODS: Array.prototype's plus what every object inherits.
  • Any other method on it can't be called on the array. It is called on an element that shares the array's name, so it keeps the element's type, as before. vscode needs this: d.forEach(d => d.dispose()) on an IDisposable[], extensions.map(extensions => extensions.syncInstalledExtensionsWithGallery(…)), and result.getStarts() after an earlier block's result: SequenceDiff[]. A first version that treated every call on x: T[] as the array's lost exactly these five right links on vscode.
  • An indexed type (groupIds: ExcalidrawElement["groupIds"], Parameters<typeof f>[0], Emitter<E>['event']) is still read as the type it indexes, as on main. Treating it as unknown changed 66 vscode edges both ways: typed links dropped, and new 0.65 name guesses, some into test fixtures. That needs its own decision, so it's left out here and flagged as a follow-up.

#2637's sync parking (localTypeBinding) now reads all this from the type inference gives, instead of re-reading the declaration line (declaresArrayOf and decl.line are gone):

Validation

Fresh-index A/B vs main

  • vscode (15,358 files): nodes and files identical. 16 edges removed, 0 added, all calls instance-method 0.9: exactly the array-method calls above. Their 16 refs stay failed instead. 132 failed refs change only their parking key: element and indexed-type calls now wait for their type's module (fix(sync): a member call on a local typed by an imported class links when the class's module changes #2637 parked 5,734 for comparison).
  • 10 JS/TS repos (folo, vben, milo-ui, ghostfolio, infrahub, bitwarden, medusa, react-native, refine-examples, excalidraw): nodes, edges and files identical, as none hits the pattern. Failed refs differ only in their parking key, for the same reason: excalidraw 31, ghostfolio 20, folo 16, medusa 4, refine-examples 2, bitwarden 1, react-native 1.

Tests

__tests__/ts-array-annotated-receivers.test.ts has 5 tests.

Failing on main 53ad0d4 (run in a detached worktree):

  • array methods on items: Item[], const list: Item[], all: Item [] and grid: Item[][] link to no Item method;
  • the same for repos: Repository<string>[].

Guards, which pass on main:

  • all.findLast gets no name guess onto an unrelated Stack.findLast;
  • d.forEach((d) => d.flush()) still links d.flush to Item.flush;
  • item: Item and repo: Repository<string> still link.

__tests__/sync-typed-local-member-retry.test.ts (#2637) gains two files:

  • cs.forEach((cs) => cs.get('k')) on an LRUCache<…>[]: cs.get waits under module:lru-cache and is relinked by all three sync scenarios, which fail on main; cs.forEach keeps forEach.
  • max: LRUCache<…>['max']: max.toFixed waits under module:lru-cache.

Mutations:

  • without the element fallback, the d.flush guard and the three sync scenarios fail;
  • parking an array method fails cs.forEach.

Full suite (Windows) on this branch: 592 files passed (6 skipped), 7,259 tests passed (82 skipped), 1 failed: daemon-pid-reuse.test.ts, an EBUSY on removing its temp dir after it kills the daemon. That is the Windows teardown flake; it passed twice when re-run alone.

The branch merges cleanly with #2638 (nested workspace globs).

🤖 Generated with Claude Code

colbymchenry and others added 8 commits October 18, 2026 15:34
…ray's, not the class's

`items: Item[]` was read as an `Item`: the annotation's capture stopped at
`[`, so `items.find(…)` linked to `Item.find` whenever the class declares
a method of that name. On vscode, `distinctElements.find(…)` on an
`ExplorerItem[]` reached `ExplorerItem.find` and `ranges.map(…)` on an
`OffsetRange[]` reached `OffsetRange.map`.

The annotation pattern now captures an array of a type whole
(`Item[]`, `Item<K>[]`), a type no project class is, so the call links to
nothing and isn't matched by its name either. With that, the sync
parking added in 2637 reads an array local from the type inference
gives it and no longer re-reads the declaration line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On vscode, reading `x: T[]` as the array lost five right links:
`d.forEach(d => d.dispose())` on an `IDisposable[]`,
`extensions.map(extensions => extensions.syncInstalledExtensionsWithGallery(…))`
and `result.getStarts()` after an earlier `result: SequenceDiff[]`. A
method arrays don't have can't be called on the array, so the receiver
there is an element that shares its name. Such a call now takes the
element's type as it did before, and only a method arrays have links to
nothing. The sync parking follows the same rule: an array's own method
keeps its member's tail, an element's call waits for the element type's
module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`groupIds: ExcalidrawElement["groupIds"]` was read as an
`ExcalidrawElement` too, the capture stopping at `[`, so
`groupIds.filter(…)` would reach the class's own `filter` if it had one.
The annotation pattern now captures any bracket suffix; an indexed type
says no type of its own, as an unannotated local doesn't, and the sync
parking doesn't wait for the class's module for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Typing `T["k"]` as unknown changed 66 vscode edges: it dropped typed
links (`onCommandExecuted.fire` through an `Emitter<E>['event']`) and
let name guesses link `Parameters<typeof f>[0]` locals at 0.65, some to
test fixtures. That is a change of its own; this one keeps to arrays. An
indexed type is read as the type it indexes, as on main, and its call
waits for that type's module, which resolution reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant