Skip to content

finding(react): the properties hoist enumerates a degenerate properties into indexed node keys — properties: "text" still reaches the element as 0…8 #6760

Description

@os-sales

Measured while implementing objectui#6752, outside its fence (that card is scoped to the props bag and is explicitly forbidden from changing hoist behaviour), so filed separately. Observation-class in the same sense objectui#6752 was: nothing is known to depend on it today.

The fact, measured on b76ca6764

objectui#6752 fixed the props half of "a degenerate value must not have its shape reinterpreted by an object spread". The properties half is still open, and the site is NOT the evaluation memo — it is the hoist.

Rendered through the real SchemaRenderer with a probe that records the React props the element receives:

{ type: "test:deg", properties: "not-a-bag" }
-> React props named 0, 1, 2, 3, 4, 5, 6, 7, 8 (one per character)
{ type: "test:deg", properties: ["x", "y"] }
-> React props named 0, 1

Why the existing guard does not stop it

The evaluation memo guards properties with a real-object test, and its comment used to explain that guard by saying the value FEEDS the hoist. That explanation does not survive measurement. Ablating the guard to bare truthiness and re-rendering the same nodes leaves the indexed keys COMPLETELY UNCHANGED, because the hoist does its own enumeration:

if (newSchema.properties) {
const props = newSchema.properties;
for (const [k, v] of Object.entries(props)) { ... newSchema[k] = v; }
}

Object.entries("not-a-bag") yields the character indices whatever the memo did. The only thing the memo guard moves is whether schema.properties still holds the value the author wrote. That measurement is what selected the arm on objectui#6752, and the shape is pinned there in packages/react/src/__tests__/SchemaRenderer.degeneratePropsBag.test.tsx (the leg named "the properties channel is untouched — this card changed one bag"), so it is recorded as known and accepted rather than overlooked.

Blast radius, honestly bounded

Same denominator as objectui#6752: the objectui#6708 census walked every JSON document, every json doc fence and every TypeScript object literal and found zero authored nodes with a degenerate config bag. Latent shape, not a live failure. What makes it worth recording is that the two halves of one hazard are now fixed on one side and open on the other, and the open side is the one the spec spelling uses.

Arms, if anyone takes it

  • guard the hoist the way the memo is guarded, so a degenerate properties contributes no node keys;
  • or decide the hoist may enumerate whatever it is handed, and say so where the hoist is written.

Not picked here — objectui#6752 scoped hoist behaviour out explicitly.

Related: objectui#6752 (the props half, fixed), objectui#5123 (the two-bag precedence ruling), objectui#6708 (the census).

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatchedpriority:p2

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions