Skip to content

finding(plugin-timeline): a slot-less Date impostor passes the gantt type gate and then crashes new Date mid-render #7027

Description

@claude

Found while implementing #6907 (the gantt-date spelling rule, PR #7026). Not repaired there: it sits in findUnusableGanttDate's type gate, one function upstream of the speller that card is about, and it is #6781's surface rather than #6907's.

What is wrong

isGanttDateType uses value instanceof Date as the repo idiom for "is a Date". An object that inherits from Date.prototype without owning a [[DateValue]] internal slot passes that gate, and then new Date(value) in isUnusable runs ToPrimitive on it and throws:

Object.create(Date.prototype) instanceof Date          -> true   (passes the type gate)
new Date(Object.create(Date.prototype)).getTime()      -> THREW TypeError:
    Method Date.prototype.toString called on incompatible receiver [object Object]

Measured on b458300ca with a throwaway node probe. The crash is uncaught and mid-render, so the author gets a blank screen where a named diagnostic belongs — the exact failure mode #6781 set out to remove when it put the type gate before new Date, and the reason spellGanttDateValue was written total by construction.

So #6905's totality argument for findUnusableGanttDate — "new Date is only ever reached with a string, a finite number, or a Date, none of which throw" — has one gap: instanceof Date admits a Date-shaped object that is not a Date.

Why it was left alone rather than fixed in #6907

Judged not reachable from an authored document, which is why this is filed as an observation rather than repaired inline:

  • ObjectUI metadata is JSON, and JSON cannot express Object.create(Date.prototype).
  • Data adapters produce real Dates (via new Date(...)), plain objects, strings or numbers — all of which own their slots or are refused cleanly.

It needs hand-written JS that deliberately constructs a slot-less Date impostor. Recording it because #6781's totality property is stated out loud in the code, and a stated invariant with a known gap should be either closed or documented rather than quietly untrue.

Suggested scope if picked up

packages/plugin-timeline/src/renderer.tsx, isGanttDateType / isUnusable. Two routes, both cheap:

  1. Tighten the gate so it tests the slot, not just the prototype chain — e.g. judge Number.isFinite(value.getTime()) behind a total guard, or use the Object.prototype.toString.call(value) === '[object Date]' brand test. Note that brand test consults Symbol.toStringTag, which can itself be a throwing getter, so it is not free.
  2. Decide the invariant is "reachable inputs only" and say so in the docblock, narrowing the totality claim to the values an authored document can carry.

Route 2 may well be the right answer under startup scope discipline; the point of this card is that the choice should be made rather than inherited. Whichever wins, spellGanttDateValue itself is unaffected — after #6907 it is total over every value that reaches it.

Refs: #6907 / PR #7026 (the spelling rule) - #6781 / PR #6905 (the type rule and its totality argument) - #6759 / #6770 (the pinned spellings)


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions