@gost-kit/eslint-plugin 0.0.0-stage → 0.2.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 +35 -0
- package/LICENSE +21 -0
- package/README.md +52 -2
- package/RULES.md +312 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15 -0
- package/dist/lib/boolean-type.d.ts +2 -0
- package/dist/lib/boolean-type.js +14 -0
- package/dist/lib/common-source.d.ts +2 -0
- package/dist/lib/common-source.js +39 -0
- package/dist/lib/component-refs.d.ts +5 -0
- package/dist/lib/component-refs.js +58 -0
- package/dist/lib/dispatch-type.d.ts +2 -0
- package/dist/lib/dispatch-type.js +103 -0
- package/dist/lib/handler-body.d.ts +2 -0
- package/dist/lib/handler-body.js +32 -0
- package/dist/lib/member-key.d.ts +4 -0
- package/dist/lib/member-key.js +18 -0
- package/dist/lib/optional-call.d.ts +2 -0
- package/dist/lib/optional-call.js +55 -0
- package/dist/lib/raw-state.d.ts +2 -0
- package/dist/lib/raw-state.js +27 -0
- package/dist/lib/reactive-context.d.ts +6 -0
- package/dist/lib/reactive-context.js +48 -0
- package/dist/lib/ready-boolean-expression.d.ts +2 -0
- package/dist/lib/ready-boolean-expression.js +17 -0
- package/dist/lib/rune-callee.d.ts +2 -0
- package/dist/lib/rune-callee.js +19 -0
- package/dist/lib/selector-fix.d.ts +2 -0
- package/dist/lib/selector-fix.js +122 -0
- package/dist/lib/store-state.d.ts +2 -0
- package/dist/lib/store-state.js +57 -0
- package/dist/lib/svelte-scripts.d.ts +5 -0
- package/dist/lib/svelte-scripts.js +25 -0
- package/dist/lib/ui-handlers.d.ts +10 -0
- package/dist/lib/ui-handlers.js +84 -0
- package/dist/lib/value-variable.d.ts +2 -0
- package/dist/lib/value-variable.js +25 -0
- package/dist/rules/no-component-state.d.ts +2 -0
- package/dist/rules/no-component-state.js +46 -0
- package/dist/rules/ready-boolean-condition.d.ts +2 -0
- package/dist/rules/ready-boolean-condition.js +49 -0
- package/dist/rules/require-feature-state-selector.d.ts +2 -0
- package/dist/rules/require-feature-state-selector.js +58 -0
- package/dist/rules/single-intent-dispatch.d.ts +2 -0
- package/dist/rules/single-intent-dispatch.js +89 -0
- package/package.json +55 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @gost-kit/eslint-plugin
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9f59da3: Add the opt-in ready-boolean-condition rule for Svelte if/else-if conditions.
|
|
8
|
+
Accept ready boolean reads and literals, report computations/truthiness, and
|
|
9
|
+
require typed parser services with strictNullChecks for reads. The Svelte config
|
|
10
|
+
registers the rule off; projects select business/UI-Kit scopes explicitly.
|
|
11
|
+
|
|
12
|
+
## 0.1.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- 11cc6af: Add opt-in no-component-state diagnostics for state and bindable rune creation,
|
|
17
|
+
with binding-aware empty bind:this ref exceptions and no autofix. Keep the
|
|
18
|
+
public Svelte factory policy unchanged; consumers select application and UI-Kit
|
|
19
|
+
scopes with explicit ESLint overrides.
|
|
20
|
+
|
|
21
|
+
Reuse value-namespace rune resolution in the existing selector rule so type-only
|
|
22
|
+
names/imports do not hide derived or raw-state diagnostics. Consolidate rune and
|
|
23
|
+
Svelte script matching; document explicit plugin registration for extra JS scopes.
|
|
24
|
+
|
|
25
|
+
- d6883b7: Add the typed require-feature-state-selector architecture rule for GOST Store
|
|
26
|
+
and explicit local Svelte raw state. Enable it in typed Svelte config scopes,
|
|
27
|
+
with conservative selector autofix and V2/V3 collision-safe names.
|
|
28
|
+
- 730b73a: Add opt-in single-intent-dispatch for typed business UI callbacks, using public
|
|
29
|
+
Dispatch declaration/signature provenance. Diagnose multiple dispatches or
|
|
30
|
+
conditional intent selection without autofix. Keep the shared Svelte factory
|
|
31
|
+
default off and allow consumer UI-Kit exclusions through ordinary overrides.
|
|
32
|
+
|
|
33
|
+
Include optional calls/accesses that gate Dispatch in conditional-delivery
|
|
34
|
+
diagnostics, without treating unrelated optional data reads as dispatch gates.
|
|
35
|
+
Document the on-prefix and whole-handler syntactic-policy boundaries explicitly.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anton Smolko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gost-kit/eslint-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
GOST architecture rules for frontend development. Independently versioned;
|
|
4
|
+
see package.json and the release changelog. Requires Node ^26.0.0,
|
|
5
|
+
ESLint ^10.14.0 and TypeScript ^5.9.3.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
`@gost-kit/eslint-config-svelte` installs and enables the plugin for typed Svelte
|
|
10
|
+
components and `.svelte.ts` modules. The selector rule is enabled by default;
|
|
11
|
+
`gost/no-component-state`, `gost/single-intent-dispatch` and
|
|
12
|
+
`gost/ready-boolean-condition` require explicit
|
|
13
|
+
consumer overrides for business component paths. UI-Kit paths can opt out.
|
|
14
|
+
No separate plugin registration is needed.
|
|
15
|
+
Other consumers may register the named public export in their flat config:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { plugin } from '@gost-kit/eslint-plugin';
|
|
19
|
+
|
|
20
|
+
export default [
|
|
21
|
+
// Configure a typed parser and a consumer TypeScript project first.
|
|
22
|
+
{
|
|
23
|
+
files: ['**/*.svelte', '**/*.svelte.ts'],
|
|
24
|
+
plugins: { gost: plugin },
|
|
25
|
+
rules: { 'gost/require-feature-state-selector': 'error' },
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The plugin does not install application runtime packages. Store detection uses
|
|
31
|
+
`@gost-kit/common` types already present in the consumer's TypeScript program.
|
|
32
|
+
Nested common installations are supported; a direct common dependency of the
|
|
33
|
+
application is not required for inferred Store detection.
|
|
34
|
+
Local `$state.raw` bindings are recognized from their original lexical scope.
|
|
35
|
+
For `require-feature-state-selector`, type information is required; an enabled typed rule without parser services
|
|
36
|
+
reports a configuration error. For the selector rule, stateless and JavaScript-script
|
|
37
|
+
components are skipped. Mixed components are checked per script: a typed module script does
|
|
38
|
+
not enable the rule for a JavaScript instance script. The Svelte config does not enable this typed rule for `.svelte.js`.
|
|
39
|
+
|
|
40
|
+
`single-intent-dispatch` uses an `on*` name heuristic, including function-valued
|
|
41
|
+
non-event props such as `once`/`only`. Its whole-handler condition policy also
|
|
42
|
+
reports conditions after an unconditional Dispatch, without control-flow proof.
|
|
43
|
+
Optional invocation/access that gates a genuine Dispatch is conditional delivery;
|
|
44
|
+
ordinary optional data reads remain outside that gate check.
|
|
45
|
+
|
|
46
|
+
See [RULES.md](RULES.md) for diagnostics, autofix boundaries and examples.
|
|
47
|
+
Tests and test helpers are excluded from published artifacts.
|
|
48
|
+
|
|
49
|
+
`ready-boolean-condition` checks Svelte if/else-if conditions: true/false or
|
|
50
|
+
ready boolean identifier/static field reads. Computations and truthiness are
|
|
51
|
+
reported; typed reads require strictNullChecks and parser services. JS/stateless
|
|
52
|
+
reads are checked too when services are available. No autofix or presenter
|
|
53
|
+
provenance guarantee is provided. See the inventory for opt-in boundaries.
|
package/RULES.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# GOST architecture rules
|
|
2
|
+
|
|
3
|
+
## gost/require-feature-state-selector
|
|
4
|
+
|
|
5
|
+
Select an immutable reactive branch before passing it to a computation in
|
|
6
|
+
`$derived` or the synchronous callback of `$derived.by`:
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
// Reported; a simple standalone declaration can be fixed with eslint --fix.
|
|
10
|
+
const view = $derived(userPresenter(root.state.user));
|
|
11
|
+
|
|
12
|
+
// Correct.
|
|
13
|
+
const user = $derived(root.state.user);
|
|
14
|
+
const view = $derived(userPresenter(user));
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`root` is not a special name. The rule recognizes the declaration provenance of
|
|
18
|
+
`Store.state` from the public `@gost-kit/common` API, including aliases,
|
|
19
|
+
reexports, inferred `createStore` returns, nullable Store and supported mapped
|
|
20
|
+
or intersection types preserving that provenance. Declaration ownership is checked
|
|
21
|
+
against the actual common package, including nested installations where the
|
|
22
|
+
consumer cannot directly resolve common. It does not identify Store
|
|
23
|
+
solely by its name or structural assignability. Mixed unions and any/unknown
|
|
24
|
+
are not assumed to be Store.
|
|
25
|
+
|
|
26
|
+
A directly declared local `$state.raw` also represents a replaceable snapshot:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
let model = $state.raw({ user: { name: 'Ada' } });
|
|
30
|
+
const user = $derived(model.user);
|
|
31
|
+
const view = $derived(userPresenter(user));
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A plain `const snapshot = model` captures the current raw snapshot and is not
|
|
35
|
+
itself a reactive alias. Ordinary objects, deep-proxy `$state` and ordinary
|
|
36
|
+
functions named `$state`/`$derived` are not automatically subject to this rule.
|
|
37
|
+
TypeScript types alone cannot prove Svelte reactivity: both state runes return
|
|
38
|
+
`T`. `$state.raw` detection therefore inspects original variable bindings.
|
|
39
|
+
|
|
40
|
+
### Diagnostic scope
|
|
41
|
+
|
|
42
|
+
Direct reads used in call arguments or receivers, constructor arguments and
|
|
43
|
+
tagged template substitutions inside reactive expressions are reported, including multiple branches, method chains, static bracket keys,
|
|
44
|
+
optional calls/access, and passing an entire `Store.state` snapshot.
|
|
45
|
+
Selectors without a computation call remain valid, including `$derived.by`
|
|
46
|
+
selectors. Event handlers and other nonreactive reads are outside the rule.
|
|
47
|
+
Callbacks passed as values are not assumed to run synchronously; the explicit
|
|
48
|
+
`$derived.by` callback is inspected when it is neither async nor a generator.
|
|
49
|
+
Instance field initializers are deferred; static fields and computed keys are
|
|
50
|
+
evaluated with the class expression and remain in scope. Helpers, arbitrary returned reactive values,
|
|
51
|
+
aliased snapshots, dynamic state keys and dependencies hidden across modules
|
|
52
|
+
are not traced. No claim of complete interprocedural analysis or presenter
|
|
53
|
+
purity is made.
|
|
54
|
+
|
|
55
|
+
### Autofix
|
|
56
|
+
|
|
57
|
+
Autofix handles a standalone top-level derived declaration in a rune module or
|
|
58
|
+
component instance script, with a single direct function call and one immediate
|
|
59
|
+
feature-field argument. A selector is inserted immediately before the declaration.
|
|
60
|
+
The final field supplies the name; occupied names use `V2`, `V3`, etc. Imports,
|
|
61
|
+
later declarations and references visible from outer scopes are considered.
|
|
62
|
+
Repeated fixing does not add selectors. Field names beginning with `$` receive
|
|
63
|
+
no fix: Svelte reserves dollar-prefixed bindings, including suffixed names.
|
|
64
|
+
|
|
65
|
+
Constructors and tagged templates have diagnostics only.
|
|
66
|
+
Complex `$derived.by`, nested declarations, multiple inputs/declarators,
|
|
67
|
+
receivers, optional/dynamic access, deeper property/getter paths, conditional
|
|
68
|
+
arguments, spreads and exported declarations retain diagnostics without fixes.
|
|
69
|
+
Fixes preserve surrounding comments and line endings; Prettier can then normalize
|
|
70
|
+
layout. The normal derived contract still requires pure computations.
|
|
71
|
+
|
|
72
|
+
Separate selection lets Svelte skip downstream updates when the feature's value
|
|
73
|
+
is referentially unchanged. It does not make a mutable feature immutable or
|
|
74
|
+
prevent recomputation when the selected feature actually changes.
|
|
75
|
+
|
|
76
|
+
Sources: [Svelte update propagation](https://svelte.dev/docs/svelte/$derived#Update-propagation),
|
|
77
|
+
[Svelte state](https://svelte.dev/docs/svelte/$state),
|
|
78
|
+
[typed ESLint rules](https://typescript-eslint.io/developers/custom-rules/).
|
|
79
|
+
|
|
80
|
+
## gost/no-component-state
|
|
81
|
+
|
|
82
|
+
Opt in for application components whose logical state belongs in root/feature
|
|
83
|
+
state. The shared Svelte config registers this rule but leaves it off:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
export default [
|
|
87
|
+
...(await createSvelteConfig({ tsconfigRootDir: import.meta.dirname })),
|
|
88
|
+
{
|
|
89
|
+
files: ['src/**/*.svelte'],
|
|
90
|
+
rules: { 'gost/no-component-state': 'error' },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
files: ['src/ui-kit/**/*.svelte'],
|
|
94
|
+
rules: { 'gost/no-component-state': 'off' },
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The UI-Kit path is illustrative: the consumer selects its own boundaries.
|
|
100
|
+
Independent UI components may own local state. Disabling this rule preserves
|
|
101
|
+
all other shared Svelte and TypeScript checks. No architecture flag, runtime
|
|
102
|
+
Store dependency or type-aware parser services are required by this rule.
|
|
103
|
+
|
|
104
|
+
Within selected files, direct intrinsic `$state(...)`, `$state.raw(...)` and
|
|
105
|
+
`$bindable(...)` calls report `componentState`, including module scripts,
|
|
106
|
+
nested helpers and class fields. Lexically shadowed ordinary functions are
|
|
107
|
+
ignored. Use state selectors, read-only `$derived`, pure presenter calls and
|
|
108
|
+
intent actions for application logic. `$props`, ordinary variables and temporary
|
|
109
|
+
browser handles are permitted.
|
|
110
|
+
|
|
111
|
+
A simple DOM/component ref is allowed when a rune initializes a direct identifier
|
|
112
|
+
in the instance script with no argument, `null` or unshadowed `undefined`, and
|
|
113
|
+
that same variable binding appears directly in `bind:this`:
|
|
114
|
+
|
|
115
|
+
```svelte
|
|
116
|
+
<script lang="ts">
|
|
117
|
+
let input = $state<HTMLInputElement>();
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<input bind:this={input} />
|
|
121
|
+
<button type="button" onclick={() => input?.focus()}>Focus</button>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
An unused empty rune, nonempty initializer, module-script binding or same name
|
|
125
|
+
in a different scope does not qualify. `bind:value` is not a ref exception.
|
|
126
|
+
The exception identifies ref initialization; it does not verify every subsequent
|
|
127
|
+
write to the ref.
|
|
128
|
+
|
|
129
|
+
There is no autofix: state ownership, actions and lifecycle require design.
|
|
130
|
+
This first rule checks the listed rune creations, not all mutations or state
|
|
131
|
+
hidden behind imports/helpers. Props/derived values still require read-only
|
|
132
|
+
usage in review. Dispatch composition (#17), boolean template conditions (#18)
|
|
133
|
+
and presenter computations (#19) are separate checks. `.svelte.ts` shell modules
|
|
134
|
+
remain available outside the component-only override.
|
|
135
|
+
|
|
136
|
+
The shared config registers `gost` for `.svelte` and `.svelte.ts`, not `.svelte.js`.
|
|
137
|
+
If deliberately extending this policy to `.svelte.js`, install `@gost-kit/eslint-plugin`
|
|
138
|
+
explicitly in the consumer and register the plugin
|
|
139
|
+
in that override (`plugins: { gost: plugin }`, with the named `plugin` import from
|
|
140
|
+
`@gost-kit/eslint-plugin`). Enabling the rule alone for that extension fails plugin
|
|
141
|
+
lookup. The default typed selector rule remains excluded from `.svelte.js`.
|
|
142
|
+
|
|
143
|
+
## gost/single-intent-dispatch
|
|
144
|
+
|
|
145
|
+
Business UI callbacks forward one intent action. The reducer chooses business
|
|
146
|
+
transitions/effects from current state. UI-Kit owns local state, browser events
|
|
147
|
+
and their conditional processing. When a business feature needs such a mechanism,
|
|
148
|
+
extract a library component and consume ready values/semantic callbacks.
|
|
149
|
+
|
|
150
|
+
The shared config registers this typed rule but leaves it off. Enable it for
|
|
151
|
+
business component files and exclude UI-Kit with ordinary late overrides:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
export default [
|
|
155
|
+
...(await createSvelteConfig({ tsconfigRootDir: import.meta.dirname })),
|
|
156
|
+
{
|
|
157
|
+
files: ['src/**/*.svelte'],
|
|
158
|
+
rules: { 'gost/single-intent-dispatch': 'error' },
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
files: ['src/ui-kit/**/*.svelte'],
|
|
162
|
+
rules: { 'gost/single-intent-dispatch': 'off' },
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Paths are illustrative. Other Svelte/TypeScript rules keep their configured
|
|
168
|
+
levels. Type services are required for TypeScript instance scripts; a missing
|
|
169
|
+
project is a configuration error. JS/stateless/module-only scripts and ordinary
|
|
170
|
+
TS/rune modules are skipped before requesting services.
|
|
171
|
+
|
|
172
|
+
Handlers are found through `on*` props/attributes (including `onConfirm` and `{onConfirm}` shorthand) and
|
|
173
|
+
legacy `on:` directives, rather than function names. This is a lexical prefix
|
|
174
|
+
heuristic, not event-prop type discovery: a function-valued prop named `once` or
|
|
175
|
+
`only` is also treated as a handler. Such non-event props can produce false
|
|
176
|
+
positives; use an appropriate consumer scope or a targeted ESLint exception.
|
|
177
|
+
Lowercase custom callbacks such as `onemailchange` remain supported. Inline functions, local
|
|
178
|
+
function declarations, const function expressions and simple const aliases are
|
|
179
|
+
supported, including transparent TypeScript `satisfies`/`as` wrappers. Handlers
|
|
180
|
+
declared in template `{@const}` tags are outside this first version's discovery. Reassigned callbacks, arbitrary member methods/imported functions,
|
|
181
|
+
spread props and callback names without the `on` prefix are not traced.
|
|
182
|
+
Forwarding `onconfirm={presenter.confirm}` is permitted without claiming to
|
|
183
|
+
verify the method body or presenter purity.
|
|
184
|
+
|
|
185
|
+
A dispatch is recognized by declaration provenance of the public
|
|
186
|
+
`@gost-kit/common` `Dispatch` type and its call signature, including simple type
|
|
187
|
+
aliases/reexports, const/destructured aliases, `Store.dispatch` and inferred
|
|
188
|
+
`createStore` returns. Signature ownership matters: TypeScript may reuse alias
|
|
189
|
+
metadata for structurally equal local callbacks. Name/shape alone, plain
|
|
190
|
+
`AsyncTap`, any/unknown and mixed unions do not prove Dispatch; genuine nullable
|
|
191
|
+
Dispatch is supported. Optional invocation/access that can suppress that
|
|
192
|
+
Dispatch (`send?.(action)`, `store?.dispatch(action)` or `store.dispatch?.(action)`)
|
|
193
|
+
is conditional intent delivery and reports `conditionalDispatch` for a single call.
|
|
194
|
+
This includes a Dispatch evaluated in skipped arguments/computed keys of an
|
|
195
|
+
outer optional expression, such as `consume?.(dispatch(action))` or
|
|
196
|
+
`values?.[String(dispatch(action))]`.
|
|
197
|
+
Ordinary optional data reads and optional accesses after the Dispatch result are
|
|
198
|
+
not treated as a dispatch gate. Parenthesized chains that end before an
|
|
199
|
+
unconditional call follow JavaScript chain boundaries, not optional-call semantics.
|
|
200
|
+
No application runtime packages are installed by tooling.
|
|
201
|
+
|
|
202
|
+
The handler's own body and parameter defaults report:
|
|
203
|
+
|
|
204
|
+
- `multipleDispatch`: two or more direct Dispatch calls, even in mutually
|
|
205
|
+
exclusive branches. Send one intent and move selection to the reducer.
|
|
206
|
+
- `conditionalDispatch`: one Dispatch plus `if`, `switch`, loops, a conditional
|
|
207
|
+
or logical expression/assignment (`??=`, `&&=`, `||=`), including early guards and an action's
|
|
208
|
+
conditional `type`. Browser-event guards have no special exception.
|
|
209
|
+
|
|
210
|
+
This is a syntactic whole-handler policy, not control-flow dependence analysis:
|
|
211
|
+
a listed condition anywhere in the handler's own evaluation is reported when
|
|
212
|
+
that handler contains a Dispatch, including a condition after an unconditional
|
|
213
|
+
Dispatch and a condition unrelated to choosing its action. The diagnostic directs
|
|
214
|
+
conditional logic to the reducer/UI-Kit; it does not prove that a particular
|
|
215
|
+
condition selected the dispatched action.
|
|
216
|
+
|
|
217
|
+
Dispatch in a parameter default is conditional on the argument being absent;
|
|
218
|
+
it is counted and diagnosed accordingly. Ordinary ready-value defaults are
|
|
219
|
+
permitted. Nested callback defaults are deferred like other nested functions.
|
|
220
|
+
|
|
221
|
+
Multiple-call diagnostics take priority; one handler produces one report even
|
|
222
|
+
if bound to several events. Direct/void/await calls with a ready action/value
|
|
223
|
+
are permitted. Nested function bodies and nonstatic instance initializers are
|
|
224
|
+
not part of the enclosing handler's evaluation. Static initialization/computed
|
|
225
|
+
keys remain included. Helper/constructor call graphs are not followed.
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
// Reported: business choice remains in the component.
|
|
229
|
+
const handleSubmit = () => {
|
|
230
|
+
if (hasUser) {
|
|
231
|
+
void dispatch({ type: 'UpdateUserAction' });
|
|
232
|
+
} else {
|
|
233
|
+
void dispatch({ type: 'CreateUserAction' });
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Use an intent callback supplied to a UI-Kit component.
|
|
238
|
+
const handleSubmit = () => {
|
|
239
|
+
void dispatch({ type: 'SubmitUserAction' });
|
|
240
|
+
};
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
There is no autofix: action/reducer/UI component contracts require design.
|
|
244
|
+
This rule does not prove absence of all browser APIs, hidden dispatches or
|
|
245
|
+
business logic. A lone `preventDefault()` without a branch is not automatically
|
|
246
|
+
a dispatch diagnostic, although it violates the business-component boundary.
|
|
247
|
+
Event adaptation belongs in UI-Kit. Effects outside discovered UI callbacks may
|
|
248
|
+
perform several dispatches. Presentation computations (including templates) are
|
|
249
|
+
#19, boolean template conditions #18, and testable local UI-state #27.
|
|
250
|
+
|
|
251
|
+
## gost/ready-boolean-condition
|
|
252
|
+
|
|
253
|
+
Opt in for business `.svelte` files. `{#if}` and `{:else if}` read a ready
|
|
254
|
+
boolean; ordinary else and conditional rendering remain available.
|
|
255
|
+
|
|
256
|
+
```svelte
|
|
257
|
+
{#if view.isLoaded}
|
|
258
|
+
<p>{view.name}</p>
|
|
259
|
+
{:else if isFailed}
|
|
260
|
+
<p>Failed</p>
|
|
261
|
+
{/if}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Allowed: boolean literals true/false, identifiers and static field chains rooted
|
|
265
|
+
at an identifier (for example view.ready), including quoted keys and number-valued
|
|
266
|
+
AST literal indexes such as flags[0] or flags[0x10]. The final type must be
|
|
267
|
+
definitely boolean. this.isReady is outside the supported root form. Signed keys
|
|
268
|
+
such as flags[-1] are unary expressions, and bigint keys such as flags[10n] are
|
|
269
|
+
not number-valued literals; both are outside this first-version syntax boundary.
|
|
270
|
+
Optional chains are accepted only when the complete expression excludes
|
|
271
|
+
undefined/null. Boolean aliases, boolean-only unions and proven boolean generic
|
|
272
|
+
constraints are accepted. TypeScript flow narrowing applies at the expression.
|
|
273
|
+
|
|
274
|
+
Reported with `readyBoolean`: comparisons, logical operations, negation, calls,
|
|
275
|
+
ternaries, dynamic keys, assertions/non-null assertions/satisfies wrappers,
|
|
276
|
+
object/string/number truthiness, nullable booleans and any/unknown/never. Move
|
|
277
|
+
the calculation to a pure presenter and read its named boolean result. There
|
|
278
|
+
is no autofix or suggestion that invents a presenter/field name.
|
|
279
|
+
|
|
280
|
+
Identifier/member reads require actual parser type services and strictNullChecks,
|
|
281
|
+
either directly or via strict. Explicit strictNullChecks: false overrides strict.
|
|
282
|
+
Missing services/null checking is a configuration error, not a successful check.
|
|
283
|
+
Literal booleans and invalid syntax do not need services; components without
|
|
284
|
+
conditions do not request them. Type services also cover JS-script and stateless
|
|
285
|
+
components when the configured parser supplies a program. Inferred any in JS
|
|
286
|
+
props is rejected just like TypeScript any. Script if statements, ordinary TS and
|
|
287
|
+
rune modules are outside the rule.
|
|
288
|
+
|
|
289
|
+
This checks shape and type, not provenance/purity. A local boolean or boolean
|
|
290
|
+
getter is not proof of a presenter. Rule #19 addresses presentation calculations
|
|
291
|
+
outside template conditions. Static indexed reads follow consumer TypeScript
|
|
292
|
+
options; enable noUncheckedIndexedAccess to expose possible missing entries.
|
|
293
|
+
The rule does not validate runtime input that the declared types conceal.
|
|
294
|
+
|
|
295
|
+
`createSvelteConfig` registers it explicitly off. Enable with a later override:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
{
|
|
299
|
+
files: ['src/**/*.svelte'],
|
|
300
|
+
rules: { 'gost/ready-boolean-condition': 'error' },
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Independent UI-Kit components may use a later matching off override while
|
|
305
|
+
retaining the other TypeScript/Svelte checks.
|
|
306
|
+
|
|
307
|
+
The current svelte-eslint-parser 1.10.1 models component generics as aliases to
|
|
308
|
+
their constraints (unknown when unconstrained). Conditional types over those
|
|
309
|
+
component generics may therefore resolve differently from a particular caller's
|
|
310
|
+
instantiation. This rule checks the parser-provided program, not every generic
|
|
311
|
+
instantiation; use an explicitly boolean view field for that boundary. It cannot
|
|
312
|
+
recover source type distinctions already erased by the parser.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readyBooleanCondition } from './rules/ready-boolean-condition.js';
|
|
2
|
+
import { singleIntentDispatch } from './rules/single-intent-dispatch.js';
|
|
3
|
+
import { noComponentState } from './rules/no-component-state.js';
|
|
4
|
+
import { requireFeatureStateSelector } from './rules/require-feature-state-selector.js';
|
|
5
|
+
export const plugin = {
|
|
6
|
+
meta: { name: '@gost-kit/eslint-plugin' },
|
|
7
|
+
// typescript-eslint still types the legacy context members removed in ESLint 10.
|
|
8
|
+
// At this public boundary the implementation uses only the current APIs.
|
|
9
|
+
rules: {
|
|
10
|
+
'ready-boolean-condition': readyBooleanCondition,
|
|
11
|
+
'single-intent-dispatch': singleIntentDispatch,
|
|
12
|
+
'no-component-state': noComponentState,
|
|
13
|
+
'require-feature-state-selector': requireFeatureStateSelector,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export const booleanType = (checker, type) => {
|
|
3
|
+
if (type.flags &
|
|
4
|
+
(ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
if (type.flags & ts.TypeFlags.TypeParameter) {
|
|
8
|
+
const constraint = checker.getBaseConstraintOfType(type);
|
|
9
|
+
return (constraint !== undefined &&
|
|
10
|
+
constraint !== type &&
|
|
11
|
+
booleanType(checker, constraint));
|
|
12
|
+
}
|
|
13
|
+
return checker.isTypeAssignableTo(type, checker.getBooleanType());
|
|
14
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
// Identify the package owning the declaration, without requiring a hoisted
|
|
4
|
+
// @gost-kit/common import from the consumer's source directory.
|
|
5
|
+
export const createCommonSourceCheck = () => {
|
|
6
|
+
const directories = new Map();
|
|
7
|
+
const isCommonDirectory = (directory) => {
|
|
8
|
+
const cached = directories.get(directory);
|
|
9
|
+
if (cached !== undefined) {
|
|
10
|
+
return cached;
|
|
11
|
+
}
|
|
12
|
+
const filename = join(directory, 'package.json');
|
|
13
|
+
const contents = ts.sys.readFile(filename);
|
|
14
|
+
let common = false;
|
|
15
|
+
if (contents !== undefined) {
|
|
16
|
+
const statement = ts.parseJsonText(filename, contents).statements[0];
|
|
17
|
+
if (statement &&
|
|
18
|
+
ts.isExpressionStatement(statement) &&
|
|
19
|
+
ts.isObjectLiteralExpression(statement.expression)) {
|
|
20
|
+
common = statement.expression.properties.some((property) => {
|
|
21
|
+
return (ts.isPropertyAssignment(property) &&
|
|
22
|
+
ts.isStringLiteral(property.name) &&
|
|
23
|
+
property.name.text === 'name' &&
|
|
24
|
+
ts.isStringLiteral(property.initializer) &&
|
|
25
|
+
property.initializer.text === '@gost-kit/common');
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const parent = dirname(directory);
|
|
31
|
+
common = parent !== directory && isCommonDirectory(parent);
|
|
32
|
+
}
|
|
33
|
+
directories.set(directory, common);
|
|
34
|
+
return common;
|
|
35
|
+
};
|
|
36
|
+
return (source) => {
|
|
37
|
+
return isCommonDirectory(dirname(source.fileName));
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { svelteScripts, scriptHasAttribute, inScript } from './svelte-scripts.js';
|
|
2
|
+
import { findValueVariable } from './value-variable.js';
|
|
3
|
+
import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
|
|
4
|
+
export const createComponentRefCheck = (source) => {
|
|
5
|
+
const variables = new Set();
|
|
6
|
+
const instanceScripts = (svelteScripts(source) ?? []).filter((script) => {
|
|
7
|
+
return (!scriptHasAttribute(script, 'module') &&
|
|
8
|
+
!scriptHasAttribute(script, 'context', 'module'));
|
|
9
|
+
});
|
|
10
|
+
const isEmpty = (call) => {
|
|
11
|
+
if (call.arguments.length === 0) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (call.arguments.length !== 1) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const value = call.arguments[0];
|
|
18
|
+
if (value.type === AST_NODE_TYPES.Literal && value.raw === 'null') {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (value.type === AST_NODE_TYPES.Identifier &&
|
|
22
|
+
value.name === 'undefined') {
|
|
23
|
+
const variable = findValueVariable(source.getScope(value), value.name);
|
|
24
|
+
return !variable || variable.defs.length === 0;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
recordBinding(node) {
|
|
30
|
+
// This listener runs only for SvelteDirective nodes supplied by the parser.
|
|
31
|
+
const directive = node;
|
|
32
|
+
if (directive.kind !== 'Binding' ||
|
|
33
|
+
directive.key.name.name !== 'this' ||
|
|
34
|
+
directive.expression?.type !== AST_NODE_TYPES.Identifier) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const identifier = directive.expression;
|
|
38
|
+
const variable = findValueVariable(source.getScope(identifier), identifier.name);
|
|
39
|
+
if (variable) {
|
|
40
|
+
variables.add(variable);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
allows(call) {
|
|
44
|
+
const declaration = call.parent;
|
|
45
|
+
if (declaration.type !== AST_NODE_TYPES.VariableDeclarator ||
|
|
46
|
+
declaration.init !== call ||
|
|
47
|
+
declaration.id.type !== AST_NODE_TYPES.Identifier ||
|
|
48
|
+
!isEmpty(call)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (!inScript(call, instanceScripts)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const variable = findValueVariable(source.getScope(declaration.id), declaration.id.name);
|
|
55
|
+
return !!variable && variables.has(variable);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
};
|