Repository: ota-meshi/eslint-plugin-svelte
Pull request #332 (2022-12-29 04:43:27)

feat: add `svelte/Infinite-reactive-loop` rule
close: https://github.com/ota-meshi/eslint-plugin-svelte/issues/299

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: d60b0446dfdf9f90e04dab00e5ad63809823574d

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                 | Type  |
| -------------------- | ----- |
| eslint-plugin-svelte | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/baseballyama/eslint-plugin-svelte/new/feature/infinite-reactive-loop?filename=.changeset/swift-bags-walk.md&value=---%0A%22eslint-plugin-svelte%22%3A%20patch%0A---%0A%0A%5BWIP%5D%20feat%3A%20add%20%60svelte%2FInfinite-reactive-loop%60%20rule%0A)


codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/ota-meshi/eslint-plugin-svelte/pr/332/builds/330504) or the icon next to each commit SHA.
baseballyama: @ota-meshi What do you think about the current specification and implementation? Do you think there are TODOs?
ota-meshi: I misunderstood that this PR was still in the work in progress 😓. I will make time to check this PR. So please wait :pray:

Patch:
diff --git a/.changeset/slimy-brooms-report.md b/.changeset/slimy-brooms-report.md
new file mode 100644
index 0000000..6521a89
--- /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 7f4340e..534bc64 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 eab73c0..1b53df6 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 0000000..c34aa61
--- /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: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
+
+## :book: Rule Details
+
+Svelte runtime prevents calling the same reactive statement twice in a microtask.<br/>
+But between different microtask, it doesn't prevent.<br/>
+This rule reports those possible infinite loop.
+
+<ESLintCodeBlock>
+
+<!--eslint-skip-->
+
+```svelte
+<script>
+  /* eslint svelte/infinite-reactive-loop: "error" */
+  import { count } from "./store.js"
+  import { tick } from "svelte"
+  let a = 0
+
+  // ✓ GOOD
+  $: if (a < 10) {
+    a += 1
+    $count += 1
+  }
+
+  $: (async () => {
+    // You can update a state in the same micro task.
+    a += 1
+    $count += 1
+    await new Promise((resolve) => setTimeout(resolve, 100))
+  })()
+
+  $: (async () => {
+    await doSomething_ok()
+  })()
+
+  const doSomething_ok = async () => {
+    await fetchFromServer()
+    // You can update a state even in different microtask
+    // if you don't refer the state in reactive statement.
+    a += 1
+  }
+
+  // ✗ BAD
+  $: (async () => {
+    await doSomething()
+    // Do not update a state in different micro task.
+    a += 1
+    $count += 1
+  })()
+
+  $: tick(() => {
+    a = a + 1
+    $count += 1
+  })
+
+  $: (async () => {
+    console.log(a)
+    // This rule checks caller function recursively.
+    await doSomething_ng_1()
+  })()
+
+  const doSomething_ng_1 = async () => {
+    a += 1
+    await fetchFromServer()
+    doSomething_ng_2()
+  }
+
+  const doSomething_ng_2 = () => {
+    a += 1
+  }
+</script>
+```
+
+</ESLintCodeBlock>
+
+## :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 d78e852..55c8e49 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 0000000..83586ac
--- /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<TSESTree.Identifier> = 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 ceba1f7..e9d10b5 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,


