@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +52 -2
  4. package/RULES.md +312 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +15 -0
  7. package/dist/lib/boolean-type.d.ts +2 -0
  8. package/dist/lib/boolean-type.js +14 -0
  9. package/dist/lib/common-source.d.ts +2 -0
  10. package/dist/lib/common-source.js +39 -0
  11. package/dist/lib/component-refs.d.ts +5 -0
  12. package/dist/lib/component-refs.js +58 -0
  13. package/dist/lib/dispatch-type.d.ts +2 -0
  14. package/dist/lib/dispatch-type.js +103 -0
  15. package/dist/lib/handler-body.d.ts +2 -0
  16. package/dist/lib/handler-body.js +32 -0
  17. package/dist/lib/member-key.d.ts +4 -0
  18. package/dist/lib/member-key.js +18 -0
  19. package/dist/lib/optional-call.d.ts +2 -0
  20. package/dist/lib/optional-call.js +55 -0
  21. package/dist/lib/raw-state.d.ts +2 -0
  22. package/dist/lib/raw-state.js +27 -0
  23. package/dist/lib/reactive-context.d.ts +6 -0
  24. package/dist/lib/reactive-context.js +48 -0
  25. package/dist/lib/ready-boolean-expression.d.ts +2 -0
  26. package/dist/lib/ready-boolean-expression.js +17 -0
  27. package/dist/lib/rune-callee.d.ts +2 -0
  28. package/dist/lib/rune-callee.js +19 -0
  29. package/dist/lib/selector-fix.d.ts +2 -0
  30. package/dist/lib/selector-fix.js +122 -0
  31. package/dist/lib/store-state.d.ts +2 -0
  32. package/dist/lib/store-state.js +57 -0
  33. package/dist/lib/svelte-scripts.d.ts +5 -0
  34. package/dist/lib/svelte-scripts.js +25 -0
  35. package/dist/lib/ui-handlers.d.ts +10 -0
  36. package/dist/lib/ui-handlers.js +84 -0
  37. package/dist/lib/value-variable.d.ts +2 -0
  38. package/dist/lib/value-variable.js +25 -0
  39. package/dist/rules/no-component-state.d.ts +2 -0
  40. package/dist/rules/no-component-state.js +46 -0
  41. package/dist/rules/ready-boolean-condition.d.ts +2 -0
  42. package/dist/rules/ready-boolean-condition.js +49 -0
  43. package/dist/rules/require-feature-state-selector.d.ts +2 -0
  44. package/dist/rules/require-feature-state-selector.js +58 -0
  45. package/dist/rules/single-intent-dispatch.d.ts +2 -0
  46. package/dist/rules/single-intent-dispatch.js +89 -0
  47. 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
- # Temporary Holding Version
1
+ # @gost-kit/eslint-plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
@@ -0,0 +1,2 @@
1
+ import type { ESLint } from 'eslint';
2
+ export declare const plugin: ESLint.Plugin;
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,2 @@
1
+ import ts from 'typescript';
2
+ export declare const booleanType: (checker: ts.TypeChecker, type: ts.Type) => boolean;
@@ -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,2 @@
1
+ import ts from 'typescript';
2
+ export declare const createCommonSourceCheck: () => (source: ts.SourceFile) => boolean;
@@ -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,5 @@
1
+ import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
+ export declare const createComponentRefCheck: (source: TSESLint.SourceCode) => {
3
+ recordBinding(node: TSESTree.Node): void;
4
+ allows(call: TSESTree.CallExpression): boolean;
5
+ };
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ import type { ParserServicesWithTypeInformation, TSESTree } from '@typescript-eslint/utils';
2
+ export declare const createDispatchCheck: (services: ParserServicesWithTypeInformation) => (node: TSESTree.Node) => boolean;