diff --git a/.changeset/petite-shirts-smash.md b/.changeset/petite-shirts-smash.md new file mode 100644 index 000000000..4900d72b0 --- /dev/null +++ b/.changeset/petite-shirts-smash.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': patch +--- + +fix: add `ignoreLocalVariables` to `prefer-svelte-reactivity` rule to avoid false positives diff --git a/docs/rules/prefer-svelte-reactivity.md b/docs/rules/prefer-svelte-reactivity.md index 4a5edf986..a1f448d44 100644 --- a/docs/rules/prefer-svelte-reactivity.md +++ b/docs/rules/prefer-svelte-reactivity.md @@ -105,7 +105,18 @@ export default e; ## :wrench: Options -Nothing. +```json +{ + "svelte/prefer-svelte-reactivity": [ + "error", + { + "ignoreLocalVariables": true + } + ] +} +``` + +- `ignoreLocalVariables` ... Set to `true` to ignore variables declared anywhere other than the top level, such as inside functions. The default is `true`. In almost all cases, we do not need to set this to `false`. ## :books: Further Reading diff --git a/packages/eslint-plugin-svelte/src/meta.ts b/packages/eslint-plugin-svelte/src/meta.ts index e3afe182b..fa5d7dc09 100644 --- a/packages/eslint-plugin-svelte/src/meta.ts +++ b/packages/eslint-plugin-svelte/src/meta.ts @@ -1,5 +1,5 @@ // IMPORTANT! // This file has been automatically generated, // in order to update its content execute "pnpm run update" -export const name = 'eslint-plugin-svelte' as const; -export const version = '3.11.0' as const; +export const name = 'eslint-plugin-svelte'; +export const version = '3.11.0'; diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index f4fd5c81d..bfc9d75de 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -320,7 +320,7 @@ export interface RuleOptions { * disallow using mutable instances of built-in classes where a reactive alternative is provided by svelte/reactivity * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-svelte-reactivity/ */ - 'svelte/prefer-svelte-reactivity'?: Linter.RuleEntry<[]> + 'svelte/prefer-svelte-reactivity'?: Linter.RuleEntry /** * Prefer using writable $derived instead of $state and $effect * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-writable-derived/ @@ -580,6 +580,10 @@ type SveltePreferConst = []|[{ excludedRunes?: string[] [k: string]: unknown | undefined }] +// ----- svelte/prefer-svelte-reactivity ----- +type SveltePreferSvelteReactivity = []|[{ + ignoreLocalVariables?: boolean +}] // ----- svelte/require-event-prefix ----- type SvelteRequireEventPrefix = []|[{ checkAsyncFunctions?: boolean diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-svelte-reactivity.ts b/packages/eslint-plugin-svelte/src/rules/prefer-svelte-reactivity.ts index 7118b054d..5c6a05bc6 100644 --- a/packages/eslint-plugin-svelte/src/rules/prefer-svelte-reactivity.ts +++ b/packages/eslint-plugin-svelte/src/rules/prefer-svelte-reactivity.ts @@ -2,7 +2,8 @@ import { ReferenceTracker } from '@eslint-community/eslint-utils'; import { createRule } from '../utils/index.js'; import type { TSESTree } from '@typescript-eslint/types'; import { findVariable, isIn } from '../utils/ast-utils.js'; -import { getSvelteContext } from '../utils/svelte-context.js'; +import { getSvelteContext } from 'src/utils/svelte-context.js'; +import type { AST } from 'svelte-eslint-parser'; export default createRule('prefer-svelte-reactivity', { meta: { @@ -12,7 +13,18 @@ export default createRule('prefer-svelte-reactivity', { category: 'Possible Errors', recommended: true }, - schema: [], + schema: [ + { + type: 'object', + properties: { + ignoreLocalVariables: { + type: 'boolean', + default: true + } + }, + additionalProperties: false + } + ], messages: { mutableDateUsed: 'Found a mutable instance of the built-in Date class. Use SvelteDate instead.', @@ -31,6 +43,7 @@ export default createRule('prefer-svelte-reactivity', { ] }, create(context) { + const options = context.options[0] ?? { ignoreLocalVariables: true }; const exportedVars: TSESTree.Node[] = []; return { ...(getSvelteContext(context)?.svelteFileType === '.svelte.[js|ts]' && { @@ -78,6 +91,10 @@ export default createRule('prefer-svelte-reactivity', { [ReferenceTracker.CONSTRUCT]: true } })) { + if (options.ignoreLocalVariables && !isTopLevelDeclaration(node)) { + continue; + } + const messageId = path[0] === 'Date' ? 'mutableDateUsed' @@ -135,6 +152,32 @@ export default createRule('prefer-svelte-reactivity', { } }); +function isTopLevelDeclaration(node: TSESTree.Node | AST.SvelteNode): boolean { + let declaration: TSESTree.Node | AST.SvelteNode | null = node; + while ( + declaration && + declaration.type !== 'VariableDeclaration' && + declaration.type !== 'FunctionDeclaration' && + declaration.type !== 'ClassDeclaration' && + declaration.type !== 'ExportDefaultDeclaration' + ) { + declaration = declaration.parent as TSESTree.Node | AST.SvelteNode | null; + } + + if (!declaration) { + return false; + } + + const parentType: string | undefined = declaration.parent?.type; + return ( + parentType === 'SvelteScriptElement' || + parentType === 'Program' || + parentType === 'ExportDefaultDeclaration' || + parentType === 'ExportNamedDeclaration' || + parentType === 'ExportAllDeclaration' + ); +} + function isDateMutable(referenceTracker: ReferenceTracker, ctorNode: TSESTree.Expression): boolean { return !referenceTracker .iteratePropertyReferences(ctorNode, { diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/local-variables-config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/local-variables-config.json new file mode 100644 index 000000000..281f629c3 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/local-variables-config.json @@ -0,0 +1,7 @@ +{ + "options": [ + { + "ignoreLocalVariables": false + } + ] +} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/local-variables-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/local-variables-errors.yaml new file mode 100644 index 000000000..cc874f53c --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/local-variables-errors.yaml @@ -0,0 +1,4 @@ +- message: Found a mutable instance of the built-in Map class. Use SvelteMap instead. + line: 3 + column: 15 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/local-variables-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/local-variables-input.svelte new file mode 100644 index 000000000..b66ee27ae --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/invalid/local-variables-input.svelte @@ -0,0 +1,8 @@ + + +{foo()} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/valid/local-variables1-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/valid/local-variables1-input.svelte new file mode 100644 index 000000000..6f1229d32 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/valid/local-variables1-input.svelte @@ -0,0 +1,13 @@ + + +{foo()} diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/valid/local-variables2-input.svelte.js b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/valid/local-variables2-input.svelte.js new file mode 100644 index 000000000..607764ae4 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-svelte-reactivity/valid/local-variables2-input.svelte.js @@ -0,0 +1,16 @@ +let toys = $state([]); + +export function getUniqueToys() { + let names = new Set(); + let uniqueToys = []; + for (const toy in toys) { + if (!names.has(toy.name)) { + uniqueToys.push(toy); + } + } + return uniqueToys; +} + +let uniqueToys = $derived.by(getUniqueToys); + +console.log(uniqueToys) \ No newline at end of file