Fix crash when a call signature's type parameter cannot be reused - #4842
Fix crash when a call signature's type parameter cannot be reused#4842Nicolaev Eduard (nikeedw) wants to merge 1 commit into
Conversation
The PseudoTypeKindSingleCallSignature branch of pseudoTypeToNode appended the result of reuseNode into the type parameter list without checking it. reuseNode returns nil whenever the recovery boundary in tryReuseExistingNodeHelper fails, and that nil survived into the NodeList, where the printer dereferenced it in NodeList.HasTrailingComma while deciding whether to write a trailing comma. Serialize the type parameter from the checker instead, mirroring the fallback reuseTypeNode already performs for type nodes. Fixes microsoft#4748 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Note
This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.
|
Thanks for running CI — everything came back green, including the Windows and race-mode legs I could not run locally. On the CLA: I am not going to sign it, so please do not hold this open waiting on that. It is nothing about the agreement itself — I simply do not want to enter into it for something that started as a bug report against my own codebase. Practically that means this PR should not be merged as it stands. I would suggest treating it as a written-up proposal rather than a contribution. The diagnosis, the minimal repro and the reasoning for why the nil survives into the printer are all in #4748 and in the description above, and the regression test is small enough to re-derive from scratch. One caveat I would rather state myself than have someone find later: this may only be treating the symptom. The change makes the type-parameter path handle a nil from So it is entirely your call — adopt the approach, pick a different one, or close this. Either way the crash in #4748 is real and reproduces from the two files above. |
|
@microsoft-github-policy-service agree |
|
CLA signed above (I reconsidered — my mistake, apologies). Could not reopen this PR myself; superseded by #4846, same branch unchanged. Context in #4748 (comment) |
Fixes #4748.
The crash
The
PseudoTypeKindSingleCallSignaturebranch ofpseudoTypeToNodeappends the result ofreuseNodeinto a call signature's type parameter list without checking it:reuseNodereturns nil whenever the recovery boundary intryReuseExistingNodeHelperfails. The nil is stored in theNodeListand survives all the way to the printer, which dereferences the list's last element while deciding whether to write a trailing comma:Every other caller of
reuseNodecopes with the nil —reuseTypeNodein particular reports an inference fallback and re-serializes the node from the checker. This one call site does not.When it happens
Four things have to line up:
SingleCallSignatureand tries to reuse the original type parameter nodes.unique symbolfrom the test case below and you get a perfectly good<K extends "count" | "name">. It only returns nil when the rewrite itself reaches something unnameable..d.ts.The original report came from a mobx-state-tree codebase, where all four fall out of ordinary usage: a generic
<Key extends keyof typeof self>setter declared inside.actions(self => ({ ... })), MST'sunique symbolbrands, andtypes.composepulling models in across files.Why it only reproduces on the incremental path
For a
noEmitordeclaration: falseproject the declaration printer runs in exactly one place — computing d.ts shape signatures. InaffectedFilesHandler.updateShapeSignature:A cold run takes the
useFileVersionAsSignature: truepath and hashes the file text, so nothing is ever printed. A warm run computes the real d.ts signature for each affected file, prints the malformed tree, and crashes.A direct
--declaration --emitDeclarationOnlybuild hides the bug from the other side: files in this shape always carry declaration diagnostics (TS2527 / TS4023), and their emit is skipped. So the malformed node list is built on every run; only the signature path ever prints it.The crash is not limited to
noEmit. Onmainthe repro below also crashes with a plain JS-emittingincrementalbuild and withcomposite: true.The fix
Fall back to serializing the type parameter from the checker, mirroring what
reuseTypeNodealready does for type nodes.typeParameterToDeclarationalways returns a node, so the list can no longer contain a nil.Test
internal/execute/tsctestsgains a two-file incremental scenario underTestTscDeclarationEmit. Without the fix it panics; with it the baseline shows the signature being computed correctly:Both declaration diagnostics (TS2527 and TS4023) are still reported, so the only behavioural change is that a crash becomes a correctly serialized type parameter.
Verification
npx hereby build,npx hereby lint(0 issues),npx hereby format(no changes),npx hereby test— all pass.internal/testrunneragainst the TypeScript submodule, produces no changed baselines other than the new one added here.AI assistance disclosure
Per CONTRIBUTING.md: this patch was authored with the help of Claude Code. This is not a queue-driven contribution — #4748 is my own bug report against my own codebase, and I drove this investigation myself. I have read and understand the change, and I will be the one handling review feedback.