@opetope/lint 0.14.0 → 0.16.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 +76 -0
- package/README.md +75 -87
- 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 +8 -7
- 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,81 @@
|
|
|
1
1
|
# @opetope/lint
|
|
2
2
|
|
|
3
|
+
## 0.16.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f3e4e4a: Make npm READMEs self-contained, pin installation examples to the documented version and link to the same release's
|
|
8
|
+
public documentation. Clarify timer cancellation and physical cleanup, switch-navigation persistence and screen
|
|
9
|
+
matching, and correct timer examples so cancellation can finish their owned bodies.
|
|
10
|
+
- f66744b: `CommandHook.dismiss()` clears the outcome a consumer shows until its next run settles; it belongs to that outcome, so a timer of an old outcome never clears a new one, and a `CommandHook` built by hand in a test now needs it (D543). Kit `failureOf(status)` reads a domain refusal returned as a `Result` and a thrown `failed` outcome for one place in the UI, `null` while a run is in flight (D543).
|
|
11
|
+
`opetope/no-command-in-deps` offers `hook.dismiss` as the dependency of a callback that reads it (D543).
|
|
12
|
+
- fedf20a: `opetope/define-feature-property-order` no longer points its message and description at the removed specification: the
|
|
13
|
+
message cites the decisions behind the order (D542, D280, D477), and the description says that the sections of
|
|
14
|
+
`defineFeature` keep one declaration order.
|
|
15
|
+
- c88bc3d: A lazy header loads its body file, `body: () => import('./feature.body')`, whose default export is `defineFeature.body(header, { own, exports, provides })`, and reads its exports from that export; a `FeatureBody` carries the id of the header it implements. The header is a `LazyFeature`, whose fourth explicit type argument is now the loaded value rather than the exports; `defineFeature.body` refuses a feature without a body loader, and `FeatureBodyOf` is removed — `opetope/no-retired-vocabulary` names the replacement (D542).
|
|
16
|
+
|
|
17
|
+
## 0.15.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 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).
|
|
22
|
+
- 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).
|
|
23
|
+
|
|
24
|
+
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`.
|
|
25
|
+
|
|
26
|
+
- 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).
|
|
27
|
+
|
|
28
|
+
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).
|
|
29
|
+
|
|
30
|
+
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).
|
|
31
|
+
|
|
32
|
+
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).
|
|
33
|
+
|
|
34
|
+
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).
|
|
35
|
+
|
|
36
|
+
- 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).
|
|
37
|
+
|
|
38
|
+
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).
|
|
39
|
+
|
|
40
|
+
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.
|
|
41
|
+
|
|
42
|
+
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.
|
|
43
|
+
|
|
44
|
+
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).
|
|
45
|
+
|
|
46
|
+
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).
|
|
47
|
+
|
|
48
|
+
- 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).
|
|
49
|
+
|
|
50
|
+
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).
|
|
51
|
+
|
|
52
|
+
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).
|
|
53
|
+
|
|
54
|
+
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).
|
|
55
|
+
|
|
56
|
+
- 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).
|
|
57
|
+
|
|
58
|
+
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).
|
|
59
|
+
|
|
60
|
+
- 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).
|
|
61
|
+
|
|
62
|
+
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.
|
|
63
|
+
|
|
64
|
+
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.
|
|
65
|
+
|
|
66
|
+
### Patch Changes
|
|
67
|
+
|
|
68
|
+
- 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).
|
|
69
|
+
- 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`.
|
|
70
|
+
|
|
71
|
+
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).
|
|
72
|
+
|
|
73
|
+
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).
|
|
74
|
+
|
|
75
|
+
- 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).
|
|
76
|
+
- 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).
|
|
77
|
+
- 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.
|
|
78
|
+
|
|
3
79
|
## 0.14.0
|
|
4
80
|
|
|
5
81
|
No changes in this release.
|
package/README.md
CHANGED
|
@@ -1,123 +1,111 @@
|
|
|
1
1
|
# `@opetope/lint`
|
|
2
2
|
|
|
3
|
-
ESLint rules for
|
|
3
|
+
ESLint and Oxlint rules for Opetope authoring: model writes, feature declarations, hook dependencies and package
|
|
4
|
+
boundaries. Use the recommended rules first, then add layer rules when your project names its directories.
|
|
5
|
+
The plugin complements TypeScript and runtime validation and needs no Opetope runtime packages.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
9
|
+
For an ESLint flat configuration:
|
|
10
|
+
|
|
7
11
|
```sh
|
|
8
|
-
npm install --save-exact --save-dev @opetope/lint
|
|
12
|
+
npm install --save-exact --save-dev @opetope/lint@0.16.0 'eslint@^9' '@typescript-eslint/parser@^8'
|
|
9
13
|
```
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
Packages are ESM-only and support Node 20.19+.
|
|
15
|
+
The API is experimental. Keep all Opetope packages at the same exact version. The commands pin the API shown
|
|
16
|
+
here; for prereleases, select `@next` on every Opetope package. Packages are ESM-only and support Node 20.19+.
|
|
17
|
+
ESLint 9 and `@typescript-eslint/parser` 8 are optional peers: an ESLint host installs both explicitly.
|
|
13
18
|
|
|
14
19
|
## Example
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
feature, host, React entry and cleanup in one visible module.
|
|
21
|
+
Put this in `eslint.config.mjs`, then run `npx eslint src`:
|
|
18
22
|
|
|
19
23
|
```js
|
|
20
|
-
// eslint.config.mjs
|
|
21
24
|
import opetope from '@opetope/lint';
|
|
25
|
+
import parser from '@typescript-eslint/parser';
|
|
22
26
|
|
|
23
27
|
export default [
|
|
24
28
|
{
|
|
25
29
|
files: ['src/**/*.{ts,tsx}'],
|
|
30
|
+
languageOptions: { parser },
|
|
26
31
|
...opetope.configs.recommended,
|
|
27
32
|
},
|
|
28
33
|
];
|
|
29
34
|
```
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
`recommended` supplies the `opetope` plugin and its authoring rules. `configs.layers({ models, ui, integration })`
|
|
37
|
+
adds rules using the globs you supply; `configs.internalImports({ allow })` names any trusted host integration
|
|
38
|
+
allowed to use the implementation ABI. The [lint reference](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md) lists every rule,
|
|
39
|
+
its default severity, layer configuration and the Oxlint setup.
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
[Guides](../runtime/docs/guides/index.md) show individual tasks. Documentation is shipped with
|
|
35
|
-
`@opetope/runtime` in `docs/`, so an installed application can read it without access to this repository.
|
|
41
|
+
## Oxlint
|
|
36
42
|
|
|
37
|
-
For
|
|
38
|
-
`npm run ci:test --workspace @opetope/lint` where provided. The root `npm run check` performs full acceptance;
|
|
39
|
-
[contributor commands](../runtime/docs/maintainers/contributing.md) describe the build and package checks.
|
|
43
|
+
For an Oxlint host, use the plugin without the ESLint peers:
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
```sh
|
|
46
|
+
npm install --save-exact --save-dev @opetope/lint@0.16.0 'oxlint@^1.80.0'
|
|
47
|
+
```
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
Put this in `.oxlintrc.json`, then run `npx oxlint src`. Both paths are relative to that config file. The shipped
|
|
50
|
+
fragment supplies severities; `jsPlugins` registers the plugin. Under Oxlint the plugin loads no ESLint, parser or
|
|
51
|
+
TypeScript.
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"extends": ["./node_modules/@opetope/lint/oxlintrc.json"],
|
|
56
|
+
"jsPlugins": [{ "name": "opetope", "specifier": "./node_modules/@opetope/lint/dist/index.js" }]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
[See `recommended`](../runtime/docs/reference/lint.md#recommended).
|
|
60
|
+
## Documentation
|
|
52
61
|
|
|
53
|
-
|
|
54
|
-
[
|
|
62
|
+
- [Start: build, mount and close one feature](https://unpkg.com/@opetope/runtime@0.16.0/docs/start.md).
|
|
63
|
+
- [Guides: recipes for individual tasks](https://unpkg.com/@opetope/runtime@0.16.0/docs/guides/index.md).
|
|
64
|
+
- [API reference](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md): signatures, options and lifecycle guarantees.
|
|
65
|
+
|
|
66
|
+
The links open the Markdown files from this release. The full documentation also ships in `@opetope/runtime/docs`;
|
|
67
|
+
with Runtime installed, start at `node_modules/@opetope/runtime/docs/start.md`.
|
|
68
|
+
|
|
69
|
+
## Rule reference
|
|
70
|
+
|
|
71
|
+
Each rule's help link points to its canonical contract and examples.
|
|
72
|
+
|
|
73
|
+
- <a id="capture-command-cleanup"></a>[`capture-command-cleanup`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#capture-command-cleanup)
|
|
74
|
+
- <a id="define-feature-property-order"></a>[`define-feature-property-order`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#define-feature-property-order)
|
|
75
|
+
- <a id="enabled-predicate"></a>[`enabled-predicate`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#enabled-predicate)
|
|
76
|
+
- <a id="id-naming"></a>[`id-naming`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#id-naming)
|
|
77
|
+
- <a id="layer-placement"></a>[`layer-placement`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#layer-placement)
|
|
78
|
+
- <a id="no-behavior-in-integration"></a>[`no-behavior-in-integration`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#no-behavior-in-integration)
|
|
79
|
+
- <a id="no-command-in-deps"></a>[`no-command-in-deps`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#no-command-in-deps)
|
|
80
|
+
- <a id="no-dead-abort-check"></a>[`no-dead-abort-check`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#no-dead-abort-check)
|
|
81
|
+
- <a id="no-event-without-emit"></a>[`no-event-without-emit`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#no-event-without-emit)
|
|
82
|
+
- <a id="no-inline-requires-models"></a>[`no-inline-requires-models`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#no-inline-requires-models)
|
|
83
|
+
- <a id="no-internal-imports"></a>[`no-internal-imports`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#no-internal-imports)
|
|
84
|
+
- <a id="no-redundant-const-tuple"></a>[`no-redundant-const-tuple`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#no-redundant-const-tuple)
|
|
85
|
+
- <a id="no-retired-vocabulary"></a>[`no-retired-vocabulary`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#no-retired-vocabulary)
|
|
86
|
+
- <a id="no-single-command-lane"></a>[`no-single-command-lane`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#no-single-command-lane)
|
|
87
|
+
- <a id="no-snapshot-in-update"></a>[`no-snapshot-in-update`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#no-snapshot-in-update)
|
|
88
|
+
- <a id="no-snapshot-read-in-render"></a>[`no-snapshot-read-in-render`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#no-snapshot-read-in-render)
|
|
89
|
+
- <a id="no-subscribe-outside-models"></a>[`no-subscribe-outside-models`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#no-subscribe-outside-models)
|
|
90
|
+
- <a id="no-write-after-source-write"></a>[`no-write-after-source-write`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#no-write-after-source-write)
|
|
91
|
+
- <a id="prefer-derive"></a>[`prefer-derive`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#prefer-derive)
|
|
92
|
+
- <a id="prefer-effect-current"></a>[`prefer-effect-current`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#prefer-effect-current)
|
|
93
|
+
- <a id="prefer-lazy-body-record"></a>[`prefer-lazy-body-record`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#prefer-lazy-body-record)
|
|
94
|
+
- <a id="prefer-model-selection"></a>[`prefer-model-selection`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#prefer-model-selection)
|
|
95
|
+
- <a id="prefer-read"></a>[`prefer-read`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#prefer-read)
|
|
96
|
+
- <a id="prefer-when-over-enabled"></a>[`prefer-when-over-enabled`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#prefer-when-over-enabled)
|
|
97
|
+
- <a id="require-declared-models"></a>[`require-declared-models`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#require-declared-models)
|
|
98
|
+
- <a id="require-literal-id"></a>[`require-literal-id`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#require-literal-id)
|
|
99
|
+
- <a id="require-static-feature"></a>[`require-static-feature`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#require-static-feature)
|
|
100
|
+
- <a id="scope-builder-releases-subscriptions"></a>[`scope-builder-releases-subscriptions`](https://unpkg.com/@opetope/runtime@0.16.0/docs/reference/lint.md#scope-builder-releases-subscriptions)
|
|
55
101
|
|
|
102
|
+
<a id="opetopelint"></a>
|
|
103
|
+
<a id="usage"></a>
|
|
104
|
+
<a id="configs"></a>
|
|
105
|
+
<a id="recommended"></a>
|
|
106
|
+
<a id="layers"></a>
|
|
56
107
|
<a id="internalimports"></a>
|
|
57
|
-
[See `internalImports`](../runtime/docs/reference/lint.md#internalimports).
|
|
58
|
-
|
|
59
108
|
<a id="rules"></a>
|
|
60
|
-
[See Rules](../runtime/docs/reference/lint.md#rules).
|
|
61
|
-
|
|
62
|
-
<a id="no-internal-imports"></a>
|
|
63
|
-
[See `no-internal-imports`](../runtime/docs/reference/lint.md#no-internal-imports).
|
|
64
|
-
|
|
65
|
-
<a id="enabled-predicate"></a>
|
|
66
|
-
[See `enabled-predicate`](../runtime/docs/reference/lint.md#enabled-predicate).
|
|
67
|
-
|
|
68
|
-
<a id="define-feature-property-order"></a>
|
|
69
|
-
[See `define-feature-property-order`](../runtime/docs/reference/lint.md#define-feature-property-order).
|
|
70
|
-
|
|
71
|
-
<a id="no-redundant-const-tuple"></a>
|
|
72
|
-
[See `no-redundant-const-tuple`](../runtime/docs/reference/lint.md#no-redundant-const-tuple).
|
|
73
|
-
|
|
74
|
-
<a id="require-declared-models"></a>
|
|
75
|
-
[See `require-declared-models`](../runtime/docs/reference/lint.md#require-declared-models).
|
|
76
|
-
|
|
77
|
-
<a id="require-literal-id"></a>
|
|
78
|
-
[See `require-literal-id`](../runtime/docs/reference/lint.md#require-literal-id).
|
|
79
|
-
|
|
80
|
-
<a id="id-naming"></a>
|
|
81
|
-
[See `id-naming`](../runtime/docs/reference/lint.md#id-naming).
|
|
82
|
-
|
|
83
|
-
<a id="layer-placement"></a>
|
|
84
|
-
[See `layer-placement`](../runtime/docs/reference/lint.md#layer-placement).
|
|
85
|
-
|
|
86
|
-
<a id="no-snapshot-read-in-render"></a>
|
|
87
|
-
[See `no-snapshot-read-in-render`](../runtime/docs/reference/lint.md#no-snapshot-read-in-render).
|
|
88
|
-
|
|
89
|
-
<a id="no-snapshot-in-update"></a>
|
|
90
|
-
[See `no-snapshot-in-update`](../runtime/docs/reference/lint.md#no-snapshot-in-update).
|
|
91
|
-
|
|
92
|
-
<a id="no-command-in-deps"></a>
|
|
93
|
-
[See `no-command-in-deps`](../runtime/docs/reference/lint.md#no-command-in-deps).
|
|
94
|
-
|
|
95
|
-
<a id="no-subscribe-outside-models"></a>
|
|
96
|
-
[See `no-subscribe-outside-models`](../runtime/docs/reference/lint.md#no-subscribe-outside-models).
|
|
97
|
-
|
|
98
|
-
<a id="prefer-effect-current"></a>
|
|
99
|
-
[See `prefer-effect-current`](../runtime/docs/reference/lint.md#prefer-effect-current).
|
|
100
|
-
|
|
101
|
-
<a id="no-write-after-source-write"></a>
|
|
102
|
-
[See `no-write-after-source-write`](../runtime/docs/reference/lint.md#no-write-after-source-write).
|
|
103
|
-
|
|
104
|
-
<a id="capture-command-cleanup"></a>
|
|
105
|
-
[See `capture-command-cleanup`](../runtime/docs/reference/lint.md#capture-command-cleanup).
|
|
106
|
-
|
|
107
|
-
<a id="prefer-lazy-body-record"></a>
|
|
108
|
-
[See `prefer-lazy-body-record`](../runtime/docs/reference/lint.md#prefer-lazy-body-record).
|
|
109
|
-
|
|
110
|
-
<a id="prefer-model-selection"></a>
|
|
111
|
-
[See `prefer-model-selection`](../runtime/docs/reference/lint.md#prefer-model-selection).
|
|
112
|
-
|
|
113
|
-
<a id="no-retired-vocabulary"></a>
|
|
114
|
-
[See `no-retired-vocabulary`](../runtime/docs/reference/lint.md#no-retired-vocabulary).
|
|
115
|
-
|
|
116
109
|
<a id="coexistence-with-key-sorting"></a>
|
|
117
|
-
[See Coexistence with key sorting](../runtime/docs/reference/lint.md#coexistence-with-key-sorting).
|
|
118
|
-
|
|
119
110
|
<a id="running-the-rules-under-oxlint"></a>
|
|
120
|
-
[See Running the rules under Oxlint](../runtime/docs/reference/lint.md#running-the-rules-under-oxlint).
|
|
121
|
-
|
|
122
111
|
<a id="checks"></a>
|
|
123
|
-
[See contributor checks](../runtime/docs/maintainers/contributing.md).
|
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(["dismiss","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 s(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?s(n,t,!1):t===void 0?y:i.key===void 0||t.key===i.key?s(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 a(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){a(n,e,i,t);return}for(const f of e.properties)f.type===o.Property&&a(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"?a(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\n/** D543: `dismiss` changes with the outcome, so it is a dependency the rule can offer, as a status is. */\nconst HOOK_MEMBERS = new Set(['dismiss', '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,22 +1,23 @@
|
|
|
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
|
-
* renamed in the parameter list, taken apart by a declaration inside the body, or given a second name by a
|
|
7
|
-
* A closure over a member, a member stored
|
|
8
|
-
* and every rule that reads this says so in
|
|
6
|
+
* renamed in the parameter list, taken apart by a declaration inside the body, or given a second name by a declaration
|
|
7
|
+
* that initializes it (`const`, `let` or `var`). A closure over a member, a member stored in a record or assigned
|
|
8
|
+
* after its declaration and a context handed to a helper are not followed, and every rule that reads this says so in
|
|
9
|
+
* its own boundary.
|
|
9
10
|
*/
|
|
10
11
|
type ContextMembers<Key extends string> = {
|
|
11
12
|
readonly bound: Map<Key, Set<Variable>>;
|
|
12
13
|
readonly parameter: Variable | undefined;
|
|
13
14
|
};
|
|
14
15
|
/** 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:
|
|
16
|
+
declare function fromContext<Key extends string>(source: SourceCode, node: TSESTree.Node, members: ContextMembers<Key>, key: Key): boolean;
|
|
16
17
|
/**
|
|
17
18
|
* The context of one callback, whether it arrived whole under a name or taken apart where the callback receives it.
|
|
18
19
|
* The position is part of the contract: a command takes its context second and a reaction run takes it first.
|
|
19
20
|
*/
|
|
20
|
-
declare function contextMembers<Key extends string>(source:
|
|
21
|
+
declare function contextMembers<Key extends string>(source: SourceCode, fn: FunctionNode, position: number, keys: readonly Key[], declarators: readonly TSESTree.VariableDeclarator[]): ContextMembers<Key>;
|
|
21
22
|
export type { ContextMembers };
|
|
22
23
|
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 declaration\n * that initializes it (`const`, `let` or `var`). A closure over a member, a member stored in a record or assigned\n * after its declaration and a context handed to a helper are not followed, and every rule that reads this says so in\n * 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":"2KAoBA,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"}
|