From e99403536274807c6e527e228be938c3a3100bc9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 13:33:53 +0000 Subject: [PATCH 1/2] fix(lint): waive only a MISSING `object` beside a `dataSource` binding `suppliedByDataSource` matched the props row's issue by path alone, so a present-but-wrong `object` (`object: 7`, `object: null`) beside a `dataSource.object` binding was silenced, while the same value with no binding was reported. Its docblocks scope the waiver to absence; the code now reads absence off the component (no key, or `undefined`) and lets every other issue at that path through unchanged. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude --- .../20400-datasource-waiver-missing-only.md | 25 +++++++++ .../lint/src/validate-component-props.test.ts | 52 +++++++++++++++++++ packages/lint/src/validate-component-props.ts | 8 ++- 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 .changeset/20400-datasource-waiver-missing-only.md diff --git a/.changeset/20400-datasource-waiver-missing-only.md b/.changeset/20400-datasource-waiver-missing-only.md new file mode 100644 index 00000000000..ad036b11a7d --- /dev/null +++ b/.changeset/20400-datasource-waiver-missing-only.md @@ -0,0 +1,25 @@ +--- +'@objectstack/lint': patch +--- + +`component-props-invalid` no longer hides a wrong `object` prop on a component that also carries a `dataSource` binding + +Clause-②: no + +A page component whose `dataSource.object` names the object may leave the flat +`properties.object` shorthand out: the binding supplies it, so the rule does not +report the props schema's required `object` as missing. That waiver was matched +on the issue's path alone, so it also swallowed every other issue the props +schema raised at `object`. A present but wrong value, such as `object: 7` or +`object: null`, was reported without a binding and silently passed with one. + +The waiver now covers what its contract says: a missing `object`, meaning no +key or an explicit `undefined`. A value the author did write is judged as +written, and it is reported at `properties.object` exactly as it is on the same +component without a binding. + +Effect on `os validate`, `os lint` and `os build`: a document that sets both a +`dataSource` binding and a wrong-typed `properties.object` now gets one +`component-props-invalid` warning it did not get before. The rule stays +advisory, so nothing that validated before is refused. A component that binds +through `dataSource` and omits `properties.object` is still clean. diff --git a/packages/lint/src/validate-component-props.test.ts b/packages/lint/src/validate-component-props.test.ts index 776b8b641df..96035d4f925 100644 --- a/packages/lint/src/validate-component-props.test.ts +++ b/packages/lint/src/validate-component-props.test.ts @@ -231,6 +231,58 @@ describe('validateComponentProps — value verdicts', () => { ]); }); + /** + * The waiver above covers a MISSING `object` only — no key, or `undefined`. + * A present value the row rejects is the author's own, and the binding + * supplies nothing in its place, so the row's verdict on it must reach the + * author exactly as it does with no binding at all. Each case is judged + * twice, beside the binding and without it, and the two answers must be the + * same finding: equality rather than wording, so the pin measures that the + * waiver lets the row's issue through unchanged. + */ + it.each([ + ['a number', 7], + ['null', null], + ])('reports a present-but-wrong `object` (%s) beside a `dataSource` binding, as it does without one', (_label, value) => { + const component = { type: 'element:number', properties: { object: value, aggregate: 'count' } }; + const withBinding = validateComponentProps( + stackWith([{ ...component, dataSource: { object: 'contact' } }]), + ); + const without = validateComponentProps(stackWith([component])); + + // Control first: with nothing supplying `object`, the row reports it. + expect(without.map((f) => [f.rule, f.path])).toEqual([ + [COMPONENT_PROPS_INVALID, 'pages[0].regions[0].components[0].properties.object'], + ]); + // The binding does not silence it. + expect(withBinding.map((f) => [f.rule, f.path])).toEqual([ + [COMPONENT_PROPS_INVALID, 'pages[0].regions[0].components[0].properties.object'], + ]); + expect(withBinding).toEqual(without); + }); + + it('still waives `object: undefined` beside a binding — an explicit undefined is a missing value', () => { + const findings = validateComponentProps( + stackWith([ + { + type: 'element:number', + dataSource: { object: 'contact' }, + properties: { object: undefined, aggregate: 'count' }, + }, + ]), + ); + expect(findings).toEqual([]); + + // …and the same bag with no binding is reported, so the silence above is + // the waiver and not the row accepting `undefined`. + const without = validateComponentProps( + stackWith([{ type: 'element:number', properties: { object: undefined, aggregate: 'count' } }]), + ); + expect(invalid(without).map((f) => f.path)).toEqual([ + 'pages[0].regions[0].components[0].properties.object', + ]); + }); + /** * #5775 — the retirement's author-facing channel. `displayField` was the * picker's REQUIRED prop and no renderer ever read it, so the tombstone's diff --git a/packages/lint/src/validate-component-props.ts b/packages/lint/src/validate-component-props.ts index 2b6bcb7bdae..f4793c57828 100644 --- a/packages/lint/src/validate-component-props.ts +++ b/packages/lint/src/validate-component-props.ts @@ -175,7 +175,13 @@ const DATASOURCE_SUPPLIED_PROP = 'object'; function suppliedByDataSource(issue: LintZodIssue, component: AnyRec): boolean { if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false; const dataSource = isRec(component.dataSource) ? component.dataSource : undefined; - return strName(dataSource?.object) !== undefined; + if (strName(dataSource?.object) === undefined) return false; + // "Missing" is read off the component, never off the issue: the path alone + // also matches a PRESENT value the row rejects (`object: 7`, `object: null`), + // and the binding supplies nothing there — the author wrote that value and + // the row's own verdict on it stands. Only no key, or `undefined`, is waived. + const props = isRec(component.properties) ? component.properties : undefined; + return props?.[DATASOURCE_SUPPLIED_PROP] === undefined; } /** From 89ae5540b5b586842cad69c81a695894ba6dcd48 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 14:33:45 +0000 Subject: [PATCH 2/2] docs(changeset): say what `--strict` does with the new warning The closing sentence said nothing that validated before is refused, which is false under `os validate --strict` and `os lint --strict`: both fail on any warning, so a bound component with a wrong-typed `properties.object` passed there before and fails now. State both halves. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude --- .changeset/20400-datasource-waiver-missing-only.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/20400-datasource-waiver-missing-only.md b/.changeset/20400-datasource-waiver-missing-only.md index ad036b11a7d..d9d2723b4f4 100644 --- a/.changeset/20400-datasource-waiver-missing-only.md +++ b/.changeset/20400-datasource-waiver-missing-only.md @@ -21,5 +21,7 @@ component without a binding. Effect on `os validate`, `os lint` and `os build`: a document that sets both a `dataSource` binding and a wrong-typed `properties.object` now gets one `component-props-invalid` warning it did not get before. The rule stays -advisory, so nothing that validated before is refused. A component that binds +advisory: without `--strict` nothing that validated before is refused; under +`os validate --strict` or `os lint --strict` the new warning fails the run, as +every warning does. A component that binds through `dataSource` and omits `properties.object` is still clean.