Skip to content

Commit fe306ed

Browse files
authored
fix: maximum call stack error in svelte/infinite-reactive-loop rule (#418)
1 parent aa773b6 commit fe306ed

File tree

3 files changed

+97
-76
lines changed

3 files changed

+97
-76
lines changed

.changeset/pretty-emus-hammer.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-svelte": patch
3+
---
4+
5+
fix: maximum call stack error in `svelte/infinite-reactive-loop` rule

src/rules/infinite-reactive-loop.ts

+87-76
Original file line numberDiff line numberDiff line change
@@ -274,101 +274,112 @@ function doLint(
274274
reactiveVariableReferences: TSESTree.Identifier[],
275275
pIsSameTask: boolean,
276276
) {
277-
let isSameMicroTask = pIsSameTask
277+
const processed = new Set<TSESTree.Node>()
278+
verifyInternal(ast, callFuncIdentifiers, pIsSameTask)
278279

279-
const differentMicroTaskEnterNodes: TSESTree.Node[] = []
280+
/** verify for node */
281+
function verifyInternal(
282+
ast: TSESTree.Node,
283+
callFuncIdentifiers: TSESTree.Identifier[],
284+
pIsSameTask: boolean,
285+
) {
286+
if (processed.has(ast)) {
287+
// Avoid infinite recursion with recursive references.
288+
return
289+
}
290+
processed.add(ast)
280291

281-
traverseNodes(ast, {
282-
enterNode(node) {
283-
// Promise.then() or Promise.catch() is called.
284-
if (isPromiseThenOrCatchBody(node)) {
285-
differentMicroTaskEnterNodes.push(node)
286-
isSameMicroTask = false
287-
}
292+
let isSameMicroTask = pIsSameTask
288293

289-
// `tick`, `setTimeout`, `setInterval` , `queueMicrotask` is called
290-
for (const { node: callExpression } of [
291-
...tickCallExpressions,
292-
...taskReferences,
293-
]) {
294-
if (isChildNode(callExpression, node)) {
294+
const differentMicroTaskEnterNodes: TSESTree.Node[] = []
295+
296+
traverseNodes(ast, {
297+
enterNode(node) {
298+
// Promise.then() or Promise.catch() is called.
299+
if (isPromiseThenOrCatchBody(node)) {
295300
differentMicroTaskEnterNodes.push(node)
296301
isSameMicroTask = false
297302
}
298-
}
299303

300-
// left side of await block
301-
if (
302-
node.parent?.type === "AssignmentExpression" &&
303-
node.parent?.right.type === "AwaitExpression" &&
304-
node.parent?.left === node
305-
) {
306-
differentMicroTaskEnterNodes.push(node)
307-
isSameMicroTask = false
308-
}
309-
310-
if (node.type === "Identifier" && isFunctionCall(node)) {
311-
// traverse used functions body
312-
const functionDeclarationNode = getFunctionDeclarationNode(
313-
context,
314-
node,
315-
)
316-
if (functionDeclarationNode) {
317-
doLint(
318-
context,
319-
functionDeclarationNode,
320-
[...callFuncIdentifiers, node],
321-
tickCallExpressions,
322-
taskReferences,
323-
reactiveVariableNames,
324-
reactiveVariableReferences,
325-
isSameMicroTask,
326-
)
304+
// `tick`, `setTimeout`, `setInterval` , `queueMicrotask` is called
305+
for (const { node: callExpression } of [
306+
...tickCallExpressions,
307+
...taskReferences,
308+
]) {
309+
if (isChildNode(callExpression, node)) {
310+
differentMicroTaskEnterNodes.push(node)
311+
isSameMicroTask = false
312+
}
327313
}
328-
}
329314

330-
if (!isSameMicroTask) {
315+
// left side of await block
331316
if (
332-
isReactiveVariableNode(reactiveVariableReferences, node) &&
333-
reactiveVariableNames.includes(node.name) &&
334-
isNodeForAssign(node)
317+
node.parent?.type === "AssignmentExpression" &&
318+
node.parent?.right.type === "AwaitExpression" &&
319+
node.parent?.left === node
335320
) {
336-
context.report({
321+
differentMicroTaskEnterNodes.push(node)
322+
isSameMicroTask = false
323+
}
324+
325+
if (node.type === "Identifier" && isFunctionCall(node)) {
326+
// traverse used functions body
327+
const functionDeclarationNode = getFunctionDeclarationNode(
328+
context,
337329
node,
338-
loc: node.loc,
339-
messageId: "unexpected",
340-
})
341-
callFuncIdentifiers.forEach((callFuncIdentifier) => {
330+
)
331+
if (functionDeclarationNode) {
332+
verifyInternal(
333+
functionDeclarationNode,
334+
[...callFuncIdentifiers, node],
335+
isSameMicroTask,
336+
)
337+
}
338+
}
339+
340+
if (!isSameMicroTask) {
341+
if (
342+
isReactiveVariableNode(reactiveVariableReferences, node) &&
343+
reactiveVariableNames.includes(node.name) &&
344+
isNodeForAssign(node)
345+
) {
342346
context.report({
343-
node: callFuncIdentifier,
344-
loc: callFuncIdentifier.loc,
345-
messageId: "unexpectedCall",
346-
data: {
347-
variableName: node.name,
348-
},
347+
node,
348+
loc: node.loc,
349+
messageId: "unexpected",
350+
})
351+
callFuncIdentifiers.forEach((callFuncIdentifier) => {
352+
context.report({
353+
node: callFuncIdentifier,
354+
loc: callFuncIdentifier.loc,
355+
messageId: "unexpectedCall",
356+
data: {
357+
variableName: node.name,
358+
},
359+
})
349360
})
350-
})
361+
}
351362
}
352-
}
353-
},
354-
leaveNode(node) {
355-
if (node.type === "AwaitExpression") {
356-
if ((ast.parent?.type as string) === "SvelteReactiveStatement") {
357-
// MEMO: It checks that `await` is used in reactive statement directly or not.
358-
// If `await` is used in inner function of a reactive statement, result of `isInsideOfFunction` will be `true`.
359-
if (!isInsideOfFunction(node)) {
363+
},
364+
leaveNode(node) {
365+
if (node.type === "AwaitExpression") {
366+
if ((ast.parent?.type as string) === "SvelteReactiveStatement") {
367+
// MEMO: It checks that `await` is used in reactive statement directly or not.
368+
// If `await` is used in inner function of a reactive statement, result of `isInsideOfFunction` will be `true`.
369+
if (!isInsideOfFunction(node)) {
370+
isSameMicroTask = false
371+
}
372+
} else {
360373
isSameMicroTask = false
361374
}
362-
} else {
363-
isSameMicroTask = false
364375
}
365-
}
366376

367-
if (differentMicroTaskEnterNodes.includes(node)) {
368-
isSameMicroTask = true
369-
}
370-
},
371-
})
377+
if (differentMicroTaskEnterNodes.includes(node)) {
378+
isSameMicroTask = true
379+
}
380+
},
381+
})
382+
}
372383
}
373384

374385
export default createRule("infinite-reactive-loop", {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
$: {
3+
const foo = (recurse) => (recurse ? foo(false) : undefined)
4+
}
5+
</script>

0 commit comments

Comments
 (0)