Skip to content

Commit 88c4eaa

Browse files
committed
add internal eslint rule
1 parent c891a92 commit 88c4eaa

File tree

7 files changed

+197
-14
lines changed

7 files changed

+197
-14
lines changed

packages/eslint-plugin-svelte/eslint.config.mjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import * as myPlugin from '@ota-meshi/eslint-plugin';
22
import * as tseslint from 'typescript-eslint';
3+
import { createJiti } from 'jiti';
4+
const jiti = createJiti(import.meta.url);
5+
const internal = {
6+
rules: {
7+
'prefer-find-variable-safe': await jiti.import('./internal-rules/prefer-find-variable-safe.ts')
8+
}
9+
};
310

411
/**
512
* @type {import('eslint').Linter.Config[]}
@@ -54,6 +61,9 @@ const config = [
5461
},
5562
{
5663
files: ['src/**'],
64+
plugins: {
65+
internal
66+
},
5767
rules: {
5868
'@typescript-eslint/no-restricted-imports': [
5969
'error',
@@ -80,7 +90,8 @@ const config = [
8090
{ object: 'context', property: 'getCwd', message: 'Use `context.cwd`' },
8191
{ object: 'context', property: 'getScope', message: 'Use src/utils/compat.ts' },
8292
{ object: 'context', property: 'parserServices', message: 'Use src/utils/compat.ts' }
83-
]
93+
],
94+
'internal/prefer-find-variable-safe': 'error'
8495
}
8596
},
8697
...tseslint.config({
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import type { Rule } from 'eslint';
2+
import { ReferenceTracker, findVariable } from '@eslint-community/eslint-utils';
3+
import path from 'node:path';
4+
import type { TSESTree } from '@typescript-eslint/types';
5+
import type { Variable } from '@typescript-eslint/scope-manager';
6+
7+
export default {
8+
meta: {
9+
docs: {
10+
description: 'enforce to use findVariableSafe() to avoid infinite recursion',
11+
category: 'Best Practices',
12+
recommended: false,
13+
conflictWithPrettier: false,
14+
url: 'https://github.com/sveltejs/eslint-plugin-svelte/blob/v3.12.3/docs/rules/prefer-find-variable-safe.md'
15+
},
16+
messages: {
17+
preferFindVariableSafe: 'Prefer to use findVariableSafe() to avoid infinite recursion.'
18+
},
19+
schema: [],
20+
type: 'suggestion'
21+
},
22+
create(context: Rule.RuleContext): Rule.RuleListener {
23+
const referenceTracker = new ReferenceTracker(
24+
context.sourceCode.scopeManager.globalScope as never
25+
);
26+
let astUtilsPath = path.relative(
27+
path.dirname(context.physicalFilename),
28+
path.join(import.meta.dirname, '..', 'src', 'utils', 'ast-utils')
29+
);
30+
if (!astUtilsPath.startsWith('.')) {
31+
astUtilsPath = `./${astUtilsPath}`;
32+
}
33+
const findVariableCalls = [
34+
...referenceTracker.iterateEsmReferences({
35+
[astUtilsPath]: {
36+
[ReferenceTracker.ESM]: true,
37+
findVariable: {
38+
[ReferenceTracker.CALL]: true
39+
}
40+
},
41+
[`${astUtilsPath}.js`]: {
42+
[ReferenceTracker.ESM]: true,
43+
findVariable: {
44+
[ReferenceTracker.CALL]: true
45+
}
46+
},
47+
[`${astUtilsPath}.ts`]: {
48+
[ReferenceTracker.ESM]: true,
49+
findVariable: {
50+
[ReferenceTracker.CALL]: true
51+
}
52+
}
53+
})
54+
];
55+
type FunctionContext = {
56+
node:
57+
| TSESTree.FunctionDeclaration
58+
| TSESTree.FunctionExpression
59+
| TSESTree.ArrowFunctionExpression;
60+
identifier: TSESTree.Identifier | null;
61+
findVariableCall?: TSESTree.CallExpression;
62+
calls: Set<TSESTree.Identifier>;
63+
upper: FunctionContext | null;
64+
};
65+
let functionStack: FunctionContext | null = null;
66+
const functionContexts: FunctionContext[] = [];
67+
68+
function getFunctionVariableName(
69+
node:
70+
| TSESTree.FunctionDeclaration
71+
| TSESTree.FunctionExpression
72+
| TSESTree.ArrowFunctionExpression
73+
) {
74+
if (node.type === 'FunctionDeclaration') {
75+
return node.id;
76+
}
77+
if (node.parent?.type === 'VariableDeclarator' && node.parent.id.type === 'Identifier') {
78+
return node.parent.id;
79+
}
80+
return null;
81+
}
82+
83+
function* iterateVariables(node: TSESTree.Identifier) {
84+
const visitedNodes = new Set<TSESTree.Identifier>();
85+
let currentNode: TSESTree.Identifier | null = node;
86+
while (currentNode) {
87+
if (visitedNodes.has(currentNode)) break;
88+
const variable = findVariable(
89+
context.sourceCode.getScope(currentNode),
90+
currentNode
91+
) as Variable | null;
92+
if (!variable) break;
93+
yield variable;
94+
const def = variable.defs[0];
95+
if (!def) break;
96+
if (def.type !== 'Variable' || !def.node.init) break;
97+
if (def.node.init.type !== 'Identifier') break;
98+
currentNode = def.node.init;
99+
visitedNodes.add(currentNode);
100+
}
101+
}
102+
103+
/**
104+
* Verify a function context to report if necessary.
105+
* Reports when a function contains a call to findVariable and the function is recursive.
106+
*/
107+
function verifyFunctionContext(functionContext: FunctionContext) {
108+
if (!functionContext.findVariableCall) return;
109+
if (!hasRecursive(functionContext)) return;
110+
context.report({
111+
node: functionContext.findVariableCall,
112+
messageId: 'preferFindVariableSafe'
113+
});
114+
}
115+
116+
function hasRecursive(functionContext: FunctionContext) {
117+
const buffer = [functionContext];
118+
const visitedContext = new Set<FunctionContext>();
119+
let current;
120+
while ((current = buffer.shift())) {
121+
if (visitedContext.has(current)) continue;
122+
visitedContext.add(current);
123+
if (!current.identifier) continue;
124+
for (const variable of iterateVariables(current.identifier)) {
125+
for (const { identifier } of variable.references) {
126+
if (identifier.type !== 'Identifier') continue;
127+
if (functionContext.calls.has(identifier)) {
128+
return true;
129+
}
130+
buffer.push(...functionContexts.filter((ctx) => ctx.calls.has(identifier)));
131+
}
132+
}
133+
}
134+
return false;
135+
}
136+
137+
return {
138+
':function'(
139+
node:
140+
| TSESTree.FunctionDeclaration
141+
| TSESTree.FunctionExpression
142+
| TSESTree.ArrowFunctionExpression
143+
) {
144+
functionStack = {
145+
node,
146+
identifier: getFunctionVariableName(node),
147+
calls: new Set(),
148+
upper: functionStack
149+
};
150+
functionContexts.push(functionStack);
151+
},
152+
':function:exit'() {
153+
functionStack = functionStack?.upper || null;
154+
},
155+
CallExpression(node) {
156+
if (!functionStack) return;
157+
if (findVariableCalls.some((call) => call.node === node)) {
158+
functionStack.findVariableCall = node;
159+
}
160+
if (node.callee.type === 'Identifier') {
161+
functionStack.calls.add(node.callee);
162+
}
163+
},
164+
'Program:exit'() {
165+
for (const functionContext of functionContexts) {
166+
verifyFunctionContext(functionContext);
167+
}
168+
}
169+
};
170+
}
171+
};

