Repository: sveltejs/eslint-plugin-svelte (ts)

Issue:
New rule: `svelte/no-$inspect`
### Motivation

THe motivation is same as [svelte/no-at-debug-tags](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/).the 

### Description

The `$inspect` should be removed when you no longer need it after you use it for debugging.

### Examples

```svelte
<script>
	let count = $state(0);
	let message = $state('hello');
        <!-- ✗ BAD -->
	$inspect(count, message); // will console.log when `count` or `message` change
</script>

<button onclick={() => count++}>Increment</button>
<input bind:value={message} />
```


### Additional comments

_No response_

Pull request description:
feat(no-inspect): add `no-inspect` rule
Add rule that warns if `$inspect` is used.

Closes #812


Patch:
diff --git a/.changeset/witty-games-mix.md b/.changeset/witty-games-mix.md
new file mode 100644
index 0000000..6ceae68
--- /dev/null
+++ b/.changeset/witty-games-mix.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-svelte": minor
+---
+
+feat(no-inspect): add `no-inspect` rule
diff --git a/README.md b/README.md
index 610a270..59268a6 100644
--- a/README.md
+++ b/README.md
@@ -419,6 +419,7 @@ These rules relate to better ways of doing things to help you avoid problems:
 | [svelte/no-ignored-unsubscribe](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-ignored-unsubscribe/) | disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores. |  |
 | [svelte/no-immutable-reactive-statements](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-immutable-reactive-statements/) | disallow reactive statements that don't reference reactive values. |  |
 | [svelte/no-inline-styles](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-inline-styles/) | disallow attributes and directives that produce inline styles |  |
+| [svelte/no-inspect](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-inspect/) | Warns against the use of `$inspect` directive |  |
 | [svelte/no-reactive-functions](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-reactive-functions/) | it's not necessary to define functions in reactive statements | :bulb: |
 | [svelte/no-reactive-literals](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-reactive-literals/) | don't assign literal values in reactive statements | :bulb: |
 | [svelte/no-svelte-internal](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-svelte-internal/) | svelte/internal will be removed in Svelte 6. |  |
diff --git a/docs-svelte-kit/package.json b/docs-svelte-kit/package.json
index 4a8998d..f8c00b7 100644
--- a/docs-svelte-kit/package.json
+++ b/docs-svelte-kit/package.json
@@ -11,53 +11,53 @@
     "svelte-kit": "vite"
   },
   "devDependencies": {
-    "@babel/core": "^7.24.5",
-    "@babel/types": "^7.24.5",
-    "@fontsource/fira-mono": "^5.0.13",
-    "@ota-meshi/eslint-plugin": "^0.17.1",
+    "@babel/core": "^7.25.8",
+    "@babel/types": "^7.25.8",
+    "@fontsource/fira-mono": "^5.1.0",
+    "@ota-meshi/eslint-plugin": "^0.17.5",
     "@sindresorhus/slugify": "^2.2.1",
-    "@sveltejs/adapter-static": "^3.0.1",
-    "@sveltejs/kit": "^2.5.9",
-    "@sveltejs/vite-plugin-svelte": "^3.1.0",
+    "@sveltejs/adapter-static": "^3.0.5",
+    "@sveltejs/kit": "^2.7.1",
+    "@sveltejs/vite-plugin-svelte": "^3.1.2",
     "@types/babel__core": "^7.20.5",
     "@types/cross-spawn": "^6.0.6",
     "@types/escape-html": "^1.0.4",
     "@types/eslint-scope": "^3.7.7",
     "@types/eslint-visitor-keys": "^3.3.0",
     "@types/less": "^3.0.6",
-    "@types/markdown-it": "^14.1.1",
+    "@types/markdown-it": "^14.1.2",
     "@types/markdown-it-container": "^2.0.10",
     "@types/markdown-it-emoji": "^3.0.1",
-    "@types/node": "^20.12.12",
+    "@types/node": "^20.16.11",
     "@types/prismjs": "^1.26.4",
-    "@types/stylus": "^0.48.42",
-    "@typescript/vfs": "^1.5.0",
-    "acorn": "^8.11.3",
+    "@types/stylus": "^0.48.43",
+    "@typescript/vfs": "^1.6.0",
+    "acorn": "^8.13.0",
     "assert": "^2.1.0",
     "cross-spawn": "^7.0.3",
     "env-cmd": "^10.1.0",
     "esbuild": "^0.24.0",
     "escape-html": "^1.0.3",
     "eslint-plugin-svelte": "workspace:^",
-    "eslint-scope": "^8.0.1",
-    "eslint-visitor-keys": "^4.0.0",
-    "espree": "^10.0.1",
-    "globals": "^15.2.0",
-    "markdown-it-anchor": "^9.0.0",
+    "eslint-scope": "^8.1.0",
+    "eslint-visitor-keys": "^4.1.0",
+    "espree": "^10.2.0",
+    "globals": "^15.11.0",
+    "markdown-it-anchor": "^9.2.0",
     "markdown-it-container": "^4.0.0",
     "markdown-it-emoji": "^3.0.0",
     "pako": "^2.1.0",
-    "postcss-nested": "^6.0.1",
+    "postcss-nested": "^6.2.0",
     "prism-svelte": "^0.5.0",
     "prismjs": "^1.29.0",
-    "sass": "^1.77.2",
-    "source-map-js": "^1.2.0",
+    "sass": "^1.79.5",
+    "source-map-js": "^1.2.1",
     "stylus": "^0.63.0",
-    "svelte": "^5.0.0-next.112",
+    "svelte": "^5.0.0-next.268",
     "svelte-adapter-ghpages": "0.2.2",
     "util": "^0.12.5",
-    "vite": "^5.2.11",
-    "vite-plugin-eslint4b": "^0.4.2",
+    "vite": "^5.4.9",
+    "vite-plugin-eslint4b": "^0.4.6",
     "vite-plugin-svelte-md": "^0.1.7"
   }
 }
