fix(plugin-calendar): ObjectCalendar's renderer consumes or declares every forwarded prop (#4492) - #4502
Merged
yinlianghui merged 2 commits intoAug 12, 2026
Conversation
…every forwarded prop (#4492)
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
ACCEPT — step-7 复核 by PM session
Flipping ready + arming auto-merge. The calendar package's whitelist migration is now COMPLETE across all four registrations (#4433 → #4452 → #4453 → #4492). Generated by Claude Code Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 12, 2026 22:03
yinlianghui
deleted the
claude/issue-4492-objectcalendar-props-contract
branch
August 12, 2026 22:04
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4492.
The defect
One shared
ObjectCalendarRendereris registered under bothplugin-calendar:object-calendarandview:calendar, and it ended in the same raw spread #4453 removed from thecalendar-viewrenderer next door:propsis everythingSchemaRendererhands a registered widget: the node's authored keys, the contents of itspropscontainer, the injected runtime props and a host's trailing props — an unbounded set, spread onto a component whose props are a closed list.ObjectCalendarPropsdeclares eight callbacks and alocale, so an authored value under any of those names landed on the declared prop, and an SDUI author writing JSON can never produce a function.onDateClick: 'NOT-A-FUNCTION'window.error: onDateClick is not a functiononNavigate: 'NOT-A-FUNCTION'window.error: onNavigate is not a functionlocale: 'en_US'SchemaErrorBoundary/Incorrect locale information providedThe two handler cases are worse than an error boundary: React does not route event-handler errors to
SchemaErrorBoundary, so they surfaced as uncaught window errors while the calendar kept looking fine and that gesture was dead.onDateClickis additionally the unconditional shape —ObjectCalendar.tsxtestsif (onDateClick)and a non-empty string is truthy — andonNavigate?.(date)guards nullish, not non-callable.localeis fatal at render:toLocaleDateString('en_US')throwsRangeError. The underscore spelling is the one a producer writes by accident.The contract
The #4425 phase-2 ruling (option 1, whitelist bounded by declaration) applied to this renderer, exactly as #4453 / PR #4494 applied it to
calendar-view. The forward set is now exactlyObjectCalendarProps, each key resolved to the type that prop declares; everything else is dropped.restis READ for declared keys and never spread — that is the whole of the fix.A deny-list could not close this: the leak is the open tail of author-supplied keys, which no enumeration can finish. This list is finishable because
ObjectCalendarPropsdeclares it — and dropping the tail costs nothing, becauseObjectCalendardestructures a closed list and reads no other prop. That is the census proof that nothing is lost.Both registrations share one implementation, so one fix closes
object-calendarandview:calendartogether; both are pinned.Census — every key the old spread could carry
Injected or forwarded by
SchemaRenderer:schemaboundschemaclassName(merged node className + scope class)events, thepropscontainer,properties,bindariaLabel/ariaDescribedBy/roleand the resolvedaria-*disabled(always__disabledor undefined)data-obj-id,data-obj-type,data-obj-schema-invalid,data-debug-*id,name,label,description, … (BaseSchema keys)Declared registry inputs (both registrations declare the same two), plus the flat spelling
ObjectView/ListViewemit — all consumed throughschema, never through the props channel, so none needs a forward:objectNameschema.objectNamecalendargetCalendarConfig(schema)startDateField,endDateField,titleField,colorField,allDayField,defaultView,filter,sortschemaDeclared
ObjectCalendarProps— all 14 audited:schemaboundschemadataSourceuseSchemaContext(); a host-passed adapter still wins, now validated by its call surface (find)classNamedataObjectViewuses)loadingonEventClick,onRowClick,onDateClick,onEdit,onDelete,onNavigate,onViewChange,onEventDroplocaleIntl.getCanonicalLocalesacceptsThree census notes:
CalendarViewhasslotMinutes;ObjectCalendarPropsdeclares no numeric key, so that resolver has no counterpart here — census-confirmed rather than assumed.onEdit/onDeleteare declared but never destructured byObjectCalendar(the record drawer builds its own edit/delete fromdataSource), so forwarding them is inert. They are listed anyway because the hatch is bounded by the DECLARATION; a key that is declared, inert and silently dropped would be a second, quieter contract. Behaviour is identical before and after: a value under either name does nothing.dataSourceis two different things sharing one name — the spec's elementdataSourceBINDING and the runtime ADAPTER.SchemaRendereralready strips the binding off the node, and deliberately preserves a host's explicit ReactdataSourceprop; that host path is kept at its old precedence, but type-checked, so a binding-shaped object arriving through thepropscontainer falls back to the context adapter instead of shadowing it (the shape behinddataSource.find is not a function, objectstack#5576).Red-first (verbatim)
packages/plugin-calendar/src/object-calendar-renderer.propsContract.test.tsxrun against the pre-fix renderer (git checkout origin/main -- index.tsx, restored after): 9 failed | 10 passed (19) — the 9 new pins red, the 10 must-not-change pins green on BOTH sides.Handler errors do not reach
SchemaErrorBoundary, so the pins capture the windowerrorevent, a synchronous throw out offireEvent, andconsole.error, and assert on the UNION — the environment does not get to decide the verdict by moving the report between channels.Post-fix: all 19 green. Both authoring channels are pinned for every authored case (the node's own key and the
propscontainer), per #4452's lesson, and both registrations are pinned so a future change that fixes only one cannot pass.onEventClick— provenance, stated honestlyThe filing recorded that it could not reproduce a crash on
onEventClick:ObjectCalendarcalls the parent handler only when the local navigation is not an overlay (if (!navIsOverlay)), and the default navigation config is a drawer, so the call was skipped. The filing claimed the exposure in the code path but not its reachability.This PR does not restate that claim — it splits the key into two cases and reports what each one measured:
navigation: { mode: 'none' }is the smallest non-overlay config (navIsOverlayfalse, anduseNavigationOverlay's ownhandleClickreturns early fornone, so no router is involved). MEASURED red pre-fix, verbatimwindow.error: onEventClick is not a function.So the answer to the filing's open question is: the exposure is real and reachable, and reachability depends on the navigation mode exactly as the filing suspected. That is this card's own new measurement, not a promotion of the filing's unproven claim.
Must-not-change (green on both sides)
onDateClick/onNavigatethroughSchemaRenderer's trailing props still fires, with a realDatepayload;ListView'sonRowClick— a real function on a real node key, fromListView.tsx'sbaseProps— still reaches the component;onEventClickstill fires when navigation is not an overlay;locale(de-DE) still reaches the component (asserted on German month names in the header);data/loadingpre-fetch path still reaches the component —registration.test.tsxalready pinned the renderer-level passthrough, which is whydataandloadingare forwarded at their declared types rather than dropped;classNamestill lands on the calendar container;eventson acalendar-viewnode crashes the component — the SDUI action object overwrites the computed CalendarEvent array #4433 and plugin-calendar: authoringcurrentDateas the ISO string its own registry input documents crashes calendar-view #4452 sibling pin files all stay green.Verification
The DOM-leak sweep is #4432's surface and #4434 owns its judge internals — it was not edited, only run read-only to confirm the calendar targets stay green. Rebased onto current
main(3fc2971b5) and re-verified there.Emitted
.d.tsmeasured both ways (build on this branch vs. with the renderer reverted toorigin/main):packages/plugin-calendar/dist/index.d.tsis byte-identical.ObjectCalendarRendereris an exported symbol, so its props type is published surface — it keeps its exact annotation, because what the renderer accepts is genuinely unchanged (an open record); only what it forwards narrows. The published surface does not grow, so this is a patch, not a minor.Out of scope, census-confirmed only
calendar-view'scolorMappinginput is declared but inert — nothing reads it, so an authored colour map does nothing #4493 (colorMappinginert) and plugin-calendar:calendar-view'sallowCreateinput is declared but inert — the handler it would drive is built and never passed #4454 (allowCreateinert) arecalendar-viewregistry inputs. Neither is a declared input ofobject-calendar/view:calendar, and neither appears inObjectCalendar.tsx,CalendarView.tsxor this renderer at all — on this surface they are part of the open tail and were already inert. Confirmed by this census, deliberately not fixed here; they stay their own pooled pair.Generated by Claude Code