Skip to content

[web] Do not stop pruning a detached semantics subtree at a reparented node - #195842

Merged
auto-submit[bot] merged 4 commits into
flutter:masterfrom
hannah-hyj:fix-194420-web-semantics-skip-subtree
Nov 5, 2026
Merged

auto-submit[bot] merged 4 commits into
flutter:masterfrom
hannah-hyj:fix-194420-web-semantics-skip-subtree

Conversation

@hannah-hyj

Copy link
Copy Markdown
Member

Fixes #194420

When a node is detached from its parent, EngineSemanticsOwner._finalizeTree walks the detached subtree with _visitDepthFirstInTraversalOrderCanSkipSubtree to remove every descendant that was not reparented elsewhere. The callback returns false for a reparented node so that its subtree is skipped (it moved along with the node).

The visitor, however, recursed into its children with _visitDepthFirstInTraversalOrder, whose contract is "stop the whole traversal when the callback returns false". So for any node two or more levels below the detachment root, a reparented child ended the walk of its parent's child list: later siblings that were not reparented were never visited, never disposed, and stayed in _semanticsTree. In debug mode the post-update consistency check then throws Bad state: Semantics node map was inconsistent after update on that update and on every update after it; in release mode the orphaned nodes (with their DOM elements and roles) leak and stay registered in the map under their old ids.

This is what happens when a GlobalKey'd widget is moved to a new parent while its old ancestors and an old later sibling are removed in the same frame: 0 -> 1 -> 2 -> [3, 4] becomes 0 -> 5 -> 6 -> 3, and #4 was left behind.

Recurse with the subtree-skipping visitor instead, so that a false return only skips that node's descendants and the walk continues with its siblings. The doc comment now spells out the difference between the two visitors.

The regression test replays the issue's tree shape; before the fix it fails with the node map still containing the orphaned #4.

Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.

List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.

If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

…d node

Fixes flutter#194420

When a node is detached from its parent, `EngineSemanticsOwner._finalizeTree`
walks the detached subtree with `_visitDepthFirstInTraversalOrderCanSkipSubtree`
to remove every descendant that was not reparented elsewhere. The callback
returns false for a reparented node so that its subtree is skipped (it moved
along with the node).

The visitor, however, recursed into its children with
`_visitDepthFirstInTraversalOrder`, whose contract is "stop the whole
traversal when the callback returns false". So for any node two or more levels
below the detachment root, a reparented child ended the walk of its parent's
child list: later siblings that were *not* reparented were never visited,
never disposed, and stayed in `_semanticsTree`. In debug mode the post-update
consistency check then throws `Bad state: Semantics node map was inconsistent
after update` on that update and on every update after it; in release mode the
orphaned nodes (with their DOM elements and roles) leak and stay registered in
the map under their old ids.

This is what happens when a `GlobalKey`'d widget is moved to a new parent
while its old ancestors and an old later sibling are removed in the same
frame: `0 -> 1 -> 2 -> [3, 4]` becomes `0 -> 5 -> 6 -> 3`, and #4 was left
behind.

Recurse with the subtree-skipping visitor instead, so that a false return
only skips that node's descendants and the walk continues with its siblings.
The doc comment now spells out the difference between the two visitors.

The regression test replays the issue's tree shape; before the fix it fails
with the node map still containing the orphaned #4.
@hannah-hyj
hannah-hyj requested a review from flutter-zl November 2, 2026 22:18
@hannah-hyj
hannah-hyj marked this pull request as ready for review November 2, 2026 22:19

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request fixes an issue in semantics tree traversal where skipping a subtree incorrectly stopped the traversal of remaining siblings for nodes deep in a detached subtree. This is resolved by ensuring _visitDepthFirstInTraversalOrderCanSkipSubtree recurses using itself rather than _visitDepthFirstInTraversalOrder. A regression test has been added to verify this behavior. Feedback is provided to correct a private method reference in the documentation to ensure Dartdoc links resolve properly.

@Piinks Piinks added a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) platform-web Web applications specifically team-web Owned by Web platform team labels Nov 2, 2026
…cs.dart

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@github-actions github-actions Bot added the engine flutter/engine related. See also e: labels. label Nov 2, 2026

@flutter-zl flutter-zl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Verified the bug and the fix with a before/after demo:
Before: https://flutter-demo-92-before.web.app
After: https://flutter-demo-92-after.web.app
Click Reparent, then Bump. Before throws "Semantics node map was inconsistent after update" on every update; after stays clean.

@flutter-zl flutter-zl added CICD Run CI/CD autosubmit Merge PR when tree becomes green via auto submit App labels Nov 3, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Nov 3, 2026
@auto-submit

auto-submit Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/195842, because - The status or check suite Dashboard Checks has failed. Please fix the issues identified (or deflake) before re-applying this label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) CICD Run CI/CD engine flutter/engine related. See also e: labels. platform-web Web applications specifically team-web Owned by Web platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[web] Semantics node map inconsistent after GlobalKey reparent that removes a later sibling

3 participants