@opetope/lint 0.14.0 → 0.15.0
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 +62 -0
- package/README.md +39 -2
- package/dist/ast.d.ts +14 -2
- package/dist/ast.js +1 -1
- package/dist/ast.js.map +1 -1
- package/dist/command-hooks.d.ts +7 -6
- package/dist/command-hooks.js +1 -1
- package/dist/command-hooks.js.map +1 -1
- package/dist/context-members.d.ts +4 -4
- package/dist/context-members.js +1 -1
- package/dist/context-members.js.map +1 -1
- package/dist/declaration-ingress.d.ts +43 -11
- package/dist/declaration-ingress.js +1 -1
- package/dist/declaration-ingress.js.map +1 -1
- package/dist/declarations.d.ts +1 -1
- package/dist/declarations.js +1 -1
- package/dist/declarations.js.map +1 -1
- package/dist/effect-declarations.d.ts +3 -2
- package/dist/effect-declarations.js +1 -1
- package/dist/effect-declarations.js.map +1 -1
- package/dist/entry-move-rows.d.ts +21 -0
- package/dist/entry-move-rows.js +2 -0
- package/dist/entry-move-rows.js.map +1 -0
- package/dist/entry-moves.d.ts +25 -0
- package/dist/entry-moves.js +10 -0
- package/dist/entry-moves.js.map +1 -0
- package/dist/feature-body-records.d.ts +5 -3
- package/dist/feature-body-records.js +1 -1
- package/dist/feature-body-records.js.map +1 -1
- package/dist/index.d.ts +65 -58
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lazy-body-migration.d.ts +5 -5
- package/dist/lazy-body-migration.js +1 -1
- package/dist/lazy-body-migration.js.map +1 -1
- package/dist/model-bindings.d.ts +21 -13
- package/dist/model-bindings.js +1 -1
- package/dist/model-bindings.js.map +1 -1
- package/dist/resource-vocabulary.d.ts +17 -0
- package/dist/resource-vocabulary.js +2 -0
- package/dist/resource-vocabulary.js.map +1 -0
- package/dist/retired-vocabulary-messages.d.ts +6 -0
- package/dist/retired-vocabulary-messages.js +2 -0
- package/dist/retired-vocabulary-messages.js.map +1 -0
- package/dist/retired-vocabulary.d.ts +10 -8
- package/dist/retired-vocabulary.js +1 -1
- package/dist/retired-vocabulary.js.map +1 -1
- package/dist/rule.d.ts +94 -5
- package/dist/rule.js +1 -1
- package/dist/rule.js.map +1 -1
- package/dist/rules/capture-command-cleanup.d.ts +1 -4
- package/dist/rules/capture-command-cleanup.js +1 -1
- package/dist/rules/capture-command-cleanup.js.map +1 -1
- package/dist/rules/define-feature-property-order.d.ts +1 -4
- package/dist/rules/define-feature-property-order.js +1 -1
- package/dist/rules/define-feature-property-order.js.map +1 -1
- package/dist/rules/enabled-predicate.d.ts +1 -4
- package/dist/rules/enabled-predicate.js +1 -1
- package/dist/rules/enabled-predicate.js.map +1 -1
- package/dist/rules/id-naming.d.ts +1 -3
- package/dist/rules/layer-placement.d.ts +1 -3
- package/dist/rules/no-behavior-in-integration.d.ts +8 -0
- package/dist/rules/no-behavior-in-integration.js +2 -0
- package/dist/rules/no-behavior-in-integration.js.map +1 -0
- package/dist/rules/no-command-in-deps.d.ts +1 -4
- package/dist/rules/no-command-in-deps.js +1 -1
- package/dist/rules/no-command-in-deps.js.map +1 -1
- package/dist/rules/no-dead-abort-check.d.ts +8 -0
- package/dist/rules/no-dead-abort-check.js +2 -0
- package/dist/rules/no-dead-abort-check.js.map +1 -0
- package/dist/rules/no-event-without-emit.d.ts +10 -0
- package/dist/rules/no-event-without-emit.js +2 -0
- package/dist/rules/no-event-without-emit.js.map +1 -0
- package/dist/rules/no-inline-requires-models.d.ts +8 -0
- package/dist/rules/no-inline-requires-models.js +5 -0
- package/dist/rules/no-inline-requires-models.js.map +1 -0
- package/dist/rules/no-internal-imports.d.ts +1 -3
- package/dist/rules/no-internal-imports.js +1 -1
- package/dist/rules/no-internal-imports.js.map +1 -1
- package/dist/rules/no-redundant-const-tuple.d.ts +1 -4
- package/dist/rules/no-redundant-const-tuple.js +1 -1
- package/dist/rules/no-redundant-const-tuple.js.map +1 -1
- package/dist/rules/no-retired-vocabulary.d.ts +2 -4
- package/dist/rules/no-retired-vocabulary.js +1 -1
- package/dist/rules/no-retired-vocabulary.js.map +1 -1
- package/dist/rules/no-single-command-lane.d.ts +10 -0
- package/dist/rules/no-single-command-lane.js +2 -0
- package/dist/rules/no-single-command-lane.js.map +1 -0
- package/dist/rules/no-snapshot-in-update.d.ts +1 -4
- package/dist/rules/no-snapshot-in-update.js.map +1 -1
- package/dist/rules/no-snapshot-read-in-render.d.ts +2 -3
- package/dist/rules/no-snapshot-read-in-render.js +1 -1
- package/dist/rules/no-snapshot-read-in-render.js.map +1 -1
- package/dist/rules/no-subscribe-outside-models.d.ts +1 -3
- package/dist/rules/no-subscribe-outside-models.js +1 -1
- package/dist/rules/no-subscribe-outside-models.js.map +1 -1
- package/dist/rules/no-write-after-source-write.d.ts +1 -4
- package/dist/rules/no-write-after-source-write.js +1 -1
- package/dist/rules/no-write-after-source-write.js.map +1 -1
- package/dist/rules/prefer-derive.d.ts +7 -0
- package/dist/rules/prefer-derive.js +2 -0
- package/dist/rules/prefer-derive.js.map +1 -0
- package/dist/rules/prefer-effect-current.d.ts +1 -4
- package/dist/rules/prefer-effect-current.js +1 -1
- package/dist/rules/prefer-effect-current.js.map +1 -1
- package/dist/rules/prefer-lazy-body-record.d.ts +1 -4
- package/dist/rules/prefer-lazy-body-record.js +1 -1
- package/dist/rules/prefer-lazy-body-record.js.map +1 -1
- package/dist/rules/prefer-model-selection.d.ts +1 -3
- package/dist/rules/prefer-model-selection.js +1 -1
- package/dist/rules/prefer-model-selection.js.map +1 -1
- package/dist/rules/prefer-read.d.ts +2 -0
- package/dist/rules/prefer-read.js +2 -0
- package/dist/rules/prefer-read.js.map +1 -0
- package/dist/rules/prefer-when-over-enabled.d.ts +7 -0
- package/dist/rules/prefer-when-over-enabled.js +2 -0
- package/dist/rules/prefer-when-over-enabled.js.map +1 -0
- package/dist/rules/require-declared-models.d.ts +1 -3
- package/dist/rules/require-declared-models.js +1 -1
- package/dist/rules/require-declared-models.js.map +1 -1
- package/dist/rules/require-literal-id.d.ts +2 -3
- package/dist/rules/require-literal-id.js +1 -1
- package/dist/rules/require-literal-id.js.map +1 -1
- package/dist/rules/require-static-feature.d.ts +9 -0
- package/dist/rules/require-static-feature.js +2 -0
- package/dist/rules/require-static-feature.js.map +1 -0
- package/dist/rules/scope-builder-releases-subscriptions.d.ts +10 -0
- package/dist/rules/scope-builder-releases-subscriptions.js +2 -0
- package/dist/rules/scope-builder-releases-subscriptions.js.map +1 -0
- package/dist/snapshot-updates.d.ts +4 -3
- package/dist/snapshot-updates.js +1 -1
- package/dist/snapshot-updates.js.map +1 -1
- package/dist/static-value.d.ts +3 -2
- package/dist/static-value.js +1 -1
- package/dist/static-value.js.map +1 -1
- package/oxlintrc.json +10 -0
- package/package.json +13 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
# @opetope/lint
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- fe97978: `eslint` and `@typescript-eslint/parser` are optional peer dependencies of `@opetope/lint` now, so npm no longer installs the parser for a project that does not name it. An ESLint host lists `@typescript-eslint/parser` in its own devDependencies and passes it as `languageOptions.parser`; an Oxlint host needs neither of them, nor `typescript`. The package no longer depends on `@typescript-eslint/utils`, so its two dependencies have no peers and an Oxlint host no longer receives `eslint` or a `typescript` below 6.1 with it (D494).
|
|
8
|
+
- 5e7c654: Navigation answers `kind` records named `*Outcome` — `OpenScreenOutcome`, `ReplaceScreenOutcome`, `DismissScreenOutcome` and `LinkOutcome` replace the `*Result` names — a surface exposes `regions` instead of `view`, a link source exposes `intent` instead of `pending`, and a named link catalog requires a literal declaration id. `opetope/no-retired-vocabulary` fixes the renamed imports and, where it proves their origin, `surface.view` and `LinkSource.pending`; the compiler catches the rest (D511, D512).
|
|
9
|
+
|
|
10
|
+
Behavior change: in JavaScript, a Navigation dismissal and a link handler answered a string; now they answer `{ kind }` records. Notice it where code compares an answer with a string — compare `answer.kind`.
|
|
11
|
+
|
|
12
|
+
- b6471b3: Pagination is applied to a built Resource instead of written in its declaration: `ctx.resource.load({ …, pagination: { load, merge, nextCursor } })` becomes `ctx.resource.paginate(ctx.resource.load({ … }), { cursor, load, merge })` in the model that declares it, and answers the same handle with `pagination` on it; a feature Resource has no pagination, so declare it in a model the feature owns. `ConsumerResource<Data, Key, Cursor>` loses its third parameter: a paginated `consumers` field is `PaginatedResource<Data, Key, Cursor, 'consumers'>`. In React `pagination.loadNext` is a plain function answering `Promise<PaginationOutcome>`, so `loadNext.run()`, `.inFlight` and `.outcome` are gone and the status is read from `pagination.state`. `loadNext({ retry: false })` starts nothing over a failed cursor, and `testPagination` from `@opetope/runtime/testing` stands in for the capability. `opetope/no-retired-vocabulary` and the compiler name the replacement of `pagination` in the record of `ctx.resource.load` or `ctx.resource.live` and of `nextCursor` in the record of `ctx.resource.paginate` (D486, D487, D488).
|
|
13
|
+
|
|
14
|
+
Behavior change: a `merge` that threw, or answered `skipChange` or a thenable, left `loadNext()` pending forever with nothing reported; now the reporter hears it once and every caller of that `loadNext()` is rejected with it. Notice it where a caller waited for a page that never came (D486, D487).
|
|
15
|
+
|
|
16
|
+
Behavior change: `pagination.state` was a new record on every write of the data, a tuple cursor a new array each time; now it keeps its record while its kind, its cursor and its error stand. Notice it where an effect or a memo keyed on it ran on every data write and now runs when the page moves (D487).
|
|
17
|
+
|
|
18
|
+
Behavior change: a page that answered but left its cursor where it was, or whose `merge` was refused, kept the state `failed` with the error of the page before; now the answer takes that failure away and the state is `ready`, and an operation that ended where it started publishes the record it left through `loading` again. Notice it where a view shows the error of a page (D487).
|
|
19
|
+
|
|
20
|
+
Behavior change: a page whose operation was superseded — by a `refresh()` or `reset()`, from inside its own `merge` included — was still written and published; now it does not land, the data stay as they were and `loadNext()` answers `superseded`. Notice it where a page was expected after a refresh started (D489).
|
|
21
|
+
|
|
22
|
+
- 54397dd: `lifetime: 'owner'` is now `lifetime: 'model'`, a model context's `acquire(resource)` is `hold(resource)`, a Resource view's `initial` is `fallback` and `identity.scope` is `identity.partition`; `opetope/no-retired-vocabulary` fixes each where it proves the context — the factories of `model`, `openModel` and a slot contribution's `model` included — and names the rest. Every Resource and its pagination, `model` and `consumers` alike, exposes a passive `observe()`, and `acquire()` answers `{ state, release }` instead of a disposer: call `lease.release()` (D514).
|
|
23
|
+
|
|
24
|
+
A Resource verb called inside a captured update supersedes the update, keeping the verb's work and leaving no loading state stuck. Each grace window of a `consumers` Resource compares its inputs with the request its last lease selected: returning to the parked identity reveals its latest state without restarting it, work already admitted goes on until its normal release, and when the window ends the data a lease read are released instead of staying cached until the next lease (D516).
|
|
25
|
+
|
|
26
|
+
Behavior change: a `reset`, `refresh` or `retry` that a confirmed write's reducer calls used to let that write land; now the write answers `superseded` and the verb's work goes on. Notice it where a confirmed write reports `superseded` although no key or request moved.
|
|
27
|
+
|
|
28
|
+
Behavior change: inside the grace window of a `consumers` Resource, a request whose key or partition differs from the parked one used to keep showing the old data; now the Resource shows `idle('unleased')` and its pagination `unavailable`, and `refresh`, `retry` and `reset` answer `skipped: unleased` only while the record is hidden — a visible parked record is served. Notice it where a passive reader or a projection expects data after the last lease.
|
|
29
|
+
|
|
30
|
+
Behavior change: a confirmed commit that superseded the load in flight of a one-shot Resource left `pending: 'refresh'` on its state for good; now nothing is pending after the commit, which answered the epoch that load was fetching. Notice it where a spinner reads `state.pending` (D489).
|
|
31
|
+
|
|
32
|
+
Behavior change: runtime activity inspection published `opetope.runtime-activity/5`; now it publishes `/6`, because the Resource lifetime literal changed, while graph and frame stay `/5` and model state `/1`. Notice it where a custom inspection adapter or a recorded fixture checks the revision (D514).
|
|
33
|
+
|
|
34
|
+
- 3e0dc0b: A model scope — `ctx.scope.switch`, `while` or `each` — builds a local model now: its builder `(input, child) => fields` runs synchronously, declares state, effects, events, Resources and `cleanup` on the child context, and returns the fields a consumer reads through the handle's `current`, or nothing. The old callback that opened work with `update`, `signal` and `timers` and returned a disposer is refused with an instruction: move asynchronous work into `child.effect` and the release into `child.cleanup`. Every scope answers a handle with `current`, `status` and `restart`, `ctx.effect` answers one with `restart`, and a restart answers a `RestartOutcome` (D470, D471). The predicate of `scope.while(source, open, { when })` is written `{ filter }` now, the word an effect uses; lint fixes a proven direct option (D517).
|
|
35
|
+
|
|
36
|
+
New rule `opetope/scope-builder-releases-subscriptions`, `error` in `recommended`, in `layers({ models })` and in the shipped `oxlintrc.json`: a subscription that the builder of a model scope or of a `resource.view` opens by hand is reported unless its release goes to `child.cleanup(...)`, registered before the work it releases; or declare the ingress with `child.event` or `child.resource.live` (D470).
|
|
37
|
+
|
|
38
|
+
Behavior change: a model scope whose child failed to open or to clean up reported `DynamicScopeOpenError` or `DynamicScopeCleanupError`; now it reports `ModelScopeOpenError` (`open-failed`) or `ModelScopeCleanupError` (`quarantined`), while a model event with a `request` keeps the `DynamicScope*` names. Notice it where a reporter matches these errors by name or class (D470).
|
|
39
|
+
|
|
40
|
+
Behavior change: the callback of a model scope ran once per key; a `child.effect(input, run)` that takes its work over runs again whenever the element under that key changes. Notice it where work belongs to the key rather than to each update — read the snapshot once in the builder, or follow only the part of the value that names the work (D470).
|
|
41
|
+
|
|
42
|
+
- 86c7d69: Testing fixtures have one non-React entry, `@opetope/runtime/testing`: Core no longer exports `/testing`, and React `/testing` keeps only scenarios and Slot rendering. `command` becomes `testCommand`, Navigation's `fakeLinkSource` and `fakeLinkCodec` become `testLinkSource` and `testLinkCodec`, Kit `ExternalSource.publish` becomes `set`, and the public Core `State` becomes `TestReadable` beside `testReadable`. `CommandInputArgs` is no longer exported: a wrapper forwards `RunCommandArgs` or `Parameters<CommandHook<Input, Output>['run']>`. `Scenario.waitFor` answers the accepted value, with type-guard narrowing. `opetope/no-retired-vocabulary` moves the imports with aliases that keep local and public names, and rewrites a Kit receiver only where its origin is proved (D502, D517).
|
|
43
|
+
|
|
44
|
+
Behavior change: `scenario.waitFor(read)` finished when `read` answered any truthy value; like Runtime `waitFor`, it now waits only while `read` answers `false`, `null` or `undefined`, so `0` and `''` finish at once, and it resolves with the value. Notice it where a test waits on a count or a text from JavaScript — pass an `accept` predicate for the condition it meant (D502).
|
|
45
|
+
|
|
46
|
+
- 5e7c654: A `useModel` selector reads a source only through `read`, from its second argument, now exported as `SelectionContext`: `model.count.getSnapshot()`, which read once and never updated the component, no longer compiles, and neither do `subscribe`, `acquire` or a write to a `State` (D476, D518). The types no longer check what a selector returns, so a hook generic in its model or in its value compiles without an annotation and a selector costs about half as much to check; the result is checked at run time instead (D525). The `read` of a `SelectionContext` called after the selector returned throws `TypeError: A selector's read was called after the selector returned: read inside the selector.` `opetope/no-snapshot-read-in-render`, `error` in `recommended` and in the shipped `oxlintrc.json`, now reads the inline selector as render code, so it reports more after the upgrade: a `getSnapshot()` in the selector or in a projection handed to `read(source, project)`, on any receiver, and a selector that answers the whole model, `model => model`; select the fields the component uses (D476).
|
|
47
|
+
|
|
48
|
+
Behavior change: a selector that left a `Readable` or a `ResourceView` in a field of its result used to hand the component the source object; now the selection throws `TypeError: A selector returned a Readable in field "x": read(source) inside the selector.` (or its `ResourceView` form) in development and in production. A Resource or a Command field still becomes its hook, and a source one level down passes. A result that is an array, a tuple, a promise or another container throws `Model selection requires a record of named fields, not an array, a promise or another container.` Notice it where a component throws while rendering after the upgrade.
|
|
49
|
+
|
|
50
|
+
Behavior change: a `useModel` selector that writes a `State` it closed over used to write; now it throws `TypeError: a selector reads; write in a command` before the write. Notice it where a component throws while rendering after the upgrade.
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- d70022e: The root of `@opetope/devtools` is what an application attaches the panel with: `installDevtools`, `DevtoolsHandle`, `InstallDevtoolsOptions`, `DevtoolsMode`, `DevtoolsTheme` and `defaultTheme`. Everything else it published — the protocol, the decoders, `DevtoolsGraphStore`, the projections, the session client and its options, the export and the corpus — moved to `@opetope/devtools/protocol`, which loads no React. `@opetope/devtools/integration` is gone, and `defaultTheme`, `DevtoolsTheme` and `DevtoolsMode` left `@opetope/devtools/react`. `opetope/no-retired-vocabulary` rewrites named imports and re-exports of the moved names, and a namespace import or `import()` of `/integration`, with `--fix`, and reports without a rewrite what it cannot move safely, such as `export *` from a moved entry (D495).
|
|
55
|
+
- 544bdd9: Feature `own` keeps composition and host lifetimes only — `imports`, `requires`, `model`, `select`, `attach` and `command` — and a feature command takes `parallel`, `queue` or `{ pending: 'latest' }`. `own.effect`, `own.event`, `own.resource`, `own.scope` and `own.lane` are removed: declare them in a model's `ModelContext` and compose the model through `own.model` (D501). `FeatureInstance.instance` is no longer public: pass the feature handle to `settled(instance)` or use its `ready` and `close` (D517). Runtime no longer re-exports Core's Resource declaration and state types and `skipChange` — import them from `@opetope/core`; `ResourceView` stays a shared boundary type. React removes `useSelector` and, from `@opetope/react/integration`, the `FeatureBoundary` demand family with `useFeature` and `useFeatureRetry`: select model data with `read(source, project)` in `useModel`, or create a stable `derive(source, project, { equals })` for a standalone projection and pass it to `useReadable` (D500). `opetope/no-retired-vocabulary` reports the removed words with the instruction and moves the Resource type imports with `--fix`.
|
|
56
|
+
|
|
57
|
+
Behavior change: an event of feature `own` whose subscription failed asynchronously, or a scope of feature `own` whose first child failed to open, rejected the feature's `ready`; moved into a model, the failure goes to the reporter — a scope's as `ModelScopeOpenError` — and `ready` can resolve. A synchronous throw in an event `subscribe` still rejects opening, as `preparation failed` instead of `attachment <key> failed to open`. Notice it where a test or a host waits for `ready` to reject, or matches that text rather than `name` and `code` (D501).
|
|
58
|
+
|
|
59
|
+
Behavior change: after close, a Resource declared in feature `own` and exported answered `idle('unleased')`; moved into a model the feature owns, it answers `idle('retired')`, and `settled(instance)` waits for that model's effect and event runs, so one still running ends the wait with `WaitTimeoutError`. Notice it where a test asserts the state of a closed feature's Resource or settles an instance while a run is in flight (D501).
|
|
60
|
+
|
|
61
|
+
- 0286832: An `exports` written above `own` reads an `own` whose type is the instruction `declare own before exports: exports reads only the sections written above it`, in `defineFeature.body` too, so a member read from it prints the order instead of an empty record type, and such an `exports` that spreads `own` — which compiled and exported nothing of it — is refused. Declare `own` before `exports` and leave the parameter of `exports` unannotated; the message of `opetope/define-feature-property-order` now says that type inference holds this part of the order (D477).
|
|
62
|
+
- d70022e: Eight new rules. Seven are warnings in `configs.recommended` and in the shipped `oxlintrc.json`, so a project whose lint admits no warning sees them at once: `prefer-derive` (an effect that only copies a projection of its source into a cell nothing else writes), `require-static-feature` (the sections of `defineFeature` as one literal with known keys), `no-single-command-lane` (a `ctx.lane()` only one command takes), `no-dead-abort-check` (`signal.throwIfAborted()` right before a `throw` in an effect or event run), `no-event-without-emit` (a `subscribe` that neither emits nor writes through its own `update`), `prefer-when-over-enabled` (a feature that exports nothing and hides every contribution behind one `enabled`) and `no-inline-requires-models`, whose fix moves a component written inside `requiresModels([...])(…)` into a `const` of its own. `no-behavior-in-integration` is off in `recommended`; `layers({ integration })` turns it on as a warning for the integration files that are not among the `tests` of the same call (D494).
|
|
63
|
+
- c153f4f: A `ModelStateWriter`, also a helper parameter typed with it, reads with the same passive `read(source)` as every execution, so a hand-written writer now needs `read` beside `update` (D526). `@opetope/runtime/testing` exports `read(source)` for fixtures; Runtime `waitFor` and React `Scenario.waitFor` take a readable source directly, with an optional `accept`, and answer the accepted value, while the callback form of Runtime `waitFor` now also refuses a callback that answers a promise on some calls only, and `settled` and `waitFor` refuse a spread copy of a handle. The new `prefer-read` rule warns in the recommended config on a passive `getSnapshot()` inside an execution and fixes only the forms it proves equivalent, so a project whose lint admits no warning sees it at once.
|
|
64
|
+
|
|
3
65
|
## 0.14.0
|
|
4
66
|
|
|
5
67
|
No changes in this release.
|
package/README.md
CHANGED
|
@@ -8,6 +8,11 @@ ESLint rules for the Opetope authoring and layer boundaries. The rules guide mod
|
|
|
8
8
|
npm install --save-exact --save-dev @opetope/lint
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Under ESLint, also install `eslint` and `@typescript-eslint/parser`, and pass the parser as `languageOptions.parser`:
|
|
12
|
+
both are optional peers, which npm installs only where the project lists them. Under Oxlint the plugin neither loads
|
|
13
|
+
nor installs `eslint`, the parser or `typescript`: its two dependencies, `@typescript-eslint/types` and
|
|
14
|
+
`@typescript-eslint/scope-manager`, have no peers.
|
|
15
|
+
|
|
11
16
|
Use matching exact Opetope versions; for release candidates, install every Opetope package from `@next`.
|
|
12
17
|
Packages are ESM-only and support Node 20.19+. React integrations support React and React DOM 19.
|
|
13
18
|
|
|
@@ -19,10 +24,12 @@ feature, host, React entry and cleanup in one visible module.
|
|
|
19
24
|
```js
|
|
20
25
|
// eslint.config.mjs
|
|
21
26
|
import opetope from '@opetope/lint';
|
|
27
|
+
import parser from '@typescript-eslint/parser';
|
|
22
28
|
|
|
23
29
|
export default [
|
|
24
30
|
{
|
|
25
31
|
files: ['src/**/*.{ts,tsx}'],
|
|
32
|
+
languageOptions: { parser },
|
|
26
33
|
...opetope.configs.recommended,
|
|
27
34
|
},
|
|
28
35
|
];
|
|
@@ -36,7 +43,7 @@ The [reference](../runtime/docs/reference/lint.md) owns the detailed contract, o
|
|
|
36
43
|
|
|
37
44
|
For a contributor checkout, use `npm run ci:type --workspace @opetope/lint` and
|
|
38
45
|
`npm run ci:test --workspace @opetope/lint` where provided. The root `npm run check` performs full acceptance;
|
|
39
|
-
|
|
46
|
+
`docs/maintainers/contributing.md` in the checkout describes the build and package checks.
|
|
40
47
|
|
|
41
48
|
<a id="opetopelint"></a>
|
|
42
49
|
[See @opetope/lint](../runtime/docs/reference/lint.md).
|
|
@@ -95,9 +102,15 @@ For a contributor checkout, use `npm run ci:type --workspace @opetope/lint` and
|
|
|
95
102
|
<a id="no-subscribe-outside-models"></a>
|
|
96
103
|
[See `no-subscribe-outside-models`](../runtime/docs/reference/lint.md#no-subscribe-outside-models).
|
|
97
104
|
|
|
105
|
+
<a id="scope-builder-releases-subscriptions"></a>
|
|
106
|
+
[See `scope-builder-releases-subscriptions`](../runtime/docs/reference/lint.md#scope-builder-releases-subscriptions).
|
|
107
|
+
|
|
98
108
|
<a id="prefer-effect-current"></a>
|
|
99
109
|
[See `prefer-effect-current`](../runtime/docs/reference/lint.md#prefer-effect-current).
|
|
100
110
|
|
|
111
|
+
<a id="prefer-read"></a>
|
|
112
|
+
[See `prefer-read`](../runtime/docs/reference/lint.md#prefer-read).
|
|
113
|
+
|
|
101
114
|
<a id="no-write-after-source-write"></a>
|
|
102
115
|
[See `no-write-after-source-write`](../runtime/docs/reference/lint.md#no-write-after-source-write).
|
|
103
116
|
|
|
@@ -113,6 +126,30 @@ For a contributor checkout, use `npm run ci:type --workspace @opetope/lint` and
|
|
|
113
126
|
<a id="no-retired-vocabulary"></a>
|
|
114
127
|
[See `no-retired-vocabulary`](../runtime/docs/reference/lint.md#no-retired-vocabulary).
|
|
115
128
|
|
|
129
|
+
<a id="prefer-derive"></a>
|
|
130
|
+
[See `prefer-derive`](../runtime/docs/reference/lint.md#prefer-derive).
|
|
131
|
+
|
|
132
|
+
<a id="no-behavior-in-integration"></a>
|
|
133
|
+
[See `no-behavior-in-integration`](../runtime/docs/reference/lint.md#no-behavior-in-integration).
|
|
134
|
+
|
|
135
|
+
<a id="no-single-command-lane"></a>
|
|
136
|
+
[See `no-single-command-lane`](../runtime/docs/reference/lint.md#no-single-command-lane).
|
|
137
|
+
|
|
138
|
+
<a id="no-dead-abort-check"></a>
|
|
139
|
+
[See `no-dead-abort-check`](../runtime/docs/reference/lint.md#no-dead-abort-check).
|
|
140
|
+
|
|
141
|
+
<a id="no-event-without-emit"></a>
|
|
142
|
+
[See `no-event-without-emit`](../runtime/docs/reference/lint.md#no-event-without-emit).
|
|
143
|
+
|
|
144
|
+
<a id="require-static-feature"></a>
|
|
145
|
+
[See `require-static-feature`](../runtime/docs/reference/lint.md#require-static-feature).
|
|
146
|
+
|
|
147
|
+
<a id="prefer-when-over-enabled"></a>
|
|
148
|
+
[See `prefer-when-over-enabled`](../runtime/docs/reference/lint.md#prefer-when-over-enabled).
|
|
149
|
+
|
|
150
|
+
<a id="no-inline-requires-models"></a>
|
|
151
|
+
[See `no-inline-requires-models`](../runtime/docs/reference/lint.md#no-inline-requires-models).
|
|
152
|
+
|
|
116
153
|
<a id="coexistence-with-key-sorting"></a>
|
|
117
154
|
[See Coexistence with key sorting](../runtime/docs/reference/lint.md#coexistence-with-key-sorting).
|
|
118
155
|
|
|
@@ -120,4 +157,4 @@ For a contributor checkout, use `npm run ci:type --workspace @opetope/lint` and
|
|
|
120
157
|
[See Running the rules under Oxlint](../runtime/docs/reference/lint.md#running-the-rules-under-oxlint).
|
|
121
158
|
|
|
122
159
|
<a id="checks"></a>
|
|
123
|
-
|
|
160
|
+
Contributor checks: `docs/maintainers/contributing.md` in a contributor checkout.
|
package/dist/ast.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TSESTree } from '@typescript-eslint/
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/types';
|
|
2
2
|
type FunctionLike = TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression;
|
|
3
3
|
/** `slot` and `defineFeature.body` alike: the dotted name of the simple callee shapes an author writes. */
|
|
4
4
|
declare function calleeName(node: TSESTree.CallExpression): string | undefined;
|
|
@@ -14,6 +14,18 @@ declare function memberFirstStep(node: TSESTree.MemberExpression): string | unde
|
|
|
14
14
|
declare function isFunctionLike(node: TSESTree.Node): node is FunctionLike;
|
|
15
15
|
/** The name a function is written under: its own, or the binding it is assigned to. */
|
|
16
16
|
declare function functionName(node: TSESTree.Node): string | undefined;
|
|
17
|
+
/** The names a parameter binds, whether it is one name or a pattern taken apart. */
|
|
18
|
+
declare function boundNames(node: TSESTree.Node | null | undefined, names?: TSESTree.Identifier[]): TSESTree.Identifier[];
|
|
19
|
+
declare function sourceText(node: TSESTree.Node): string | null | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* The module a type import names. TypeScript-ESLint 8 calls it `source` and keeps `argument` as a deprecated alias
|
|
22
|
+
* that warns when read; Oxlint's ESTree bridge exposes `source` as well, so `argument` is only read where it is the
|
|
23
|
+
* one there is.
|
|
24
|
+
*/
|
|
25
|
+
declare function importTypeSource(node: {
|
|
26
|
+
readonly argument?: TSESTree.Node;
|
|
27
|
+
readonly source?: TSESTree.Node;
|
|
28
|
+
}): TSESTree.Node | null;
|
|
17
29
|
/** Lexical containment by range: the one question a rule that collects nodes and answers later keeps asking. */
|
|
18
30
|
declare function within(node: TSESTree.Node, outer: TSESTree.Node): boolean;
|
|
19
|
-
export { calleeName, findProperty, functionName, isFunctionLike, memberFirstStep, memberRoot, patternKeys, propertyName, within, };
|
|
31
|
+
export { boundNames, calleeName, findProperty, functionName, importTypeSource, isFunctionLike, memberFirstStep, memberRoot, patternKeys, propertyName, sourceText, within, };
|
package/dist/ast.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as
|
|
1
|
+
import{AST_NODE_TYPES as r}from"@typescript-eslint/types";function o(e){const{callee:t}=e;if(t.type===r.Identifier)return t.name;if(!(t.type!==r.MemberExpression||t.computed)&&!(t.object.type!==r.Identifier||t.property.type!==r.Identifier))return`${t.object.name}.${t.property.name}`}function i(e){if(!(e.type!==r.Property||e.computed)){if(e.key.type===r.Identifier)return e.key.name;if(e.key.type===r.Literal&&typeof e.key.value=="string")return e.key.value}}function y(e,t){for(const n of e.properties)if(n.type===r.Property&&i(n)===t)return n}function c(e){const t=new Set;for(const n of e.properties){const u=i(n);u!==void 0&&t.add(u)}return t}function s(e){let t=e;for(;t.type===r.MemberExpression;)t=t.object;return t}function l(e){let t=e;for(;t.object.type===r.MemberExpression;)t=t.object;if(!(t.computed||t.property.type!==r.Identifier))return t.property.name}function p(e){return e.type===r.ArrowFunctionExpression||e.type===r.FunctionExpression}function m(e){let t=e;for(;t.parent.type===r.CallExpression||t.parent.type===r.TSAsExpression;)t=t.parent;const{parent:n}=t;return n.type===r.VariableDeclarator&&n.id.type===r.Identifier?n.id.name:i(n)}function b(e){return e.type===r.FunctionDeclaration?e.id?.name:p(e)?(e.type===r.FunctionExpression?e.id?.name:void 0)??m(e):void 0}function x(e){return e.type===r.ArrayPattern?e.elements:e.type===r.AssignmentPattern?[e.left]:e.type===r.RestElement?[e.argument]:e.type!==r.ObjectPattern?[]:e.properties.map(t=>t.type===r.Property?t.value:t)}function f(e,t=[]){if(e==null)return t;if(e.type===r.Identifier)t.push(e);else for(const n of x(e))f(n,t);return t}function a(e){if(e.type===r.TSLiteralType)return a(e.literal);if(e.type===r.Literal&&typeof e.value=="string")return e.value;if(e.type===r.TemplateLiteral&&e.expressions.length===0)return e.quasis[0]?.value.cooked}function E(e){return e.source??e.argument??null}function d(e,t){return e.range[0]>=t.range[0]&&e.range[1]<=t.range[1]}export{f as boundNames,o as calleeName,y as findProperty,b as functionName,E as importTypeSource,p as isFunctionLike,l as memberFirstStep,s as memberRoot,c as patternKeys,i as propertyName,a as sourceText,d as within};
|
|
2
2
|
//# sourceMappingURL=ast.js.map
|
package/dist/ast.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast.js","sources":["../src/ast.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"ast.js","sources":["../src/ast.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\ntype FunctionLike = TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression;\n\n/** `slot` and `defineFeature.body` alike: the dotted name of the simple callee shapes an author writes. */\nfunction calleeName(node: TSESTree.CallExpression): string | undefined {\n const { callee } = node;\n\n if (callee.type === AST_NODE_TYPES.Identifier) return callee.name;\n\n if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) return undefined;\n\n if (callee.object.type !== AST_NODE_TYPES.Identifier || callee.property.type !== AST_NODE_TYPES.Identifier) {\n return undefined;\n }\n\n return `${callee.object.name}.${callee.property.name}`;\n}\n\n/** The static name of a property or a destructured key; a computed or dynamic key has none. */\nfunction propertyName(node: TSESTree.Node): string | undefined {\n if (node.type !== AST_NODE_TYPES.Property || node.computed) return undefined;\n\n if (node.key.type === AST_NODE_TYPES.Identifier) return node.key.name;\n\n if (node.key.type === AST_NODE_TYPES.Literal && typeof node.key.value === 'string') return node.key.value;\n\n return undefined;\n}\n\n/** The property of an object literal under a static key. */\nfunction findProperty(node: TSESTree.ObjectExpression, name: string): TSESTree.Property | undefined {\n for (const property of node.properties) {\n if (property.type === AST_NODE_TYPES.Property && propertyName(property) === name) return property;\n }\n\n return undefined;\n}\n\nfunction patternKeys(pattern: TSESTree.ObjectPattern): ReadonlySet<string> {\n const keys = new Set<string>();\n\n for (const property of pattern.properties) {\n const name = propertyName(property);\n\n if (name !== undefined) keys.add(name);\n }\n\n return keys;\n}\n\n/** The identifier at the root of `imports.platform.submitAllowed`. */\nfunction memberRoot(node: TSESTree.MemberExpression): TSESTree.Node {\n let current: TSESTree.Node = node;\n\n while (current.type === AST_NODE_TYPES.MemberExpression) current = current.object;\n\n return current;\n}\n\n/** The first step of `context.imports.platform`, that is `imports`. */\nfunction memberFirstStep(node: TSESTree.MemberExpression): string | undefined {\n let current = node;\n\n while (current.object.type === AST_NODE_TYPES.MemberExpression) current = current.object;\n\n if (current.computed || current.property.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n return current.property.name;\n}\n\nfunction isFunctionLike(node: TSESTree.Node): node is FunctionLike {\n return node.type === AST_NODE_TYPES.ArrowFunctionExpression || node.type === AST_NODE_TYPES.FunctionExpression;\n}\n\n/** The binding a function expression is written into, past the wrappers it is passed through. */\nfunction boundName(node: FunctionLike): string | undefined {\n let current: TSESTree.Node = node;\n\n while (\n current.parent.type === AST_NODE_TYPES.CallExpression ||\n current.parent.type === AST_NODE_TYPES.TSAsExpression\n ) {\n current = current.parent;\n }\n\n const { parent } = current;\n\n if (parent.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier) {\n return parent.id.name;\n }\n\n return propertyName(parent);\n}\n\n/** The name a function is written under: its own, or the binding it is assigned to. */\nfunction functionName(node: TSESTree.Node): string | undefined {\n if (node.type === AST_NODE_TYPES.FunctionDeclaration) return node.id?.name;\n\n if (!isFunctionLike(node)) return undefined;\n\n const own = node.type === AST_NODE_TYPES.FunctionExpression ? node.id?.name : undefined;\n\n return own ?? boundName(node);\n}\n\n/** The patterns one level inside a binding pattern. */\nfunction innerPatterns(node: TSESTree.Node): readonly (TSESTree.Node | null)[] {\n if (node.type === AST_NODE_TYPES.ArrayPattern) return node.elements;\n\n if (node.type === AST_NODE_TYPES.AssignmentPattern) return [node.left];\n\n if (node.type === AST_NODE_TYPES.RestElement) return [node.argument];\n\n if (node.type !== AST_NODE_TYPES.ObjectPattern) return [];\n\n return node.properties.map(property => (property.type === AST_NODE_TYPES.Property ? property.value : property));\n}\n\n/** The names a parameter binds, whether it is one name or a pattern taken apart. */\nfunction boundNames(node: TSESTree.Node | null | undefined, names: TSESTree.Identifier[] = []): TSESTree.Identifier[] {\n if (node === null || node === undefined) return names;\n\n if (node.type === AST_NODE_TYPES.Identifier) names.push(node);\n else for (const inner of innerPatterns(node)) boundNames(inner, names);\n\n return names;\n}\n\nfunction sourceText(node: TSESTree.Node): string | null | undefined {\n if (node.type === AST_NODE_TYPES.TSLiteralType) return sourceText(node.literal);\n if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') return node.value;\n if (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0)\n return node.quasis[0]?.value.cooked;\n return undefined;\n}\n\n/**\n * The module a type import names. TypeScript-ESLint 8 calls it `source` and keeps `argument` as a deprecated alias\n * that warns when read; Oxlint's ESTree bridge exposes `source` as well, so `argument` is only read where it is the\n * one there is.\n */\nfunction importTypeSource(node: {\n readonly argument?: TSESTree.Node;\n readonly source?: TSESTree.Node;\n}): TSESTree.Node | null {\n return node.source ?? node.argument ?? null;\n}\n\n/** Lexical containment by range: the one question a rule that collects nodes and answers later keeps asking. */\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\nexport {\n boundNames,\n calleeName,\n findProperty,\n functionName,\n importTypeSource,\n isFunctionLike,\n memberFirstStep,\n memberRoot,\n patternKeys,\n propertyName,\n sourceText,\n within,\n};\n"],"names":["calleeName","node","callee","AST_NODE_TYPES","propertyName","findProperty","name","property","patternKeys","pattern","keys","memberRoot","current","memberFirstStep","isFunctionLike","boundName","parent","functionName","innerPatterns","boundNames","names","inner","sourceText","importTypeSource","within","outer"],"mappings":"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"}
|
package/dist/command-hooks.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/types';
|
|
2
|
+
import type { SourceCode, Variable } from './rule.js';
|
|
2
3
|
/** What a name holds: one hook, a selection whose fields may be hooks, or one such field (D388). */
|
|
3
4
|
type HookKind = 'candidate' | 'hook' | 'selection';
|
|
4
|
-
type HookKinds = Map<
|
|
5
|
+
type HookKinds = Map<Variable, HookKind>;
|
|
5
6
|
/**
|
|
6
7
|
* What a dependency element names: one hook (no key, not a record), one key of a record, or the record itself,
|
|
7
8
|
* which is a fresh object on every render.
|
|
@@ -10,7 +11,7 @@ type Binding = {
|
|
|
10
11
|
readonly key: string | undefined;
|
|
11
12
|
readonly proved: boolean;
|
|
12
13
|
readonly record: boolean;
|
|
13
|
-
readonly variable:
|
|
14
|
+
readonly variable: Variable;
|
|
14
15
|
};
|
|
15
16
|
/** One read of the binding: the member it ends in, and the text the author wrote to reach it. */
|
|
16
17
|
type Access = {
|
|
@@ -25,10 +26,10 @@ type Usage = {
|
|
|
25
26
|
};
|
|
26
27
|
declare const STATUS_MEMBERS: string[];
|
|
27
28
|
/** Every read of the binding inside one callback, and whether any of them takes the object as a whole. */
|
|
28
|
-
declare function usageIn(source:
|
|
29
|
+
declare function usageIn(source: SourceCode, binding: Binding, callback: TSESTree.Node | undefined): Usage;
|
|
29
30
|
/** The binding a dependency element names as a whole; a member of one is a member, and this returns nothing. */
|
|
30
|
-
declare function bindingAt(source:
|
|
31
|
+
declare function bindingAt(source: SourceCode, kinds: HookKinds, element: TSESTree.Node): Binding | undefined;
|
|
31
32
|
/** Records what one declaration binds, so a later dependency array is read against names and not against spelling. */
|
|
32
|
-
declare function classifyDeclaration(source:
|
|
33
|
+
declare function classifyDeclaration(source: SourceCode, declarator: TSESTree.VariableDeclarator, kinds: HookKinds): void;
|
|
33
34
|
export type { Access, Binding, HookKinds, Usage };
|
|
34
35
|
export { bindingAt, classifyDeclaration, STATUS_MEMBERS, usageIn };
|
package/dist/command-hooks.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as o}from"@typescript-eslint/
|
|
1
|
+
import{AST_NODE_TYPES as o}from"@typescript-eslint/types";import{apiName as v,unwrap as p,localValue as I,variableOf as d}from"./model-bindings.js";const O=new Set(["inFlight","outcome","run"]),S=["inFlight","outcome"],l=new Set(["selection"]),h=2,y={member:void 0,optional:!1,text:""},g={accesses:[],opaque:!0};function c(n){const{parent:e}=n;if(!(e.type!==o.MemberExpression||e.object!==n||e.computed)&&e.property.type===o.Identifier)return{key:e.property.name,node:e,optional:e.optional}}function a(n,e,i){return e===void 0||!O.has(e.key)?y:{member:e.key,optional:i||e.optional,text:n.getText(e.node)}}function m(n,e,i){const t=c(e);return!i.record&&i.key===void 0?a(n,t,!1):t===void 0?y:i.key===void 0||t.key===i.key?a(n,c(t.node),t.optional):void 0}function R(n,e){return n.range[0]>=e.range[0]&&n.range[1]<=e.range[1]}function x(n,e,i){if(i===void 0)return g;const t=[];let r=!1;for(const{identifier:f}of e.variable.references){if(f.type!==o.Identifier||!R(f,i))continue;const u=m(n,f,e);u!==void 0&&(u.member===void 0?r=!0:t.push(u))}return{accesses:t,opaque:r}}function A(n,e){for(const{identifier:i}of e.variable.references)if(i.type===o.Identifier&&m(n,i,e)?.member==="run")return!0;return!1}function k(n,e){return e===null?"":n.get(e)??""}function E(n,e){return!e.proved||A(n,e)?e:void 0}function M(n,e,i){const t=d(n,i),r=k(e,t);if(t===null||r==="")return;const f=l.has(r);return E(n,{key:void 0,proved:r==="candidate",record:f,variable:t})}function T(n){const e=p(n.object);if(!(n.computed||n.property.type!==o.Identifier))return e.type===o.Identifier?{key:n.property.name,object:e}:void 0}function _(n,e,i){const t=T(i);if(t===void 0)return;const r=d(n,t.object),f=k(e,r);if(!(r===null||!l.has(f)))return E(n,{key:t.key,proved:f==="selection",record:!1,variable:r})}function j(n,e,i){const t=p(i);return t.type===o.Identifier?M(n,e,t):t.type===o.MemberExpression?_(n,e,t):void 0}function s(n,e,i,t){if(e.type!==o.Identifier)return;const r=d(n,e);r!==null&&i.set(r,t)}function B(n,e,i,t,r){if(e.type!==o.ObjectPattern){s(n,e,i,t);return}for(const f of e.properties)f.type===o.Property&&s(n,f.value,i,r)}function C(n,e){if(e.parent.kind!=="const"||e.init===null)return;const i=I(n,e.init);return i.type===o.CallExpression?i:void 0}function D(n,e,i){const t=C(n,e);if(t===void 0)return;const r=v(n,t.callee,"@opetope/react");r==="useCommand"?s(n,e.id,i,"hook"):r==="useModel"&&t.arguments.length>=h&&B(n,e.id,i,"selection","candidate")}export{S as STATUS_MEMBERS,j as bindingAt,D as classifyDeclaration,x as usageIn};
|
|
2
2
|
//# sourceMappingURL=command-hooks.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-hooks.js","sources":["../src/command-hooks.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { apiName, localValue, unwrap, variableOf } from './model-bindings';\n\n/** What a name holds: one hook, a selection whose fields may be hooks, or one such field (D388). */\ntype HookKind = 'candidate' | 'hook' | 'selection';\ntype HookKinds = Map<TSESLint.Scope.Variable, HookKind>;\n/**\n * What a dependency element names: one hook (no key, not a record), one key of a record, or the record itself,\n * which is a fresh object on every render.\n */\ntype Binding = {\n readonly key: string | undefined;\n readonly proved: boolean;\n readonly record: boolean;\n readonly variable: TSESLint.Scope.Variable;\n};\n/** One read of the binding: the member it ends in, and the text the author wrote to reach it. */\ntype Access = { readonly member: string | undefined; readonly optional: boolean; readonly text: string };\n/** `opaque` is a use no member replaces: the object passed on, destructured, or read past a member we know. */\ntype Usage = { readonly accesses: readonly Access[]; readonly opaque: boolean };\ntype Step = { readonly key: string; readonly node: TSESTree.MemberExpression; readonly optional: boolean };\ntype Occurrence = TSESTree.Identifier | TSESTree.MemberExpression;\n\nconst HOOK_MEMBERS = new Set(['inFlight', 'outcome', 'run']);\nconst STATUS_MEMBERS = ['inFlight', 'outcome'];\nconst RECORD_KINDS = new Set(['selection']);\nconst SELECTION_ARITY = 2;\nconst OPAQUE: Access = { member: undefined, optional: false, text: '' };\n/** A callback this module cannot read is not proof of anything, so its binding takes no fix and no suggestion. */\nconst UNREAD: Usage = { accesses: [], opaque: true };\n\n/** One written step off an occurrence: `.save`, with the `?.` the author may have put in front of it. */\nfunction step(node: Occurrence): Step | undefined {\n const { parent } = node;\n\n if (parent.type !== AST_NODE_TYPES.MemberExpression || parent.object !== node || parent.computed) return undefined;\n\n if (parent.property.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n return { key: parent.property.name, node: parent, optional: parent.optional };\n}\n\n/** A step that lands on a member this rule knows; anything else is a use it must not rewrite. */\nfunction accessAt(source: TSESLint.SourceCode, member: Step | undefined, over: boolean): Access {\n if (member === undefined || !HOOK_MEMBERS.has(member.key)) return OPAQUE;\n\n return { member: member.key, optional: over || member.optional, text: source.getText(member.node) };\n}\n\n/** What this occurrence reads on the binding: one step for a hook, two for a record — key, then member. */\nfunction accessOf(source: TSESLint.SourceCode, node: TSESTree.Identifier, binding: Binding): Access | undefined {\n const first = step(node);\n\n if (!binding.record && binding.key === undefined) return accessAt(source, first, false);\n\n if (first === undefined) return OPAQUE;\n\n if (binding.key === undefined) return accessAt(source, step(first.node), first.optional);\n\n return first.key === binding.key ? accessAt(source, step(first.node), first.optional) : undefined;\n}\n\nfunction within(node: TSESTree.Node, callback: TSESTree.Node): boolean {\n return node.range[0] >= callback.range[0] && node.range[1] <= callback.range[1];\n}\n\n/** Every read of the binding inside one callback, and whether any of them takes the object as a whole. */\nfunction usageIn(source: TSESLint.SourceCode, binding: Binding, callback: TSESTree.Node | undefined): Usage {\n if (callback === undefined) return UNREAD;\n\n const accesses: Access[] = [];\n let opaque = false;\n\n for (const { identifier } of binding.variable.references) {\n if (identifier.type !== AST_NODE_TYPES.Identifier || !within(identifier, callback)) continue;\n\n const access = accessOf(source, identifier, binding);\n\n if (access === undefined) continue;\n if (access.member === undefined) opaque = true;\n else accesses.push(access);\n }\n\n return { accesses, opaque };\n}\n\n/**\n * A selected field is a command hook where the code calls or reads `run` on it. A status name alone proves\n * nothing: plain data may carry one, and this package reads shapes rather than types.\n */\nfunction provesRun(source: TSESLint.SourceCode, binding: Binding): boolean {\n for (const { identifier } of binding.variable.references) {\n if (identifier.type !== AST_NODE_TYPES.Identifier) continue;\n\n if (accessOf(source, identifier, binding)?.member === 'run') return true;\n }\n\n return false;\n}\n\nfunction kindOf(kinds: HookKinds, variable: TSESLint.Scope.Variable | null): string {\n return variable === null ? '' : (kinds.get(variable) ?? '');\n}\n\n/** A kind that is a claim about a selected field keeps the binding only where the code proves `run` on it. */\nfunction provedBinding(source: TSESLint.SourceCode, binding: Binding): Binding | undefined {\n return !binding.proved || provesRun(source, binding) ? binding : undefined;\n}\n\nfunction namedBinding(source: TSESLint.SourceCode, kinds: HookKinds, node: TSESTree.Identifier): Binding | undefined {\n const variable = variableOf(source, node);\n const kind = kindOf(kinds, variable);\n\n if (variable === null || kind === '') return undefined;\n\n const record = RECORD_KINDS.has(kind);\n\n return provedBinding(source, { key: undefined, proved: kind === 'candidate', record, variable });\n}\n\n/** `hooks.save`: the plain name it is read from and the key it reads, when both of them are written as names. */\nfunction keyRead(node: TSESTree.MemberExpression): { key: string; object: TSESTree.Identifier } | undefined {\n const object = unwrap(node.object);\n\n if (node.computed || node.property.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n return object.type === AST_NODE_TYPES.Identifier ? { key: node.property.name, object } : undefined;\n}\n\nfunction keyedBinding(\n source: TSESLint.SourceCode,\n kinds: HookKinds,\n node: TSESTree.MemberExpression,\n): Binding | undefined {\n const read = keyRead(node);\n\n if (read === undefined) return undefined;\n\n const variable = variableOf(source, read.object);\n const kind = kindOf(kinds, variable);\n\n if (variable === null || !RECORD_KINDS.has(kind)) return undefined;\n\n return provedBinding(source, { key: read.key, proved: kind === 'selection', record: false, variable });\n}\n\n/** The binding a dependency element names as a whole; a member of one is a member, and this returns nothing. */\nfunction bindingAt(source: TSESLint.SourceCode, kinds: HookKinds, element: TSESTree.Node): Binding | undefined {\n const value = unwrap(element);\n\n if (value.type === AST_NODE_TYPES.Identifier) return namedBinding(source, kinds, value);\n\n return value.type === AST_NODE_TYPES.MemberExpression ? keyedBinding(source, kinds, value) : undefined;\n}\n\nfunction mark(source: TSESLint.SourceCode, node: TSESTree.Node, kinds: HookKinds, kind: HookKind): void {\n if (node.type !== AST_NODE_TYPES.Identifier) return;\n\n const variable = variableOf(source, node);\n\n if (variable !== null) kinds.set(variable, kind);\n}\n\n/** `const hooks = useModel(M, select)` names the record; `const { save } = useModel(M, select)` names each key. */\nfunction markPattern(\n source: TSESLint.SourceCode,\n node: TSESTree.Node,\n kinds: HookKinds,\n whole: HookKind,\n each: HookKind,\n): void {\n if (node.type !== AST_NODE_TYPES.ObjectPattern) {\n mark(source, node, kinds, whole);\n\n return;\n }\n\n for (const property of node.properties) {\n if (property.type === AST_NODE_TYPES.Property) mark(source, property.value, kinds, each);\n }\n}\n\n/** A reassignable name holds whatever was last written to it; only a `const` holds the hook it was opened with. */\nfunction constantCall(\n source: TSESLint.SourceCode,\n declarator: TSESTree.VariableDeclarator,\n): TSESTree.CallExpression | undefined {\n if (declarator.parent.kind !== 'const' || declarator.init === null) return undefined;\n\n const value = localValue(source, declarator.init);\n\n return value.type === AST_NODE_TYPES.CallExpression ? value : undefined;\n}\n\n/** Records what one declaration binds, so a later dependency array is read against names and not against spelling. */\nfunction classifyDeclaration(\n source: TSESLint.SourceCode,\n declarator: TSESTree.VariableDeclarator,\n kinds: HookKinds,\n): void {\n const call = constantCall(source, declarator);\n\n if (call === undefined) return;\n\n const name = apiName(source, call.callee, '@opetope/react');\n\n if (name === 'useCommand') mark(source, declarator.id, kinds, 'hook');\n // D388: `useCommands` is gone; the record a render depends on now comes from `useModel` with a selection.\n // A selection field is a hook only where the code proves it; without a selector the granted model is stable.\n else if (name === 'useModel' && call.arguments.length >= SELECTION_ARITY)\n markPattern(source, declarator.id, kinds, 'selection', 'candidate');\n}\n\nexport type { Access, Binding, HookKinds, Usage };\nexport { bindingAt, classifyDeclaration, STATUS_MEMBERS, usageIn };\n"],"names":["HOOK_MEMBERS","STATUS_MEMBERS","RECORD_KINDS","SELECTION_ARITY","OPAQUE","UNREAD","step","node","parent","AST_NODE_TYPES","accessAt","source","member","over","accessOf","binding","first","within","callback","usageIn","accesses","opaque","identifier","access","provesRun","kindOf","kinds","variable","provedBinding","namedBinding","variableOf","kind","record","keyRead","object","unwrap","keyedBinding","read","bindingAt","element","value","mark","markPattern","whole","each","property","constantCall","declarator","localValue","classifyDeclaration","call","name","apiName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"command-hooks.js","sources":["../src/command-hooks.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { apiName, localValue, unwrap, variableOf } from './model-bindings';\nimport type { SourceCode, Variable } from './rule';\n\n/** What a name holds: one hook, a selection whose fields may be hooks, or one such field (D388). */\ntype HookKind = 'candidate' | 'hook' | 'selection';\ntype HookKinds = Map<Variable, HookKind>;\n/**\n * What a dependency element names: one hook (no key, not a record), one key of a record, or the record itself,\n * which is a fresh object on every render.\n */\ntype Binding = {\n readonly key: string | undefined;\n readonly proved: boolean;\n readonly record: boolean;\n readonly variable: Variable;\n};\n/** One read of the binding: the member it ends in, and the text the author wrote to reach it. */\ntype Access = { readonly member: string | undefined; readonly optional: boolean; readonly text: string };\n/** `opaque` is a use no member replaces: the object passed on, destructured, or read past a member we know. */\ntype Usage = { readonly accesses: readonly Access[]; readonly opaque: boolean };\ntype Step = { readonly key: string; readonly node: TSESTree.MemberExpression; readonly optional: boolean };\ntype Occurrence = TSESTree.Identifier | TSESTree.MemberExpression;\n\nconst HOOK_MEMBERS = new Set(['inFlight', 'outcome', 'run']);\nconst STATUS_MEMBERS = ['inFlight', 'outcome'];\nconst RECORD_KINDS = new Set(['selection']);\nconst SELECTION_ARITY = 2;\nconst OPAQUE: Access = { member: undefined, optional: false, text: '' };\n/** A callback this module cannot read is not proof of anything, so its binding takes no fix and no suggestion. */\nconst UNREAD: Usage = { accesses: [], opaque: true };\n\n/** One written step off an occurrence: `.save`, with the `?.` the author may have put in front of it. */\nfunction step(node: Occurrence): Step | undefined {\n const { parent } = node;\n\n if (parent.type !== AST_NODE_TYPES.MemberExpression || parent.object !== node || parent.computed) return undefined;\n\n if (parent.property.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n return { key: parent.property.name, node: parent, optional: parent.optional };\n}\n\n/** A step that lands on a member this rule knows; anything else is a use it must not rewrite. */\nfunction accessAt(source: SourceCode, member: Step | undefined, over: boolean): Access {\n if (member === undefined || !HOOK_MEMBERS.has(member.key)) return OPAQUE;\n\n return { member: member.key, optional: over || member.optional, text: source.getText(member.node) };\n}\n\n/** What this occurrence reads on the binding: one step for a hook, two for a record — key, then member. */\nfunction accessOf(source: SourceCode, node: TSESTree.Identifier, binding: Binding): Access | undefined {\n const first = step(node);\n\n if (!binding.record && binding.key === undefined) return accessAt(source, first, false);\n\n if (first === undefined) return OPAQUE;\n\n if (binding.key === undefined) return accessAt(source, step(first.node), first.optional);\n\n return first.key === binding.key ? accessAt(source, step(first.node), first.optional) : undefined;\n}\n\nfunction within(node: TSESTree.Node, callback: TSESTree.Node): boolean {\n return node.range[0] >= callback.range[0] && node.range[1] <= callback.range[1];\n}\n\n/** Every read of the binding inside one callback, and whether any of them takes the object as a whole. */\nfunction usageIn(source: SourceCode, binding: Binding, callback: TSESTree.Node | undefined): Usage {\n if (callback === undefined) return UNREAD;\n\n const accesses: Access[] = [];\n let opaque = false;\n\n for (const { identifier } of binding.variable.references) {\n if (identifier.type !== AST_NODE_TYPES.Identifier || !within(identifier, callback)) continue;\n\n const access = accessOf(source, identifier, binding);\n\n if (access === undefined) continue;\n if (access.member === undefined) opaque = true;\n else accesses.push(access);\n }\n\n return { accesses, opaque };\n}\n\n/**\n * A selected field is a command hook where the code calls or reads `run` on it. A status name alone proves\n * nothing: plain data may carry one, and this package reads shapes rather than types.\n */\nfunction provesRun(source: SourceCode, binding: Binding): boolean {\n for (const { identifier } of binding.variable.references) {\n if (identifier.type !== AST_NODE_TYPES.Identifier) continue;\n\n if (accessOf(source, identifier, binding)?.member === 'run') return true;\n }\n\n return false;\n}\n\nfunction kindOf(kinds: HookKinds, variable: Variable | null): string {\n return variable === null ? '' : (kinds.get(variable) ?? '');\n}\n\n/** A kind that is a claim about a selected field keeps the binding only where the code proves `run` on it. */\nfunction provedBinding(source: SourceCode, binding: Binding): Binding | undefined {\n return !binding.proved || provesRun(source, binding) ? binding : undefined;\n}\n\nfunction namedBinding(source: SourceCode, kinds: HookKinds, node: TSESTree.Identifier): Binding | undefined {\n const variable = variableOf(source, node);\n const kind = kindOf(kinds, variable);\n\n if (variable === null || kind === '') return undefined;\n\n const record = RECORD_KINDS.has(kind);\n\n return provedBinding(source, { key: undefined, proved: kind === 'candidate', record, variable });\n}\n\n/** `hooks.save`: the plain name it is read from and the key it reads, when both of them are written as names. */\nfunction keyRead(node: TSESTree.MemberExpression): { key: string; object: TSESTree.Identifier } | undefined {\n const object = unwrap(node.object);\n\n if (node.computed || node.property.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n return object.type === AST_NODE_TYPES.Identifier ? { key: node.property.name, object } : undefined;\n}\n\nfunction keyedBinding(source: SourceCode, kinds: HookKinds, node: TSESTree.MemberExpression): Binding | undefined {\n const read = keyRead(node);\n\n if (read === undefined) return undefined;\n\n const variable = variableOf(source, read.object);\n const kind = kindOf(kinds, variable);\n\n if (variable === null || !RECORD_KINDS.has(kind)) return undefined;\n\n return provedBinding(source, { key: read.key, proved: kind === 'selection', record: false, variable });\n}\n\n/** The binding a dependency element names as a whole; a member of one is a member, and this returns nothing. */\nfunction bindingAt(source: SourceCode, kinds: HookKinds, element: TSESTree.Node): Binding | undefined {\n const value = unwrap(element);\n\n if (value.type === AST_NODE_TYPES.Identifier) return namedBinding(source, kinds, value);\n\n return value.type === AST_NODE_TYPES.MemberExpression ? keyedBinding(source, kinds, value) : undefined;\n}\n\nfunction mark(source: SourceCode, node: TSESTree.Node, kinds: HookKinds, kind: HookKind): void {\n if (node.type !== AST_NODE_TYPES.Identifier) return;\n\n const variable = variableOf(source, node);\n\n if (variable !== null) kinds.set(variable, kind);\n}\n\n/** `const hooks = useModel(M, select)` names the record; `const { save } = useModel(M, select)` names each key. */\nfunction markPattern(source: SourceCode, node: TSESTree.Node, kinds: HookKinds, whole: HookKind, each: HookKind): void {\n if (node.type !== AST_NODE_TYPES.ObjectPattern) {\n mark(source, node, kinds, whole);\n\n return;\n }\n\n for (const property of node.properties) {\n if (property.type === AST_NODE_TYPES.Property) mark(source, property.value, kinds, each);\n }\n}\n\n/** A reassignable name holds whatever was last written to it; only a `const` holds the hook it was opened with. */\nfunction constantCall(\n source: SourceCode,\n declarator: TSESTree.VariableDeclarator,\n): TSESTree.CallExpression | undefined {\n if (declarator.parent.kind !== 'const' || declarator.init === null) return undefined;\n\n const value = localValue(source, declarator.init);\n\n return value.type === AST_NODE_TYPES.CallExpression ? value : undefined;\n}\n\n/** Records what one declaration binds, so a later dependency array is read against names and not against spelling. */\nfunction classifyDeclaration(source: SourceCode, declarator: TSESTree.VariableDeclarator, kinds: HookKinds): void {\n const call = constantCall(source, declarator);\n\n if (call === undefined) return;\n\n const name = apiName(source, call.callee, '@opetope/react');\n\n if (name === 'useCommand') mark(source, declarator.id, kinds, 'hook');\n // D388: `useCommands` is gone; the record a render depends on now comes from `useModel` with a selection.\n // A selection field is a hook only where the code proves it; without a selector the granted model is stable.\n else if (name === 'useModel' && call.arguments.length >= SELECTION_ARITY)\n markPattern(source, declarator.id, kinds, 'selection', 'candidate');\n}\n\nexport type { Access, Binding, HookKinds, Usage };\nexport { bindingAt, classifyDeclaration, STATUS_MEMBERS, usageIn };\n"],"names":["HOOK_MEMBERS","STATUS_MEMBERS","RECORD_KINDS","SELECTION_ARITY","OPAQUE","UNREAD","step","node","parent","AST_NODE_TYPES","accessAt","source","member","over","accessOf","binding","first","within","callback","usageIn","accesses","opaque","identifier","access","provesRun","kindOf","kinds","variable","provedBinding","namedBinding","variableOf","kind","record","keyRead","object","unwrap","keyedBinding","read","bindingAt","element","value","mark","markPattern","whole","each","property","constantCall","declarator","localValue","classifyDeclaration","call","name","apiName"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/types';
|
|
2
2
|
import type { FunctionNode } from './model-bindings.js';
|
|
3
|
-
type Variable
|
|
3
|
+
import type { SourceCode, Variable } from './rule.js';
|
|
4
4
|
/**
|
|
5
5
|
* The names one callback reads its execution context through: the parameter itself, and what was taken out of it —
|
|
6
6
|
* renamed in the parameter list, taken apart by a declaration inside the body, or given a second name by a `const`.
|
|
@@ -12,11 +12,11 @@ type ContextMembers<Key extends string> = {
|
|
|
12
12
|
readonly parameter: Variable | undefined;
|
|
13
13
|
};
|
|
14
14
|
/** A name taken from the context under this key, or the key read off the context itself. */
|
|
15
|
-
declare function fromContext<Key extends string>(source:
|
|
15
|
+
declare function fromContext<Key extends string>(source: SourceCode, node: TSESTree.Node, members: ContextMembers<Key>, key: Key): boolean;
|
|
16
16
|
/**
|
|
17
17
|
* The context of one callback, whether it arrived whole under a name or taken apart where the callback receives it.
|
|
18
18
|
* The position is part of the contract: a command takes its context second and a reaction run takes it first.
|
|
19
19
|
*/
|
|
20
|
-
declare function contextMembers<Key extends string>(source:
|
|
20
|
+
declare function contextMembers<Key extends string>(source: SourceCode, fn: FunctionNode, position: number, keys: readonly Key[], declarators: readonly TSESTree.VariableDeclarator[]): ContextMembers<Key>;
|
|
21
21
|
export type { ContextMembers };
|
|
22
22
|
export { contextMembers, fromContext };
|
package/dist/context-members.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as o}from"@typescript-eslint/
|
|
1
|
+
import{AST_NODE_TYPES as o}from"@typescript-eslint/types";import{within as b,propertyName as v}from"./ast.js";import{variableOf as p,unwrap as I}from"./model-bindings.js";const O=new Set;function P(n,t){return n.bound.get(t)??O}function c(n,t,e){const r=n.bound.get(t);r===void 0?n.bound.set(t,new Set([e])):r.add(e)}function l(n,t,e){return e!==void 0&&t.type===o.Identifier&&p(n,t)===e}function w(n,t,e,r){return t.type!==o.MemberExpression||t.computed?!1:t.property.type===o.Identifier&&t.property.name===r&&l(n,t.object,e.parameter)}function m(n,t,e,r){if(t.type!==o.Identifier)return w(n,t,e,r);const i=p(n,t);return i!==null&&P(e,r).has(i)}function s(n,t,e,r){for(const i of t.properties){if(i.type!==o.Property||i.value.type!==o.Identifier)continue;const u=v(i),f=r.find(d=>d===u),a=f===void 0?null:p(n,i.value);f!==void 0&&a!==null&&c(e,f,a)}}function x(n,t,e,r){const{id:i,init:u}=t;if(u===null)return;const f=I(u);if(i.type===o.ObjectPattern){l(n,f,e.parameter)&&s(n,i,e,r);return}const a=i.type===o.Identifier?p(n,i):null,d=r.find(y=>m(n,f,e,y));a!==null&&d!==void 0&&c(e,d,a)}function E(n,t){return t?.type===o.Identifier?p(n,t)??void 0:void 0}function M(n,t,e,r,i){const u=t.params[e],f={bound:new Map,parameter:E(n,u)};u?.type===o.ObjectPattern&&s(n,u,f,r);for(const a of i)b(a,t)&&x(n,a,f,r);return f}export{M as contextMembers,m as fromContext};
|
|
2
2
|
//# sourceMappingURL=context-members.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-members.js","sources":["../src/context-members.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"context-members.js","sources":["../src/context-members.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { propertyName, within } from './ast';\nimport type { FunctionNode } from './model-bindings';\nimport { unwrap, variableOf } from './model-bindings';\nimport type { SourceCode, Variable } from './rule';\n\n/**\n * The names one callback reads its execution context through: the parameter itself, and what was taken out of it —\n * renamed in the parameter list, taken apart by a declaration inside the body, or given a second name by a `const`.\n * A closure over a member, a member stored anywhere but a `const` and a context handed to a helper are not followed,\n * and every rule that reads this says so in its own boundary.\n */\ntype ContextMembers<Key extends string> = {\n readonly bound: Map<Key, Set<Variable>>;\n readonly parameter: Variable | undefined;\n};\n\nconst NONE: ReadonlySet<Variable> = new Set();\n\nfunction boundOf<Key extends string>(members: ContextMembers<Key>, key: Key): ReadonlySet<Variable> {\n return members.bound.get(key) ?? NONE;\n}\n\nfunction add<Key extends string>(members: ContextMembers<Key>, key: Key, variable: Variable): void {\n const bound = members.bound.get(key);\n\n if (bound === undefined) members.bound.set(key, new Set([variable]));\n else bound.add(variable);\n}\n\nfunction isParameter(source: SourceCode, node: TSESTree.Node, parameter: Variable | undefined): boolean {\n return parameter !== undefined && node.type === AST_NODE_TYPES.Identifier && variableOf(source, node) === parameter;\n}\n\n/** `execution.<key>`, read off the context parameter itself. */\nfunction isContextMember<Key extends string>(\n source: SourceCode,\n node: TSESTree.Node,\n members: ContextMembers<Key>,\n key: Key,\n): boolean {\n if (node.type !== AST_NODE_TYPES.MemberExpression || node.computed) return false;\n\n return (\n node.property.type === AST_NODE_TYPES.Identifier &&\n node.property.name === key &&\n isParameter(source, node.object, members.parameter)\n );\n}\n\n/** A name taken from the context under this key, or the key read off the context itself. */\nfunction fromContext<Key extends string>(\n source: SourceCode,\n node: TSESTree.Node,\n members: ContextMembers<Key>,\n key: Key,\n): boolean {\n if (node.type !== AST_NODE_TYPES.Identifier) return isContextMember(source, node, members, key);\n\n const variable = variableOf(source, node);\n\n return variable !== null && boundOf(members, key).has(variable);\n}\n\n/** `({ update, signal: aborting })`: the variables a pattern bound under the keys a rule reads. */\nfunction takeFrom<Key extends string>(\n source: SourceCode,\n pattern: TSESTree.ObjectPattern,\n members: ContextMembers<Key>,\n keys: readonly Key[],\n): void {\n for (const property of pattern.properties) {\n if (property.type !== AST_NODE_TYPES.Property || property.value.type !== AST_NODE_TYPES.Identifier) continue;\n\n const name = propertyName(property);\n const key = keys.find(candidate => candidate === name);\n const variable = key === undefined ? null : variableOf(source, property.value);\n\n if (key !== undefined && variable !== null) add(members, key, variable);\n }\n}\n\n/**\n * `const { update } = execution` takes the context apart one line later, and `const write = update` gives a name the\n * body already holds a second one. Declarations are read in the order they are written, so a name is known before an\n * alias of it.\n */\nfunction takeFromDeclarator<Key extends string>(\n source: SourceCode,\n declarator: TSESTree.VariableDeclarator,\n members: ContextMembers<Key>,\n keys: readonly Key[],\n): void {\n const { id, init } = declarator;\n\n if (init === null) return;\n\n const value = unwrap(init);\n\n if (id.type === AST_NODE_TYPES.ObjectPattern) {\n if (isParameter(source, value, members.parameter)) takeFrom(source, id, members, keys);\n\n return;\n }\n\n const variable = id.type === AST_NODE_TYPES.Identifier ? variableOf(source, id) : null;\n const key = keys.find(candidate => fromContext(source, value, members, candidate));\n\n if (variable !== null && key !== undefined) add(members, key, variable);\n}\n\n/** The context parameter under the name it arrived with, or nothing where the callback took it apart at once. */\nfunction receivedParameter(source: SourceCode, received: TSESTree.Parameter | undefined): Variable | undefined {\n return received?.type === AST_NODE_TYPES.Identifier ? (variableOf(source, received) ?? undefined) : undefined;\n}\n\n/**\n * The context of one callback, whether it arrived whole under a name or taken apart where the callback receives it.\n * The position is part of the contract: a command takes its context second and a reaction run takes it first.\n */\nfunction contextMembers<Key extends string>(\n source: SourceCode,\n fn: FunctionNode,\n position: number,\n keys: readonly Key[],\n declarators: readonly TSESTree.VariableDeclarator[],\n): ContextMembers<Key> {\n const received = fn.params[position];\n const members: ContextMembers<Key> = { bound: new Map(), parameter: receivedParameter(source, received) };\n\n if (received?.type === AST_NODE_TYPES.ObjectPattern) takeFrom(source, received, members, keys);\n\n for (const declarator of declarators) {\n if (within(declarator, fn)) takeFromDeclarator(source, declarator, members, keys);\n }\n\n return members;\n}\n\nexport type { ContextMembers };\nexport { contextMembers, fromContext };\n"],"names":["NONE","boundOf","members","key","add","variable","bound","isParameter","source","node","parameter","AST_NODE_TYPES","variableOf","isContextMember","fromContext","takeFrom","pattern","keys","property","name","propertyName","candidate","takeFromDeclarator","declarator","id","init","value","unwrap","receivedParameter","received","contextMembers","fn","position","declarators","within"],"mappings":"2KAmBA,MAAMA,EAA8B,IAAI,IAExC,SAASC,EAA4BC,EAA8BC,EAAQ,CACzE,OAAOD,EAAQ,MAAM,IAAIC,CAAG,GAAKH,CACnC,CAEA,SAASI,EAAwBF,EAA8BC,EAAUE,EAAkB,CACzF,MAAMC,EAAQJ,EAAQ,MAAM,IAAIC,CAAG,EAE/BG,IAAU,OAAWJ,EAAQ,MAAM,IAAIC,EAAK,IAAI,IAAI,CAACE,CAAQ,CAAC,CAAC,EAC9DC,EAAM,IAAID,CAAQ,CACzB,CAEA,SAASE,EAAYC,EAAoBC,EAAqBC,EAA+B,CAC3F,OAAOA,IAAc,QAAaD,EAAK,OAASE,EAAe,YAAcC,EAAWJ,EAAQC,CAAI,IAAMC,CAC5G,CAGA,SAASG,EACPL,EACAC,EACAP,EACAC,EAAQ,CAER,OAAIM,EAAK,OAASE,EAAe,kBAAoBF,EAAK,SAAiB,GAGzEA,EAAK,SAAS,OAASE,EAAe,YACtCF,EAAK,SAAS,OAASN,GACvBI,EAAYC,EAAQC,EAAK,OAAQP,EAAQ,SAAS,CAEtD,CAGA,SAASY,EACPN,EACAC,EACAP,EACAC,EAAQ,CAER,GAAIM,EAAK,OAASE,EAAe,WAAY,OAAOE,EAAgBL,EAAQC,EAAMP,EAASC,CAAG,EAE9F,MAAME,EAAWO,EAAWJ,EAAQC,CAAI,EAExC,OAAOJ,IAAa,MAAQJ,EAAQC,EAASC,CAAG,EAAE,IAAIE,CAAQ,CAChE,CAGA,SAASU,EACPP,EACAQ,EACAd,EACAe,EAAoB,CAEpB,UAAWC,KAAYF,EAAQ,WAAY,CACzC,GAAIE,EAAS,OAASP,EAAe,UAAYO,EAAS,MAAM,OAASP,EAAe,WAAY,SAEpG,MAAMQ,EAAOC,EAAaF,CAAQ,EAC5Bf,EAAMc,EAAK,KAAKI,GAAaA,IAAcF,CAAI,EAC/Cd,EAAWF,IAAQ,OAAY,KAAOS,EAAWJ,EAAQU,EAAS,KAAK,EAEzEf,IAAQ,QAAaE,IAAa,MAAMD,EAAIF,EAASC,EAAKE,CAAQ,CACxE,CACF,CAOA,SAASiB,EACPd,EACAe,EACArB,EACAe,EAAoB,CAEpB,KAAM,CAAE,GAAAO,EAAI,KAAAC,CAAI,EAAKF,EAErB,GAAIE,IAAS,KAAM,OAEnB,MAAMC,EAAQC,EAAOF,CAAI,EAEzB,GAAID,EAAG,OAASb,EAAe,cAAe,CACxCJ,EAAYC,EAAQkB,EAAOxB,EAAQ,SAAS,GAAGa,EAASP,EAAQgB,EAAItB,EAASe,CAAI,EAErF,MACF,CAEA,MAAMZ,EAAWmB,EAAG,OAASb,EAAe,WAAaC,EAAWJ,EAAQgB,CAAE,EAAI,KAC5ErB,EAAMc,EAAK,KAAKI,GAAaP,EAAYN,EAAQkB,EAAOxB,EAASmB,CAAS,CAAC,EAE7EhB,IAAa,MAAQF,IAAQ,QAAWC,EAAIF,EAASC,EAAKE,CAAQ,CACxE,CAGA,SAASuB,EAAkBpB,EAAoBqB,EAAwC,CACrF,OAAOA,GAAU,OAASlB,EAAe,WAAcC,EAAWJ,EAAQqB,CAAQ,GAAK,OAAa,MACtG,CAMA,SAASC,EACPtB,EACAuB,EACAC,EACAf,EACAgB,EAAmD,CAEnD,MAAMJ,EAAWE,EAAG,OAAOC,CAAQ,EAC7B9B,EAA+B,CAAE,MAAO,IAAI,IAAO,UAAW0B,EAAkBpB,EAAQqB,CAAQ,CAAC,EAEnGA,GAAU,OAASlB,EAAe,eAAeI,EAASP,EAAQqB,EAAU3B,EAASe,CAAI,EAE7F,UAAWM,KAAcU,EACnBC,EAAOX,EAAYQ,CAAE,GAAGT,EAAmBd,EAAQe,EAAYrB,EAASe,CAAI,EAGlF,OAAOf,CACT"}
|
|
@@ -1,42 +1,74 @@
|
|
|
1
|
-
import type { TSESTree } from '@typescript-eslint/
|
|
2
|
-
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/types';
|
|
3
2
|
import type { FunctionNode } from './model-bindings.js';
|
|
3
|
+
import type { SourceCode } from './rule.js';
|
|
4
4
|
type NamedMethod = {
|
|
5
5
|
readonly fn: FunctionNode;
|
|
6
6
|
readonly method: string;
|
|
7
7
|
};
|
|
8
|
+
/** `own` itself: the plain name a context parameter was given, whichever function that parameter belongs to. */
|
|
9
|
+
declare function contextName(source: SourceCode, node: TSESTree.Identifier): FunctionNode | undefined;
|
|
8
10
|
/**
|
|
9
|
-
* The method this callee names on the
|
|
11
|
+
* The method this callee names on the context parameter of some function, and that function. Whether the parameter
|
|
10
12
|
* really is a declaring context is a separate question: answering it here would make the two questions circular.
|
|
11
13
|
*/
|
|
12
|
-
declare function methodPath(source:
|
|
14
|
+
declare function methodPath(source: SourceCode, callee: TSESTree.Node): NamedMethod | undefined;
|
|
13
15
|
/**
|
|
14
16
|
* The two functions that hand an author a declaring context: the `own` section written right where `defineFeature`
|
|
15
17
|
* takes it, and the factory of a model — which is either annotated as one or written where a feature declares it,
|
|
16
|
-
* as the
|
|
18
|
+
* as the factory argument of `model(Declaration, factory)` or `model(Declaration, deps, factory)`. That form is
|
|
19
|
+
* proved by walking outward one
|
|
17
20
|
* `model(…)` at a time, and the set of functions already visited ends a chain that would come back to its start.
|
|
18
21
|
*/
|
|
19
|
-
declare function isContextFunction(source:
|
|
22
|
+
declare function isContextFunction(source: SourceCode, fn: FunctionNode): boolean;
|
|
20
23
|
/**
|
|
21
24
|
* D330: the context of a model and nothing else. The `own` section of a feature is a declaring context too, but its
|
|
22
25
|
* `command` takes the body second and hands it no writer (D139, D385), so a rule about the writer of a command asks
|
|
23
|
-
* this.
|
|
26
|
+
* this. D514: the factory of `openModel`, and of a `model(…)` a slot contribution declares, are models' contexts.
|
|
24
27
|
*/
|
|
25
|
-
declare function isModelContextFunction(source:
|
|
28
|
+
declare function isModelContextFunction(source: SourceCode, fn: FunctionNode): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* D514: the shape of a model factory without the proof of it — the function at the factory position of a call named
|
|
31
|
+
* `model` or `openModel` whose origin this file does not prove. A rule may name a retired word there, never rewrite it.
|
|
32
|
+
*/
|
|
33
|
+
declare function isModelFactoryShape(fn: FunctionNode): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* D514: the callbacks of a model context whose context is their first parameter and carried `acquire(view)` until
|
|
36
|
+
* `hold` replaced it — a command body and the subscription of an event. An effect run and an event run take their
|
|
37
|
+
* context second, behind the value, and are not followed: their `acquire` is the compiler's to refuse.
|
|
38
|
+
*/
|
|
39
|
+
declare function isExecutionCallback(source: SourceCode, fn: FunctionNode): boolean;
|
|
26
40
|
/**
|
|
27
41
|
* Whether this node stands lexically inside a callback whose opened work the declared node releases. The walk goes
|
|
28
42
|
* all the way out, so a nested function body counts; only the resolved context and the position decide.
|
|
29
43
|
*/
|
|
30
|
-
declare function isDeclaredIngress(source:
|
|
44
|
+
declare function isDeclaredIngress(source: SourceCode, node: TSESTree.Node): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* D470: a subscription whose release the builder's child context owns — handed to its `cleanup` where it is written,
|
|
47
|
+
* or held by a name that `cleanup` receives or that the function handed to `cleanup` reads, whichever was registered
|
|
48
|
+
* first. The `cleanup` of the parent or of an outer builder's child is not it: that owner outlives this child.
|
|
49
|
+
*/
|
|
50
|
+
declare function isReleasedByCleanup(source: SourceCode, node: TSESTree.CallExpression, builder: FunctionNode): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The builder whose own body a subscription is written in, and the method it builds for: the nearest function around
|
|
53
|
+
* it, where that function is a model scope or view builder. The callback of `parts.forEach(…)` and its kin runs in
|
|
54
|
+
* the builder's own run, so it is looked through; a subscription in any other nested callback belongs to the node
|
|
55
|
+
* that runs that callback.
|
|
56
|
+
*/
|
|
57
|
+
declare function enclosingBuilder(source: SourceCode, node: TSESTree.Node): {
|
|
58
|
+
readonly fn: FunctionNode;
|
|
59
|
+
readonly method: string;
|
|
60
|
+
} | undefined;
|
|
61
|
+
/** A call, not a reference: `useSyncExternalStore(source.subscribe, …)` hands the function over without calling it. */
|
|
62
|
+
declare function isSubscribeCall(node: TSESTree.CallExpression): boolean;
|
|
31
63
|
/**
|
|
32
64
|
* `context.command(run, options?)`: the body a model command is declared with, inline or bound to a local name.
|
|
33
65
|
* D447: two rules read the writes of one command body — `capture-command-cleanup` where the declaration stands, and
|
|
34
66
|
* `no-write-after-source-write` where a run invokes the name that holds it — so the resolution is one function here
|
|
35
67
|
* rather than the same six lines twice.
|
|
36
68
|
*/
|
|
37
|
-
declare function commandBody(source:
|
|
69
|
+
declare function commandBody(source: SourceCode, call: TSESTree.CallExpression): FunctionNode | undefined;
|
|
38
70
|
/**
|
|
39
71
|
* D323: a rule that rewrites code has to resolve its package exactly (D309), and this pair is that resolution: the
|
|
40
72
|
* method a callee names on a declaring context, and whether that parameter really is one.
|
|
41
73
|
*/
|
|
42
|
-
export { commandBody, isContextFunction, isDeclaredIngress, isModelContextFunction, methodPath };
|
|
74
|
+
export { commandBody, contextName, enclosingBuilder, isContextFunction, isDeclaredIngress, isExecutionCallback, isModelContextFunction, isModelFactoryShape, isReleasedByCleanup, isSubscribeCall, methodPath, };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{DefinitionType as B}from"@typescript-eslint/scope-manager";import{AST_NODE_TYPES as o}from"@typescript-eslint/types";import{propertyName as h}from"./ast.js";import{localValue as D,unwrap as g,isFunction as s,isApi as x,isFeatureSection as N,isSlot as T,definitionOf as j,variableOf as C,enclosingFunction as b}from"./model-bindings.js";const k=new Map([["event",2]]),R=new Map([["resource.view",1],["scope.each",2],["scope.switch",1],["scope.while",1]]),V=new Map([["attach",{argument:1,property:"open"}],["resource.live",{argument:0,property:"connect"}]]);function _(n,e){if(e.type!==o.Identifier&&e.type!==o.ObjectPattern)return!1;const t=e.typeAnnotation?.typeAnnotation;if(t?.type!==o.TSTypeReference)return!1;const{typeName:r}=t;return r.type===o.Identifier&&x(n,r,"@opetope/core","ModelContext",!0)}function E(n,e){return e.params.find(t=>_(n,t))}function w(n,e){return E(n,e)!==void 0}function M(n,e){const t=j(n,e);if(!(t?.type!==B.Parameter||!s(t.node)))return{fn:t.node,name:t.name}}function L(n){if(n.type===o.FunctionDeclaration)return n.id??void 0;const{parent:e}=n;return e.type===o.VariableDeclarator&&e.init===n&&e.id.type===o.Identifier?e.id:void 0}const f=new Set;function K(n){let e=n;for(;e.parent!==void 0&&g(e.parent)!==e.parent;)e=e.parent;return e}function S(n,e){const t=K(e),r=t.parent;if(r?.type!==o.CallExpression)return;const i=r.arguments.indexOf(t);if(i===-1)return;const u=a(n,r.callee);return u===void 0?U(n,r,i):R.get(u.method)===i&&c(n,u.fn)?{method:u.method,wrapped:!1}:void 0}function U(n,e,t){const r=t===0?S(n,e):void 0;return r===void 0?void 0:{method:r.method,wrapped:!0}}function I(n,e,t){const r=S(n,t);return r!==void 0&&(!r.wrapped||e.params.length>=2)?r.method:void 0}function v(n,e){const t=I(n,e,e);if(t!==void 0||f.has(e))return t;const r=L(e),i=r===void 0?null:C(n,r);if(i!==null){f.add(e);try{for(const u of i.references){const d=I(n,e,u.identifier);if(d!==void 0)return d}return}finally{f.delete(e)}}}function l(n,e){return v(n,e)!==void 0}function A(n,e){return l(n,e)?e.params[1]:E(n,e)??e.params[0]}function O(n,e){const t=M(n,e);return t!==void 0&&t.name===A(n,t.fn)?t.fn:void 0}function X(n,e){const t=M(n,e),r=t?.name.parent;if(t===void 0||r?.type!==o.Property)return;const i=r.parent===A(n,t.fn)?h(r):void 0;return i===void 0?void 0:{fn:t.fn,method:i}}function Y(n){const e=[];let t=n;for(;t.type===o.MemberExpression;){if(t.computed||t.property.type!==o.Identifier)return;e.unshift(t.property.name),t=t.object}return{root:t,steps:e}}function a(n,e){const t=Y(e);if(t?.root.type!==o.Identifier)return;const r=t.steps.length===0?void 0:O(n,t.root);if(r!==void 0)return{fn:r,method:t.steps.join(".")};const i=X(n,t.root);return i===void 0?void 0:{fn:i.fn,method:[i.method,...t.steps].join(".")}}function F(n,e){const t=e.parent;if(t.type!==o.CallExpression||t.arguments[1]!==e&&t.arguments[2]!==e)return;const r=a(n,t.callee);return r?.method==="model"?r.fn:void 0}function p(n,e){const t=new Set;let r=e;for(;r!==void 0&&!t.has(r);){if(t.add(r),N(n,r,"own",!0)||w(n,r)||l(n,r))return!0;r=F(n,r)}return!1}function P(n,e){return n.arguments.length>=2&&n.arguments.at(-1)===e&&n.arguments.length<=3}function q(n,e){const t=e.parent;return t.type===o.CallExpression&&t.arguments[1]===e&&T(n,t.callee)}function z(n,e){const t=e.parent;return t.type===o.CallExpression&&P(t,e)&&x(n,t.callee,"@opetope/runtime/testing","openModel",!0)}function c(n,e){if(w(n,e)||l(n,e)||z(n,e))return!0;const t=F(n,e);return t!==void 0&&(p(n,t)||q(n,t))}function G(n){const e=n.parent;if(e.type!==o.CallExpression||!P(e,n))return!1;const t=e.callee.type===o.MemberExpression?e.callee.property:e.callee;return t.type===o.Identifier&&(t.name==="model"||t.name==="openModel")}const H=new Map([["command",0],["event",2]]);function J(n,e){const t=e.parent;if(t.type!==o.CallExpression)return!1;const r=a(n,t.callee);if(r===void 0)return!1;const i=H.get(r.method);return i!==void 0&&t.arguments[i]===e&&c(n,r.fn)}function Q(n,e,t){if(k.get(n)===e)return!0;const r=V.get(n);return r?.argument===e&&r.property===t}function W(n,e,t,r){const i=a(n,e.callee);return i===void 0||!Q(i.method,t,r)?!1:p(n,i.fn)}function Z(n,e,t){return n.type!==o.ObjectExpression||e.type!==o.Property?t:h(e)}function $(n,e){let t=e,r;for(const i of n.getAncestors(e).reverse()){if(i.type===o.CallExpression){const u=i.arguments.indexOf(t);if(u!==-1&&W(n,i,u,r))return!0;r=void 0}else r=Z(i,t,r);t=i}return!1}function m(n,e,t){if(e.type!==o.CallExpression)return!1;const r=a(n,e.callee);return r?.method==="cleanup"&&r.fn===t}function ee(n,e){if(n.type===o.VariableDeclarator&&n.init===e)return n.id;if(n.type===o.AssignmentExpression&&n.operator==="="&&n.right===e)return n.left}function ne(n,e,t){return n.getAncestors(e).some(r=>{if(!s(r))return!1;const i=r.parent;return m(n,i,t)&&i.arguments[0]===r})}function te(n,e,t){const{parent:r}=e;if(m(n,r,t)&&r.arguments[0]===e)return!0;const i=ee(r,e);return(i?.type===o.Identifier?C(n,i):null)?.references.some(d=>{const y=d.identifier.parent;return m(n,y,t)&&y.arguments[0]===d.identifier?!0:ne(n,d.identifier,t)})===!0}const re=new Set(["every","filter","find","findIndex","flatMap","forEach","map","some"]);function ie(n){const e=n.parent;if(e.type!==o.CallExpression||e.arguments[0]!==n)return!1;const{callee:t}=e;return t.type===o.MemberExpression&&!t.computed&&t.property.type===o.Identifier&&re.has(t.property.name)}function oe(n,e){let t=b(n,e);for(;t!==void 0&&ie(t);)t=b(n,t);if(t===void 0)return;const r=v(n,t);return r===void 0?void 0:{fn:t,method:r}}function ue(n){const{callee:e}=n;return e.type!==o.MemberExpression||e.computed||n.arguments.length===0?!1:e.property.type===o.Identifier&&e.property.name==="subscribe"}function de(n,e){const[t]=e.arguments;if(t===void 0||t.type===o.SpreadElement)return;const r=D(n,g(t));return s(r)?r:void 0}export{de as commandBody,O as contextName,oe as enclosingBuilder,p as isContextFunction,$ as isDeclaredIngress,J as isExecutionCallback,c as isModelContextFunction,G as isModelFactoryShape,te as isReleasedByCleanup,ue as isSubscribeCall,a as methodPath};
|
|
2
2
|
//# sourceMappingURL=declaration-ingress.js.map
|