diff --git a/.changeset/slimy-brooms-report.md b/.changeset/slimy-brooms-report.md
new file mode 100644
index 000000000..6521a8946
--- /dev/null
+++ b/.changeset/slimy-brooms-report.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-svelte": minor
+---
+
+feat: add `svelte/Infinite-reactive-loop` rule
diff --git a/README.md b/README.md
index 7f4340e4e..534bc6460 100644
--- a/README.md
+++ b/README.md
@@ -297,6 +297,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
| Rule ID | Description | |
|:--------|:------------|:---|
+| [svelte/infinite-reactive-loop](https://ota-meshi.github.io/eslint-plugin-svelte/rules/infinite-reactive-loop/) | Svelte runtime prevents calling the same reactive statement twice in a microtask. But between different microtask, it doesn't prevent. | |
| [svelte/no-dom-manipulating](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dom-manipulating/) | disallow DOM manipulating | |
| [svelte/no-dupe-else-if-blocks](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-else-if-blocks/) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
| [svelte/no-dupe-on-directives](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-on-directives/) | disallow duplicate `on:` directives | |
diff --git a/docs/rules.md b/docs/rules.md
index eab73c0bf..1b53df68e 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -16,6 +16,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
| Rule ID | Description | |
|:--------|:------------|:---|
+| [svelte/infinite-reactive-loop](./rules/infinite-reactive-loop.md) | Svelte runtime prevents calling the same reactive statement twice in a microtask. But between different microtask, it doesn't prevent. | |
| [svelte/no-dom-manipulating](./rules/no-dom-manipulating.md) | disallow DOM manipulating | |
| [svelte/no-dupe-else-if-blocks](./rules/no-dupe-else-if-blocks.md) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
| [svelte/no-dupe-on-directives](./rules/no-dupe-on-directives.md) | disallow duplicate `on:` directives | |
diff --git a/docs/rules/infinite-reactive-loop.md b/docs/rules/infinite-reactive-loop.md
new file mode 100644
index 000000000..c34aa6112
--- /dev/null
+++ b/docs/rules/infinite-reactive-loop.md
@@ -0,0 +1,100 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "svelte/infinite-reactive-loop"
+description: "Svelte runtime prevents calling the same reactive statement twice in a microtask. But between different microtask, it doesn't prevent."
+---
+
+# svelte/infinite-reactive-loop
+
+> Svelte runtime prevents calling the same reactive statement twice in a microtask. But between different microtask, it doesn't prevent.
+
+- :exclamation: **_This rule has not been released yet._**
+
+## :book: Rule Details
+
+Svelte runtime prevents calling the same reactive statement twice in a microtask.
+But between different microtask, it doesn't prevent.
+This rule reports those possible infinite loop.
+
+
+
+
+
+```svelte
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [Svelte - Docs > COMPONENT FORMAT > 3. $: marks a statement as reactive](https://svelte.dev/docs#component-format-script-3-$-marks-a-statement-as-reactive)
+- [Svelte - Docs > COMPONENT FORMAT > 4. Prefix stores with $ to access their values](https://svelte.dev/docs#component-format-script-4-prefix-stores-with-$-to-access-their-values)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/infinite-reactive-loop.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/infinite-reactive-loop.ts)
diff --git a/package.json b/package.json
index d78e8525a..55c8e498a 100644
--- a/package.json
+++ b/package.json
@@ -174,7 +174,7 @@
"access": "public"
},
"typeCoverage": {
- "atLeast": 99.05,
+ "atLeast": 99.08,
"cache": true,
"detail": true,
"ignoreAsAssertion": true,
diff --git a/src/rules/infinite-reactive-loop.ts b/src/rules/infinite-reactive-loop.ts
new file mode 100644
index 000000000..83586ac77
--- /dev/null
+++ b/src/rules/infinite-reactive-loop.ts
@@ -0,0 +1,412 @@
+import type { TSESTree } from "@typescript-eslint/types"
+import type { AST } from "svelte-eslint-parser"
+import { ReferenceTracker } from "eslint-utils"
+import { createRule } from "../utils"
+import type { RuleContext } from "../types"
+import { findVariable } from "../utils/ast-utils"
+import { traverseNodes } from "svelte-eslint-parser"
+
+/**
+ * Get usage of `tick`
+ */
+function extractTickReferences(
+ context: RuleContext,
+): { node: TSESTree.CallExpression; name: string }[] {
+ const referenceTracker = new ReferenceTracker(context.getScope())
+ const a = referenceTracker.iterateEsmReferences({
+ svelte: {
+ [ReferenceTracker.ESM]: true,
+ tick: {
+ [ReferenceTracker.CALL]: true,
+ },
+ },
+ })
+ return Array.from(a).map(({ node, path }) => {
+ return {
+ node: node as TSESTree.CallExpression,
+ name: path[path.length - 1],
+ }
+ })
+}
+
+/**
+ * Get usage of `setTimeout`, `setInterval`, `queueMicrotask`
+ */
+function extractTaskReferences(
+ context: RuleContext,
+): { node: TSESTree.CallExpression; name: string }[] {
+ const referenceTracker = new ReferenceTracker(context.getScope())
+ const a = referenceTracker.iterateGlobalReferences({
+ setTimeout: { [ReferenceTracker.CALL]: true },
+ setInterval: { [ReferenceTracker.CALL]: true },
+ queueMicrotask: { [ReferenceTracker.CALL]: true },
+ })
+ return Array.from(a).map(({ node, path }) => {
+ return {
+ node: node as TSESTree.CallExpression,
+ name: path[path.length - 1],
+ }
+ })
+}
+
+/**
+ * If `node` is inside of `maybeAncestorNode`, return true.
+ */
+function isChildNode(
+ maybeAncestorNode: TSESTree.Node | AST.SvelteNode,
+ node: TSESTree.Node,
+): boolean {
+ let parent = node.parent
+ while (parent) {
+ if (parent === maybeAncestorNode) return true
+ parent = parent.parent
+ }
+ return false
+}
+
+/**
+ * Return true if `node` is a function call.
+ */
+function isFunctionCall(node: TSESTree.Node): boolean {
+ 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
+}
+
+/**
+ * Return true if `node` is a reactive variable.
+ */
+function isReactiveVariableNode(
+ reactiveVariableReferences: TSESTree.Identifier[],
+ node: TSESTree.Node,
+): node is TSESTree.Identifier {
+ if (node.type !== "Identifier") return false
+ return reactiveVariableReferences.includes(node)
+}
+
+/**
+ * e.g. foo.bar = baz + 1
+ * If node is `foo`, return true.
+ * Otherwise, return false.
+ */
+function isNodeForAssign(node: TSESTree.Identifier): boolean {
+ 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
+ )
+}
+
+/**
+ * Return true if `node` is inside of `then` or `catch`.
+ */
+function isPromiseThenOrCatchBody(node: TSESTree.Node): boolean {
+ 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)
+}
+
+/**
+ * Get all reactive variable reference.
+ */
+function getReactiveVariableReferences(context: RuleContext) {
+ const scopeManager = context.getSourceCode().scopeManager
+ // Find the top-level (module or global) scope.
+ // Any variable defined at the top-level (module scope or global scope) can be made reactive.
+ const toplevelScope =
+ scopeManager.globalScope?.childScopes.find(
+ (scope) => scope.type === "module",
+ ) || scopeManager.globalScope
+ if (!toplevelScope) {
+ return []
+ }
+
+ // Extracts all reactive references to variables defined in the top-level scope.
+ const reactiveVariableNodes: TSESTree.Identifier[] = []
+ 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
+}
+
+/**
+ * Get all tracked reactive variables.
+ */
+function getTrackedVariableNodes(
+ reactiveVariableReferences: TSESTree.Identifier[],
+ ast: AST.SvelteReactiveStatement,
+) {
+ const reactiveVariableNodes: Set = new Set()
+ for (const identifier of reactiveVariableReferences) {
+ if (
+ // If the identifier is within the reactive statement range,
+ // it is used within the reactive statement.
+ ast.range[0] <= identifier.range[0] &&
+ identifier.range[1] <= ast.range[1]
+ ) {
+ reactiveVariableNodes.add(identifier)
+ }
+ }
+ return reactiveVariableNodes
+}
+
+/** */
+function getDeclarationBody(
+ node: TSESTree.Node,
+ functionName?: string,
+): TSESTree.BlockStatement | TSESTree.Expression | null {
+ 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: RuleContext,
+ functionCall: TSESTree.Identifier,
+): TSESTree.BlockStatement | TSESTree.Expression | null {
+ const variable = 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
+}
+
+/**
+ * If the node is inside of a function, return true.
+ *
+ * e.g. `$: await foo()`
+ * if `node` is `foo`, return false because reactive statement is not function.
+ *
+ * e.g. `const bar = () => foo()`
+ * if `node` is `foo`, return true.
+ *
+ */
+function isInsideOfFunction(node: TSESTree.Node) {
+ let parent: TSESTree.Node | AST.SvelteReactiveStatement | null = node
+ while (parent) {
+ parent = parent.parent as TSESTree.Node | AST.SvelteReactiveStatement | null
+ 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
+}
+
+/** Let's lint! */
+function doLint(
+ context: RuleContext,
+ ast: TSESTree.Node,
+ callFuncIdentifiers: TSESTree.Identifier[],
+ tickCallExpressions: { node: TSESTree.CallExpression; name: string }[],
+ taskReferences: {
+ node: TSESTree.CallExpression
+ name: string
+ }[],
+ reactiveVariableNames: string[],
+ reactiveVariableReferences: TSESTree.Identifier[],
+ pIsSameTask: boolean,
+) {
+ let isSameMicroTask = pIsSameTask
+
+ const differentMicroTaskEnterNodes: TSESTree.Node[] = []
+
+ traverseNodes(ast, {
+ enterNode(node) {
+ // Promise.then() or Promise.catch() is called.
+ if (isPromiseThenOrCatchBody(node)) {
+ differentMicroTaskEnterNodes.push(node)
+ isSameMicroTask = false
+ }
+
+ // `tick`, `setTimeout`, `setInterval` , `queueMicrotask` is called
+ for (const { node: callExpression } of [
+ ...tickCallExpressions,
+ ...taskReferences,
+ ]) {
+ if (isChildNode(callExpression, node)) {
+ differentMicroTaskEnterNodes.push(node)
+ isSameMicroTask = false
+ }
+ }
+
+ // left side of await block
+ 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)) {
+ // traverse used functions body
+ const functionDeclarationNode = getFunctionDeclarationNode(
+ context,
+ node,
+ )
+ if (functionDeclarationNode) {
+ doLint(
+ context,
+ functionDeclarationNode,
+ [...callFuncIdentifiers, node],
+ tickCallExpressions,
+ taskReferences,
+ reactiveVariableNames,
+ reactiveVariableReferences,
+ 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 as string) === "SvelteReactiveStatement") {
+ // MEMO: It checks that `await` is used in reactive statement directly or not.
+ // If `await` is used in inner function of a reactive statement, result of `isInsideOfFunction` will be `true`.
+ if (!isInsideOfFunction(node)) {
+ isSameMicroTask = false
+ }
+ } else {
+ isSameMicroTask = false
+ }
+ }
+
+ if (differentMicroTaskEnterNodes.includes(node)) {
+ isSameMicroTask = true
+ }
+ },
+ })
+}
+
+export default 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",
+ // TODO Switch to recommended in the major version.
+ 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: AST.SvelteReactiveStatement) => {
+ 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,
+ )
+ },
+ }
+ },
+})
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index ceba1f76b..e9d10b55d 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -8,6 +8,7 @@ import htmlClosingBracketSpacing from "../rules/html-closing-bracket-spacing"
import htmlQuotes from "../rules/html-quotes"
import htmlSelfClosing from "../rules/html-self-closing"
import indent from "../rules/indent"
+import infiniteReactiveLoop from "../rules/infinite-reactive-loop"
import maxAttributesPerLine from "../rules/max-attributes-per-line"
import mustacheSpacing from "../rules/mustache-spacing"
import noAtDebugTags from "../rules/no-at-debug-tags"
@@ -59,6 +60,7 @@ export const rules = [
htmlQuotes,
htmlSelfClosing,
indent,
+ infiniteReactiveLoop,
maxAttributesPerLine,
mustacheSpacing,
noAtDebugTags,
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/await/test01-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/await/test01-errors.yaml
new file mode 100644
index 000000000..3def66667
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/await/test01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 7
+ column: 5
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/await/test01-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/await/test01-input.svelte
new file mode 100644
index 000000000..33226aa52
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/await/test01-input.svelte
@@ -0,0 +1,9 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test01-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test01-errors.yaml
new file mode 100644
index 000000000..360ccfc7d
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 6
+ column: 5
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test01-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test01-input.svelte
new file mode 100644
index 000000000..e9663f8d6
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test01-input.svelte
@@ -0,0 +1,8 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test02-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test02-errors.yaml
new file mode 100644
index 000000000..457e93042
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test02-errors.yaml
@@ -0,0 +1,8 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 8
+ column: 7
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop because this function may update `a`.
+ line: 14
+ column: 11
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test02-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test02-input.svelte
new file mode 100644
index 000000000..83b8f9fbe
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test02-input.svelte
@@ -0,0 +1,21 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test03-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test03-errors.yaml
new file mode 100644
index 000000000..c6b46f0ad
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test03-errors.yaml
@@ -0,0 +1,8 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 8
+ column: 5
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop because this function may update `a`.
+ line: 13
+ column: 11
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test03-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test03-input.svelte
new file mode 100644
index 000000000..a845142c1
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test03-input.svelte
@@ -0,0 +1,20 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test04-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test04-errors.yaml
new file mode 100644
index 000000000..592ba8cb5
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test04-errors.yaml
@@ -0,0 +1,16 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 6
+ column: 5
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 11
+ column: 5
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop because this function may update `a`.
+ line: 17
+ column: 11
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop because this function may update `a`.
+ line: 18
+ column: 5
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test04-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test04-input.svelte
new file mode 100644
index 000000000..62fbca902
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test04-input.svelte
@@ -0,0 +1,26 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test05-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test05-errors.yaml
new file mode 100644
index 000000000..90ec1cf42
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test05-errors.yaml
@@ -0,0 +1,8 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 6
+ column: 5
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop because this function may update `a`.
+ line: 12
+ column: 11
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test05-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test05-input.svelte
new file mode 100644
index 000000000..2ceb8ba99
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test05-input.svelte
@@ -0,0 +1,14 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test06-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test06-errors.yaml
new file mode 100644
index 000000000..c925e6039
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test06-errors.yaml
@@ -0,0 +1,12 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 5
+ column: 5
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop because this function may update `obj`.
+ line: 9
+ column: 11
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 10
+ column: 5
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test06-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test06-input.svelte
new file mode 100644
index 000000000..3225fffac
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test06-input.svelte
@@ -0,0 +1,12 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test07-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test07-errors.yaml
new file mode 100644
index 000000000..7002c04b2
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test07-errors.yaml
@@ -0,0 +1,8 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 5
+ column: 5
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop because this function may update `$store`.
+ line: 11
+ column: 5
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test07-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test07-input.svelte
new file mode 100644
index 000000000..8905d82f5
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test07-input.svelte
@@ -0,0 +1,19 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test08-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test08-errors.yaml
new file mode 100644
index 000000000..62970cbbb
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test08-errors.yaml
@@ -0,0 +1,12 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 6
+ column: 5
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop because this function may update `foo`.
+ line: 12
+ column: 11
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 13
+ column: 5
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test08-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test08-input.svelte
new file mode 100644
index 000000000..e763fe9ad
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test08-input.svelte
@@ -0,0 +1,16 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test09-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test09-errors.yaml
new file mode 100644
index 000000000..f8ec40608
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test09-errors.yaml
@@ -0,0 +1,4 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 11
+ column: 5
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test09-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test09-input.svelte
new file mode 100644
index 000000000..f80ae830f
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test09-input.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test10-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test10-errors.yaml
new file mode 100644
index 000000000..4b2f7de00
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test10-errors.yaml
@@ -0,0 +1,4 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 9
+ column: 30
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test10-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test10-input.svelte
new file mode 100644
index 000000000..397650fdf
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test10-input.svelte
@@ -0,0 +1,11 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test11-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test11-errors.yaml
new file mode 100644
index 000000000..14501f84c
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test11-errors.yaml
@@ -0,0 +1,4 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 8
+ column: 7
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test11-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test11-input.svelte
new file mode 100644
index 000000000..73a77c985
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test11-input.svelte
@@ -0,0 +1,11 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test01-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test01-errors.yaml
new file mode 100644
index 000000000..be27e282c
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test01-errors.yaml
@@ -0,0 +1,12 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 6
+ column: 7
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 7
+ column: 7
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 11
+ column: 7
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test01-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test01-input.svelte
new file mode 100644
index 000000000..baaaeb50d
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test01-input.svelte
@@ -0,0 +1,14 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test02-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test02-errors.yaml
new file mode 100644
index 000000000..97af81ec5
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test02-errors.yaml
@@ -0,0 +1,8 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 10
+ column: 9
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 13
+ column: 9
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test02-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test02-input.svelte
new file mode 100644
index 000000000..e915ee439
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test02-input.svelte
@@ -0,0 +1,17 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test03-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test03-errors.yaml
new file mode 100644
index 000000000..57555ae9e
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test03-errors.yaml
@@ -0,0 +1,8 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 8
+ column: 9
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 11
+ column: 9
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test03-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test03-input.svelte
new file mode 100644
index 000000000..8b8db1788
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test03-input.svelte
@@ -0,0 +1,15 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test04-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test04-errors.yaml
new file mode 100644
index 000000000..3def66667
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test04-errors.yaml
@@ -0,0 +1,4 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 7
+ column: 5
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test04-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test04-input.svelte
new file mode 100644
index 000000000..6579b2373
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test04-input.svelte
@@ -0,0 +1,9 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test05-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test05-errors.yaml
new file mode 100644
index 000000000..8f77e5a5b
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test05-errors.yaml
@@ -0,0 +1,8 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 7
+ column: 9
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 10
+ column: 9
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test05-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test05-input.svelte
new file mode 100644
index 000000000..9459a4ef3
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test05-input.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test06-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test06-errors.yaml
new file mode 100644
index 000000000..8f77e5a5b
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test06-errors.yaml
@@ -0,0 +1,8 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 7
+ column: 9
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 10
+ column: 9
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test06-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test06-input.svelte
new file mode 100644
index 000000000..cea79c5df
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test06-input.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/queueMicrotask/test01-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/queueMicrotask/test01-errors.yaml
new file mode 100644
index 000000000..dd23a84fa
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/queueMicrotask/test01-errors.yaml
@@ -0,0 +1,8 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 7
+ column: 7
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 13
+ column: 7
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/queueMicrotask/test01-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/queueMicrotask/test01-input.svelte
new file mode 100644
index 000000000..361ad448c
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/queueMicrotask/test01-input.svelte
@@ -0,0 +1,16 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/setInterval/test01-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/setInterval/test01-errors.yaml
new file mode 100644
index 000000000..dd23a84fa
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/setInterval/test01-errors.yaml
@@ -0,0 +1,8 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 7
+ column: 7
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 13
+ column: 7
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/setInterval/test01-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/setInterval/test01-input.svelte
new file mode 100644
index 000000000..e76f0bd5b
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/setInterval/test01-input.svelte
@@ -0,0 +1,16 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test01-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test01-errors.yaml
new file mode 100644
index 000000000..b1a41fb0f
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test01-errors.yaml
@@ -0,0 +1,8 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 11
+ column: 7
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 17
+ column: 7
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test01-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test01-input.svelte
new file mode 100644
index 000000000..bb968e665
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test01-input.svelte
@@ -0,0 +1,24 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test02-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test02-errors.yaml
new file mode 100644
index 000000000..48a6fc271
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test02-errors.yaml
@@ -0,0 +1,4 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 5
+ column: 5
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test02-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test02-input.svelte
new file mode 100644
index 000000000..87a7e87ca
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test02-input.svelte
@@ -0,0 +1,7 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test01-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test01-errors.yaml
new file mode 100644
index 000000000..ea025838f
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test01-errors.yaml
@@ -0,0 +1,16 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 8
+ column: 7
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 14
+ column: 7
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 20
+ column: 5
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 25
+ column: 5
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test01-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test01-input.svelte
new file mode 100644
index 000000000..148ca8346
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test01-input.svelte
@@ -0,0 +1,27 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test02-errors.yaml b/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test02-errors.yaml
new file mode 100644
index 000000000..f5fb69487
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test02-errors.yaml
@@ -0,0 +1,12 @@
+- message: Possibly it may occur an infinite reactive loop.
+ line: 17
+ column: 7
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 23
+ column: 5
+ suggestions: null
+- message: Possibly it may occur an infinite reactive loop.
+ line: 28
+ column: 5
+ suggestions: null
diff --git a/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test02-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test02-input.svelte
new file mode 100644
index 000000000..bf5784428
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test02-input.svelte
@@ -0,0 +1,30 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/valid/test01-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/valid/test01-input.svelte
new file mode 100644
index 000000000..a70d3bbc9
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test01-input.svelte
@@ -0,0 +1,19 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/valid/test02-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/valid/test02-input.svelte
new file mode 100644
index 000000000..7cad960bc
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test02-input.svelte
@@ -0,0 +1,10 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/valid/test03-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/valid/test03-input.svelte
new file mode 100644
index 000000000..410b3da92
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test03-input.svelte
@@ -0,0 +1,7 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/valid/test04-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/valid/test04-input.svelte
new file mode 100644
index 000000000..746c1e4fc
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test04-input.svelte
@@ -0,0 +1,8 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/valid/test05-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/valid/test05-input.svelte
new file mode 100644
index 000000000..400cca9f3
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test05-input.svelte
@@ -0,0 +1,11 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/valid/test06-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/valid/test06-input.svelte
new file mode 100644
index 000000000..33842716a
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test06-input.svelte
@@ -0,0 +1,13 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/valid/test07-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/valid/test07-input.svelte
new file mode 100644
index 000000000..10c4bc8b3
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test07-input.svelte
@@ -0,0 +1,16 @@
+
diff --git a/tests/fixtures/rules/infinite-reactive-loop/valid/test08-input.svelte b/tests/fixtures/rules/infinite-reactive-loop/valid/test08-input.svelte
new file mode 100644
index 000000000..1f6d0ea9a
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test08-input.svelte
@@ -0,0 +1,16 @@
+
diff --git a/tests/src/rules/infinite-reactive-loop.ts b/tests/src/rules/infinite-reactive-loop.ts
new file mode 100644
index 000000000..a59288b36
--- /dev/null
+++ b/tests/src/rules/infinite-reactive-loop.ts
@@ -0,0 +1,20 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/infinite-reactive-loop"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+ env: {
+ browser: true,
+ es2017: true,
+ },
+})
+
+tester.run(
+ "infinite-reactive-loop",
+ rule as any,
+ loadTestCases("infinite-reactive-loop"),
+)