|
"use strict"; |
|
Object.defineProperty(exports, "__esModule", { value: true }); |
|
const eslint_utils_1 = require("@eslint-community/eslint-utils"); |
|
const utils_1 = require("../utils"); |
|
const ast_utils_1 = require("../utils/ast-utils"); |
|
const svelte_eslint_parser_1 = require("svelte-eslint-parser"); |
|
const compat_1 = require("../utils/compat"); |
|
|
|
|
|
|
|
function extractTickReferences(context) { |
|
const referenceTracker = new eslint_utils_1.ReferenceTracker((0, compat_1.getSourceCode)(context).scopeManager.globalScope); |
|
const a = referenceTracker.iterateEsmReferences({ |
|
svelte: { |
|
[eslint_utils_1.ReferenceTracker.ESM]: true, |
|
tick: { |
|
[eslint_utils_1.ReferenceTracker.CALL]: true |
|
} |
|
} |
|
}); |
|
return Array.from(a).map(({ node, path }) => { |
|
return { |
|
node: node, |
|
name: path[path.length - 1] |
|
}; |
|
}); |
|
} |
|
|
|
|
|
|
|
function extractTaskReferences(context) { |
|
const referenceTracker = new eslint_utils_1.ReferenceTracker((0, compat_1.getSourceCode)(context).scopeManager.globalScope); |
|
const a = referenceTracker.iterateGlobalReferences({ |
|
setTimeout: { [eslint_utils_1.ReferenceTracker.CALL]: true }, |
|
setInterval: { [eslint_utils_1.ReferenceTracker.CALL]: true }, |
|
queueMicrotask: { [eslint_utils_1.ReferenceTracker.CALL]: true } |
|
}); |
|
return Array.from(a).map(({ node, path }) => { |
|
return { |
|
node: node, |
|
name: path[path.length - 1] |
|
}; |
|
}); |
|
} |
|
|
|
|
|
|
|
function isChildNode(maybeAncestorNode, node) { |
|
let parent = node.parent; |
|
while (parent) { |
|
if (parent === maybeAncestorNode) |
|
return true; |
|
parent = parent.parent; |
|
} |
|
return false; |
|
} |
|
|
|
|
|
|
|
function isFunctionCall(node) { |
|
if (node.type !== 'Identifier') |
|
return false; |
|
const { parent } = node; |
|
if (parent?.type !== 'CallExpression') |
|
return false; |
|
return parent.callee.type === 'Identifier' && parent.callee.name === node.name; |
|
} |
|
|
|
|
|
|
|
function isReactiveVariableNode(reactiveVariableReferences, node) { |
|
if (node.type !== 'Identifier') |
|
return false; |
|
return reactiveVariableReferences.includes(node); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function isNodeForAssign(node) { |
|
const { parent } = node; |
|
if (parent?.type === 'AssignmentExpression') { |
|
return parent.left.type === 'Identifier' && parent.left.name === node.name; |
|
} |
|
return (parent?.type === 'MemberExpression' && |
|
parent.parent?.type === 'AssignmentExpression' && |
|
parent.parent.left.type === 'MemberExpression' && |
|
parent.parent.left.object.type === 'Identifier' && |
|
parent.parent.left.object.name === node.name); |
|
} |
|
|
|
|
|
|
|
function isPromiseThenOrCatchBody(node) { |
|
if (!getDeclarationBody(node)) |
|
return false; |
|
const { parent } = node; |
|
if (parent?.type !== 'CallExpression' || parent?.callee?.type !== 'MemberExpression') { |
|
return false; |
|
} |
|
const { property } = parent.callee; |
|
if (property?.type !== 'Identifier') |
|
return false; |
|
return ['then', 'catch'].includes(property.name); |
|
} |
|
|
|
|
|
|
|
function getReactiveVariableReferences(context) { |
|
const scopeManager = (0, compat_1.getSourceCode)(context).scopeManager; |
|
|
|
|
|
const toplevelScope = scopeManager.globalScope?.childScopes.find((scope) => scope.type === 'module') || |
|
scopeManager.globalScope; |
|
if (!toplevelScope) { |
|
return []; |
|
} |
|
|
|
const reactiveVariableNodes = []; |
|
for (const variable of toplevelScope.variables) { |
|
for (const reference of variable.references) { |
|
if (reference.identifier.type === 'Identifier' && !isFunctionCall(reference.identifier)) { |
|
reactiveVariableNodes.push(reference.identifier); |
|
} |
|
} |
|
} |
|
return reactiveVariableNodes; |
|
} |
|
|
|
|
|
|
|
function getTrackedVariableNodes(reactiveVariableReferences, ast) { |
|
const reactiveVariableNodes = new Set(); |
|
for (const identifier of reactiveVariableReferences) { |
|
if ( |
|
|
|
|
|
ast.range[0] <= identifier.range[0] && |
|
identifier.range[1] <= ast.range[1]) { |
|
reactiveVariableNodes.add(identifier); |
|
} |
|
} |
|
return reactiveVariableNodes; |
|
} |
|
|
|
function getDeclarationBody(node, functionName) { |
|
if (node.type === 'VariableDeclarator' && |
|
node.id.type === 'Identifier' && |
|
(!functionName || node.id.name === functionName)) { |
|
if (node.init?.type === 'ArrowFunctionExpression' || node.init?.type === 'FunctionExpression') { |
|
return node.init.body; |
|
} |
|
} |
|
else if (node.type === 'FunctionDeclaration' && |
|
node.id?.type === 'Identifier' && |
|
(!functionName || node.id?.name === functionName)) { |
|
return node.body; |
|
} |
|
else if (!functionName && node.type === 'ArrowFunctionExpression') { |
|
return node.body; |
|
} |
|
return null; |
|
} |
|
|
|
function getFunctionDeclarationNode(context, functionCall) { |
|
const variable = (0, ast_utils_1.findVariable)(context, functionCall); |
|
if (!variable) { |
|
return null; |
|
} |
|
for (const def of variable.defs) { |
|
if (def.type === 'FunctionName') { |
|
if (def.node.type === 'FunctionDeclaration') { |
|
return def.node.body; |
|
} |
|
} |
|
if (def.type === 'Variable') { |
|
if (def.node.init && |
|
(def.node.init.type === 'FunctionExpression' || |
|
def.node.init.type === 'ArrowFunctionExpression')) { |
|
return def.node.init.body; |
|
} |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isInsideOfFunction(node) { |
|
let parent = node; |
|
while (parent) { |
|
parent = parent.parent; |
|
if (!parent) |
|
break; |
|
if (parent.type === 'FunctionDeclaration' && parent.async) |
|
return true; |
|
if (parent.type === 'VariableDeclarator' && |
|
(parent.init?.type === 'FunctionExpression' || |
|
parent.init?.type === 'ArrowFunctionExpression') && |
|
parent.init?.async) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
function doLint(context, ast, callFuncIdentifiers, tickCallExpressions, taskReferences, reactiveVariableNames, reactiveVariableReferences, pIsSameTask) { |
|
const processed = new Set(); |
|
verifyInternal(ast, callFuncIdentifiers, pIsSameTask); |
|
|
|
function verifyInternal(ast, callFuncIdentifiers, pIsSameTask) { |
|
if (processed.has(ast)) { |
|
|
|
return; |
|
} |
|
processed.add(ast); |
|
let isSameMicroTask = pIsSameTask; |
|
const differentMicroTaskEnterNodes = []; |
|
(0, svelte_eslint_parser_1.traverseNodes)(ast, { |
|
enterNode(node) { |
|
|
|
if (isPromiseThenOrCatchBody(node)) { |
|
differentMicroTaskEnterNodes.push(node); |
|
isSameMicroTask = false; |
|
} |
|
|
|
for (const { node: callExpression } of [...tickCallExpressions, ...taskReferences]) { |
|
if (isChildNode(callExpression, node)) { |
|
differentMicroTaskEnterNodes.push(node); |
|
isSameMicroTask = false; |
|
} |
|
} |
|
|
|
if (node.parent?.type === 'AssignmentExpression' && |
|
node.parent?.right.type === 'AwaitExpression' && |
|
node.parent?.left === node) { |
|
differentMicroTaskEnterNodes.push(node); |
|
isSameMicroTask = false; |
|
} |
|
if (node.type === 'Identifier' && isFunctionCall(node)) { |
|
|
|
const functionDeclarationNode = getFunctionDeclarationNode(context, node); |
|
if (functionDeclarationNode) { |
|
verifyInternal(functionDeclarationNode, [...callFuncIdentifiers, node], isSameMicroTask); |
|
} |
|
} |
|
if (!isSameMicroTask) { |
|
if (isReactiveVariableNode(reactiveVariableReferences, node) && |
|
reactiveVariableNames.includes(node.name) && |
|
isNodeForAssign(node)) { |
|
context.report({ |
|
node, |
|
loc: node.loc, |
|
messageId: 'unexpected' |
|
}); |
|
callFuncIdentifiers.forEach((callFuncIdentifier) => { |
|
context.report({ |
|
node: callFuncIdentifier, |
|
loc: callFuncIdentifier.loc, |
|
messageId: 'unexpectedCall', |
|
data: { |
|
variableName: node.name |
|
} |
|
}); |
|
}); |
|
} |
|
} |
|
}, |
|
leaveNode(node) { |
|
if (node.type === 'AwaitExpression') { |
|
if (ast.parent?.type === 'SvelteReactiveStatement') { |
|
|
|
|
|
if (!isInsideOfFunction(node)) { |
|
isSameMicroTask = false; |
|
} |
|
} |
|
else { |
|
isSameMicroTask = false; |
|
} |
|
} |
|
if (differentMicroTaskEnterNodes.includes(node)) { |
|
isSameMicroTask = true; |
|
} |
|
} |
|
}); |
|
} |
|
} |
|
exports.default = (0, utils_1.createRule)('infinite-reactive-loop', { |
|
meta: { |
|
docs: { |
|
description: "Svelte runtime prevents calling the same reactive statement twice in a microtask. But between different microtask, it doesn't prevent.", |
|
category: 'Possible Errors', |
|
|
|
recommended: false |
|
}, |
|
schema: [], |
|
messages: { |
|
unexpected: 'Possibly it may occur an infinite reactive loop.', |
|
unexpectedCall: 'Possibly it may occur an infinite reactive loop because this function may update `{{variableName}}`.' |
|
}, |
|
type: 'suggestion' |
|
}, |
|
create(context) { |
|
return { |
|
['SvelteReactiveStatement']: (ast) => { |
|
const tickCallExpressions = extractTickReferences(context); |
|
const taskReferences = extractTaskReferences(context); |
|
const reactiveVariableReferences = getReactiveVariableReferences(context); |
|
const trackedVariableNodes = getTrackedVariableNodes(reactiveVariableReferences, ast); |
|
doLint(context, ast.body, [], tickCallExpressions, taskReferences, Array.from(trackedVariableNodes).map((node) => node.name), reactiveVariableReferences, true); |
|
} |
|
}; |
|
} |
|
}); |
|
|