Repository navigation
Sidebar: hop selectedTabId row delivery to RunLoop.main + guard synchronous row onReceive #7341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -139,6 +139,56 @@ | |
| "inside a row is the #5323 feedback shape)"), | ||
| ) | ||
|
|
||
| # `.onReceive(` in a row view. Combine delivery into a sidebar row must hop | ||
| # through `.receive(on:)`: the TabManager bridges are `CurrentValueSubject`s | ||
| # that replay the current value SYNCHRONOUSLY at subscribe time -- and a lazy | ||
| # row subscribes while the LazyVStack realizes it, inside an in-flight SwiftUI | ||
| # layout transaction -- and they emit during `willSet`, so a selection change | ||
| # made mid-update delivers mid-update. Either path lets the `onReceive` action | ||
| # write row `@State` inside the transaction being laid out: the same | ||
| # write-during-layout family as the #2586/#6556 livelocks (a row shipped this | ||
| # exact shape in stable v0.64.17 via `selectedTabIdPublisher`, observed | ||
| # livelocked in the wild on 2026-07-02/03). `NotificationCenter` publishers | ||
| # deliver synchronously on the posting thread and need the same hop. | ||
| ONRECEIVE_CALL = re.compile(r"\.onReceive\s*\(") | ||
|
|
||
| ROW_SYNC_ONRECEIVE_MESSAGE = ( | ||
| ".onReceive( without .receive(on:) in a row (synchronous publisher " | ||
| "delivery -- a CurrentValueSubject replays on subscribe while the " | ||
| "LazyVStack is realizing the row and emits during willSet -- writes row " | ||
| "@State inside the in-flight layout transaction, the #2586/#6556 " | ||
| "write-during-layout livelock family; route row subscriptions through " | ||
| ".receive(on: RunLoop.main))" | ||
| ) | ||
|
|
||
|
|
||
| def find_sync_onreceive(region): | ||
| """Return True if ``region`` (neutralized Swift) contains an | ||
| ``.onReceive(`` whose publisher argument lacks a ``.receive(on:`` hop. | ||
|
|
||
| The publisher expression is the balanced-parenthesis argument list of the | ||
| ``.onReceive(`` call; the action trailing closure sits outside it, so a | ||
| ``.receive(on:)`` inside the action cannot mask a synchronous publisher. | ||
| """ | ||
| for match in ONRECEIVE_CALL.finditer(region): | ||
| i = match.end() - 1 # at the opening '(' of the argument list | ||
| depth = 0 | ||
| start = i | ||
| n = len(region) | ||
| while i < n: | ||
| ch = region[i] | ||
| if ch == "(": | ||
| depth += 1 | ||
| elif ch == ")": | ||
| depth -= 1 | ||
| if depth == 0: | ||
| break | ||
| i += 1 | ||
| publisher_expr = re.sub(r"\s+", "", region[start:i + 1]) | ||
| if ".receive(on:" not in publisher_expr: | ||
| return True | ||
| return False | ||
|
|
||
| # Lazy-fill primitives the #6188 fix depends on. Each must remain present in the | ||
| # named function (after comments/strings are stripped). | ||
| REQUIRED_PRIMITIVES = ( | ||
|
|
@@ -476,6 +526,11 @@ def check_source( | |
| "row-wrapper file contains forbidden per-row geometry " | ||
| "feedback: {0}".format(description) | ||
| ) | ||
| if find_sync_onreceive(neutralized): | ||
| violations.append( | ||
| "row-wrapper file contains forbidden synchronous delivery: " | ||
| "{0}".format(ROW_SYNC_ONRECEIVE_MESSAGE) | ||
| ) | ||
|
Comment on lines
+529
to
+533
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Test cases (o) and (p) exercise Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| for name in sorted(custom_layout_names): | ||
| if re.search(r"\b" + re.escape(name) + r"\b", neutralized): | ||
| violations.append( | ||
|
|
@@ -501,6 +556,12 @@ def check_source( | |
| "{0} contains forbidden per-row geometry feedback: " | ||
| "{1}".format(type_name, description) | ||
| ) | ||
| if find_sync_onreceive(body): | ||
| violations.append( | ||
| "{0} contains forbidden synchronous delivery: {1}".format( | ||
| type_name, ROW_SYNC_ONRECEIVE_MESSAGE | ||
| ) | ||
| ) | ||
| for name in sorted(custom_layout_names): | ||
| if re.search(r"\b" + re.escape(name) + r"\b", body): | ||
| violations.append( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,13 @@ | |
| wild on 2026-07-02. | ||
| (l) Per-row `.anchorPreference` (the #5323 virtualization defeat) fails. | ||
| (m) A required row type missing from its file fails loudly (no silent skip). | ||
| (o) An `.onReceive(` in a row whose publisher lacks a `.receive(on:)` hop | ||
| fails — a CurrentValueSubject bridge replays synchronously while the | ||
| LazyVStack realizes the row (and emits during willSet), so the action | ||
| writes row @State inside the in-flight layout transaction. This exact | ||
| shape shipped in stable v0.64.17 via `selectedTabIdPublisher`. | ||
| (p) The same subscription routed through `.receive(on: RunLoop.main)` | ||
| passes, including when `.receive(on:)` spans multiple lines. | ||
|
Comment on lines
+27
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Duplicate case letter "(o)" reused for two different tests. The new cases at lines 323-343 (and their docstring entries at lines 27-33) reuse label "(o)", which is already used later in the file (line 392: Consider relettering the new sync/deferred onReceive cases (e.g., to the next unused letters) to avoid collision with the existing wrapper-scan case. Also applies to: 323-343 🤖 Prompt for AI Agents |
||
| """ | ||
|
|
||
| import importlib.util | ||
|
|
@@ -313,6 +320,49 @@ def row_fixture(row_body): | |
| False, "per-row .anchorPreference (#5323 shape) fails", | ||
| ) else 1 | ||
|
|
||
| # (o) An .onReceive( whose publisher chain has no .receive(on:) hop: | ||
| # the CurrentValueSubject bridge replays synchronously during lazy row | ||
| # realization and emits during willSet, so the action's @State write | ||
| # lands inside the in-flight layout transaction (the #2586/#6556 | ||
| # family). This shape shipped in stable v0.64.17 and livelocked in the | ||
| # wild on 2026-07-02/03. A .receive(on:) inside the ACTION closure | ||
| # (outside the publisher argument) must not mask the violation. | ||
| sync_onreceive_row = row_fixture( | ||
| " HStack { Text(tab.title) }\n" | ||
| " .onReceive(\n" | ||
| " tabManager.selectedTabIdPublisher\n" | ||
| " .map { $0 == tab.id }\n" | ||
| " .removeDuplicates()\n" | ||
| " ) { isSelected in\n" | ||
| " observedIsActive = isSelected\n" | ||
| " }" | ||
| ) | ||
| failures += 0 if expect( | ||
| run_guard(write_fixture(workdir, "SyncOnReceiveRow.swift", sync_onreceive_row)), | ||
| False, "row .onReceive without .receive(on:) fails", | ||
| ) else 1 | ||
|
Comment on lines
+323
to
+343
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The comment asserts that "A Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Comment on lines
+323
to
+343
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The inline comment says "A Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
|
|
||
| # (p) The same subscription with a .receive(on: RunLoop.main) hop in | ||
| # the publisher chain passes -- also with the hop split across lines, | ||
| # since the real call sites chain one operator per line. | ||
|
Comment on lines
+323
to
+347
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The case (o) comment states "A Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| deferred_onreceive_row = row_fixture( | ||
| " HStack { Text(tab.title) }\n" | ||
| " .onReceive(\n" | ||
| " tabManager.selectedTabIdPublisher\n" | ||
| " .map { $0 == tab.id }\n" | ||
| " .removeDuplicates()\n" | ||
| " .receive(\n" | ||
| " on: RunLoop.main\n" | ||
| " )\n" | ||
| " ) { isSelected in\n" | ||
| " observedIsActive = isSelected\n" | ||
| " }" | ||
| ) | ||
| failures += 0 if expect( | ||
| run_guard(write_fixture(workdir, "DeferredOnReceiveRow.swift", deferred_onreceive_row)), | ||
| True, "row .onReceive with .receive(on:) passes", | ||
| ) else 1 | ||
|
Comment on lines
+323
to
+364
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the file the execution sequence is Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
|
|
||
| # (n) --file on a row-view source (no container functions) must not | ||
| # emit false "could not locate func" violations; row scanning still | ||
| # applies. (Greptile P2 on #7221.) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
regioncontains an.onReceive(whose opening(is never balanced — for example because a macro or string interpolation produced a lone(that the neutralizer didn't collapse — thewhile i < nloop exits withdepth > 0(without hitting thebreak). At that pointi == n, sopublisher_expr = re.sub(r"\s+", "", region[start:n+1])is effectively the rest of the file from the(onward. That's unlikely to contain.receive(on:, so the function would incorrectly returnTrueand emit a false positive on otherwise clean code. A small guard likeif i >= n: continue(skipping unbalanced matches) before thepublisher_exprextraction would make the failure mode more predictable, though this scenario would only arise from malformed Swift that the compiler would also reject.