Skip to content

Commit 37cfbd1

Browse files
committed
fix(no-shadow): ignore {#snippet} if it uses unser component.
1 parent 2bd1799 commit 37cfbd1

File tree

12 files changed

+168
-4
lines changed

12 files changed

+168
-4
lines changed

.changeset/silly-lions-build.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': patch
3+
---
4+
5+
fix(no-shadow): ignore `{#snippet}` if it uses unser component.
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('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('no-shadow', {
29+
meta: {
30+
...coreRule.meta,
31+
docs: {
32+
description: coreRule.meta.docs.description,
33+
category: 'Best Practices',
34+
recommended: false,
35+
extensionRule: '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/types-for-node.ts

+1
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export type ASTNodeListener = {
211211
TSUnknownKeyword?: (node: TSESTree.TSUnknownKeyword & ASTNodeWithParent) => void;
212212
TSVoidKeyword?: (node: TSESTree.TSVoidKeyword & ASTNodeWithParent) => void;
213213
Program?: (node: AST.SvelteProgram & ASTNodeWithParent) => void;
214+
'Program:exit'?: (node: AST.SvelteProgram & ASTNodeWithParent) => void;
214215
SvelteScriptElement?: (node: AST.SvelteScriptElement & ASTNodeWithParent) => void;
215216
SvelteStyleElement?: (node: AST.SvelteStyleElement & ASTNodeWithParent) => void;
216217
SvelteElement?: (node: AST.SvelteElement & ASTNodeWithParent) => void;

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

+17-4
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
*/
@@ -17,10 +33,7 @@ export function defineWrapperListener(
1733
}
1834
): RuleListener {
1935
const listener = coreRule.create(context as any);
20-
21-
const svelteListener = proxyOptions.createListenerProxy?.(listener) ?? listener;
22-
23-
return svelteListener;
36+
return proxyOptions.createListenerProxy?.(listener) ?? listener;
2437
}
2538

2639
/**

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

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import noReactiveFunctions from '../rules/no-reactive-functions.js';
4242
import noReactiveLiterals from '../rules/no-reactive-literals.js';
4343
import noReactiveReassign from '../rules/no-reactive-reassign.js';
4444
import noRestrictedHtmlElements from '../rules/no-restricted-html-elements.js';
45+
import noShadow from '../rules/no-shadow.js';
4546
import noShorthandStylePropertyOverrides from '../rules/no-shorthand-style-property-overrides.js';
4647
import noSpacesAroundEqualSignsInAttribute from '../rules/no-spaces-around-equal-signs-in-attribute.js';
4748
import noStoreAsync from '../rules/no-store-async.js';
@@ -113,6 +114,7 @@ export const rules = [
113114
noReactiveLiterals,
114115
noReactiveReassign,
115116
noRestrictedHtmlElements,
117+
noShadow,
116118
noShorthandStylePropertyOverrides,
117119
noSpacesAroundEqualSignsInAttribute,
118120
noStoreAsync,
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>
2+
function a(x) {
3+
var b = function c() {
4+
var x = '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>
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>
2+
var a = 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,12 @@
1+
import { RuleTester } from '../../utils/eslint-compat.js';
2+
import rule from '../../../src/rules/no-shadow.js';
3+
import { loadTestCases } from '../../utils/utils.js';
4+
5+
const tester = new RuleTester({
6+
languageOptions: {
7+
ecmaVersion: 2020,
8+
sourceType: 'module'
9+
}
10+
});
11+
12+
tester.run('no-shadow', rule as any, loadTestCases('no-shadow'));

0 commit comments

Comments
 (0)