diff --git a/.changeset/silly-lions-build.md b/.changeset/silly-lions-build.md
new file mode 100644
index 000000000..6e9e19ca9
--- /dev/null
+++ b/.changeset/silly-lions-build.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': patch
+---
+
+fix(no-shadow): ignore `{#snippet}` if it uses under component.
diff --git a/README.md b/README.md
index 35c16c8e8..8c50686de 100644
--- a/README.md
+++ b/README.md
@@ -363,6 +363,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [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-shadow](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-shadow/) | Disallow variable declarations from shadowing variables declared in the outer scope | |
| [svelte/no-svelte-internal](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-svelte-internal/) | svelte/internal will be removed in Svelte 6. | |
| [svelte/no-unused-class-name](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-class-name/) | disallow the use of a class in the template without a corresponding style | |
| [svelte/no-unused-svelte-ignore](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
diff --git a/docs/rules.md b/docs/rules.md
index 047b2ab29..3ed9a1404 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -60,6 +60,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [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-shadow](./rules/no-shadow.md) | Disallow variable declarations from shadowing variables declared in the outer scope | |
| [svelte/no-svelte-internal](./rules/no-svelte-internal.md) | svelte/internal will be removed in Svelte 6. | |
| [svelte/no-unused-class-name](./rules/no-unused-class-name.md) | disallow the use of a class in the template without a corresponding style | |
| [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
diff --git a/docs/rules/no-shadow.md b/docs/rules/no-shadow.md
new file mode 100644
index 000000000..80eef2f3a
--- /dev/null
+++ b/docs/rules/no-shadow.md
@@ -0,0 +1,74 @@
+---
+pageClass: 'rule-details'
+sidebarDepth: 0
+title: 'svelte/no-shadow'
+description: 'Disallow variable declarations from shadowing variables declared in the outer scope'
+---
+
+# svelte/no-shadow
+
+> Disallow variable declarations from shadowing variables declared in the outer scope
+
+- :exclamation: **_This rule has not been released yet._**
+
+## :book: Rule Details
+
+This rule reports shadowed variables, similar to the base ESLint `no-shadow` rule. However, it ignores cases where `{#snippet}` is used as a named slot in Svelte components. If this rule is active, make sure to disable the base `no-shadow` rule, as it will conflict with this rule.
+
+
+
+```svelte
+
+
+
+
+ {#snippet children()}
+
+ {#snippet children()}
+ Hello!
+ {/snippet}
+
+ {/snippet}
+
+
+
+
+ {@const foo = 1}
+
+ {@const foo = 2}
+
+
+```
+
+## :wrench: Options
+
+```json
+{
+ "svelte/no-shadow": [
+ "error",
+ { "builtinGlobals": false, "hoist": "functions", "allow": [], "ignoreOnInitialization": false }
+ ]
+}
+```
+
+- `builtinGlobals`: The `builtinGlobals` option is `false` by default. If it is `true`, the rule prevents shadowing of built-in global variables: `Object`, `Array`, `Number`, and so on.
+- `hoist`: The `hoist` option has three settings:
+ - `functions` (by default) - reports shadowing before the outer functions are defined.
+ - `all` - reports all shadowing before the outer variables/functions are defined.
+ - `never` - never report shadowing before the outer variables/functions are defined.
+- `allow`: The `allow` option is an array of identifier names for which shadowing is allowed. For example, `"resolve"`, `"reject"`, `"done"`, `"cb"`.
+- `ignoreOnInitialization`: The `ignoreOnInitialization` option is `false` by default. If it is `true`, it prevents reporting shadowing of variables in their initializers when the shadowed variable is presumably still uninitialized. The shadowed variable must be on the left side. The shadowing variable must be on the right side and declared in a callback function or in an IIFE.
+
+## :books: Further Reading
+
+- See [ESLint `no-shadow` rule](https://eslint.org/docs/latest/rules/no-shadow) for more information about the base rule.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-shadow.ts)
+- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/no-shadow.ts)
+
+Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/no-shadow)
diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts
index b07451a1b..6d934bf40 100644
--- a/packages/eslint-plugin-svelte/src/rule-types.ts
+++ b/packages/eslint-plugin-svelte/src/rule-types.ts
@@ -215,6 +215,11 @@ export interface RuleOptions {
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-restricted-html-elements/
*/
'svelte/no-restricted-html-elements'?: Linter.RuleEntry
+ /**
+ * Disallow variable declarations from shadowing variables declared in the outer scope
+ * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-shadow/
+ */
+ 'svelte/no-shadow'?: Linter.RuleEntry
/**
* disallow shorthand style properties that override related longhand properties
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-shorthand-style-property-overrides/
@@ -479,6 +484,13 @@ type SvelteNoRestrictedHtmlElements = [(string | {
elements?: [string, ...(string)[]]
message?: string
}))[]]
+// ----- svelte/no-shadow -----
+type SvelteNoShadow = []|[{
+ builtinGlobals?: boolean
+ hoist?: ("all" | "functions" | "never")
+ allow?: string[]
+ ignoreOnInitialization?: boolean
+}]
// ----- svelte/no-target-blank -----
type SvelteNoTargetBlank = []|[{
allowReferrer?: boolean
diff --git a/packages/eslint-plugin-svelte/src/rules/no-shadow.ts b/packages/eslint-plugin-svelte/src/rules/no-shadow.ts
new file mode 100644
index 000000000..3101325d4
--- /dev/null
+++ b/packages/eslint-plugin-svelte/src/rules/no-shadow.ts
@@ -0,0 +1,83 @@
+import { createRule } from '../utils/index.js';
+import { defineWrapperListener, getProxyContent, getCoreRule } from '../utils/eslint-core.js';
+import type { TSESTree } from '@typescript-eslint/types';
+import type { Scope } from '@typescript-eslint/scope-manager';
+import type { Range } from 'svelte-eslint-parser/lib/ast/common.js';
+import { getScope as getScopeUtil } from '../utils/ast-utils.js';
+import { getSourceCode as getSourceCodeCompat } from '../utils/compat.js';
+
+const coreRule = getCoreRule('no-shadow');
+
+function removeSnippetIdentifiers(snippetIdentifierNodeLocations: Range[], scope: Scope): Scope {
+ return {
+ ...scope,
+ variables: scope.variables.filter((variable) => {
+ return !snippetIdentifierNodeLocations.some(([start, end]) => {
+ return variable.identifiers.every((identifier) => {
+ const { range } = identifier;
+ return range[0] === start && range[1] === end;
+ });
+ });
+ }),
+ childScopes: scope.childScopes.map((scope) => {
+ return removeSnippetIdentifiers(snippetIdentifierNodeLocations, scope);
+ })
+ } as Scope;
+}
+
+export default createRule('no-shadow', {
+ meta: {
+ ...coreRule.meta,
+ docs: {
+ description: coreRule.meta.docs.description,
+ category: 'Best Practices',
+ recommended: false,
+ extensionRule: 'no-shadow'
+ }
+ },
+ create(context) {
+ const snippetIdentifierNodeLocations: Range[] = [];
+
+ function getScope(node: TSESTree.Node) {
+ const scope = getScopeUtil(context, node);
+ return removeSnippetIdentifiers(snippetIdentifierNodeLocations, scope);
+ }
+
+ function getSourceCode() {
+ const sourceCode = getSourceCodeCompat(context);
+ return new Proxy(sourceCode, {
+ get(target, key) {
+ if (key === 'getScope') {
+ return getScope;
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
+ return (target as any)[key];
+ }
+ });
+ }
+
+ return defineWrapperListener(
+ coreRule,
+ getProxyContent(context, {
+ sourceCode: getSourceCode()
+ }),
+ {
+ createListenerProxy(coreListener) {
+ return {
+ ...coreListener,
+ SvelteSnippetBlock(node) {
+ const parent = node.parent;
+ if (parent.type === 'SvelteElement' && parent.kind === 'component') {
+ snippetIdentifierNodeLocations.push(node.id.range);
+ }
+ coreListener.SvelteSnippetBlock?.(node);
+ },
+ 'Program:exit'(node) {
+ coreListener['Program:exit']?.(node);
+ }
+ };
+ }
+ }
+ );
+ }
+});
diff --git a/packages/eslint-plugin-svelte/src/utils/eslint-core.ts b/packages/eslint-plugin-svelte/src/utils/eslint-core.ts
index d78f293e7..32017685e 100644
--- a/packages/eslint-plugin-svelte/src/utils/eslint-core.ts
+++ b/packages/eslint-plugin-svelte/src/utils/eslint-core.ts
@@ -6,6 +6,22 @@ import { Linter } from 'eslint';
import Module from 'module';
const require = Module.createRequire(import.meta.url);
+
+export function getProxyContent(context: RuleContext, overrides: any): RuleContext {
+ const cache: any = {};
+ return new Proxy(context, {
+ get(_t, key) {
+ if (key in cache) {
+ return cache[key];
+ }
+ if (key in overrides) {
+ return (cache[key] = overrides[key]);
+ }
+ return (context as any)[key];
+ }
+ });
+}
+
/**
* Define the wrapped core rule.
*/
@@ -17,10 +33,7 @@ export function defineWrapperListener(
}
): RuleListener {
const listener = coreRule.create(context as any);
-
- const svelteListener = proxyOptions.createListenerProxy?.(listener) ?? listener;
-
- return svelteListener;
+ return proxyOptions.createListenerProxy?.(listener) ?? listener;
}
/**
diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts
index cb0cbe896..e78ccef6a 100644
--- a/packages/eslint-plugin-svelte/src/utils/rules.ts
+++ b/packages/eslint-plugin-svelte/src/utils/rules.ts
@@ -42,6 +42,7 @@ import noReactiveFunctions from '../rules/no-reactive-functions.js';
import noReactiveLiterals from '../rules/no-reactive-literals.js';
import noReactiveReassign from '../rules/no-reactive-reassign.js';
import noRestrictedHtmlElements from '../rules/no-restricted-html-elements.js';
+import noShadow from '../rules/no-shadow.js';
import noShorthandStylePropertyOverrides from '../rules/no-shorthand-style-property-overrides.js';
import noSpacesAroundEqualSignsInAttribute from '../rules/no-spaces-around-equal-signs-in-attribute.js';
import noStoreAsync from '../rules/no-store-async.js';
@@ -113,6 +114,7 @@ export const rules = [
noReactiveLiterals,
noReactiveReassign,
noRestrictedHtmlElements,
+ noShadow,
noShorthandStylePropertyOverrides,
noSpacesAroundEqualSignsInAttribute,
noStoreAsync,
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/invalid/basic-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/invalid/basic-errors.yaml
new file mode 100644
index 000000000..8f4b845d8
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/invalid/basic-errors.yaml
@@ -0,0 +1,4 @@
+- message: "'x' is already declared in the upper scope on line 2 column 13."
+ line: 4
+ column: 8
+ suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/invalid/basic-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/invalid/basic-input.svelte
new file mode 100644
index 000000000..8efb0238c
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/invalid/basic-input.svelte
@@ -0,0 +1,7 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/invalid/const-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/invalid/const-errors.yaml
new file mode 100644
index 000000000..a1ed736a7
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/invalid/const-errors.yaml
@@ -0,0 +1,4 @@
+- message: "'foo' is already declared in the upper scope on line 6 column 10."
+ line: 8
+ column: 11
+ suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/invalid/const-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/invalid/const-input.svelte
new file mode 100644
index 000000000..52f16463a
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/invalid/const-input.svelte
@@ -0,0 +1,10 @@
+
+
+
+ {@const foo = 1}
+
+ {@const foo = 2}
+
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/basic-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/basic-input.svelte
new file mode 100644
index 000000000..4edef5a33
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/basic-input.svelte
@@ -0,0 +1,10 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/snippet1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/snippet1-input.svelte
new file mode 100644
index 000000000..e286d3982
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/snippet1-input.svelte
@@ -0,0 +1,13 @@
+
+
+
+ {#snippet children()}
+
+ {#snippet children()}
+ Hello!
+ {/snippet}
+
+ {/snippet}
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/snippet1-requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/snippet1-requirements.json
new file mode 100644
index 000000000..0192b1098
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/snippet1-requirements.json
@@ -0,0 +1,3 @@
+{
+ "svelte": ">=5.0.0-0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/snippet2-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/snippet2-input.svelte
new file mode 100644
index 000000000..7226eebab
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/snippet2-input.svelte
@@ -0,0 +1,10 @@
+
+
+
+ {#snippet children()}
+ Hello!
+ {/snippet}
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/snippet2-requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/snippet2-requirements.json
new file mode 100644
index 000000000..0192b1098
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-shadow/valid/snippet2-requirements.json
@@ -0,0 +1,3 @@
+{
+ "svelte": ">=5.0.0-0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/src/rules/no-shadow.ts b/packages/eslint-plugin-svelte/tests/src/rules/no-shadow.ts
new file mode 100644
index 000000000..84637a28f
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/src/rules/no-shadow.ts
@@ -0,0 +1,12 @@
+import { RuleTester } from '../../utils/eslint-compat.js';
+import rule from '../../../src/rules/no-shadow.js';
+import { loadTestCases } from '../../utils/utils.js';
+
+const tester = new RuleTester({
+ languageOptions: {
+ ecmaVersion: 2020,
+ sourceType: 'module'
+ }
+});
+
+tester.run('no-shadow', rule as any, loadTestCases('no-shadow'));