@gost-kit/eslint-plugin 0.0.0-stage → 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.
Files changed (41) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +45 -2
  4. package/RULES.md +249 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +13 -0
  7. package/dist/lib/common-source.d.ts +2 -0
  8. package/dist/lib/common-source.js +39 -0
  9. package/dist/lib/component-refs.d.ts +5 -0
  10. package/dist/lib/component-refs.js +58 -0
  11. package/dist/lib/dispatch-type.d.ts +2 -0
  12. package/dist/lib/dispatch-type.js +103 -0
  13. package/dist/lib/handler-body.d.ts +2 -0
  14. package/dist/lib/handler-body.js +32 -0
  15. package/dist/lib/member-key.d.ts +2 -0
  16. package/dist/lib/member-key.js +13 -0
  17. package/dist/lib/optional-call.d.ts +2 -0
  18. package/dist/lib/optional-call.js +55 -0
  19. package/dist/lib/raw-state.d.ts +2 -0
  20. package/dist/lib/raw-state.js +27 -0
  21. package/dist/lib/reactive-context.d.ts +6 -0
  22. package/dist/lib/reactive-context.js +48 -0
  23. package/dist/lib/rune-callee.d.ts +2 -0
  24. package/dist/lib/rune-callee.js +19 -0
  25. package/dist/lib/selector-fix.d.ts +2 -0
  26. package/dist/lib/selector-fix.js +122 -0
  27. package/dist/lib/store-state.d.ts +2 -0
  28. package/dist/lib/store-state.js +57 -0
  29. package/dist/lib/svelte-scripts.d.ts +5 -0
  30. package/dist/lib/svelte-scripts.js +25 -0
  31. package/dist/lib/ui-handlers.d.ts +10 -0
  32. package/dist/lib/ui-handlers.js +84 -0
  33. package/dist/lib/value-variable.d.ts +2 -0
  34. package/dist/lib/value-variable.js +25 -0
  35. package/dist/rules/no-component-state.d.ts +2 -0
  36. package/dist/rules/no-component-state.js +46 -0
  37. package/dist/rules/require-feature-state-selector.d.ts +2 -0
  38. package/dist/rules/require-feature-state-selector.js +58 -0
  39. package/dist/rules/single-intent-dispatch.d.ts +2 -0
  40. package/dist/rules/single-intent-dispatch.js +89 -0
  41. package/package.json +55 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # @gost-kit/eslint-plugin
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 11cc6af: Add opt-in no-component-state diagnostics for state and bindable rune creation,
8
+ with binding-aware empty bind:this ref exceptions and no autofix. Keep the
9
+ public Svelte factory policy unchanged; consumers select application and UI-Kit
10
+ scopes with explicit ESLint overrides.
11
+
12
+ Reuse value-namespace rune resolution in the existing selector rule so type-only
13
+ names/imports do not hide derived or raw-state diagnostics. Consolidate rune and
14
+ Svelte script matching; document explicit plugin registration for extra JS scopes.
15
+
16
+ - d6883b7: Add the typed require-feature-state-selector architecture rule for GOST Store
17
+ and explicit local Svelte raw state. Enable it in typed Svelte config scopes,
18
+ with conservative selector autofix and V2/V3 collision-safe names.
19
+ - 730b73a: Add opt-in single-intent-dispatch for typed business UI callbacks, using public
20
+ Dispatch declaration/signature provenance. Diagnose multiple dispatches or
21
+ conditional intent selection without autofix. Keep the shared Svelte factory
22
+ default off and allow consumer UI-Kit exclusions through ordinary overrides.
23
+
24
+ Include optional calls/accesses that gate Dispatch in conditional-delivery
25
+ diagnostics, without treating unrelated optional data reads as dispatch gates.
26
+ 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,46 @@
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` and `gost/single-intent-dispatch` require explicit
12
+ consumer overrides for business component paths. UI-Kit paths can opt out.
13
+ No separate plugin registration is needed.
14
+ Other consumers may register the named public export in their flat config:
15
+
16
+ ```ts
17
+ import { plugin } from '@gost-kit/eslint-plugin';
18
+
19
+ export default [
20
+ // Configure a typed parser and a consumer TypeScript project first.
21
+ {
22
+ files: ['**/*.svelte', '**/*.svelte.ts'],
23
+ plugins: { gost: plugin },
24
+ rules: { 'gost/require-feature-state-selector': 'error' },
25
+ },
26
+ ];
27
+ ```
28
+
29
+ The plugin does not install application runtime packages. Store detection uses
30
+ `@gost-kit/common` types already present in the consumer's TypeScript program.
31
+ Nested common installations are supported; a direct common dependency of the
32
+ application is not required for inferred Store detection.
33
+ Local `$state.raw` bindings are recognized from their original lexical scope.
34
+ For `require-feature-state-selector`, type information is required; an enabled typed rule without parser services
35
+ reports a configuration error. Stateless and JavaScript-script components are
36
+ skipped. Mixed components are checked per script: a typed module script does
37
+ not enable the rule for a JavaScript instance script. The Svelte config does not enable this typed rule for `.svelte.js`.
38
+
39
+ `single-intent-dispatch` uses an `on*` name heuristic, including function-valued
40
+ non-event props such as `once`/`only`. Its whole-handler condition policy also
41
+ reports conditions after an unconditional Dispatch, without control-flow proof.
42
+ Optional invocation/access that gates a genuine Dispatch is conditional delivery;
43
+ ordinary optional data reads remain outside that gate check.
44
+
45
+ See [RULES.md](RULES.md) for diagnostics, autofix boundaries and examples.
46
+ Tests and test helpers are excluded from published artifacts.
package/RULES.md ADDED
@@ -0,0 +1,249 @@
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.
@@ -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,13 @@
1
+ import { singleIntentDispatch } from './rules/single-intent-dispatch.js';
2
+ import { noComponentState } from './rules/no-component-state.js';
3
+ import { requireFeatureStateSelector } from './rules/require-feature-state-selector.js';
4
+ export const plugin = {
5
+ meta: { name: '@gost-kit/eslint-plugin' },
6
+ // typescript-eslint still types the legacy context members removed in ESLint 10.
7
+ // At this public boundary the implementation uses only the current APIs.
8
+ rules: {
9
+ 'single-intent-dispatch': singleIntentDispatch,
10
+ 'no-component-state': noComponentState,
11
+ 'require-feature-state-selector': requireFeatureStateSelector,
12
+ },
13
+ };
@@ -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;
@@ -0,0 +1,103 @@
1
+ import ts from 'typescript';
2
+ import { createCommonSourceCheck } from './common-source.js';
3
+ export const createDispatchCheck = (services) => {
4
+ const checker = services.program.getTypeChecker();
5
+ const isCommonSource = createCommonSourceCheck();
6
+ const signatures = new Set();
7
+ const symbols = new Map();
8
+ const isPublicDispatch = (symbol) => {
9
+ const cached = symbols.get(symbol);
10
+ if (cached !== undefined) {
11
+ return cached;
12
+ }
13
+ const matches = symbol.getDeclarations()?.some((declaration) => {
14
+ const source = declaration.getSourceFile();
15
+ if (!isCommonSource(source)) {
16
+ return false;
17
+ }
18
+ const module = checker.getSymbolAtLocation(source);
19
+ const exported = module &&
20
+ checker.getExportsOfModule(module).find((item) => {
21
+ return item.name === 'Dispatch';
22
+ });
23
+ const target = exported &&
24
+ (exported.flags & ts.SymbolFlags.Alias
25
+ ? checker.getAliasedSymbol(exported)
26
+ : exported);
27
+ if (target !== symbol) {
28
+ return false;
29
+ }
30
+ checker
31
+ .getDeclaredTypeOfSymbol(target)
32
+ .getCallSignatures()
33
+ .forEach((signature) => {
34
+ if (signature.declaration) {
35
+ signatures.add(signature.declaration);
36
+ }
37
+ });
38
+ return true;
39
+ }) ?? false;
40
+ symbols.set(symbol, matches);
41
+ return matches;
42
+ };
43
+ const hasDispatchAlias = (original, seen) => {
44
+ const symbol = original.flags & ts.SymbolFlags.Alias
45
+ ? checker.getAliasedSymbol(original)
46
+ : original;
47
+ if (seen.has(symbol)) {
48
+ return false;
49
+ }
50
+ if (isPublicDispatch(symbol)) {
51
+ return true;
52
+ }
53
+ const next = new Set(seen).add(symbol);
54
+ return (symbol.getDeclarations()?.some((declaration) => {
55
+ if (!ts.isTypeAliasDeclaration(declaration)) {
56
+ return false;
57
+ }
58
+ let target = declaration.type;
59
+ while (ts.isParenthesizedTypeNode(target)) {
60
+ target = target.type;
61
+ }
62
+ if (!ts.isTypeReferenceNode(target)) {
63
+ return false;
64
+ }
65
+ const referenced = checker.getSymbolAtLocation(target.typeName);
66
+ return !!referenced && hasDispatchAlias(referenced, next);
67
+ }) ?? false);
68
+ };
69
+ const isDispatch = (type, seen) => {
70
+ if (seen.has(type) ||
71
+ type.flags &
72
+ (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) {
73
+ return false;
74
+ }
75
+ const next = new Set(seen).add(type);
76
+ if (type.isUnion()) {
77
+ const members = type.types.filter((member) => {
78
+ return !(member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined));
79
+ });
80
+ return (members.length > 0 &&
81
+ members.every((member) => {
82
+ return isDispatch(member, next);
83
+ }));
84
+ }
85
+ const symbol = type.aliasSymbol;
86
+ if (!symbol) {
87
+ return false;
88
+ }
89
+ if (!hasDispatchAlias(symbol, new Set())) {
90
+ return false;
91
+ }
92
+ // TS may intern structurally equal aliases. Their signature declarations
93
+ // still distinguish a local callback from the public Dispatch contract.
94
+ const calls = type.getCallSignatures();
95
+ return (calls.length > 0 &&
96
+ calls.every((signature) => {
97
+ return !!signature.declaration && signatures.has(signature.declaration);
98
+ }));
99
+ };
100
+ return (node) => {
101
+ return isDispatch(services.getTypeAtLocation(node), new Set());
102
+ };
103
+ };
@@ -0,0 +1,2 @@
1
+ import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
+ export declare const visitHandlerBody: (root: TSESTree.Node, source: TSESLint.SourceCode, visit: (node: TSESTree.Node) => void) => void;
@@ -0,0 +1,32 @@
1
+ import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
2
+ // Visit only the handler's own evaluation. Deferred function bodies are separate callbacks.
3
+ export const visitHandlerBody = (root, source, visit) => {
4
+ const walk = (node) => {
5
+ if (node.type === AST_NODE_TYPES.ArrowFunctionExpression ||
6
+ node.type === AST_NODE_TYPES.FunctionExpression ||
7
+ node.type === AST_NODE_TYPES.FunctionDeclaration) {
8
+ return;
9
+ }
10
+ visit(node);
11
+ (source.visitorKeys[node.type] ?? []).forEach((key) => {
12
+ if ((node.type === AST_NODE_TYPES.PropertyDefinition ||
13
+ node.type === AST_NODE_TYPES.AccessorProperty) &&
14
+ !node.static &&
15
+ key === 'value') {
16
+ return;
17
+ }
18
+ const value = node[key];
19
+ if (Array.isArray(value)) {
20
+ value.forEach((child) => {
21
+ if (child) {
22
+ walk(child);
23
+ }
24
+ });
25
+ }
26
+ else if (value) {
27
+ walk(value);
28
+ }
29
+ });
30
+ };
31
+ walk(root);
32
+ };
@@ -0,0 +1,2 @@
1
+ import { type TSESTree } from '@typescript-eslint/utils';
2
+ export declare const memberKey: (node: TSESTree.MemberExpression) => string | undefined;
@@ -0,0 +1,13 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ export const memberKey = (node) => {
3
+ if (!node.computed && node.property.type === AST_NODE_TYPES.Identifier) {
4
+ return node.property.name;
5
+ }
6
+ if (node.computed && node.property.type === AST_NODE_TYPES.Literal) {
7
+ return node.property.raw.startsWith("'") ||
8
+ node.property.raw.startsWith('"')
9
+ ? String(node.property.value)
10
+ : undefined;
11
+ }
12
+ return undefined;
13
+ };
@@ -0,0 +1,2 @@
1
+ import { type TSESTree } from '@typescript-eslint/utils';
2
+ export declare const isOptionalCall: (call: TSESTree.CallExpression) => boolean;