@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
|
@@ -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,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,18 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
export const memberKey = (node, { numeric = false } = {}) => {
|
|
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
|
+
if (node.property.raw.startsWith("'") ||
|
|
8
|
+
node.property.raw.startsWith('"')) {
|
|
9
|
+
return String(node.property.value);
|
|
10
|
+
}
|
|
11
|
+
// Match numeric AST literals, including values such as 1e999; finiteness
|
|
12
|
+
// would change syntax classification. Existing callers remain names-only.
|
|
13
|
+
if (numeric && typeof node.property.value === 'number') {
|
|
14
|
+
return String(node.property.value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
};
|
|
@@ -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,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,17 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import { memberKey } from './member-key.js';
|
|
3
|
+
export const readyBooleanExpression = (node) => {
|
|
4
|
+
if (node.type === AST_NODE_TYPES.Identifier) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
if (node.type === AST_NODE_TYPES.ChainExpression) {
|
|
8
|
+
return readyBooleanExpression(node.expression);
|
|
9
|
+
}
|
|
10
|
+
if (node.type !== AST_NODE_TYPES.MemberExpression) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const staticKey = memberKey(node, { numeric: true }) !== undefined;
|
|
14
|
+
return (staticKey &&
|
|
15
|
+
node.object.type !== AST_NODE_TYPES.Super &&
|
|
16
|
+
readyBooleanExpression(node.object));
|
|
17
|
+
};
|
|
@@ -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,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 {};
|