Skip to content

Commit 133e8fe

Browse files
committed
fix(lint,cli): os lint / os compile 不再放行一个 os validate 会拒绝的 react 页面 (#4340 后续)
`validateReactPageProps` 只手工接在 `os validate` 上,`os lint` 和 `os compile` 从来没跑过它。在 showcase 的 react 页面植入一处 gating 违规 (`<ListView filters={['no_such_col','=',stage]}>` —— 谓词命中不了任何行, SqlDriver 吞掉 "no such column" 后返回 [],列表回空且与「本来就没数据」无法区分) 实测: os lint os compile os validate 修复前 exit 0 放行 exit 0 放行 exit 1 拒绝 修复后 exit 1 拒绝 exit 1 拒绝 exit 1 拒绝 #4340 之后这条规则已经是整个 react 页面表面唯一的字段解析闸门:<ListView> 的 columns/fields/sort/grouping/userFilters、<ObjectForm> 的 fields/initialValues/sections/subforms、record:* 一族(与元数据表面共用同一张 COMPONENT_FIELD_SPECS)、<ObjectChart> 的 aggregate/axes,以及 searchableFields。 所以漏接不是少几条警告 —— 而是这些绑定在 build 路径上完全没人看,包括其中会 gate 的那些。 现接入 REFERENCE_INTEGRITY_RULES,`os validate` 那处手工接线随之删除,三个命令的 答案由构造保证一致。这正是 suite 设立要终结的漂移(#3583 §5 D5),也是 validateReadonlyFlowWrites 在 #4394 里刚走过的同一条路:那次是「一张 map、 两个检查、两套命令集合」,这次是「一次 JSX parse、七个 rule id、一套命令集合」。 按 charter,react-prop-missing-required / react-prop-typo 是 shape 而非解析问题, 本不属于 suite。它们随行的理由与 validateActionBodyWrites 完全相同(其注释已写明): 落在同一次 parse 的同一份 source 上,拆成第二个 member 就要把每个 react 页面 parse 两遍,而改回手工接线正是 suite 要终结的那种漂移。同时更正了那条注释里 "The ONE member here that emits two rule ids" 的说法 —— 现在不止一个。 规则行为零变化:id、严重级、文案都不动;输入也不变(`os validate` 原本就传 result.data,suite 拿到的是同一个)。#4402 的接线守卫会在下次有人想再手工接一条 规则时直接报错——本次删除 validate.ts 的直接 import 也正是被它逼出来的。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012rK2McPogrTJBKdUsThrGs
1 parent 302e972 commit 133e8fe

4 files changed

Lines changed: 95 additions & 38 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@objectstack/lint": patch
3+
"@objectstack/cli": patch
4+
---
5+
6+
fix(lint,cli): `os lint` / `os compile` 不再放行一个 `os validate` 会拒绝的 react 页面
7+
8+
`validateReactPageProps` 只手工接在 `os validate` 上,另外两个命令从来没跑过它。
9+
在 showcase 的 react 页面上植入一处 gating 违规(`<ListView filters={['no_such_col','=',stage]}>`
10+
—— 谓词命中不了任何行,列表回空,和「本来就没数据」无法区分)实测:
11+
12+
```
13+
os lint os compile os validate
14+
修复前 exit 0 放行 exit 0 放行 exit 1 拒绝
15+
修复后 exit 1 拒绝 exit 1 拒绝 exit 1 拒绝
16+
```
17+
18+
这条规则在 #4340 之后已经是**整个 react 页面表面唯一**的字段解析闸门:
19+
`<ListView>` 的 columns/fields/sort/grouping/userFilters、`<ObjectForm>`
20+
fields/initialValues/sections/subforms、`record:*` 一族(与元数据表面共用同一张
21+
`COMPONENT_FIELD_SPECS`)、`<ObjectChart>` 的 aggregate/axes、以及 `searchableFields`
22+
漏接不是少几条警告 —— 而是这些绑定在 build 路径上**完全没人看**,包括其中会 gate 的那些。
23+
24+
现接入 `REFERENCE_INTEGRITY_RULES`,`os validate` 里那处手工接线随之删除,三个命令的
25+
答案由构造保证一致。这正是 suite 设立要终结的漂移(#3583 §5 D5),也是
26+
`validateReadonlyFlowWrites`#4394 里刚走过的同一条路 —— 那次的教训是
27+
「一张 map、两个检查、两套命令集合」,这次是「一次 JSX parse、七个 rule id、
28+
一套命令集合」。
29+
30+
规则行为零变化:id、严重级、文案都不动;喂进去的输入也不变(`os validate` 原本就
31+
`result.data`,suite 拿到的是同一个)。`#4402` 的接线守卫会在下一次有人想再手工
32+
接一条规则时直接报错。
33+
34+
`validateReactPageProps` 沿用 `validateHookBodyWrites` / `validateActionBodyWrites`
35+
的惰性约定:只有真的存在 `kind:'react'` 页面时才加载 TypeScript 编译器。

packages/cli/src/commands/validate.ts

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { validateDashboardActionRefs } from '@objectstack/lint';
2323
import { validateFilterTokens } from '@objectstack/lint';
2424
import { validateReferenceIntegrity } from '@objectstack/lint';
2525
import { validateResponsiveStyles } from '@objectstack/lint';
26-
import { validateJsxPages, validateReactPages, validateReactPageProps, validatePageSourceStyling } from '@objectstack/lint';
26+
import { validateJsxPages, validateReactPages, validatePageSourceStyling } from '@objectstack/lint';
2727
import { validateCapabilityReferences } from '@objectstack/lint';
2828
import { validateVisibilityPredicates } from '@objectstack/lint';
2929
import { validateSecurityPosture, validateOrgAxisRedLines } from '@objectstack/lint';
@@ -454,38 +454,13 @@ export default class Validate extends Command {
454454
this.exit(1);
455455
}
456456

457-
// 3d. React-source pages — prop usage against the component contract
458-
// (ADR-0081 Phase 2): missing required bindings (error) + likely
459-
// prop typos (warning), parsed from the real JSX.
460-
if (!flags.json) printStep('Checking React-source page props (ADR-0081)...');
461-
const reactPropFindings = validateReactPageProps(result.data as Record<string, unknown>);
462-
const reactPropErrors = reactPropFindings.filter((f) => f.severity === 'error');
463-
const reactPropWarnings = reactPropFindings.filter((f) => f.severity === 'warning');
464-
if (!flags.json) {
465-
for (const w of reactPropWarnings.slice(0, 50)) {
466-
console.log(chalk.yellow(` \u26a0 ${w.where}: ${w.message}`));
467-
console.log(chalk.dim(` ${w.hint}`));
468-
}
469-
}
470-
if (reactPropErrors.length > 0) {
471-
if (flags.json) {
472-
await emitJson({
473-
valid: false,
474-
errors: reactPropErrors,
475-
warnings: [...widgetWarnings, ...styleWarnings, ...jsxWarnings, ...reactPropWarnings],
476-
duration: timer.elapsed(),
477-
});
478-
this.exit(1);
479-
}
480-
console.log('');
481-
printError(`React-source page prop check failed (${reactPropErrors.length} issue${reactPropErrors.length > 1 ? 's' : ''})`);
482-
for (const f of reactPropErrors.slice(0, 50)) {
483-
console.log(` \u2022 ${f.where}: ${f.message}`);
484-
console.log(chalk.dim(` ${f.hint}`));
485-
console.log(chalk.dim(` rule: ${f.rule} at ${f.path}`));
486-
}
487-
this.exit(1);
488-
}
457+
// 3d. React-source page PROPS are checked by `REFERENCE_INTEGRITY_RULES`
458+
// (step 3a above), not from here (#4340 follow-up). They ran from
459+
// this call site ALONE, so `os lint` and `os compile` accepted a
460+
// react page whose every field binding was stale — including the
461+
// gating ones. That is `validateReadonlyFlowWrites`' divergence
462+
// (#4394) one surface over. The input is unchanged: the suite is
463+
// handed the same `result.data` this block passed.
489464

490465
// 3e. Source-tier page styling (ADR-0065): Tailwind className in a
491466
// kind:'html'/'react' page source silently no-ops (the build never

packages/lint/src/reference-integrity-suite.test.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('reference-integrity suite — membership', () => {
3030
'validateActionBodyWrites',
3131
'validateFlowNodeWrites',
3232
'validateReadonlyFlowWrites',
33+
'validateReactPageProps',
3334
]);
3435
});
3536

