Repository: ota-meshi/eslint-plugin-svelte (ts)

Issue:
svelte/no-dom-manipulating
### Motivation

Many Svelte users face `Uncaught (in promise) TypeError: Cannot read property 'removeChild' of null`.

https://github.com/sveltejs/svelte/issues/6037

This is because userland code (or library code) manipulates DOM directly.
Then between the actual DOM and Svelte runtime expected DOM is different and such an error occurs.

Some user wants to use a library that includes DOM manipulation, so I think the Svelte runtime also need to update.
But at the same time, always actual DOM and Svelte expected DOM also should be same as much as possible.
Therefore we should not use DOM manipulating functions.

### Description

In general, DOM manipulating should delegate to Svelte runtime. If you manipulate the DOM directly, the Svelte runtime may confuse because there is a difference between the actual DOM and the Svelte runtime's expected DOM.
Therefore this rule reports where you use DOM manipulating function.
We don't recommend but If you intentionally manipulate the DOM, simply you can ignore this ESLint report.


### Examples

```svelte
<script>
  let div;
  let show;
  
  // ✓ GOOD
  const toggle = show = !show;
  
  // ✗ BAD
  const remove = () => div.remove();
  
</script>

{#if show}
  <div bind:this="{div}">div</div>
{/if}

<button on:click="{() => toggle()}">Click Me (Good)</button>
<button on:click="{() => remove()}">Click Me (Bad)</button>
```


### Additional comments

