@opetope/lint 0.1.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 +5 -0
- package/LICENSE +21 -0
- package/README.md +335 -0
- package/README.ru.md +335 -0
- package/dist/ast.d.ts +18 -0
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -0
- package/dist/declarations.d.ts +8 -0
- package/dist/declarations.js +2 -0
- package/dist/declarations.js.map +1 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/model-bindings.d.ts +14 -0
- package/dist/model-bindings.js +2 -0
- package/dist/model-bindings.js.map +1 -0
- package/dist/rule.d.ts +9 -0
- package/dist/rule.js +2 -0
- package/dist/rule.js.map +1 -0
- package/dist/rules/define-feature-property-order.d.ts +5 -0
- package/dist/rules/define-feature-property-order.js +2 -0
- package/dist/rules/define-feature-property-order.js.map +1 -0
- package/dist/rules/id-naming.d.ts +8 -0
- package/dist/rules/id-naming.js +2 -0
- package/dist/rules/id-naming.js.map +1 -0
- package/dist/rules/layer-placement.d.ts +8 -0
- package/dist/rules/layer-placement.js +2 -0
- package/dist/rules/layer-placement.js.map +1 -0
- package/dist/rules/no-snapshot-read-in-render.d.ts +4 -0
- package/dist/rules/no-snapshot-read-in-render.js +2 -0
- package/dist/rules/no-snapshot-read-in-render.js.map +1 -0
- package/dist/rules/no-subscribe-outside-models.d.ts +4 -0
- package/dist/rules/no-subscribe-outside-models.js +2 -0
- package/dist/rules/no-subscribe-outside-models.js.map +1 -0
- package/dist/rules/prefer-model-selection.d.ts +7 -0
- package/dist/rules/prefer-model-selection.js +2 -0
- package/dist/rules/prefer-model-selection.js.map +1 -0
- package/dist/rules/require-declared-models.d.ts +5 -0
- package/dist/rules/require-declared-models.js +2 -0
- package/dist/rules/require-declared-models.js.map +1 -0
- package/dist/rules/require-literal-id.d.ts +7 -0
- package/dist/rules/require-literal-id.js +2 -0
- package/dist/rules/require-literal-id.js.map +1 -0
- package/dist/rules/when-predicate.d.ts +6 -0
- package/dist/rules/when-predicate.js +2 -0
- package/dist/rules/when-predicate.js.map +1 -0
- package/dist/static-value.d.ts +15 -0
- package/dist/static-value.js +2 -0
- package/dist/static-value.js.map +1 -0
- package/package.json +54 -0
package/README.ru.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# @opetope/lint
|
|
2
|
+
|
|
3
|
+
Правила ESLint для кода, который пишет автор на Opetope. Плагин держит только то, что синтаксическая проверка может
|
|
4
|
+
доказать об объявлении: формы, которые компилируются и работают, но говорят не то, что имел в виду автор. То, на что
|
|
5
|
+
уже отвечают проверка типов, рантайм и анализ мёртвого кода, остаётся за ними (см. [`../docs/decisions.md`](https://www.npmjs.com/package/@opetope/runtime), D227).
|
|
6
|
+
|
|
7
|
+
Пакет не зависит ни от одного другого пакета `@opetope/*` и не читает типы, поэтому хост может линтовать исходники,
|
|
8
|
+
которые ещё не собирал.
|
|
9
|
+
|
|
10
|
+
## Установка
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install --save-dev @opetope/lint 'eslint@^9' '@typescript-eslint/parser@^8'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Используйте согласованные версии Opetope. Для release candidate добавьте `@next` каждому пакету `@opetope/*` в команде.
|
|
17
|
+
API поставляется только в ESM; требуется Node 20.19+. Команды разработки ниже относятся к contributor checkout.
|
|
18
|
+
|
|
19
|
+
Нормативные руководства EN/RU поставляются в `@opetope/runtime`: после его установки откройте
|
|
20
|
+
`node_modules/@opetope/runtime/docs/spec.md` или `spec.ru.md`; рецепты находятся в `cookbook.md` и `cookbook.ru.md`.
|
|
21
|
+
Для чтения установленных руководств доступ к GitHub не нужен.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// eslint.config.mjs
|
|
27
|
+
import opetope from '@opetope/lint';
|
|
28
|
+
|
|
29
|
+
export default [
|
|
30
|
+
{
|
|
31
|
+
files: ['src/**/*.{ts,tsx}'],
|
|
32
|
+
...opetope.configs.recommended,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Плагин — это и default export, и именованный `opetopeLint`. Конфигурация регистрирует его в namespace `opetope`,
|
|
38
|
+
поэтому правило называется `opetope/when-predicate`. Для разбора нужен `@typescript-eslint/parser`; он и `eslint`
|
|
39
|
+
объявлены peer dependencies.
|
|
40
|
+
|
|
41
|
+
## Configs
|
|
42
|
+
|
|
43
|
+
### `recommended`
|
|
44
|
+
|
|
45
|
+
Все правила, которые действуют везде, где пишут на Opetope: `when-predicate`, `define-feature-property-order`,
|
|
46
|
+
`require-literal-id`, `id-naming`, `no-snapshot-read-in-render` и `require-declared-models` как error. Правило, которому нужно знать, где хост
|
|
47
|
+
держит свои файлы, здесь выключено и приходит через `layers`; `prefer-model-selection` выключено потому, что число
|
|
48
|
+
hooks, которое компонент вправе держать, каждый проект решает для себя.
|
|
49
|
+
|
|
50
|
+
### `layers`
|
|
51
|
+
|
|
52
|
+
`opetope.configs.layers({ integration, models, ui, tests })` принимает глобы собственных слоёв проекта и
|
|
53
|
+
возвращает ту конфигурацию, которую каждый из них заслужил. Слой без глобов — это слой, которого у проекта нет, и
|
|
54
|
+
он не даёт ничего; файл, который не заявил ни один глоб, не размещает никто.
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
...opetope.configs.layers({
|
|
58
|
+
integration: ['src/features/*/integration/**/*.{ts,tsx}'],
|
|
59
|
+
models: ['src/features/*/models/**/*.ts'],
|
|
60
|
+
ui: ['src/features/*/ui/**/*.{ts,tsx}'],
|
|
61
|
+
}),
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`integration`, `models` и `ui` включают `layer-placement` для своих файлов. `models` вместе с этим включает
|
|
65
|
+
`no-subscribe-outside-models` для всех исходников и отступает в самом слое моделей и в `tests`, у которых значение
|
|
66
|
+
по умолчанию `['**/__tests__/**', '**/*.spec.ts', '**/*.spec.tsx']`. Подписка покрыта и в файле, который не заявил
|
|
67
|
+
ни один слой: забытая подписка — это ровно то, как она переживает своего читателя.
|
|
68
|
+
|
|
69
|
+
### `internalImports`
|
|
70
|
+
|
|
71
|
+
`opetope.configs.internalImports({ allow, files })` держит `@opetope/*/internal` вне кода, который не является ни
|
|
72
|
+
рантаймом, ни его интеграцией с хостом. `allow` называет файлы, которые эту работу делают, — ограничение до них не
|
|
73
|
+
доходит, поэтому всё остальное, что проект там запрещает, продолжает действовать, — а `files` задаёт, что
|
|
74
|
+
ограничение покрывает; по умолчанию это все `.ts` и `.tsx`.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
...opetope.configs.internalImports({ allow: ['src/bootstrap/**/devtools.ts'] }),
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`no-restricted-imports` — core-правило с одним значением на файл, и во flat config побеждает последняя запись,
|
|
81
|
+
которая его называет. Проект, который уже настраивает это правило, берёт `opetope.internalImportPattern` в свою
|
|
82
|
+
запись, а не раскрывает эту конфигурацию: она заменила бы его запись для каждого покрытого файла:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
'no-restricted-imports': ['error', { paths: myPaths, patterns: [...myPatterns, opetope.internalImportPattern] }],
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Файлы, которые интегрируют хост, получают ту же запись без этого одного паттерна и сохраняют все остальные
|
|
89
|
+
ограничения проекта.
|
|
90
|
+
|
|
91
|
+
## Rules
|
|
92
|
+
|
|
93
|
+
### `when-predicate`
|
|
94
|
+
|
|
95
|
+
`when` вклада отвечает фактом видимости этого экземпляра, а не источником, который этот факт несёт (D220). Правило
|
|
96
|
+
читает `when` у вкладов `slot`, `pipe` и `register`, а также любой `when`, чья функция деструктурирует контекст
|
|
97
|
+
вычисления `{ exports, imports, own, read }`, и сообщает о трёх формах:
|
|
98
|
+
|
|
99
|
+
| Написано | О чём сообщает |
|
|
100
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
101
|
+
| `when: ({ imports }) => imports.x.allowed` | возвращает источник; фикс приводит к `({ imports, read }) => read(imports.x.allowed)` |
|
|
102
|
+
| `when: async ({ read }) => read(x)` | предикат отвечает синхронно; без фикса |
|
|
103
|
+
| `when: () => allowed` | `Readable<boolean>` передаётся в `when` как есть, без обёртки; без фикса |
|
|
104
|
+
|
|
105
|
+
Фикс оборачивает возвращённое обращение к полю в `read(...)` и добавляет `read` в деструктуризацию контекста, если
|
|
106
|
+
его там нет. Именованный контекст читается через себя: `context => context.imports.x.allowed` становится
|
|
107
|
+
`context => context.read(context.imports.x.allowed)`.
|
|
108
|
+
|
|
109
|
+
**Граница.** Правило читает формы, а не типы. `({ read }) => read(counter)` над не-boolean источником остаётся
|
|
110
|
+
ошибкой типов, как и `read` над тем, что не является `Readable`. Если обращение к полю контекста вычисления — это
|
|
111
|
+
обычное значение, а не источник, предикат не может изменить свой ответ, и правило сообщает о нём как о написанном
|
|
112
|
+
для источника; вынеси такое решение из `when` или заглуши строку. `when` вне вклада — позиционный
|
|
113
|
+
`(current, previous)` у `effect` или значение источника у `scope.while` — правило не трогает.
|
|
114
|
+
|
|
115
|
+
### `define-feature-property-order`
|
|
116
|
+
|
|
117
|
+
Фича объявляет свои секции в одном порядке: `id`, `imports`, `requires`, `own`, `exports`, `provides`, `when` и
|
|
118
|
+
загрузчик `body`, который заменяет три последние стадии (spec §2.1, D186). Порядок совпадает с порядком чтения
|
|
119
|
+
объявления — идентичность, рёбра, стадии, затем время жизни и загрузчик, — поэтому читатель находит секцию по её
|
|
120
|
+
месту, а сортировщик ключей можно настроить на тот же порядок вместо второго.
|
|
121
|
+
|
|
122
|
+
Фикс переставляет свойства. Он отступает, если между двумя секциями стоит комментарий: такой комментарий не
|
|
123
|
+
принадлежит ни одной из них, и перестановка увела бы его от строки, которую он объясняет; комментарий внутри секции
|
|
124
|
+
переезжает вместе с ней. Объект с ключом, который не является секцией, или со spread остаётся проверке типов.
|
|
125
|
+
|
|
126
|
+
### `require-declared-models`
|
|
127
|
+
|
|
128
|
+
Компонент, читающий UI-модель отдельного mount, объявляет её через `requiresModels`. Owner-модели остаются
|
|
129
|
+
доступны всему subtree вклада без такого объявления (D158, D250).
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { defineFeature } from '@opetope/runtime';
|
|
133
|
+
import { requiresModels, useModel } from '@opetope/react';
|
|
134
|
+
|
|
135
|
+
const Form = () => {
|
|
136
|
+
const actions = useModel(FormActions);
|
|
137
|
+
return null;
|
|
138
|
+
};
|
|
139
|
+
const DeclaredForm = requiresModels([FormActions])(Form);
|
|
140
|
+
|
|
141
|
+
defineFeature({
|
|
142
|
+
id: 'example.form',
|
|
143
|
+
provides: ({ slot }) => ({
|
|
144
|
+
form: slot(FormSlot, ({ model }) => ({
|
|
145
|
+
Component: DeclaredForm,
|
|
146
|
+
models: [model(FormActions, createActions)],
|
|
147
|
+
})),
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Правило проверяет вклады, переданные capability `slot` из видимого callback `provides` в `defineFeature` или
|
|
153
|
+
`defineFeature.body`. Локальный компонент, получающий UI-модели, объявляет свои требования; каждый видимый
|
|
154
|
+
компонент или hook, читающий одну из этих моделей, объявляет собственный список. Обёртка родителя не объявляет
|
|
155
|
+
требования вложенной функции.
|
|
156
|
+
|
|
157
|
+
Поддерживаются named и namespace imports, aliases импортов и локальных значений, именованный контекст `provides`,
|
|
158
|
+
inline и именованные компоненты. Bindings сравниваются в своих лексических scope: посторонняя локальная функция
|
|
159
|
+
`useModel`, `requiresModels` или `slot` не становится API Opetope из-за совпадения имени. Named API из относительных
|
|
160
|
+
реэкспортов распознаётся по экспортированным именам без чтения другого файла.
|
|
161
|
+
|
|
162
|
+
**Граница.** Анализ ограничен одним модулем и следует видимым локальным объявлениям и возвратам фабрик.
|
|
163
|
+
Произвольные объекты с `Component` и `models` не считаются вкладом. Реализации импортированных компонентов,
|
|
164
|
+
динамические списки моделей и непрозрачные helpers остаются непроверенными: отсутствие диагностики не доказывает
|
|
165
|
+
полноту их требований. Ссылки на модели должны быть видимыми локальными идентификаторами или aliases; правило не
|
|
166
|
+
читает типы и не обходит отрендеренное React-дерево. Autofix отсутствует: выбор модели для чтения принадлежит
|
|
167
|
+
автору. Проверки типов и runtime authority продолжают действовать.
|
|
168
|
+
|
|
169
|
+
### `require-literal-id`
|
|
170
|
+
|
|
171
|
+
Объявление называет себя строкой, которая написана, а не собрана там, где стоит объявление. Правило читает каждое
|
|
172
|
+
объявление публичного словаря, которое называет себя: `defineFeature`, `defineApplication`, `defineCondition`,
|
|
173
|
+
`defineHostContract`, `defineModel`, `definePort`, `defineSlot`, `defineSwitchSlot`, `definePipe` и
|
|
174
|
+
`defineRegistry`. У `defineHostContract`, `defineModel` и `definePort` id — первый аргумент, у остальных — ключ
|
|
175
|
+
`id` в опциях.
|
|
176
|
+
|
|
177
|
+
Написана — это строковый литерал, шаблон без выражений, имя, которое разрешается в импорт или в `const` того же
|
|
178
|
+
модуля, и чтение поля у такого имени. Собрана — всё, что складывает место вызова: шаблон с выражением, конкатенация,
|
|
179
|
+
вызов функции или имя, которое разрешается в параметр.
|
|
180
|
+
|
|
181
|
+
Проект, который порождает набор объявлений по имени, говорит об этом один раз — называя такую фабрику:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
'opetope/require-literal-id': ['error', { allowInCallees: ['createSlots'] }],
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Имя сверяется и с функцией, внутри которой написано объявление, и с вызовом, в который оно передано, поэтому
|
|
188
|
+
покрыты обе формы фабрики.
|
|
189
|
+
|
|
190
|
+
**Граница.** Имена правило отслеживает только внутри одного модуля: id, импортированный из другого файла, написан
|
|
191
|
+
там, и там же проверяется его собственный текст. `defineModule`, `defineCallTarget` и `defineCallLane` из ядра оно
|
|
192
|
+
не читает: их автор не пишет.
|
|
193
|
+
|
|
194
|
+
### `id-naming`
|
|
195
|
+
|
|
196
|
+
Id начинается с буквы и соединяет сегменты из букв и цифр одним из `.`, `/`, `:` или `-`, не длиннее 160 символов.
|
|
197
|
+
Это грамматика, которую принимает `declarationId` в `@opetope/core`; id вне неё — это `DeclarationError` в момент
|
|
198
|
+
выполнения объявления, а правило говорит об этом до запуска кода.
|
|
199
|
+
|
|
200
|
+
Проект, который зарезервировал первый сегмент, называет его, и каждое объявление таких файлов обязано с него
|
|
201
|
+
начинаться:
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
'opetope/id-naming': ['error', { prefix: 'workspace' }],
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Граница.** Правило проверяет те id, чей текст видно в файле: литерал или имя, которое разрешается в литерал того
|
|
208
|
+
же модуля. Id, пришедший из другого модуля, проверяется там, где он написан.
|
|
209
|
+
|
|
210
|
+
### `layer-placement`
|
|
211
|
+
|
|
212
|
+
Каждое объявление написано в слое, который им владеет. Правило не сообщает ничего, пока проект не назвал свои слои
|
|
213
|
+
через `configs.layers`: имена каталогов не являются законом фреймворка.
|
|
214
|
+
|
|
215
|
+
| Объявление | Слой |
|
|
216
|
+
| ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
217
|
+
| `defineFeature` | integration — фича сочленяет два других слоя |
|
|
218
|
+
| `defineModel` | models или контракты UI, которые объявляют модель монтирования |
|
|
219
|
+
| `defineSlot`, `defineSwitchSlot`, `definePipe`, `defineRegistry`, `definePort`, `defineCondition`, `defineHostContract` | integration или контракты UI рядом с компонентом |
|
|
220
|
+
| `defineApplication` | ни один из них: им владеет bootstrap хоста |
|
|
221
|
+
|
|
222
|
+
**Это конвенция проекта, а не закон библиотеки.** Фреймворк говорит, что UI фичи импортирует только её собственные
|
|
223
|
+
контракты, а модель не знает о фиче; где лежат эти файлы — выбор проекта, и правило держит тот выбор, который он
|
|
224
|
+
объявил.
|
|
225
|
+
|
|
226
|
+
### `no-snapshot-read-in-render`
|
|
227
|
+
|
|
228
|
+
`getSnapshot()`, вызванный во время рендера компонента или хука, читает значение один раз и никогда не услышит
|
|
229
|
+
следующее. Читайте через `useReadable` или выбирайте вместе с командами той же модели:
|
|
230
|
+
`useModel(Declaration, (model, { read }) => ...)` (D205, D214).
|
|
231
|
+
|
|
232
|
+
Область рендера — это функция с именем `use…` либо функция с заглавной буквы, возвращающая элементы. Правило
|
|
233
|
+
смотрит на ту функцию, внутри которой написан вызов, поэтому снимок, прочитанный в обработчике события, эффекте
|
|
234
|
+
или любом другом вложенном колбэке, остаётся: они выполняются после рендера, и им нужно значение того момента.
|
|
235
|
+
|
|
236
|
+
**Граница.** Правило читает имя получателя, а не его тип: сообщается о каждом `getSnapshot()` в области рендера,
|
|
237
|
+
какому бы объекту он ни принадлежал. Функция с заглавной буквы, не возвращающая элементов, — это фабрика, и её
|
|
238
|
+
чтения остаются, включая фабрику модели вклада, которая читает props своего монтирования.
|
|
239
|
+
|
|
240
|
+
### `no-subscribe-outside-models`
|
|
241
|
+
|
|
242
|
+
Подписка, написанная руками, владеет уборкой, которой не видит ничто вокруг. Правило сообщает о `x.subscribe(...)`
|
|
243
|
+
и молчит, пока `configs.layers` не назвал слой моделей, которому такая подписка разрешена; в компоненте читатель —
|
|
244
|
+
это `useReadable` или выбор модели, а в фиче и в модели — `effect`, `event` или `stream`.
|
|
245
|
+
|
|
246
|
+
Передача функции без вызова подпиской здесь не является:
|
|
247
|
+
`useSyncExternalStore(source.subscribe, source.getSnapshot)` передаёт ссылку, и читатель владеет тем, что начал.
|
|
248
|
+
|
|
249
|
+
### `prefer-model-selection`
|
|
250
|
+
|
|
251
|
+
Компонент, который берёт одну выданную модель и читает её поля по хуку на поле, повторяет одно и то же
|
|
252
|
+
подключение. От трёх полей и выше правило показывает на один выбор вместо них (D205, D214):
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
'opetope/prefer-model-selection': ['error', { threshold: 3 }],
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Оно считает `useReadable(model.field)` и `useCommand(model.field)` над переменной из `useModel(Declaration)` без
|
|
259
|
+
выбора и считает каждую выданную модель отдельно: две модели в одном компоненте — это два гранта, а одно и то же
|
|
260
|
+
имя переменной в двух компонентах — два счёта. Фикса нет: форму выбора пишет автор.
|
|
261
|
+
|
|
262
|
+
## Coexistence with key sorting
|
|
263
|
+
|
|
264
|
+
Хост, который сортирует ключи объектов по алфавиту, разойдётся с `define-feature-property-order`: секции фичи
|
|
265
|
+
упорядочены по смыслу, а не по имени. `recommended` не трогает ни одного правила сортировки — примирять их дело
|
|
266
|
+
хоста, и способа два.
|
|
267
|
+
|
|
268
|
+
**`perfectionist/sort-objects`** принимает именованный порядок ровно для двух callee, которые объявляют секции, и
|
|
269
|
+
сохраняет обычную конфигурацию для всех остальных объектов:
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
'perfectionist/sort-objects': [
|
|
273
|
+
'error',
|
|
274
|
+
{
|
|
275
|
+
customGroups: [
|
|
276
|
+
{ elementNamePattern: '^id$', groupName: 'feature-id' },
|
|
277
|
+
{ elementNamePattern: '^imports$', groupName: 'feature-imports' },
|
|
278
|
+
{ elementNamePattern: '^requires$', groupName: 'feature-requires' },
|
|
279
|
+
{ elementNamePattern: '^own$', groupName: 'feature-own' },
|
|
280
|
+
{ elementNamePattern: '^exports$', groupName: 'feature-exports' },
|
|
281
|
+
{ elementNamePattern: '^provides$', groupName: 'feature-provides' },
|
|
282
|
+
{ elementNamePattern: '^when$', groupName: 'feature-when' },
|
|
283
|
+
{ elementNamePattern: '^body$', groupName: 'feature-body' },
|
|
284
|
+
],
|
|
285
|
+
groups: [
|
|
286
|
+
'feature-id',
|
|
287
|
+
'feature-imports',
|
|
288
|
+
'feature-requires',
|
|
289
|
+
'feature-own',
|
|
290
|
+
'feature-exports',
|
|
291
|
+
'feature-provides',
|
|
292
|
+
'feature-when',
|
|
293
|
+
'feature-body',
|
|
294
|
+
'unknown',
|
|
295
|
+
],
|
|
296
|
+
order: 'asc',
|
|
297
|
+
type: 'natural',
|
|
298
|
+
useConfigurationIf: { callingFunctionNamePattern: '^(?:defineFeature|defineFeature\\.body)$' },
|
|
299
|
+
},
|
|
300
|
+
{ order: 'asc', type: 'natural' },
|
|
301
|
+
],
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**ESLint core `sort-keys` и правило с тем же id в Oxlint** не настраиваются по callee и не знают исключения на
|
|
305
|
+
объявление: они сообщают о каждом ключе, который идёт после большего, в любом объекте. Выключите правило для
|
|
306
|
+
файлов, объявляющих фичи, или оберните объявление в `/* eslint-disable sort-keys */` и
|
|
307
|
+
`/* eslint-enable sort-keys */` — `// eslint-disable-next-line sort-keys` покрывает одну строку, то есть глушит
|
|
308
|
+
одну секцию, а не многострочное объявление. Oxlint читает те же директивы под своим префиксом,
|
|
309
|
+
`// oxlint-disable-next-line sort-keys`.
|
|
310
|
+
|
|
311
|
+
Фикс этого правила переставляет ключи только внутри объекта вызова `defineFeature`, поэтому он никогда не трогает
|
|
312
|
+
то, чем правило сортировки владеет в остальном файле.
|
|
313
|
+
|
|
314
|
+
## Running the rules under Oxlint
|
|
315
|
+
|
|
316
|
+
Oxlint читает ESLint-плагины через `jsPlugins`: импортирует модуль по пути и берёт его default export. Правила
|
|
317
|
+
остаются на чистом rule API — `meta`, `create(context)` с visitors, `context.report` с `fix`, `getText` и
|
|
318
|
+
`getCommentsInside` у source code — без typed services и без ESLint-only возможностей, поэтому их выполняют оба
|
|
319
|
+
линтера, вместе с автофиксом:
|
|
320
|
+
|
|
321
|
+
```json
|
|
322
|
+
{
|
|
323
|
+
"jsPlugins": [{ "name": "opetope", "specifier": "./node_modules/@opetope/lint/dist/index.js" }],
|
|
324
|
+
"rules": { "opetope/when-predicate": "error" }
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Алиас `name` фиксирует namespace, поэтому id правила одинаков в обоих линтерах. `jsPlugins` находится в alpha и вне
|
|
329
|
+
semver: `npm run ci:test` запускает собранный плагин под Oxlint на валидной и невалидной фикстуре и проверяет
|
|
330
|
+
фикс, который тот пишет, — поломка этого моста падает здесь, а не у хоста.
|
|
331
|
+
|
|
332
|
+
## Checks
|
|
333
|
+
|
|
334
|
+
Из этого пакета: `npm run ci:test`, `npm run ci:type`, `npm run ci:eslint`, `npm run build`. Тест моста Oxlint
|
|
335
|
+
читает `dist`, поэтому `npm run build` идёт первым.
|
package/dist/ast.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
type FunctionLike = TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression;
|
|
3
|
+
/** `slot` and `defineFeature.body` alike: the dotted name of the simple callee shapes an author writes. */
|
|
4
|
+
declare function calleeName(node: TSESTree.CallExpression): string | undefined;
|
|
5
|
+
/** The static name of a property or a destructured key; a computed or dynamic key has none. */
|
|
6
|
+
declare function propertyName(node: TSESTree.Node): string | undefined;
|
|
7
|
+
/** The property of an object literal under a static key. */
|
|
8
|
+
declare function findProperty(node: TSESTree.ObjectExpression, name: string): TSESTree.Property | undefined;
|
|
9
|
+
declare function patternKeys(pattern: TSESTree.ObjectPattern): ReadonlySet<string>;
|
|
10
|
+
/** The identifier at the root of `imports.platform.submitAllowed`. */
|
|
11
|
+
declare function memberRoot(node: TSESTree.MemberExpression): TSESTree.Node;
|
|
12
|
+
/** The first step of `context.imports.platform`, that is `imports`. */
|
|
13
|
+
declare function memberFirstStep(node: TSESTree.MemberExpression): string | undefined;
|
|
14
|
+
declare function isFunctionLike(node: TSESTree.Node): node is FunctionLike;
|
|
15
|
+
/** The name a function is written under: its own, or the binding it is assigned to. */
|
|
16
|
+
declare function functionName(node: TSESTree.Node): string | undefined;
|
|
17
|
+
export type { FunctionLike };
|
|
18
|
+
export { calleeName, findProperty, functionName, isFunctionLike, memberFirstStep, memberRoot, patternKeys, propertyName, };
|
package/dist/ast.js
ADDED
|
@@ -0,0 +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};
|
|
2
|
+
//# sourceMappingURL=ast.js.map
|
package/dist/ast.js.map
ADDED
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
type Declaration = {
|
|
3
|
+
readonly id: TSESTree.Node;
|
|
4
|
+
readonly name: string;
|
|
5
|
+
};
|
|
6
|
+
/** The name and the id expression of a declaration call, or `undefined` when the call declares nothing. */
|
|
7
|
+
declare function declarationOf(node: TSESTree.CallExpression): Declaration | undefined;
|
|
8
|
+
export { declarationOf };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{AST_NODE_TYPES as o}from"@typescript-eslint/utils";import{calleeName as d,findProperty as f}from"./ast.js";const r=new Map([["defineApplication","options"],["defineCondition","options"],["defineFeature","options"],["defineHostContract","argument"],["defineModel","argument"],["definePipe","options"],["definePort","argument"],["defineRegistry","options"],["defineSlot","options"],["defineSwitchSlot","options"]]);function u(e){const[n]=e.arguments;if(n?.type===o.ObjectExpression)return f(n,"id")?.value}function s(e){const[n]=e.arguments;if(!(n===void 0||n.type===o.SpreadElement))return n}function p(e){const n=d(e),i=n===void 0?void 0:r.get(n);if(n===void 0||i===void 0)return;const t=i==="options"?u(e):s(e);return t===void 0?void 0:{id:t,name:n}}export{p as declarationOf};
|
|
2
|
+
//# sourceMappingURL=declarations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"declarations.js","sources":["../src/declarations.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { calleeName, findProperty } from './ast';\n\n/** Where a declaration carries its id: as its first argument, or as the `id` key of its options. */\ntype IdLocation = 'argument' | 'options';\n\n/**\n * Every declaration of the public vocabulary that names itself, taken from the three safe entries: three of them\n * read the id as their first argument, the rest as the `id` key of their options. The kernel's own `defineModule`,\n * `defineCallTarget` and `defineCallLane` are absent because an author never writes them.\n */\nconst DECLARATIONS = new Map<string, IdLocation>([\n ['defineApplication', 'options'],\n ['defineCondition', 'options'],\n ['defineFeature', 'options'],\n ['defineHostContract', 'argument'],\n ['defineModel', 'argument'],\n ['definePipe', 'options'],\n ['definePort', 'argument'],\n ['defineRegistry', 'options'],\n ['defineSlot', 'options'],\n ['defineSwitchSlot', 'options'],\n]);\n\ntype Declaration = {\n readonly id: TSESTree.Node;\n readonly name: string;\n};\n\nfunction idOfOptions(node: TSESTree.CallExpression): TSESTree.Node | undefined {\n const [options] = node.arguments;\n\n if (options?.type !== AST_NODE_TYPES.ObjectExpression) return undefined;\n\n return findProperty(options, 'id')?.value;\n}\n\nfunction idOfArgument(node: TSESTree.CallExpression): TSESTree.Node | undefined {\n const [first] = node.arguments;\n\n if (first === undefined || first.type === AST_NODE_TYPES.SpreadElement) return undefined;\n\n return first;\n}\n\n/** The name and the id expression of a declaration call, or `undefined` when the call declares nothing. */\nfunction declarationOf(node: TSESTree.CallExpression): Declaration | undefined {\n const name = calleeName(node);\n const location = name === undefined ? undefined : DECLARATIONS.get(name);\n\n if (name === undefined || location === undefined) return undefined;\n\n const id = location === 'options' ? idOfOptions(node) : idOfArgument(node);\n\n return id === undefined ? undefined : { id, name };\n}\n\nexport { declarationOf };\n"],"names":["DECLARATIONS","idOfOptions","node","options","AST_NODE_TYPES","findProperty","idOfArgument","first","declarationOf","name","calleeName","location","id"],"mappings":"kHAaA,MAAMA,EAAe,IAAI,IAAwB,CAC/C,CAAC,oBAAqB,SAAS,EAC/B,CAAC,kBAAmB,SAAS,EAC7B,CAAC,gBAAiB,SAAS,EAC3B,CAAC,qBAAsB,UAAU,EACjC,CAAC,cAAe,UAAU,EAC1B,CAAC,aAAc,SAAS,EACxB,CAAC,aAAc,UAAU,EACzB,CAAC,iBAAkB,SAAS,EAC5B,CAAC,aAAc,SAAS,EACxB,CAAC,mBAAoB,SAAS,CAC/B,CAAA,EAOD,SAASC,EAAYC,EAA6B,CAChD,KAAM,CAACC,CAAO,EAAID,EAAK,UAEvB,GAAIC,GAAS,OAASC,EAAe,iBAErC,OAAOC,EAAaF,EAAS,IAAI,GAAG,KACtC,CAEA,SAASG,EAAaJ,EAA6B,CACjD,KAAM,CAACK,CAAK,EAAIL,EAAK,UAErB,GAAI,EAAAK,IAAU,QAAaA,EAAM,OAASH,EAAe,eAEzD,OAAOG,CACT,CAGA,SAASC,EAAcN,EAA6B,CAClD,MAAMO,EAAOC,EAAWR,CAAI,EACtBS,EAAWF,IAAS,OAAY,OAAYT,EAAa,IAAIS,CAAI,EAEvE,GAAIA,IAAS,QAAaE,IAAa,OAAW,OAElD,MAAMC,EAAKD,IAAa,UAAYV,EAAYC,CAAI,EAAII,EAAaJ,CAAI,EAEzE,OAAOU,IAAO,OAAY,OAAY,CAAE,GAAAA,EAAI,KAAAH,CAAI,CAClD"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { ESLint, Linter } from 'eslint';
|
|
2
|
+
/**
|
|
3
|
+
* The layers a host names for itself, as globs of the files that hold each one, with the tests that are allowed
|
|
4
|
+
* to reach past a layer. `tests` defaults to the usual two shapes of a test path.
|
|
5
|
+
*/
|
|
6
|
+
type LayerOptions = {
|
|
7
|
+
readonly integration?: readonly string[];
|
|
8
|
+
readonly models?: readonly string[];
|
|
9
|
+
readonly tests?: readonly string[];
|
|
10
|
+
readonly ui?: readonly string[];
|
|
11
|
+
};
|
|
12
|
+
type InternalImportOptions = {
|
|
13
|
+
readonly allow?: readonly string[];
|
|
14
|
+
readonly files?: readonly string[];
|
|
15
|
+
};
|
|
16
|
+
declare const plugin: {
|
|
17
|
+
meta: {
|
|
18
|
+
name: string;
|
|
19
|
+
version: string;
|
|
20
|
+
};
|
|
21
|
+
rules: {
|
|
22
|
+
'define-feature-property-order': import("@typescript-eslint/utils/ts-eslint").RuleModule<"sectionOrder", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
23
|
+
name: string;
|
|
24
|
+
};
|
|
25
|
+
'id-naming': import("@typescript-eslint/utils/ts-eslint").RuleModule<"invalidId" | "missingPrefix", readonly [{
|
|
26
|
+
readonly prefix?: string;
|
|
27
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
28
|
+
name: string;
|
|
29
|
+
};
|
|
30
|
+
'layer-placement': import("@typescript-eslint/utils/ts-eslint").RuleModule<"misplacedDeclaration", readonly [{
|
|
31
|
+
readonly layer?: "integration" | "models" | "ui";
|
|
32
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
33
|
+
name: string;
|
|
34
|
+
};
|
|
35
|
+
'no-snapshot-read-in-render': import("@typescript-eslint/utils/ts-eslint").RuleModule<"snapshotInRender", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
36
|
+
name: string;
|
|
37
|
+
};
|
|
38
|
+
'no-subscribe-outside-models': import("@typescript-eslint/utils/ts-eslint").RuleModule<"handWrittenSubscription", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
39
|
+
name: string;
|
|
40
|
+
};
|
|
41
|
+
'prefer-model-selection': import("@typescript-eslint/utils/ts-eslint").RuleModule<"separateHooks", readonly [{
|
|
42
|
+
readonly threshold: number;
|
|
43
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
44
|
+
name: string;
|
|
45
|
+
};
|
|
46
|
+
'require-declared-models': import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingWrapper" | "undeclaredUiModel", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
47
|
+
name: string;
|
|
48
|
+
};
|
|
49
|
+
'require-literal-id': import("@typescript-eslint/utils/ts-eslint").RuleModule<"computedId", readonly [{
|
|
50
|
+
readonly allowInCallees: readonly string[];
|
|
51
|
+
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
52
|
+
name: string;
|
|
53
|
+
};
|
|
54
|
+
'when-predicate': import("@typescript-eslint/utils/ts-eslint").RuleModule<"whenIsAsync" | "whenReturnsBareSource" | "whenReturnsSource", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
55
|
+
name: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The laws every Opetope author writes against, with no assumption about where a host keeps its files. A rule that
|
|
61
|
+
* needs such an assumption is off here and arrives through `layers`. The namespace is `opetope`, so a rule reads
|
|
62
|
+
* `opetope/when-predicate` wherever a host names it.
|
|
63
|
+
*/
|
|
64
|
+
type NativeRule = NonNullable<ESLint.Plugin['rules']>[string];
|
|
65
|
+
type NativeRules = {
|
|
66
|
+
[Name in keyof typeof plugin.rules]: Omit<(typeof plugin.rules)[Name], 'create'> & NativeRule;
|
|
67
|
+
};
|
|
68
|
+
declare const configuredPlugin: {
|
|
69
|
+
meta: typeof plugin.meta;
|
|
70
|
+
rules: NativeRules;
|
|
71
|
+
} & {
|
|
72
|
+
configs: {
|
|
73
|
+
internalImports: ({ allow, files, }?: InternalImportOptions) => readonly Linter.Config[];
|
|
74
|
+
layers: (options: LayerOptions) => readonly Linter.Config[];
|
|
75
|
+
recommended: {
|
|
76
|
+
name: string;
|
|
77
|
+
plugins: {
|
|
78
|
+
opetope: {
|
|
79
|
+
meta: typeof plugin.meta;
|
|
80
|
+
rules: NativeRules;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
rules: {
|
|
84
|
+
'opetope/define-feature-property-order': "error";
|
|
85
|
+
'opetope/id-naming': "error";
|
|
86
|
+
'opetope/layer-placement': "off";
|
|
87
|
+
'opetope/no-snapshot-read-in-render': "error";
|
|
88
|
+
'opetope/no-subscribe-outside-models': "off";
|
|
89
|
+
'opetope/prefer-model-selection': "off";
|
|
90
|
+
'opetope/require-declared-models': "error";
|
|
91
|
+
'opetope/require-literal-id': "error";
|
|
92
|
+
'opetope/when-predicate': "error";
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
internalImportPattern: {
|
|
97
|
+
group: string[];
|
|
98
|
+
message: string;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
declare const opetopeLint: typeof configuredPlugin & Pick<ESLint.Plugin, "configs">;
|
|
102
|
+
export default opetopeLint;
|
|
103
|
+
export { opetopeLint };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{defineFeaturePropertyOrder as p}from"./rules/define-feature-property-order.js";import{idNaming as a}from"./rules/id-naming.js";import{layerPlacement as l}from"./rules/layer-placement.js";import{noSnapshotReadInRender as d}from"./rules/no-snapshot-read-in-render.js";import{noSubscribeOutsideModels as m}from"./rules/no-subscribe-outside-models.js";import{preferModelSelection as c}from"./rules/prefer-model-selection.js";import{requireDeclaredModels as u}from"./rules/require-declared-models.js";import{requireLiteralId as f}from"./rules/require-literal-id.js";import{whenPredicate as g}from"./rules/when-predicate.js";const b=["integration","models","ui"],n=["**/*.ts","**/*.tsx"],h=["**/__tests__/**","**/*.spec.ts","**/*.spec.tsx"],y={meta:{name:"@opetope/lint",version:"0.1.0"},rules:{"define-feature-property-order":p,"id-naming":a,"layer-placement":l,"no-snapshot-read-in-render":d,"no-subscribe-outside-models":m,"prefer-model-selection":c,"require-declared-models":u,"require-literal-id":f,"when-predicate":g}},o=y,I={name:"opetope/recommended",plugins:{opetope:o},rules:{"opetope/define-feature-property-order":"error","opetope/id-naming":"error","opetope/layer-placement":"off","opetope/no-snapshot-read-in-render":"error","opetope/no-subscribe-outside-models":"off","opetope/prefer-model-selection":"off","opetope/require-declared-models":"error","opetope/require-literal-id":"error","opetope/when-predicate":"error"}},P=e=>b.flatMap(r=>{const t=e[r]??[];return t.length===0?[]:[{files:[...t],name:`opetope/layers-${r}`,plugins:{opetope:o},rules:{"opetope/layer-placement":["error",{layer:r}]}}]}),S=({models:e=[],tests:r=h})=>e.length===0?[]:[{files:n,name:"opetope/layers-subscriptions",plugins:{opetope:o},rules:{"opetope/no-subscribe-outside-models":"error"}},{files:[...e,...r],name:"opetope/layers-subscriptions-allowed",rules:{"opetope/no-subscribe-outside-models":"off"}}],_=e=>[...P(e),...S(e)],s={group:["@opetope/*/internal"],message:"The internal entries carry the runtime and its host integration. Feature UI, models and data layers import the safe entries."},E=({allow:e=[],files:r=n}={})=>[{files:[...r],...e.length===0?{}:{ignores:[...e]},name:"opetope/internal-imports",rules:{"no-restricted-imports":["error",{patterns:[s]}]}}],R=Object.assign(o,{configs:{internalImports:E,layers:_,recommended:I},internalImportPattern:s}),i=R;export{i as default,i as opetopeLint};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import type { ESLint, Linter } from 'eslint';\n\nimport { defineFeaturePropertyOrder } from './rules/define-feature-property-order';\nimport { idNaming } from './rules/id-naming';\nimport { layerPlacement } from './rules/layer-placement';\nimport { noSnapshotReadInRender } from './rules/no-snapshot-read-in-render';\nimport { noSubscribeOutsideModels } from './rules/no-subscribe-outside-models';\nimport { preferModelSelection } from './rules/prefer-model-selection';\nimport { requireDeclaredModels } from './rules/require-declared-models';\nimport { requireLiteralId } from './rules/require-literal-id';\nimport { whenPredicate } from './rules/when-predicate';\n\n/**\n * The layers a host names for itself, as globs of the files that hold each one, with the tests that are allowed\n * to reach past a layer. `tests` defaults to the usual two shapes of a test path.\n */\ntype LayerOptions = {\n readonly integration?: readonly string[];\n readonly models?: readonly string[];\n readonly tests?: readonly string[];\n readonly ui?: readonly string[];\n};\n\ntype InternalImportOptions = {\n readonly allow?: readonly string[];\n readonly files?: readonly string[];\n};\n\nconst LAYERS = ['integration', 'models', 'ui'] as const;\nconst SOURCE_FILES = ['**/*.ts', '**/*.tsx'];\nconst TEST_FILES = ['**/__tests__/**', '**/*.spec.ts', '**/*.spec.tsx'];\n\nconst plugin = {\n meta: { name: '@opetope/lint', version: '0.1.0' },\n rules: {\n 'define-feature-property-order': defineFeaturePropertyOrder,\n 'id-naming': idNaming,\n 'layer-placement': layerPlacement,\n 'no-snapshot-read-in-render': noSnapshotReadInRender,\n 'no-subscribe-outside-models': noSubscribeOutsideModels,\n 'prefer-model-selection': preferModelSelection,\n 'require-declared-models': requireDeclaredModels,\n 'require-literal-id': requireLiteralId,\n 'when-predicate': whenPredicate,\n },\n};\n\n/**\n * The laws every Opetope author writes against, with no assumption about where a host keeps its files. A rule that\n * needs such an assumption is off here and arrives through `layers`. The namespace is `opetope`, so a rule reads\n * `opetope/when-predicate` wherever a host names it.\n */\n// Rules use the parser's typed AST internally. The public bridge exposes the\n// supported ESLint rule API; package smoke tests execute it under both linters.\ntype NativeRule = NonNullable<ESLint.Plugin['rules']>[string];\ntype NativeRules = {\n [Name in keyof typeof plugin.rules]: Omit<(typeof plugin.rules)[Name], 'create'> & NativeRule;\n};\nconst nativePlugin = plugin as unknown as { meta: typeof plugin.meta; rules: NativeRules };\n\nconst recommended = {\n name: 'opetope/recommended',\n plugins: { opetope: nativePlugin },\n rules: {\n 'opetope/define-feature-property-order': 'error',\n 'opetope/id-naming': 'error',\n 'opetope/layer-placement': 'off',\n 'opetope/no-snapshot-read-in-render': 'error',\n 'opetope/no-subscribe-outside-models': 'off',\n 'opetope/prefer-model-selection': 'off',\n 'opetope/require-declared-models': 'error',\n 'opetope/require-literal-id': 'error',\n 'opetope/when-predicate': 'error',\n },\n} satisfies Linter.Config;\n\n/**\n * Placement is a convention of the project that names its layers, not a law of the framework: a layer without a\n * glob is a layer this host does not have, and it produces no configuration.\n */\nconst placement = (options: LayerOptions): readonly Linter.Config[] =>\n LAYERS.flatMap(layer => {\n const files = options[layer] ?? [];\n\n if (files.length === 0) return [];\n\n return [\n {\n files: [...files],\n name: `opetope/layers-${layer}`,\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/layer-placement': ['error', { layer }] },\n } satisfies Linter.Config,\n ];\n });\n\n/**\n * A subscription is written where the state lives, so this pair arrives only with the model layer that owns it. The\n * restriction covers every source file and steps aside in that layer and in tests: a file no layer claims is\n * covered rather than forgotten.\n */\nconst subscriptions = ({ models = [], tests = TEST_FILES }: LayerOptions): readonly Linter.Config[] =>\n models.length === 0\n ? []\n : [\n {\n files: SOURCE_FILES,\n name: 'opetope/layers-subscriptions',\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/no-subscribe-outside-models': 'error' },\n },\n {\n files: [...models, ...tests],\n name: 'opetope/layers-subscriptions-allowed',\n rules: { 'opetope/no-subscribe-outside-models': 'off' },\n },\n ];\n\nconst layers = (options: LayerOptions): readonly Linter.Config[] => [...placement(options), ...subscriptions(options)];\n\n/**\n * The one restriction this package expresses through a core rule rather than a rule of its own, so a project that\n * already configures `no-restricted-imports` adds this entry to its own list instead of losing it.\n */\nconst INTERNAL_IMPORT_PATTERN = {\n group: ['@opetope/*/internal'],\n message:\n 'The internal entries carry the runtime and its host integration. Feature UI, models and data layers import the safe entries.',\n};\n\n/**\n * The internal entries are for runtime implementation and host integration. `allow` names the files that do that\n * work; the restriction simply does not reach them, so whatever else a project restricts there stays in force.\n *\n * `no-restricted-imports` is a core rule with one value per file: in a flat config the last entry that names it\n * wins. A project that already configures it takes `internalImportPattern` into that entry instead of spreading\n * this config, which would replace the entry for every file it covers.\n */\nconst internalImports = ({\n allow = [],\n files = SOURCE_FILES,\n}: InternalImportOptions = {}): readonly Linter.Config[] => [\n {\n files: [...files],\n ...(allow.length === 0 ? {} : { ignores: [...allow] }),\n name: 'opetope/internal-imports',\n rules: { 'no-restricted-imports': ['error', { patterns: [INTERNAL_IMPORT_PATTERN] }] },\n },\n];\n\nconst configuredPlugin = Object.assign(nativePlugin, {\n configs: { internalImports, layers, recommended },\n internalImportPattern: INTERNAL_IMPORT_PATTERN,\n});\n\n// ESLint consumes the rule map. Config factories are additional plugin helpers;\n// expose both contracts without requiring casts at native flat-config call sites.\n// oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- Required by native ESLint config typetests.\nconst opetopeLint = configuredPlugin as unknown as typeof configuredPlugin & Pick<ESLint.Plugin, 'configs'>;\n\n/*\n * Two shapes of one object. The default is what a tool reads when it loads a plugin by path — Oxlint's `jsPlugins`\n * takes `(await import(path)).default` — and what an ESLint flat config imports by convention; the named export is\n * for a config that prefers to say which binding it takes.\n */\n// oxlint-disable-next-line import/no-default-export\n// eslint-disable-next-line import-x/no-default-export\nexport default opetopeLint;\nexport { opetopeLint };\n"],"names":["LAYERS","SOURCE_FILES","TEST_FILES","plugin","defineFeaturePropertyOrder","idNaming","layerPlacement","noSnapshotReadInRender","noSubscribeOutsideModels","preferModelSelection","requireDeclaredModels","requireLiteralId","whenPredicate","nativePlugin","recommended","placement","options","layer","files","subscriptions","models","tests","layers","INTERNAL_IMPORT_PATTERN","internalImports","allow","configuredPlugin","opetopeLint"],"mappings":"knBA4BA,MAAMA,EAAS,CAAC,cAAe,SAAU,IAAI,EACvCC,EAAe,CAAC,UAAW,UAAU,EACrCC,EAAa,CAAC,kBAAmB,eAAgB,eAAe,EAEhEC,EAAS,CACb,KAAM,CAAE,KAAM,gBAAiB,QAAS,OAAO,EAC/C,MAAO,CACL,gCAAiCC,EACjC,YAAaC,EACb,kBAAmBC,EACnB,6BAA8BC,EAC9B,8BAA+BC,EAC/B,yBAA0BC,EAC1B,0BAA2BC,EAC3B,qBAAsBC,EACtB,iBAAkBC,CACnB,GAcGC,EAAeV,EAEfW,EAAc,CAClB,KAAM,sBACN,QAAS,CAAE,QAASD,CAAY,EAChC,MAAO,CACL,wCAAyC,QACzC,oBAAqB,QACrB,0BAA2B,MAC3B,qCAAsC,QACtC,sCAAuC,MACvC,iCAAkC,MAClC,kCAAmC,QACnC,6BAA8B,QAC9B,yBAA0B,OAC3B,GAOGE,EAAaC,GACjBhB,EAAO,QAAQiB,GAAQ,CACrB,MAAMC,EAAQF,EAAQC,CAAK,GAAK,CAAA,EAEhC,OAAIC,EAAM,SAAW,EAAU,CAAA,EAExB,CACL,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,kBAAkBD,CAAK,GAC7B,QAAS,CAAE,QAASJ,CAAY,EAChC,MAAO,CAAE,0BAA2B,CAAC,QAAS,CAAE,MAAAI,CAAK,CAAE,CAAC,CACjC,EAE7B,CAAC,EAOGE,EAAgB,CAAC,CAAE,OAAAC,EAAS,CAAA,EAAI,MAAAC,EAAQnB,CAAU,IACtDkB,EAAO,SAAW,EACd,CAAA,EACA,CACE,CACE,MAAOnB,EACP,KAAM,+BACN,QAAS,CAAE,QAASY,CAAY,EAChC,MAAO,CAAE,sCAAuC,OAAO,CACxD,EACD,CACE,MAAO,CAAC,GAAGO,EAAQ,GAAGC,CAAK,EAC3B,KAAM,uCACN,MAAO,CAAE,sCAAuC,KAAK,CACtD,GAGHC,EAAUN,GAAoD,CAAC,GAAGD,EAAUC,CAAO,EAAG,GAAGG,EAAcH,CAAO,CAAC,EAM/GO,EAA0B,CAC9B,MAAO,CAAC,qBAAqB,EAC7B,QACE,gIAWEC,EAAkB,CAAC,CACvB,MAAAC,EAAQ,CAAA,EACR,MAAAP,EAAQjB,CAAY,EACK,KAAiC,CAC1D,CACE,MAAO,CAAC,GAAGiB,CAAK,EAChB,GAAIO,EAAM,SAAW,EAAI,CAAA,EAAK,CAAE,QAAS,CAAC,GAAGA,CAAK,GAClD,KAAM,2BACN,MAAO,CAAE,wBAAyB,CAAC,QAAS,CAAE,SAAU,CAACF,CAAuB,CAAC,CAAE,CAAC,CACrF,GAGGG,EAAmB,OAAO,OAAOb,EAAc,CACnD,QAAS,CAAE,gBAAAW,EAAiB,OAAAF,EAAQ,YAAAR,CAAW,EAC/C,sBAAuBS,CACxB,CAAA,EAKKI,EAAcD"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
3
|
+
type FunctionNode = TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration | TSESTree.FunctionExpression;
|
|
4
|
+
type ModelBinding = TSESLint.Scope.Variable | string;
|
|
5
|
+
declare function isFunction(node: TSESTree.Node): node is FunctionNode;
|
|
6
|
+
declare function unwrap(node: TSESTree.Node): TSESTree.Node;
|
|
7
|
+
/** Local values only: an imported component or a parameter has no inspectable implementation here. */
|
|
8
|
+
declare function localValue(source: TSESLint.SourceCode, node: TSESTree.Node, seen?: Set<TSESTree.Node>): TSESTree.Node;
|
|
9
|
+
declare function isApi(source: TSESLint.SourceCode, node: TSESTree.Node, module: string, name: string): boolean;
|
|
10
|
+
declare function modelBinding(source: TSESLint.SourceCode, node: TSESTree.Node, seen?: Set<TSESTree.Node>): ModelBinding | undefined;
|
|
11
|
+
declare function isSlot(source: TSESLint.SourceCode, node: TSESTree.Node): boolean;
|
|
12
|
+
declare function enclosingFunction(source: TSESLint.SourceCode, node: TSESTree.Node): FunctionNode | undefined;
|
|
13
|
+
export type { FunctionNode, ModelBinding };
|
|
14
|
+
export { enclosingFunction, isApi, isFunction, isSlot, localValue, modelBinding, unwrap };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{AST_NODE_TYPES as r,TSESLint as u,ASTUtils as E}from"@typescript-eslint/utils";import{propertyName as d}from"./ast.js";function s(e){return e.type===r.ArrowFunctionExpression||e.type===r.FunctionDeclaration||e.type===r.FunctionExpression}function a(e){return e.type===r.TSAsExpression||e.type===r.TSSatisfiesExpression||e.type===r.TSNonNullExpression?a(e.expression):e}function m(e,n){return E.findVariable(e.getScope(n),n)}function f(e,n){return m(e,n)?.defs[0]}function y(e){if(e?.type===u.Scope.DefinitionType.FunctionName)return e.node;if(e?.type===u.Scope.DefinitionType.Variable)return e.node.init??void 0}function c(e,n,t=new Set){const i=a(n);if(i.type!==r.Identifier||t.has(i))return i;t.add(i);const p=y(f(e,i));return p===void 0?i:c(e,p,t)}function l(e){return e.imported.type===r.Identifier?e.imported.name:e.imported.value}function S(e,n){return e.type===r.ImportDeclaration&&(e.source.value===n||e.source.value.startsWith("."))}function x(e,n,t,i){if(n.type!==r.Identifier)return!1;const p=f(e,n);return p?.type!==u.Scope.DefinitionType.ImportBinding||!S(p.parent,t)?!1:p.node.type===r.ImportSpecifier&&l(p.node)===i}function I(e,n){if(!(e.type!==r.MemberExpression||e.computed||e.property.type!==r.Identifier||e.property.name!==n))return e.object.type===r.Identifier?e.object:void 0}function D(e,n,t,i){const p=I(n,i);if(p===void 0)return!1;const o=f(e,p);return o?.type===u.Scope.DefinitionType.ImportBinding&&S(o.parent,t)&&o.node.type===r.ImportNamespaceSpecifier}function b(e,n,t,i){const p=c(e,n);return x(e,p,t,i)||D(e,p,t,i)}function F(e){if(!(e?.type!==u.Scope.DefinitionType.ImportBinding||e.node.type!==r.ImportSpecifier||e.parent.type!==r.ImportDeclaration))return`import:${e.parent.source.value}:${l(e.node)}`}function g(e,n){return F(f(e,n))??m(e,n)??`unbound:${n.name}`}function v(e,n,t=new Set){const i=a(n);if(i.type!==r.Identifier)return;const p=f(e,i),o=y(p);return o?.type===r.Identifier&&!t.has(i)?(t.add(i),v(e,o,t)):g(e,i)}function j(e,n){if(n.type!==r.CallExpression)return!1;let t=n.callee;return t.type===r.MemberExpression&&!t.computed&&t.property.type===r.Identifier&&t.property.name==="body"&&(t=t.object),b(e,t,"@opetope/runtime","defineFeature")}function A(e,n){const t=n.parent;return t.type===r.Property&&d(t)==="provides"&&t.parent.type===r.ObjectExpression&&j(e,t.parent.parent)}function T(e,n){const t=f(e,n);if(!(t?.type!==u.Scope.DefinitionType.Parameter||!s(t.node)||!A(e,t.node)))return t.node}function P(e,n){const t=T(e,n)?.params[0];return t?.type!==r.ObjectPattern?!1:t.properties.some(i=>i.type===r.Property&&d(i)==="slot"&&i.value.type===r.Identifier&&i.value.name===n.name)}function N(e,n){const t=I(n,"slot");if(t===void 0)return!1;const p=T(e,t)?.params[0];return p?.type===r.Identifier&&p.name===t.name}function O(e,n){const t=c(e,n);return t.type===r.Identifier?P(e,t):N(e,t)}function B(e,n){return e.getAncestors(n).reverse().find(s)}export{B as enclosingFunction,b as isApi,s as isFunction,O as isSlot,c as localValue,v as modelBinding,a as unwrap};
|
|
2
|
+
//# sourceMappingURL=model-bindings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-bindings.js","sources":["../src/model-bindings.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES, ASTUtils, TSESLint } from '@typescript-eslint/utils';\n\nimport { propertyName } from './ast';\n\ntype FunctionNode = TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration | TSESTree.FunctionExpression;\ntype ModelBinding = TSESLint.Scope.Variable | string;\n\nfunction isFunction(node: TSESTree.Node): node is FunctionNode {\n return (\n node.type === AST_NODE_TYPES.ArrowFunctionExpression ||\n node.type === AST_NODE_TYPES.FunctionDeclaration ||\n node.type === AST_NODE_TYPES.FunctionExpression\n );\n}\n\nfunction unwrap(node: TSESTree.Node): TSESTree.Node {\n if (\n node.type === AST_NODE_TYPES.TSAsExpression ||\n node.type === AST_NODE_TYPES.TSSatisfiesExpression ||\n node.type === AST_NODE_TYPES.TSNonNullExpression\n )\n return unwrap(node.expression);\n\n return node;\n}\n\nfunction variableOf(source: TSESLint.SourceCode, node: TSESTree.Identifier): TSESLint.Scope.Variable | null {\n return ASTUtils.findVariable(source.getScope(node), node);\n}\n\nfunction definitionOf(source: TSESLint.SourceCode, node: TSESTree.Identifier): TSESLint.Scope.Definition | undefined {\n return variableOf(source, node)?.defs[0];\n}\n\nfunction definedValue(definition: TSESLint.Scope.Definition | undefined): TSESTree.Node | undefined {\n if (definition?.type === TSESLint.Scope.DefinitionType.FunctionName) return definition.node;\n if (definition?.type === TSESLint.Scope.DefinitionType.Variable) return definition.node.init ?? undefined;\n return undefined;\n}\n\n/** Local values only: an imported component or a parameter has no inspectable implementation here. */\nfunction localValue(source: TSESLint.SourceCode, node: TSESTree.Node, seen = new Set<TSESTree.Node>()): TSESTree.Node {\n const value = unwrap(node);\n\n if (value.type !== AST_NODE_TYPES.Identifier || seen.has(value)) return value;\n\n seen.add(value);\n const defined = definedValue(definitionOf(source, value));\n\n return defined === undefined ? value : localValue(source, defined, seen);\n}\n\nfunction importName(node: TSESTree.ImportSpecifier): string {\n return node.imported.type === AST_NODE_TYPES.Identifier ? node.imported.name : node.imported.value;\n}\n\n/** Relative re-exports are useful inside a package; unrelated external imports never match. */\nfunction importsFrom(node: TSESTree.Node, module: string): boolean {\n return (\n node.type === AST_NODE_TYPES.ImportDeclaration &&\n (node.source.value === module || node.source.value.startsWith('.'))\n );\n}\n\nfunction importedApi(source: TSESLint.SourceCode, node: TSESTree.Node, module: string, name: string): boolean {\n if (node.type !== AST_NODE_TYPES.Identifier) return false;\n\n const definition = definitionOf(source, node);\n\n if (definition?.type !== TSESLint.Scope.DefinitionType.ImportBinding || !importsFrom(definition.parent, module))\n return false;\n\n return definition.node.type === AST_NODE_TYPES.ImportSpecifier && importName(definition.node) === name;\n}\n\nfunction memberObject(node: TSESTree.Node, name: string): TSESTree.Identifier | undefined {\n if (\n node.type !== AST_NODE_TYPES.MemberExpression ||\n node.computed ||\n node.property.type !== AST_NODE_TYPES.Identifier ||\n node.property.name !== name\n )\n return undefined;\n return node.object.type === AST_NODE_TYPES.Identifier ? node.object : undefined;\n}\n\nfunction namespaceApi(source: TSESLint.SourceCode, node: TSESTree.Node, module: string, name: string): boolean {\n const object = memberObject(node, name);\n if (object === undefined) return false;\n const definition = definitionOf(source, object);\n return (\n definition?.type === TSESLint.Scope.DefinitionType.ImportBinding &&\n importsFrom(definition.parent, module) &&\n definition.node.type === AST_NODE_TYPES.ImportNamespaceSpecifier\n );\n}\n\nfunction isApi(source: TSESLint.SourceCode, node: TSESTree.Node, module: string, name: string): boolean {\n const value = localValue(source, node);\n\n return importedApi(source, value, module, name) || namespaceApi(source, value, module, name);\n}\n\nfunction importedModel(definition: TSESLint.Scope.Definition | undefined): string | undefined {\n if (\n definition?.type !== TSESLint.Scope.DefinitionType.ImportBinding ||\n definition.node.type !== AST_NODE_TYPES.ImportSpecifier ||\n definition.parent.type !== AST_NODE_TYPES.ImportDeclaration\n )\n return undefined;\n return `import:${definition.parent.source.value}:${importName(definition.node)}`;\n}\n\n/** Identity, not spelling: equal names in different lexical scopes are different model declarations. */\nfunction directModelBinding(source: TSESLint.SourceCode, identity: TSESTree.Identifier): ModelBinding {\n return importedModel(definitionOf(source, identity)) ?? variableOf(source, identity) ?? `unbound:${identity.name}`;\n}\n\nfunction modelBinding(\n source: TSESLint.SourceCode,\n node: TSESTree.Node,\n seen = new Set<TSESTree.Node>(),\n): ModelBinding | undefined {\n const identity = unwrap(node);\n if (identity.type !== AST_NODE_TYPES.Identifier) return undefined;\n const definition = definitionOf(source, identity);\n const defined = definedValue(definition);\n if (defined?.type === AST_NODE_TYPES.Identifier && !seen.has(identity)) {\n seen.add(identity);\n return modelBinding(source, defined, seen);\n }\n return directModelBinding(source, identity);\n}\n\nfunction isFeatureCall(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n if (node.type !== AST_NODE_TYPES.CallExpression) return false;\n\n let callee: TSESTree.Node = node.callee;\n\n if (\n callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.property.type === AST_NODE_TYPES.Identifier &&\n callee.property.name === 'body'\n )\n callee = callee.object;\n\n return isApi(source, callee, '@opetope/runtime', 'defineFeature');\n}\n\nfunction isProvides(source: TSESLint.SourceCode, fn: FunctionNode): boolean {\n const property = fn.parent;\n\n return (\n property.type === AST_NODE_TYPES.Property &&\n propertyName(property) === 'provides' &&\n property.parent.type === AST_NODE_TYPES.ObjectExpression &&\n isFeatureCall(source, property.parent.parent)\n );\n}\n\nfunction providesParameter(source: TSESLint.SourceCode, node: TSESTree.Identifier): FunctionNode | undefined {\n const definition = definitionOf(source, node);\n if (\n definition?.type !== TSESLint.Scope.DefinitionType.Parameter ||\n !isFunction(definition.node) ||\n !isProvides(source, definition.node)\n )\n return undefined;\n return definition.node;\n}\n\nfunction slotParameter(source: TSESLint.SourceCode, node: TSESTree.Identifier): boolean {\n const pattern = providesParameter(source, node)?.params[0];\n if (pattern?.type !== AST_NODE_TYPES.ObjectPattern) return false;\n return pattern.properties.some(\n property =>\n property.type === AST_NODE_TYPES.Property &&\n propertyName(property) === 'slot' &&\n property.value.type === AST_NODE_TYPES.Identifier &&\n property.value.name === node.name,\n );\n}\n\nfunction namedSlot(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n const object = memberObject(node, 'slot');\n if (object === undefined) return false;\n const fn = providesParameter(source, object);\n const parameter = fn?.params[0];\n return parameter?.type === AST_NODE_TYPES.Identifier && parameter.name === object.name;\n}\n\nfunction isSlot(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n const value = localValue(source, node);\n\n return value.type === AST_NODE_TYPES.Identifier ? slotParameter(source, value) : namedSlot(source, value);\n}\n\nfunction enclosingFunction(source: TSESLint.SourceCode, node: TSESTree.Node): FunctionNode | undefined {\n return source.getAncestors(node).reverse().find(isFunction);\n}\n\nexport type { FunctionNode, ModelBinding };\nexport { enclosingFunction, isApi, isFunction, isSlot, localValue, modelBinding, unwrap };\n"],"names":["isFunction","node","AST_NODE_TYPES","unwrap","variableOf","source","ASTUtils","definitionOf","definedValue","definition","TSESLint","localValue","seen","value","defined","importName","importsFrom","module","importedApi","name","memberObject","namespaceApi","object","isApi","importedModel","directModelBinding","identity","modelBinding","isFeatureCall","callee","isProvides","fn","property","propertyName","providesParameter","slotParameter","pattern","namedSlot","parameter","isSlot","enclosingFunction"],"mappings":"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"}
|
package/dist/rule.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Every rule documents itself in the package README; the anchor is the rule name, so a report always names the
|
|
4
|
+
* section that explains the law behind it.
|
|
5
|
+
*/
|
|
6
|
+
declare const createRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
9
|
+
export { createRule };
|
package/dist/rule.js
ADDED