diff --git a/docs-svelte-kit/src/lib/header/Header.svelte b/docs-svelte-kit/src/lib/header/Header.svelte
index 39a8672..78af595 100644
--- a/docs-svelte-kit/src/lib/header/Header.svelte
+++ b/docs-svelte-kit/src/lib/header/Header.svelte
@@ -77,6 +77,7 @@
 			target="_blank"
 			class="github-link"
 			rel="noopener noreferrer"
+			aria-label="GitHub"
 		>
 			<svg
 				version="1.1"
diff --git a/docs/rules.md b/docs/rules.md
index 7f115da..22798e0 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -56,6 +56,7 @@ These rules relate to better ways of doing things to help you avoid problems:
 | [svelte/no-ignored-unsubscribe](./rules/no-ignored-unsubscribe.md)                       | disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores.                                                  |          |
 | [svelte/no-immutable-reactive-statements](./rules/no-immutable-reactive-statements.md)   | disallow reactive statements that don't reference reactive values.                                                                        |          |
 | [svelte/no-inline-styles](./rules/no-inline-styles.md)                                   | disallow attributes and directives that produce inline styles                                                                             |          |
+| [svelte/no-inspect](./rules/no-inspect.md)                                               | Warns against the use of `$inspect` directive                                                                                             |          |
 | [svelte/no-reactive-functions](./rules/no-reactive-functions.md)                         | it's not necessary to define functions in reactive statements                                                                             | :bulb:   |
 | [svelte/no-reactive-literals](./rules/no-reactive-literals.md)                           | don't assign literal values in reactive statements                                                                                        | :bulb:   |
 | [svelte/no-svelte-internal](./rules/no-svelte-internal.md)                               | svelte/internal will be removed in Svelte 6.                                                                                              |          |
