Skip to content

Commit 075fb54

Browse files
committed
fix(@typescript-eslint/no-shadow): ignore {#snippet} if it uses under component
1 parent 811619c commit 075fb54

File tree

18 files changed

+304
-0
lines changed

18 files changed

+304
-0
lines changed

.changeset/quiet-mangos-nail.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': patch
3+
---
4+
5+
fix(@typescript-eslint/no-shadow): ignore `{#snippet}` if it uses under component

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ These rules relate to better ways of doing things to help you avoid problems:
354354

355355
| Rule ID | Description | |
356356
|:--------|:------------|:---|
357+
| [svelte/@typescript-eslint/no-shadow](https://sveltejs.github.io/eslint-plugin-svelte/rules/@typescript-eslint/no-shadow/) | Disallow variable declarations from shadowing variables declared in the outer scope | |
357358
| [svelte/block-lang](https://sveltejs.github.io/eslint-plugin-svelte/rules/block-lang/) | disallows the use of languages other than those specified in the configuration for the lang attribute of `<script>` and `<style>` blocks. | :bulb: |
358359
| [svelte/button-has-type](https://sveltejs.github.io/eslint-plugin-svelte/rules/button-has-type/) | disallow usage of button without an explicit type attribute | |
359360
| [svelte/no-at-debug-tags](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/) | disallow the use of `{@debug}` | :star: |

docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ These rules relate to better ways of doing things to help you avoid problems:
5151

5252
| Rule ID | Description | |
5353
| :--------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :------- |
54+
| [svelte/@typescript-eslint/no-shadow](./rules/@typescript-eslint/no-shadow.md) | Disallow variable declarations from shadowing variables declared in the outer scope | |
5455
| [svelte/block-lang](./rules/block-lang.md) | disallows the use of languages other than those specified in the configuration for the lang attribute of `<script>` and `<style>` blocks. | :bulb: |
5556
| [svelte/button-has-type](./rules/button-has-type.md) | disallow usage of button without an explicit type attribute | |
5657
| [svelte/no-at-debug-tags](./rules/no-at-debug-tags.md) | disallow the use of `{@debug}` | :star: |
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
pageClass: 'rule-details'
3+
sidebarDepth: 0
4+
title: 'svelte/@typescript-eslint/no-shadow'
5+
description: 'Disallow variable declarations from shadowing variables declared in the outer scope'
6+
---
7+
8+
# svelte/@typescript-eslint/no-shadow
9+
10+
> Disallow variable declarations from shadowing variables declared in the outer scope
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
14+
## :book: Rule Details
15+
16+
This rule reports shadowed variables, similar to the base ESLint `@typescript-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 `@typescript-eslint/no-shadow` and `svelte/no-shadow` and `no-shadow` rule, as it will conflict with this rule.
17+
18+
<!--eslint-skip-->
19+
20+
```svelte
21+
<script lang="ts">
22+
/* eslint svelte/@typescript-eslint/no-shadow: "error" */
23+
import ComponentWithSnippet from './ComponentWithSnippet.svelte';
24+
</script>
25+
26+
<!-- ✓ GOOD -->
27+
<ComponentWithSnippet>
28+
{#snippet children()}
29+
<AnotherComponentWithSnippet>
30+
{#snippet children()}
31+
Hello!
32+
{/snippet}
33+
</AnotherComponentWithSnippet>
34+
{/snippet}
35+
</ComponentWithSnippet>
36+
<!-- ✗ BAD -->
37+
<ComponentWithSnippet>
38+
{@const foo = 1}
39+
<ComponentWithSnippet>
40+
{@const foo = 2}
41+
</ComponentWithSnippet>
42+
</ComponentWithSnippet>
43+
```
44+
45+
## :wrench: Options
46+
47+
```json
48+
{
49+
"svelte/no-shadow": [
50+
"error",
51+
{ "builtinGlobals": false, "hoist": "functions", "allow": [], "ignoreOnInitialization": false }
52+
]
53+
}
54+
```
55+
56+
- `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.
57+
- `hoist`: The `hoist` option has three settings:
58+
- `functions` (by default) - reports shadowing before the outer functions are defined.
59+
- `all` - reports all shadowing before the outer variables/functions are defined.
60+
- `never` - never report shadowing before the outer variables/functions are defined.
61+
- `allow`: The `allow` option is an array of identifier names for which shadowing is allowed. For example, `"resolve"`, `"reject"`, `"done"`, `"cb"`.
62+
- `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.
63+
- `ignoreTypeValueShadow`: Whether to ignore types named the same as a variable. Default: `true`. This is generally safe because you cannot use variables in type locations without a `typeof` operator, so there's little risk of confusion.
64+
- `ignoreFunctionTypeParameterNameValueShadow`: Whether to ignore function parameters named the same as a variable. Default: `true`. Each of a function type's arguments creates a value variable within the scope of the function type. This is done so that you can reference the type later using the `typeof` operator.
65+
66+
## :books: Further Reading
67+
68+
- See [typescript-eslint `no-shadow` rule](https://typescript-eslint.io/rules/no-shadow/) for more information about the base rule.
69+
70+
## :mag: Implementation
71+
72+
- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/@typescript-eslint/no-shadow.ts)
73+
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/@typescript-eslint/no-shadow.ts)
74+
75+
<sup>Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/@typescript-eslint/no-shadow)</sup>

packages/eslint-plugin-svelte/src/rule-types.ts

+20
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ declare module 'eslint' {
1313
}
1414

1515
export interface RuleOptions {
16+
/**
17+
* Disallow variable declarations from shadowing variables declared in the outer scope
18+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/@typescript-eslint/no-shadow/
19+
*/
20+
'svelte/@typescript-eslint/no-shadow'?: Linter.RuleEntry<SvelteTypescriptEslintNoShadow>
1621
/**
1722
* disallow conditionals where the type is always truthy or always falsy
1823
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/@typescript-eslint/no-unnecessary-condition/
@@ -363,6 +368,21 @@ export interface RuleOptions {
363368
}
364369

365370
/* ======= Declarations ======= */
371+
// ----- svelte/@typescript-eslint/no-shadow -----
372+
type SvelteTypescriptEslintNoShadow = []|[{
373+
374+
allow?: string[]
375+
376+
builtinGlobals?: boolean
377+
378+
hoist?: ("all" | "functions" | "never")
379+
380+
ignoreFunctionTypeParameterNameValueShadow?: boolean
381+
382+
ignoreOnInitialization?: boolean
383+
384+
ignoreTypeValueShadow?: boolean
385+
}]
366386
// ----- svelte/@typescript-eslint/no-unnecessary-condition -----
367387
type SvelteTypescriptEslintNoUnnecessaryCondition = []|[{
368388

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { createRule } from '../../utils/index.js';
2+
import { defineWrapperListener, getProxyContent, getCoreRule } from '../../utils/eslint-core.js';
3+
import type { TSESTree } from '@typescript-eslint/types';
4+
import type { Scope } from '@typescript-eslint/scope-manager';
5+
import type { Range } from 'svelte-eslint-parser/lib/ast/common.js';
6+
import { getScope as getScopeUtil } from '../../utils/ast-utils.js';
7+
import { getSourceCode as getSourceCodeCompat } from '../../utils/compat.js';
8+
9+
const coreRule = getCoreRule('@typescript-eslint/no-shadow');
10+
11+
function removeSnippetIdentifiers(snippetIdentifierNodeLocations: Range[], scope: Scope): Scope {
12+
return {
13+
...scope,
14+
variables: scope.variables.filter((variable) => {
15+
return !snippetIdentifierNodeLocations.some(([start, end]) => {
16+
return variable.identifiers.every((identifier) => {
17+
const { range } = identifier;
18+
return range[0] === start && range[1] === end;
19+
});
20+
});
21+
}),
22+
childScopes: scope.childScopes.map((scope) => {
23+
return removeSnippetIdentifiers(snippetIdentifierNodeLocations, scope);
24+
})
25+
} as Scope;
26+
}
27+
28+
export default createRule('@typescript-eslint/no-shadow', {
29+
meta: {
30+
...coreRule.meta,
31+
docs: {
32+
description: coreRule.meta.docs.description,
33+
category: 'Best Practices',
34+
recommended: false,
35+
extensionRule: '@typescript-eslint/no-shadow'
36+
}
37+
},
38+
create(context) {
39+
const snippetIdentifierNodeLocations: Range[] = [];
40+
41+
function getScope(node: TSESTree.Node) {
42+
const scope = getScopeUtil(context, node);
43+
return removeSnippetIdentifiers(snippetIdentifierNodeLocations, scope);
44+
}
45+
46+
function getSourceCode() {
47+
const sourceCode = getSourceCodeCompat(context);
48+
return new Proxy(sourceCode, {
49+
get(target, key) {
50+
if (key === 'getScope') {
51+
return getScope;
52+
}
53+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
54+
return (target as any)[key];
55+
}
56+
});
57+
}
58+
59+
return defineWrapperListener(
60+
coreRule,
61+
getProxyContent(context, {
62+
sourceCode: getSourceCode()
63+
}),
64+
{
65+
createListenerProxy(coreListener) {
66+
return {
67+
...coreListener,
68+
SvelteSnippetBlock(node) {
69+
const parent = node.parent;
70+
if (parent.type === 'SvelteElement' && parent.kind === 'component') {
71+
snippetIdentifierNodeLocations.push(node.id.range);
72+
}
73+
coreListener.SvelteSnippetBlock?.(node);
74+
},
75+
'Program:exit'(node) {
76+
coreListener['Program:exit']?.(node);
77+
}
78+
};
79+
}
80+
}
81+
);
82+
}
83+
});

packages/eslint-plugin-svelte/src/utils/eslint-core.ts

+29
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ import { Linter } from 'eslint';
66
import Module from 'module';
77

88
const require = Module.createRequire(import.meta.url);
9+
10+
export function getProxyContent(context: RuleContext, overrides: any): RuleContext {
11+
const cache: any = {};
12+
return new Proxy(context, {
13+
get(_t, key) {
14+
if (key in cache) {
15+
return cache[key];
16+
}
17+
if (key in overrides) {
18+
return (cache[key] = overrides[key]);
19+
}
20+
return (context as any)[key];
21+
}
22+
});
23+
}
24+
925
/**
1026
* Define the wrapped core rule.
1127
*/
@@ -73,11 +89,24 @@ export function buildProxyListener(
7389
}
7490

7591
let ruleMap: Map<string, RuleModule> | null = null;
92+
let tsRuleMap: Map<string, RuleModule> | null = null;
7693

7794
/**
7895
* Get the core rule implementation from the rule name
7996
*/
8097
export function getCoreRule(ruleName: string): RuleModule {
98+
if (ruleName.startsWith('@typescript-eslint/')) {
99+
if (tsRuleMap == null) {
100+
const rules = require('@typescript-eslint/eslint-plugin').rules;
101+
for (const [name, rule] of Object.entries(rules)) {
102+
if (tsRuleMap == null) {
103+
tsRuleMap = new Map();
104+
}
105+
tsRuleMap.set(`@typescript-eslint/${name}`, rule as RuleModule);
106+
}
107+
}
108+
return tsRuleMap!.get(ruleName)!;
109+
}
81110
try {
82111
const map: Map<string, RuleModule> = ruleMap
83112
? ruleMap

packages/eslint-plugin-svelte/src/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// This file has been automatically generated,
33
// in order to update its content execute "pnpm run update"
44
import type { RuleModule } from '../types.js';
5+
import typescriptEslintNoShadow from '../rules/@typescript-eslint/no-shadow.js';
56
import typescriptEslintNoUnnecessaryCondition from '../rules/@typescript-eslint/no-unnecessary-condition.js';
67
import blockLang from '../rules/block-lang.js';
78
import buttonHasType from '../rules/button-has-type.js';
@@ -73,6 +74,7 @@ import validEachKey from '../rules/valid-each-key.js';
7374
import validPropNamesInKitPages from '../rules/valid-prop-names-in-kit-pages.js';
7475

7576
export const rules = [
77+
typescriptEslintNoShadow,
7678
typescriptEslintNoUnnecessaryCondition,
7779
blockLang,
7880
buttonHasType,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: "'x' is already declared in the upper scope on line 2 column 13."
2+
line: 4
3+
column: 8
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script lang="ts">
2+
function a(x: string) {
3+
var b = function c() {
4+
var x: string = 'foo';
5+
};
6+
}
7+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: "'foo' is already declared in the upper scope on line 6 column 10."
2+
line: 8
3+
column: 11
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script lang="ts">
2+
import ComponentWithSnippet from './ComponentWithSnippet.svelte';
3+
</script>
4+
5+
<ComponentWithSnippet>
6+
{@const foo = 1}
7+
<ComponentWithSnippet>
8+
{@const foo = 2}
9+
</ComponentWithSnippet>
10+
</ComponentWithSnippet>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script lang="ts">
2+
var a: number = 3;
3+
var b = (x) => {
4+
a++;
5+
return x + a;
6+
};
7+
setTimeout(() => {
8+
b(a);
9+
}, 0);
10+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
import ComponentWithSnippet from './ComponentWithSnippet.svelte';
3+
</script>
4+
5+
<ComponentWithSnippet>
6+
{#snippet children()}
7+
<AnotherComponentWithSnippet>
8+
{#snippet children()}
9+
Hello!
10+
{/snippet}
11+
</AnotherComponentWithSnippet>
12+
{/snippet}
13+
</ComponentWithSnippet>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelte": ">=5.0.0-0"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script lang="ts">
2+
import ComponentWithSnippet from './ComponentWithSnippet.svelte';
3+
const children: number = 1;
4+
</script>
5+
6+
<ComponentWithSnippet>
7+
{#snippet children()}
8+
Hello!
9+
{/snippet}
10+
</ComponentWithSnippet>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelte": ">=5.0.0-0"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { RuleTester } from '../../../utils/eslint-compat.js';
2+
import rule from '../../../../src/rules/@typescript-eslint/no-shadow.js';
3+
import { loadTestCases, RULES_PROJECT } from '../../../utils/utils.js';
4+
5+
const tester = new RuleTester({
6+
languageOptions: {
7+
ecmaVersion: 2020,
8+
sourceType: 'module',
9+
parserOptions: {
10+
parser: {
11+
ts: '@typescript-eslint/parser',
12+
js: 'espree'
13+
},
14+
project: RULES_PROJECT,
15+
disallowAutomaticSingleRunInference: true
16+
}
17+
}
18+
});
19+
20+
tester.run(
21+
'@typescript-eslint/no-shadow',
22+
rule as any,
23+
loadTestCases('@typescript-eslint/no-shadow')
24+
);

0 commit comments

Comments
 (0)