Repository navigation
Commit d2e7d6c
fix(cli): a renamed destructuring key off ctx is a key, not a free identifier (#22489)
Fixes #22459
Clause-②: no
The rule's published text negates this refusal: its own remedy line
reads "Inline the value(s) into the handler, or reach them through
`ctx`." (`packages/cli/src/lint/hook-body-lowering.ts:163`), and `const
{ previous: prev } = ctx` reaches the value through `ctx`. Removing a
misrejection that the rule's text already denies is `no`.
## What changed
The free-identifier scan behind `objectstack lint`'s
`hook-body/not-lowerable` rule and `objectstack build`'s hook lowering
(`packages/cli/src/utils/detect-free-identifiers.ts`) counted a renamed
destructuring key as a value reference. For a hook that destructures a
renamed key off `ctx`, such as `const { event, input, previous: prev } =
ctx`, it reported `previous` as free. The binding walk
(`collectBindings`) already bound the alias `prev`. The reference walk
(`collectReferences`) then met the element's `propertyName` as a plain
identifier. So lint refused a lowerable hook with advice describing what
it already did, and the build bundled it instead of lowering it.
`collectReferences` now handles a `BindingElement` the way it already
handles a `PropertyAssignment`. A non-computed `propertyName` is a key,
not a reference. The walk still visits a computed key's expression, the
binding name (an alias is subtracted by `bindings`; a nested pattern
recurses), and the default, because `{ [k]: v = d }` reads `k` and `d`.
It is one branch in the existing walker, with no second walker.
`extract-hook-body.ts`, the rule's message text and `packages/spec` are
untouched.
Changeset: `.changeset/22459-hook-lowering-destructuring-alias.md`,
`@objectstack/cli` `patch`. The remedy is: none needed, because a hook
written this way now lowers.
## Reproduction, red then green
- **Red.** The new pins, run against the unmodified `collectReferences`
(base `f66c440de`): `detect-free-identifiers.test.ts` has 8 failed and
37 passed (45). The failures read `expected [ 'previous' ] to deeply
equal []` (the card's A shape, an object pattern inside an array
pattern, a nested callback parameter, and the compiled `.toString()`
shape), `[ 'a', 'b' ]` (nested), `[ 'key' ]` (defaulted alias), `[
'message' ]` (catch clause), and `[ 'FALLBACK', 'key' ]` where the
control expects `[ 'FALLBACK' ]`.
- **Green** at `38fa4d085`: `detect-free-identifiers.test.ts` and
`hook-body-lowering.test.ts` give 63 passed (63).
- **Ablation** (one-shot, run from the committed fix). The new branch
was neutralised on disk with `scripts/ablation-replace.mjs`: anchor hit
1 time and went from 1 to 0, and the blob went `b768875ce125` to
`dcd006617c08`. Both suites then gave 9 failed and 54 passed (63). The
ninth failure is the lint/build pin: `expected [ { severity: 'error',
…(3) } ] to deeply equal []`. On restore, the blob matched HEAD
(`b768875ce125`) and `git diff HEAD` was empty.
Pins, as triage directed:
- the card's A shape (`{ event, input, previous: prev } = ctx`) lowers;
- nested (`{ a: { b: c } } = ctx`) and defaulted (`{ key: alias = d }`,
`d` in scope) shapes lower, and so does a computed key with an in-scope
default;
- control: a defaulted alias whose default is free still reports
`FALLBACK` (also the shorthand `{ previous = FALLBACK }`), and a free
computed key still reports `KEY`;
- control: a genuinely free `previous` (no `ctx` source) is still
reported, at the helper and at lint/build level.
## H2 and H3 (measured)
- **Array destructuring** (`const [first, second] = ctx.items`): no
false positive before the fix. It was green on base, because an array
element has no key. **Parameter destructuring**: the handler's own
top-level parameters never had it, because the reference walk does not
visit top-level parameter patterns (the existing `({ a: { b } }) => b +
1` pin was green on base). The same element shape inside the body DID
have it: `ctx.items.map(({ previous: prev }) => prev.n)`, `const [{
previous: prev }] = ctx.items`, and `catch ({ message: msg })` were all
red on base. The same rule covers them, and each is pinned.
- **End to end** (`hook-body-lowering.test.ts`): for a hook that
destructures a renamed key off `ctx`, `checkHookBodyLowering` returns
`[]`. `lowerCallables` records no extraction warning, `bodyExtracted` is
1, and the lowered `body.source` carries `previous: prev`. The control
hook reads a module-scope `previous`. It is still an `error` under
`hook-body/not-lowerable` at `hooks[0].handler`, and the build records
`['free-identifiers', ['previous']]` and lowers nothing. Under the
ablation the positive pin went red; the control stayed green both ways.
## Verification, at `38fa4d085`
- `pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2`: 272 files passed. 2 files failed on a prerequisite, not
a verdict ("packages/cli is not built (./dist/index.js is absent)"):
`published-subpath-hook-body.pin` and `published-subpath-console.pin`.
After `pnpm --filter @objectstack/cli build`, those two gave 29 passed
(29). Totals: 274 of 274 files, 4023 + 29 tests passed, 29 skipped. The
`integration` layer is declared to CI, because the diff touches no
integration-layer file and no spawn entry.
- `pnpm --filter @objectstack/cli typecheck`: exit 0. `tsc -p
tsconfig.json --listFilesOnly` includes both edited test files (count
2).
- Lint, as a proven narrowing rather than the full `pnpm lint`. (1)
Population, read from `eslint.config.mjs` through the ESLint API: none
of the 3 changed TS files is ignored. The changeset `.md` is outside
every `files` glob. (2) Count, from `--format json`: 3 files, 0 errors,
0 warnings. (3) Invariance: the config enables no type-aware linting
(each file's resolved `parserOptions` is `{ ecmaVersion, sourceType }`
only, with no `project`), and the diff touches no ESLint config, plugin
or baseline. So no untouched file's verdict can move.
- Gates: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` (no paths) derives 64 commands;
all 64 exit 0. Four of them (`check:dual-build-cjs-loads`, `check:i18n`,
`check:i18n-coverage`, `check:i18n-walk-parity`) first exited 3,
PREREQUISITE NOT MET, and exited 0 after the build. `--ran` reconciles:
64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.
## Acceptance notes
- **Observation, not filed (reach not measured).** Lowering drops the
handler's own parameter list. The runner wraps a hook body as `(async
(ctx) => { … })(ctx)`, and the scan treats the handler's top-level
parameters as bound. Measured at `38fa4d085`, a pre-existing path this
PR does not touch: `extractHookBody(({ input }) => { input.x = 1; })`
lowers to the source `input.x=1`, and `(c) => { c.input.x = 1; }` lowers
to `c.input.x=1`. `checkHookBodyLowering` reports 0 issues for both.
Relatedly, the reference walk never visits a top-level parameter
pattern's default: `detectFreeIdentifiers('({ x = FREE }) => x')` gives
`[]`, while the in-body spelling gives `['FREE']`. NOT MEASURED: whether
such a body throws when the runtime evaluates it. Carrier: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 35ef501 commit d2e7d6c
4 files changed
Lines changed: 147 additions & 2 deletions
File tree
- .changeset
- packages/cli/src
- lint
- utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
| 26 | + | |
25 | 27 | | |
26 | 28 | | |
27 | 29 | | |
| |||
139 | 141 | | |
140 | 142 | | |
141 | 143 | | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
142 | 194 | | |
143 | 195 | | |
144 | 196 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
78 | 137 | | |
79 | 138 | | |
80 | 139 | | |
| |||
83 | 142 | | |
84 | 143 | | |
85 | 144 | | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
86 | 154 | | |
87 | 155 | | |
88 | 156 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
227 | 227 | | |
228 | 228 | | |
229 | 229 | | |
230 | | - | |
231 | | - | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
232 | 233 | | |
233 | 234 | | |
234 | 235 | | |
| |||
253 | 254 | | |
254 | 255 | | |
255 | 256 | | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
256 | 270 | | |
257 | 271 | | |
258 | 272 | | |
| |||
0 commit comments