Skip to content

finding(plugin-timeline): spellGanttDateValue's Array.isArray is the last non-total operation on the gantt date path — a revoked Proxy crashes it #7036

Description

@os-sam

Found while implementing #7027 (the gantt date brand gate). Not repaired there: #7027's charter is the type gate, and this is a different reflective operation in a different function, whose current form was a deliberate choice by #6907 rather than an oversight. Fixing it would be second-guessing a landed ruling, so it is recorded instead.

What is wrong

spellGanttDateValue chooses between an array / a function / an object with Array.isArray and typeof. Its docblock argues — correctly — that these "read no author-controlled property", and it explicitly rejected Object.prototype.toString.call because that consults a Symbol.toStringTag getter that can throw.

Array.isArray reads no property, but it is still not total. On a revoked Proxy it throws:

const { proxy, revoke } = Proxy.revocable({}, {}); revoke();
Array.isArray(proxy)   -> THREW TypeError:
    Cannot perform 'IsArray' on a proxy that has been revoked

IsArray recurses into the proxy's [[ProxyTarget]], and a revoked proxy has none. typeof proxy is still 'object', so nothing upstream separates it out.

What #7027 does and does not change about it

Measured with a node probe replicating both functions' branch bodies verbatim (not measured in-render):

tree where a revoked Proxy crashes
7fc5c3c12 (before #7027) isGanttDateTypeinstanceof walks [[GetPrototypeOf]], which throws on a revoked proxy
after #7027 spellGanttDateValue — the new brand test refuses it cleanly, then Array.isArray throws while naming it

So this is not a regression and #7027 does not introduce it: the value crashed the render before and crashes it after, at a different line. What changed is that it is now the only remaining throw site on this path, which is why it is worth a card of its own rather than staying invisible inside a larger one.

Why this is p3-shaped, and the honest argument against fixing it

Suggested scope if picked up

packages/plugin-timeline/src/renderer.tsx, spellGanttDateValue's trailing three branches. Either make the array test total, or state in the docblock that the totality claim excludes a revoked proxy and why that is acceptable — the same either/or #7027 was filed under. If it is taken, the row belongs in the existing pinned adversarial set (packages/plugin-timeline/src/__tests__/timeline-gantt-date-brand-7027.test.tsx), not in a new file.

⚠️ Worth reading with #7027's header: this is the fifth totality claim on this one code path, and the fourth to be falsified by the next card's measurement (#6759#6905#6907#7027 → this). The lesson that pattern keeps teaching is that totality asserted in prose is a hypothesis; the only thing that has settled it here is an exercised input set.

Refs: #7027 (the brand gate) · #6907 / PR #7026 (the spelling rule, which chose Array.isArray) · #6781 / PR #6905 (the type rule)

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfindingpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions