@opetope/lint 0.10.1 → 0.12.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +464 -4
  2. package/README.md +314 -63
  3. package/README.ru.md +253 -63
  4. package/dist/ast.d.ts +3 -1
  5. package/dist/ast.js +1 -1
  6. package/dist/ast.js.map +1 -1
  7. package/dist/command-hooks.d.ts +2 -2
  8. package/dist/command-hooks.js +1 -1
  9. package/dist/command-hooks.js.map +1 -1
  10. package/dist/context-members.d.ts +22 -0
  11. package/dist/context-members.js +2 -0
  12. package/dist/context-members.js.map +1 -0
  13. package/dist/declaration-ingress.d.ts +10 -2
  14. package/dist/declaration-ingress.js +1 -1
  15. package/dist/declaration-ingress.js.map +1 -1
  16. package/dist/effect-declarations.d.ts +20 -0
  17. package/dist/effect-declarations.js +2 -0
  18. package/dist/effect-declarations.js.map +1 -0
  19. package/dist/index.d.ts +11 -3
  20. package/dist/index.js +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/retired-vocabulary.d.ts +31 -0
  23. package/dist/retired-vocabulary.js +2 -0
  24. package/dist/retired-vocabulary.js.map +1 -0
  25. package/dist/rules/capture-command-cleanup.js +1 -1
  26. package/dist/rules/capture-command-cleanup.js.map +1 -1
  27. package/dist/rules/define-feature-property-order.js +1 -1
  28. package/dist/rules/define-feature-property-order.js.map +1 -1
  29. package/dist/rules/enabled-predicate.d.ts +6 -0
  30. package/dist/rules/enabled-predicate.js +2 -0
  31. package/dist/rules/enabled-predicate.js.map +1 -0
  32. package/dist/rules/no-command-in-deps.js +1 -1
  33. package/dist/rules/no-command-in-deps.js.map +1 -1
  34. package/dist/rules/no-internal-imports.js +1 -1
  35. package/dist/rules/no-internal-imports.js.map +1 -1
  36. package/dist/rules/no-retired-vocabulary.d.ts +5 -0
  37. package/dist/rules/no-retired-vocabulary.js +2 -0
  38. package/dist/rules/no-retired-vocabulary.js.map +1 -0
  39. package/dist/rules/no-write-after-source-write.d.ts +6 -0
  40. package/dist/rules/no-write-after-source-write.js +2 -0
  41. package/dist/rules/no-write-after-source-write.js.map +1 -0
  42. package/dist/rules/prefer-effect-current.js +1 -1
  43. package/dist/rules/prefer-effect-current.js.map +1 -1
  44. package/oxlintrc.json +3 -1
  45. package/package.json +1 -1
  46. package/dist/rules/when-predicate.d.ts +0 -6
  47. package/dist/rules/when-predicate.js +0 -2
  48. package/dist/rules/when-predicate.js.map +0 -1
