Skip to content

Commit

Permalink
fix(no-shadow): ignore {#snippet} if it uses under component.
Browse files Browse the repository at this point in the history
  • Loading branch information
baseballyama committed Jan 5, 2025
1 parent 2bd1799 commit 1eff52b
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/silly-lions-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-svelte': patch
---

fix(no-shadow): ignore `{#snippet}` if it uses under component.
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { AST } from 'svelte-eslint-parser';
import type { TSESTree } from '@typescript-eslint/types';
import type { ASTNode, RuleContext, RuleListener } from '../../types.js';
import * as SV from './svelte.js';
import * as ES from './es.js';
Expand Down Expand Up @@ -244,7 +243,7 @@ export function defineVisitor(
offsets.ignore(node);
}
},
'Program:exit'(node: TSESTree.Program) {
'Program:exit'(node: AST.SvelteProgram) {
const calculator = offsets.getOffsetCalculator();

let prevToken: AnyToken | null = null;
Expand Down
83 changes: 83 additions & 0 deletions packages/eslint-plugin-svelte/src/rules/no-shadow.ts
Original file line number Diff line number Diff line change
@@ -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);
}
};
}
}
);
}
});
1 change: 1 addition & 0 deletions packages/eslint-plugin-svelte/src/types-for-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export type ASTNodeListener = {
TSUnknownKeyword?: (node: TSESTree.TSUnknownKeyword & ASTNodeWithParent) => void;
TSVoidKeyword?: (node: TSESTree.TSVoidKeyword & ASTNodeWithParent) => void;
Program?: (node: AST.SvelteProgram & ASTNodeWithParent) => void;
'Program:exit'?: (node: AST.SvelteProgram & ASTNodeWithParent) => void;
SvelteScriptElement?: (node: AST.SvelteScriptElement & ASTNodeWithParent) => void;
SvelteStyleElement?: (node: AST.SvelteStyleElement & ASTNodeWithParent) => void;
SvelteElement?: (node: AST.SvelteElement & ASTNodeWithParent) => void;
Expand Down
21 changes: 17 additions & 4 deletions packages/eslint-plugin-svelte/src/utils/eslint-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin-svelte/src/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -113,6 +114,7 @@ export const rules = [
noReactiveLiterals,
noReactiveReassign,
noRestrictedHtmlElements,
noShadow,
noShorthandStylePropertyOverrides,
noSpacesAroundEqualSignsInAttribute,
noStoreAsync,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- message: "'x' is already declared in the upper scope on line 2 column 13."
line: 4
column: 8
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
function a(x) {
var b = function c() {
var x = 'foo';
};
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- message: "'foo' is already declared in the upper scope on line 6 column 10."
line: 8
column: 11
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script>
import ComponentWithSnippet from './ComponentWithSnippet.svelte';
</script>

<ComponentWithSnippet>
{@const foo = 1}
<ComponentWithSnippet>
{@const foo = 2}
</ComponentWithSnippet>
</ComponentWithSnippet>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script>
var a = 3;
var b = (x) => {
a++;
return x + a;
};
setTimeout(() => {
b(a);
}, 0);
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script>
import ComponentWithSnippet from './ComponentWithSnippet.svelte';
</script>

<ComponentWithSnippet>
{#snippet children()}
<AnotherComponentWithSnippet>
{#snippet children()}
Hello!
{/snippet}
</AnotherComponentWithSnippet>
{/snippet}
</ComponentWithSnippet>
12 changes: 12 additions & 0 deletions packages/eslint-plugin-svelte/tests/src/rules/no-shadow.ts
Original file line number Diff line number Diff line change
@@ -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'));

0 comments on commit 1eff52b

Please sign in to comment.