@@ -107,6 +108,17 @@ describe('reference-integrity suite — every member actually runs', () => {
107108
},
108109
],
109110
},
111+
// validateReactPageProps: the same component family one surface over,
112+
// authored as JSX. `<ObjectForm>` with no `objectName` binds nothing, so
113+
// this GATES — and it gated on `os validate` alone until this member
114+
// joined the suite. A separate page from `lead_detail` on purpose: the
115+
// metadata walk above must stay the only source of `page-field-unknown`
116+
// here, or this member could go silent behind it.
117+
{
118+
name: 'lead_console',
119+
kind: 'react',
120+
source: 'function Page(){ return <ObjectForm mode="edit" />; }',
121+
},
110122
],
111123
datasets: [
112124
{
@@ -221,11 +233,21 @@ describe('reference-integrity suite — every member actually runs', () => {
221233
expect(rules).toContain('agent-authoring-withdrawn');
222234
expect(rules).toContain('hook-body-write-unknown-field');
223235
expect(rules).toContain('action-body-write-unknown-field');
224-
// The one member that emits a second rule id — see the suite's comment on
225-
// why it rides along instead of becoming its own entry.
236+
// Members that emit a rule id beyond their resolution one — see the suite's
237+
// comments on why these ride along instead of becoming their own entries.
226238
expect(rules).toContain('action-record-write-discarded');
227239
expect(rules).toContain('flow-node-write-unknown-field');
228240
expect(rules).toContain('flow-update-readonly-field');
241+
expect(rules).toContain('react-prop-missing-required');
242+
});
243+
244+
it('carries a gating react-page-prop finding through the suite (#4340)', () => {
245+
const findings = validateReferenceIntegrity(stack);
246+
const react = findings.find((f) => f.rule === 'react-prop-missing-required');
247+
// Must reach the CLI as an ERROR: `os lint` and `os compile` saw NOTHING
248+
// from this rule until it joined the suite, so a react page binding nothing
249+
// sailed through the build the way a readonly flow write did (#4394).
250+
expect(react?.severity).toBe('error');
229251
});
230252

231253
it('carries a gating flow-template finding through the suite (#3810)', () => {
@@ -247,9 +269,9 @@ describe('reference-integrity suite — every member actually runs', () => {
247269
expect(typeof f.message).toBe('string');
248270
expect(typeof f.hint).toBe('string');
249271
}
250-
// Object references run first, readonly flow writes last.
272+
// Object references run first, react page props last.
251273
expect(findings[0].rule).toBe('object-reference-unknown');
252-
expect(findings[findings.length - 1].rule).toBe('flow-update-readonly-field');
274+
expect(findings[findings.length - 1].rule).toBe('react-prop-missing-required');
253275
});
254276

255277
it('returns nothing for an empty stack', () => {

packages/lint/src/reference-integrity-suite.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import { validateHookBodyWrites } from './validate-hook-body-writes.js';
6969
import { validateActionBodyWrites } from './validate-action-body-writes.js';
7070
import { validateFlowNodeWrites } from './validate-flow-node-writes.js';
7171
import { validateReadonlyFlowWrites } from './validate-readonly-flow-writes.js';
72+
import { validateReactPageProps } from './validate-react-page-props.js';
7273

7374
export type ReferenceIntegritySeverity = 'error' | 'warning';
7475

@@ -126,7 +127,8 @@ export const REFERENCE_INTEGRITY_RULES: readonly ReferenceIntegrityRule[] = [
126127
// carries over — an action's `ctx.input` is its params bag, not a record
127128
// (see that module's ledger). Lazy on the same terms.
128129
//
129-
// The ONE member here that emits two rule ids. Besides resolving `ctx.api`
130+
// The first member here to emit more than one rule id (`validateReactPageProps`
131+
// below is the other, and carries the most). Besides resolving `ctx.api`
130132
// writes against declared fields (`action-body-write-unknown-field`), it
131133
// reports a `ctx.record` write that can reach nothing
132134
// (`action-record-write-discarded`, #4345) — not a resolution question, so
@@ -154,6 +156,29 @@ export const REFERENCE_INTEGRITY_RULES: readonly ReferenceIntegrityRule[] = [
154156
// build the other command would have stopped. Joining the suite is the whole
155157
// fix; the two hand-wired call sites are deleted with it (#4345 follow-up).
156158
{ name: 'validateReadonlyFlowWrites', run: validateReadonlyFlowWrites },
159+
// The `kind:'react'` page surface. Every prop a react block binds BY FIELD
160+
// NAME is resolved against the object it names (#4340) — `<ListView columns>`,
161+
// `<ObjectForm fields>`, the `record:*` family through the SAME
162+
// `COMPONENT_FIELD_SPECS` table `validatePageFieldBindings` walks one surface
163+
// over, plus `<ObjectChart>`'s aggregate/axes (#3701/#3729) and
164+
// `searchableFields` (#4329). Squarely the charter's question, on the surface
165+
// where it had no answer at all.
166+
//
167+
// It was hand-wired into `os validate` ALONE, so `os lint` and `os compile`
168+
// accepted a react page whose every field binding was stale — including the
169+
// gating ones (a missing required binding, a filter position naming no field:
170+
// the predicate can never match and the list comes back empty). That is
171+
// `validateReadonlyFlowWrites`' divergence again, one surface over, and it is
172+
// the reason this entry exists rather than a fourth hand-wiring.
173+
//
174+
// Like `validateActionBodyWrites` above, it emits ids that are not resolution
175+
// questions — `react-prop-missing-required` and `react-prop-typo` are shape,
176+
// and by the charter belong outside. They ride along for the same reason: they
177+
// fall out of the SAME TypeScript parse of the SAME page source, and splitting
178+
// them into a second member would parse every react page twice to say two
179+
// things about one walk. Lazy on the same terms as the hook/action body rules
180+
// — only a page that is actually `kind:'react'` loads the compiler.
181+
{ name: 'validateReactPageProps', run: validateReactPageProps },
157182
];
158183

159184
/**

0 commit comments

Comments
 (0)