feat(lint): view/page 可见性谓词的 CEL 语法构建期闸门 (#6253) - #6472
Merged
Conversation
新增 error 级规则 `visibility-predicate-syntax`:view/page 的可见性谓词 (`visibleWhen` 及两个已弃用别名 `visibleOn` / `visibility`)如果规范 CEL 前端解析不了,构建期直接拒收,不再零诊断放行到运行时 fail-open。 按维护者 2026-08-07 对 #6253 的裁定:判 blocking error,与 ADR-0032 下 其它谓词面同级,不设 warning 档、不写本面豁免。 判定仍取 `parseCelToAst`(规范前端),本规则不自建 Environment、不手写 tokenizer;新增的是对既有判定的上报与自纠措辞。明确不走 `compile()` / `validateExpression`——那是 parse + 类型检查,实测会拒掉 `type == 'grid'`, 从语法分支推翻本文件已钉测试的既有盲点,并把闸门从「解析不了」扩张成 「类型检查不过」。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
`quoteSource(source!)` 改为在 `if (source && syntaxFault)` 内由编译器收窄, 去掉本文件注释自己反对的 `!` 断言。行为不变。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
hotlong
marked this pull request as ready for review
August 8, 2026 01:00
hotlong
enabled auto-merge
August 8, 2026 01:01
`check:type-check-debt`(TypeScript Type Check job 的最后一步)红: @objectstack/lint 的 TEST_DEBT 实测 42 → 47(+5)。 根因是一处**既有**缺陷,被本单新增的测试放大:测试文件第 10 行 `from './validate-visibility-predicates'` 缺 `.js` 后缀。在 NodeNext 下这是 TS2835,且该模块因此解析为 `any`,于是文件里每一个 `.map((f) => …)` / `.filter((f) => …)` 回调都级联出 TS7006(`f` 隐式 any)。本单新增的断言带来 更多这类回调,把既有级联乘大了。 补上 `.js` 后(与同包所有测试文件、以及本文件自己的第二条 import `./authoring-rules.js` 一致),该文件的测试层错误 28 → **0**, 整包 47 → **19**,低于台账记录的 42。 **台账未抬**(棘轮只减不增,#5278):entry 仍是 42,门现在把它报成 改进(`ℹ TEST_DEBT records 42, tsc now reports 19 (-23)`)。 **测试未削弱**:本次改动是 1 增 1 删的单行 import,断言一条未动 —— 118 处 expect、86 条用例(其中本单新增 32 条)全绿。 本地未复现是因为检查面不同:@objectstack/lint 的 tsconfig 把 `*.test.ts` 排除在外,所以 `pnpm --filter @objectstack/lint typecheck` 根本不读测试文件, 而该门会解除排除后重测。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
This was referenced Aug 8, 2026
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.
Fixes #6253
view/page 的可见性谓词此前在构建期无人判语法。本 PR 新增 error 级规则
visibility-predicate-syntax,落在 PR #6248 已经注释过的那个parseCelToAst返回null的分支上。裁定与路线
按维护者 2026-08-07 的裁定执行:判 blocking error,与 ADR-0032 下其它谓词面
(validation rule / flow / action)同级;不设 warning 档,不为本面写豁免。
实施方在这一点上没有裁量,本 PR 也不提替代方案、不加软化开关。
为什么这一面此前没人报
validate-expressions.ts(ADR-0032)对它遍历到的每条谓词都跑validateExpression,语法错报 blocking error —— 但它的遍历面是 objects / flows / actions / sharingRules /
hooks,从不走
views与pages。走这一面的三条规则都明确不判语法,理由是「不发明第二个语法判定」。那条政策在它自己的调用点上成立(
validateExpression就在同一批调用点上跑),在 view/page 面上不成立:那里没有第二个判定,沉默就是没人报。
后果是 #5149 同型的 fail-open:谓词求值失败 →
evalFieldPredicate返回 fallback →可见性 fallback 是
true→ 元素无条件渲染,与「没写谓词」在屏幕上一模一样。判定仍然不是本包给的(这正是旧政策要保护的东西)
旧政策的真实内容是「对『什么能解析』只有一个答案」,这一点完整保留:判定取
parseCelToAst(规范前端,带 #3306 改写与DEFAULT_LIMITS,#4812),本规则不自建
Environment、不手写 tokenizer。新增的是对既有判定的上报,外加原始报错缺的自纠措辞 —— cel-js 只说
Unexpected character: =并画一个 caret,既没点名作者写的运算符,也没给出 CEL 的写法。
明确不走
validateExpression/celEngine.compile尽管那才是 ADR-0032 的入口。
compile()是 parse + 类型检查,差别不是理论上的 ——本 PR 实测它会以
no such overload: type == string拒掉type == 'grid',而那正是本文件已钉测试的既有盲点(字段名与 CEL 类型名相同时不判,因为改读 overload 消息会
误杀合法的
type(record.x) == string)。从语法分支绕过去会把那条决定悄悄推翻,并把一条error 级闸门从「解析不了」扩张成「类型检查不过」—— 而这一面的谓词绝大多数是
dyn。裁定说的是语法,parse 判定恰好就是语法。已加测试钉住这条边界。
消息自纠
实测过的非 CEL 拼法各自点名并给出 CEL 写法:
===→==、!==→!=、<>→!=、and→&&、or→||、not→!、单个=→==。扫描前先把字符串字面量抹平,所以
record.msg == 'a === b' and record.n > 1归咎于and而不是字面量里的===;record.msg == 'a === b'本身能解析,压根不报。??与 SQL 的IN (…)故意不进表:两者都会解析失败、都照报(带前端原话),但都没有「换一个 token」就能修好的等价写法,给半个修法只会让作者多跑一趟。
真实 CLI 门后的输出(下面「端到端」一节的注入实验):
边界(均已钉测试)
parseCelToAst对空源也返回null,没有这道 guard会把「没写谓词」报成坏 CEL。
DEFAULT_LIMITS超限属于边界错而非语法错,照报但引用前端原话、不假装找到了typo(与 ADR-0032 把两者一并归入「invalid CEL predicate」的既有做法一致);
超长谓词在消息里省略,单条 runaway 表达式刷不满控制台。
让位。该互斥性由「断言整个上报集合」钉住,而不是靠调用方内部实现。
反向验证(先声明,后运行)
声明的方向:RED(常规方向)—— 把生产改动摘掉、测试全留,新增的钉子测试应当变红。
预测 21 红 / 65 绿(共 86),并逐条列出了哪些断言会红。
实测结果:21 failed | 65 passed (86),且逐条命中,与声明完全一致。
诚实记录的一点:纯「不报」型断言在功能被删掉时是空绿的。因此凡是有意义的地方,
都把否定断言与同一测试内的肯定断言配对(如「CEL 写法是干净的 —— 配对以防空绿」、
「空谓词不是语法错」、「一条坏谓词只出一个 finding」),删掉规则时红的是配对里的
肯定那一半。仍然空绿的几条(如「字符串字面量里的
===不是错」「不扩张到类型检查」「若干合法谓词不报」)在报告里按空绿如实标注,不当作证据。
另有一条既有测试原本断言整条规则沉默(
'country === "USA"'→[]),它钉的正是本 PR 删掉的那个分支 —— 按「整条替换」处置,重写为它幸存的那半个事实:解析不出 AST
的源不产生裸标识符判定。
验证证据
pnpm lint(ESLint,家族闸在此 job 内)pnpm --filter @objectstack/lint typecheckpnpm --filter @objectstack/cli typecheckpnpm --filter @objectstack/lint testpnpm --filter @objectstack/cli testpnpm --filter @objectstack/metadata-protocol testpnpm check:nul-bytespnpm check:empty-changesetpnpm check:adr-anchorspnpm --filter @objectstack/lint check:doc-formula-expressions新增 32 条测试。另按字节纪律对改动文件做了闸门之外的自扫描
(
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'),干净。消费半径清扫
规则只有注册表一个调用方,但改动会把原本沉默的谓词变成阻断,所以全仓清扫了会喂到
这条规则的 fixture:全仓 examples / apps / packages 的 view/page 可见性谓词都能通过
规范前端解析。唯一的
===出现在packages/spec/src/ui/view.test.ts(:1126 / :1240 /:1245 / :1291 / :1373)—— 那是纯 schema 测试样本,只跑
FormFieldSchema.parse,不经 lint,是本单援引的证据而非待修点,按要求只读未改。CLI 的模板与脚手架里没有
任何可见性谓词。
端到端(证明真的会拦)
三个示例 app
os validate全部通过、零 visibility finding、exit 0。随后往
app-showcase的表单谓词临时注入===,os validateexit 1、✗ Author-time rules failed (1 issue)(见上面的输出),随即已还原并复验通过 ——「坏谓词发不出去」是实测的,不是推断的。
明确没有做的事
validate-expressions.ts—— 本单不改 ADR-0032 的遍历面。packages/spec/src/ui/view.test.ts—— 那里的===是样本,不是待修点。validate-null-guards.ts—— 同一条沉默政策的另一条规则,不在本单射程内。顺带说明:它的沉默在它自己的调用点上仍然成立(
validateExpression就在旁边跑),本 PR 的论据不构成对它的改动理由。
content/docs/releases/。validateVisibilityPredicates的 tier 在 lint(devx): view/page 谓词(visibleWhen/visibleOn 等)的裸标识符构建期静态校验 —— #5149 裁决拆单 #6128 已是gating、commands 已是 build/lint/validate,本规则的
error直接沿用(已加测试复核该前提仍然成立)。skip-changeset标签(本 PR 有 changeset)。Changeset
.changeset/lint-visibility-predicate-syntax-gate.md,@objectstack/lint: minor。取 minor 的理由是照搬同族先例:同一文件、同样「新增一条 error 级规则」的
visibility-bare-identifier(#6128)用的就是 minor。这是新增能力而非破坏性 API 变更 ——公开签名只是追加了一个规则 id 常量,既有类型与函数签名一律未变。
规则 id 常量已按
rule-id-barrel-exports.test.ts的要求加入已发布 barrel(
packages/lint/src/index.ts)—— 这是本 PR 唯一一处在 issue 指定文件面之外的改动,纯机械要求:少了这一行,该闸门会红。
Generated by Claude Code