|
| 1 | +--- |
| 2 | +"@objectstack/lint": patch |
| 3 | +--- |
| 4 | + |
| 5 | +fix(lint): an object declaring no fields is unjudgeable, not "has no such field" (#4383) |
| 6 | + |
| 7 | +`hook-body-write-unknown-field` and `action-body-write-unknown-field` reported |
| 8 | +**every** field write to an object that declares no `fields` — an external |
| 9 | +object, or a datasource-introspected schema whose columns are resolved at |
| 10 | +runtime. Measured before the fix: |
| 11 | + |
| 12 | +``` |
| 13 | +hook : ["hook-body-write-unknown-field / warning"] ← false |
| 14 | +action: ["action-body-write-unknown-field / warning"] ← false |
| 15 | +flow : [] ← correct |
| 16 | +``` |
| 17 | + |
| 18 | +`indexObjectFields` returns an **empty Set** for such an object rather than |
| 19 | +`undefined`, and both rules only asked "is this object in the stack?" — |
| 20 | +`targetSets.every((s) => s !== undefined)` and `if (!known) continue`. An empty |
| 21 | +Set is neither undefined nor falsy, so it became the answer to `has(field)`, |
| 22 | +and the answer is always `false`. |
| 23 | + |
| 24 | +That field map is not empty, it is **unknown**. The distinction already existed |
| 25 | +in two other rules of the same family, each with its reason written down — |
| 26 | +`validate-searchable-fields` skip #2 and `validate-flow-node-writes` (#4369, |
| 27 | +which added the guard because it gates). Two of four had it; the drift shape |
| 28 | +#3583 and #4330 exist to remove. |
| 29 | + |
| 30 | +**Fixed once, not twice.** The guard now lives in a shared |
| 31 | +`judgeableFieldsOf(index, objectName)` that returns the declared names only when |
| 32 | +they are a sound basis for a "resolves to nothing" judgement, and `undefined` |
| 33 | +for both unjudgeable cases — cross-package objects and fields-less ones. All |
| 34 | +three write-set rules route their lookups through it, so a fourth cannot repeat |
| 35 | +the omission. It is internal to the family (not re-exported from the package |
| 36 | +barrel), same as `indexObjectFields` and `IMPLICIT_FIELDS`. |
| 37 | + |
| 38 | +One semantic call worth naming: a **multi-target** hook where only *some* |
| 39 | +targets are judgeable is now skipped entirely. The `ctx.input` finding fires |
| 40 | +only when a field is missing from EVERY target, and an unjudgeable target is one |
| 41 | +the field might well exist on — so judging the remainder would assert "missing |
| 42 | +everywhere" on evidence that does not cover everywhere. Consistent with the |
| 43 | +rule's stated asymmetry: prefer a missed finding to a false one. |
| 44 | + |
| 45 | +No behaviour change for objects that declare fields: an unknown field on a |
| 46 | +normal object still warns exactly as before, pinned by a test placed next to |
| 47 | +each new skip so the guard cannot swallow the real finding. |
0 commit comments