@opetope/lint 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/README.md +186 -8
- package/README.ru.md +187 -8
- package/dist/command-hooks.d.ts +34 -0
- package/dist/command-hooks.js +2 -0
- package/dist/command-hooks.js.map +1 -0
- package/dist/declaration-ingress.d.ts +8 -0
- package/dist/declaration-ingress.js +2 -0
- package/dist/declaration-ingress.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/model-bindings.d.ts +8 -2
- package/dist/model-bindings.js +1 -1
- package/dist/model-bindings.js.map +1 -1
- package/dist/rules/no-command-in-deps.d.ts +6 -0
- package/dist/rules/no-command-in-deps.js +2 -0
- package/dist/rules/no-command-in-deps.js.map +1 -0
- package/dist/rules/no-snapshot-in-update.d.ts +6 -0
- package/dist/rules/no-snapshot-in-update.js +2 -0
- package/dist/rules/no-snapshot-in-update.js.map +1 -0
- package/dist/rules/no-subscribe-outside-models.js +1 -1
- package/dist/rules/no-subscribe-outside-models.js.map +1 -1
- package/dist/snapshot-updates.d.ts +39 -0
- package/dist/snapshot-updates.js +2 -0
- package/dist/snapshot-updates.js.map +1 -0
- package/oxlintrc.json +16 -0
- package/package.json +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,123 @@
|
|
|
1
1
|
# @opetope/lint
|
|
2
2
|
|
|
3
|
+
## 0.9.2
|
|
4
|
+
|
|
5
|
+
No changes in this release.
|
|
6
|
+
|
|
7
|
+
## 0.9.1
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- d7f346a: `no-subscribe-outside-models` leaves the ingress of a declared node alone, `@opetope/lint` ships an Oxlint config
|
|
12
|
+
fragment, and `combineAbortSignals` becomes host vocabulary on `@opetope/runtime` (D299).
|
|
13
|
+
|
|
14
|
+
**The rule.** `x.subscribe(...)` written lexically inside a callback where a declared node opens its work and owns
|
|
15
|
+
the release of it is no longer reported: the disposer that callback returns belongs to the node and is drained with
|
|
16
|
+
the generation that opened it. Four callbacks qualify, and each returns `Awaitable<Disposer | void>` — `connect` of
|
|
17
|
+
a `stream` (third argument), `subscribe` of an `event` (second), the `open` of
|
|
18
|
+
`scope.while`/`scope.switch`/`scope.keyed` (second), and the `open` member of an `attach`. This was a false
|
|
19
|
+
positive under this package's own `configs.layers` — a feature's `own.stream` lives in the integration layer, where
|
|
20
|
+
the rule is `error`, and it was reported for exactly the shape its own message prescribes. The position is the
|
|
21
|
+
contract, so `attach`'s `close`, and `consume`, `run`, `load`, `when` and `key`, keep the report; so does a node
|
|
22
|
+
whose origin the rule cannot prove. The context resolves by import: the `own` section written right where a
|
|
23
|
+
`defineFeature`/`defineFeature.body` from `@opetope/runtime` receives it, and a model factory — the second argument
|
|
24
|
+
of `model(Declaration, factory)` on that `own`, or a function whose first parameter is annotated `ModelContext`
|
|
25
|
+
from `@opetope/core`. Both anchors take the package name exactly, unlike `no-command-in-deps`, because a check that
|
|
26
|
+
uses an import to stay silent must not accept a host's own `./my-own-context`. `own.stream(...)` and a destructured
|
|
27
|
+
`stream(...)` read alike, alias included, as do `own.scope.while(...)` and `scope.while(...)`. There is no fix to
|
|
28
|
+
write: the change removes reports.
|
|
29
|
+
|
|
30
|
+
**The fragment.** `@opetope/lint/oxlintrc.json` is generated from `configs.recommended.rules` and shipped in the
|
|
31
|
+
package, so an Oxlint host extends the list instead of transcribing it:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"extends": ["./node_modules/@opetope/lint/oxlintrc.json"],
|
|
36
|
+
"jsPlugins": [{ "name": "opetope", "specifier": "./node_modules/@opetope/lint/dist/index.js" }]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`extends` takes a file path, not a package specifier, which is why the fragment sits at the root of the package.
|
|
41
|
+
It carries rules only: Oxlint merges a `jsPlugins` entry through `extends` too, but a host that declares the plugin
|
|
42
|
+
itself would then register the name `opetope` twice, and that fails the configuration. Configurations merge first
|
|
43
|
+
to last, so rules a project wants stricter go after `extends`. `configs.recommended.rules` is also documented as a
|
|
44
|
+
readable table in the package README.
|
|
45
|
+
|
|
46
|
+
**The export.** `combineAbortSignals(signals)` and its result type `CombinedAbortSignalHandle` are published on
|
|
47
|
+
`@opetope/runtime`. It answers `{ signal, dispose }`: `signal` aborts with the exact reason of whichever input
|
|
48
|
+
aborts first, including one already aborted at the call, and `dispose` releases the listeners it took and is
|
|
49
|
+
idempotent. The name says handle because that is what it is — a handle over a signal, not a signal. The published
|
|
50
|
+
browser floor has no `AbortSignal.any`. The public surface moves to 39 values and 38 types against targets of 40
|
|
51
|
+
and 60; the size consumers are unchanged to the byte, because the symbol already reaches them from a module those
|
|
52
|
+
consumers treat as external.
|
|
53
|
+
|
|
54
|
+
**Migrating a host.** The gain is that a subscription inside an ingress callback is legal on its own, not because
|
|
55
|
+
a directory override silences the rule. In the application this was measured on, three such subscriptions — inside
|
|
56
|
+
the `connect` of a `stream` in three models — are today covered only by that project's `off` override for its model
|
|
57
|
+
layer; after this release the same code is correct one directory up as well, and the override can narrow to what it
|
|
58
|
+
is actually for. Directives are a smaller story: the one `// oxlint-disable-next-line
|
|
59
|
+
opetope/no-subscribe-outside-models` in that application sits on a subscription inside a `new Promise` executor,
|
|
60
|
+
which is not an ingress, is still reported, and keeps its directive until `refresh()` returns a promise (D297). A
|
|
61
|
+
hand-written `opetope/*` block in `.oxlintrc.json` is replaced by the two lines above, leaving only the rules the
|
|
62
|
+
project turns on for its own directories — for this package's own layers that is `layer-placement`,
|
|
63
|
+
`no-subscribe-outside-models` and `prefer-model-selection`. A local copy of a combined-signal helper — the one an
|
|
64
|
+
application kept because `@opetope/core/internal` is closed to it — is deleted in favour of the import from
|
|
65
|
+
`@opetope/runtime`.
|
|
66
|
+
|
|
67
|
+
- d7f346a: `opetope/no-command-in-deps` keeps a command hook out of a React dependency array.
|
|
68
|
+
|
|
69
|
+
A hook read from `useCommand`, from a key of `useCommands` or from a `Call` field of a `useModel` selection is a
|
|
70
|
+
snapshot of the render it was read in: the object is rebuilt on every `inFlight` and `outcome` change, while `run`
|
|
71
|
+
keeps one identity for the life of the binding. An application that wrote
|
|
72
|
+
`useEffect(() => { load.run(); return () => { unload.run(); }; }, [load, unload])` therefore started the command,
|
|
73
|
+
saw the status flip, saw the dependency change, ran the cleanup and ran the effect again — an infinite render loop.
|
|
74
|
+
The same application kept eight `useCallback(() => void submit.run('buy'), [submit])`, which looped over nothing but
|
|
75
|
+
voided their memo on each status of the command. Both shapes compile and run, and say something other than what
|
|
76
|
+
their author meant, which is what this package is for (D290).
|
|
77
|
+
|
|
78
|
+
The rule reads the dependency array of React's `useEffect`, `useLayoutEffect`, `useInsertionEffect`, `useCallback`,
|
|
79
|
+
`useMemo` and `useImperativeHandle`, and reports an element that is the hook binding taken whole; `x.run` and the
|
|
80
|
+
statuses `x.inFlight` and `x.outcome` are members of that object and stay. The record itself carries its own report:
|
|
81
|
+
`useCommands` builds a new object on every render, and so does `useModel` with a selection, so `[hooks]` and
|
|
82
|
+
`[order]` change on every render rather than on every status.
|
|
83
|
+
|
|
84
|
+
A callback that reaches the binding through a single `run` is fixed to that path — past an `as`, a `satisfies` or a
|
|
85
|
+
`!`, and without doubling a path the array already names. A callback that reads more than one path gets one
|
|
86
|
+
suggestion per path, statuses before `run`; a callback the rule cannot read through — written elsewhere, keeping
|
|
87
|
+
the object itself, taking it apart, or reading a member outside those three — gets the report and no suggestion,
|
|
88
|
+
because a suggestion is applied unattended under Oxlint's `--fix-suggestions`. Names are resolved by import and not
|
|
89
|
+
by spelling, a local alias included; a `useModel` selection field is a hook only where the same code proves it by
|
|
90
|
+
reading `run` on it, and a field read as data is of unknown kind and is left alone. The rule is `error` in
|
|
91
|
+
`configs.recommended`.
|
|
92
|
+
|
|
93
|
+
- d7f346a: `opetope/no-snapshot-in-update` keeps a snapshot read out of the value of the write that replaces it.
|
|
94
|
+
|
|
95
|
+
`update(sessions, { items: sessions.getSnapshot().items, kind: 'loading' })` reads the state it is about to
|
|
96
|
+
replace. The argument is computed where it is written and the write happens after it, so the value is built from
|
|
97
|
+
what the state held at one moment and lands on what it holds at another. After D288 the same shape has a second
|
|
98
|
+
fault: a write through a context whose signal is aborted is dropped silently and its updater is never called, but
|
|
99
|
+
an argument is already computed by then — and the cells of a closed model are closed, so `getSnapshot()` throws
|
|
100
|
+
`ReadableError('closed')` and a silently dropped write becomes an exception in the author's body. An application
|
|
101
|
+
wrote the shape nine times, eight of them `loading`/`failed` transitions that keep what is already loaded (D295).
|
|
102
|
+
|
|
103
|
+
The rule reads a call of `update` — by that name or through a context — with exactly two arguments, whose first
|
|
104
|
+
argument names a state and whose value reads `getSnapshot()` on that same state, written into the value or through
|
|
105
|
+
a name of the same function that holds the read: `const current = s.getSnapshot()`,
|
|
106
|
+
`const { items } = s.getSnapshot()`, or a `let` nothing writes again. A snapshot of another readable inside the
|
|
107
|
+
value is a read of another state and stays, as do a value that is already an updater and a second argument that is
|
|
108
|
+
a spread, whose arguments are assembled somewhere else. The scalar form counts too:
|
|
109
|
+
`update(total, total.getSnapshot() + amount)` is the same write.
|
|
110
|
+
|
|
111
|
+
The fix writes the updater: `update(sessions, previous => ({ ...previous, kind: 'loading' }))`, with every read of
|
|
112
|
+
that state replaced by the parameter — a destructured field by the field of it — and a `const` that held the read
|
|
113
|
+
removed, comment and all, where this value was its only reader. The parameter is `previous`, or `snapshot` where an
|
|
114
|
+
enclosing scope already holds `previous`. Where the parameter would shadow a name the file has, where the named
|
|
115
|
+
read has another reader, and wherever the read was taken apart or opened with a `let`, the same rewrite arrives as
|
|
116
|
+
a suggestion; a value that cannot become the body of an updater at all — one that writes an `await` or a `yield`
|
|
117
|
+
there, assigns, increments, `delete`s, or keeps a read for a later call — gets the report and nothing else, because
|
|
118
|
+
Oxlint applies the first suggestion unattended. Everything else the value called travels into the updater with it,
|
|
119
|
+
so it runs at write time and not at all for a dropped write. The rule is `error` in `configs.recommended`.
|
|
120
|
+
|
|
3
121
|
## 0.9.0
|
|
4
122
|
|
|
5
123
|
No changes in this release.
|
package/README.md
CHANGED
|
@@ -43,9 +43,30 @@ both it and `eslint` are peer dependencies.
|
|
|
43
43
|
### `recommended`
|
|
44
44
|
|
|
45
45
|
Every rule that holds wherever Opetope is written: `when-predicate`, `define-feature-property-order`,
|
|
46
|
-
`require-literal-id`, `id-naming`, `no-internal-imports`, `no-snapshot-read-in-render
|
|
47
|
-
|
|
48
|
-
of hooks a component may keep is a taste a
|
|
46
|
+
`require-literal-id`, `id-naming`, `no-internal-imports`, `no-snapshot-read-in-render`, `no-snapshot-in-update`,
|
|
47
|
+
`no-command-in-deps` and `require-declared-models` as errors. A rule that needs to know where a host keeps its files is off here and arrives
|
|
48
|
+
through `layers`; `prefer-model-selection` is off because the number of hooks a component may keep is a taste a
|
|
49
|
+
project settles for itself.
|
|
50
|
+
|
|
51
|
+
`configs.recommended.rules` in full, which is also what `@opetope/lint/oxlintrc.json` ships:
|
|
52
|
+
|
|
53
|
+
| Rule | `recommended` | Turned on by |
|
|
54
|
+
| ------------------------------- | ------------- | ------------------------------------------- |
|
|
55
|
+
| `define-feature-property-order` | `error` | |
|
|
56
|
+
| `id-naming` | `error` | |
|
|
57
|
+
| `layer-placement` | `off` | `layers({ integration, models, ui })` |
|
|
58
|
+
| `no-command-in-deps` | `error` | |
|
|
59
|
+
| `no-internal-imports` | `error` | also scoped by `internalImports({ allow })` |
|
|
60
|
+
| `no-snapshot-in-update` | `error` | |
|
|
61
|
+
| `no-snapshot-read-in-render` | `error` | |
|
|
62
|
+
| `no-subscribe-outside-models` | `off` | `layers({ models })` |
|
|
63
|
+
| `prefer-model-selection` | `off` | the project, with its own `threshold` |
|
|
64
|
+
| `require-declared-models` | `error` | |
|
|
65
|
+
| `require-literal-id` | `error` | |
|
|
66
|
+
| `when-predicate` | `error` | |
|
|
67
|
+
|
|
68
|
+
The three `off` rules are the three that need something only the project knows: which directories are which layer,
|
|
69
|
+
and how many hooks one component may keep.
|
|
49
70
|
|
|
50
71
|
### `layers`
|
|
51
72
|
|
|
@@ -244,6 +265,95 @@ stays: those run after render, and the value of that moment is the one they want
|
|
|
244
265
|
reported, whichever object it belongs to. A capitalized function that returns no element is a factory and keeps its
|
|
245
266
|
reads — including a contribution's model factory, which reads the props of its own mount.
|
|
246
267
|
|
|
268
|
+
### `no-snapshot-in-update`
|
|
269
|
+
|
|
270
|
+
A value passed to `update` reads the state it is about to replace — a composite one,
|
|
271
|
+
`update(sessions, { items: sessions.getSnapshot().items, kind: 'loading' })`, and a scalar one,
|
|
272
|
+
`update(total, total.getSnapshot() + amount)`, alike. The read runs where the argument is written — before the
|
|
273
|
+
write, and on a state that may already be closed, where `getSnapshot()` throws a `ReadableError` — and the value it
|
|
274
|
+
computed then lands on a state that may have moved since. The updater form has neither problem: it reads at write
|
|
275
|
+
time, and a write dropped because its context was aborted never calls the updater at all (D282, D288, D295).
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
update(sessions, previous => ({ ...previous, kind: 'loading' }));
|
|
279
|
+
update(total, previous => previous + amount);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The rule reports a call of `update` — by that name or through a context, `ctx.update` — with exactly two arguments,
|
|
283
|
+
whose first argument names a state and whose value reads `getSnapshot()` on that same state: written into the value
|
|
284
|
+
itself, or through a name of the same function that holds the read — `const current = s.getSnapshot()`,
|
|
285
|
+
`const { items } = s.getSnapshot()`, or a `let` nothing writes again. A `getSnapshot()` of another readable inside
|
|
286
|
+
the value is a read of another state and stays; so do a value that is already an updater, a read written anywhere
|
|
287
|
+
but in that value, and a second argument that is a spread, whose arguments are assembled somewhere else.
|
|
288
|
+
|
|
289
|
+
The fix writes the updater: the value becomes `previous => ...` with every read of that state replaced by the
|
|
290
|
+
parameter — a destructured field by the field of it, `previous.items` — and the `const` that held the read goes with
|
|
291
|
+
it, comment and all, where this value was its only reader. The parameter is named `previous`, or `snapshot` where an
|
|
292
|
+
enclosing scope already holds `previous`. Where the parameter would shadow a name the file has, where the named read
|
|
293
|
+
has another reader that stays behind, and wherever the read was taken apart or opened with a `let`, the same rewrite
|
|
294
|
+
arrives as a suggestion instead, because those are the rewrites a reader should look at. A value that cannot move
|
|
295
|
+
into a function body at all — one that writes an `await` or a `yield` there, assigns, increments or `delete`s —
|
|
296
|
+
gets the report and nothing else, and so does a read this value keeps for a later call.
|
|
297
|
+
|
|
298
|
+
**The boundary.** `update(S, ...S.getSnapshot()...)` is specific enough on its own, so the rule resolves no import
|
|
299
|
+
and reads no type: a host's own `update` of the same shape is reported too, and the fix is right for it only if it
|
|
300
|
+
accepts an updater as its second argument. A name is the binding it resolves to, but a member expression is the
|
|
301
|
+
path it spells, so two different objects written `this.state` are one state here. The rewrite moves the whole value
|
|
302
|
+
into a function body, so everything else the value called — `Date.now()`, a helper, a formatter — is computed at
|
|
303
|
+
write time instead of where it was written, and is not computed at all for a write the runtime drops; that is what
|
|
304
|
+
the updater form means, and it is worth reading once before the rewrite lands. A read the value reaches by any other
|
|
305
|
+
route — a helper that takes the state and reads it, a name declared in another function, a name the code writes
|
|
306
|
+
again, a value that is already an updater — is not seen, and silence is not proof that a value computed itself from
|
|
307
|
+
the previous one.
|
|
308
|
+
|
|
309
|
+
### `no-command-in-deps`
|
|
310
|
+
|
|
311
|
+
A command hook is a snapshot of the render it was read in: the object is rebuilt on every `inFlight` and `outcome`
|
|
312
|
+
change, while `run` keeps one identity for the life of the binding (D290). A dependency array that names the object
|
|
313
|
+
therefore fires on each of those changes: an effect re-runs, a memo is voided, and an effect that calls `run` from
|
|
314
|
+
it never settles — it starts the command, the status flips, the dependency changes, the cleanup runs, and the
|
|
315
|
+
effect runs again.
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
const load = useCommand(Screen.load);
|
|
319
|
+
const unload = useCommand(Screen.unload);
|
|
320
|
+
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
load.run();
|
|
323
|
+
return () => void unload.run();
|
|
324
|
+
}, [load.run, unload.run]);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The rule reads the dependency array of `useEffect`, `useLayoutEffect`, `useInsertionEffect`, `useCallback`,
|
|
328
|
+
`useMemo` and `useImperativeHandle`, and reports an element that is a command hook binding taken whole: the
|
|
329
|
+
variable of a `useCommand`, a key or a destructured name of `useCommands`, and a field of a `useModel` selection
|
|
330
|
+
the same code reads `run` on. `x.run` and the statuses `x.inFlight` and `x.outcome` are members of that object rather
|
|
331
|
+
than the object, and they stay.
|
|
332
|
+
|
|
333
|
+
The record itself is the worse shape and carries its own report: `useCommands` builds a new object on every render,
|
|
334
|
+
and so does `useModel` with a selection, so `[hooks]` and `[order]` change on every render rather than on every
|
|
335
|
+
status. The answer is the field the callback reads — `hooks.save.run`.
|
|
336
|
+
|
|
337
|
+
The fix is written where the answer is one: a callback that reaches the binding through a single `run` gets that
|
|
338
|
+
path in place of the element, whatever the element was written past — an `as`, a `satisfies` or a `!` is replaced
|
|
339
|
+
along with it, and an array that already names the path loses the element instead of doubling it. Where the callback
|
|
340
|
+
reads more than one path, or reads a status, the rule offers suggestions instead — one per path, statuses first —
|
|
341
|
+
because which of them the dependency means is the author's to say. A callback this rule cannot read through — one
|
|
342
|
+
written elsewhere, one that keeps the object itself, takes it apart, or reads a member outside those four — gets the
|
|
343
|
+
report and no suggestion at all.
|
|
344
|
+
|
|
345
|
+
**The boundary.** Names are resolved by import within one module, not by spelling: `useCommand`, `useCommands` and
|
|
346
|
+
`useModel` are the imports of `@opetope/react`, the six hooks are React's own, and a local alias of either —
|
|
347
|
+
`import { useEffect as effect }` — is the same import. React is the module named `react` exactly; an `@opetope/*`
|
|
348
|
+
name is also taken from a relative import, which is how a package re-exports its own entry through a barrel, so a
|
|
349
|
+
neighbouring `./scheduling` is React's no more than `@host/scheduling` is. React's hooks are read through a
|
|
350
|
+
namespace or a default binding as well (`React.useEffect`); an `@opetope/*` entry has no default export, and only a
|
|
351
|
+
namespace reaches it. Only a `const` holds the hook it was opened with, so a `let` is left alone. A selected
|
|
352
|
+
`useModel` field is a hook only where the code proves it by reading `run` on it — a status name proves nothing,
|
|
353
|
+
since plain data carries one as easily — and a field read as data is of unknown kind and is left alone, as is
|
|
354
|
+
`useModel(Declaration)` without a selection, which keeps returning the granted model. A dependency array of a call
|
|
355
|
+
that is not one of the six hooks belongs to whoever declared it.
|
|
356
|
+
|
|
247
357
|
### `no-subscribe-outside-models`
|
|
248
358
|
|
|
249
359
|
A subscription written by hand owns a cleanup that nothing around it can see. The rule reports `x.subscribe(...)`
|
|
@@ -253,6 +363,47 @@ is `useReadable` or a model selection, and in a feature or a model it is `effect
|
|
|
253
363
|
Handing the function over without calling it is not a subscription written here:
|
|
254
364
|
`useSyncExternalStore(source.subscribe, source.getSnapshot)` passes a reference, and the reader owns what it starts.
|
|
255
365
|
|
|
366
|
+
Neither is a subscription written where a declared node opens its work and owns the release of it. The disposer
|
|
367
|
+
such a callback returns belongs to that node and is drained with the generation that opened it, so the rule stays
|
|
368
|
+
silent there — this is the shape its own message asks for (D299):
|
|
369
|
+
|
|
370
|
+
| Callback | Where | Ingress |
|
|
371
|
+
| ---------------------------------------------- | --------------------------- | ------- |
|
|
372
|
+
| `stream(from, target, connect)` | third argument | yes |
|
|
373
|
+
| `event(from, subscribe, run)` | second argument | yes |
|
|
374
|
+
| `scope.while` / `scope.switch` / `scope.keyed` | `open`, second argument | yes |
|
|
375
|
+
| `attach(source, { open })` | the `open` member | yes |
|
|
376
|
+
| `attach(source, { open, close })` | the `close` member | no |
|
|
377
|
+
| `consume`, `run`, `load`, `when`, `key` | modifiers of the same calls | no |
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
own: ({ imports, stream }) => ({
|
|
381
|
+
book: stream(
|
|
382
|
+
imports.exchange,
|
|
383
|
+
() => 'BTCUSD',
|
|
384
|
+
async ({ emit, signal }) => {
|
|
385
|
+
return imports.exchange.repository.subscribe('BTCUSD', emit, signal);
|
|
386
|
+
},
|
|
387
|
+
),
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Inside counts all the way down — a nested arrow, a `function` body, an `await` on the subscription itself, a
|
|
392
|
+
disposer named before it is returned. The context is resolved by import, not by spelling: the `own` section written
|
|
393
|
+
right where a `defineFeature`/`defineFeature.body` **taken from `@opetope/runtime` itself** receives it, and a model
|
|
394
|
+
factory — the second argument of `model(Declaration, factory)` on that same `own`, or a function whose first
|
|
395
|
+
parameter is annotated `ModelContext` from `@opetope/core`. Unlike `no-command-in-deps`, these two anchors take the
|
|
396
|
+
package name and nothing else: a relative import here would let a host's own `./my-own-context` silence the rule.
|
|
397
|
+
`own.stream(...)` and a destructured `stream(...)` read alike, an alias answers with the key it was taken from
|
|
398
|
+
(`{ stream: openStream }` is still `stream`), and `own.scope.while(...)` reads like the destructured
|
|
399
|
+
`scope.while(...)`.
|
|
400
|
+
|
|
401
|
+
Four shapes therefore keep the report, and each is a limit of what syntax proves: a `stream` traced to none of
|
|
402
|
+
those anchors (imported from elsewhere, unresolvable, or belonging to another `defineFeature`); a callback written
|
|
403
|
+
elsewhere and passed in by name, which is not lexically inside; an `own` section hoisted to its own binding
|
|
404
|
+
(`const own = ({ stream }) => …; defineFeature(id, { own })`); and `model(Decl, createOrderModel)` where the factory
|
|
405
|
+
is a named function without the `ModelContext` annotation.
|
|
406
|
+
|
|
256
407
|
### `prefer-model-selection`
|
|
257
408
|
|
|
258
409
|
A component that takes one granted model and reads its fields through a hook each repeats the same wiring. From
|
|
@@ -328,16 +479,43 @@ linters run them, autofix included:
|
|
|
328
479
|
|
|
329
480
|
```json
|
|
330
481
|
{
|
|
331
|
-
"
|
|
332
|
-
"
|
|
482
|
+
"extends": ["./node_modules/@opetope/lint/oxlintrc.json"],
|
|
483
|
+
"jsPlugins": [{ "name": "opetope", "specifier": "./node_modules/@opetope/lint/dist/index.js" }]
|
|
333
484
|
}
|
|
334
485
|
```
|
|
335
486
|
|
|
487
|
+
Those are the two lines, and neither of them is a transcription. `@opetope/lint/oxlintrc.json` is generated from
|
|
488
|
+
`configs.recommended.rules` and shipped in the package, so a rule the package adds arrives with the upgrade instead
|
|
489
|
+
of being missed by a hand-written severity map. `extends` in `.oxlintrc.json` takes a **file path**, resolved
|
|
490
|
+
relative to the config that names it — not a package specifier — which is why the fragment sits at the root of the
|
|
491
|
+
package and the path a host writes is the same string as its export name. Configurations merge first to last, so a
|
|
492
|
+
rule the project wants stricter goes in its own `rules` after `extends`: the three `off` entries above are exactly
|
|
493
|
+
the ones a project turns on by its own directories.
|
|
494
|
+
|
|
495
|
+
The fragment carries rules and nothing else. Oxlint does merge a `jsPlugins` entry through `extends`, but a host
|
|
496
|
+
that declares the plugin itself — under a wrapper package, as an isolated peer install needs — would then register
|
|
497
|
+
the name `opetope` twice, and the second registration fails the whole configuration. Where the plugin comes from is
|
|
498
|
+
the host's deployment; the severities are this package's contract.
|
|
499
|
+
|
|
336
500
|
The alias `name` fixes the namespace, so a rule keeps the same id under either linter. `jsPlugins` is alpha and
|
|
337
|
-
outside semver: `npm run ci:test` runs the built plugin under Oxlint on a valid and an invalid fixture
|
|
338
|
-
the fix it writes, so a break in that bridge fails here
|
|
501
|
+
outside semver: `npm run ci:test` runs the built plugin under Oxlint on a valid and an invalid fixture, through a
|
|
502
|
+
config that extends the shipped fragment, and checks the fix it writes, so a break in that bridge fails here
|
|
503
|
+
instead of in a host.
|
|
504
|
+
|
|
505
|
+
`--fix-suggestions` applies the first suggestion of a report without asking, so a suggestion here is only ever a
|
|
506
|
+
rewrite the reported code already justifies. `no-command-in-deps` offers the paths the callback itself reads, the
|
|
507
|
+
status it watches before the `run` it calls; where it cannot read the use through — the object passed on, taken
|
|
508
|
+
apart, or read past the members the rule knows — it reports and offers nothing, and the dependency stays as written
|
|
509
|
+
until its author changes it.
|
|
510
|
+
|
|
511
|
+
`no-snapshot-in-update` holds the same line from the other side: its suggestion is the fix it would not write
|
|
512
|
+
unasked — a parameter that shadows a name the file already has, or a named read that stays behind for its other
|
|
513
|
+
reader — and both rewrites leave the code doing what it did. Where the value cannot become the body of an updater
|
|
514
|
+
at all, there is no rewrite to offer and the report stands alone.
|
|
339
515
|
|
|
340
516
|
## Checks
|
|
341
517
|
|
|
342
518
|
From this package: `npm run ci:test`, `npm run ci:type`, `npm run ci:eslint`, `npm run build`. The Oxlint bridge
|
|
343
|
-
test reads `dist`, so `npm run build` comes first.
|
|
519
|
+
test reads `dist`, so `npm run build` comes first. `ci:test` also checks that the committed `oxlintrc.json` still
|
|
520
|
+
equals `configs.recommended.rules`; after changing that config run `npm run oxlintrc:generate` and commit the
|
|
521
|
+
result, which is why the build never writes it.
|
package/README.ru.md
CHANGED
|
@@ -43,9 +43,30 @@ export default [
|
|
|
43
43
|
### `recommended`
|
|
44
44
|
|
|
45
45
|
Все правила, которые действуют везде, где пишут на Opetope: `when-predicate`, `define-feature-property-order`,
|
|
46
|
-
`require-literal-id`, `id-naming`, `no-internal-imports`, `no-snapshot-read-in-render
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
`require-literal-id`, `id-naming`, `no-internal-imports`, `no-snapshot-read-in-render`, `no-snapshot-in-update`,
|
|
47
|
+
`no-command-in-deps` и `require-declared-models` как error. Правило, которому нужно знать, где хост держит свои файлы, здесь выключено и
|
|
48
|
+
приходит через `layers`; `prefer-model-selection` выключено потому, что число hooks, которое компонент вправе
|
|
49
|
+
держать, каждый проект решает для себя.
|
|
50
|
+
|
|
51
|
+
`configs.recommended.rules` целиком — и ровно это поставляется как `@opetope/lint/oxlintrc.json`:
|
|
52
|
+
|
|
53
|
+
| Правило | `recommended` | Включает |
|
|
54
|
+
| ------------------------------- | ------------- | ----------------------------------------------- |
|
|
55
|
+
| `define-feature-property-order` | `error` | |
|
|
56
|
+
| `id-naming` | `error` | |
|
|
57
|
+
| `layer-placement` | `off` | `layers({ integration, models, ui })` |
|
|
58
|
+
| `no-command-in-deps` | `error` | |
|
|
59
|
+
| `no-internal-imports` | `error` | плюс сужение через `internalImports({ allow })` |
|
|
60
|
+
| `no-snapshot-in-update` | `error` | |
|
|
61
|
+
| `no-snapshot-read-in-render` | `error` | |
|
|
62
|
+
| `no-subscribe-outside-models` | `off` | `layers({ models })` |
|
|
63
|
+
| `prefer-model-selection` | `off` | проект, со своим `threshold` |
|
|
64
|
+
| `require-declared-models` | `error` | |
|
|
65
|
+
| `require-literal-id` | `error` | |
|
|
66
|
+
| `when-predicate` | `error` | |
|
|
67
|
+
|
|
68
|
+
Три `off` — это ровно те три, которым нужно знание, которое есть только у проекта: какие каталоги каким слоем
|
|
69
|
+
являются и сколько hooks вправе держать один компонент.
|
|
49
70
|
|
|
50
71
|
### `layers`
|
|
51
72
|
|
|
@@ -246,6 +267,96 @@ Id начинается с буквы и соединяет сегменты и
|
|
|
246
267
|
какому бы объекту он ни принадлежал. Функция с заглавной буквы, не возвращающая элементов, — это фабрика, и её
|
|
247
268
|
чтения остаются, включая фабрику модели вклада, которая читает props своего монтирования.
|
|
248
269
|
|
|
270
|
+
### `no-snapshot-in-update`
|
|
271
|
+
|
|
272
|
+
Значение, переданное в `update`, читает то состояние, которое собирается заменить, — и составное,
|
|
273
|
+
`update(sessions, { items: sessions.getSnapshot().items, kind: 'loading' })`, и скалярное,
|
|
274
|
+
`update(total, total.getSnapshot() + amount)`. Чтение выполняется там, где написан аргумент, — до записи и на
|
|
275
|
+
состоянии, которое может быть уже закрыто, где `getSnapshot()` бросает `ReadableError`, — а вычисленное значение
|
|
276
|
+
ложится на состояние, которое с тех пор могло уйти вперёд. У формы с апдейтером нет ни одной из этих бед: она
|
|
277
|
+
читает в момент записи, а у записи, отброшенной вместе с отменённым контекстом, апдейтер не вызывается вовсе
|
|
278
|
+
(D282, D288, D295).
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
update(sessions, previous => ({ ...previous, kind: 'loading' }));
|
|
282
|
+
update(total, previous => previous + amount);
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Правило сообщает о вызове `update` — по этому имени или через контекст, `ctx.update`, — ровно с двумя аргументами,
|
|
286
|
+
у которого первым аргументом назван state, а значение читает `getSnapshot()` на нём же: написанный прямо в
|
|
287
|
+
значении или через имя той же функции, которое держит это чтение, — `const current = s.getSnapshot()`,
|
|
288
|
+
`const { items } = s.getSnapshot()` или `let`, который больше никто не переписывает. `getSnapshot()` другого
|
|
289
|
+
readable внутри значения — чтение другого состояния, и оно остаётся; остаются и значение, которое уже является
|
|
290
|
+
апдейтером, и чтение, написанное где угодно, кроме этого значения, и второй аргумент-spread, чьи аргументы
|
|
291
|
+
собраны в другом месте.
|
|
292
|
+
|
|
293
|
+
Фикс пишет апдейтер: значение становится `previous => ...`, где каждое чтение этого состояния заменено параметром —
|
|
294
|
+
а разобранное поле тем же полем параметра, `previous.items`, — и `const`, державший чтение, уходит вместе с ним, с
|
|
295
|
+
комментарием на той же строке, там, где это значение было его единственным читателем. Параметр называется
|
|
296
|
+
`previous`, а где `previous` уже занят объемлющей областью — `snapshot`. Там, где параметр затенил бы имя, которое
|
|
297
|
+
в файле есть, там, где у именованного чтения остаётся другой читатель, и везде, где чтение разобрано или открыто
|
|
298
|
+
через `let`, та же правка приходит suggestion'ом: это ровно те правки, на которые стоит посмотреть. Значение,
|
|
299
|
+
которое вообще нельзя перенести в тело функции — написанные в нём `await` или `yield`, присваивание, инкремент,
|
|
300
|
+
`delete`, — получает сообщение и ничего больше, и так же остаётся чтение, отложенное до более позднего вызова.
|
|
301
|
+
|
|
302
|
+
**Граница.** Форма `update(S, ...S.getSnapshot()...)` достаточно определённа сама по себе, поэтому правило не
|
|
303
|
+
разрешает импортов и не читает типов: собственный `update` хоста той же формы тоже получает сообщение, и фикс
|
|
304
|
+
верен для него только тогда, когда вторым аргументом он принимает апдейтер. Имя — это привязка, к которой оно
|
|
305
|
+
разрешается, а member expression — путь, который он пишет, поэтому два разных объекта, записанных как `this.state`,
|
|
306
|
+
здесь одно состояние. Правка переносит всё значение в тело функции, поэтому и всё остальное, что значение вызывало
|
|
307
|
+
— `Date.now()`, помощник, форматтер, — вычисляется в момент записи, а не там, где написано, и не вычисляется вовсе
|
|
308
|
+
для записи, которую рантайм отбросил; это и значит форма с апдейтером, и это стоит прочитать один раз до того, как
|
|
309
|
+
правка ляжет. Чтение, до которого значение дотягивается любым другим путём — помощник, который принимает state и
|
|
310
|
+
читает его сам, имя, объявленное в другой функции, имя, которое код переписывает, значение, уже являющееся
|
|
311
|
+
апдейтером, — не видно, и молчание правила не доказывает, что значение вычислено из предыдущего.
|
|
312
|
+
|
|
313
|
+
### `no-command-in-deps`
|
|
314
|
+
|
|
315
|
+
Хук команды — это снимок того рендера, в котором его прочитали: объект пересобирается на каждой смене `inFlight` и
|
|
316
|
+
`outcome`, а `run` держит одну идентичность всё время жизни привязки (D290). Массив зависимостей, который называет
|
|
317
|
+
этот объект, срабатывает поэтому на каждой такой смене: эффект перезапускается, memo обнуляется, а эффект, который
|
|
318
|
+
вызывает из него `run`, не останавливается никогда — он запускает команду, статус переключается, зависимость
|
|
319
|
+
меняется, выполняется уборка, и эффект запускается снова.
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
const load = useCommand(Screen.load);
|
|
323
|
+
const unload = useCommand(Screen.unload);
|
|
324
|
+
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
load.run();
|
|
327
|
+
return () => void unload.run();
|
|
328
|
+
}, [load.run, unload.run]);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Правило читает массив зависимостей `useEffect`, `useLayoutEffect`, `useInsertionEffect`, `useCallback`, `useMemo` и
|
|
332
|
+
`useImperativeHandle` и сообщает об элементе, который целиком является привязкой хука команды: о переменной
|
|
333
|
+
`useCommand`, о ключе или деструктурированном имени `useCommands` и о поле выбора `useModel`, на котором тот же код
|
|
334
|
+
читает `run`. `x.run` и статусы `x.inFlight` и `x.outcome` — это члены объекта, а не сам объект, и они остаются.
|
|
335
|
+
|
|
336
|
+
Сама запись — форма ещё хуже, и у неё собственное сообщение: `useCommands` строит новый объект на каждом рендере, и
|
|
337
|
+
то же делает `useModel` с выбором, поэтому `[hooks]` и `[order]` меняются на каждом рендере, а не на каждом
|
|
338
|
+
статусе. Ответ здесь — то поле, которое читает колбэк: `hooks.save.run`.
|
|
339
|
+
|
|
340
|
+
Фикс пишется там, где ответ один: колбэк, который дотягивается до привязки единственным путём через `run`, получает
|
|
341
|
+
на месте элемента этот путь — вместе со всем, через что элемент был написан: `as`, `satisfies` или `!` заменяются
|
|
342
|
+
вместе с ним, а массив, в котором этот путь уже есть, теряет элемент вместо того, чтобы удвоить его. Там, где
|
|
343
|
+
колбэк читает больше одного пути или читает статус, правило предлагает suggestions — по одной на путь, статусы
|
|
344
|
+
первыми, — потому что какой из них имеет в виду зависимость, говорит автор. Колбэк, сквозь который правило не
|
|
345
|
+
видит, — написанный в другом месте, держащий сам объект, разбирающий его или читающий член вне этих четырёх —
|
|
346
|
+
получает сообщение и ни одной suggestion.
|
|
347
|
+
|
|
348
|
+
**Граница.** Имена разрешаются импортом в пределах одного модуля, а не написанием: `useCommand`, `useCommands` и
|
|
349
|
+
`useModel` — это импорты `@opetope/react`, шесть хуков — собственные хуки React, а локальный алиас любого из них —
|
|
350
|
+
`import { useEffect as effect }` — это тот же импорт. React — это модуль, названный ровно `react`; имя `@opetope/*`
|
|
351
|
+
берётся ещё и из относительного импорта, потому что так пакет реэкспортирует собственный вход через barrel, и
|
|
352
|
+
соседний `./scheduling` — React не больше, чем `@host/scheduling`. Хуки React читаются и через namespace, и через
|
|
353
|
+
default-привязку (`React.useEffect`); у входа `@opetope/*` default-экспорта нет, и до него дотягивается только
|
|
354
|
+
namespace. Только `const` держит тот хук, которым его открыли, поэтому `let` остаётся в стороне. Выбранное поле
|
|
355
|
+
`useModel` — хук только там, где код доказал это чтением `run` на нём: имя статуса не доказывает ничего, такое имя
|
|
356
|
+
столь же легко носят обычные данные. Поле, прочитанное как данные, остаётся полем неизвестного рода, и так же
|
|
357
|
+
остаётся `useModel(Declaration)` без выбора, который по-прежнему возвращает выданную модель. Массив зависимостей
|
|
358
|
+
вызова, который не является одним из шести хуков, принадлежит тому, кто его объявил.
|
|
359
|
+
|
|
249
360
|
### `no-subscribe-outside-models`
|
|
250
361
|
|
|
251
362
|
Подписка, написанная руками, владеет уборкой, которой не видит ничто вокруг. Правило сообщает о `x.subscribe(...)`
|
|
@@ -255,6 +366,47 @@ Id начинается с буквы и соединяет сегменты и
|
|
|
255
366
|
Передача функции без вызова подпиской здесь не является:
|
|
256
367
|
`useSyncExternalStore(source.subscribe, source.getSnapshot)` передаёт ссылку, и читатель владеет тем, что начал.
|
|
257
368
|
|
|
369
|
+
Не является ею и подписка, написанная там, где объявленный узел открывает свою работу и сам её освобождает.
|
|
370
|
+
Disposer, который возвращает такой колбэк, принадлежит этому узлу и сливается вместе с поколением, которое его
|
|
371
|
+
открыло, — поэтому правило там молчит: это ровно та форма, которую предписывает его собственное сообщение (D299):
|
|
372
|
+
|
|
373
|
+
| Колбэк | Где | Ингресс |
|
|
374
|
+
| ---------------------------------------------- | --------------------------- | ------- |
|
|
375
|
+
| `stream(from, target, connect)` | третий аргумент | да |
|
|
376
|
+
| `event(from, subscribe, run)` | второй аргумент | да |
|
|
377
|
+
| `scope.while` / `scope.switch` / `scope.keyed` | `open`, второй аргумент | да |
|
|
378
|
+
| `attach(source, { open })` | член `open` | да |
|
|
379
|
+
| `attach(source, { open, close })` | член `close` | нет |
|
|
380
|
+
| `consume`, `run`, `load`, `when`, `key` | модификаторы тех же вызовов | нет |
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
own: ({ imports, stream }) => ({
|
|
384
|
+
book: stream(
|
|
385
|
+
imports.exchange,
|
|
386
|
+
() => 'BTCUSD',
|
|
387
|
+
async ({ emit, signal }) => {
|
|
388
|
+
return imports.exchange.repository.subscribe('BTCUSD', emit, signal);
|
|
389
|
+
},
|
|
390
|
+
),
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
«Внутри» считается до самого низа: вложенная стрелка, тело `function`, `await` на самой подписке, disposer,
|
|
395
|
+
которому сначала дали имя. Контекст разрешается импортом, а не написанием: секция `own`, написанная прямо там, где
|
|
396
|
+
её принимает `defineFeature`/`defineFeature.body`, взятый **из самого `@opetope/runtime`**, и фабрика модели —
|
|
397
|
+
второй аргумент `model(Declaration, factory)` на том же `own` либо функция, первый параметр которой аннотирован
|
|
398
|
+
`ModelContext` из `@opetope/core`. В отличие от `no-command-in-deps`, эти два якоря принимают только имя пакета:
|
|
399
|
+
относительный импорт здесь позволил бы собственному `./my-own-context` хоста заглушить правило.
|
|
400
|
+
`own.stream(...)` и деструктурированный `stream(...)` читаются одинаково, алиас отвечает тем ключом, из которого его
|
|
401
|
+
достали (`{ stream: openStream }` — это по-прежнему `stream`), а `own.scope.while(...)` читается как
|
|
402
|
+
деструктурированный `scope.while(...)`.
|
|
403
|
+
|
|
404
|
+
Поэтому сообщение остаётся в четырёх формах, и каждая — граница того, что доказывает синтаксис: `stream`, который
|
|
405
|
+
правило не проследило ни до одного якоря (импортированный откуда-то ещё, неразрешимый или принадлежащий чужому
|
|
406
|
+
`defineFeature`); колбэк, написанный в другом месте и переданный сюда именем, — он не стоит лексически внутри;
|
|
407
|
+
секция `own`, вынесенная в собственную привязку (`const own = ({ stream }) => …; defineFeature(id, { own })`); и
|
|
408
|
+
`model(Decl, createOrderModel)`, где фабрика — именованная функция без аннотации `ModelContext`.
|
|
409
|
+
|
|
258
410
|
### `prefer-model-selection`
|
|
259
411
|
|
|
260
412
|
Компонент, который берёт одну выданную модель и читает её поля по хуку на поле, повторяет одно и то же
|
|
@@ -329,16 +481,43 @@ Oxlint читает ESLint-плагины через `jsPlugins`: импорти
|
|
|
329
481
|
|
|
330
482
|
```json
|
|
331
483
|
{
|
|
332
|
-
"
|
|
333
|
-
"
|
|
484
|
+
"extends": ["./node_modules/@opetope/lint/oxlintrc.json"],
|
|
485
|
+
"jsPlugins": [{ "name": "opetope", "specifier": "./node_modules/@opetope/lint/dist/index.js" }]
|
|
334
486
|
}
|
|
335
487
|
```
|
|
336
488
|
|
|
489
|
+
Это две строки, и ни одна из них не транскрипция. `@opetope/lint/oxlintrc.json` сгенерирован из
|
|
490
|
+
`configs.recommended.rules` и поставляется в пакете, поэтому правило, которое пакет добавил, приходит вместе с
|
|
491
|
+
обновлением, а не теряется в написанной руками severity-карте. `extends` в `.oxlintrc.json` принимает **путь
|
|
492
|
+
файла**, разрешаемый относительно конфига, который его назвал, — не specifier пакета; поэтому фрагмент лежит в корне
|
|
493
|
+
пакета, и путь, который пишет хост, совпадает с именем его экспорта. Конфигурации сливаются от первой к последней,
|
|
494
|
+
поэтому правило, которое проект хочет строже, он пишет в собственных `rules` после `extends`: три `off` выше — ровно
|
|
495
|
+
те, которые проект включает по своим каталогам.
|
|
496
|
+
|
|
497
|
+
Фрагмент несёт правила и ничего больше. Oxlint действительно сливает через `extends` и запись `jsPlugins`, но хост,
|
|
498
|
+
который объявляет плагин сам — под пакетом-обёрткой, как того требует изолированная установка peer-ов, — объявит имя
|
|
499
|
+
`opetope` второй раз, и это отвергнет всю конфигурацию. Откуда берётся плагин — деплой хоста; severities — контракт
|
|
500
|
+
этого пакета.
|
|
501
|
+
|
|
337
502
|
Алиас `name` фиксирует namespace, поэтому id правила одинаков в обоих линтерах. `jsPlugins` находится в alpha и вне
|
|
338
|
-
semver: `npm run ci:test` запускает собранный плагин под Oxlint на валидной и невалидной фикстуре
|
|
339
|
-
фикс, который тот
|
|
503
|
+
semver: `npm run ci:test` запускает собранный плагин под Oxlint на валидной и невалидной фикстуре — через конфиг,
|
|
504
|
+
который `extends`-ит поставляемый фрагмент, — и проверяет фикс, который тот пишет: поломка этого моста падает здесь,
|
|
505
|
+
а не у хоста.
|
|
506
|
+
|
|
507
|
+
`--fix-suggestions` применяет первую suggestion сообщения, ничего не спрашивая, поэтому suggestion здесь — только
|
|
508
|
+
такая правка, которую оправдывает сам прочитанный код. `no-command-in-deps` предлагает те пути, которые читает сам
|
|
509
|
+
колбэк: статус, за которым он следит, раньше `run`, который он вызывает. Там, где правило не видит использование
|
|
510
|
+
насквозь — объект передан дальше, разобран или прочитан за пределами известных ему членов, — оно сообщает и не
|
|
511
|
+
предлагает ничего, и зависимость остаётся написанной так, как её написали, пока её не изменит автор.
|
|
512
|
+
|
|
513
|
+
`no-snapshot-in-update` держит ту же линию с другой стороны: его suggestion — это тот самый фикс, который правило
|
|
514
|
+
не стало писать без спроса: параметр, затеняющий имя, которое в файле уже есть, или именованное чтение, которое
|
|
515
|
+
остаётся ради другого читателя. Обе правки оставляют код делать то же, что он делал. Там, где значение вообще не
|
|
516
|
+
может стать телом апдейтера, предлагать нечего, и сообщение стоит одно.
|
|
340
517
|
|
|
341
518
|
## Checks
|
|
342
519
|
|
|
343
520
|
Из этого пакета: `npm run ci:test`, `npm run ci:type`, `npm run ci:eslint`, `npm run build`. Тест моста Oxlint
|
|
344
|
-
читает `dist`, поэтому `npm run build` идёт первым.
|
|
521
|
+
читает `dist`, поэтому `npm run build` идёт первым. `ci:test` заодно проверяет, что закоммиченный `oxlintrc.json`
|
|
522
|
+
всё ещё равен `configs.recommended.rules`; после изменения этого конфига выполните `npm run oxlintrc:generate` и
|
|
523
|
+
закоммитьте результат — именно поэтому файл не пишет сборка.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
/** What a name holds: one hook, a record of them, a selection whose fields may be hooks, or one such field. */
|
|
3
|
+
type HookKind = 'candidate' | 'hook' | 'hooks' | 'selection';
|
|
4
|
+
type HookKinds = Map<TSESLint.Scope.Variable, HookKind>;
|
|
5
|
+
/**
|
|
6
|
+
* What a dependency element names: one hook (no key, not a record), one key of a record, or the record itself,
|
|
7
|
+
* which is a fresh object on every render.
|
|
8
|
+
*/
|
|
9
|
+
type Binding = {
|
|
10
|
+
readonly key: string | undefined;
|
|
11
|
+
readonly proved: boolean;
|
|
12
|
+
readonly record: boolean;
|
|
13
|
+
readonly variable: TSESLint.Scope.Variable;
|
|
14
|
+
};
|
|
15
|
+
/** One read of the binding: the member it ends in, and the text the author wrote to reach it. */
|
|
16
|
+
type Access = {
|
|
17
|
+
readonly member: string | undefined;
|
|
18
|
+
readonly optional: boolean;
|
|
19
|
+
readonly text: string;
|
|
20
|
+
};
|
|
21
|
+
/** `opaque` is a use no member replaces: the object passed on, destructured, or read past a member we know. */
|
|
22
|
+
type Usage = {
|
|
23
|
+
readonly accesses: readonly Access[];
|
|
24
|
+
readonly opaque: boolean;
|
|
25
|
+
};
|
|
26
|
+
declare const STATUS_MEMBERS: string[];
|
|
27
|
+
/** Every read of the binding inside one callback, and whether any of them takes the object as a whole. */
|
|
28
|
+
declare function usageIn(source: TSESLint.SourceCode, binding: Binding, callback: TSESTree.Node | undefined): Usage;
|
|
29
|
+
/** The binding a dependency element names as a whole; a member of one is a member, and this returns nothing. */
|
|
30
|
+
declare function bindingAt(source: TSESLint.SourceCode, kinds: HookKinds, element: TSESTree.Node): Binding | undefined;
|
|
31
|
+
/** Records what one declaration binds, so a later dependency array is read against names and not against spelling. */
|
|
32
|
+
declare function classifyDeclaration(source: TSESLint.SourceCode, declarator: TSESTree.VariableDeclarator, kinds: HookKinds): void;
|
|
33
|
+
export type { Access, Binding, HookKinds, Usage };
|
|
34
|
+
export { bindingAt, classifyDeclaration, STATUS_MEMBERS, usageIn };
|