fix(lint): every exported rule id constant is reachable from a published barrel (#5648) - #5735
Merged
Merged
Conversation
…hed barrel (#5648) A rule writes its id into every finding's `f.rule`, and that string is what `os lint --json` / `os validate` put in front of consumers — Studio's finding renderer, downstream filtering, and `suppressWarnings: ['<rule-id>']` in authored metadata. Rule files export a named constant for exactly that reason: a consumer should compare against the constant, not retype the slug. But `package.json#exports` opens only "." and "./runtime" (and `tsup.config.ts` builds only those two entries), so a constant no barrel re-exports is not merely inconvenient to reach — it is unreachable. There is no deep path to fall back to, and the consumer's only remaining option is the literal the constant existed to eliminate. #5648 reported one (`FLOW_TRIGGER_UNKNOWN_EVENT`). The sweep it asked for found SIX more, across five rule files and all predating it: `APPROVAL_APPROVER_TYPE_UNSUPPORTED`, `SECURITY_FLS_UNQUALIFIED_KEY`, `FIELD_GROUP_SHADOWED`, `WIDGET_LEGACY_ANALYTICS_SHAPE`, `WIDGET_LEGACY_ANALYTICS_UNRENDERABLE`, `REACT_CHART_DRILLDOWN_INVALID`. `WIDGET_LEGACY_ANALYTICS_SHAPE` shows the cost best: the rule's own message tells the user to `Suppress with suppressWarnings: ['widget-legacy-analytics-shape']` — it teaches the id while withholding the constant that carries it. Nothing failed when a barrel line was forgotten: the rule kept working, and its unit test imported the constant from the rule file directly rather than through the barrel, so the omission surfaced only when someone consumed it from outside the package. Seven independent omissions of one one-line kind is a missing check, not a memory problem. So the judgement moves into the package's own test surface (`src/rule-id-barrel-exports.test.ts`): forgetting the barrel line now fails in the same run the new rule's own tests go green. The invariant is package-local — nothing outside defines lint rule ids — and it needs to actually IMPORT the barrel to verify values, which vitest gives for free; a `scripts/` gate would either reimplement an ES parser or have to build dist first, and would move the feedback into another job. Built against two shapes of false green: - Discovery reads `src/` from the filesystem rather than a hand-kept list, so a new rule file is enumerated as soon as it exists, and a floor on the id count makes a future break in the extraction pattern fail loudly instead of passing over an empty set. - The two entries are named statically (a fully dynamic import cannot be bundled reliably), but a separate case derives the same set independently from `package.json#exports` AND `tsup.config.ts` and compares — a third entry that is not registered fails rather than going silently unchecked. Classification is by VALUE SHAPE, not emission site: an earlier draft matched `rule: NAME` next to the definition and missed the four `REACT_CHART_*` ids, which reach their finding through a helper argument — including one of the real gaps. Export surface only, no behavior change: no `f.rule` string moved, and consumers already comparing literals keep working. Co-authored-by: Claude <noreply@anthropic.com>
|
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:
|
os-zhuang
marked this pull request as ready for review
August 6, 2026 02:53
os-zhuang
enabled auto-merge
August 6, 2026 02:53
This was referenced Aug 6, 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 #5648
前提核对
issue 的前提成立,且比它报的更广。
origin/main(205e81b) 上:packages/lint/src/validate-flow-trigger-readiness.ts:57导出FLOW_TRIGGER_UNKNOWN_EVENT,src/index.ts与src/runtime.ts均无该名(grep -c= 0)。package.json#exports确实只开"."与"./runtime",tsup.config.ts的 entry 也只有src/index.ts与src/runtime.ts,index.ts内没有任何export *(全部逐名列出)。三者合起来印证 issue 的判断:没进 barrel 不是「不好取」,而是完全取不到,没有深路径可绕。为什么这不是一行的事
规则把自己的 id 写进每条 finding 的
f.rule,而那个字符串就是os lint --json/os validate递到消费者手上的东西:Studio 的 finding 渲染、下游按规则过滤/抑制、以及被授权元数据里的suppressWarnings数组。规则文件为此导出同名常量,消费者本该比对常量而不是重敲 slug。issue 建议的全量清查翻出另外六处同类漏项,散在五个规则文件,成因都远早于 #5648:
FLOW_TRIGGER_UNKNOWN_EVENTvalidate-flow-trigger-readiness.ts:57APPROVAL_APPROVER_TYPE_UNSUPPORTEDvalidate-approval-approvers.ts:57SECURITY_FLS_UNQUALIFIED_KEYvalidate-security-posture.ts:68FIELD_GROUP_SHADOWEDvalidate-semantic-roles.ts:23WIDGET_LEGACY_ANALYTICS_SHAPEvalidate-widget-bindings.ts:86WIDGET_LEGACY_ANALYTICS_UNRENDERABLEvalidate-widget-bindings.ts:87REACT_CHART_DRILLDOWN_INVALIDvalidate-react-page-props.ts:259七个都是活的规则 id:各有发射点与单测(已逐个核对,非休眠代码)。其中
WIDGET_LEGACY_ANALYTICS_SHAPE最能说明代价 —— 规则打给用户的提示原话是Suppress with suppressWarnings: ['widget-legacy-analytics-shape'](validate-widget-bindings.ts:417),即它主动教消费者用这个 id,却不让消费者拿到承载它的常量。漏项之所以能一路静默:规则照常工作,而它自己的单测从规则文件直接 import 常量(不经 barrel),于是唯一会暴露的时刻是有人从包外去消费它。同一种一行漏项独立发生七次,不是「下次记牢」能解决的记性问题,而是缺一条判定 —— 所以按 issue 正文的建议走了 gate 化这一支。
改动
src/index.ts,各自插进所属模块已有的导出块,顺序与源文件一致)。src/rule-id-barrel-exports.test.ts—— 4 条用例,把「每个导出的规则 id 常量必须 barrel 可达」变成 packages/lint 测试面的门禁。"@objectstack/lint": patch(导出面变化即发布面变化)。分层:为什么落测试面而不是
scripts/门禁scripts/门禁要么自己写一个 ES 解析器(即再造一份易错的静态分析),要么先构建 dist 才能检查。scripts/门禁会把它挪到另一个 job。门禁如何防两类假绿
1. 「规则文件根本没被枚举到」 —— 发现面是对
src/的文件系统读取,不是手写规则文件清单,新规则文件一存在即被枚举。另加一条covers the whole rule surface:对 id 条数(> 100)与覆盖文件数(> 30)压下限,使将来改坏提取式后**在空集上「全绿」**这条路走不通。同一用例还断言无重名常量 —— 两个文件导出同名,barrel 只可能再导出其一,却会替另一条规则作答。2. 「barrel entry 根本没被 import」 —— 两个 entry 是静态列出的(全动态 import 无法可靠打包),但另有
barrel entries match the published exports map从package.json#exports与tsup.config.ts各自独立推导出同一集合并三方比对。新增第三个 entry 若不登记就会红,而不是悄悄不被检查;同时也挡住「发布了但没构建」的 entry —— 否则门禁会拿一个根本不出货的文件去判定可达性,这本身就是假绿。分类按取值形状,而非发射位置。 早先一版靠「定义旁边有
rule: NAME」来认规则 id,结果漏掉了经辅助函数参数发射的四个REACT_CHART_*—— 恰好包含本次真实漏项之一(REACT_CHART_DRILLDOWN_INVALID)。形状是每个 id 都有的性质,与它怎么旅行无关。悬空导出:诚实标注为「可见」而非主门禁。 反向用例(
no barrel export names a rule id that no rule file defines)看着像能抓改名残留,实际抓不到 —— 因为 barrel 再导出一个源模块已不存在的名字根本过不了 tsc。这一点是实测过的,不是推断:临时往 barrel 加一行FLOW_TRIGGER_RENAMED_GHOST后pnpm --filter @objectstack/lint typecheck报所以该用例的真实价值是抓仍能解析但已漂移的情况(barrel 行指向的模块与定义处不一致、或规则 id 形状的字符串从不再发射它的地方导出),测试注释里就是这么写的,没有夸大成改名门禁。
全量清查表(131 个规则 id 常量 / 47 个规则文件)
runtime.ts一列统一为「无」:该 entry 按设计只导出函数与类型(runRuntimeAuthoringRules等),不承载任何规则 id 常量 —— 与 issue 表格里两个已导出常量在 runtime 侧同为「无」一致。故下表状态即index.ts状态,FIXED= 本 PR 补入。清查中被正确排除的唯一非规则 id 字符串常量:
RELATED_LIST_TYPE = 'record:related_list'(validate-page-field-bindings.ts:183,page block 类型,:与_都不是 id 拼法)。NULL_GUARD_HINT是多行模板拼接,不是字面量常量,同样在外。#5651 / #5663 / #5695 今天新增的常量:验证过,自带导出。
FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID(#5651)与FLOW_MULTI_WRITE_UNFILTERED(#5663)在index.ts中各命中 1 次;#5695 未新增常量(只改了flow-runas-unscoped的证据搜索范围)。即近期车道确实各自补了 barrel 行,本次七处均为更早的存量漏项。反向验证(方向:红 —— 事前预判并命中)
事前预判:注掉本 PR 补的
FLOW_TRIGGER_UNKNOWN_EVENT导出行 → 可达性用例转红并点名该常量;其余三条(entry 集合、发现面下限、反向孤儿)保持绿,因为它们都不依赖这一行。实测完全吻合:恢复后 4/4 绿。
可达性实测(证明修复真的到消费者手上)
从构建产物取值(ESM 与 CJS 双格式,并核对
.d.ts/.d.cts类型面各命中 1 次declare const):验证
pnpm --filter @objectstack/lint testTest Files 60 passed (60)/Tests 1383 passed, 4 skipped (1387)pnpm --filter @objectstack/lint typecheckpnpm --filter @objectstack/lint buildpnpm --filter @objectstack/cli testTest Files 83 passed (83)/Tests 825 passed (825)node scripts/check-nul-bytes.mjsOK (scanned 5614 tracked text file(s)),并对改动文件另跑控制字符自扫,干净eslint改动文件CLI 首跑曾出现 46 个文件红,原因是新 worktree 里 CLI 的工作区依赖未构建(
Failed to resolve entry for package "@objectstack/types"),补pnpm --workspace-concurrency=2 --filter '@objectstack/cli^...' build后 83/83 全绿 —— 与本改动无关,记录在此以免误读。范围
仅导出面,无行为变化:没有任何
f.rule字符串被改动,原先对字面量比对的消费者继续可用;新增的只是「也可以 import 常量」这条路。未碰content/docs/releases/。顺带说明一处未纳入本 PR 的工作区改动:构建依赖时
packages/spec/authorable-surface.base.json被gen:schema自动重锚(baseRev指向当前 merge base)。已git checkout --还原,不进本 PR。该现象已有在案 issue,故未另开新单:#5358(--check模式也会重写该文件)与变体 #5370。Generated by Claude Code