Skip to content

fix(lint): 零字段对象是「无法判断」而非「没有这个字段」(#4383) - #4385

Merged
os-zhuang merged 1 commit into
mainfrom
claude/lint-fieldless-object-skip
Jul 31, 2026
Merged

fix(lint): 零字段对象是「无法判断」而非「没有这个字段」(#4383)#4385
os-zhuang merged 1 commit into
mainfrom
claude/lint-fieldless-object-skip

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #4383

问题

hook-body-write-unknown-fieldaction-body-write-unknown-field 会把每一个写到「不声明 fields 的对象」的字段标成未知字段——external 对象、数据源自省 schema(列在运行时解析)都属于这一类。

修复前实测:

hook  : ["hook-body-write-unknown-field / warning"]     ← 误报
action: ["action-body-write-unknown-field / warning"]   ← 误报
flow  : []                                              ← 正确

根因

indexObjectFields 对这类对象返回的是空 Set,不是 undefined。而两条规则只问了「这个对象在不在 stack 里」:

targetSets.every((s) => s !== undefined)   // 空 Set 不是 undefined
if (!known) continue;                       // 空 Set 是 truthy

空 Set 既不是 undefined 也不是 falsy,于是它成了 has(field) 的答案——而这个答案恒为 false

那张字段表不是空的,是未知的

为什么算 bug 而不是保守取舍

同一家族里另外两条规则早就区分了这两者,而且各自写了理由:

四条里两条有、两条没有。正是 #3583 建套件、#4330 收拢 system-fields 在处理的漂移形状。而且它直接违反那两条规则自己头注释里的原则:「a false positive kills an advisory lint」。

修法:修一次,不是修两次

新增共享的 judgeableFieldsOf(index, objectName) —— 仅当声明的字段名足以支撑一个「这个名字解析不到任何东西」的判断时才返回它们,两种无法判断的情形(跨包对象、零字段对象)都返回 undefined。三条 write-set 规则的查表全部改走它,所以第四条规则不可能再漏掉同一道 guard。

它属于家族内部,从包 barrel 再导出,与 indexObjectFieldsIMPLICIT_FIELDS 一致。

一个需要点名的语义决定

多目标 hook 里只要有一个目标无法判断,现在整体跳过,而不是拿剩下的凑合判。

理由:ctx.input 的 finding 只在字段在每一个目标上都不存在时才报,而那个无法判断的目标恰恰是它可能存在的地方。只判断其余目标,等于用覆盖不到"每一个"的证据去断言"每一个都没有"。这与该规则自己声明的不对称取向一致:宁可漏报,不可误报。

验证

  • 修复后对同一组 repro:三条规则全部 [];
  • 回归护栏:正常对象上的未知字段仍然照报(normal: ["hook-body-write-unknown-field"]),每个新增 skip 旁边都配了一条这样的测试,防止 guard 把真 finding 一起吞掉;
  • @objectstack/lint 42 文件 / 665 测试全绿(新增 5 条),typecheck 干净。

🤖 Generated with Claude Code

…such field" (#4383)

`hook-body-write-unknown-field` and `action-body-write-unknown-field` reported
EVERY field write to an object that declares no `fields` — an external object,
or a datasource-introspected schema whose columns are resolved at runtime.

`indexObjectFields` returns an empty Set for such an object rather than
`undefined`, and both rules only asked "is this object in the stack?"
(`targetSets.every((s) => s !== undefined)`, `if (!known) continue`). An empty
Set is neither undefined nor falsy, so it became the answer to `has(field)` —
and that answer is always false.

The field map is not empty, it is UNKNOWN. Two other rules in the same family
already drew that distinction with their reasons written down
(validate-searchable-fields skip #2, and validate-flow-node-writes, which added
the guard in #4369 because it gates). Two of four had it: the drift shape #3583
and #4330 exist to remove.

Fixed once, not twice. The guard now lives in a shared
`judgeableFieldsOf(index, objectName)` returning the declared names only when
they are a sound basis for a "resolves to nothing" judgement, and `undefined`
for both unjudgeable cases. All three write-set rules route through it, so a
fourth cannot repeat the omission. Internal to the family — not re-exported from
the package barrel, same as `indexObjectFields` and `IMPLICIT_FIELDS`.

One semantic call worth naming: a multi-target hook where only SOME targets are
judgeable is now skipped entirely. The `ctx.input` finding fires only when a
field is missing from every target, and an unjudgeable target is one the field
might well exist on — judging the remainder would assert "missing everywhere" on
evidence that does not cover everywhere. Consistent with the rule's stated
asymmetry: prefer a missed finding to a false one.

No behaviour change for objects that declare fields; a test sits next to each
new skip so the guard cannot swallow the real finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 31, 2026 10:11am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

2 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit 7d80695 into main Jul 31, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/lint-fieldless-object-skip branch July 31, 2026 10:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[lint] 零字段对象(external / 数据源自省 schema)让 hook / action write-set 规则把每一个写都误报

1 participant