Skip to content

refactor: unify the two state node description methods and keep describe() cheap - #25528

Open
totally-not-ai[bot] wants to merge 2 commits into
mainfrom
refactor/unify-state-node-describe
Open

refactor: unify the two state node description methods and keep describe() cheap#25528
totally-not-ai[bot] wants to merge 2 commits into
mainfrom
refactor/unify-state-node-describe

Conversation

@totally-not-ai

@totally-not-ai totally-not-ai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

StateNode had two separate ways of describing a node in a message to a developer:

  • formatOwnerComponentToString() — used only in the IllegalStateException thrown when a component is moved between UIs
  • describe() — used in log messages

Two formats for the same job meant two places to maintain and two sets of quirks. This folds everything the former did into describe() and deletes it:

  • the attach location, in addition to the create location
  • identification of an element that has no component
  • the fallback to the component's own toString() when component tracking is disabled, which is the case in production mode — without a create/attach location it is the only way to tell instances of the same class apart

The error message for a component moved between UIs now also starts with the node id. That is redundant in an exception about one specific node, but it keeps a single description format everywhere.

Keeping it structural and cheap

describe() is called on log paths that run per RPC message, so the second commit tightens what it is allowed to do:

  • No outer HTML. formatOwnerComponentToString() used ownerElement.toString(), which serializes the markup of the entire subtree. An element that has no component is now identified by its id and class attributes instead.
  • No application content. A text node is reported as text node, without its text — application content does not belong in a server log.
  • Bounded output. Application-provided parts (attribute values, the component's toString()) are truncated at 200 characters.

describe() runs application code — an overridden getParent(), hashCode(), or toString() — so it still catches RuntimeException and reports the failure with the details gathered so far, rather than turning a log message into an error. That guarantee is what formatOwnerComponentToString() was originally written for, and it is now pinned by tests for both the getParent() and the toString() case.

No public or protected API changed: describe() already existed and only its Javadoc was updated; formatOwnerComponentToString() was private.

Test summary

# Status What the test verifies Why it matters
1 An element with no component is described by node id, tag, id and class, and the description contains neither a child's text nor <span The whole point of the change: describe() must not serialize the subtree's outer HTML on a per-message log path
2 A 1000-character attribute value ends in ... and the whole description stays under 500 chars Unbounded application input in a per-RPC log line is a cost and log-flooding problem
3 A text node is reported as text node, without its text, and without a threw marker Application content must not leak into a server log, and text nodes must not fall into the error path
4 With tracking enabled, the description contains both created at <file>:<line> and attached at <file>:<line>, and not the component's toString() The attach location is what this change folds in from the removed method; toString() would be noise when locations exist
5 With no tracking information available, the description contains the component class and its own toString() This is the production-mode path — without it a description cannot tell two instances of the same class apart
6 An overridden getParent() that throws yields a description naming the component class and the exception type instead of propagating describe() is called from log/error paths; a throwing description would replace the real problem
7 An overridden toString() that throws is handled the same way This is the exact case the deleted formatOwnerComponentToString() guarded, now reached through the toString() fallback in row 5
8 The cross-UI move error starts with Offending component: node id= and still contains the component class Confirms the error message switched to describe() without losing the information a developer needs
  • StateNodeTest.describe_element_nodeIdTagAndAttributesIncludedWithoutMarkup — 1
  • StateNodeTest.describe_longAttributeValue_truncated — 2
  • StateNodeTest.describe_textNode_reportedAsTextNodeWithoutContent — 3
  • StateNodeDescribeTest.describe_trackingEnabled_createAndAttachLocationIncluded — 4
  • StateNodeTest.describe_componentWithoutTrackingInformation_classAndToStringIncluded — 5
  • StateNodeTest.describe_getParentThrows_failureDescribedWithDetailsSoFar — 6
  • StateNodeTest.describe_toStringThrows_failureDescribedWithDetailsSoFar — 7
  • ComponentTest.cannotMoveComponentsToOtherUI — 8

StateNodeDescribeTest is a new class deliberately placed in com.vaadin.flow rather than com.vaadin.flow.internal, because ComponentTracker skips frames from the latter when looking for the relevant stack frame — a test there could never observe a create location.

Deliberately left untested: the used in '<route class>' branch, which this change only moves into the new private describeElement helper without altering its behaviour, and the truncation of a very long component toString(), which shares the same truncate helper already pinned by row 2. The node id= only case for a node without element features is still covered by the pre-existing StateNodeTest.describe_nodeWithoutElementFeatures_onlyNodeIdIncluded, unchanged on this branch.

StateNode had two methods for describing a node in a message to a
developer: formatOwnerComponentToString, used in the error thrown when a
component is moved between UIs, and describe, used in log messages.

Fold everything the former had into describe: the attach location, the
markup of an element that has no component, the text of a text node, and
the fallback to the component's own toString when component tracking is
disabled, which is the case in production mode. Application-provided
parts are truncated since the outer HTML of an element covers its whole
subtree.

The error message for a component moved between UIs now also starts with
the node id, which is redundant there but keeps a single description
format.
describe() is called on log paths that run per RPC message, so it must
not serialize the outer HTML of the whole subtree, and it should not
write application content such as the text of a text node into a server
log. Identify an element that has no component by its id and class
attributes instead, and report a text node without its text.

Also pin that a component whose toString() throws does not turn a
description into an error, which is the situation the removed
formatOwnerComponentToString was originally written to guard against.
@github-actions github-actions Bot added the +0.0.1 label Sep 8, 2026
@sonarqubecloud

sonarqubecloud Bot commented Sep 8, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Test Results

 1 437 files  +1   1 521 suites  +1   1h 32m 37s ⏱️ - 2m 37s
11 912 tests +4  11 844 ✅ +4  68 💤 ±0  0 ❌ ±0 
12 230 runs  +4  12 162 ✅ +4  68 💤 ±0  0 ❌ ±0 

Results for commit f59447a. ± Comparison against base commit b07b661.

@Legioth
Legioth self-requested a review September 9, 2026 07:56
@Legioth Legioth self-assigned this Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant