Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(react): mapDensity still coerces a non-string rowHeight to a density, where the core twin abstains #4459

Closed

Description

@yinlianghui

Found while implementing #4442 (PR #4457, the hasOwnProperty guard on mapDensity). Not fixed there: #4442's ruling scoped the work to the prototype-chain index and the prototype-member spellings, and this is a different defect class — coercion, not an unguarded prototype read. Same relationship #4442 itself had to #4440.

What

After #4442 lands, packages/react/src/spec-bridge/bridges/list-view.ts reads:

function mapDensity(rowHeight?: RowHeight) {
  if (!rowHeight) return undefined;
  if (!Object.prototype.hasOwnProperty.call(ROW_HEIGHT_TO_DENSITY, rowHeight)) return undefined;
  return ROW_HEIGHT_TO_DENSITY[rowHeight];
}

The first guard rejects only falsy values. Any truthy non-string survives it, and both hasOwnProperty.call and the index then coerce the key with String(...) — so a value that is not a string at all can still select a density.

The core twin does not do this. packages/core/src/utils/normalize-list-view.ts:83 opens with a type guard, not a truthiness guard:

export function rowHeightToDensityMode(rowHeight: unknown): DensityMode | undefined {
  if (typeof rowHeight !== 'string') return undefined;
  ...

So the two surfaces disagree again, on a different input family:

rowHeight core bridge
['compact'] undefined 'compact'
new String('compact') undefined 'compact'
{ toString: () => 'compact' } undefined 'compact'

Measured through SpecBridge.transformListView (whose parameter is any — the untyped boundary a host's stored JSON actually crosses) while implementing #4442, and reproduced standalone against both function bodies.

Note the direction: this is the opposite of #4442. There the bridge leaked a function where core abstained; here the bridge invents a legitimate-looking density where core abstains. The second is quieter — nothing downstream can tell the value was fabricated from an array.

Why it matters beyond the two functions

packages/react/src/spec-bridge/__tests__/RowHeightDensityAgreement.test.ts is titled "core and the spec bridge give one answer", and after #4442 its off-spec lists are all strings. The invariant the file claims is therefore broader than the invariant it pins, and this is the gap. A JSON-authored view can hold an array or an object at rowHeight just as easily as a bad string.

Impact

No user hits this today, on the same evidence as #4442: the three-repo sweep in PR #4439, re-run at newer HEADs for #4440, found no off-spec rowHeight authored anywhere at all — let alone a non-string one. Filed as a defect rather than an observation because it is a live disagreement between two published surfaces that a dedicated pin file exists to prevent, but grading it is triage's call, not the filer's.

The fix, when someone takes it

Mirror core's opening guard, one line:

if (typeof rowHeight !== 'string') return undefined;

replacing the if (!rowHeight) truthiness check ('' is falsy and not an own property, so the empty-string case is unaffected either way). Worth extending the agreement pin's invariant loop to a non-string family (['compact'], new String('compact'), an object with a toString) in the same PR, since that is the assertion this currently breaks.


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 12, 2026
  2. yinlianghui commented on Aug 12, 2026

    @yinlianghui
    ContributorAuthor

    CLAIM — session session_017Qqyix2QcnpUC9XeYVDzx3 (objectui whole-repo seat), branch claude/issue-4459-bridge-nonstring-guard.

    Ruling (delegated authority; open veto window): fix as filed. Replace mapDensity's truthiness guard with core's type guard (typeof rowHeight !== 'string' → abstain), and extend the agreement pin's invariant loop with a non-string family (['compact'], new String('compact'), { toString: () => 'compact' }) — that is the assertion the current disagreement breaks, and it closes the gap between the pin file's title claim and what it pins. Empty-string behavior unchanged either way (falsy AND not an own property).

    Red-first: pre-fix transformListView({ name: 'x', rowHeight: ['compact'] }).density is 'compact' (measured in the issue body, via the any-typed boundary); post-fix undefined, agreeing with core. The direction that must NOT change: all five spec strings, and the string off-spec/prototype families #4447/#4457 pinned.

    Grading: runtime-only, off-spec non-string input — changeset patch for @object-ui/react unless the emitted .d.ts moves (it should not).

    Surface (mutual exclusion): packages/react/src/spec-bridge/** + one changeset. Base is current main (PR #4457 landed, so the surface is free). Disjoint from the only other in-flight card #4456 (eslint config / e2e).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions