Skip to content

fix(@typescript-eslint/no-shadow): ignore {#snippet} if it uses under component #995

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-mangos-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-svelte': patch
---

fix(@typescript-eslint/no-shadow): ignore `{#snippet}` if it uses under component
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ These rules relate to better ways of doing things to help you avoid problems:

| Rule ID | Description | |
|:--------|:------------|:---|
| [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 | |
| [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: |
| [svelte/button-has-type](https://sveltejs.github.io/eslint-plugin-svelte/rules/button-has-type/) | disallow usage of button without an explicit type attribute | |
| [svelte/no-at-debug-tags](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/) | disallow the use of `{@debug}` | :star: |
Expand Down
1 change: 1 addition & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ These rules relate to better ways of doing things to help you avoid problems:

| Rule ID | Description | |
| :--------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :------- |
| [svelte/@typescript-eslint/no-shadow](./rules/@typescript-eslint/no-shadow.md) | Disallow variable declarations from shadowing variables declared in the outer scope | |
| [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: |
| [svelte/button-has-type](./rules/button-has-type.md) | disallow usage of button without an explicit type attribute | |
| [svelte/no-at-debug-tags](./rules/no-at-debug-tags.md) | disallow the use of `{@debug}` | :star: |
Expand Down
75 changes: 75 additions & 0 deletions docs/rules/@typescript-eslint/no-shadow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
pageClass: 'rule-details'
sidebarDepth: 0
title: 'svelte/@typescript-eslint/no-shadow'
description: 'Disallow variable declarations from shadowing variables declared in the outer scope'
---

# svelte/@typescript-eslint/no-shadow

> Disallow variable declarations from shadowing variables declared in the outer scope
- :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 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.

<!--eslint-skip-->

```svelte
<script lang="ts">
/* eslint svelte/@typescript-eslint/no-shadow: "error" */
import ComponentWithSnippet from './ComponentWithSnippet.svelte';
</script>
<!-- ✓ GOOD -->
<ComponentWithSnippet>
{#snippet children()}
<AnotherComponentWithSnippet>
{#snippet children()}
Hello!
{/snippet}
</AnotherComponentWithSnippet>
{/snippet}
</ComponentWithSnippet>
<!-- ✗ BAD -->
<ComponentWithSnippet>
{@const foo = 1}
<ComponentWithSnippet>
{@const foo = 2}
</ComponentWithSnippet>
</ComponentWithSnippet>
```

## :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.
- `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.
- `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.

## :books: Further Reading

- See [typescript-eslint `no-shadow` rule](https://typescript-eslint.io/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/@typescript-eslint/no-shadow.ts)
- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/@typescript-eslint/no-shadow.ts)

<sup>Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/@typescript-eslint/no-shadow)</sup>
20 changes: 20 additions & 0 deletions packages/eslint-plugin-svelte/src/rule-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ declare module 'eslint' {
}

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

/* ======= Declarations ======= */
// ----- svelte/@typescript-eslint/no-shadow -----
type SvelteTypescriptEslintNoShadow = []|[{

allow?: string[]

builtinGlobals?: boolean

hoist?: ("all" | "functions" | "never")

ignoreFunctionTypeParameterNameValueShadow?: boolean

ignoreOnInitialization?: boolean

ignoreTypeValueShadow?: boolean
}]
// ----- svelte/@typescript-eslint/no-unnecessary-condition -----
type SvelteTypescriptEslintNoUnnecessaryCondition = []|[{

Expand Down
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('@typescript-eslint/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('@typescript-eslint/no-shadow', {
meta: {
...coreRule.meta,
docs: {
description: coreRule.meta.docs.description,
category: 'Best Practices',
recommended: false,
extensionRule: '@typescript-eslint/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);
}
};
}
}
);
}
});
29 changes: 29 additions & 0 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 Down Expand Up @@ -73,11 +89,24 @@ export function buildProxyListener(
}

let ruleMap: Map<string, RuleModule> | null = null;
let tsRuleMap: Map<string, RuleModule> | null = null;

/**
* Get the core rule implementation from the rule name
*/
export function getCoreRule(ruleName: string): RuleModule {
if (ruleName.startsWith('@typescript-eslint/')) {
if (tsRuleMap == null) {
const rules = require('@typescript-eslint/eslint-plugin').rules;
for (const [name, rule] of Object.entries(rules)) {
if (tsRuleMap == null) {
tsRuleMap = new Map();
}
tsRuleMap.set(`@typescript-eslint/${name}`, rule as RuleModule);
}
}
return tsRuleMap!.get(ruleName)!;
}
try {
const map: Map<string, RuleModule> = ruleMap
? ruleMap
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 @@ -2,6 +2,7 @@
// This file has been automatically generated,
// in order to update its content execute "pnpm run update"
import type { RuleModule } from '../types.js';
import typescriptEslintNoShadow from '../rules/@typescript-eslint/no-shadow.js';
import typescriptEslintNoUnnecessaryCondition from '../rules/@typescript-eslint/no-unnecessary-condition.js';
import blockLang from '../rules/block-lang.js';
import buttonHasType from '../rules/button-has-type.js';
Expand Down Expand Up @@ -73,6 +74,7 @@ import validEachKey from '../rules/valid-each-key.js';
import validPropNamesInKitPages from '../rules/valid-prop-names-in-kit-pages.js';

export const rules = [
typescriptEslintNoShadow,
typescriptEslintNoUnnecessaryCondition,
blockLang,
buttonHasType,
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 lang="ts">
function a(x: string) {
var b = function c() {
var x: string = '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 lang="ts">
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 lang="ts">
var a: number = 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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"svelte": ">=5.0.0-0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script lang="ts">
import ComponentWithSnippet from './ComponentWithSnippet.svelte';
const children: number = 1;
</script>

<ComponentWithSnippet>
{#snippet children()}
Hello!
{/snippet}
</ComponentWithSnippet>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"svelte": ">=5.0.0-0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { RuleTester } from '../../../utils/eslint-compat.js';
import rule from '../../../../src/rules/@typescript-eslint/no-shadow.js';
import { loadTestCases, RULES_PROJECT } from '../../../utils/utils.js';

const tester = new RuleTester({
languageOptions: {
ecmaVersion: 2020,
sourceType: 'module',
parserOptions: {
parser: {
ts: '@typescript-eslint/parser',
js: 'espree'
},
project: RULES_PROJECT,
disallowAutomaticSingleRunInference: true
}
}
});

tester.run(
'@typescript-eslint/no-shadow',
rule as any,
loadTestCases('@typescript-eslint/no-shadow')
);
Loading