packages/eslint-plugin-svelte/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"eslint-typegen": "^2.3.0",
9292
"eslint-visitor-keys": "^4.2.1",
9393
"espree": "^10.4.0",
94+
"jiti": "^2.5.1",
9495
"less": "^4.4.1",
9596
"mocha": "~11.7.2",
9697
"postcss-nested": "^7.0.2",

packages/eslint-plugin-svelte/src/rules/no-immutable-reactive-statements.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { AST } from 'svelte-eslint-parser';
22
import { createRule } from '../utils/index.js';
33
import type { Scope, Variable, Reference, Definition } from '@typescript-eslint/scope-manager';
44
import type { TSESTree } from '@typescript-eslint/types';
5-
import { findVariable, iterateIdentifiers } from '../utils/ast-utils.js';
5+
import { findVariableSafe, iterateIdentifiers } from '../utils/ast-utils.js';
66

77
export default createRule('no-immutable-reactive-statements', {
88
meta: {
@@ -153,7 +153,7 @@ export default createRule('no-immutable-reactive-statements', {
153153
/** Checks whether the given pattern has writing or not. */
154154
function hasWriteReference(pattern: TSESTree.DestructuringPattern): boolean {
155155
for (const id of iterateIdentifiers(pattern)) {
156-
const variable = findVariable(context, id);
156+
const variable = findVariableSafe(hasWriteReference, context, id);
157157
if (variable && hasWrite(variable)) return true;
158158
}
159159

packages/eslint-plugin-svelte/src/rules/no-not-function-handler.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AST } from 'svelte-eslint-parser';
22
import type { TSESTree } from '@typescript-eslint/types';
33
import { createRule } from '../utils/index.js';
4-
import { findVariable } from '../utils/ast-utils.js';
4+
import { findVariableSafe } from '../utils/ast-utils.js';
55
import { EVENT_NAMES } from '../utils/events.js';
66

77
const PHRASES = {
@@ -37,23 +37,19 @@ export default createRule('no-not-function-handler', {
3737
},
3838
create(context) {
3939
/** Find data expression */
40-
function findRootExpression(
41-
node: TSESTree.Expression,
42-
already = new Set<TSESTree.Identifier>()
43-
): TSESTree.Expression {
44-
if (node.type !== 'Identifier' || already.has(node)) {
40+
function findRootExpression(node: TSESTree.Expression): TSESTree.Expression {
41+
if (node.type !== 'Identifier') {
4542
return node;
4643
}
47-
already.add(node);
48-
const variable = findVariable(context, node);
44+
const variable = findVariableSafe(findRootExpression, context, node);
4945
if (!variable || variable.defs.length !== 1) {
5046
return node;
5147
}
5248
const def = variable.defs[0];
5349
if (def.type === 'Variable') {
5450
if (def.parent.kind === 'const' && def.node.init) {
5551
const init = def.node.init;
56-
return findRootExpression(init, already);
52+
return findRootExpression(init);
5753
}
5854
}
5955
return node;

packages/eslint-plugin-svelte/tsconfig.build.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"tools/**/*",
66
"typings/**/*",
77
"vite.config.mts",
8-
"docs-svelte-kit/**/*.mts"
8+
"docs-svelte-kit/**/*.mts",
9+
"internal-rules/**/*",
10+
"eslint.config.mts"
911
]
1012
}

packages/eslint-plugin-svelte/tsconfig.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
"tests/utils/**/*",
2727
"tools/**/*",
2828
"vite.config.mts",
29-
"docs-svelte-kit/**/*.mts"
29+
"docs-svelte-kit/**/*.mts",
30+
"internal-rules/**/*",
31+
"eslint.config.mts"
3032
],
3133
"exclude": ["lib/**/*", "tests/fixtures/**/*"]
3234
}

0 commit comments

Comments
 (0)