Test patch:
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 0000000..3def666
--- /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 0000000..33226aa
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/await/test01-input.svelte
@@ -0,0 +1,9 @@
+<script>
+  let a = 0
+
+  $: (async () => {
+    a = a + 1
+    await doSomething()
+    a = a + 1
+  })()
+</script>
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 0000000..360ccfc
--- /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 0000000..e9663f8
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test01-input.svelte
@@ -0,0 +1,8 @@
+<script>
+  const fetch = () => new Promise((resolve) => setTimeout(resolve, 100))
+  let a = 0
+
+  $: fetch().then(() => {
+    a = a + 1
+  })
+</script>
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 0000000..457e930
--- /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 0000000..83b8f9f
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test02-input.svelte
@@ -0,0 +1,21 @@
+<script>
+  let a = 0
+  const fetch = async () => {
+    await new Promise((resolve) => setTimeout(resolve, 100))
+  }
+  const doSomething = () => {
+    fetch().then(() => {
+      a += 1
+    })
+  }
+
+  $: (async () => {
+    console.log(a)
+    await doSomething()
+  })()
+
+  $: (async () => {
+    // should not report here
+    await doSomething()
+  })()
+</script>
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 0000000..c6b46f0
--- /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 0000000..a845142
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test03-input.svelte
@@ -0,0 +1,20 @@
+<script>
+  let a = 0
+  const fetch = async () => {
+    await new Promise((resolve) => setTimeout(resolve, 100))
+  }
+  const doSomething = async () => {
+    await fetch()
+    a += 1
+  }
+
+  $: (async () => {
+    console.log(a)
+    await doSomething()
+  })()
+
+  $: (async () => {
+    // should not report here
+    await doSomething()
+  })()
+</script>
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 0000000..592ba8c
--- /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 0000000..62fbca9
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test04-input.svelte
@@ -0,0 +1,26 @@
+<script>
+  import { count } from "./store.js"
+  let a = 0
+  const doSomething = async () => {
+    await fetchFromServer()
+    a += 1
+    $count += 1
+  }
+
+  const doSomething2 = () => {
+    a += 1
+    $count += 1
+  }
+
+  $: (async () => {
+    console.log(a)
+    await doSomething()
+    doSomething2()
+  })()
+
+  $: (async () => {
+    // should not report here
+    await doSomething()
+    doSomething2()
+  })()
+</script>
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 0000000..90ec1cf
--- /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 0000000..2ceb8ba
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test05-input.svelte
@@ -0,0 +1,14 @@
+<script>
+  let a = 0
+  let obj = { a: 0 }
+  const doSomething = async () => {
+    await fetchFromServer()
+    a += 1
+    obj.a += 1
+  }
+
+  $: (async () => {
+    console.log(a)
+    await doSomething()
+  })()
+</script>
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 0000000..c925e60
--- /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 0000000..3225fff
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test06-input.svelte
@@ -0,0 +1,12 @@
+<script>
+  let obj = { a: 0 }
+  const doSomething = async () => {
+    await fetchFromServer()
+    obj.a += 1
+  }
+
+  $: (async () => {
+    await doSomething()
+    obj.a += 1
+  })()
+</script>
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 0000000..7002c04
--- /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 0000000..8905d82
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test07-input.svelte
@@ -0,0 +1,19 @@
+<script>
+  import { store } from "./store.js"
+
+  const doSomething = () => {
+    $store += 1
+  }
+
+  $: (async () => {
+    console.log($store)
+    await fetch()
+    doSomething()
+  })()
+
+  $: (async () => {
+    // should not report here
+    await fetch()
+    doSomething()
+  })()
+</script>
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 0000000..62970cb
--- /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 0000000..e763fe9
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test08-input.svelte
@@ -0,0 +1,16 @@
+<script>
+  let foo = { obj: 1 }
+
+  const doSomething = async () => {
+    await fetchFromServer()
+    foo.obj += 1
+  }
+
+  $: (async () => {
+    const obj = { a: 0 }
+    console.log(obj)
+    await doSomething()
+    foo.obj += 1
+    obj.a = 1
+  })()
+</script>
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 0000000..f8ec406
--- /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 0000000..f80ae83
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test09-input.svelte
@@ -0,0 +1,13 @@
+<script>
+  let obj = { a: 0 }
+
+  const doSomething = async () => {
+    await new Promise((resolve) => setTimeout(resolve, 100))
+    return 1
+  }
+
+  $: (async () => {
+    // eslint-disable-next-line require-atomic-updates -- ignore
+    obj.a += await doSomething((obj.a += 1))
+  })()
+</script>
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 0000000..4b2f7de
--- /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 0000000..397650f
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test10-input.svelte
@@ -0,0 +1,11 @@
+<script>
+  let obj = { a: 0 }
+
+  const doSomething = (a, b) => {
+    console.log({ a, b })
+  }
+
+  $: (async () => {
+    doSomething((await "a", (obj.a += 1)))
+  })()
+</script>
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 0000000..14501f8
--- /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 0000000..73a77c9
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/function-call/test11-input.svelte
@@ -0,0 +1,11 @@
+<script>
+  let a = 0
+
+  $: {
+    a += 1
+    void (async () => {
+      await new Promise((resolve) => setTimeout(resolve, 100))
+      a += 1
+    })()
+  }
+</script>
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 0000000..be27e28
--- /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 0000000..baaaeb5
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test01-input.svelte
@@ -0,0 +1,14 @@
+<script>
+  let a = 0
+
+  $: (() => {
+    Promise.resolve().then(() => {
+      a = a + 1
+      a += 1
+    })
+
+    Promise.resolve().catch(() => {
+      a = a + 1
+    })
+  })()
+</script>
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 0000000..97af81e
--- /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 0000000..e915ee4
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test02-input.svelte
@@ -0,0 +1,17 @@
+<script>
+  let a = 0
+
+  $: {
+    a = a + 1
+    new Promise((resolve, reject) => {
+      /** do something */
+    })
+      .then(() => {
+        a = a + 1
+      })
+      .catch(() => {
+        a = a + 1
+      })
+    a = a + 1
+  }
+</script>
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 0000000..57555ae
--- /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 0000000..8b8db17
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test03-input.svelte
@@ -0,0 +1,15 @@
+<script>
+  let a = 0
+
+  $: (() => {
+    a = a + 1
+    Promise.all([])
+      .then(() => {
+        a = a + 1
+      })
+      .catch(() => {
+        a = a + 1
+      })
+    a = a + 1
+  })()
+</script>
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 0000000..3def666
--- /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 0000000..6579b23
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test04-input.svelte
@@ -0,0 +1,9 @@
+<script>
+  let a = 0
+
+  $: (async () => {
+    a = a + 1
+    await Promise.resolve()
+    a = a + 1
+  })()
+</script>
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 0000000..8f77e5a
--- /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 0000000..9459a4e
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test05-input.svelte
@@ -0,0 +1,13 @@
+<script>
+  let a = 0
+
+  $: (() => {
+    Promise.resolve()
+      .catch(() => {
+        a = a + 1
+      })
+      .catch(() => {
+        a = a + 1
+      })
+  })()
+</script>
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 0000000..8f77e5a
--- /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 0000000..cea79c5
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/promise/test06-input.svelte
@@ -0,0 +1,13 @@
+<script>
+  let a = 0
+
+  $: (() => {
+    Promise.resolve()
+      .catch(() => {
+        a = a + 1
+      })
+      .then(() => {
+        a = a + 1
+      })
+  })()
+</script>
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 0000000..dd23a84
--- /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 0000000..361ad44
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/queueMicrotask/test01-input.svelte
@@ -0,0 +1,16 @@
+<script>
+  const queueMicrotask2 = queueMicrotask
+  let a = 0
+
+  $: {
+    queueMicrotask(() => {
+      a = a + 1
+    })
+  }
+
+  $: {
+    queueMicrotask2(() => {
+      a = a + 1
+    })
+  }
+</script>
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 0000000..dd23a84
--- /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 0000000..e76f0bd
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/setInterval/test01-input.svelte
@@ -0,0 +1,16 @@
+<script>
+  const setInterval2 = setInterval
+  let a = 0
+
+  $: {
+    setInterval(() => {
+      a = a + 1
+    })
+  }
+
+  $: {
+    setInterval2(() => {
+      a = a + 1
+    })
+  }
+</script>
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 0000000..b1a41fb
--- /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 0000000..bb968e6
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test01-input.svelte
@@ -0,0 +1,24 @@
+<script>
+  const setTimeout2 = setTimeout
+  let a = 0
+
+  function doSomething(fn) {
+    fn()
+  }
+
+  $: {
+    setTimeout(() => {
+      a = a + 1
+    })
+  }
+
+  $: {
+    setTimeout2(() => {
+      a = a + 1
+    })
+
+    doSomething(() => {
+      a = a + 1
+    })
+  }
+</script>
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 0000000..48a6fc2
--- /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 0000000..87a7e87
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/setTimeout/test02-input.svelte
@@ -0,0 +1,7 @@
+<script>
+  import { store } from "./store.js"
+
+  $: setTimeout(() => {
+    $store += 1
+  }, 100)
+</script>
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 0000000..ea02583
--- /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 0000000..148ca83
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test01-input.svelte
@@ -0,0 +1,27 @@
+<script>
+  import { tick } from "svelte"
+  const tick2 = tick
+  let a = 0
+
+  $: {
+    tick(() => {
+      a = a + 1
+    })
+  }
+
+  $: {
+    tick2(() => {
+      a = a + 1
+    })
+  }
+
+  $: (async () => {
+    await tick()
+    a = a + 1
+  })()
+
+  $: (async () => {
+    await tick2()
+    a = a + 1
+  })()
+</script>
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 0000000..f5fb694
--- /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 0000000..bf57844
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/invalid/tick/test02-input.svelte
@@ -0,0 +1,30 @@
+<script>
+  import { tick as tick2 } from "svelte"
+  let a = 0
+
+  function tick(fn) {
+    fn()
+  }
+
+  $: {
+    tick(() => {
+      a = a + 1
+    })
+  }
+
+  $: {
+    tick2(() => {
+      a = a + 1
+    })
+  }
+
+  $: (async () => {
+    await tick()
+    a = a + 1
+  })()
+
+  $: (async () => {
+    await tick2()
+    a = a + 1
+  })()
+</script>
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 0000000..a70d3bb
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test01-input.svelte
@@ -0,0 +1,19 @@
+<script>
+  let a = 0
+
+  $: (async () => {
+    a = a + 1
+    a += 1
+    await new Promise((resolve) => {})
+  })()
+
+  $: (async () => {
+    let a = 0
+    await new Promise((resolve) => {
+      setTimeout(() => {
+        a = a + 1
+        a += 1
+      }, 100)
+    })
+  })()
+</script>
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 0000000..7cad960
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test02-input.svelte
@@ -0,0 +1,10 @@
+<script>
+  let a = 0
+
+  const func = () => {
+    setTimeout(() => {
+      a = a + 1
+    }, 100)
+  }
+  $: func()
+</script>
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 0000000..410b3da
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test03-input.svelte
@@ -0,0 +1,7 @@
+<script>
+  let a = 0
+
+  $: (async () => {
+    await doSomething((a += 1))
+  })()
+</script>
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 0000000..746c1e4
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test04-input.svelte
@@ -0,0 +1,8 @@
+<script>
+  let list = [0]
+
+  $: (async () => {
+    await doSomething()
+    list.push(list.length)
+  })()
+</script>
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 0000000..400cca9
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test05-input.svelte
@@ -0,0 +1,11 @@
+<script>
+  import { store } from "./store.js"
+
+  const doSomething = () => {
+    $store += 1
+  }
+
+  $: (async () => {
+    await doSomething()
+  })()
+</script>
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 0000000..3384271
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test06-input.svelte
@@ -0,0 +1,13 @@
+<script>
+  let obj = { a: 0 }
+
+  const doSomething = async () => {
+    obj.a += 1
+    await fetch()
+  }
+
+  $: (async () => {
+    obj.a += 1
+    await doSomething()
+  })()
+</script>
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 0000000..10c4bc8
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test07-input.svelte
@@ -0,0 +1,16 @@
+<script>
+  let obj = { a: 0 }
+
+  const doSomething = async () => {
+    obj.a += 1
+    await fetch()
+  }
+
+  $: (async () => {
+    const doSomething = async () => {
+      await fetch()
+    }
+    obj.a += 1
+    await doSomething()
+  })()
+</script>
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 0000000..1f6d0ea
--- /dev/null
+++ b/tests/fixtures/rules/infinite-reactive-loop/valid/test08-input.svelte
@@ -0,0 +1,16 @@
+<script>
+  let a = 0
+
+  const func = () => {
+    return {
+      then: (fun) => fun(),
+      catch: (fun) => fun(),
+    }
+  }
+
+  $: (() => {
+    // TODO: This is a false positive. we need to check if the `then` or `catch` is `Promise` object's function.
+    // func().then(() => (a += 1))
+    // func().catch(() => (a += 1))
+  })()
+</script>
diff --git a/tests/src/rules/infinite-reactive-loop.ts b/tests/src/rules/infinite-reactive-loop.ts
new file mode 100644
index 0000000..a59288b
--- /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"),
+)