@opetope/lint 0.9.3 → 0.9.5
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 +88 -0
- package/README.md +68 -9
- package/README.ru.md +67 -9
- package/dist/declaration-ingress.d.ts +22 -1
- package/dist/declaration-ingress.js +1 -1
- package/dist/declaration-ingress.js.map +1 -1
- package/dist/declarations.d.ts +4 -2
- package/dist/declarations.js +1 -1
- package/dist/declarations.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/rules/layer-placement.js +1 -1
- package/dist/rules/layer-placement.js.map +1 -1
- package/dist/rules/no-redundant-const-tuple.d.ts +5 -0
- package/dist/rules/no-redundant-const-tuple.js +2 -0
- package/dist/rules/no-redundant-const-tuple.js.map +1 -0
- package/dist/rules/prefer-effect-current.d.ts +5 -0
- package/dist/rules/prefer-effect-current.js +2 -0
- package/dist/rules/prefer-effect-current.js.map +1 -0
- package/dist/rules/require-literal-id.js +1 -1
- package/dist/rules/require-literal-id.js.map +1 -1
- package/oxlintrc.json +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,93 @@
|
|
|
1
1
|
# @opetope/lint
|
|
2
2
|
|
|
3
|
+
## 0.9.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 58b36bf: Add the optional Navigation package: the screen value protocol (`defineScreen`, branded screen, occurrence and
|
|
8
|
+
request identities, command results as data) and `createNavigator`, a reusable model fragment with a stack mode, a
|
|
9
|
+
switch mode and a renderer-neutral presentation handshake. The root entry peers on `@opetope/core` only, so a host
|
|
10
|
+
adapter outside Opetope can hold the protocol and the reducers (D310). The `./react` subpath adds `defineSurface`,
|
|
11
|
+
a factory over switch-slot families where the content contribution of a screen is its registration on a surface, with
|
|
12
|
+
`@opetope/react` and `react` as optional peers so the root stays React-free (D311). The `./links` subpath adds deep links on the root's peer alone —
|
|
13
|
+
`defineLink` with a Standard Schema, a catalog, a handler registry whose entries a link mints itself, guards as
|
|
14
|
+
`{ ready, resolve? }` data, and `createLinkDispatcher`, a model fragment that retains one intent, waits for a guard
|
|
15
|
+
or a registration instead of dropping it, and reports a failure before the acknowledgement that ends its run (D312).
|
|
16
|
+
D313 makes the package answer what its first application worked around: a `ScreenId` carries `dismissible`, a region
|
|
17
|
+
declares whether it shows an occurrence or the declaration (so a page bar draws the title of a page nobody opened
|
|
18
|
+
without minting a fake occurrence), `LinkGuards` is exported, `NavigatorPersistence.key` lets a host keep its own
|
|
19
|
+
storage keys, and a `./testing` subpath publishes `createScreenEntryId`, `fakeLinkSource` and
|
|
20
|
+
`fakeLinkCodec` on the Core peer alone. The dispatcher no longer loses an intent whose
|
|
21
|
+
handler registered while the attempt was running: a wait now remembers the registry that attempt read, not the one
|
|
22
|
+
that exists after it. `opetope/require-literal-id` also learns the four declaration factories of this package.
|
|
23
|
+
|
|
24
|
+
D315 answers an independent review of the package. A surface view now lives as long as the contribution it was minted
|
|
25
|
+
for, so a second registration of one screen no longer drops the occurrences a navigator holds; a displaced
|
|
26
|
+
whole-surface exit applies the removal it withheld, so a `push` never revives the overlays a `clear` closed and a
|
|
27
|
+
`replace` aimed at a leaving occurrence answers `superseded`; an exit of one occurrence survives a change of the
|
|
28
|
+
stack that does not concern it; a switch navigator applies a restored selection before the default fallback, so a
|
|
29
|
+
cold start opens on the page it remembered, in place, without minting a default nobody saw. Every non-cancellation
|
|
30
|
+
failure on the path of an intent — a guard that could not resolve itself, a codec that threw, a handler registered
|
|
31
|
+
through another declaration — is now reported to `diagnostics` and then acknowledged, instead of leaving the intent
|
|
32
|
+
retained forever. `availableScreens` and `entries` keep their identity when the same screens are published again and
|
|
33
|
+
when the stack moves, `SurfaceEntry.screen` carries the kind its surface declared, `FakeLinkSource` records
|
|
34
|
+
`accepted` beside `acknowledged`, and the refusals of a link parameter and of a foreign handler name the link they
|
|
35
|
+
belong to. `opetope/layer-placement` also learns `defineSurface`.
|
|
36
|
+
|
|
37
|
+
D314 keeps the payload fact in the types instead of the run time: the kind of a screen is read from its `Data`, a
|
|
38
|
+
`ScreenId<'none'>` carries the `request()` an erased page bar opens it with, `defineSurface(id, { payload: 'none' })`
|
|
39
|
+
declares a surface of screens without data, switch mode asks for that availability by type, the record with no fields
|
|
40
|
+
is `EmptyRecord`, and `NavigatorPersistence.key` is required.
|
|
41
|
+
|
|
42
|
+
- fa553e6: `opetope/no-redundant-const-tuple` removes the `as const` a `derive` tuple does not need.
|
|
43
|
+
|
|
44
|
+
D279 made the tuple source of `derive` a `const` type parameter: `[left, right]` infers its own tuple and the
|
|
45
|
+
selector sees exact values without an assertion at the call site. Application code writes `as const` beside it
|
|
46
|
+
anyway — a pass over 23 features of a consuming application found the shape in seven slices, nine times in one of
|
|
47
|
+
them. It compiles, it breaks nothing and it says nothing: it repeats what the signature already says, and a reader
|
|
48
|
+
has to check the types to learn whether it is load-bearing here.
|
|
49
|
+
|
|
50
|
+
The rule reads the first argument of a `derive` resolved to `@opetope/core`, and reports only where that argument
|
|
51
|
+
is an array literal carrying `as const`. The fix removes the assertion and nothing else; it stands down when a
|
|
52
|
+
comment sits between the tuple and the assertion, because such a comment belongs to neither and removing the range
|
|
53
|
+
would take it along.
|
|
54
|
+
|
|
55
|
+
An assertion written anywhere else is left alone: on one element of the tuple, on the result of the selector, on
|
|
56
|
+
the options record, and on the single-source form, whose argument is not a tuple at all. `as Sources` is a
|
|
57
|
+
different assertion. The import is resolved within one module — a local alias, a `const` that holds the import and
|
|
58
|
+
a namespace binding all reach the same export — and, unlike `no-command-in-deps`, the rule recognizes the import
|
|
59
|
+
from `@opetope/core` and nothing else: a re-export through a host's own barrel it does not see. That is the
|
|
60
|
+
deliberate price of a safe fix, because this rule rewrites code — a host's own `derive` has no `const` type
|
|
61
|
+
parameter to make the assertion redundant, so removing it there would change the inferred types in silence. The
|
|
62
|
+
rule is `error` in `configs.recommended`, and `@opetope/lint/oxlintrc.json` carries it to an Oxlint host with the
|
|
63
|
+
upgrade (D309).
|
|
64
|
+
|
|
65
|
+
- bab00b8: `opetope/prefer-effect-current` replaces the read of a source inside the run of its own effect.
|
|
66
|
+
|
|
67
|
+
The `run` of an effect is already given the value it was started for. `source.getSnapshot()` inside that run reads
|
|
68
|
+
the source a second time and says nothing about which run this is; the fix writes `execution.current`, which is the
|
|
69
|
+
value this run answers and the one `when` and `skipInitial` are written against. A pass over 23 features of a
|
|
70
|
+
consuming application found the shape three times.
|
|
71
|
+
|
|
72
|
+
The fix writes the word the run already has — the name of the context for `execution => …`, the local name `current`
|
|
73
|
+
was destructured under for `({ current }) => …` — and a run that named neither gets the report without a rewrite.
|
|
74
|
+
|
|
75
|
+
Two cases are reported and not rewritten, because there the two values legitimately differ. Past the first `await`
|
|
76
|
+
of the run, `current` is the value this run started with and the snapshot is the value of the moment; a change of the
|
|
77
|
+
source normally restarts the run, so the two normally agree, and «normally» is not a licence to rewrite. Under
|
|
78
|
+
`when` the filter admits some values and skips others, so the source moves without restarting the run (D168, D296); a
|
|
79
|
+
modifier record the rule cannot read is treated as if it carried the predicate. A read inside a nested callback of
|
|
80
|
+
the run — a `timers.delay`, a subscription handler — is left alone entirely: that code runs later than the run does.
|
|
81
|
+
|
|
82
|
+
Like every rule of this package that rewrites code, it resolves its context exactly (D309): the `effect` it acts on
|
|
83
|
+
is declared on a parameter typed `ModelContext` from `@opetope/core`, or on the context of a `model(Declaration,
|
|
84
|
+
factory)` written inside a `defineFeature`. The rule is `error` in `configs.recommended`, and
|
|
85
|
+
`@opetope/lint/oxlintrc.json` carries it to an Oxlint host with the upgrade (D323).
|
|
86
|
+
|
|
87
|
+
## 0.9.4
|
|
88
|
+
|
|
89
|
+
No changes in this release.
|
|
90
|
+
|
|
3
91
|
## 0.9.3
|
|
4
92
|
|
|
5
93
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ both it and `eslint` are peer dependencies.
|
|
|
44
44
|
|
|
45
45
|
Every rule that holds wherever Opetope is written: `when-predicate`, `define-feature-property-order`,
|
|
46
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
|
|
47
|
+
`no-redundant-const-tuple`, `no-command-in-deps`, `prefer-effect-current` and `require-declared-models` as errors. A rule that needs to know where a host keeps its files is off here and arrives
|
|
48
48
|
through `layers`; `prefer-model-selection` is off because the number of hooks a component may keep is a taste a
|
|
49
49
|
project settles for itself.
|
|
50
50
|
|
|
@@ -57,9 +57,11 @@ project settles for itself.
|
|
|
57
57
|
| `layer-placement` | `off` | `layers({ integration, models, ui })` |
|
|
58
58
|
| `no-command-in-deps` | `error` | |
|
|
59
59
|
| `no-internal-imports` | `error` | also scoped by `internalImports({ allow })` |
|
|
60
|
+
| `no-redundant-const-tuple` | `error` | |
|
|
60
61
|
| `no-snapshot-in-update` | `error` | |
|
|
61
62
|
| `no-snapshot-read-in-render` | `error` | |
|
|
62
63
|
| `no-subscribe-outside-models` | `off` | `layers({ models })` |
|
|
64
|
+
| `prefer-effect-current` | `error` | |
|
|
63
65
|
| `prefer-model-selection` | `off` | the project, with its own `threshold` |
|
|
64
66
|
| `require-declared-models` | `error` | |
|
|
65
67
|
| `require-literal-id` | `error` | |
|
|
@@ -154,6 +156,30 @@ The fix reorders the properties. It stands down when a comment sits between two
|
|
|
154
156
|
belongs to neither and reordering would move it away from the line it explains; a comment inside a section travels
|
|
155
157
|
with it. An object with a key that is not a section, or with a spread, is left to the type checker.
|
|
156
158
|
|
|
159
|
+
### `no-redundant-const-tuple`
|
|
160
|
+
|
|
161
|
+
`derive` takes its tuple of sources as a `const` type parameter, so `[left, right]` infers its own tuple and the
|
|
162
|
+
selector already sees exact values. The `as const` written beside it states what the signature already states
|
|
163
|
+
(D279, D309):
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const summary = derive([total, label], (value, suffix) => `${value} ${suffix}`);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The rule reads the first argument of a `derive` resolved to `@opetope/core`, and only where that argument is an
|
|
170
|
+
array literal carrying `as const`. The fix removes the assertion and nothing else. It stands down when a comment
|
|
171
|
+
sits between the tuple and the assertion, because such a comment belongs to neither and the removal would take it
|
|
172
|
+
along.
|
|
173
|
+
|
|
174
|
+
**The boundary.** An assertion written anywhere else is left alone: on one element of the tuple, where it says
|
|
175
|
+
something about that element; on the result of the selector; on the options record; and on the single-source form,
|
|
176
|
+
whose argument is not a tuple at all. `as Sources` is a different assertion and says something else. The import is
|
|
177
|
+
resolved within one module — a local alias, a `const` that holds the import and a namespace binding all reach the
|
|
178
|
+
same export — and, unlike `no-command-in-deps`, this rule recognizes the import from `@opetope/core` and nothing
|
|
179
|
+
else: a re-export through a host's own barrel it does not see. That is the deliberate price of a safe fix. This
|
|
180
|
+
rule rewrites code, and a host's own `derive` has no `const` type parameter to make the assertion redundant, so
|
|
181
|
+
removing it there would change the inferred types in silence.
|
|
182
|
+
|
|
157
183
|
### `require-declared-models`
|
|
158
184
|
|
|
159
185
|
A component that reads a per-mount UI model declares that model with `requiresModels`. Owner models remain
|
|
@@ -201,7 +227,9 @@ choice. Types and runtime authority checks continue to apply.
|
|
|
201
227
|
A declaration names itself with a string that is written, not built where the declaration stands. The rule reads
|
|
202
228
|
every declaration of the public vocabulary that names itself — `defineFeature`, `defineApplication`,
|
|
203
229
|
`defineCondition`, `defineHostContract`, `defineModel`, `definePort`, `defineSlot`, `defineSwitchSlot`,
|
|
204
|
-
`definePipe` and `defineRegistry
|
|
230
|
+
`definePipe` and `defineRegistry`, and the four of the optional Navigation package: `defineScreen`,
|
|
231
|
+
`defineSurface`, `defineLink` and `defineLinkHandlers`. D280 gives them one form: the id is the first argument of
|
|
232
|
+
every one of them.
|
|
205
233
|
|
|
206
234
|
Written means a string literal, a template with no expressions, a name that resolves to an import or to a `const`
|
|
207
235
|
of the same module, and a property read from such a name. Built means anything the call site assembles: a template
|
|
@@ -224,7 +252,10 @@ there, and that is where its own text is checked. The kernel's `defineModule`, `
|
|
|
224
252
|
|
|
225
253
|
An id starts with a letter and joins segments of letters and digits with one of `.`, `/`, `:` or `-`, within 160
|
|
226
254
|
characters. That is the grammar `declarationId` accepts in `@opetope/core`; an id outside it is a `DeclarationError`
|
|
227
|
-
the moment the declaration runs, and this rule says so before the code runs.
|
|
255
|
+
the moment the declaration runs, and this rule says so before the code runs. A screen, a surface and a link handler
|
|
256
|
+
registry pass their id through the same `declarationId`, so this rule reads them too; `defineLink` is left out of it
|
|
257
|
+
because a link id is a canonical lower-case path segment of a URL, which the package checks itself and which no
|
|
258
|
+
reserved prefix applies to (D313).
|
|
228
259
|
|
|
229
260
|
A project that reserves a first segment names it, and every declaration of those files must open with it:
|
|
230
261
|
|
|
@@ -240,12 +271,12 @@ one in the same module. An id that arrives from another module is checked where
|
|
|
240
271
|
Each declaration is written in the layer that owns it. The rule reports nothing until a project names its layers
|
|
241
272
|
through `configs.layers`, because the names of directories are not a law of the framework:
|
|
242
273
|
|
|
243
|
-
| Declaration
|
|
244
|
-
|
|
|
245
|
-
| `defineFeature`
|
|
246
|
-
| `defineModel`
|
|
247
|
-
| `defineSlot`, `defineSwitchSlot`, `definePipe`, `defineRegistry`, `definePort`, `defineCondition`, `defineHostContract` | integration, or the UI contracts beside the component |
|
|
248
|
-
| `defineApplication`
|
|
274
|
+
| Declaration | Layer |
|
|
275
|
+
| ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
276
|
+
| `defineFeature` | integration — a feature composes the other two layers |
|
|
277
|
+
| `defineModel` | models, or the UI contracts that declare a mount model |
|
|
278
|
+
| `defineSlot`, `defineSwitchSlot`, `defineSurface`, `definePipe`, `defineRegistry`, `definePort`, `defineCondition`, `defineHostContract` | integration, or the UI contracts beside the component |
|
|
279
|
+
| `defineApplication` | none of them: the host bootstrap owns it |
|
|
249
280
|
|
|
250
281
|
**This is a convention of the project, not a law of the library.** The framework says a feature's UI imports only
|
|
251
282
|
that feature's own contracts, and that a model knows nothing about the feature; where those files live is the
|
|
@@ -404,6 +435,34 @@ elsewhere and passed in by name, which is not lexically inside; an `own` section
|
|
|
404
435
|
(`const own = ({ stream }) => …; defineFeature(id, { own })`); and `model(Decl, createOrderModel)` where the factory
|
|
405
436
|
is a named function without the `ModelContext` annotation.
|
|
406
437
|
|
|
438
|
+
### `prefer-effect-current`
|
|
439
|
+
|
|
440
|
+
The `run` of an effect is already given the value it was started for. Reading the same source through
|
|
441
|
+
`source.getSnapshot()` inside that run reads it a second time and says nothing about which run this is, so the rule
|
|
442
|
+
rewrites it to `execution.current` (D296, D323):
|
|
443
|
+
|
|
444
|
+
```js
|
|
445
|
+
// before
|
|
446
|
+
ctx.effect(quotes, execution => publish(quotes.getSnapshot()));
|
|
447
|
+
// after
|
|
448
|
+
ctx.effect(quotes, execution => publish(execution.current));
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
The fix writes the word the run already has: the name of the context for `execution => …`, and the local name
|
|
452
|
+
`current` was destructured under for `({ current }) => …`. A run that named neither has no word to write, so the
|
|
453
|
+
report stands alone.
|
|
454
|
+
|
|
455
|
+
**The two cases it reports without rewriting.** Past the first `await` of the run, `current` is the value this run
|
|
456
|
+
started with and the snapshot is the value of the moment; a change of the source normally restarts the run, so the
|
|
457
|
+
two normally agree — and «normally» is not a licence to rewrite. Under `when` they legitimately differ: the filter
|
|
458
|
+
admits some values and skips others, so the source moves without restarting the run (D168, D296). A modifier record
|
|
459
|
+
this rule cannot read — one assembled elsewhere — is treated as if it carried the predicate.
|
|
460
|
+
|
|
461
|
+
A read inside a nested callback of the run — a `timers.delay`, a subscription handler — is left alone: that code
|
|
462
|
+
runs later than the run does, and the value of that moment is the one it wants. Like every rule that rewrites code,
|
|
463
|
+
this one resolves its context exactly: the `effect` it acts on is the one declared on a `ModelContext` parameter or
|
|
464
|
+
on the context of a `model(Declaration, factory)` written inside a `defineFeature` of `@opetope/runtime` (D309).
|
|
465
|
+
|
|
407
466
|
### `prefer-model-selection`
|
|
408
467
|
|
|
409
468
|
A component that takes one granted model and reads its fields through a hook each repeats the same wiring. From
|
package/README.ru.md
CHANGED
|
@@ -44,7 +44,7 @@ export default [
|
|
|
44
44
|
|
|
45
45
|
Все правила, которые действуют везде, где пишут на Opetope: `when-predicate`, `define-feature-property-order`,
|
|
46
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. Правило, которому нужно знать, где хост держит свои файлы, здесь выключено и
|
|
47
|
+
`no-redundant-const-tuple`, `no-command-in-deps`, `prefer-effect-current` и `require-declared-models` как error. Правило, которому нужно знать, где хост держит свои файлы, здесь выключено и
|
|
48
48
|
приходит через `layers`; `prefer-model-selection` выключено потому, что число hooks, которое компонент вправе
|
|
49
49
|
держать, каждый проект решает для себя.
|
|
50
50
|
|
|
@@ -57,9 +57,11 @@ export default [
|
|
|
57
57
|
| `layer-placement` | `off` | `layers({ integration, models, ui })` |
|
|
58
58
|
| `no-command-in-deps` | `error` | |
|
|
59
59
|
| `no-internal-imports` | `error` | плюс сужение через `internalImports({ allow })` |
|
|
60
|
+
| `no-redundant-const-tuple` | `error` | |
|
|
60
61
|
| `no-snapshot-in-update` | `error` | |
|
|
61
62
|
| `no-snapshot-read-in-render` | `error` | |
|
|
62
63
|
| `no-subscribe-outside-models` | `off` | `layers({ models })` |
|
|
64
|
+
| `prefer-effect-current` | `error` | |
|
|
63
65
|
| `prefer-model-selection` | `off` | проект, со своим `threshold` |
|
|
64
66
|
| `require-declared-models` | `error` | |
|
|
65
67
|
| `require-literal-id` | `error` | |
|
|
@@ -155,6 +157,30 @@ Node. Применяйте конфигурацию ко всем использ
|
|
|
155
157
|
принадлежит ни одной из них, и перестановка увела бы его от строки, которую он объясняет; комментарий внутри секции
|
|
156
158
|
переезжает вместе с ней. Объект с ключом, который не является секцией, или со spread остаётся проверке типов.
|
|
157
159
|
|
|
160
|
+
### `no-redundant-const-tuple`
|
|
161
|
+
|
|
162
|
+
`derive` принимает кортеж источников `const`-параметром типа, поэтому `[left, right]` выводит собственный кортеж, и
|
|
163
|
+
селектор уже видит точные значения. Написанный рядом `as const` утверждает то, что и так утверждает сигнатура
|
|
164
|
+
(D279, D309):
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
const summary = derive([total, label], (value, suffix) => `${value} ${suffix}`);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Правило читает первый аргумент `derive`, разрешённого к `@opetope/core`, и только там, где этот аргумент —
|
|
171
|
+
литерал массива с `as const`. Фикс убирает утверждение и ничего больше. Он отступает, если между кортежем и
|
|
172
|
+
утверждением стоит комментарий: такой комментарий не принадлежит ни одному из них, и удаление увело бы его вместе
|
|
173
|
+
с диапазоном.
|
|
174
|
+
|
|
175
|
+
**Граница.** Утверждение, написанное в другом месте, остаётся нетронутым: на одном элементе кортежа, где оно
|
|
176
|
+
говорит об этом элементе; на результате селектора; на записи опций; и на форме с одним источником, аргумент
|
|
177
|
+
которой вообще не кортеж. `as Sources` — другое утверждение и говорит другое. Импорт разрешается в пределах одного
|
|
178
|
+
модуля: локальный alias, `const`, который держит импорт, и namespace-привязка достигают одного и того же экспорта,
|
|
179
|
+
— а в отличие от `no-command-in-deps` правило узнаёт только импорт из `@opetope/core` и ничего больше: реэкспорт
|
|
180
|
+
через собственный barrel хоста оно не видит. Это осознанная цена за безопасный фикс. Правило правит код, а у
|
|
181
|
+
собственного `derive` хоста нет `const`-параметра типа, который делал бы утверждение лишним, — снятие `as const`
|
|
182
|
+
там молча изменило бы выведенные типы.
|
|
183
|
+
|
|
158
184
|
### `require-declared-models`
|
|
159
185
|
|
|
160
186
|
Компонент, читающий UI-модель отдельного mount, объявляет её через `requiresModels`. Owner-модели остаются
|
|
@@ -202,7 +228,8 @@ inline и именованные компоненты. Bindings сравнива
|
|
|
202
228
|
Объявление называет себя строкой, которая написана, а не собрана там, где стоит объявление. Правило читает каждое
|
|
203
229
|
объявление публичного словаря, которое называет себя: `defineFeature`, `defineApplication`, `defineCondition`,
|
|
204
230
|
`defineHostContract`, `defineModel`, `definePort`, `defineSlot`, `defineSwitchSlot`, `definePipe` и
|
|
205
|
-
`defineRegistry
|
|
231
|
+
`defineRegistry`, — и четыре из необязательного пакета Navigation: `defineScreen`, `defineSurface`, `defineLink` и
|
|
232
|
+
`defineLinkHandlers`. D280 даёт им одну форму: id — первый аргумент у каждого.
|
|
206
233
|
|
|
207
234
|
Написана — это строковый литерал, шаблон без выражений, имя, которое разрешается в импорт или в `const` того же
|
|
208
235
|
модуля, и чтение поля у такого имени. Собрана — всё, что складывает место вызова: шаблон с выражением, конкатенация,
|
|
@@ -225,7 +252,10 @@ inline и именованные компоненты. Bindings сравнива
|
|
|
225
252
|
|
|
226
253
|
Id начинается с буквы и соединяет сегменты из букв и цифр одним из `.`, `/`, `:` или `-`, не длиннее 160 символов.
|
|
227
254
|
Это грамматика, которую принимает `declarationId` в `@opetope/core`; id вне неё — это `DeclarationError` в момент
|
|
228
|
-
выполнения объявления, а правило говорит об этом до запуска кода.
|
|
255
|
+
выполнения объявления, а правило говорит об этом до запуска кода. Экран, поверхность и registry обработчиков ссылок
|
|
256
|
+
проходят через тот же `declarationId`, поэтому правило читает и их; `defineLink` из него исключён: id ссылки — это
|
|
257
|
+
канонический сегмент пути URL в нижнем регистре, который пакет проверяет сам и к которому зарезервированный префикс
|
|
258
|
+
неприменим (D313).
|
|
229
259
|
|
|
230
260
|
Проект, который зарезервировал первый сегмент, называет его, и каждое объявление таких файлов обязано с него
|
|
231
261
|
начинаться:
|
|
@@ -242,12 +272,12 @@ Id начинается с буквы и соединяет сегменты и
|
|
|
242
272
|
Каждое объявление написано в слое, который им владеет. Правило не сообщает ничего, пока проект не назвал свои слои
|
|
243
273
|
через `configs.layers`: имена каталогов не являются законом фреймворка.
|
|
244
274
|
|
|
245
|
-
| Объявление
|
|
246
|
-
|
|
|
247
|
-
| `defineFeature`
|
|
248
|
-
| `defineModel`
|
|
249
|
-
| `defineSlot`, `defineSwitchSlot`, `definePipe`, `defineRegistry`, `definePort`, `defineCondition`, `defineHostContract` | integration или контракты UI рядом с компонентом |
|
|
250
|
-
| `defineApplication`
|
|
275
|
+
| Объявление | Слой |
|
|
276
|
+
| ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
277
|
+
| `defineFeature` | integration — фича сочленяет два других слоя |
|
|
278
|
+
| `defineModel` | models или контракты UI, которые объявляют модель монтирования |
|
|
279
|
+
| `defineSlot`, `defineSwitchSlot`, `defineSurface`, `definePipe`, `defineRegistry`, `definePort`, `defineCondition`, `defineHostContract` | integration или контракты UI рядом с компонентом |
|
|
280
|
+
| `defineApplication` | ни один из них: им владеет bootstrap хоста |
|
|
251
281
|
|
|
252
282
|
**Это конвенция проекта, а не закон библиотеки.** Фреймворк говорит, что UI фичи импортирует только её собственные
|
|
253
283
|
контракты, а модель не знает о фиче; где лежат эти файлы — выбор проекта, и правило держит тот выбор, который он
|
|
@@ -407,6 +437,34 @@ own: ({ imports, stream }) => ({
|
|
|
407
437
|
секция `own`, вынесенная в собственную привязку (`const own = ({ stream }) => …; defineFeature(id, { own })`); и
|
|
408
438
|
`model(Decl, createOrderModel)`, где фабрика — именованная функция без аннотации `ModelContext`.
|
|
409
439
|
|
|
440
|
+
### `prefer-effect-current`
|
|
441
|
+
|
|
442
|
+
`run` эффекта уже получил то значение, ради которого его запустили. Чтение того же источника через
|
|
443
|
+
`source.getSnapshot()` внутри этого прогона читает его второй раз и ничего не говорит о том, какой это прогон,
|
|
444
|
+
поэтому правило переписывает его в `execution.current` (D296, D323):
|
|
445
|
+
|
|
446
|
+
```js
|
|
447
|
+
// было
|
|
448
|
+
ctx.effect(quotes, execution => publish(quotes.getSnapshot()));
|
|
449
|
+
// стало
|
|
450
|
+
ctx.effect(quotes, execution => publish(execution.current));
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Фикс пишет то слово, которое у прогона уже есть: имя контекста для `execution => …` и локальное имя, под которым
|
|
454
|
+
взят `current`, для `({ current }) => …`. Прогон, не назвавший ни того ни другого, писать нечем, и тогда остаётся
|
|
455
|
+
только сообщение.
|
|
456
|
+
|
|
457
|
+
**Два случая, о которых правило сообщает, но не переписывает.** После первого `await` в прогоне `current` — это
|
|
458
|
+
значение, с которым прогон начался, а снимок — значение текущего момента; смена источника обычно перезапускает
|
|
459
|
+
прогон, поэтому обычно они совпадают, — а «обычно» не даёт права на правку. Под `when` они расходятся законно:
|
|
460
|
+
фильтр допускает одни значения и пропускает другие, поэтому источник меняется без перезапуска прогона (D168, D296).
|
|
461
|
+
Запись модификаторов, которую правило прочитать не может — собранную в другом месте, — оно считает несущей предикат.
|
|
462
|
+
|
|
463
|
+
Чтение внутри вложенного колбэка прогона — в `timers.delay`, в обработчике подписки — остаётся как есть: этот код
|
|
464
|
+
выполняется позже прогона, и ему нужно значение того момента. Как и всякое правило, которое правит код, это
|
|
465
|
+
разрешает свой контекст точно: `effect`, о котором идёт речь, объявлен на параметре типа `ModelContext` либо на
|
|
466
|
+
контексте `model(Declaration, factory)`, написанном внутри `defineFeature` из `@opetope/runtime` (D309).
|
|
467
|
+
|
|
410
468
|
### `prefer-model-selection`
|
|
411
469
|
|
|
412
470
|
Компонент, который берёт одну выданную модель и читает её поля по хуку на поле, повторяет одно и то же
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
2
|
import { TSESLint } from '@typescript-eslint/utils';
|
|
3
|
+
import type { FunctionNode } from './model-bindings.js';
|
|
4
|
+
type NamedMethod = {
|
|
5
|
+
readonly fn: FunctionNode;
|
|
6
|
+
readonly method: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The method this callee names on the first parameter of some function, and that function. Whether the parameter
|
|
10
|
+
* really is a declaring context is a separate question: answering it here would make the two questions circular.
|
|
11
|
+
*/
|
|
12
|
+
declare function methodPath(source: TSESLint.SourceCode, callee: TSESTree.Node): NamedMethod | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* The two functions that hand an author a declaring context: the `own` section written right where `defineFeature`
|
|
15
|
+
* takes it, and the factory of a model — which is either annotated as one or written where a feature declares it,
|
|
16
|
+
* as the second argument of `model(Declaration, factory)`. That second form is proved by walking outward one
|
|
17
|
+
* `model(…)` at a time, and the set of functions already visited ends a chain that would come back to its start.
|
|
18
|
+
*/
|
|
19
|
+
declare function isContextFunction(source: TSESLint.SourceCode, fn: FunctionNode): boolean;
|
|
3
20
|
/**
|
|
4
21
|
* Whether this node stands lexically inside a callback whose opened work the declared node releases. The walk goes
|
|
5
22
|
* all the way out, so a nested function body counts; only the resolved context and the position decide.
|
|
6
23
|
*/
|
|
7
24
|
declare function isDeclaredIngress(source: TSESLint.SourceCode, node: TSESTree.Node): boolean;
|
|
8
|
-
|
|
25
|
+
/**
|
|
26
|
+
* D323: a rule that rewrites code has to resolve its package exactly (D309), and this pair is that resolution: the
|
|
27
|
+
* method a callee names on a declaring context, and whether that parameter really is one.
|
|
28
|
+
*/
|
|
29
|
+
export { isContextFunction, isDeclaredIngress, methodPath };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as i,TSESLint as
|
|
1
|
+
import{AST_NODE_TYPES as i,TSESLint as a}from"@typescript-eslint/utils";import{propertyName as s}from"./ast.js";import{isFeatureSection as c,isApi as m,definitionOf as l,isFunction as y}from"./model-bindings.js";const h=new Map([["event",1],["scope.keyed",1],["scope.switch",1],["scope.while",1],["stream",2]]),g=new Map([["attach",{argument:1,property:"open"}]]);function x(t){const[n]=t.params;if(n?.type===i.Identifier||n?.type===i.ObjectPattern)return n}function I(t,n){const e=x(n)?.typeAnnotation?.typeAnnotation;if(e?.type!==i.TSTypeReference)return!1;const{typeName:r}=e;return r.type===i.Identifier&&m(t,r,"@opetope/core","ModelContext",!0)}function u(t,n){const e=l(t,n);if(!(e?.type!==a.Scope.DefinitionType.Parameter||!y(e.node)))return{fn:e.node,name:e.name}}function S(t,n){const e=u(t,n);return e!==void 0&&e.name===e.fn.params[0]?e.fn:void 0}function w(t,n){const e=u(t,n),r=e?.name.parent;if(e===void 0||r?.type!==i.Property)return;const o=r.parent===e.fn.params[0]?s(r):void 0;return o===void 0?void 0:{fn:e.fn,method:o}}function P(t){const n=[];let e=t;for(;e.type===i.MemberExpression;){if(e.computed||e.property.type!==i.Identifier)return;n.unshift(e.property.name),e=e.object}return{root:e,steps:n}}function d(t,n){const e=P(n);if(e?.root.type!==i.Identifier)return;const r=e.steps.length===0?void 0:S(t,e.root);if(r!==void 0)return{fn:r,method:e.steps.join(".")};const o=w(t,e.root);return o===void 0?void 0:{fn:o.fn,method:[o.method,...e.steps].join(".")}}function E(t,n){const e=n.parent;if(e.type!==i.CallExpression||e.arguments[1]!==n)return;const r=d(t,e.callee);return r?.method==="model"?r.fn:void 0}function p(t,n){const e=new Set;let r=n;for(;r!==void 0&&!e.has(r);){if(e.add(r),c(t,r,"own",!0)||I(t,r))return!0;r=E(t,r)}return!1}function b(t,n,e){if(h.get(t)===n)return!0;const r=g.get(t);return r?.argument===n&&r.property===e}function A(t,n,e,r){const o=d(t,n.callee);return o!==void 0&&b(o.method,e,r)&&p(t,o.fn)}function C(t,n,e){return t.type!==i.ObjectExpression||n.type!==i.Property?e:s(n)}function M(t,n){let e=n,r;for(const o of t.getAncestors(n).reverse()){if(o.type===i.CallExpression){const f=o.arguments.indexOf(e);if(f!==-1&&A(t,o,f,r))return!0;r=void 0}else r=C(o,e,r);e=o}return!1}export{p as isContextFunction,M as isDeclaredIngress,d as methodPath};
|
|
2
2
|
//# sourceMappingURL=declaration-ingress.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"declaration-ingress.js","sources":["../src/declaration-ingress.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils';\n\nimport { propertyName } from './ast';\nimport type { FunctionNode } from './model-bindings';\nimport { definitionOf, isApi, isFeatureSection, isFunction } from './model-bindings';\n\n/**\n * The callbacks a declared node opens its work in and owns the release of: the disposer they return is run by the\n * node, in the drain of the generation that opened it. The argument index is part of the contract — `consume`,\n * `run`, `load`, `when` and `key` sit in the same calls and are not ingress.\n */\nconst positionalIngress = new Map([\n ['event', 1],\n ['scope.keyed', 1],\n ['scope.switch', 1],\n ['scope.while', 1],\n ['stream', 2],\n]);\n\n/** The one such callback written as a record member. `attach`'s `close` is a release, not an opening. */\nconst recordIngress = new Map([['attach', { argument: 1, property: 'open' }]]);\n\ntype Annotated = TSESTree.Identifier | TSESTree.ObjectPattern;\ntype NamedMethod = { readonly fn: FunctionNode; readonly method: string };\ntype ParameterOf = { readonly fn: FunctionNode; readonly name: TSESTree.BindingName };\n\nfunction annotatedParameter(fn: FunctionNode): Annotated | undefined {\n const [parameter] = fn.params;\n\n if (parameter?.type === AST_NODE_TYPES.Identifier || parameter?.type === AST_NODE_TYPES.ObjectPattern) {\n return parameter;\n }\n\n return undefined;\n}\n\n/** The factory of a model says so in the parameter it takes: the name `ModelContext` carries where it is exported. */\nfunction declaresModelContext(source: TSESLint.SourceCode, fn: FunctionNode): boolean {\n const annotation = annotatedParameter(fn)?.typeAnnotation?.typeAnnotation;\n\n if (annotation?.type !== AST_NODE_TYPES.TSTypeReference) return false;\n\n const { typeName } = annotation;\n\n return typeName.type === AST_NODE_TYPES.Identifier && isApi(source, typeName, '@opetope/core', 'ModelContext', true);\n}\n\n/** The function this name is a parameter of, and the pattern node that bound it there. */\nfunction parameterOf(source: TSESLint.SourceCode, node: TSESTree.Identifier): ParameterOf | undefined {\n const definition = definitionOf(source, node);\n\n if (definition?.type !== TSESLint.Scope.DefinitionType.Parameter || !isFunction(definition.node)) return undefined;\n\n return { fn: definition.node, name: definition.name };\n}\n\n/** `own` itself: the plain name a context parameter was given, whichever function that parameter belongs to. */\nfunction contextName(source: TSESLint.SourceCode, node: TSESTree.Identifier): FunctionNode | undefined {\n const parameter = parameterOf(source, node);\n\n return parameter !== undefined && parameter.name === parameter.fn.params[0] ? parameter.fn : undefined;\n}\n\n/** `({ stream }) => stream(…)`: the method under the key the destructuring took it from, alias included. */\nfunction destructuredMethod(source: TSESLint.SourceCode, node: TSESTree.Identifier): NamedMethod | undefined {\n const parameter = parameterOf(source, node);\n const property = parameter?.name.parent;\n\n if (parameter === undefined || property?.type !== AST_NODE_TYPES.Property) return undefined;\n\n const method = property.parent === parameter.fn.params[0] ? propertyName(property) : undefined;\n\n return method === undefined ? undefined : { fn: parameter.fn, method };\n}\n\n/** The dotted steps of a member chain and the name it starts from: `own.scope.while` is `own` and `scope.while`. */\nfunction memberSteps(callee: TSESTree.Node): { root: TSESTree.Node; steps: readonly string[] } | undefined {\n const steps: string[] = [];\n let current: TSESTree.Node = callee;\n\n while (current.type === AST_NODE_TYPES.MemberExpression) {\n if (current.computed || current.property.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n steps.unshift(current.property.name);\n current = current.object;\n }\n\n return { root: current, steps };\n}\n\n/**\n * The method this callee names on the first parameter of some function, and that function. Whether the parameter\n * really is a declaring context is a separate question: answering it here would make the two questions circular.\n */\nfunction methodPath(source: TSESLint.SourceCode, callee: TSESTree.Node): NamedMethod | undefined {\n const chain = memberSteps(callee);\n\n if (chain?.root.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n const named = chain.steps.length === 0 ? undefined : contextName(source, chain.root);\n\n if (named !== undefined) return { fn: named, method: chain.steps.join('.') };\n\n const destructured = destructuredMethod(source, chain.root);\n\n return destructured === undefined\n ? undefined\n : { fn: destructured.fn, method: [destructured.method, ...chain.steps].join('.') };\n}\n\n/** One step outward: the context whose `model(…)` call declares this factory, where that is how it was written. */\nfunction declaringContext(source: TSESLint.SourceCode, fn: FunctionNode): FunctionNode | undefined {\n const call = fn.parent;\n\n if (call.type !== AST_NODE_TYPES.CallExpression || call.arguments[1] !== fn) return undefined;\n\n const declaring = methodPath(source, call.callee);\n\n return declaring?.method === 'model' ? declaring.fn : undefined;\n}\n\n/**\n * The two functions that hand an author a declaring context: the `own` section written right where `defineFeature`\n * takes it, and the factory of a model — which is either annotated as one or written where a feature declares it,\n * as the second argument of `model(Declaration, factory)`. That second form is proved by walking outward one\n * `model(…)` at a time, and the set of functions already visited ends a chain that would come back to its start.\n */\nfunction isContextFunction(source: TSESLint.SourceCode, fn: FunctionNode): boolean {\n const seen = new Set<FunctionNode>();\n let current: FunctionNode | undefined = fn;\n\n while (current !== undefined && !seen.has(current)) {\n seen.add(current);\n\n if (isFeatureSection(source, current, 'own', true) || declaresModelContext(source, current)) return true;\n\n current = declaringContext(source, current);\n }\n\n return false;\n}\n\nfunction admitsIngress(method: string, position: number, property: string | undefined): boolean {\n if (positionalIngress.get(method) === position) return true;\n\n const record = recordIngress.get(method);\n\n return record?.argument === position && record.property === property;\n}\n\n/** The shape decides first and the import second: a call that cannot be ingress never pays for the resolution. */\nfunction isIngressArgument(\n source: TSESLint.SourceCode,\n call: TSESTree.CallExpression,\n position: number,\n property: string | undefined,\n): boolean {\n const named = methodPath(source, call.callee);\n\n return named !== undefined && admitsIngress(named.method, position, property) && isContextFunction(source, named.fn);\n}\n\n/** The record member the walk came through since the last call boundary: the `{ open }` of an `attach`. */\nfunction memberProperty(parent: TSESTree.Node, child: TSESTree.Node, current: string | undefined): string | undefined {\n if (parent.type !== AST_NODE_TYPES.ObjectExpression || child.type !== AST_NODE_TYPES.Property) return current;\n\n return propertyName(child);\n}\n\n/**\n * Whether this node stands lexically inside a callback whose opened work the declared node releases. The walk goes\n * all the way out, so a nested function body counts; only the resolved context and the position decide.\n */\nfunction isDeclaredIngress(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n let child: TSESTree.Node = node;\n let property: string | undefined;\n\n for (const parent of source.getAncestors(node).reverse()) {\n if (parent.type === AST_NODE_TYPES.CallExpression) {\n const position = parent.arguments.indexOf(child as TSESTree.CallExpressionArgument);\n\n if (position !== -1 && isIngressArgument(source, parent, position, property)) return true;\n\n property = undefined;\n } else {\n property = memberProperty(parent, child, property);\n }\n\n child = parent;\n }\n\n return false;\n}\n\nexport { isDeclaredIngress };\n"],"names":["positionalIngress","recordIngress","annotatedParameter","fn","parameter","AST_NODE_TYPES","declaresModelContext","source","annotation","typeName","isApi","parameterOf","node","definition","definitionOf","TSESLint","isFunction","contextName","destructuredMethod","property","method","propertyName","memberSteps","callee","steps","current","methodPath","chain","named","destructured","declaringContext","call","declaring","isContextFunction","seen","isFeatureSection","admitsIngress","position","record","isIngressArgument","memberProperty","parent","child","isDeclaredIngress"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"declaration-ingress.js","sources":["../src/declaration-ingress.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils';\n\nimport { propertyName } from './ast';\nimport type { FunctionNode } from './model-bindings';\nimport { definitionOf, isApi, isFeatureSection, isFunction } from './model-bindings';\n\n/**\n * The callbacks a declared node opens its work in and owns the release of: the disposer they return is run by the\n * node, in the drain of the generation that opened it. The argument index is part of the contract — `consume`,\n * `run`, `load`, `when` and `key` sit in the same calls and are not ingress.\n */\nconst positionalIngress = new Map([\n ['event', 1],\n ['scope.keyed', 1],\n ['scope.switch', 1],\n ['scope.while', 1],\n ['stream', 2],\n]);\n\n/** The one such callback written as a record member. `attach`'s `close` is a release, not an opening. */\nconst recordIngress = new Map([['attach', { argument: 1, property: 'open' }]]);\n\ntype Annotated = TSESTree.Identifier | TSESTree.ObjectPattern;\ntype NamedMethod = { readonly fn: FunctionNode; readonly method: string };\ntype ParameterOf = { readonly fn: FunctionNode; readonly name: TSESTree.BindingName };\n\nfunction annotatedParameter(fn: FunctionNode): Annotated | undefined {\n const [parameter] = fn.params;\n\n if (parameter?.type === AST_NODE_TYPES.Identifier || parameter?.type === AST_NODE_TYPES.ObjectPattern) {\n return parameter;\n }\n\n return undefined;\n}\n\n/** The factory of a model says so in the parameter it takes: the name `ModelContext` carries where it is exported. */\nfunction declaresModelContext(source: TSESLint.SourceCode, fn: FunctionNode): boolean {\n const annotation = annotatedParameter(fn)?.typeAnnotation?.typeAnnotation;\n\n if (annotation?.type !== AST_NODE_TYPES.TSTypeReference) return false;\n\n const { typeName } = annotation;\n\n return typeName.type === AST_NODE_TYPES.Identifier && isApi(source, typeName, '@opetope/core', 'ModelContext', true);\n}\n\n/** The function this name is a parameter of, and the pattern node that bound it there. */\nfunction parameterOf(source: TSESLint.SourceCode, node: TSESTree.Identifier): ParameterOf | undefined {\n const definition = definitionOf(source, node);\n\n if (definition?.type !== TSESLint.Scope.DefinitionType.Parameter || !isFunction(definition.node)) return undefined;\n\n return { fn: definition.node, name: definition.name };\n}\n\n/** `own` itself: the plain name a context parameter was given, whichever function that parameter belongs to. */\nfunction contextName(source: TSESLint.SourceCode, node: TSESTree.Identifier): FunctionNode | undefined {\n const parameter = parameterOf(source, node);\n\n return parameter !== undefined && parameter.name === parameter.fn.params[0] ? parameter.fn : undefined;\n}\n\n/** `({ stream }) => stream(…)`: the method under the key the destructuring took it from, alias included. */\nfunction destructuredMethod(source: TSESLint.SourceCode, node: TSESTree.Identifier): NamedMethod | undefined {\n const parameter = parameterOf(source, node);\n const property = parameter?.name.parent;\n\n if (parameter === undefined || property?.type !== AST_NODE_TYPES.Property) return undefined;\n\n const method = property.parent === parameter.fn.params[0] ? propertyName(property) : undefined;\n\n return method === undefined ? undefined : { fn: parameter.fn, method };\n}\n\n/** The dotted steps of a member chain and the name it starts from: `own.scope.while` is `own` and `scope.while`. */\nfunction memberSteps(callee: TSESTree.Node): { root: TSESTree.Node; steps: readonly string[] } | undefined {\n const steps: string[] = [];\n let current: TSESTree.Node = callee;\n\n while (current.type === AST_NODE_TYPES.MemberExpression) {\n if (current.computed || current.property.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n steps.unshift(current.property.name);\n current = current.object;\n }\n\n return { root: current, steps };\n}\n\n/**\n * The method this callee names on the first parameter of some function, and that function. Whether the parameter\n * really is a declaring context is a separate question: answering it here would make the two questions circular.\n */\nfunction methodPath(source: TSESLint.SourceCode, callee: TSESTree.Node): NamedMethod | undefined {\n const chain = memberSteps(callee);\n\n if (chain?.root.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n const named = chain.steps.length === 0 ? undefined : contextName(source, chain.root);\n\n if (named !== undefined) return { fn: named, method: chain.steps.join('.') };\n\n const destructured = destructuredMethod(source, chain.root);\n\n return destructured === undefined\n ? undefined\n : { fn: destructured.fn, method: [destructured.method, ...chain.steps].join('.') };\n}\n\n/** One step outward: the context whose `model(…)` call declares this factory, where that is how it was written. */\nfunction declaringContext(source: TSESLint.SourceCode, fn: FunctionNode): FunctionNode | undefined {\n const call = fn.parent;\n\n if (call.type !== AST_NODE_TYPES.CallExpression || call.arguments[1] !== fn) return undefined;\n\n const declaring = methodPath(source, call.callee);\n\n return declaring?.method === 'model' ? declaring.fn : undefined;\n}\n\n/**\n * The two functions that hand an author a declaring context: the `own` section written right where `defineFeature`\n * takes it, and the factory of a model — which is either annotated as one or written where a feature declares it,\n * as the second argument of `model(Declaration, factory)`. That second form is proved by walking outward one\n * `model(…)` at a time, and the set of functions already visited ends a chain that would come back to its start.\n */\nfunction isContextFunction(source: TSESLint.SourceCode, fn: FunctionNode): boolean {\n const seen = new Set<FunctionNode>();\n let current: FunctionNode | undefined = fn;\n\n while (current !== undefined && !seen.has(current)) {\n seen.add(current);\n\n if (isFeatureSection(source, current, 'own', true) || declaresModelContext(source, current)) return true;\n\n current = declaringContext(source, current);\n }\n\n return false;\n}\n\nfunction admitsIngress(method: string, position: number, property: string | undefined): boolean {\n if (positionalIngress.get(method) === position) return true;\n\n const record = recordIngress.get(method);\n\n return record?.argument === position && record.property === property;\n}\n\n/** The shape decides first and the import second: a call that cannot be ingress never pays for the resolution. */\nfunction isIngressArgument(\n source: TSESLint.SourceCode,\n call: TSESTree.CallExpression,\n position: number,\n property: string | undefined,\n): boolean {\n const named = methodPath(source, call.callee);\n\n return named !== undefined && admitsIngress(named.method, position, property) && isContextFunction(source, named.fn);\n}\n\n/** The record member the walk came through since the last call boundary: the `{ open }` of an `attach`. */\nfunction memberProperty(parent: TSESTree.Node, child: TSESTree.Node, current: string | undefined): string | undefined {\n if (parent.type !== AST_NODE_TYPES.ObjectExpression || child.type !== AST_NODE_TYPES.Property) return current;\n\n return propertyName(child);\n}\n\n/**\n * Whether this node stands lexically inside a callback whose opened work the declared node releases. The walk goes\n * all the way out, so a nested function body counts; only the resolved context and the position decide.\n */\nfunction isDeclaredIngress(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n let child: TSESTree.Node = node;\n let property: string | undefined;\n\n for (const parent of source.getAncestors(node).reverse()) {\n if (parent.type === AST_NODE_TYPES.CallExpression) {\n const position = parent.arguments.indexOf(child as TSESTree.CallExpressionArgument);\n\n if (position !== -1 && isIngressArgument(source, parent, position, property)) return true;\n\n property = undefined;\n } else {\n property = memberProperty(parent, child, property);\n }\n\n child = parent;\n }\n\n return false;\n}\n\n/**\n * D323: a rule that rewrites code has to resolve its package exactly (D309), and this pair is that resolution: the\n * method a callee names on a declaring context, and whether that parameter really is one.\n */\nexport { isContextFunction, isDeclaredIngress, methodPath };\n"],"names":["positionalIngress","recordIngress","annotatedParameter","fn","parameter","AST_NODE_TYPES","declaresModelContext","source","annotation","typeName","isApi","parameterOf","node","definition","definitionOf","TSESLint","isFunction","contextName","destructuredMethod","property","method","propertyName","memberSteps","callee","steps","current","methodPath","chain","named","destructured","declaringContext","call","declaring","isContextFunction","seen","isFeatureSection","admitsIngress","position","record","isIngressArgument","memberProperty","parent","child","isDeclaredIngress"],"mappings":"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"}
|
package/dist/declarations.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ type Declaration = {
|
|
|
3
3
|
readonly id: TSESTree.Node;
|
|
4
4
|
readonly name: string;
|
|
5
5
|
};
|
|
6
|
-
/** The name and the id expression of a
|
|
6
|
+
/** The name and the id expression of a call whose id follows the Core grammar, or `undefined` for anything else. */
|
|
7
7
|
declare function declarationOf(node: TSESTree.CallExpression): Declaration | undefined;
|
|
8
|
-
|
|
8
|
+
/** The same, for every declaration that names itself first, whatever grammar its own id follows. */
|
|
9
|
+
declare function namedDeclarationOf(node: TSESTree.CallExpression): Declaration | undefined;
|
|
10
|
+
export { declarationOf, namedDeclarationOf };
|
package/dist/declarations.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as
|
|
1
|
+
import{AST_NODE_TYPES as r}from"@typescript-eslint/utils";import{calleeName as o}from"./ast.js";const d=new Set(["defineApplication","defineCondition","defineFeature","defineHostContract","defineLinkHandlers","defineModel","definePipe","definePort","defineRegistry","defineScreen","defineSlot","defineSurface","defineSwitchSlot"]),u=new Set([...d,"defineLink"]);function a(e){const[n]=e.arguments;if(!(n===void 0||n.type===r.SpreadElement))return n}function f(e,n){const i=o(e);if(i===void 0||!n.has(i))return;const t=a(e);return t===void 0?void 0:{id:t,name:i}}function c(e){return f(e,d)}function S(e){return f(e,u)}export{c as declarationOf,S as namedDeclarationOf};
|
|
2
2
|
//# sourceMappingURL=declarations.js.map
|
package/dist/declarations.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"declarations.js","sources":["../src/declarations.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { calleeName } from './ast';\n\n/**\n * Every declaration of the public vocabulary
|
|
1
|
+
{"version":3,"file":"declarations.js","sources":["../src/declarations.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { calleeName } from './ast';\n\n/**\n * Every declaration of the public vocabulary whose id is a Core declaration id, taken from the three safe entries\n * and from the optional Navigation package, whose screen, surface and handler registry all pass their id through\n * `declarationId` (D310, D311, D313). D280 gives them one form: the id is the first argument, whatever else the\n * declaration carries. The kernel's own `defineModule`, `defineCallTarget` and `defineCallLane` are absent because\n * an author never writes them.\n */\nconst DECLARATIONS = new Set([\n 'defineApplication',\n 'defineCondition',\n 'defineFeature',\n 'defineHostContract',\n 'defineLinkHandlers',\n 'defineModel',\n 'definePipe',\n 'definePort',\n 'defineRegistry',\n 'defineScreen',\n 'defineSlot',\n 'defineSurface',\n 'defineSwitchSlot',\n]);\n\n/**\n * The same declarations plus the one with a grammar of its own: a link id is a canonical lower-case path segment,\n * not a Core declaration id, so `id-naming` must not measure it — and a reserved prefix would refuse it outright.\n * Naming itself first is nonetheless its law too, so a computed link id is as unfindable as any other (D312, D313).\n */\nconst NAMED_DECLARATIONS = new Set([...DECLARATIONS, 'defineLink']);\n\ntype Declaration = {\n readonly id: TSESTree.Node;\n readonly name: string;\n};\n\nfunction idOfArgument(node: TSESTree.CallExpression): TSESTree.Node | undefined {\n const [first] = node.arguments;\n\n if (first === undefined || first.type === AST_NODE_TYPES.SpreadElement) return undefined;\n\n return first;\n}\n\nfunction declarationIn(node: TSESTree.CallExpression, names: ReadonlySet<string>): Declaration | undefined {\n const name = calleeName(node);\n\n if (name === undefined || !names.has(name)) return undefined;\n\n const id = idOfArgument(node);\n\n return id === undefined ? undefined : { id, name };\n}\n\n/** The name and the id expression of a call whose id follows the Core grammar, or `undefined` for anything else. */\nfunction declarationOf(node: TSESTree.CallExpression): Declaration | undefined {\n return declarationIn(node, DECLARATIONS);\n}\n\n/** The same, for every declaration that names itself first, whatever grammar its own id follows. */\nfunction namedDeclarationOf(node: TSESTree.CallExpression): Declaration | undefined {\n return declarationIn(node, NAMED_DECLARATIONS);\n}\n\nexport { declarationOf, namedDeclarationOf };\n"],"names":["DECLARATIONS","NAMED_DECLARATIONS","idOfArgument","node","first","AST_NODE_TYPES","declarationIn","names","name","calleeName","id","declarationOf","namedDeclarationOf"],"mappings":"gGAYA,MAAMA,EAAe,IAAI,IAAI,CAC3B,oBACA,kBACA,gBACA,qBACA,qBACA,cACA,aACA,aACA,iBACA,eACA,aACA,gBACA,kBACD,CAAA,EAOKC,EAAqB,IAAI,IAAI,CAAC,GAAGD,EAAc,YAAY,CAAC,EAOlE,SAASE,EAAaC,EAA6B,CACjD,KAAM,CAACC,CAAK,EAAID,EAAK,UAErB,GAAI,EAAAC,IAAU,QAAaA,EAAM,OAASC,EAAe,eAEzD,OAAOD,CACT,CAEA,SAASE,EAAcH,EAA+BI,EAA0B,CAC9E,MAAMC,EAAOC,EAAWN,CAAI,EAE5B,GAAIK,IAAS,QAAa,CAACD,EAAM,IAAIC,CAAI,EAAG,OAE5C,MAAME,EAAKR,EAAaC,CAAI,EAE5B,OAAOO,IAAO,OAAY,OAAY,CAAE,GAAAA,EAAI,KAAAF,CAAI,CAClD,CAGA,SAASG,EAAcR,EAA6B,CAClD,OAAOG,EAAcH,EAAMH,CAAY,CACzC,CAGA,SAASY,EAAmBT,EAA6B,CACvD,OAAOG,EAAcH,EAAMF,CAAkB,CAC/C"}
|
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,9 @@ declare const plugin: {
|
|
|
38
38
|
'no-internal-imports': import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalImport", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
39
39
|
name: string;
|
|
40
40
|
};
|
|
41
|
+
'no-redundant-const-tuple': import("@typescript-eslint/utils/ts-eslint").RuleModule<"redundantConstTuple", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
42
|
+
name: string;
|
|
43
|
+
};
|
|
41
44
|
'no-snapshot-in-update': import("@typescript-eslint/utils/ts-eslint").RuleModule<"snapshotInUpdate" | "writeFromPrevious", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
42
45
|
name: string;
|
|
43
46
|
};
|
|
@@ -47,6 +50,9 @@ declare const plugin: {
|
|
|
47
50
|
'no-subscribe-outside-models': import("@typescript-eslint/utils/ts-eslint").RuleModule<"handWrittenSubscription", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
48
51
|
name: string;
|
|
49
52
|
};
|
|
53
|
+
'prefer-effect-current': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferEffectCurrent", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
54
|
+
name: string;
|
|
55
|
+
};
|
|
50
56
|
'prefer-model-selection': import("@typescript-eslint/utils/ts-eslint").RuleModule<"separateHooks", readonly [{
|
|
51
57
|
readonly threshold: number;
|
|
52
58
|
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
@@ -95,9 +101,11 @@ declare const configuredPlugin: {
|
|
|
95
101
|
'opetope/layer-placement': "off";
|
|
96
102
|
'opetope/no-command-in-deps': "error";
|
|
97
103
|
'opetope/no-internal-imports': "error";
|
|
104
|
+
'opetope/no-redundant-const-tuple': "error";
|
|
98
105
|
'opetope/no-snapshot-in-update': "error";
|
|
99
106
|
'opetope/no-snapshot-read-in-render': "error";
|
|
100
107
|
'opetope/no-subscribe-outside-models': "off";
|
|
108
|
+
'opetope/prefer-effect-current': "error";
|
|
101
109
|
'opetope/prefer-model-selection': "off";
|
|
102
110
|
'opetope/require-declared-models': "error";
|
|
103
111
|
'opetope/require-literal-id': "error";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{defineFeaturePropertyOrder as p}from"./rules/define-feature-property-order.js";import{idNaming as s}from"./rules/id-naming.js";import{layerPlacement as i}from"./rules/layer-placement.js";import{noCommandInDeps as l}from"./rules/no-command-in-deps.js";import{noInternalImports as m}from"./rules/no-internal-imports.js";import{
|
|
1
|
+
import{defineFeaturePropertyOrder as p}from"./rules/define-feature-property-order.js";import{idNaming as s}from"./rules/id-naming.js";import{layerPlacement as i}from"./rules/layer-placement.js";import{noCommandInDeps as l}from"./rules/no-command-in-deps.js";import{noInternalImports as m}from"./rules/no-internal-imports.js";import{noRedundantConstTuple as a}from"./rules/no-redundant-const-tuple.js";import{noSnapshotInUpdate as d}from"./rules/no-snapshot-in-update.js";import{noSnapshotReadInRender as c}from"./rules/no-snapshot-read-in-render.js";import{noSubscribeOutsideModels as f}from"./rules/no-subscribe-outside-models.js";import{preferEffectCurrent as u}from"./rules/prefer-effect-current.js";import{preferModelSelection as g}from"./rules/prefer-model-selection.js";import{requireDeclaredModels as b}from"./rules/require-declared-models.js";import{requireLiteralId as h}from"./rules/require-literal-id.js";import{whenPredicate as y}from"./rules/when-predicate.js";const I=["integration","models","ui"],S=["**/*.ts","**/*.tsx"],q=["**/__tests__/**","**/*.spec.ts","**/*.spec.tsx"],E={meta:{name:"@opetope/lint",version:"0.9.5"},rules:{"define-feature-property-order":p,"id-naming":s,"layer-placement":i,"no-command-in-deps":l,"no-internal-imports":m,"no-redundant-const-tuple":a,"no-snapshot-in-update":d,"no-snapshot-read-in-render":c,"no-subscribe-outside-models":f,"prefer-effect-current":u,"prefer-model-selection":g,"require-declared-models":b,"require-literal-id":h,"when-predicate":y}},r=E,_={name:"opetope/recommended",plugins:{opetope:r},rules:{"opetope/define-feature-property-order":"error","opetope/id-naming":"error","opetope/layer-placement":"off","opetope/no-command-in-deps":"error","opetope/no-internal-imports":"error","opetope/no-redundant-const-tuple":"error","opetope/no-snapshot-in-update":"error","opetope/no-snapshot-read-in-render":"error","opetope/no-subscribe-outside-models":"off","opetope/prefer-effect-current":"error","opetope/prefer-model-selection":"off","opetope/require-declared-models":"error","opetope/require-literal-id":"error","opetope/when-predicate":"error"}},j=e=>I.flatMap(o=>{const t=e[o]??[];return t.length===0?[]:[{files:[...t],name:`opetope/layers-${o}`,plugins:{opetope:r},rules:{"opetope/layer-placement":["error",{layer:o}]}}]}),x=({models:e=[],tests:o=q})=>e.length===0?[]:[{files:S,name:"opetope/layers-subscriptions",plugins:{opetope:r},rules:{"opetope/no-subscribe-outside-models":"error"}},{files:[...e,...o],name:"opetope/layers-subscriptions-allowed",rules:{"opetope/no-subscribe-outside-models":"off"}}],L=e=>[...j(e),...x(e)],P=({allow:e=[],files:o=["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"]}={})=>[{files:[...o],name:"opetope/internal-imports",plugins:{opetope:r},rules:{"opetope/no-internal-imports":"error"}},...e.length===0?[]:[{files:[...e],name:"opetope/internal-imports-allowed",rules:{"opetope/no-internal-imports":"off"}}]],R=Object.assign(r,{configs:{internalImports:P,layers:L,recommended:_}}),n=R;export{n as default,n as opetopeLint};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import type { ESLint, Linter } from 'eslint';\n\nimport { defineFeaturePropertyOrder } from './rules/define-feature-property-order';\nimport { idNaming } from './rules/id-naming';\nimport { layerPlacement } from './rules/layer-placement';\nimport { noCommandInDeps } from './rules/no-command-in-deps';\nimport { noInternalImports } from './rules/no-internal-imports';\nimport { noSnapshotInUpdate } from './rules/no-snapshot-in-update';\nimport { noSnapshotReadInRender } from './rules/no-snapshot-read-in-render';\nimport { noSubscribeOutsideModels } from './rules/no-subscribe-outside-models';\nimport { preferModelSelection } from './rules/prefer-model-selection';\nimport { requireDeclaredModels } from './rules/require-declared-models';\nimport { requireLiteralId } from './rules/require-literal-id';\nimport { whenPredicate } from './rules/when-predicate';\n\n/**\n * The layers a host names for itself, as globs of the files that hold each one, with the tests that are allowed\n * to reach past a layer. `tests` defaults to the usual two shapes of a test path.\n */\ntype LayerOptions = {\n readonly integration?: readonly string[];\n readonly models?: readonly string[];\n readonly tests?: readonly string[];\n readonly ui?: readonly string[];\n};\n\ntype InternalImportOptions = {\n readonly allow?: readonly string[];\n readonly files?: readonly string[];\n};\n\nconst LAYERS = ['integration', 'models', 'ui'] as const;\nconst SOURCE_FILES = ['**/*.ts', '**/*.tsx'];\nconst TEST_FILES = ['**/__tests__/**', '**/*.spec.ts', '**/*.spec.tsx'];\n\nconst plugin = {\n meta: { name: '@opetope/lint', version: '0.9.
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import type { ESLint, Linter } from 'eslint';\n\nimport { defineFeaturePropertyOrder } from './rules/define-feature-property-order';\nimport { idNaming } from './rules/id-naming';\nimport { layerPlacement } from './rules/layer-placement';\nimport { noCommandInDeps } from './rules/no-command-in-deps';\nimport { noInternalImports } from './rules/no-internal-imports';\nimport { noRedundantConstTuple } from './rules/no-redundant-const-tuple';\nimport { noSnapshotInUpdate } from './rules/no-snapshot-in-update';\nimport { noSnapshotReadInRender } from './rules/no-snapshot-read-in-render';\nimport { noSubscribeOutsideModels } from './rules/no-subscribe-outside-models';\nimport { preferEffectCurrent } from './rules/prefer-effect-current';\nimport { preferModelSelection } from './rules/prefer-model-selection';\nimport { requireDeclaredModels } from './rules/require-declared-models';\nimport { requireLiteralId } from './rules/require-literal-id';\nimport { whenPredicate } from './rules/when-predicate';\n\n/**\n * The layers a host names for itself, as globs of the files that hold each one, with the tests that are allowed\n * to reach past a layer. `tests` defaults to the usual two shapes of a test path.\n */\ntype LayerOptions = {\n readonly integration?: readonly string[];\n readonly models?: readonly string[];\n readonly tests?: readonly string[];\n readonly ui?: readonly string[];\n};\n\ntype InternalImportOptions = {\n readonly allow?: readonly string[];\n readonly files?: readonly string[];\n};\n\nconst LAYERS = ['integration', 'models', 'ui'] as const;\nconst SOURCE_FILES = ['**/*.ts', '**/*.tsx'];\nconst TEST_FILES = ['**/__tests__/**', '**/*.spec.ts', '**/*.spec.tsx'];\n\nconst plugin = {\n meta: { name: '@opetope/lint', version: '0.9.5' },\n rules: {\n 'define-feature-property-order': defineFeaturePropertyOrder,\n 'id-naming': idNaming,\n 'layer-placement': layerPlacement,\n 'no-command-in-deps': noCommandInDeps,\n 'no-internal-imports': noInternalImports,\n 'no-redundant-const-tuple': noRedundantConstTuple,\n 'no-snapshot-in-update': noSnapshotInUpdate,\n 'no-snapshot-read-in-render': noSnapshotReadInRender,\n 'no-subscribe-outside-models': noSubscribeOutsideModels,\n 'prefer-effect-current': preferEffectCurrent,\n 'prefer-model-selection': preferModelSelection,\n 'require-declared-models': requireDeclaredModels,\n 'require-literal-id': requireLiteralId,\n 'when-predicate': whenPredicate,\n },\n};\n\n/**\n * The laws every Opetope author writes against, with no assumption about where a host keeps its files. A rule that\n * needs such an assumption is off here and arrives through `layers`. The namespace is `opetope`, so a rule reads\n * `opetope/when-predicate` wherever a host names it.\n */\n// Rules use the parser's typed AST internally. The public bridge exposes the\n// supported ESLint rule API; package smoke tests execute it under both linters.\ntype NativeRule = NonNullable<ESLint.Plugin['rules']>[string];\ntype NativeRules = {\n [Name in keyof typeof plugin.rules]: Omit<(typeof plugin.rules)[Name], 'create'> & NativeRule;\n};\nconst nativePlugin = plugin as unknown as { meta: typeof plugin.meta; rules: NativeRules };\n\nconst recommended = {\n name: 'opetope/recommended',\n plugins: { opetope: nativePlugin },\n rules: {\n 'opetope/define-feature-property-order': 'error',\n 'opetope/id-naming': 'error',\n 'opetope/layer-placement': 'off',\n 'opetope/no-command-in-deps': 'error',\n 'opetope/no-internal-imports': 'error',\n 'opetope/no-redundant-const-tuple': 'error',\n 'opetope/no-snapshot-in-update': 'error',\n 'opetope/no-snapshot-read-in-render': 'error',\n 'opetope/no-subscribe-outside-models': 'off',\n 'opetope/prefer-effect-current': 'error',\n 'opetope/prefer-model-selection': 'off',\n 'opetope/require-declared-models': 'error',\n 'opetope/require-literal-id': 'error',\n 'opetope/when-predicate': 'error',\n },\n} satisfies Linter.Config;\n\n/**\n * Placement is a convention of the project that names its layers, not a law of the framework: a layer without a\n * glob is a layer this host does not have, and it produces no configuration.\n */\nconst placement = (options: LayerOptions): readonly Linter.Config[] =>\n LAYERS.flatMap(layer => {\n const files = options[layer] ?? [];\n\n if (files.length === 0) return [];\n\n return [\n {\n files: [...files],\n name: `opetope/layers-${layer}`,\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/layer-placement': ['error', { layer }] },\n } satisfies Linter.Config,\n ];\n });\n\n/**\n * A subscription is written where the state lives, so this pair arrives only with the model layer that owns it. The\n * restriction covers every source file and steps aside in that layer and in tests: a file no layer claims is\n * covered rather than forgotten.\n */\nconst subscriptions = ({ models = [], tests = TEST_FILES }: LayerOptions): readonly Linter.Config[] =>\n models.length === 0\n ? []\n : [\n {\n files: SOURCE_FILES,\n name: 'opetope/layers-subscriptions',\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/no-subscribe-outside-models': 'error' },\n },\n {\n files: [...models, ...tests],\n name: 'opetope/layers-subscriptions-allowed',\n rules: { 'opetope/no-subscribe-outside-models': 'off' },\n },\n ];\n\nconst layers = (options: LayerOptions): readonly Linter.Config[] => [...placement(options), ...subscriptions(options)];\n\n/**\n * Only library implementation and explicitly named host integration can cross the internal ABI. A dedicated rule\n * leaves the project's `no-restricted-imports` untouched and also covers literal dynamic imports in JS tests.\n * Allowed files explicitly override `recommended`, which enables this rule as well.\n */\nconst internalImports = ({\n allow = [],\n files = ['**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}'],\n}: InternalImportOptions = {}): readonly Linter.Config[] => [\n {\n files: [...files],\n name: 'opetope/internal-imports',\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/no-internal-imports': 'error' },\n },\n ...(allow.length === 0\n ? []\n : [\n {\n files: [...allow],\n name: 'opetope/internal-imports-allowed',\n rules: { 'opetope/no-internal-imports': 'off' as const },\n },\n ]),\n];\n\nconst configuredPlugin = Object.assign(nativePlugin, {\n configs: { internalImports, layers, recommended },\n});\n\n// ESLint consumes the rule map. Config factories are additional plugin helpers;\n// expose both contracts without requiring casts at native flat-config call sites.\n// oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- Required by native ESLint config typetests.\nconst opetopeLint = configuredPlugin as unknown as typeof configuredPlugin & Pick<ESLint.Plugin, 'configs'>;\n\n/*\n * Two shapes of one object. The default is what a tool reads when it loads a plugin by path — Oxlint's `jsPlugins`\n * takes `(await import(path)).default` — and what an ESLint flat config imports by convention; the named export is\n * for a config that prefers to say which binding it takes.\n */\n// oxlint-disable-next-line import/no-default-export\n// eslint-disable-next-line import-x/no-default-export\nexport default opetopeLint;\nexport { opetopeLint };\n"],"names":["LAYERS","SOURCE_FILES","TEST_FILES","plugin","defineFeaturePropertyOrder","idNaming","layerPlacement","noCommandInDeps","noInternalImports","noRedundantConstTuple","noSnapshotInUpdate","noSnapshotReadInRender","noSubscribeOutsideModels","preferEffectCurrent","preferModelSelection","requireDeclaredModels","requireLiteralId","whenPredicate","nativePlugin","recommended","placement","options","layer","files","subscriptions","models","tests","layers","internalImports","allow","configuredPlugin","opetopeLint"],"mappings":"88BAiCA,MAAMA,EAAS,CAAC,cAAe,SAAU,IAAI,EACvCC,EAAe,CAAC,UAAW,UAAU,EACrCC,EAAa,CAAC,kBAAmB,eAAgB,eAAe,EAEhEC,EAAS,CACb,KAAM,CAAE,KAAM,gBAAiB,QAAS,OAAO,EAC/C,MAAO,CACL,gCAAiCC,EACjC,YAAaC,EACb,kBAAmBC,EACnB,qBAAsBC,EACtB,sBAAuBC,EACvB,2BAA4BC,EAC5B,wBAAyBC,EACzB,6BAA8BC,EAC9B,8BAA+BC,EAC/B,wBAAyBC,EACzB,yBAA0BC,EAC1B,0BAA2BC,EAC3B,qBAAsBC,EACtB,iBAAkBC,CACnB,GAcGC,EAAef,EAEfgB,EAAc,CAClB,KAAM,sBACN,QAAS,CAAE,QAASD,CAAY,EAChC,MAAO,CACL,wCAAyC,QACzC,oBAAqB,QACrB,0BAA2B,MAC3B,6BAA8B,QAC9B,8BAA+B,QAC/B,mCAAoC,QACpC,gCAAiC,QACjC,qCAAsC,QACtC,sCAAuC,MACvC,gCAAiC,QACjC,iCAAkC,MAClC,kCAAmC,QACnC,6BAA8B,QAC9B,yBAA0B,OAC3B,GAOGE,EAAaC,GACjBrB,EAAO,QAAQsB,GAAQ,CACrB,MAAMC,EAAQF,EAAQC,CAAK,GAAK,CAAA,EAEhC,OAAIC,EAAM,SAAW,EAAU,CAAA,EAExB,CACL,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,kBAAkBD,CAAK,GAC7B,QAAS,CAAE,QAASJ,CAAY,EAChC,MAAO,CAAE,0BAA2B,CAAC,QAAS,CAAE,MAAAI,CAAK,CAAE,CAAC,CACjC,EAE7B,CAAC,EAOGE,EAAgB,CAAC,CAAE,OAAAC,EAAS,CAAA,EAAI,MAAAC,EAAQxB,CAAU,IACtDuB,EAAO,SAAW,EACd,CAAA,EACA,CACE,CACE,MAAOxB,EACP,KAAM,+BACN,QAAS,CAAE,QAASiB,CAAY,EAChC,MAAO,CAAE,sCAAuC,OAAO,CACxD,EACD,CACE,MAAO,CAAC,GAAGO,EAAQ,GAAGC,CAAK,EAC3B,KAAM,uCACN,MAAO,CAAE,sCAAuC,KAAK,CACtD,GAGHC,EAAUN,GAAoD,CAAC,GAAGD,EAAUC,CAAO,EAAG,GAAGG,EAAcH,CAAO,CAAC,EAO/GO,EAAkB,CAAC,CACvB,MAAAC,EAAQ,CAAA,EACR,MAAAN,EAAQ,CAAC,sCAAsC,CAAC,EACvB,KAAiC,CAC1D,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,2BACN,QAAS,CAAE,QAASL,CAAY,EAChC,MAAO,CAAE,8BAA+B,OAAO,CAChD,EACD,GAAIW,EAAM,SAAW,EACjB,CAAA,EACA,CACE,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,mCACN,MAAO,CAAE,8BAA+B,KAAc,CACvD,IAIHC,EAAmB,OAAO,OAAOZ,EAAc,CACnD,QAAS,CAAE,gBAAAU,EAAiB,OAAAD,EAAQ,YAAAR,CAAW,CAChD,CAAA,EAKKY,EAAcD"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{calleeName as o}from"../ast.js";import{createRule as r}from"../rule.js";const d=new Map([["defineCondition",["integration","ui"]],["defineFeature",["integration"]],["defineHostContract",["integration","ui"]],["defineModel",["models","ui"]],["definePipe",["integration","ui"]],["definePort",["integration","ui"]],["defineRegistry",["integration","ui"]],["defineSlot",["integration","ui"]],["defineSwitchSlot",["integration","ui"]]]),l=r({create(a,[{layer:i}]){return i===void 0?{}:{CallExpression(n){const e=o(n),t=e===void 0?void 0:d.get(e);e===void 0||t===void 0||t.includes(i)||a.report({data:{allowed:t.join(" or "),declaration:e,layer:i},messageId:"misplacedDeclaration",node:n.callee})}}},meta:{defaultOptions:[{}],docs:{description:"Each declaration is written in the layer that owns it, as the host named its layers."},messages:{misplacedDeclaration:"{{declaration}} is written in the {{layer}} layer, and it belongs to {{allowed}}."},schema:[{additionalProperties:!1,properties:{layer:{enum:["integration","models","ui"],type:"string"}},type:"object"}],type:"problem"},name:"layer-placement"});export{l as layerPlacement};
|
|
1
|
+
import{calleeName as o}from"../ast.js";import{createRule as r}from"../rule.js";const d=new Map([["defineCondition",["integration","ui"]],["defineFeature",["integration"]],["defineHostContract",["integration","ui"]],["defineModel",["models","ui"]],["definePipe",["integration","ui"]],["definePort",["integration","ui"]],["defineRegistry",["integration","ui"]],["defineSlot",["integration","ui"]],["defineSurface",["integration","ui"]],["defineSwitchSlot",["integration","ui"]]]),l=r({create(a,[{layer:i}]){return i===void 0?{}:{CallExpression(n){const e=o(n),t=e===void 0?void 0:d.get(e);e===void 0||t===void 0||t.includes(i)||a.report({data:{allowed:t.join(" or "),declaration:e,layer:i},messageId:"misplacedDeclaration",node:n.callee})}}},meta:{defaultOptions:[{}],docs:{description:"Each declaration is written in the layer that owns it, as the host named its layers."},messages:{misplacedDeclaration:"{{declaration}} is written in the {{layer}} layer, and it belongs to {{allowed}}."},schema:[{additionalProperties:!1,properties:{layer:{enum:["integration","models","ui"],type:"string"}},type:"object"}],type:"problem"},name:"layer-placement"});export{l as layerPlacement};
|
|
2
2
|
//# sourceMappingURL=layer-placement.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layer-placement.js","sources":["../../src/rules/layer-placement.ts"],"sourcesContent":["import { calleeName } from '../ast';\nimport { createRule } from '../rule';\n\ntype Layer = 'integration' | 'models' | 'ui';\ntype MessageIds = 'misplacedDeclaration';\ntype Options = readonly [{ readonly layer?: Layer }];\n\n/**\n * Where each declaration belongs, in the layout the guide describes: a feature composes, so it lives in the\n * integration layer; a model is its own layer or a UI contract of the feature that grants it; a contract is written\n * by the side that owns the extension point, which is the integration layer or the UI contracts beside it.\n */\nconst PLACEMENT = new Map<string, readonly Layer[]>([\n ['defineCondition', ['integration', 'ui']],\n ['defineFeature', ['integration']],\n ['defineHostContract', ['integration', 'ui']],\n ['defineModel', ['models', 'ui']],\n ['definePipe', ['integration', 'ui']],\n ['definePort', ['integration', 'ui']],\n ['defineRegistry', ['integration', 'ui']],\n ['defineSlot', ['integration', 'ui']],\n ['defineSwitchSlot', ['integration', 'ui']],\n]);\n\nconst layerPlacement = createRule<Options, MessageIds>({\n create(context, [{ layer }]) {\n if (layer === undefined) return {};\n\n return {\n CallExpression(node): void {\n const name = calleeName(node);\n const allowed = name === undefined ? undefined : PLACEMENT.get(name);\n\n if (name === undefined || allowed === undefined || allowed.includes(layer)) return;\n\n context.report({\n data: { allowed: allowed.join(' or '), declaration: name, layer },\n messageId: 'misplacedDeclaration',\n node: node.callee,\n });\n },\n };\n },\n meta: {\n defaultOptions: [{}],\n docs: {\n description: 'Each declaration is written in the layer that owns it, as the host named its layers.',\n },\n messages: {\n misplacedDeclaration: '{{declaration}} is written in the {{layer}} layer, and it belongs to {{allowed}}.',\n },\n schema: [\n {\n additionalProperties: false,\n properties: {\n layer: { enum: ['integration', 'models', 'ui'], type: 'string' },\n },\n type: 'object',\n },\n ],\n type: 'problem',\n },\n name: 'layer-placement',\n});\n\nexport { layerPlacement };\n"],"names":["PLACEMENT","layerPlacement","createRule","context","layer","node","name","calleeName","allowed"],"mappings":"+
|
|
1
|
+
{"version":3,"file":"layer-placement.js","sources":["../../src/rules/layer-placement.ts"],"sourcesContent":["import { calleeName } from '../ast';\nimport { createRule } from '../rule';\n\ntype Layer = 'integration' | 'models' | 'ui';\ntype MessageIds = 'misplacedDeclaration';\ntype Options = readonly [{ readonly layer?: Layer }];\n\n/**\n * Where each declaration belongs, in the layout the guide describes: a feature composes, so it lives in the\n * integration layer; a model is its own layer or a UI contract of the feature that grants it; a contract is written\n * by the side that owns the extension point, which is the integration layer or the UI contracts beside it. A\n * surface is such a contract — it is a factory over slot families, like `defineSwitchSlot` — while `defineScreen`\n * is not: a screen is a destination the model layer declares and names (D310, D311, D315).\n */\nconst PLACEMENT = new Map<string, readonly Layer[]>([\n ['defineCondition', ['integration', 'ui']],\n ['defineFeature', ['integration']],\n ['defineHostContract', ['integration', 'ui']],\n ['defineModel', ['models', 'ui']],\n ['definePipe', ['integration', 'ui']],\n ['definePort', ['integration', 'ui']],\n ['defineRegistry', ['integration', 'ui']],\n ['defineSlot', ['integration', 'ui']],\n ['defineSurface', ['integration', 'ui']],\n ['defineSwitchSlot', ['integration', 'ui']],\n]);\n\nconst layerPlacement = createRule<Options, MessageIds>({\n create(context, [{ layer }]) {\n if (layer === undefined) return {};\n\n return {\n CallExpression(node): void {\n const name = calleeName(node);\n const allowed = name === undefined ? undefined : PLACEMENT.get(name);\n\n if (name === undefined || allowed === undefined || allowed.includes(layer)) return;\n\n context.report({\n data: { allowed: allowed.join(' or '), declaration: name, layer },\n messageId: 'misplacedDeclaration',\n node: node.callee,\n });\n },\n };\n },\n meta: {\n defaultOptions: [{}],\n docs: {\n description: 'Each declaration is written in the layer that owns it, as the host named its layers.',\n },\n messages: {\n misplacedDeclaration: '{{declaration}} is written in the {{layer}} layer, and it belongs to {{allowed}}.',\n },\n schema: [\n {\n additionalProperties: false,\n properties: {\n layer: { enum: ['integration', 'models', 'ui'], type: 'string' },\n },\n type: 'object',\n },\n ],\n type: 'problem',\n },\n name: 'layer-placement',\n});\n\nexport { layerPlacement };\n"],"names":["PLACEMENT","layerPlacement","createRule","context","layer","node","name","calleeName","allowed"],"mappings":"+EAcA,MAAMA,EAAY,IAAI,IAA8B,CAClD,CAAC,kBAAmB,CAAC,cAAe,IAAI,CAAC,EACzC,CAAC,gBAAiB,CAAC,aAAa,CAAC,EACjC,CAAC,qBAAsB,CAAC,cAAe,IAAI,CAAC,EAC5C,CAAC,cAAe,CAAC,SAAU,IAAI,CAAC,EAChC,CAAC,aAAc,CAAC,cAAe,IAAI,CAAC,EACpC,CAAC,aAAc,CAAC,cAAe,IAAI,CAAC,EACpC,CAAC,iBAAkB,CAAC,cAAe,IAAI,CAAC,EACxC,CAAC,aAAc,CAAC,cAAe,IAAI,CAAC,EACpC,CAAC,gBAAiB,CAAC,cAAe,IAAI,CAAC,EACvC,CAAC,mBAAoB,CAAC,cAAe,IAAI,CAAC,CAC3C,CAAA,EAEKC,EAAiBC,EAAgC,CACrD,OAAOC,EAAS,CAAC,CAAE,MAAAC,CAAK,CAAE,EAAC,CACzB,OAAIA,IAAU,OAAkB,CAAA,EAEzB,CACL,eAAeC,EAAI,CACjB,MAAMC,EAAOC,EAAWF,CAAI,EACtBG,EAAUF,IAAS,OAAY,OAAYN,EAAU,IAAIM,CAAI,EAE/DA,IAAS,QAAaE,IAAY,QAAaA,EAAQ,SAASJ,CAAK,GAEzED,EAAQ,OAAO,CACb,KAAM,CAAE,QAASK,EAAQ,KAAK,MAAM,EAAG,YAAaF,EAAM,MAAAF,CAAK,EAC/D,UAAW,uBACX,KAAMC,EAAK,MACZ,CAAA,CACH,EAEJ,EACA,KAAM,CACJ,eAAgB,CAAC,CAAA,CAAE,EACnB,KAAM,CACJ,YAAa,sFACd,EACD,SAAU,CACR,qBAAsB,mFACvB,EACD,OAAQ,CACN,CACE,qBAAsB,GACtB,WAAY,CACV,MAAO,CAAE,KAAM,CAAC,cAAe,SAAU,IAAI,EAAG,KAAM,QAAQ,CAC/D,EACD,KAAM,QACP,CACF,EACD,KAAM,SACP,EACD,KAAM,iBACP,CAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{AST_NODE_TYPES as s}from"@typescript-eslint/utils";import{isApi as o}from"../model-bindings.js";import{createRule as i}from"../rule.js";function a(e){if(e?.type!==s.TSAsExpression)return;const{typeAnnotation:n}=e;return n.type===s.TSTypeReference&&n.typeName.type===s.Identifier&&n.typeName.name==="const"&&e.expression.type===s.ArrayExpression?e:void 0}function p(e,n){const t=n.expression.range[1];return e.getCommentsInside(n).some(r=>r.range[0]>=t)}const u=i({create(e){return{CallExpression(n){const t=a(n.arguments[0]);t===void 0||!o(e.sourceCode,n.callee,"@opetope/core","derive",!0)||e.report({fix:p(e.sourceCode,t)?null:r=>r.removeRange([t.expression.range[1],t.range[1]]),messageId:"redundantConstTuple",node:t.typeAnnotation})}}},meta:{docs:{description:"The tuple source of `derive` infers itself, so `as const` on it states nothing."},fixable:"code",messages:{redundantConstTuple:"The tuple source of `derive` is a `const` type parameter, so it already infers exact values: `as const` here changes nothing (D279, D309)."},schema:[],type:"suggestion"},name:"no-redundant-const-tuple"});export{u as noRedundantConstTuple};
|
|
2
|
+
//# sourceMappingURL=no-redundant-const-tuple.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-redundant-const-tuple.js","sources":["../../src/rules/no-redundant-const-tuple.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { isApi } from '../model-bindings';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'redundantConstTuple';\n\n/** `as const` and nothing else: any other assertion on the same tuple says something this rule is not about. */\nfunction constAssertionOf(node: TSESTree.Node | undefined): TSESTree.TSAsExpression | undefined {\n if (node?.type !== AST_NODE_TYPES.TSAsExpression) return undefined;\n\n const { typeAnnotation } = node;\n const named =\n typeAnnotation.type === AST_NODE_TYPES.TSTypeReference &&\n typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier &&\n typeAnnotation.typeName.name === 'const';\n\n return named && node.expression.type === AST_NODE_TYPES.ArrayExpression ? node : undefined;\n}\n\n/**\n * A comment between the tuple and the assertion belongs to neither, and removing the range would take it with the\n * words it explains, so such an assertion is reported without a fix.\n */\nfunction hasTrailingComment(source: TSESLint.SourceCode, node: TSESTree.TSAsExpression): boolean {\n const from = node.expression.range[1];\n\n return source.getCommentsInside(node).some(comment => comment.range[0] >= from);\n}\n\nconst noRedundantConstTuple = createRule<[], MessageIds>({\n create(context) {\n return {\n CallExpression(node): void {\n // The shape decides first and the import second: a call that cannot be a tuple `derive` pays for nothing.\n const assertion = constAssertionOf(node.arguments[0]);\n\n // The package name exactly, unlike `no-command-in-deps`: this rule rewrites code, and a host's own `derive`\n // has no const type parameter to make the assertion redundant, so removing it there would change inference\n // in silence.\n if (assertion === undefined || !isApi(context.sourceCode, node.callee, '@opetope/core', 'derive', true)) return;\n\n context.report({\n fix: hasTrailingComment(context.sourceCode, assertion)\n ? null\n : fixer => fixer.removeRange([assertion.expression.range[1], assertion.range[1]]),\n messageId: 'redundantConstTuple',\n node: assertion.typeAnnotation,\n });\n },\n };\n },\n meta: {\n docs: {\n description: 'The tuple source of `derive` infers itself, so `as const` on it states nothing.',\n },\n fixable: 'code',\n messages: {\n redundantConstTuple:\n 'The tuple source of `derive` is a `const` type parameter, so it already infers exact values: `as const` here changes nothing (D279, D309).',\n },\n schema: [],\n type: 'suggestion',\n },\n name: 'no-redundant-const-tuple',\n});\n\nexport { noRedundantConstTuple };\n"],"names":["constAssertionOf","node","AST_NODE_TYPES","typeAnnotation","hasTrailingComment","source","from","comment","noRedundantConstTuple","createRule","context","assertion","isApi","fixer"],"mappings":"+IASA,SAASA,EAAiBC,EAA+B,CACvD,GAAIA,GAAM,OAASC,EAAe,eAAgB,OAElD,KAAM,CAAE,eAAAC,CAAc,EAAKF,EAM3B,OAJEE,EAAe,OAASD,EAAe,iBACvCC,EAAe,SAAS,OAASD,EAAe,YAChDC,EAAe,SAAS,OAAS,SAEnBF,EAAK,WAAW,OAASC,EAAe,gBAAkBD,EAAO,MACnF,CAMA,SAASG,EAAmBC,EAA6BJ,EAA6B,CACpF,MAAMK,EAAOL,EAAK,WAAW,MAAM,CAAC,EAEpC,OAAOI,EAAO,kBAAkBJ,CAAI,EAAE,KAAKM,GAAWA,EAAQ,MAAM,CAAC,GAAKD,CAAI,CAChF,CAEA,MAAME,EAAwBC,EAA2B,CACvD,OAAOC,EAAO,CACZ,MAAO,CACL,eAAeT,EAAI,CAEjB,MAAMU,EAAYX,EAAiBC,EAAK,UAAU,CAAC,CAAC,EAKhDU,IAAc,QAAa,CAACC,EAAMF,EAAQ,WAAYT,EAAK,OAAQ,gBAAiB,SAAU,EAAI,GAEtGS,EAAQ,OAAO,CACb,IAAKN,EAAmBM,EAAQ,WAAYC,CAAS,EACjD,KACAE,GAASA,EAAM,YAAY,CAACF,EAAU,WAAW,MAAM,CAAC,EAAGA,EAAU,MAAM,CAAC,CAAC,CAAC,EAClF,UAAW,sBACX,KAAMA,EAAU,cACjB,CAAA,CACH,EAEJ,EACA,KAAM,CACJ,KAAM,CACJ,YAAa,iFACd,EACD,QAAS,OACT,SAAU,CACR,oBACE,4IACH,EACD,OAAQ,CAAA,EACR,KAAM,YACP,EACD,KAAM,0BACP,CAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{AST_NODE_TYPES as s}from"@typescript-eslint/utils";import{findProperty as y,propertyName as b}from"../ast.js";import{methodPath as x,isContextFunction as w}from"../declaration-ingress.js";import{isFunction as c,unwrap as d,variableOf as p}from"../model-bindings.js";import{createRule as E}from"../rule.js";function a(r,e){return r.range[0]>=e.range[0]&&r.range[1]<=e.range[1]}function v(r){const{callee:e}=r;if(!(e.type!==s.MemberExpression||e.computed)&&!(e.property.type!==s.Identifier||e.property.name!=="getSnapshot"))return r.arguments.length===0?d(e.object):void 0}function C(r,e,i){if(i.type!==s.Identifier)return!1;const f=p(r,e),o=p(r,i);return f===null&&o===null?e.name===i.name:f===o}function I(r){for(const e of r.properties)if(!(e.type!==s.Property||b(e)!=="current"))return e.value.type===s.Identifier?e.value.name:void 0}function P(r){const e=r.params[0];if(e!==void 0){if(e.type===s.Identifier)return`${e.name}.current`;if(e.type===s.ObjectPattern)return I(e)}}function T(r){if(r===void 0)return!1;const e=d(r);return e.type!==s.ObjectExpression?!0:y(e,"when")!==void 0}function R(r){const e=r.arguments[0],i=r.arguments[1];if(!(e===void 0||e.type!==s.Identifier)&&!(i===void 0||!c(i)))return{fixable:!T(r.arguments[2]),from:e,run:i}}const S=E({create(r){const e=r.sourceCode,i={barriers:[],calls:[],functions:[]},f=n=>{const t=x(e,n.callee);if(t?.method!=="effect")return;const u=R(n);if(u!==void 0)return w(e,t.fn)?u:void 0},o=(n,t)=>i.functions.some(u=>u!==t&&a(u,t)&&a(n,u)),m=(n,t)=>i.barriers.some(u=>a(u,t)&&u.range[0]<n.range[0]),l=(n,t)=>{const u=P(n.run);if(u===void 0||!n.fixable||m(t,n.run)){r.report({messageId:"preferEffectCurrent",node:t});return}r.report({fix:g=>g.replaceTextRange(t.range,u),messageId:"preferEffectCurrent",node:t})},h=n=>{for(const t of i.calls){if(!a(t,n.run)||o(t,n.run))continue;const u=v(t);u===void 0||!C(e,n.from,u)||l(n,t)}};return{":function"(n){c(n)&&i.functions.push(n)},"AwaitExpression, YieldExpression"(n){i.barriers.push(n)},CallExpression(n){i.calls.push(n)},"Program:exit"(){for(const n of i.calls){const t=f(n);t!==void 0&&h(t)}}}},meta:{docs:{description:"The run of an effect answers a value it was given; it does not read that value back."},fixable:"code",messages:{preferEffectCurrent:"This reads the source of the effect it is already inside. The run carries that value as `execution.current`, which is the value this run was started for and the one `when` and `skipInitial` are written against; `getSnapshot()` here reads the source a second time and says nothing about which run this is (D296, D323)."},schema:[],type:"problem"},name:"prefer-effect-current"});export{S as preferEffectCurrent};
|
|
2
|
+
//# sourceMappingURL=prefer-effect-current.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-effect-current.js","sources":["../../src/rules/prefer-effect-current.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { findProperty, propertyName } from '../ast';\nimport { isContextFunction, methodPath } from '../declaration-ingress';\nimport type { FunctionNode } from '../model-bindings';\nimport { isFunction, unwrap, variableOf } from '../model-bindings';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'preferEffectCurrent';\n\n/** What one file wrote, collected before any read is judged: the rule answers on `Program:exit` (D290). */\ntype Written = {\n readonly barriers: TSESTree.Node[];\n readonly calls: TSESTree.CallExpression[];\n readonly functions: FunctionNode[];\n};\n\n/** One `effect(source, run, options?)` declared on a context of this library, with what a rewrite of it needs. */\ntype Declared = { readonly fixable: boolean; readonly from: TSESTree.Identifier; readonly run: FunctionNode };\n\nfunction within(node: TSESTree.Node, outer: TSESTree.Node): boolean {\n return node.range[0] >= outer.range[0] && node.range[1] <= outer.range[1];\n}\n\n/** `s.getSnapshot()`, and the receiver it was read on; a reference that is not called reads nothing. */\nfunction snapshotReceiver(node: TSESTree.CallExpression): TSESTree.Node | undefined {\n const { callee } = node;\n\n if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) return undefined;\n\n if (callee.property.type !== AST_NODE_TYPES.Identifier || callee.property.name !== 'getSnapshot') return undefined;\n\n return node.arguments.length === 0 ? unwrap(callee.object) : undefined;\n}\n\n/** The same source: a name is the binding it resolves to, and two unresolved names are the same spelling. */\nfunction sameSource(source: TSESLint.SourceCode, from: TSESTree.Identifier, candidate: TSESTree.Node): boolean {\n if (candidate.type !== AST_NODE_TYPES.Identifier) return false;\n\n const declared = variableOf(source, from);\n const read = variableOf(source, candidate);\n\n return declared === null && read === null ? from.name === candidate.name : declared === read;\n}\n\n/** The local name `current` was destructured under, where the run took it that way. */\nfunction destructuredCurrent(pattern: TSESTree.ObjectPattern): string | undefined {\n for (const property of pattern.properties) {\n if (property.type !== AST_NODE_TYPES.Property || propertyName(property) !== 'current') continue;\n\n return property.value.type === AST_NODE_TYPES.Identifier ? property.value.name : undefined;\n }\n\n return undefined;\n}\n\n/**\n * What the read is replaced with. A named context answers `execution.current`; a destructured one answers the local\n * name. A run that took neither has no word for the value, so the report carries no fix.\n */\nfunction currentExpression(run: FunctionNode): string | undefined {\n const parameter = run.params[0];\n\n if (parameter === undefined) return undefined;\n\n if (parameter.type === AST_NODE_TYPES.Identifier) return `${parameter.name}.current`;\n\n if (parameter.type !== AST_NODE_TYPES.ObjectPattern) return undefined;\n\n return destructuredCurrent(parameter);\n}\n\n/**\n * D296: `when` is a filter of values, not a gate on the lifetime of the effect, so under it the source moves without\n * restarting the run. `execution.current` is then the value this run was started for and the snapshot is the newer\n * one — two different facts, and a silent rewrite between them is exactly what a fixable rule may not do.\n */\nfunction declaresWhen(options: TSESTree.CallExpressionArgument | undefined): boolean {\n if (options === undefined) return false;\n\n const record = unwrap(options);\n\n // A record assembled elsewhere may carry the predicate, and a rule that rewrites code assumes it does.\n if (record.type !== AST_NODE_TYPES.ObjectExpression) return true;\n\n return findProperty(record, 'when') !== undefined;\n}\n\n/** The source and the body of an `effect`, where both are written in the shape this rule can answer for. */\nfunction effectArguments(call: TSESTree.CallExpression): Declared | undefined {\n const from = call.arguments[0];\n const run = call.arguments[1];\n\n if (from === undefined || from.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n if (run === undefined || !isFunction(run)) return undefined;\n\n return { fixable: !declaresWhen(call.arguments[2]), from, run };\n}\n\nconst preferEffectCurrent = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n const written: Written = { barriers: [], calls: [], functions: [] };\n\n const effectRun = (call: TSESTree.CallExpression): Declared | undefined => {\n const named = methodPath(source, call.callee);\n\n if (named?.method !== 'effect') return undefined;\n\n const declared = effectArguments(call);\n\n if (declared === undefined) return undefined;\n\n return isContextFunction(source, named.fn) ? declared : undefined;\n };\n /** A function written inside the run runs later than the run does, and a snapshot read there is its own. */\n const nested = (node: TSESTree.Node, run: FunctionNode): boolean =>\n written.functions.some(fn => fn !== run && within(fn, run) && within(node, fn));\n /**\n * After an `await` the two are no longer the same value: `current` is what this run started with, the snapshot\n * is what the source holds now. The run usually restarts on a change, so they usually agree — and «usually» is\n * not a licence to rewrite, so past the first barrier the rule only says so.\n */\n const settled = (node: TSESTree.Node, run: FunctionNode): boolean =>\n written.barriers.some(barrier => within(barrier, run) && barrier.range[0] < node.range[0]);\n const report = (declared: Declared, read: TSESTree.CallExpression): void => {\n const replacement = currentExpression(declared.run);\n\n if (replacement === undefined || !declared.fixable || settled(read, declared.run)) {\n context.report({ messageId: 'preferEffectCurrent', node: read });\n\n return;\n }\n\n context.report({\n fix: fixer => fixer.replaceTextRange(read.range, replacement),\n messageId: 'preferEffectCurrent',\n node: read,\n });\n };\n const reportReads = (declared: Declared): void => {\n for (const read of written.calls) {\n if (!within(read, declared.run) || nested(read, declared.run)) continue;\n\n const receiver = snapshotReceiver(read);\n\n if (receiver === undefined || !sameSource(source, declared.from, receiver)) continue;\n\n report(declared, read);\n }\n };\n\n return {\n ':function'(node: TSESTree.Node): void {\n if (isFunction(node)) written.functions.push(node);\n },\n 'AwaitExpression, YieldExpression'(node: TSESTree.Node): void {\n written.barriers.push(node);\n },\n CallExpression(node): void {\n written.calls.push(node);\n },\n 'Program:exit'(): void {\n for (const call of written.calls) {\n const declared = effectRun(call);\n\n if (declared !== undefined) reportReads(declared);\n }\n },\n };\n },\n meta: {\n docs: {\n description: 'The run of an effect answers a value it was given; it does not read that value back.',\n },\n fixable: 'code',\n messages: {\n preferEffectCurrent:\n 'This reads the source of the effect it is already inside. The run carries that value as `execution.current`, which is the value this run was started for and the one `when` and `skipInitial` are written against; `getSnapshot()` here reads the source a second time and says nothing about which run this is (D296, D323).',\n },\n schema: [],\n type: 'problem',\n },\n name: 'prefer-effect-current',\n});\n\nexport { preferEffectCurrent };\n"],"names":["within","node","outer","snapshotReceiver","callee","AST_NODE_TYPES","unwrap","sameSource","source","from","candidate","declared","variableOf","read","destructuredCurrent","pattern","property","propertyName","currentExpression","run","parameter","declaresWhen","options","record","findProperty","effectArguments","call","isFunction","preferEffectCurrent","createRule","context","written","effectRun","named","methodPath","isContextFunction","nested","fn","settled","barrier","report","replacement","fixer","reportReads","receiver"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as
|
|
1
|
+
import{AST_NODE_TYPES as i}from"@typescript-eslint/utils";import{calleeName as n,functionName as o}from"../ast.js";import{namedDeclarationOf as l}from"../declarations.js";import{createRule as s}from"../rule.js";import{staticValueOf as d}from"../static-value.js";function c(e){return e.type===i.CallExpression?n(e):o(e)}function m(e,a){let t=e.parent;for(;t!=null;){const r=c(t);if(r!==void 0&&a.includes(r))return!0;t=t.parent}return!1}const u=s({create(e,[{allowInCallees:a}]){return{CallExpression(t){const r=l(t);r===void 0||d(r.id,e.sourceCode.getScope(r.id)).kind!=="computed"||m(t,a)||e.report({data:{declaration:r.name},messageId:"computedId",node:r.id})}}},meta:{defaultOptions:[{allowInCallees:[]}],docs:{description:"A declaration names itself with a string that is written, not built at the call site."},messages:{computedId:"The id of {{declaration}} is built here, so nothing can find the declaration by its name. Write the id as a string, or name the factory that may compute it in `allowInCallees`."},schema:[{additionalProperties:!1,properties:{allowInCallees:{items:{type:"string"},type:"array"}},type:"object"}],type:"problem"},name:"require-literal-id"});export{u as requireLiteralId};
|
|
2
2
|
//# sourceMappingURL=require-literal-id.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"require-literal-id.js","sources":["../../src/rules/require-literal-id.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { calleeName, functionName } from '../ast';\nimport {
|
|
1
|
+
{"version":3,"file":"require-literal-id.js","sources":["../../src/rules/require-literal-id.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { calleeName, functionName } from '../ast';\nimport { namedDeclarationOf } from '../declarations';\nimport { createRule } from '../rule';\nimport { staticValueOf } from '../static-value';\n\ntype MessageIds = 'computedId';\ntype Options = readonly [{ readonly allowInCallees: readonly string[] }];\n\n/** A factory names itself either as the function that writes the declaration or as the call that receives it. */\nfunction enclosingName(node: TSESTree.Node): string | undefined {\n if (node.type === AST_NODE_TYPES.CallExpression) return calleeName(node);\n\n return functionName(node);\n}\n\nfunction isInsideAllowedFactory(node: TSESTree.Node, allowed: readonly string[]): boolean {\n // The parent of `Program` is `null`, so the walk ends there rather than at an absent property.\n let current: TSESTree.Node | null | undefined = node.parent;\n\n while (current !== undefined && current !== null) {\n const name = enclosingName(current);\n\n if (name !== undefined && allowed.includes(name)) return true;\n\n current = current.parent;\n }\n\n return false;\n}\n\nconst requireLiteralId = createRule<Options, MessageIds>({\n create(context, [{ allowInCallees }]) {\n return {\n CallExpression(node): void {\n const declaration = namedDeclarationOf(node);\n\n if (declaration === undefined) return;\n\n const value = staticValueOf(declaration.id, context.sourceCode.getScope(declaration.id));\n\n if (value.kind !== 'computed' || isInsideAllowedFactory(node, allowInCallees)) return;\n\n context.report({\n data: { declaration: declaration.name },\n messageId: 'computedId',\n node: declaration.id,\n });\n },\n };\n },\n meta: {\n defaultOptions: [{ allowInCallees: [] }],\n docs: {\n description: 'A declaration names itself with a string that is written, not built at the call site.',\n },\n messages: {\n computedId:\n 'The id of {{declaration}} is built here, so nothing can find the declaration by its name. Write the id as a string, or name the factory that may compute it in `allowInCallees`.',\n },\n schema: [\n {\n additionalProperties: false,\n properties: {\n allowInCallees: { items: { type: 'string' }, type: 'array' },\n },\n type: 'object',\n },\n ],\n type: 'problem',\n },\n name: 'require-literal-id',\n});\n\nexport { requireLiteralId };\n"],"names":["enclosingName","node","AST_NODE_TYPES","calleeName","functionName","isInsideAllowedFactory","allowed","current","name","requireLiteralId","createRule","context","allowInCallees","declaration","namedDeclarationOf","staticValueOf"],"mappings":"sQAYA,SAASA,EAAcC,EAAmB,CACxC,OAAIA,EAAK,OAASC,EAAe,eAAuBC,EAAWF,CAAI,EAEhEG,EAAaH,CAAI,CAC1B,CAEA,SAASI,EAAuBJ,EAAqBK,EAA0B,CAE7E,IAAIC,EAA4CN,EAAK,OAErD,KAAgCM,GAAY,MAAM,CAChD,MAAMC,EAAOR,EAAcO,CAAO,EAElC,GAAIC,IAAS,QAAaF,EAAQ,SAASE,CAAI,EAAG,MAAO,GAEzDD,EAAUA,EAAQ,MACpB,CAEA,MAAO,EACT,CAEA,MAAME,EAAmBC,EAAgC,CACvD,OAAOC,EAAS,CAAC,CAAE,eAAAC,CAAc,CAAE,EAAC,CAClC,MAAO,CACL,eAAeX,EAAI,CACjB,MAAMY,EAAcC,EAAmBb,CAAI,EAEvCY,IAAgB,QAENE,EAAcF,EAAY,GAAIF,EAAQ,WAAW,SAASE,EAAY,EAAE,CAAC,EAE7E,OAAS,YAAcR,EAAuBJ,EAAMW,CAAc,GAE5ED,EAAQ,OAAO,CACb,KAAM,CAAE,YAAaE,EAAY,IAAI,EACrC,UAAW,aACX,KAAMA,EAAY,EACnB,CAAA,CACH,EAEJ,EACA,KAAM,CACJ,eAAgB,CAAC,CAAE,eAAgB,CAAA,EAAI,EACvC,KAAM,CACJ,YAAa,uFACd,EACD,SAAU,CACR,WACE,kLACH,EACD,OAAQ,CACN,CACE,qBAAsB,GACtB,WAAY,CACV,eAAgB,CAAE,MAAO,CAAE,KAAM,QAAQ,EAAI,KAAM,OAAO,CAC3D,EACD,KAAM,QACP,CACF,EACD,KAAM,SACP,EACD,KAAM,oBACP,CAAA"}
|
package/oxlintrc.json
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
"opetope/layer-placement": "off",
|
|
6
6
|
"opetope/no-command-in-deps": "error",
|
|
7
7
|
"opetope/no-internal-imports": "error",
|
|
8
|
+
"opetope/no-redundant-const-tuple": "error",
|
|
8
9
|
"opetope/no-snapshot-in-update": "error",
|
|
9
10
|
"opetope/no-snapshot-read-in-render": "error",
|
|
10
11
|
"opetope/no-subscribe-outside-models": "off",
|
|
12
|
+
"opetope/prefer-effect-current": "error",
|
|
11
13
|
"opetope/prefer-model-selection": "off",
|
|
12
14
|
"opetope/require-declared-models": "error",
|
|
13
15
|
"opetope/require-literal-id": "error",
|