@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
@@ -0,0 +1,55 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ const hasOptionalAccess = (node) => {
3
+ let current = node;
4
+ while (true) {
5
+ if (current.type === AST_NODE_TYPES.CallExpression ||
6
+ current.type === AST_NODE_TYPES.MemberExpression) {
7
+ if (current.optional) {
8
+ return true;
9
+ }
10
+ current =
11
+ current.type === AST_NODE_TYPES.CallExpression
12
+ ? current.callee
13
+ : current.object;
14
+ }
15
+ else if (current.type === AST_NODE_TYPES.TSAsExpression ||
16
+ current.type === AST_NODE_TYPES.TSSatisfiesExpression ||
17
+ current.type === AST_NODE_TYPES.TSNonNullExpression ||
18
+ current.type === AST_NODE_TYPES.TSTypeAssertion ||
19
+ current.type === AST_NODE_TYPES.TSInstantiationExpression) {
20
+ current = current.expression;
21
+ }
22
+ else {
23
+ // A nested ChainExpression ends an earlier chain (e.g. `(store?.dispatch)()`).
24
+ // Arguments are not part of this path; enclosing argument/key gates are checked below.
25
+ return false;
26
+ }
27
+ }
28
+ };
29
+ export const isOptionalCall = (call) => {
30
+ if (hasOptionalAccess(call)) {
31
+ return true;
32
+ }
33
+ let current = call;
34
+ while (current.parent) {
35
+ const parent = current.parent;
36
+ if (parent.type === AST_NODE_TYPES.ArrowFunctionExpression ||
37
+ parent.type === AST_NODE_TYPES.FunctionExpression ||
38
+ parent.type === AST_NODE_TYPES.FunctionDeclaration) {
39
+ return false;
40
+ }
41
+ // Optional calls/accesses skip their arguments and computed keys too.
42
+ if (((parent.type === AST_NODE_TYPES.CallExpression &&
43
+ parent.arguments.some((argument) => {
44
+ return argument === current;
45
+ })) ||
46
+ (parent.type === AST_NODE_TYPES.MemberExpression &&
47
+ parent.computed &&
48
+ parent.property === current)) &&
49
+ hasOptionalAccess(parent)) {
50
+ return true;
51
+ }
52
+ current = parent;
53
+ }
54
+ return false;
55
+ };
@@ -0,0 +1,2 @@
1
+ import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
+ export declare const createRawStateCheck: (source: TSESLint.SourceCode) => (node: TSESTree.Node) => boolean;
@@ -0,0 +1,27 @@
1
+ import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
2
+ import { runeCallee } from './rune-callee.js';
3
+ import { findValueVariable } from './value-variable.js';
4
+ // Inspect the original scope: Svelte's typed parser synthesizes rune declarations
5
+ // in a virtual TS file. Plain aliases of raw state capture a nonreactive snapshot.
6
+ export const createRawStateCheck = (source) => {
7
+ return (node) => {
8
+ if (node.type !== AST_NODE_TYPES.Identifier) {
9
+ return false;
10
+ }
11
+ const variable = findValueVariable(source.getScope(node), node.name);
12
+ return (variable?.defs.some((definition) => {
13
+ if (definition.type !== 'Variable' ||
14
+ definition.node.id.type !== AST_NODE_TYPES.Identifier ||
15
+ definition.node.id.name !== node.name) {
16
+ return false;
17
+ }
18
+ const init = definition.node.init;
19
+ if (init?.type !== AST_NODE_TYPES.CallExpression ||
20
+ init.callee.type !== AST_NODE_TYPES.MemberExpression ||
21
+ !runeCallee(init.callee, source, '$state', 'raw')) {
22
+ return false;
23
+ }
24
+ return true;
25
+ }) ?? false);
26
+ };
27
+ };
@@ -0,0 +1,6 @@
1
+ import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
+ export declare const isDerived: (node: TSESTree.CallExpression, source: TSESLint.SourceCode) => boolean;
3
+ export declare const reactiveContext: (node: TSESTree.Node, source: TSESLint.SourceCode) => {
4
+ derived: TSESTree.CallExpression;
5
+ call: TSESTree.CallExpression | TSESTree.NewExpression | TSESTree.TaggedTemplateExpression;
6
+ } | undefined;
@@ -0,0 +1,48 @@
1
+ import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
2
+ import { runeCallee } from './rune-callee.js';
3
+ import { memberKey } from './member-key.js';
4
+ export const isDerived = (node, source) => {
5
+ return !!runeCallee(node.callee, source, '$derived', 'by');
6
+ };
7
+ export const reactiveContext = (node, source) => {
8
+ let current = node;
9
+ let call;
10
+ while (current.parent) {
11
+ const parent = current.parent;
12
+ if (parent.type === AST_NODE_TYPES.CallExpression) {
13
+ if (isDerived(parent, source)) {
14
+ return call ? { derived: parent, call } : undefined;
15
+ }
16
+ }
17
+ if (parent.type === AST_NODE_TYPES.CallExpression ||
18
+ parent.type === AST_NODE_TYPES.NewExpression ||
19
+ parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
20
+ call ??= parent;
21
+ }
22
+ // Instance fields execute only when an instance is created. Static fields
23
+ // and computed keys execute while the class expression itself is evaluated.
24
+ if ((parent.type === AST_NODE_TYPES.PropertyDefinition ||
25
+ parent.type === AST_NODE_TYPES.AccessorProperty) &&
26
+ !parent.static &&
27
+ parent.value === current) {
28
+ return undefined;
29
+ }
30
+ if (parent.type === AST_NODE_TYPES.ArrowFunctionExpression ||
31
+ parent.type === AST_NODE_TYPES.FunctionExpression ||
32
+ parent.type === AST_NODE_TYPES.FunctionDeclaration) {
33
+ const owner = parent.parent;
34
+ if (owner?.type === AST_NODE_TYPES.CallExpression &&
35
+ owner.callee.type === AST_NODE_TYPES.MemberExpression &&
36
+ memberKey(owner.callee) === 'by' &&
37
+ owner.arguments[0] === parent &&
38
+ !parent.async &&
39
+ !parent.generator &&
40
+ isDerived(owner, source)) {
41
+ return call ? { derived: owner, call } : undefined;
42
+ }
43
+ return undefined;
44
+ }
45
+ current = parent;
46
+ }
47
+ return undefined;
48
+ };
@@ -0,0 +1,2 @@
1
+ import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
+ export declare const runeCallee: (callee: TSESTree.CallExpression["callee"], source: TSESLint.SourceCode, name: string, key?: string) => TSESTree.Identifier | undefined;
@@ -0,0 +1,19 @@
1
+ import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
2
+ import { memberKey } from './member-key.js';
3
+ import { findValueVariable } from './value-variable.js';
4
+ // Return the intrinsic identifier, preserving static member-key support.
5
+ export const runeCallee = (callee, source, name, key) => {
6
+ const identifier = callee.type === AST_NODE_TYPES.Identifier
7
+ ? callee
8
+ : key &&
9
+ callee.type === AST_NODE_TYPES.MemberExpression &&
10
+ memberKey(callee) === key &&
11
+ callee.object.type === AST_NODE_TYPES.Identifier
12
+ ? callee.object
13
+ : undefined;
14
+ if (identifier?.name !== name) {
15
+ return undefined;
16
+ }
17
+ const variable = findValueVariable(source.getScope(identifier), name);
18
+ return variable && variable.defs.length > 0 ? undefined : identifier;
19
+ };
@@ -0,0 +1,2 @@
1
+ import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
+ export declare const selectorFix: (state: TSESTree.Expression, derived: TSESTree.CallExpression, call: TSESTree.CallExpression, source: TSESLint.SourceCode) => ((fixer: TSESLint.RuleFixer) => TSESLint.RuleFix[]) | undefined;
@@ -0,0 +1,122 @@
1
+ import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
2
+ import { memberKey } from './member-key.js';
3
+ const reserved = new Set([
4
+ 'arguments',
5
+ 'eval',
6
+ 'await',
7
+ 'break',
8
+ 'case',
9
+ 'catch',
10
+ 'class',
11
+ 'const',
12
+ 'continue',
13
+ 'debugger',
14
+ 'default',
15
+ 'delete',
16
+ 'do',
17
+ 'else',
18
+ 'enum',
19
+ 'export',
20
+ 'extends',
21
+ 'false',
22
+ 'finally',
23
+ 'for',
24
+ 'function',
25
+ 'if',
26
+ 'implements',
27
+ 'import',
28
+ 'in',
29
+ 'instanceof',
30
+ 'interface',
31
+ 'let',
32
+ 'new',
33
+ 'null',
34
+ 'package',
35
+ 'private',
36
+ 'protected',
37
+ 'public',
38
+ 'return',
39
+ 'static',
40
+ 'super',
41
+ 'switch',
42
+ 'this',
43
+ 'throw',
44
+ 'true',
45
+ 'try',
46
+ 'typeof',
47
+ 'var',
48
+ 'void',
49
+ 'while',
50
+ 'with',
51
+ 'yield',
52
+ ]);
53
+ // A deliberately small syntax boundary: a standalone derived with one direct call.
54
+ export const selectorFix = (state, derived, call, source) => {
55
+ if (derived.callee.type !== AST_NODE_TYPES.Identifier ||
56
+ derived.arguments[0] !== call ||
57
+ call.optional ||
58
+ call.callee.type !== AST_NODE_TYPES.Identifier ||
59
+ call.arguments.length !== 1) {
60
+ return undefined;
61
+ }
62
+ const selected = call.arguments[0];
63
+ const declarator = derived.parent;
64
+ const declaration = declarator?.parent;
65
+ if (selected.type !== AST_NODE_TYPES.MemberExpression ||
66
+ declarator?.type !== AST_NODE_TYPES.VariableDeclarator ||
67
+ declarator.init !== derived ||
68
+ declaration?.type !== AST_NODE_TYPES.VariableDeclaration ||
69
+ declaration.declarations.length !== 1 ||
70
+ declaration.parent.type === AST_NODE_TYPES.ExportNamedDeclaration ||
71
+ declaration.parent.type === AST_NODE_TYPES.ForStatement ||
72
+ declaration.parent.type === AST_NODE_TYPES.ForOfStatement ||
73
+ declaration.parent.type === AST_NODE_TYPES.ForInStatement) {
74
+ return undefined;
75
+ }
76
+ // Accept one immediate feature property only; deeper user-defined getters
77
+ // and accesses through containers have no static purity guarantee.
78
+ if (selected.object !== state ||
79
+ (state.type !== AST_NODE_TYPES.Identifier &&
80
+ (state.type !== AST_NODE_TYPES.MemberExpression ||
81
+ state.object.type !== AST_NODE_TYPES.Identifier ||
82
+ state.optional)) ||
83
+ selected.optional) {
84
+ return undefined;
85
+ }
86
+ const base = memberKey(selected);
87
+ if (!base || base.startsWith('$') || !/^[A-Z_a-z][$\w]*$/u.test(base)) {
88
+ return undefined;
89
+ }
90
+ const occupied = new Set();
91
+ let scope = source.getScope(declaration);
92
+ while (scope) {
93
+ scope.variables.forEach((variable) => {
94
+ occupied.add(variable.name);
95
+ });
96
+ scope.through.forEach((reference) => {
97
+ occupied.add(reference.identifier.name);
98
+ });
99
+ scope = scope.upper;
100
+ }
101
+ let name = base;
102
+ let version = 2;
103
+ while (occupied.has(name) || reserved.has(name)) {
104
+ name = `${base}V${version}`;
105
+ version += 1;
106
+ }
107
+ // Svelte adds script elements to ESTree; its parser supplies this parent.
108
+ const parent = declaration.parent;
109
+ if (parent.type !== AST_NODE_TYPES.Program &&
110
+ parent.type !== 'SvelteScriptElement') {
111
+ return undefined;
112
+ }
113
+ const indentation = source.lines[declaration.loc.start.line - 1].slice(0, declaration.loc.start.column);
114
+ const leading = /^\s*$/u.test(indentation) ? indentation : '';
115
+ const lineEnd = source.text.includes('\r\n') ? '\r\n' : '\n';
116
+ return (fixer) => {
117
+ return [
118
+ fixer.insertTextBefore(declaration, `const ${name} = $derived(${source.getText(selected)});${lineEnd}${leading}`),
119
+ fixer.replaceText(selected, name),
120
+ ];
121
+ };
122
+ };
@@ -0,0 +1,2 @@
1
+ import type { ParserServicesWithTypeInformation, TSESTree } from '@typescript-eslint/utils';
2
+ export declare const createStoreStateCheck: (services: ParserServicesWithTypeInformation) => (node: TSESTree.Node) => boolean;
@@ -0,0 +1,57 @@
1
+ import ts from 'typescript';
2
+ import { createCommonSourceCheck } from './common-source.js';
3
+ export const createStoreStateCheck = (services) => {
4
+ const program = services.program;
5
+ const checker = program.getTypeChecker();
6
+ const isCommonSource = createCommonSourceCheck();
7
+ const declarations = new Map();
8
+ const isStoreDeclaration = (declaration) => {
9
+ const cached = declarations.get(declaration);
10
+ if (cached !== undefined) {
11
+ return cached;
12
+ }
13
+ const source = declaration.getSourceFile();
14
+ let matches = false;
15
+ if (isCommonSource(source)) {
16
+ const module = checker.getSymbolAtLocation(source);
17
+ const exported = module &&
18
+ checker.getExportsOfModule(module).find((symbol) => {
19
+ return symbol.name === 'Store';
20
+ });
21
+ const store = exported &&
22
+ (exported.flags & ts.SymbolFlags.Alias
23
+ ? checker.getAliasedSymbol(exported)
24
+ : exported);
25
+ matches = store
26
+ ? (checker
27
+ .getDeclaredTypeOfSymbol(store)
28
+ .getProperty('state')
29
+ ?.getDeclarations()
30
+ ?.includes(declaration) ?? false)
31
+ : false;
32
+ }
33
+ declarations.set(declaration, matches);
34
+ return matches;
35
+ };
36
+ const isStore = (type) => {
37
+ if (type.flags &
38
+ (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) {
39
+ return false;
40
+ }
41
+ if (type.isUnion()) {
42
+ const members = type.types.filter((member) => {
43
+ return !(member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined));
44
+ });
45
+ return members.length > 0 && members.every(isStore);
46
+ }
47
+ return (type
48
+ .getProperty('state')
49
+ ?.getDeclarations()
50
+ ?.some((declaration) => {
51
+ return isStoreDeclaration(declaration);
52
+ }) ?? false);
53
+ };
54
+ return (node) => {
55
+ return isStore(services.getTypeAtLocation(node));
56
+ };
57
+ };
@@ -0,0 +1,5 @@
1
+ import type { AST } from 'svelte-eslint-parser';
2
+ import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
3
+ export declare const svelteScripts: (source: TSESLint.SourceCode) => AST.SvelteScriptElement[] | undefined;
4
+ export declare const scriptHasAttribute: (script: AST.SvelteScriptElement, name: string, value?: string) => boolean;
5
+ export declare const inScript: (node: TSESTree.Node, scripts: readonly AST.SvelteScriptElement[]) => boolean;
@@ -0,0 +1,25 @@
1
+ export const svelteScripts = (source) => {
2
+ const services = source.parserServices;
3
+ if (!services || !('isSvelte' in services) || services.isSvelte !== true) {
4
+ return undefined;
5
+ }
6
+ // Only the Svelte parser supplies the extended ESTree script elements.
7
+ return source.ast.body.filter((node) => {
8
+ return node.type === 'SvelteScriptElement';
9
+ });
10
+ };
11
+ export const scriptHasAttribute = (script, name, value) => {
12
+ return script.startTag.attributes.some((attribute) => {
13
+ return (attribute.type === 'SvelteAttribute' &&
14
+ attribute.key.name === name &&
15
+ (value === undefined ||
16
+ attribute.value.some((item) => {
17
+ return item.type === 'SvelteLiteral' && item.value === value;
18
+ })));
19
+ });
20
+ };
21
+ export const inScript = (node, scripts) => {
22
+ return scripts.some((script) => {
23
+ return node.range[0] >= script.range[0] && node.range[1] <= script.range[1];
24
+ });
25
+ };
@@ -0,0 +1,10 @@
1
+ import type { AST } from 'svelte-eslint-parser';
2
+ import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
3
+ type Handler = TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration;
4
+ export declare const createUiHandlerCollector: (source: TSESLint.SourceCode, scripts: readonly AST.SvelteScriptElement[]) => {
5
+ handlers: Set<Handler>;
6
+ attribute(node: TSESTree.Node): void;
7
+ shorthand(node: TSESTree.Node): void;
8
+ directive(node: TSESTree.Node): void;
9
+ };
10
+ export {};
@@ -0,0 +1,84 @@
1
+ import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
2
+ import { findValueVariable } from './value-variable.js';
3
+ import { inScript } from './svelte-scripts.js';
4
+ export const createUiHandlerCollector = (source, scripts) => {
5
+ const handlers = new Set();
6
+ const resolve = (node, inline, seen) => {
7
+ if (node.type === AST_NODE_TYPES.ArrowFunctionExpression ||
8
+ node.type === AST_NODE_TYPES.FunctionExpression ||
9
+ node.type === AST_NODE_TYPES.FunctionDeclaration) {
10
+ return inline || inScript(node, scripts) ? node : undefined;
11
+ }
12
+ if (node.type === AST_NODE_TYPES.TSSatisfiesExpression ||
13
+ node.type === AST_NODE_TYPES.TSAsExpression ||
14
+ node.type === AST_NODE_TYPES.TSTypeAssertion ||
15
+ node.type === AST_NODE_TYPES.TSNonNullExpression ||
16
+ node.type === AST_NODE_TYPES.ChainExpression) {
17
+ return resolve(node.expression, inline, seen);
18
+ }
19
+ if (node.type !== AST_NODE_TYPES.Identifier) {
20
+ return undefined;
21
+ }
22
+ const variable = findValueVariable(source.getScope(node), node.name);
23
+ if (!variable ||
24
+ seen.has(variable) ||
25
+ variable.references.some((reference) => {
26
+ return reference.isWrite() && !reference.init;
27
+ })) {
28
+ return undefined;
29
+ }
30
+ const next = new Set(seen).add(variable);
31
+ return variable.defs
32
+ .map((definition) => {
33
+ if (definition.type === 'FunctionName' &&
34
+ definition.node.type === AST_NODE_TYPES.FunctionDeclaration) {
35
+ return resolve(definition.node, false, next);
36
+ }
37
+ if (definition.type === 'Variable' &&
38
+ definition.parent.kind === 'const' &&
39
+ definition.node.id.type === AST_NODE_TYPES.Identifier &&
40
+ definition.node.init) {
41
+ return resolve(definition.node.init, false, next);
42
+ }
43
+ return undefined;
44
+ })
45
+ .find((handler) => {
46
+ return !!handler;
47
+ });
48
+ };
49
+ const add = (expression) => {
50
+ const handler = resolve(expression, true, new Set());
51
+ if (handler) {
52
+ handlers.add(handler);
53
+ }
54
+ };
55
+ return {
56
+ handlers,
57
+ attribute(node) {
58
+ // Parser-specific listeners are the only boundary for extended Svelte nodes.
59
+ const attribute = node;
60
+ if (!attribute.key.name.startsWith('on') ||
61
+ attribute.key.name.length <= 2) {
62
+ return;
63
+ }
64
+ attribute.value.forEach((value) => {
65
+ if (value.type === 'SvelteMustacheTag') {
66
+ add(value.expression);
67
+ }
68
+ });
69
+ },
70
+ shorthand(node) {
71
+ const attribute = node;
72
+ if (attribute.key.name.startsWith('on') &&
73
+ attribute.key.name.length > 2) {
74
+ add(attribute.value);
75
+ }
76
+ },
77
+ directive(node) {
78
+ const directive = node;
79
+ if (directive.kind === 'EventHandler' && directive.expression) {
80
+ add(directive.expression);
81
+ }
82
+ },
83
+ };
84
+ };
@@ -0,0 +1,2 @@
1
+ import { type TSESLint } from '@typescript-eslint/utils';
2
+ export declare const findValueVariable: (initialScope: TSESLint.Scope.Scope, name: string) => import("@typescript-eslint/scope-manager").Variable | undefined;
@@ -0,0 +1,25 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ // A nearer type-only declaration does not shadow a runtime value in an outer scope.
3
+ export const findValueVariable = (initialScope, name) => {
4
+ for (let scope = initialScope; scope; scope = scope.upper) {
5
+ const variable = scope.set.get(name);
6
+ // Plain JavaScript parsers do not supply the TypeScript namespace flag.
7
+ if (variable &&
8
+ variable.isValueVariable !== false &&
9
+ (variable.defs.length === 0 ||
10
+ variable.defs.some((definition) => {
11
+ if (definition.isVariableDefinition === false) {
12
+ return false;
13
+ }
14
+ if (definition.type !== 'ImportBinding') {
15
+ return true;
16
+ }
17
+ return (definition.parent.importKind !== 'type' &&
18
+ !(definition.node.type === AST_NODE_TYPES.ImportSpecifier &&
19
+ definition.node.importKind === 'type'));
20
+ }))) {
21
+ return variable;
22
+ }
23
+ }
24
+ return undefined;
25
+ };
@@ -0,0 +1,2 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const noComponentState: ESLintUtils.RuleModule<"componentState", [], unknown, ESLintUtils.RuleListener>;
@@ -0,0 +1,46 @@
1
+ import { AST_NODE_TYPES, ESLintUtils, } from '@typescript-eslint/utils';
2
+ import { runeCallee } from '../lib/rune-callee.js';
3
+ import { createComponentRefCheck } from '../lib/component-refs.js';
4
+ export const noComponentState = ESLintUtils.RuleCreator.withoutDocs({
5
+ meta: {
6
+ type: 'suggestion',
7
+ docs: {
8
+ description: 'Disallow component-owned logical state in explicitly selected files.',
9
+ },
10
+ schema: [],
11
+ messages: {
12
+ componentState: '{{rune}} creates component-owned state. Move logical state to root/feature state and use intent actions, or exclude this independent UI component from the rule scope.',
13
+ },
14
+ },
15
+ defaultOptions: [],
16
+ create(context) {
17
+ const source = context.sourceCode;
18
+ const refs = createComponentRefCheck(source);
19
+ const calls = [];
20
+ return {
21
+ SvelteDirective: refs.recordBinding,
22
+ CallExpression(node) {
23
+ const callee = node.callee;
24
+ const identifier = runeCallee(callee, source, '$state', 'raw') ??
25
+ runeCallee(callee, source, '$bindable');
26
+ if (!identifier) {
27
+ return;
28
+ }
29
+ calls.push({
30
+ node,
31
+ rune: callee.type === AST_NODE_TYPES.MemberExpression
32
+ ? '$state.raw'
33
+ : identifier.name,
34
+ });
35
+ },
36
+ 'Program:exit'() {
37
+ calls.forEach(({ node, rune }) => {
38
+ if (rune !== '$bindable' && refs.allows(node)) {
39
+ return;
40
+ }
41
+ context.report({ node, messageId: 'componentState', data: { rune } });
42
+ });
43
+ },
44
+ };
45
+ },
46
+ });
@@ -0,0 +1,2 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const requireFeatureStateSelector: ESLintUtils.RuleModule<"selectFeature", [], unknown, ESLintUtils.RuleListener>;
@@ -0,0 +1,58 @@
1
+ import { svelteScripts, scriptHasAttribute, inScript, } from '../lib/svelte-scripts.js';
2
+ import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
3
+ import { memberKey } from '../lib/member-key.js';
4
+ import { createStoreStateCheck } from '../lib/store-state.js';
5
+ import { reactiveContext } from '../lib/reactive-context.js';
6
+ import { createRawStateCheck } from '../lib/raw-state.js';
7
+ import { selectorFix } from '../lib/selector-fix.js';
8
+ export const requireFeatureStateSelector = ESLintUtils.RuleCreator.withoutDocs({
9
+ meta: {
10
+ type: 'suggestion',
11
+ docs: { description: 'Select Store state before reactive calls.' },
12
+ schema: [],
13
+ fixable: 'code',
14
+ messages: {
15
+ selectFeature: 'Select the feature with a separate $derived before computing its presentation: const user = $derived(store.state.user); const view = $derived(presenter(user)); This skips downstream updates when the feature reference is unchanged.',
16
+ },
17
+ },
18
+ defaultOptions: [],
19
+ create(context) {
20
+ const typedScripts = svelteScripts(context.sourceCode)?.filter((script) => {
21
+ return scriptHasAttribute(script, 'lang', 'ts');
22
+ });
23
+ if (typedScripts?.length === 0) {
24
+ return {};
25
+ }
26
+ const services = ESLintUtils.getParserServices(context);
27
+ const isStore = createStoreStateCheck(services);
28
+ const isRawState = createRawStateCheck(context.sourceCode);
29
+ return {
30
+ MemberExpression(node) {
31
+ if (typedScripts && !inScript(node, typedScripts)) {
32
+ return;
33
+ }
34
+ const storeState = memberKey(node) === 'state' && isStore(node.object);
35
+ if (!storeState && !isRawState(node.object)) {
36
+ return;
37
+ }
38
+ const reactive = reactiveContext(node, context.sourceCode);
39
+ if (!reactive) {
40
+ return;
41
+ }
42
+ const parent = node.parent;
43
+ const location = storeState &&
44
+ parent.type === AST_NODE_TYPES.MemberExpression &&
45
+ parent.object === node
46
+ ? parent
47
+ : node;
48
+ context.report({
49
+ node: location,
50
+ messageId: 'selectFeature',
51
+ fix: reactive.call.type === AST_NODE_TYPES.CallExpression
52
+ ? selectorFix(storeState ? node : node.object, reactive.derived, reactive.call, context.sourceCode)
53
+ : undefined,
54
+ });
55
+ },
56
+ };
57
+ },
58
+ });
@@ -0,0 +1,2 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const singleIntentDispatch: ESLintUtils.RuleModule<"multipleDispatch" | "conditionalDispatch", [], unknown, ESLintUtils.RuleListener>;