package/README.ru.md CHANGED
@@ -35,18 +35,19 @@ export default [
35
35
  ```
36
36
 
37
37
  Плагин — это и default export, и именованный `opetopeLint`. Конфигурация регистрирует его в namespace `opetope`,
38
- поэтому правило называется `opetope/when-predicate`. Для разбора нужен `@typescript-eslint/parser`; он и `eslint`
38
+ поэтому правило называется `opetope/enabled-predicate`. Для разбора нужен `@typescript-eslint/parser`; он и `eslint`
39
39
  объявлены peer dependencies.
40
40
 
41
41
  ## Configs
42
42
 
43
43
  ### `recommended`
44
44
 
45
- Все правила, которые действуют везде, где пишут на Opetope: `when-predicate`, `define-feature-property-order`,
45
+ Все правила, которые действуют везде, где пишут на Opetope: `enabled-predicate`, `define-feature-property-order`,
46
46
  `require-literal-id`, `id-naming`, `no-internal-imports`, `no-snapshot-read-in-render`, `no-snapshot-in-update`,
47
- `no-redundant-const-tuple`, `no-command-in-deps`, `prefer-effect-current`, `capture-command-cleanup` и `require-declared-models` как error. Правило, которому нужно знать, где хост держит свои файлы, здесь выключено и
48
- приходит через `layers`; `prefer-model-selection` выключено потому, что число hooks, которое компонент вправе
49
- держать, каждый проект решает для себя.
47
+ `no-redundant-const-tuple`, `no-command-in-deps`, `prefer-effect-current`, `no-write-after-source-write`,
48
+ `capture-command-cleanup`, `require-declared-models` и `no-retired-vocabulary` как error. Правило, которому нужно знать, где хост держит
49
+ свои файлы, здесь выключено и приходит через `layers`; `prefer-model-selection` выключено потому, что число hooks,
50
+ которое компонент вправе держать, каждый проект решает для себя.
50
51
 
51
52
  `configs.recommended.rules` целиком — и ровно это поставляется как `@opetope/lint/oxlintrc.json`:
52
53
 
@@ -59,14 +60,16 @@ export default [
59
60
  | `no-command-in-deps` | `error` | |
60
61
  | `no-internal-imports` | `error` | плюс сужение через `internalImports({ allow })` |
61
62
  | `no-redundant-const-tuple` | `error` | |
63
+ | `no-retired-vocabulary` | `error` | |
62
64
  | `no-snapshot-in-update` | `error` | |
63
65
  | `no-snapshot-read-in-render` | `error` | |
64
66
  | `no-subscribe-outside-models` | `off` | `layers({ models })` |
67
+ | `no-write-after-source-write` | `error` | |
65
68
  | `prefer-effect-current` | `error` | |
66
69
  | `prefer-model-selection` | `off` | проект, со своим `threshold` |
67
70
  | `require-declared-models` | `error` | |
68
71
  | `require-literal-id` | `error` | |
69
- | `when-predicate` | `error` | |
72
+ | `enabled-predicate` | `error` | |
70
73
 
71
74
  Три `off` — это ровно те три, которым нужно знание, которое есть только у проекта: какие каталоги каким слоем
72
75
  являются и сколько hooks вправе держать один компонент.
@@ -116,7 +119,7 @@ export default [
116
119
  Код приложения и тесты импортируют публичные входы Opetope. Правило запрещает `@opetope/*/internal` и вложенные
117
120
  пути в imports, re-exports, литеральных `import()`, незатенённых `require()`, TypeScript import types и
118
121
  `import = require`. Type-only импорты тоже проверяются: внутренний тип связывает потребителя с приватным ABI.
119
- Для запуска настоящего Call модели есть `runCall` из `@opetope/core/testing`, для открытия модели без фичи — `openModel` из `@opetope/runtime/testing`, для создания фикстуры — `command` из `@opetope/react/testing`.
122
+ Для запуска настоящего Command модели есть `runCommand` из `@opetope/core/testing`, для открытия модели без фичи — `openModel` из `@opetope/runtime/testing`, для создания фикстуры — `command` из `@opetope/react/testing`.
120
123
  Автоматического исключения для тестов нет. Доверенные файлы реализации библиотеки или интеграции хоста можно
121
124
  разрешить явным override; autofix отсутствует, потому что публичная замена зависит от импортированной операции.
122
125
 
@@ -124,17 +127,17 @@ export default [
124
127
  вычисляемые пути, граф re-exports или пользовательские загрузчики. Локальный `require` не считается загрузчиком
125
128
  Node. Применяйте конфигурацию ко всем используемым JavaScript/TypeScript исходникам и тестам.
126
129
 
127
- ### `when-predicate`
130
+ ### `enabled-predicate`
128
131
 
129
- `when` вклада отвечает фактом видимости этого экземпляра, а не источником, который этот факт несёт (D220). Правило
130
- читает `when` у вкладов `slot`, `pipe` и `register`, а также любой `when`, чья функция деструктурирует контекст
131
- вычисления `{ exports, imports, own, read }`, и сообщает о трёх формах:
132
+ `enabled` вклада отвечает фактом видимости этого экземпляра, а не источником, который этот факт несёт (D220).
133
+ Правило читает `enabled` у вкладов `slot`, `pipe` и `register`, а также любой `enabled`, чья функция деструктурирует
134
+ контекст вычисления `{ exports, imports, own, read }`, и сообщает о трёх формах:
132
135
 
133
- | Написано | О чём сообщает |
134
- | ------------------------------------------ | ------------------------------------------------------------------------------------- |
135
- | `when: ({ imports }) => imports.x.allowed` | возвращает источник; фикс приводит к `({ imports, read }) => read(imports.x.allowed)` |
136
- | `when: async ({ read }) => read(x)` | предикат отвечает синхронно; без фикса |
137
- | `when: () => allowed` | `Readable<boolean>` передаётся в `when` как есть, без обёртки; без фикса |
136
+ | Написано | О чём сообщает |
137
+ | --------------------------------------------- | ------------------------------------------------------------------------------------- |
138
+ | `enabled: ({ imports }) => imports.x.allowed` | возвращает источник; фикс приводит к `({ imports, read }) => read(imports.x.allowed)` |
139
+ | `enabled: async ({ read }) => read(x)` | предикат отвечает синхронно; без фикса |
140
+ | `enabled: () => allowed` | `Readable<boolean>` передаётся в `enabled` как есть, без обёртки; без фикса |
138
141
 
139
142
  Фикс оборачивает возвращённое обращение к полю в `read(...)` и добавляет `read` в деструктуризацию контекста, если
140
143
  его там нет. Именованный контекст читается через себя: `context => context.imports.x.allowed` становится
@@ -143,8 +146,9 @@ Node. Применяйте конфигурацию ко всем использ
143
146
  **Граница.** Правило читает формы, а не типы. `({ read }) => read(counter)` над не-boolean источником остаётся
144
147
  ошибкой типов, как и `read` над тем, что не является `Readable`. Если обращение к полю контекста вычисления — это
145
148
  обычное значение, а не источник, предикат не может изменить свой ответ, и правило сообщает о нём как о написанном
146
- для источника; вынеси такое решение из `when` или заглуши строку. `when` вне вклада — позиционный
147
- `(current, previous)` у `effect` или значение источника у `scope.while` — правило не трогает.
149
+ для источника; вынеси такое решение из `enabled` или заглуши строку. `when` — другое поле, и правило не трогает его,
150
+ где бы он ни стоял, — значение источника у `scope.while`; а фильтр изменений у эффекта — вовсе не `when`: это
151
+ `filter`, и D379 дал ему это имя, чтобы одно слово не отвечало на два вопроса.
148
152
 
149
153
  ### `define-feature-property-order`
150
154
 
@@ -189,11 +193,15 @@ const summary = derive([total, label], (value, suffix) => `${value} ${suffix}`);
189
193
 
190
194
  ```tsx
191
195
  import { defineFeature } from '@opetope/runtime';
192
- import { requiresModels, useModel } from '@opetope/react';
196
+ import { requiresModels, useCommand, useModel } from '@opetope/react';
193
197
 
194
198
  const Form = () => {
195
- const actions = useModel(FormActions);
196
- return null;
199
+ const submit = useCommand(useModel(FormActions).submit);
200
+ return (
201
+ <button disabled={submit.inFlight} onClick={() => void submit.run()}>
202
+ Отправить
203
+ </button>
204
+ );
197
205
  };
198
206
  const DeclaredForm = requiresModels([FormActions])(Form);
199
207
 
@@ -303,8 +311,8 @@ Id начинается с буквы и соединяет сегменты и
303
311
  Значение, переданное в `update`, читает то состояние, которое собирается заменить, — и составное,
304
312
  `update(sessions, { items: sessions.getSnapshot().items, kind: 'loading' })`, и скалярное,
305
313
  `update(total, total.getSnapshot() + amount)`. Чтение выполняется там, где написан аргумент, — до записи и на
306
- состоянии, которое может быть уже закрыто, где `getSnapshot()` бросает `ReadableError`, — а вычисленное значение
307
- ложится на состояние, которое с тех пор могло уйти вперёд. У формы с апдейтером нет ни одной из этих бед: она
314
+ состоянии, которое могло уже остановиться, где `getSnapshot()` отвечает значением, которое больше никто не
315
+ сдвинет, — а вычисленное значение ложится на состояние, которое с тех пор могло уйти вперёд. У формы с апдейтером нет ни одной из этих бед: она
308
316
  читает в момент записи, а у записи, отброшенной вместе с отменённым контекстом, апдейтер не вызывается вовсе
309
317
  (D282, D288, D295).
310
318
 
@@ -361,12 +369,12 @@ useEffect(() => {
361
369
 
362
370
  Правило читает массив зависимостей `useEffect`, `useLayoutEffect`, `useInsertionEffect`, `useCallback`, `useMemo` и
363
371
  `useImperativeHandle` и сообщает об элементе, который целиком является привязкой хука команды: о переменной
364
- `useCommand`, о ключе или деструктурированном имени `useCommands` и о поле выбора `useModel`, на котором тот же код
365
- читает `run`. `x.run` и статусы `x.inFlight` и `x.outcome` — это члены объекта, а не сам объект, и они остаются.
372
+ `useCommand` и о ключе или деструктурированном имени выбора `useModel`, на котором тот же код читает `run`.
373
+ `x.run` и статусы `x.inFlight` и `x.outcome` — это члены объекта, а не сам объект, и они остаются.
366
374
 
367
- Сама запись — форма ещё хуже, и у неё собственное сообщение: `useCommands` строит новый объект на каждом рендере, и
368
- то же делает `useModel` с выбором, поэтому `[hooks]` и `[order]` меняются на каждом рендере, а не на каждом
369
- статусе. Ответ здесь — то поле, которое читает колбэк: `hooks.save.run`.
375
+ Сама запись — форма ещё хуже, и у неё собственное сообщение: `useModel` с выбором строит новый объект на каждом
376
+ рендере, поэтому `[hooks]` и `[order]` меняются на каждом рендере, а не на каждом статусе. Ответ здесь — то поле,
377
+ которое читает колбэк: `hooks.save.run`.
370
378
 
371
379
  Фикс пишется там, где ответ один: колбэк, который дотягивается до привязки единственным путём через `run`, получает
372
380
  на месте элемента этот путь — вместе со всем, через что элемент был написан: `as`, `satisfies` или `!` заменяются
@@ -376,8 +384,8 @@ useEffect(() => {
376
384
  видит, — написанный в другом месте, держащий сам объект, разбирающий его или читающий член вне этих четырёх —
377
385
  получает сообщение и ни одной suggestion.
378
386
 
379
- **Граница.** Имена разрешаются импортом в пределах одного модуля, а не написанием: `useCommand`, `useCommands` и
380
- `useModel` — это импорты `@opetope/react`, шесть хуков — собственные хуки React, а локальный алиас любого из них —
387
+ **Граница.** Имена разрешаются импортом в пределах одного модуля, а не написанием: `useCommand` и `useModel` —
388
+ это импорты `@opetope/react`, шесть хуков — собственные хуки React, а локальный алиас любого из них —
381
389
  `import { useEffect as effect }` — это тот же импорт. React — это модуль, названный ровно `react`; имя `@opetope/*`
382
390
  берётся ещё и из относительного импорта, потому что так пакет реэкспортирует собственный вход через barrel, и
383
391
  соседний `./scheduling` — React не больше, чем `@host/scheduling`. Хуки React читаются и через namespace, и через
@@ -401,14 +409,15 @@ namespace. Только `const` держит тот хук, которым ег
401
409
  Disposer, который возвращает такой колбэк, принадлежит этому узлу и сливается вместе с поколением, которое его
402
410
  открыло, — поэтому правило там молчит: это ровно та форма, которую предписывает его собственное сообщение (D299):
403
411
 
404
- | Колбэк | Где | Ингресс |
405
- | ---------------------------------------------- | --------------------------- | ------- |
406
- | `resource.live({ connect })` | член `connect` | да |
407
- | `event(from, subscribe, run)` | второй аргумент | да |
408
- | `scope.while` / `scope.switch` / `scope.keyed` | `open`, второй аргумент | да |
409
- | `attach(source, { open })` | член `open` | да |
410
- | `attach(source, { open, close })` | член `close` | нет |
411
- | `apply`, `load`, `run`, `when`, `key` | модификаторы тех же вызовов | нет |
412
+ | Колбэк | Где | Ингресс |
413
+ | --------------------------------------- | --------------------------- | ------- |
414
+ | `resource.live({ connect })` | член `connect` | да |
415
+ | `event(source, subscribe, run)` | второй аргумент | да |
416
+ | `scope.while` / `scope.switch` | `open`, второй аргумент | да |
417
+ | `scope.each(source, key, open)` | `open`, третий аргумент | да |
418
+ | `attach(source, { open })` | член `open` | да |
419
+ | `attach(source, { open, close })` | член `close` | нет |
420
+ | `apply`, `load`, `run`, `filter`, `key` | модификаторы тех же вызовов | нет |
412
421
 
413
422
  `resource.load` не открывает подписки и ингресса не имеет вовсе, поэтому `subscribe`, написанный в его `load`,
414
423
  сообщается как любая другая подписка руками (D349).
@@ -421,7 +430,7 @@ own: ({ imports, resource }) => ({
421
430
  lifetime: 'owner',
422
431
  source: imports.exchange,
423
432
  }),
424
- });
433
+ }),
425
434
  ```
426
435
 
427
436
  «Внутри» считается до самого низа: вложенная стрелка, тело `function`, `await` на самой подписке, disposer,
@@ -442,32 +451,137 @@ own: ({ imports, resource }) => ({
442
451
 
443
452
  ### `prefer-effect-current`
444
453
 
445
- `run` эффекта уже получил то значение, ради которого его запустили. Чтение того же источника через
446
- `source.getSnapshot()` внутри этого прогона читает его второй раз и ничего не говорит о том, какой это прогон,
447
- поэтому правило переписывает его в `execution.current` (D296, D323):
454
+ `run` эффекта уже получил то значение, ради которого его запустили, — с D379 первым параметром. Чтение того же
455
+ источника через `source.getSnapshot()` внутри этого прогона читает его второй раз и ничего не говорит о том, какой
456
+ это прогон, поэтому правило переписывает его в этот параметр (D296, D323, D379):
448
457
 
449
458
  ```js
450
459
  // было
451
- ctx.effect(quotes, execution => publish(quotes.getSnapshot()));
460
+ ctx.effect(quotes, current => publish(quotes.getSnapshot()));
452
461
  // стало
453
- ctx.effect(quotes, execution => publish(execution.current));
462
+ ctx.effect(quotes, current => publish(current));
454
463
  ```
455
464
 
456
- Фикс пишет то слово, которое у прогона уже есть: имя контекста для `execution => …` и локальное имя, под которым
457
- взят `current`, для `({ current }) => …`. Прогон, не назвавший ни того ни другого, писать нечем, и тогда остаётся
458
- только сообщение.
465
+ Фикс пишет то слово, которое у прогона уже есть: имя, под которым автор связал первый параметр, каким бы оно ни
466
+ было. Прогону, который разобрал само значение на части или не взял параметра вовсе, назвать значение целиком нечем,
467
+ и тогда остаётся только сообщение.
459
468
 
460
- **Два случая, о которых правило сообщает, но не переписывает.** После первого `await` в прогоне `current` — это
469
+ **Два случая, о которых правило сообщает, но не переписывает.** После первого `await` в прогоне параметр — это
461
470
  значение, с которым прогон начался, а снимок — значение текущего момента; смена источника обычно перезапускает
462
- прогон, поэтому обычно они совпадают, — а «обычно» не даёт права на правку. Под `when` они расходятся законно:
463
- фильтр допускает одни значения и пропускает другие, поэтому источник меняется без перезапуска прогона (D168, D296).
464
- Запись модификаторов, которую правило прочитать не может — собранную в другом месте, — оно считает несущей предикат.
471
+ прогон, поэтому обычно они совпадают, — а «обычно» не даёт права на правку. Под `filter` они расходятся законно:
472
+ предикат допускает одни значения и пропускает другие, поэтому источник меняется без перезапуска прогона
473
+ (D168, D296, D379). Запись модификаторов, которую правило прочитать не может — собранную в другом месте, — оно
474
+ считает несущей предикат.
465
475
 
466
476
  Чтение внутри вложенного колбэка прогона — в `timers.delay`, в обработчике подписки — остаётся как есть: этот код
467
477
  выполняется позже прогона, и ему нужно значение того момента. Как и всякое правило, которое правит код, это
468
478
  разрешает свой контекст точно: `effect`, о котором идёт речь, объявлен на параметре типа `ModelContext` либо на
469
479
  контексте `model(Declaration, factory)`, написанном внутри `defineFeature` из `@opetope/runtime` (D309).
470
480
 
481
+ ### `no-write-after-source-write`
482
+
483
+ Запись прогона эффекта в ячейку, которую читает его собственный источник, меняет значение, публикуемое этим
484
+ источником, а уведомление синхронно: прогон отменяется внутри самой записи, поэтому каждая следующая запись прогона
485
+ отбрасывается, а `invoke` после неё отклоняется. Первая из отброшенных записей доходит до репортера как `LostWrite`
486
+ — одна на прогон, и в ней сказано, что остальные ушли вместе с ней (D432); отклонённый `invoke` по-прежнему молчит,
487
+ и молчит любая другая потеря записи. Правило сообщает о записи, сдвигающей источник, когда за ней в том же прогоне
488
+ может следовать ещё одна запись (spec §2.11, D288, D365, D432, D447):
489
+
490
+ ```ts
491
+ // сообщается: депозит так и не записан, потому что освобождение ключа завершило прогон
492
+ const key = context.state<string | null>('idempotency-1');
493
+ const deposit = context.state<string | null>(null);
494
+ const source = derive([frames, key], (frame, held) => `${frame}:${String(held)}`);
495
+
496
+ context.effect(source, (_current, { update }) => {
497
+ update(key, null);
498
+ update(deposit, 'deposit-1');
499
+ });
500
+ ```
501
+
502
+ Команда, которую прогон зовёт через `invoke`, пишет под той же властью. `invoke` отдаёт команде отмену прогона,
503
+ поэтому один abort кончает обоих, и запись такой команды в ячейку, которую читает источник прогона, завершает прогон
504
+ ровно так же, как его собственная запись. Запись, до которой прогон после этого дошёл, отбрасывается и попадает в
505
+ `LostWrite` наравне с любой другой (D447), — но чаще прогон в этот момент _ждёт_ этот самый вызов, и тогда обещание,
506
+ которого он ждёт, отменяется, а тело не доходит ни до какой записи. Отброшенной записи там нет, рантайму сообщать не
507
+ о чем, поэтому эту форму находит только правило — и сообщает о вызове там, где он написан:
508
+
509
+ ```ts
510
+ // сообщается: ключ освобождает запись команды, а до депозита под вызовом прогон не доходит
511
+ const key = context.state<string | null>('idempotency-1');
512
+ const deposit = context.state<string | null>(null);
513
+ const source = derive([frames, key], (frame, held) => `${frame}:${String(held)}`);
514
+ const release = context.command(({ update }: ModelCommandContext<void>) => {
515
+ update(key, null);
516
+ });
517
+
518
+ context.effect(source, async (_current, { invoke, update }) => {
519
+ await invoke(release);
520
+ update(deposit, 'deposit-1');
521
+ });
522
+ ```
523
+
524
+ Виден ли дроп по данным — вопрос отдельный, и именно из-за него такой дефект находили в проде, а не в тесте: цикл
525
+ открывает прогон-наследник для нового значения, если его допускает `filter`, и наследник, повторяющий запись, потерю
526
+ скрывает — ячейка оказывается правильной через второй прогон. Потеря остаётся целиком там, где наследник эту запись
527
+ не повторяет, — а именно так и ведёт себя гард (`if (held === null) return`), — и там, где наследник не открывается
528
+ вовсе: так ведёт себя `filter`, отвергающий новое значение (D329). Запись репортеру приходит в каждой из этих форм,
529
+ включая скрытую: она говорит об отброшенной записи, а не о значении, с которым осталась ячейка (D432).
530
+
531
+ Фикса нет: какую из двух записей автор хотел сохранить, знает только он, а сообщение называет три выхода — сделать
532
+ запись в источник последней записью прогона; держать накапливаемое вне источника и читать его через `getSnapshot()`,
533
+ что заодно снимает и второй прогон; либо взять `execution.capture(state)` до сдвигающей записи — это единственный
534
+ выход, который спасает саму эту запись целиком, хотя и не спасает ничего написанного после неё. Называет теми же
535
+ словами, какими их печатает запись `LostWrite`, и равенство этих двух текстов держит `ci:source`: правило автор
536
+ читает раньше, чем отчитается хоть один прогон (D319, D432, D442, D447).
537
+
538
+ **Что доказывается до сообщения.** Три вещи, и четвёртая — там, где сдвигающую запись делает команда; там, где хотя
539
+ бы одна не доказана, правило молчит.
540
+
541
+ _Объявление._ `effect` объявлен на контексте этой библиотеки — `effect` на параметре типа `ModelContext` либо на
542
+ контексте `model(Declaration, factory)`, написанном внутри `defineFeature` из `@opetope/runtime` (D309). Прогон —
543
+ это функция, которую берёт объявление: написанная там же или связанная с именем в этом модуле.
544
+
545
+ _Ячейка входит в источник._ Либо источник — это она сама, либо это `derive` из `@opetope/core` — написанный там, где
546
+ его берёт объявление, или удержанный именем в этой области, — чьи зависимости её называют и чей селектор публикует
547
+ значение этой зависимости. Последнее условие и есть вырезка самого закона: запись, после которой опубликованное
548
+ значение осталось равным, не завершает ничего, поэтому селектор, который лишь задаёт вопрос о своей зависимости —
549
+ `held === null ? 'none' : 'some'`, `!held`, `typeof held`, `held ? a : b`, — публикует для одного ключа то же слово,
550
+ что и для другого, и над ним правило молчит. Молчит и над селектором, который зависимость выбрасывает, и над
551
+ селектором, которого этот файл прочитать не может.
552
+
553
+ _Запись может выполниться после._ Записи сделаны писателем этого прогона, прочитанным под теми же именами, под
554
+ которыми `capture-command-cleanup` читает писателя команды: `execution.update(…)`, `({ update })` или
555
+ `({ update: write })` в списке параметров и `const { update } = execution` в теле. Более поздняя запись обязана
556
+ стоять в том же блоке, в более позднем операторе и без безусловного выхода из прогона между ними: у
557
+ `if (held === null) { update(key, null); return; }` запись под `if` после этой выполниться не может, и о ней не
558
+ сообщается. `break` и `continue` таким выходом не являются: код под ними выполняется.
559
+
560
+ _Эту запись делает команда._ Там, где сдвигающая запись — это вызов, а не запись, вызов сделан через `invoke` этого
561
+ прогона, прочитанный под теми же именами, что и писатель, а его первый аргумент — имя, за которым в этом модуле
562
+ стоит `context.command(body)`, объявленный на контексте модели: `command` секции `own` у фичи берёт тело вторым и
563
+ писателя не выдаёт (D385, D386). Тело — функция, которую берёт это объявление, написанная там же или связанная с
564
+ именем в этом модуле, и хотя бы одна запись через контекст этого тела называет ячейку этого источника. Более поздняя
565
+ запись прогона обязана следовать за вызовом по тому же правилу, по которому она следует за записью.
566
+
567
+ **Граница.** Молчание не доказывает, что запись ляжет. За пределами форм выше правило не читает: источник,
568
+ собранный в другой функции; источник, прочитанный с объекта (`deps.sources` — форма рецепта-защёлки); ячейку,
569
+ приехавшую параметром; `derive` над `derive`; писателя, удержанного замыканием. Сдвигающей записью оно считает только
570
+ `update`, поэтому commit в источник — тот же закон, «`update` или commit» — не сообщается; не сообщается и третья
571
+ форма той же потери — `capture`, взятый **после** сдвигающей записи: он бросает собственную отмену прогона и не
572
+ репортится нигде. Две записи одного оператора не последовательность — ветви `if`, плечи тернарного оператора,
573
+ половины `try`, стороны запятой, — как и две записи одного `case`. Запись во вложенном колбэке прогона остаётся как
574
+ есть: когда выполняется таймер или disposer, прогон уже кончился, и у её потери своя причина. Цикл, у которого запись
575
+ в источник стоит последней, теряет первую запись следующего витка, и этого правило не видит.
576
+
577
+ У вызова своя граница, и это граница одного шага. Команда, приехавшая зависимостью (`invoke(deps.release)`), команда,
578
+ объявленная в другом модуле, команда на контексте, который этот файл разрешить не может, и команда, тело которой
579
+ собрано в другом месте, не называют здесь ничего. Читается ровно один шаг: команда, сдвигающая источник через вторую
580
+ команду, которую зовёт сама, или через `capture` ячейки источника, не сообщается; не сообщается и команда, чьё тело
581
+ пишет в источник писателем, взятым не со своего контекста. И `invoke` прогона, у которого вся потеря — сам вызов
582
+ (записи после вызова нет), тоже не сообщается: там ничего не потеряно — `await` не возвращается, а делать прогону
583
+ больше было нечего (D447).
584
+
471
585
  ### `capture-command-cleanup`
472
586
 
473
587
  Писатель команды живёт столько же, сколько её вызывающий (D288). Запись в `finally` или `catch` у `try`, который
@@ -477,7 +591,7 @@ ctx.effect(quotes, execution => publish(execution.current));
477
591
 
478
592
  ```ts
479
593
  // сообщается: после отмены вызывающего `busy` остаётся `true`
480
- ctx.call(async (input, { signal, update }) => {
594
+ ctx.command(async ({ input, signal, update }: ModelCommandContext<Payload>) => {
481
595
  update(busy, true);
482
596
  try {
483
597
  await host.send(input, signal);
@@ -487,7 +601,7 @@ ctx.call(async (input, { signal, update }) => {
487
601
  });
488
602
 
489
603
  // уборка ложится, пока жива модель
490
- ctx.call(async (input, { capture, signal, update }) => {
604
+ ctx.command(async ({ capture, input, signal, update }: ModelCommandContext<Payload>) => {
491
605
  update(busy, true);
492
606
  const done = capture(busy);
493
607
  try {
@@ -502,16 +616,16 @@ ctx.call(async (input, { capture, signal, update }) => {
502
616
  запись отменённого вызова не должна затирать то, что написал более новый вызов. Поэтому сообщение называет оба выхода.
503
617
  Уборка, которая обязана выполниться для отменённого вызова, идёт через `capture(state)`, взятый до `await`; ответ,
504
618
  который не должен лечь после отмены, — отказ, результат, — стоит в `catch` после `rethrowIfCancelled(cause)`. Там, где
505
- вызывающий отменяет один прогон, чтобы начать следующий, — поиск, который потребитель запускает заново, Call,
619
+ вызывающий отменяет один прогон, чтобы начать следующий, — поиск, который потребитель запускает заново, Command,
506
620
  вызванный прогоном эффекта и отменённый более новым значением, — запись старого прогона и есть та, что лечь не должна,
507
621
  и об этом говорит комментарий-отключение.
508
622
 
509
- При `policy: 'parallel'` сообщение commit не советует. Вызовы такой команды перекрываются, поэтому захваченная уборка
510
- отменённого вызова сняла бы флаг, который ещё держит другой вызов, — флаг, общий для параллельных вызовов, гонится в
511
- любом случае, — и сообщение говорит держать такое состояние на вызов или оставить ход потребителю через `inFlight`.
512
- Правило видит политику, только когда `{ policy: 'parallel' }` написан на самом `call`; запись модификаторов,
513
- собранная в другом месте, читается как очередь по умолчанию, где lane ждёт физического тела и захваченная уборка
514
- ложится по порядку.
623
+ При `concurrency: 'parallel'` сообщение commit не советует. Вызовы такой команды перекрываются, поэтому захваченная
624
+ уборка отменённого вызова сняла бы флаг, который ещё держит другой вызов, — флаг, общий для параллельных вызовов,
625
+ гонится в любом случае, — и сообщение говорит держать такое состояние на вызов или оставить ход потребителю через
626
+ `inFlight`. Правило видит порядок, только когда `{ concurrency: 'parallel' }` написан на самом `command`; запись
627
+ модификаторов, собранная в другом месте, читается как очередь по умолчанию, где lane ждёт физического тела и
628
+ захваченная уборка ложится по порядку (D387).
515
629
 
516
630
  Правило — эвристика над синтаксисом и ничего не доказывает о записи, о которой молчит. Писателя тела команды оно
517
631
  читает под такими именами: `execution.update(…)`; `({ update })` или `({ update: write })` в списке параметров;
@@ -528,9 +642,9 @@ ctx.call(async (input, { capture, signal, update }) => {
528
642
  `true`, и сообщения нет. Поэтому флаг, который обязан сняться при отмене, пишется в `finally` через `capture(state)`.
529
643
  Проверка в `finally` ничего не спасает и сообщается по-прежнему, как и проверка `signal.aborted`, которая не бросает и
530
644
  не возвращает. `effect`, `event` и остальные реакции правило не трогает: их прогон отменило более новое значение, и
531
- сброс его записи — ровно то, что нужно (D288). Контекст разрешается точно: `call` параметра типа `ModelContext` либо
532
- контекста `model(Declaration, factory)` внутри `defineFeature` из `@opetope/runtime` (D309); собственный `call` фичи
533
- писателя телу не даёт.
645
+ сброс его записи — ровно то, что нужно (D288). Контекст разрешается точно: `command` параметра типа `ModelContext`
646
+ либо контекста `model(Declaration, factory)` внутри `defineFeature` из `@opetope/runtime` (D309); собственный
647
+ `command` фичи писателя телу не даёт.
534
648
 
535
649
  ### `prefer-model-selection`
536
650
 
@@ -545,6 +659,82 @@ ctx.call(async (input, { capture, signal, update }) => {
545
659
  выбора и считает каждую выданную модель отдельно: две модели в одном компоненте — это два гранта, а одно и то же
546
660
  имя переменной в двух компонентах — два счёта. Фикса нет: форму выбора пишет автор.
547
661
 
662
+ ### `no-retired-vocabulary`
663
+
664
+ Слова, снятые волной D365–D411, — каждое названо вместе с тем, которое его заменило. Правило написано ровно на один
665
+ проход миграции: в этом релизе ни у одного из них нет алиаса, поэтому исходник, который всё ещё пишет такое слово,
666
+ просто не мигрирован, а после миграции правило не срабатывает никогда.
667
+
668
+ | Снято | Стало | Решение |
669
+ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------- |
670
+ | `invalidate()` у Resource | `reset()` | D373 |
671
+ | `backpressure` у события | `delivery` | D374 |
672
+ | `kind` внутри записи доставки | `pending` | D374 |
673
+ | `key` внутри записи доставки | `by` | D374 |
674
+ | `activity` у состояния `loading` | `failure` | D375 |
675
+ | `scope.keyed(source, open, { key })` | `scope.each(source, key, open)` | D378 |
676
+ | `skipInitial` | `initial`, с перевёрнутой полярностью | D379 |
677
+ | `when` у эффекта | `filter` **или** `initial` | D379 |
678
+ | `onDispose` | `finalize` | D379 |
679
+ | `externalReadable` | `fromExternal` | D380 |
680
+ | `Call`, `ctx.call`, `runCall` | `Command`, `ctx.command`, `runCommand` | D385 |
681
+ | `policy`, `queueBy`, `lane` | `concurrency` | D387 |
682
+ | `ctx.calls`, `own.calls`, `useCommands` | `ctx.select`, `own.select`, `useModel(Declaration, select)` | D388 |
683
+ | `once` | `memoize` | D389 |
684
+ | `from` у `defineCondition` | `source` | D411 |
685
+ | `when` у вклада | `enabled` | D411 |
686
+ | `overflow: 'reject'` у live Resource | `overflow: 'drop'` | D411 |
687
+ | `status` у `CommandOutcome` | `kind` | D411 |
688
+ | `ResourceActivity` из `@opetope/devtools` | `ResourceNodeActivity` | D411 |
689
+ | `ResourceDisposer` | `Disposer` | D411 |
690
+ | `NavigatorPolicy`, `surface.policy` | `NavigatorCatalogue`, `surface.catalogue` | D411 |
691
+ | `ApplicationConditionBinding`, `ApplicationExecution` и `ApplicationImportBinding` из `@opetope/runtime/internal` | те же имена, из `@opetope/runtime` | D411 |
692
+
693
+ **Одну строку по слову не выполнить, и отчёт договаривает остальное.** `ctx.call` становится `ctx.command`, а
694
+ вместе с ним тело `(input, context) => …` становится телом, которое принимает один свой контекст, — и входу больше
695
+ негде называться: частичного вывода типовых аргументов в TypeScript нет, и `command<Deposit>(…)` не маршрут.
696
+ Единственное место — аннотация деструктурируемого контекста: `({ input, update }: ModelCommandContext<Deposit>)` у
697
+ модели и `({ input, source }: FeatureCommandContext<typeof within, Deposit>)` у фичи, — поэтому отчёт называет эту
698
+ аннотацию рядом со словом. Переименование, выполненное дословно без неё, оставляет это правило молчащим, а сборку
699
+ красной, и каждый отказ при этом стоит на верно написанном коде (D444).
700
+
701
+ **Автофикса нет, и это решение, а не недоделка (D404).** Часть строк здесь не один-к-одному. `policy`, `queueBy` и
702
+ `lane` схлопываются в одно `concurrency`, чья форма зависит от того, какую комбинацию написала запись: `{ by }`,
703
+ `{ by, lane }` и `{ lane, pending: 'latest' }` — три разных значения из тех же трёх слов. `when` у эффекта
704
+ становится `filter` **или** `initial`, и что именно — вопрос о том, что проверяет предикат:
705
+ `filter: (_current, previous) => previous !== undefined` — это вообще не фильтр, а `initial: false`, и руководство
706
+ автора требует именно этого. А `skipInitial` становится `initial` с перевёрнутой полярностью, то есть единственная
707
+ строка, выглядящая чисто механической, — ровно та, которая скомпилируется и будет значить обратное. Автофикс,
708
+ угадавший неверно, молча перепишет исходник потребителя, а это хуже отказа, — поэтому каждое сообщение называет
709
+ снятое слово, называет замену и прямо говорит там, где замена является выбором.
710
+
711
+ **Граница.** Принадлежность доказывается символом, а не написанием. Снятый член репортится только на объявляющем
712
+ контексте этой библиотеки — параметре, аннотированном `ModelContext` из `@opetope/core`, или секции `own`,
713
+ написанной там, где её принимает `defineFeature` из `@opetope/runtime`, — и разрешается через алиас или
714
+ деструктурированный член так же, как `no-subscribe-outside-models` разрешает ingress объявленного узла. Снятое имя
715
+ репортится только там, где его экспортирует модуль `@opetope/*`, а там, где имя снято на одном входе и живо на
716
+ другом, модуль спрашивается точно: `ResourceActivity` по-прежнему то, как `@opetope/core` называет фоновую работу
717
+ состояния `ready`, а три имени `@opetope/runtime/internal` — по-прежнему имена, только на том входе, с которого их
718
+ берёт автор. Гейт вклада репортится на секции `provides`, которую принимает `defineFeature` из `@opetope/runtime`, — так
719
+ же, как объявленный узел репортится на `own`; `status` свершившегося ответа — только на `outcome` от `useCommand`
720
+ из `@opetope/react`, а `policy` поверхности — только на значении, объявленном через `defineSurface` из
721
+ `@opetope/navigation/react`. Resource — это значение, объявленное через
722
+ `resource.load` или `resource.live` такого контекста, либо запись, которую отвечает `useResource` из
723
+ `@opetope/react`; его состояние — `state` этой записи или `getSnapshot()` от него.
724
+ `activity` репортится только внутри сужения, доказывающего, что состояние — `loading`: `if`, тернарник или левая
725
+ половина `&&`, написанные про то же самое выражение, — потому что у `ready` это поле осталось.
726
+
727
+ Молчание здесь и есть смысл. `Call` внутри `'Margin Call'` и внутри ключей каталога переводов, `{ once: true }` у
728
+ `addEventListener`, собственный параметр `policy` хоста и `policy` чужого объекта, `Navigator.reset()` и поле
729
+ `delivery` чужой записи — всё
730
+ это не трогается, как и `when` у фичи и у `scope.while`, где слово не снято вовсе, и `status` чужой записи. Не трогается и то,
731
+ чего правило не видит: Resource, до которого дошли через запись модели, а не через объявление, которое его
732
+ построило; состояние, суженное в `switch` или внутри helper-а; запись опций, собранная в другом модуле. Поэтому
733
+ молчание правила не доказывает, что миграция закончена: вторая половина — компилятор, который теперь печатает
734
+ замену для каждого слова, которое может назвать (D401, D444). Шесть строк выше — слова, которые носила запись
735
+ опций, и каждая такая запись называет своё снятое слово, чтобы отказ нёс замену: `skipInitial`, `when` и
736
+ `onDispose` у эффекта, `backpressure` у события, `when` у вклада и `from` у `defineCondition`.
737
+
548
738
  ## Coexistence with key sorting
549
739
 
550
740
  Хост, который сортирует ключи объектов по алфавиту, разойдётся с `define-feature-property-order`: секции фичи
package/dist/ast.d.ts CHANGED
@@ -14,5 +14,7 @@ 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
+ /** Lexical containment by range: the one question a rule that collects nodes and answers later keeps asking. */
18
+ declare function within(node: TSESTree.Node, outer: TSESTree.Node): boolean;
17
19
  export type { FunctionLike };
18
- export { calleeName, findProperty, functionName, isFunctionLike, memberFirstStep, memberRoot, patternKeys, propertyName, };
20
+ export { calleeName, findProperty, functionName, isFunctionLike, memberFirstStep, memberRoot, patternKeys, propertyName, within, };
package/dist/ast.js CHANGED
@@ -1,2 +1,2 @@
1
- import{AST_NODE_TYPES as r}from"@typescript-eslint/utils";function u(t){const{callee:e}=t;if(e.type===r.Identifier)return e.name;if(!(e.type!==r.MemberExpression||e.computed)&&!(e.object.type!==r.Identifier||e.property.type!==r.Identifier))return`${e.object.name}.${e.property.name}`}function i(t){if(!(t.type!==r.Property||t.computed)){if(t.key.type===r.Identifier)return t.key.name;if(t.key.type===r.Literal&&typeof t.key.value=="string")return t.key.value}}function f(t,e){for(const n of t.properties)if(n.type===r.Property&&i(n)===e)return n}function c(t){const e=new Set;for(const n of t.properties){const p=i(n);p!==void 0&&e.add(p)}return e}function y(t){let e=t;for(;e.type===r.MemberExpression;)e=e.object;return e}function a(t){let e=t;for(;e.object.type===r.MemberExpression;)e=e.object;if(!(e.computed||e.property.type!==r.Identifier))return e.property.name}function o(t){return t.type===r.ArrowFunctionExpression||t.type===r.FunctionExpression}function s(t){let e=t;for(;e.parent.type===r.CallExpression||e.parent.type===r.TSAsExpression;)e=e.parent;const{parent:n}=e;return n.type===r.VariableDeclarator&&n.id.type===r.Identifier?n.id.name:i(n)}function m(t){return t.type===r.FunctionDeclaration?t.id?.name:o(t)?(t.type===r.FunctionExpression?t.id?.name:void 0)??s(t):void 0}export{u as calleeName,f as findProperty,m as functionName,o as isFunctionLike,a as memberFirstStep,y as memberRoot,c as patternKeys,i as propertyName};
1
+ import{AST_NODE_TYPES as t}from"@typescript-eslint/utils";function o(r){const{callee:e}=r;if(e.type===t.Identifier)return e.name;if(!(e.type!==t.MemberExpression||e.computed)&&!(e.object.type!==t.Identifier||e.property.type!==t.Identifier))return`${e.object.name}.${e.property.name}`}function i(r){if(!(r.type!==t.Property||r.computed)){if(r.key.type===t.Identifier)return r.key.name;if(r.key.type===t.Literal&&typeof r.key.value=="string")return r.key.value}}function f(r,e){for(const n of r.properties)if(n.type===t.Property&&i(n)===e)return n}function c(r){const e=new Set;for(const n of r.properties){const p=i(n);p!==void 0&&e.add(p)}return e}function y(r){let e=r;for(;e.type===t.MemberExpression;)e=e.object;return e}function a(r){let e=r;for(;e.object.type===t.MemberExpression;)e=e.object;if(!(e.computed||e.property.type!==t.Identifier))return e.property.name}function u(r){return r.type===t.ArrowFunctionExpression||r.type===t.FunctionExpression}function s(r){let e=r;for(;e.parent.type===t.CallExpression||e.parent.type===t.TSAsExpression;)e=e.parent;const{parent:n}=e;return n.type===t.VariableDeclarator&&n.id.type===t.Identifier?n.id.name:i(n)}function m(r){return r.type===t.FunctionDeclaration?r.id?.name:u(r)?(r.type===t.FunctionExpression?r.id?.name:void 0)??s(r):void 0}function d(r,e){return r.range[0]>=e.range[0]&&r.range[1]<=e.range[1]}export{o as calleeName,f as findProperty,m as functionName,u as isFunctionLike,a as memberFirstStep,y as memberRoot,c as patternKeys,i as propertyName,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 type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\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\nexport type { FunctionLike };\nexport {\n calleeName,\n findProperty,\n functionName,\n isFunctionLike,\n memberFirstStep,\n memberRoot,\n patternKeys,\n propertyName,\n};\n"],"names":["calleeName","node","callee","AST_NODE_TYPES","propertyName","findProperty","name","property","patternKeys","pattern","keys","memberRoot","current","memberFirstStep","isFunctionLike","boundName","parent","functionName"],"mappings":"0DAMA,SAASA,EAAWC,EAA6B,CAC/C,KAAM,CAAE,OAAAC,CAAM,EAAKD,EAEnB,GAAIC,EAAO,OAASC,EAAe,WAAY,OAAOD,EAAO,KAE7D,GAAI,EAAAA,EAAO,OAASC,EAAe,kBAAoBD,EAAO,WAE1D,EAAAA,EAAO,OAAO,OAASC,EAAe,YAAcD,EAAO,SAAS,OAASC,EAAe,YAIhG,MAAO,GAAGD,EAAO,OAAO,IAAI,IAAIA,EAAO,SAAS,IAAI,EACtD,CAGA,SAASE,EAAaH,EAAmB,CACvC,GAAI,EAAAA,EAAK,OAASE,EAAe,UAAYF,EAAK,UAElD,IAAIA,EAAK,IAAI,OAASE,EAAe,WAAY,OAAOF,EAAK,IAAI,KAEjE,GAAIA,EAAK,IAAI,OAASE,EAAe,SAAW,OAAOF,EAAK,IAAI,OAAU,SAAU,OAAOA,EAAK,IAAI,MAGtG,CAGA,SAASI,EAAaJ,EAAiCK,EAAY,CACjE,UAAWC,KAAYN,EAAK,WAC1B,GAAIM,EAAS,OAASJ,EAAe,UAAYC,EAAaG,CAAQ,IAAMD,EAAM,OAAOC,CAI7F,CAEA,SAASC,EAAYC,EAA+B,CAClD,MAAMC,EAAO,IAAI,IAEjB,UAAWH,KAAYE,EAAQ,WAAY,CACzC,MAAMH,EAAOF,EAAaG,CAAQ,EAE9BD,IAAS,QAAWI,EAAK,IAAIJ,CAAI,CACvC,CAEA,OAAOI,CACT,CAGA,SAASC,EAAWV,EAA+B,CACjD,IAAIW,EAAyBX,EAE7B,KAAOW,EAAQ,OAAST,EAAe,kBAAkBS,EAAUA,EAAQ,OAE3E,OAAOA,CACT,CAGA,SAASC,EAAgBZ,EAA+B,CACtD,IAAIW,EAAUX,EAEd,KAAOW,EAAQ,OAAO,OAAST,EAAe,kBAAkBS,EAAUA,EAAQ,OAElF,GAAI,EAAAA,EAAQ,UAAYA,EAAQ,SAAS,OAAST,EAAe,YAEjE,OAAOS,EAAQ,SAAS,IAC1B,CAEA,SAASE,EAAeb,EAAmB,CACzC,OAAOA,EAAK,OAASE,EAAe,yBAA2BF,EAAK,OAASE,EAAe,kBAC9F,CAGA,SAASY,EAAUd,EAAkB,CACnC,IAAIW,EAAyBX,EAE7B,KACEW,EAAQ,OAAO,OAAST,EAAe,gBACvCS,EAAQ,OAAO,OAAST,EAAe,gBAEvCS,EAAUA,EAAQ,OAGpB,KAAM,CAAE,OAAAI,CAAM,EAAKJ,EAEnB,OAAII,EAAO,OAASb,EAAe,oBAAsBa,EAAO,GAAG,OAASb,EAAe,WAClFa,EAAO,GAAG,KAGZZ,EAAaY,CAAM,CAC5B,CAGA,SAASC,EAAahB,EAAmB,CACvC,OAAIA,EAAK,OAASE,EAAe,oBAA4BF,EAAK,IAAI,KAEjEa,EAAeb,CAAI,GAEZA,EAAK,OAASE,EAAe,mBAAqBF,EAAK,IAAI,KAAO,SAEhEc,EAAUd,CAAI,EAJD,MAK7B"}
1
+ {"version":3,"file":"ast.js","sources":["../src/ast.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\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/** 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 type { FunctionLike };\nexport {\n calleeName,\n findProperty,\n functionName,\n isFunctionLike,\n memberFirstStep,\n memberRoot,\n patternKeys,\n propertyName,\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","within","outer"],"mappings":"0DAMA,SAASA,EAAWC,EAA6B,CAC/C,KAAM,CAAE,OAAAC,CAAM,EAAKD,EAEnB,GAAIC,EAAO,OAASC,EAAe,WAAY,OAAOD,EAAO,KAE7D,GAAI,EAAAA,EAAO,OAASC,EAAe,kBAAoBD,EAAO,WAE1D,EAAAA,EAAO,OAAO,OAASC,EAAe,YAAcD,EAAO,SAAS,OAASC,EAAe,YAIhG,MAAO,GAAGD,EAAO,OAAO,IAAI,IAAIA,EAAO,SAAS,IAAI,EACtD,CAGA,SAASE,EAAaH,EAAmB,CACvC,GAAI,EAAAA,EAAK,OAASE,EAAe,UAAYF,EAAK,UAElD,IAAIA,EAAK,IAAI,OAASE,EAAe,WAAY,OAAOF,EAAK,IAAI,KAEjE,GAAIA,EAAK,IAAI,OAASE,EAAe,SAAW,OAAOF,EAAK,IAAI,OAAU,SAAU,OAAOA,EAAK,IAAI,MAGtG,CAGA,SAASI,EAAaJ,EAAiCK,EAAY,CACjE,UAAWC,KAAYN,EAAK,WAC1B,GAAIM,EAAS,OAASJ,EAAe,UAAYC,EAAaG,CAAQ,IAAMD,EAAM,OAAOC,CAI7F,CAEA,SAASC,EAAYC,EAA+B,CAClD,MAAMC,EAAO,IAAI,IAEjB,UAAWH,KAAYE,EAAQ,WAAY,CACzC,MAAMH,EAAOF,EAAaG,CAAQ,EAE9BD,IAAS,QAAWI,EAAK,IAAIJ,CAAI,CACvC,CAEA,OAAOI,CACT,CAGA,SAASC,EAAWV,EAA+B,CACjD,IAAIW,EAAyBX,EAE7B,KAAOW,EAAQ,OAAST,EAAe,kBAAkBS,EAAUA,EAAQ,OAE3E,OAAOA,CACT,CAGA,SAASC,EAAgBZ,EAA+B,CACtD,IAAIW,EAAUX,EAEd,KAAOW,EAAQ,OAAO,OAAST,EAAe,kBAAkBS,EAAUA,EAAQ,OAElF,GAAI,EAAAA,EAAQ,UAAYA,EAAQ,SAAS,OAAST,EAAe,YAEjE,OAAOS,EAAQ,SAAS,IAC1B,CAEA,SAASE,EAAeb,EAAmB,CACzC,OAAOA,EAAK,OAASE,EAAe,yBAA2BF,EAAK,OAASE,EAAe,kBAC9F,CAGA,SAASY,EAAUd,EAAkB,CACnC,IAAIW,EAAyBX,EAE7B,KACEW,EAAQ,OAAO,OAAST,EAAe,gBACvCS,EAAQ,OAAO,OAAST,EAAe,gBAEvCS,EAAUA,EAAQ,OAGpB,KAAM,CAAE,OAAAI,CAAM,EAAKJ,EAEnB,OAAII,EAAO,OAASb,EAAe,oBAAsBa,EAAO,GAAG,OAASb,EAAe,WAClFa,EAAO,GAAG,KAGZZ,EAAaY,CAAM,CAC5B,CAGA,SAASC,EAAahB,EAAmB,CACvC,OAAIA,EAAK,OAASE,EAAe,oBAA4BF,EAAK,IAAI,KAEjEa,EAAeb,CAAI,GAEZA,EAAK,OAASE,EAAe,mBAAqBF,EAAK,IAAI,KAAO,SAEhEc,EAAUd,CAAI,EAJD,MAK7B,CAGA,SAASiB,EAAOjB,EAAqBkB,EAAoB,CACvD,OAAOlB,EAAK,MAAM,CAAC,GAAKkB,EAAM,MAAM,CAAC,GAAKlB,EAAK,MAAM,CAAC,GAAKkB,EAAM,MAAM,CAAC,CAC1E"}
@@ -1,6 +1,6 @@
1
1
  import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
- /** What a name holds: one hook, a record of them, a selection whose fields may be hooks, or one such field. */
3
- type HookKind = 'candidate' | 'hook' | 'hooks' | 'selection';
2
+ /** What a name holds: one hook, a selection whose fields may be hooks, or one such field (D388). */
3
+ type HookKind = 'candidate' | 'hook' | 'selection';
4
4
  type HookKinds = Map<TSESLint.Scope.Variable, HookKind>;
5
5
  /**
6
6
  * What a dependency element names: one hook (no key, not a record), one key of a record, or the record itself,
@@ -1,2 +1,2 @@
1
- import{AST_NODE_TYPES as o}from"@typescript-eslint/utils";import{apiName as h,localValue as I,variableOf as d,unwrap as p}from"./model-bindings.js";const O=new Set(["inFlight","outcome","run"]),S=["inFlight","outcome"],l=new Set(["hooks","selection"]),g=2,y={member:void 0,optional:!1,text:""},R={accesses:[],opaque:!0};function s(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 c(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=s(e);return!i.record&&i.key===void 0?c(n,t,!1):t===void 0?y:i.key===void 0||t.key===i.key?c(n,s(t.node),t.optional):void 0}function x(n,e){return n.range[0]>=e.range[0]&&n.range[1]<=e.range[1]}function A(n,e,i){if(i===void 0)return R;const t=[];let r=!1;for(const{identifier:f}of e.variable.references){if(f.type!==o.Identifier||!x(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 M(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||M(n,e)?e:void 0}function T(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 C(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=C(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?T(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 v(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 B(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=B(n,e);if(t===void 0)return;const r=h(n,t.callee,"@opetope/react");r==="useCommand"?a(n,e.id,i,"hook"):r==="useCommands"?v(n,e.id,i,"hooks","hook"):r==="useModel"&&t.arguments.length>=g&&v(n,e.id,i,"selection","candidate")}export{S as STATUS_MEMBERS,j as bindingAt,D as classifyDeclaration,A as usageIn};
1
+ import{AST_NODE_TYPES as o}from"@typescript-eslint/utils";import{apiName as v,localValue as I,variableOf as d,unwrap as p}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