JavaScript: a non-optional list slot reads as empty when a peer sends none - #8798
Merged
knutwannheden merged 1 commit intoSep 8, 2026
Merged
knutwannheden merged 1 commit into
knutwannheden merged 1 commit into
Conversation
knutwannheden
marked this pull request as draft
September 7, 2026 12:59
knutwannheden
force-pushed
the
js-printer-crashes-on-absent-propertyassignment.modifiers
branch
from
September 7, 2026 13:25
16cc277 to
a144dbd
Compare
PropertyAssignment.modifiers prints as empty
knutwannheden
force-pushed
the
js-printer-crashes-on-absent-propertyassignment.modifiers
branch
from
September 8, 2026 00:54
a144dbd to
4f4d1dc
Compare
… none `JS.PropertyAssignment.modifiers` was added in #8720, so an LST serialized before that deserializes with the field null. `JavaScriptSender` sends nothing for a null list, and the TypeScript receiver was the one call site taking the bare `receiveList` for a slot the model declares non-optional, so `modifiers` stayed undefined. `JavaScriptPrinter` then threw `TypeError: propertyAssignment.modifiers is not iterable`, failing the print of every source file containing an object literal, and `NormalizeWhitespaceVisitor` threw from `mapAsync`. `receiveListDefined` now reads an absent list as empty rather than asserting non-null to the type checker alone, and `modifiers` uses it, as the other 19 non-optional list slots do. That supersedes the consumer-side guard added in #8799, reverted here along with its test: the state it tolerated can no longer reach a consumer.
knutwannheden
force-pushed
the
js-printer-crashes-on-absent-propertyassignment.modifiers
branch
from
September 8, 2026 01:16
4f4d1dc to
2793307
Compare
knutwannheden
marked this pull request as ready for review
September 8, 2026 01:35
knutwannheden
deleted the
js-printer-crashes-on-absent-propertyassignment.modifiers
branch
September 8, 2026 01:38
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.
Printing a JavaScript or TypeScript source file whose LST predates #8720 fails with a
TypeErrorfrom the printer. Observed while running recipes over a corpus of open-source repositories: 879 failed prints across 293 files in 7 repositories, reproduced on two consecutive days and on two different bundled@openrewrite/rewriteversions (8.91.3 and 8.91.5).Per repository: Netflix/pollyjs 411, spring-guides/tut-spring-security-and-angular-js 339, Netflix/conductor 69, spring-projects/spring-security 33, Netflix/metaflow 21, spring-projects/spring-restdocs 3, spring-projects/spring-session 3. The failure does not mark the source file as errored, so it is easy to miss. #8799 hit the same absent slot from the auto-formatter.
Mechanism
JS.PropertyAssignment.modifierswas added in #8720, so an LST serialized before it deserializes into the current Java type withmodifiers == null.RpcSendQueue.sendListsends nothing for a null list against a null before-value, and the receiver leaves the slot at itsundefineddefault. A freshly parsed tree is unaffected — the TypeScript parser populates the slot, so both peers hold[].Fix
The receiver already has a helper for this. Non-optional list slots take
receiveListDefined, optional ones takereceiveList— 19 slots against 1 injavascript/rpc.ts, and injava/rpc.tsthe bare calls end in|| []. #8720 added the one slot that reads a non-optional list with the un-coerced call (JSX.Tag.children, the only other bare call, is genuinely optional: its self-closing union branch declareschildren?: undefined).But
receiveListDefinedwas(await this.receiveList(...))!— a non-null assertion the compiler erases, so it promised the type checker something it never enforced at runtime. It now coerces with?? [], which makes its name true and closes the same gap for all 53 of its call sites.modifiersthen uses it.This deliberately leaves
JS.PropertyAssignmentalone. A null list is already harmless on the Java side —ListUtils.map(null, …)returns null and the wither is a no-op — so the model needs no nullable-slot accommodation.Superseding #8799
#8799 guarded one consumer,
JavaScriptVisitor.visitPropertyAssignment, with?? []. That did not cover this crash, becauseJavaScriptPrinteroverrides that method —print.tshas 15 morefor (… of x.modifiers)loops, and every other one is safe only because its slot is received throughreceiveListDefined. Guarding the receiver instead is what keeps all 16 safe, so the guard is reverted here andvisitor.tsis back to treating all 10 of itsmodifiersslots alike.Its test goes with it. It cast past the model's own
readonly modifiers: J.Modifier[]to build a tree the type forbids, then asserted a consumer tolerated it — so it passed onmainwhile the crash above still shipped. The replacement asserts the state cannot reach a consumer in the first place.Tests
absent-list-slot.test.tsround-trips a parsed compilation unit whosemodifiersslot is absent throughRpcSendQueue/RpcReceiveQueue, then asserts the received tree holds[]and prints back to its source. Both halves are load-bearing under mutation: restoring the!inreceiveListDefinedfails it, and so does puttingmodifiersback on the barereceiveList.