I think Vue also can face a similar situation but I couldn't find a such rule.
(I tried to run with Vue.js at [playground](https://sfc.vuejs.org/#eNqFUslOwzAQ/ZXBF1qJ2GI5lRSV7cCVsy8lmUIgXuQlCEX5d8ZOW6DQcoiUmfdme889u7aWdxHZjJW+co0N4DFEeyU1QKOscQF6h6sBVs4oOCbq8aXUMqyxW6Ps9YhJxkUOUz/JiAXQIvV7Me8wB2oyCS7iNJcnoCKy0ajDGk0IQPoqo32AVdQVQZMpzK+gT3nIzXi3bCMScvQVJXSQuhTjEbQ+BQGVbZcB8zF9n9gDkQDKce2uaFZzyVJesrQCBdulKCNyYfkUQzAaFlXbVG9ESXtJljGAMw536IMzH1mLsTRVibGMaKXYLsJO2ChcoZaWv3qjSfh8mlwDXrLZ5ljJspIz+nkJwfqZEFHbt2dOS4oFYcJFTU5gURu1OOdn/OJU1I0P3/McvSqenHn36GiiZCffmgtKdugKh7pGh+7gsB3uj4E72K+ha38GEmD7Rv58c/utr00VFanLnzHct5h+bz4e6olko9KSTblDZTqcTP9/DhtXm5oc3TTY4/Eph8fcGHCce8Dg4RPWByD5) and the situation is the same.)
I would like to know why Vue.js doesn't need to care about it.

Pull request:
feat: add `svelte/no-dom-manipulating` rule
This PR adds `svelte/no-dom-manipulating` rule.

close #300 

Patch:
diff --git a/.changeset/thick-boxes-warn.md b/.changeset/thick-boxes-warn.md
new file mode 100644
index 0000000..acb45f2
--- /dev/null
+++ b/.changeset/thick-boxes-warn.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-svelte": minor
+---
+
+feat: add `svelte/no-dom-manipulating` rule
diff --git a/.stylelintignore b/.stylelintignore
index 9a04d6d..4ddb7e0 100644
--- a/.stylelintignore
+++ b/.stylelintignore
@@ -14,3 +14,4 @@ LICENSE
 *.md
 /docs-svelte-kit/
 /coverage
+/build
diff --git a/README.md b/README.md
index 1415dfc..bc66951 100644
--- a/README.md
+++ b/README.md
@@ -298,6 +298,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
 
 | Rule ID | Description |    |
 |:--------|:------------|:---|
+| [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-style-properties](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-style-properties/) | disallow duplicate style properties | :star: |
 | [svelte/no-dynamic-slot-name](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dynamic-slot-name/) | disallow dynamic slot name | :star::wrench: |
diff --git a/docs/rules.md b/docs/rules.md
index 075eb29..953e7c2 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/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-style-properties](./rules/no-dupe-style-properties.md) | disallow duplicate style properties | :star: |
 | [svelte/no-dynamic-slot-name](./rules/no-dynamic-slot-name.md) | disallow dynamic slot name | :star::wrench: |
diff --git a/docs/rules/no-dom-manipulating.md b/docs/rules/no-dom-manipulating.md
new file mode 100644
index 0000000..21f3aa6
--- /dev/null
+++ b/docs/rules/no-dom-manipulating.md
@@ -0,0 +1,109 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "svelte/no-dom-manipulating"
+description: "disallow DOM manipulating"
+---
+
+# svelte/no-dom-manipulating
+
+> disallow DOM manipulating
+
+- :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
+
+In general, DOM manipulating should delegate to Svelte runtime. If you manipulate the DOM directly, the Svelte runtime may confuse because there is a difference between the actual DOM and the Svelte runtime's expected DOM.
+Therefore this rule reports where you use DOM manipulating function.
+We don't recommend but If you intentionally manipulate the DOM, simply you can ignore this ESLint report.
+
+<ESLintCodeBlock>
+
+<!--eslint-skip-->
+
+```svelte
+<script>
+  /* eslint svelte/no-dom-manipulating: "error" */
+  let foo, bar, show
+
+  /* ✓ GOOD */
+  const toggle = () => (show = !show)
+
+  /* ✗ BAD */
+  const remove = () => foo.remove()
+  const update = () => (bar.textContent = "Update!")
+</script>
+
+{#if show}
+  <div bind:this={foo}>Foo</div>
+{/if}
+<div bind:this={bar}>
+  {#if show}
+    Bar
+  {/if}
+</div>
+
+<button on:click={() => toggle()}>Click Me (Good)</button>
+<button on:click={() => remove()}>Click Me (Bad)</button>
+<button on:click={() => update()}>Click Me (Bad)</button>
+```
+
+</ESLintCodeBlock>
+
+This rule only tracks and checks variables given with `bind:this={}`. In other words, it doesn't track things like function arguments given to `transition:` directives. These functions have been well tested and are often used more carefully.
+
+<ESLintCodeBlock>
+
+<!--eslint-skip-->
+
+```svelte
+<script>
+  /* eslint svelte/no-dom-manipulating: "error" */
+  let visible = false
+
+  function typewriter(node, { speed = 1 }) {
+    const valid =
+      node.childNodes.length === 1 &&
+      node.childNodes[0].nodeType === Node.TEXT_NODE
+
+    if (!valid) {
+      throw new Error(
+        `This transition only works on elements with a single text node child`,
+      )
+    }
+
+    const text = node.textContent
+    const duration = text.length / (speed * 0.01)
+
+    return {
+      duration,
+      tick: (t) => {
+        const i = Math.trunc(text.length * t)
+        node.textContent = text.slice(0, i) // It does not report.
+      },
+    }
+  }
+</script>
+
+<label>
+  <input type="checkbox" bind:checked={visible} />
+  visible
+</label>
+
+{#if visible}
+  <p transition:typewriter>The quick brown fox jumps over the lazy dog</p>
+{/if}
+```
+
+</ESLintCodeBlock>
+
+See also <https://svelte.jp/examples/custom-js-transitions>.
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-dom-manipulating.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-dom-manipulating.ts)
diff --git a/src/rules/no-dom-manipulating.ts b/src/rules/no-dom-manipulating.ts
new file mode 100644
index 0000000..5dd5122
--- /dev/null
+++ b/src/rules/no-dom-manipulating.ts
@@ -0,0 +1,131 @@
+import type { AST } from "svelte-eslint-parser"
+import type { TSESTree } from "@typescript-eslint/types"
+import { createRule } from "../utils"
+import { findVariable, getNodeName } from "../utils/ast-utils"
+import type { Variable } from "@typescript-eslint/scope-manager"
+import { getPropertyName } from "eslint-utils"
+
+const DOM_MANIPULATING_METHODS = new Set([
+  "appendChild", // https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild
+  "insertBefore", // https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore
+  "normalize", // https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize
+  "removeChild", // https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild
+  "replaceChild", // https://developer.mozilla.org/en-US/docs/Web/API/Node/replaceChild
+  "after", // https://developer.mozilla.org/en-US/docs/Web/API/Element/after
+  "append", // https://developer.mozilla.org/en-US/docs/Web/API/Element/append
+  "before", // https://developer.mozilla.org/en-US/docs/Web/API/Element/before
+  "insertAdjacentElement", // https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement
+  "insertAdjacentHTML", // https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
+  "insertAdjacentText", // https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentText
+  "prepend", // https://developer.mozilla.org/en-US/docs/Web/API/Element/prepend
+  "remove", // https://developer.mozilla.org/en-US/docs/Web/API/Element/remove
+  "replaceChildren", // https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceChildren
+  "replaceWith", // https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceWith
+])
+const DOM_MANIPULATING_PROPERTIES = new Set([
+  "textContent", // https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent
+  "innerHTML", // https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
+  "outerHTML", // https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML
+  "innerText", // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText
+  "outerText", // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/outerText
+])
+
+export default createRule("no-dom-manipulating", {
+  meta: {
+    docs: {
+      description: "disallow DOM manipulating",
+      category: "Possible Errors",
+      recommended: false,
+    },
+    schema: [],
+    messages: {
+      disallowManipulateDOM:
+        "Don't manipulate the DOM directly. The Svelte runtime can get confused if there is a difference between the actual DOM and the DOM expected by the Svelte runtime.",
+    },
+    type: "problem",
+  },
+  create(context) {
+    const domVariables = new Set<Variable>()
+
+    /**
+     * Verify DOM variable identifier node
+     */
+    function verifyIdentifier(
+      node: TSESTree.Identifier | TSESTree.JSXIdentifier,
+    ) {
+      const member = node.parent
+      if (member?.type !== "MemberExpression" || member.object !== node) {
+        return
+      }
+      const name = getPropertyName(member)
+      if (!name) {
+        return
+      }
+      let target: TSESTree.Expression = member
+      let parent = target.parent
+      while (parent?.type === "ChainExpression") {
+        target = parent
+        parent = parent.parent
+      }
+      if (!parent) {
+        return
+      }
+      if (parent.type === "CallExpression") {
+        if (parent.callee !== target || !DOM_MANIPULATING_METHODS.has(name)) {
+          return
+        }
+      } else if (parent.type === "AssignmentExpression") {
+        if (parent.left !== target || !DOM_MANIPULATING_PROPERTIES.has(name)) {
+          return
+        }
+      }
+      context.report({
+        node: member,
+        messageId: "disallowManipulateDOM",
+      })
+    }
+
+    return {
+      "SvelteDirective[kind='Binding']"(node: AST.SvelteBindingDirective) {
+        if (
+          node.key.name.name !== "this" ||
+          !node.expression ||
+          node.expression.type !== "Identifier"
+        ) {
+          // not bind:this={id}
+          return
+        }
+        const element = node.parent.parent
+        if (element.type !== "SvelteElement" || !isHTMLElement(element)) {
+          // not HTML element
+          return
+        }
+        const variable = findVariable(context, node.expression)
+        if (
+          !variable ||
+          (variable.scope.type !== "module" && variable.scope.type !== "global")
+        ) {
+          return
+        }
+        domVariables.add(variable)
+      },
+      "Program:exit"() {
+        for (const variable of domVariables) {
+          for (const reference of variable.references) {
+            verifyIdentifier(reference.identifier)
+          }
+        }
+      },
+    }
+
+    /**
+     * Checks whether the given node is a HTML element or not.
+     */
+    function isHTMLElement(node: AST.SvelteElement) {
+      return (
+        node.kind === "html" ||
+        (node.kind === "special" && getNodeName(node) === "svelte:element")
+      )
+    }
+  },
+})
diff --git a/src/utils/ast-utils.ts b/src/utils/ast-utils.ts
index 8cfb4e1..0d89cc0 100644
--- a/src/utils/ast-utils.ts
+++ b/src/utils/ast-utils.ts
@@ -518,19 +518,17 @@ function getAttributeValueRangeTokens(
  * Returns name of SvelteElement
  */
 export function getNodeName(node: SvAST.SvelteElement): string {
-  if ("name" in node.name) {
+  if (node.name.type === "Identifier" || node.name.type === "SvelteName") {
     return node.name.name
   }
-  let object = ""
+  const memberPath = [node.name.property.name]
   let currentObject = node.name.object
-  while ("object" in currentObject) {
-    object = `${currentObject.property.name}.${object}`
+  while (currentObject.type === "SvelteMemberExpressionName") {
+    memberPath.unshift(currentObject.property.name)
     currentObject = currentObject.object
   }
-  if ("name" in currentObject) {
-    object = `${currentObject.name}.${object}`
-  }
-  return object + node.name.property.name
+  memberPath.unshift(currentObject.name)
+  return memberPath.join(".")
 }
 
 /**
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index 7e62e8e..ac31ae0 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -12,6 +12,7 @@ import maxAttributesPerLine from "../rules/max-attributes-per-line"
 import mustacheSpacing from "../rules/mustache-spacing"
 import noAtDebugTags from "../rules/no-at-debug-tags"
 import noAtHtmlTags from "../rules/no-at-html-tags"
+import noDomManipulating from "../rules/no-dom-manipulating"
 import noDupeElseIfBlocks from "../rules/no-dupe-else-if-blocks"
 import noDupeStyleProperties from "../rules/no-dupe-style-properties"
 import noDynamicSlotName from "../rules/no-dynamic-slot-name"
@@ -59,6 +60,7 @@ export const rules = [
   mustacheSpacing,
   noAtDebugTags,
   noAtHtmlTags,
+  noDomManipulating,
   noDupeElseIfBlocks,
   noDupeStyleProperties,
   noDynamicSlotName,
diff --git a/typings/eslint-utils/index.d.ts b/typings/eslint-utils/index.d.ts
index d5d5d28..206b809 100644
--- a/typings/eslint-utils/index.d.ts
+++ b/typings/eslint-utils/index.d.ts
@@ -1,12 +1,16 @@
 import type { AST } from "svelte-eslint-parser"
 import type { TSESTree } from "@typescript-eslint/types"
 import type { Scope } from "@typescript-eslint/scope-manager"
-import type { CALL, CONSTRUCT, ESM, READ } from "eslint-utils/referenceTracker"
+import type {
+  CALL,
+  CONSTRUCT,
+  ESM,
+  READ,
+  TraceMap,
+} from "eslint-utils/referenceTracker"
 export {
   ReferenceTracker,
   TrackedReferences,
-  getPropertyName,
-  getInnermostScope,
 } from "../../node_modules/@types/eslint-utils"
 type Token = { type: string; value: string }
 export function isArrowToken(token: Token): boolean


Test patch:
diff --git a/tests/fixtures/rules/no-dom-manipulating/invalid/chain01-errors.yaml b/tests/fixtures/rules/no-dom-manipulating/invalid/chain01-errors.yaml
new file mode 100644
index 0000000..6714e92
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/invalid/chain01-errors.yaml
@@ -0,0 +1,14 @@
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 5
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 9
+  column: 7
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dom-manipulating/invalid/chain01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/invalid/chain01-input.svelte
new file mode 100644
index 0000000..5dcc159
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/invalid/chain01-input.svelte
@@ -0,0 +1,16 @@
+<script>
+  let foo
+
+  const remove1 = () => {
+    foo?.remove()
+  }
+  const remove2 = () => {
+    // eslint-disable-next-line no-unsafe-optional-chaining -- ignore
+    ;(foo?.remove)()
+  }
+</script>
+
+<p bind:this={foo}>div</p>
+
+<button on:click={() => remove1()}>Click Me</button>
+<button on:click={() => remove2()}>Click Me</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/invalid/remove-text01-errors.yaml b/tests/fixtures/rules/no-dom-manipulating/invalid/remove-text01-errors.yaml
new file mode 100644
index 0000000..6053774
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/invalid/remove-text01-errors.yaml
@@ -0,0 +1,7 @@
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 9
+  column: 25
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dom-manipulating/invalid/remove-text01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/invalid/remove-text01-input.svelte
new file mode 100644
index 0000000..9fade1b
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/invalid/remove-text01-input.svelte
@@ -0,0 +1,19 @@
+<script>
+  let div
+  let show
+
+  // ✓ GOOD
+  const toggle = () => (show = !show)
+
+  // ✗ BAD
+  const update = () => (div.textContent = "foo")
+</script>
+
+<div bind:this={div}>
+  {#if show}
+    div
+  {/if}
+</div>
+
+<button on:click={() => toggle()}>Click Me (Good)</button>
+<button on:click={() => update()}>Click Me (Bad)</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/invalid/remove01-errors.yaml b/tests/fixtures/rules/no-dom-manipulating/invalid/remove01-errors.yaml
new file mode 100644
index 0000000..e5ec193
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/invalid/remove01-errors.yaml
@@ -0,0 +1,7 @@
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 9
+  column: 24
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dom-manipulating/invalid/remove01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/invalid/remove01-input.svelte
new file mode 100644
index 0000000..c667f30
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/invalid/remove01-input.svelte
@@ -0,0 +1,17 @@
+<script>
+  let div
+  let show
+
+  // ✓ GOOD
+  const toggle = () => (show = !show)
+
+  // ✗ BAD
+  const remove = () => div.remove()
+</script>
+
+{#if show}
+  <div bind:this={div}>div</div>
+{/if}
+
+<button on:click={() => toggle()}>Click Me (Good)</button>
+<button on:click={() => remove()}>Click Me (Bad)</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/invalid/svelte-element01-errors.yaml b/tests/fixtures/rules/no-dom-manipulating/invalid/svelte-element01-errors.yaml
new file mode 100644
index 0000000..cfef8c2
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/invalid/svelte-element01-errors.yaml
@@ -0,0 +1,14 @@
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 7
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 8
+  column: 5
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dom-manipulating/invalid/svelte-element01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/invalid/svelte-element01-input.svelte
new file mode 100644
index 0000000..2b6f8ff
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/invalid/svelte-element01-input.svelte
@@ -0,0 +1,15 @@
+<script>
+  export let tag = "div"
+  let foo
+  let bar
+
+  const remove = () => {
+    foo.remove()
+    bar.remove()
+  }
+</script>
+
+<p bind:this={foo}>div</p>
+<svelte:element this={tag} bind:this={bar}>div</svelte:element>
+
+<button on:click={() => remove()}>Click Me</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/invalid/well-known-method01-errors.yaml b/tests/fixtures/rules/no-dom-manipulating/invalid/well-known-method01-errors.yaml
new file mode 100644
index 0000000..3662024
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/invalid/well-known-method01-errors.yaml
@@ -0,0 +1,105 @@
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 6
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 7
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 8
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 9
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 10
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 11
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 12
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 13
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 14
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 15
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 16
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 17
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 18
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 19
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 20
+  column: 5
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dom-manipulating/invalid/well-known-method01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/invalid/well-known-method01-input.svelte
new file mode 100644
index 0000000..fa82c94
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/invalid/well-known-method01-input.svelte
@@ -0,0 +1,29 @@
+<script>
+  let foo
+  let div
+  const update = () => {
+    const newNode = document.createElement("div")
+    div.appendChild(newNode) // https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild
+    div.insertBefore(newNode, foo) // https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore
+    div.normalize() // https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize
+    div.removeChild(foo) // https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild
+    div.replaceChild(newNode, foo) // https://developer.mozilla.org/en-US/docs/Web/API/Node/replaceChild
+    div.after(newNode) // https://developer.mozilla.org/en-US/docs/Web/API/Element/after
+    div.append(newNode) // https://developer.mozilla.org/en-US/docs/Web/API/Element/append
+    div.before(newNode) // https://developer.mozilla.org/en-US/docs/Web/API/Element/before
+    div.insertAdjacentElement("beforebegin", newNode) // https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement
+    div.insertAdjacentHTML("beforebegin", "<strong>inserted text</strong>") // https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
+    div.insertAdjacentText("beforebegin", "Foo") // https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentText
+    div.prepend(newNode) // https://developer.mozilla.org/en-US/docs/Web/API/Element/prepend
+    div.remove() // https://developer.mozilla.org/en-US/docs/Web/API/Element/remove
+    div.replaceChildren(newNode) // https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceChildren
+    div.replaceWith(newNode) // https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceWith
+  }
+</script>
+
+<div bind:this={div}>
+  <div bind:this={foo} />
+  div
+</div>
+
+<button on:click={() => update()}>Click Me</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/invalid/well-known-prop01-errors.yaml b/tests/fixtures/rules/no-dom-manipulating/invalid/well-known-prop01-errors.yaml
new file mode 100644
index 0000000..493178b
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/invalid/well-known-prop01-errors.yaml
@@ -0,0 +1,35 @@
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 4
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 5
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 6
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 7
+  column: 5
+  suggestions: null
+- message:
+    Don't manipulate the DOM directly. The Svelte runtime can get confused
+    if there is a difference between the actual DOM and the DOM expected by the
+    Svelte runtime.
+  line: 8
+  column: 5
+  suggestions: null
diff --git a/tests/fixtures/rules/no-dom-manipulating/invalid/well-known-prop01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/invalid/well-known-prop01-input.svelte
new file mode 100644
index 0000000..676de2f
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/invalid/well-known-prop01-input.svelte
@@ -0,0 +1,14 @@
+<script>
+  let div
+  const update = () => {
+    div.textContent = "" // https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent
+    div.innerHTML = "" // https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
+    div.outerHTML = "" // https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML
+    div.innerText = "" // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText
+    div.outerText = "" // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/outerText
+  }
+</script>
+
+<div bind:this={div}>div</div>
+
+<button on:click={() => update()}>Click Me</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/valid/computed-member01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/valid/computed-member01-input.svelte
new file mode 100644
index 0000000..97d6823
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/valid/computed-member01-input.svelte
@@ -0,0 +1,8 @@
+<script>
+  let div
+  const remove = () => div[remove]()
+</script>
+
+<div bind:this={div}>div</div>
+
+<button on:click={() => remove()}>Click Me</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/valid/loop01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/valid/loop01-input.svelte
new file mode 100644
index 0000000..367d3d5
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/valid/loop01-input.svelte
@@ -0,0 +1,15 @@
+<script>
+  const list = [1, 2, 3]
+
+  const remove = () => {
+    for (const item of list) {
+      item.remove()
+    }
+  }
+</script>
+
+{#each list as item}
+  <input bind:this={item} />
+{/each}
+
+<button on:click={() => remove()}>Click Me</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/valid/non-bind-this01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/valid/non-bind-this01-input.svelte
new file mode 100644
index 0000000..62c3df0
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/valid/non-bind-this01-input.svelte
@@ -0,0 +1,13 @@
+<script>
+  let foo
+  let bar
+
+  const remove = () => {
+    foo.remove()
+    bar.remove()
+  }
+</script>
+
+<input bind:value={foo} />
+
+<button on:click={() => remove()}>Click Me</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/valid/non-element01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/valid/non-element01-input.svelte
new file mode 100644
index 0000000..075aac9
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/valid/non-element01-input.svelte
@@ -0,0 +1,15 @@
+<script>
+  import MyComponent from "./MyComponent.svelte"
+  let foo
+  let bar
+
+  const remove = () => {
+    foo.remove()
+    bar.remove()
+  }
+</script>
+
+<MyComponent bind:this={foo}>div</MyComponent>
+<svelte:component this={MyComponent} bind:this={bar}>div</svelte:component>
+
+<button on:click={() => remove()}>Click Me</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/valid/remove-text01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/valid/remove-text01-input.svelte
new file mode 100644
index 0000000..95326d2
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/valid/remove-text01-input.svelte
@@ -0,0 +1,15 @@
+<script>
+  let div
+  let show
+
+  // ✓ GOOD
+  const toggle = () => (show = !show)
+</script>
+
+<div bind:this={div}>
+  {#if show}
+    div
+  {/if}
+</div>
+
+<button on:click={() => toggle()}>Click Me</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/valid/remove01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/valid/remove01-input.svelte
new file mode 100644
index 0000000..cad26e3
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/valid/remove01-input.svelte
@@ -0,0 +1,13 @@
+<script>
+  let div
+  let show
+
+  // ✓ GOOD
+  const toggle = () => (show = !show)
+</script>
+
+{#if show}
+  <div bind:this={div}>div</div>
+{/if}
+
+<button on:click={() => toggle()}>Click Me</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/valid/unknown-method01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/valid/unknown-method01-input.svelte
new file mode 100644
index 0000000..9cf3823
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/valid/unknown-method01-input.svelte
@@ -0,0 +1,8 @@
+<script>
+  let div
+  const remove = () => div.unknown()
+</script>
+
+<div bind:this={div}>div</div>
+
+<button on:click={() => remove()}>Click Me</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/valid/unknown-prop01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/valid/unknown-prop01-input.svelte
new file mode 100644
index 0000000..e18ff94
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/valid/unknown-prop01-input.svelte
@@ -0,0 +1,8 @@
+<script>
+  let div
+  const remove = () => (div.unknown = "")
+</script>
+
+<div bind:this={div}>div</div>
+
+<button on:click={() => remove()}>Click Me</button>
diff --git a/tests/fixtures/rules/no-dom-manipulating/valid/unknown-var01-input.svelte b/tests/fixtures/rules/no-dom-manipulating/valid/unknown-var01-input.svelte
new file mode 100644
index 0000000..00b0aef
--- /dev/null
+++ b/tests/fixtures/rules/no-dom-manipulating/valid/unknown-var01-input.svelte
@@ -0,0 +1,9 @@
+<script>
+  const remove = () => {
+    unknown.remove()
+  }
+</script>
+
+<input bind:this={unknown} />
+
+<button on:click={() => remove()}>Click Me</button>
diff --git a/tests/src/rules/no-dom-manipulating.ts b/tests/src/rules/no-dom-manipulating.ts
new file mode 100644
index 0000000..1e70ab7
--- /dev/null
+++ b/tests/src/rules/no-dom-manipulating.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/no-dom-manipulating"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: "module",
+  },
+})
+
+tester.run(
+  "no-dom-manipulating",
+  rule as any,
+  loadTestCases("no-dom-manipulating"),
+)