diff --git a/docs/rules/no-inspect.md b/docs/rules/no-inspect.md
new file mode 100644
index 0000000..3689268
--- /dev/null
+++ b/docs/rules/no-inspect.md
@@ -0,0 +1,39 @@
+---
+pageClass: 'rule-details'
+sidebarDepth: 0
+title: 'svelte/no-inspect'
+description: 'Warns against the use of `$inspect` directive'
+---
+
+# svelte/no-inspect
+
+> Warns against the use of `$inspect` directive
+
+- :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
+
+This rule reports usages of `$inspect`.
+
+<ESLintCodeBlock>
+
+<!--eslint-skip-->
+
+```svelte
+<script>
+  /* eslint svelte/no-inspect: "error" */
+  // ✗ BAD
+  $inspect(1);
+</script>
+```
+
+</ESLintCodeBlock>
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-inspect.ts)
+- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/no-inspect.ts)
diff --git a/package.json b/package.json
index cfecec6..aa42ba6 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
   },
   "devDependencies": {
     "@changesets/changelog-github": "^0.5.0",
-    "@changesets/cli": "^2.27.8",
+    "@changesets/cli": "^2.27.9",
     "@changesets/get-release-plan": "^4.0.4",
     "@eslint-community/eslint-plugin-eslint-comments": "^4.4.0",
     "@ota-meshi/eslint-plugin": "^0.17.5",
@@ -23,25 +23,25 @@
     "@typescript-eslint/eslint-plugin": "^7.18.0",
     "@typescript-eslint/parser": "^7.18.0",
     "env-cmd": "^10.1.0",
-    "eslint": "^9.10.0",
+    "eslint": "^9.12.0",
     "eslint-config-prettier": "^9.1.0",
     "eslint-formatter-friendly": "^7.0.0",
     "eslint-plugin-eslint-plugin": "^6.2.0",
-    "eslint-plugin-jsdoc": "^50.2.2",
-    "eslint-plugin-json-schema-validator": "^5.1.2",
+    "eslint-plugin-jsdoc": "^50.4.1",
+    "eslint-plugin-json-schema-validator": "^5.1.3",
     "eslint-plugin-jsonc": "^2.16.0",
     "eslint-plugin-markdown": "^5.1.0",
     "eslint-plugin-mdx": "^3.1.5",
-    "eslint-plugin-n": "^17.10.2",
+    "eslint-plugin-n": "^17.11.1",
     "eslint-plugin-node-dependencies": "^0.12.0",
     "eslint-plugin-prettier": "^5.2.1",
     "eslint-plugin-regexp": "^2.6.0",
     "eslint-plugin-yml": "^1.14.0",
-    "npm-run-all2": "^6.2.2",
+    "npm-run-all2": "^6.2.3",
     "prettier": "^3.3.3",
-    "prettier-plugin-svelte": "^3.2.6",
+    "prettier-plugin-svelte": "^3.2.7",
     "rimraf": "^6.0.1",
-    "typescript": "~5.6.0",
+    "typescript": "~5.6.3",
     "typescript-eslint": "^7.18.0"
   },
   "publishConfig": {
diff --git a/packages/eslint-plugin-svelte/package.json b/packages/eslint-plugin-svelte/package.json
index 0e30290..8c19fca 100644
--- a/packages/eslint-plugin-svelte/package.json
+++ b/packages/eslint-plugin-svelte/package.json
@@ -69,39 +69,39 @@
     "svelte-eslint-parser": "^0.41.1"
   },
   "devDependencies": {
-    "@babel/core": "^7.25.2",
-    "@babel/eslint-parser": "^7.25.1",
-    "@babel/plugin-proposal-function-bind": "^7.24.7",
+    "@babel/core": "^7.25.8",
+    "@babel/eslint-parser": "^7.25.8",
+    "@babel/plugin-proposal-function-bind": "^7.25.8",
     "@eslint-community/eslint-plugin-eslint-comments": "^4.4.0",
     "@types/babel__core": "^7.20.5",
     "@types/eslint-utils": "^3.0.5",
     "@types/esutils": "^2.0.2",
     "@types/json-schema": "^7.0.15",
     "@types/less": "^3.0.6",
-    "@types/mocha": "^10.0.8",
-    "@types/node": "^20.16.5",
+    "@types/mocha": "^10.0.9",
+    "@types/node": "^20.16.11",
     "@types/postcss-safe-parser": "^5.0.4",
     "@types/semver": "^7.5.8",
-    "@types/stylus": "^0.48.42",
-    "acorn": "^8.12.1",
+    "@types/stylus": "^0.48.43",
+    "acorn": "^8.13.0",
     "assert": "^2.1.0",
     "esbuild": "^0.24.0",
     "esbuild-register": "^3.6.0",
-    "eslint-scope": "^8.0.2",
+    "eslint-scope": "^8.1.0",
     "eslint-typegen": "^0.3.2",
-    "eslint-visitor-keys": "^4.0.0",
-    "espree": "^10.1.0",
+    "eslint-visitor-keys": "^4.1.0",
+    "espree": "^10.2.0",
     "less": "^4.2.0",
     "mocha": "^10.7.3",
-    "nyc": "^17.0.0",
+    "nyc": "^17.1.0",
     "postcss-nested": "^6.2.0",
-    "sass": "^1.78.0",
+    "sass": "^1.79.5",
     "source-map-js": "^1.2.1",
     "stylus": "^0.63.0",
-    "svelte": "5.0.0-next.249",
+    "svelte": "^5.0.0-next.268",
     "svelte-i18n": "^4.0.0",
-    "type-coverage": "^2.29.1",
-    "yaml": "^2.5.1"
+    "type-coverage": "^2.29.7",
+    "yaml": "^2.6.0"
   },
   "publishConfig": {
     "access": "public"
diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts
index c6b683f..84c7b3c 100644
--- a/packages/eslint-plugin-svelte/src/rule-types.ts
+++ b/packages/eslint-plugin-svelte/src/rule-types.ts
@@ -164,6 +164,11 @@ export interface RuleOptions {
    * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-inner-declarations/
    */
   'svelte/no-inner-declarations'?: Linter.RuleEntry<SvelteNoInnerDeclarations>
+  /**
+   * Warns against the use of `$inspect` directive
+   * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-inspect/
+   */
+  'svelte/no-inspect'?: Linter.RuleEntry<[]>
   /**
    * disallow use of not function in event handler
    * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-not-function-handler/
diff --git a/packages/eslint-plugin-svelte/src/rules/no-inspect.ts b/packages/eslint-plugin-svelte/src/rules/no-inspect.ts
new file mode 100644
index 0000000..22e30ce
--- /dev/null
+++ b/packages/eslint-plugin-svelte/src/rules/no-inspect.ts
@@ -0,0 +1,31 @@
+import type { TSESTree } from '@typescript-eslint/types';
+
+import { createRule } from '../utils';
+
+export default createRule('no-inspect', {
+	meta: {
+		docs: {
+			description: 'Warns against the use of `$inspect` directive',
+			category: 'Best Practices',
+			// TODO: Enable recommended in major version
+			recommended: false,
+			default: 'warn'
+		},
+		schema: [],
+		messages: {
+			unexpected: 'Do not use $inspect directive'
+		},
+		type: 'suggestion'
+	},
+	create(context) {
+		return {
+			Identifier(node: TSESTree.Identifier) {
+				if (node.name !== '$inspect') {
+					return;
+				}
+
+				context.report({ messageId: 'unexpected', node });
+			}
+		};
+	}
+});
diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts
index af0dd20..01f9045 100644
--- a/packages/eslint-plugin-svelte/src/utils/rules.ts
+++ b/packages/eslint-plugin-svelte/src/utils/rules.ts
@@ -32,6 +32,7 @@ import noIgnoredUnsubscribe from '../rules/no-ignored-unsubscribe';
 import noImmutableReactiveStatements from '../rules/no-immutable-reactive-statements';
 import noInlineStyles from '../rules/no-inline-styles';
 import noInnerDeclarations from '../rules/no-inner-declarations';
+import noInspect from '../rules/no-inspect';
 import noNotFunctionHandler from '../rules/no-not-function-handler';
 import noObjectInTextMustaches from '../rules/no-object-in-text-mustaches';
 import noReactiveFunctions from '../rules/no-reactive-functions';
@@ -97,6 +98,7 @@ export const rules = [
 	noImmutableReactiveStatements,
 	noInlineStyles,
 	noInnerDeclarations,
+	noInspect,
 	noNotFunctionHandler,
 	noObjectInTextMustaches,
 	noReactiveFunctions,
diff --git a/packages/eslint-plugin-svelte/tools/new-rule.ts b/packages/eslint-plugin-svelte/tools/new-rule.ts
index 14b86f8..a76cc32 100644
--- a/packages/eslint-plugin-svelte/tools/new-rule.ts
+++ b/packages/eslint-plugin-svelte/tools/new-rule.ts
@@ -5,7 +5,7 @@ import { writeAndFormat } from './lib/write';
 const logger = console;
 
 // main
-void (async (ruleId) => {
+void (async ([ruleId, ...args]) => {
 	if (ruleId == null) {
 		logger.error('Usage: pnpm run new <RuleID>');
 		process.exitCode = 1;
@@ -21,7 +21,7 @@ void (async (ruleId) => {
 
 	const ruleFile = path.resolve(__dirname, `../src/rules/${ruleId}.ts`);
 	const testFile = path.resolve(__dirname, `../tests/src/rules/${ruleId}.ts`);
-	const docFile = path.resolve(__dirname, `../docs/rules/${ruleId}.md`);
+	const docFile = path.resolve(__dirname, `../../../docs/rules/${ruleId}.md`);
 	const fixturesRoot = path.resolve(__dirname, `../tests/fixtures/rules/${ruleId}/`);
 	try {
 		fs.mkdirSync(path.dirname(ruleFile), { recursive: true });
@@ -130,12 +130,43 @@ This rule reports ???.
 `
 	);
 
-	cp.execSync(`code "${ruleFile}"`);
-	cp.execSync(`code "${testFile}"`);
-	cp.execSync(`code "${docFile}"`);
-})(process.argv[2]);
+	const { code } = expectedArgs(args);
+	if (!code) {
+		return;
+	}
+
+	try {
+		// Use code -v to know if vscode is installed and do not print anything to the console
+		cp.execSync('code -v', { stdio: 'ignore' });
+		cp.execSync(`code "${ruleFile}"`);
+		cp.execSync(`code "${testFile}"`);
+		cp.execSync(`code "${docFile}"`);
+	} catch (_) {
+		logger.error('Unable to find code command. Will not open files with VSCode.');
+	}
+})(process.argv.slice(2));
 
 /** Get module path */
 function getModulePath(from: string, module: string): string {
 	return path.relative(path.dirname(from), module).replace(/.ts$/u, '');
 }
+
+/** Argument parsing */
+function expectedArgs(args: string[]) {
+	const result = { code: false };
+
+	for (let i = 0; i < args.length; i++) {
+		const arg = args[i];
+		const split = args[i].split('=');
+		if (arg === '--code') {
+			// Passing --code alone is the same as --code=true
+			result.code = args[i + 1] === 'true' || args[i + 1] === undefined;
+		} else if (split.length === 2) {
+			result.code = split[1] === 'true';
+		} else if (split.length > 2) {
+			logger.error('Usage: pnpm run new <RuleID> <--code=boolean>');
+		}
+	}
+
+	return result;
+}


Test patch:
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-inspect/invalid/test01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-inspect/invalid/test01-errors.yaml
new file mode 100644
index 0000000..db86c5f
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-inspect/invalid/test01-errors.yaml
@@ -0,0 +1,12 @@
+- message: Do not use $inspect directive
+  line: 2
+  column: 3
+  suggestions: null
+- message: Do not use $inspect directive
+  line: 5
+  column: 13
+  suggestions: null
+- message: Do not use $inspect directive
+  line: 8
+  column: 5
+  suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-inspect/invalid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-inspect/invalid/test01-input.svelte
new file mode 100644
index 0000000..c6a6943
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-inspect/invalid/test01-input.svelte
@@ -0,0 +1,10 @@
+<script>
+  $inspect(1);
+  $state(0);
+
+  const a = $inspect(1);
+
+  const _ = () => {
+    $inspect(1);
+  }
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-inspect/valid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-inspect/valid/test01-input.svelte
new file mode 100644
index 0000000..d65891b
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-inspect/valid/test01-input.svelte
@@ -0,0 +1,3 @@
+<script>
+  const _ = $state(1);
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/src/rules/no-inspect.ts b/packages/eslint-plugin-svelte/tests/src/rules/no-inspect.ts
new file mode 100644
index 0000000..cfaa549
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/src/rules/no-inspect.ts
@@ -0,0 +1,12 @@
+import { RuleTester } from '../../utils/eslint-compat';
+import rule from '../../../src/rules/no-inspect';
+import { loadTestCases } from '../../utils/utils';
+
+const tester = new RuleTester({
+	languageOptions: {
+		ecmaVersion: 2020,
+		sourceType: 'module'
+	}
+});
+
+tester.run('no-inspect', rule as any, loadTestCases('no-inspect'));