diff --git a/.changeset/green-squids-compete.md b/.changeset/green-squids-compete.md new file mode 100644 index 000000000..7666a2ad1 --- /dev/null +++ b/.changeset/green-squids-compete.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-svelte': minor +--- + +Add `prefer-const` rule diff --git a/README.md b/README.md index 2601875d0..e492f8a76 100644 --- a/README.md +++ b/README.md @@ -367,6 +367,7 @@ These rules relate to better ways of doing things to help you avoid problems: | [svelte/no-unused-class-name](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-class-name/) | disallow the use of a class in the template without a corresponding style | | | [svelte/no-unused-svelte-ignore](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: | | [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: | +| [svelte/prefer-const](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-const/) | Require `const` declarations for variables that are never reassigned after declared | :wrench: | | [svelte/prefer-destructured-store-props](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/) | destructure values from object stores for better change tracking & fewer redraws | :bulb: | | [svelte/require-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/) | require keyed `{#each}` block | | | [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | | diff --git a/docs/rules.md b/docs/rules.md index 29701ff7e..d2dd3d7a7 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -64,6 +64,7 @@ These rules relate to better ways of doing things to help you avoid problems: | [svelte/no-unused-class-name](./rules/no-unused-class-name.md) | disallow the use of a class in the template without a corresponding style | | | [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: | | [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: | +| [svelte/prefer-const](./rules/prefer-const.md) | Require `const` declarations for variables that are never reassigned after declared | :wrench: | | [svelte/prefer-destructured-store-props](./rules/prefer-destructured-store-props.md) | destructure values from object stores for better change tracking & fewer redraws | :bulb: | | [svelte/require-each-key](./rules/require-each-key.md) | require keyed `{#each}` block | | | [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | | diff --git a/docs/rules/prefer-const.md b/docs/rules/prefer-const.md new file mode 100644 index 000000000..f24b70fe2 --- /dev/null +++ b/docs/rules/prefer-const.md @@ -0,0 +1,69 @@ +--- +pageClass: 'rule-details' +sidebarDepth: 0 +title: 'svelte/prefer-const' +description: 'Require `const` declarations for variables that are never reassigned after declared' +--- + +# svelte/prefer-const + +> Require `const` declarations for variables that are never reassigned after declared + +- :exclamation: **_This rule has not been released yet._** +- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule. + +## :book: Rule Details + +This rule reports the same as the base ESLint `prefer-const` rule, except that ignores Svelte reactive values such as `$derived` and `$props`. If this rule is active, make sure to disable the base `prefer-const` rule, as it will conflict with this rule. + + + +```svelte + + + + +``` + +## :wrench: Options + +```json +{ + "svelte/prefer-const": [ + "error", + { + "destructuring": "any", + "ignoreReadonly": true + } + ] +} +``` + +- `destructuring`: The kind of the way to address variables in destructuring. There are 2 values: + - `any` (default): if any variables in destructuring should be const, this rule warns for those variables. + - `all`: if all variables in destructuring should be const, this rule warns the variables. Otherwise, ignores them. +- `ignoreReadonly`: If `true`, this rule will ignore variables that are read between the declaration and the _first_ assignment. + +## :books: Further Reading + +- See [ESLint `prefer-const` rule](https://eslint.org/docs/latest/rules/prefer-const) 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/prefer-const.ts) +- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts) + +Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/prefer-const) diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts index 8b7cbe80c..71eae844b 100644 --- a/packages/eslint-plugin-svelte/src/rule-types.ts +++ b/packages/eslint-plugin-svelte/src/rule-types.ts @@ -264,6 +264,11 @@ export interface RuleOptions { * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-class-directive/ */ 'svelte/prefer-class-directive'?: Linter.RuleEntry + /** + * Require `const` declarations for variables that are never reassigned after declared + * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-const/ + */ + 'svelte/prefer-const'?: Linter.RuleEntry /** * destructure values from object stores for better change tracking & fewer redraws * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/ @@ -485,6 +490,11 @@ type SvelteNoUselessMustaches = []|[{ type SveltePreferClassDirective = []|[{ prefer?: ("always" | "empty") }] +// ----- svelte/prefer-const ----- +type SveltePreferConst = []|[{ + destructuring?: ("any" | "all") + ignoreReadBeforeAssign?: boolean +}] // ----- svelte/shorthand-attribute ----- type SvelteShorthandAttribute = []|[{ prefer?: ("always" | "never") diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-const.ts b/packages/eslint-plugin-svelte/src/rules/prefer-const.ts new file mode 100644 index 000000000..0d079b1e1 --- /dev/null +++ b/packages/eslint-plugin-svelte/src/rules/prefer-const.ts @@ -0,0 +1,81 @@ +import type { TSESTree } from '@typescript-eslint/types'; + +import { createRule } from '../utils/index.js'; +import { defineWrapperListener, getCoreRule } from '../utils/eslint-core.js'; + +const coreRule = getCoreRule('prefer-const'); + +/** + * Finds and returns the callee of a declaration node within variable declarations or object patterns. + */ +function findDeclarationCallee(node: TSESTree.Expression) { + const { parent } = node; + if (parent.type === 'VariableDeclarator' && parent.init?.type === 'CallExpression') { + return parent.init.callee; + } + + return null; +} + +/** + * Determines if a declaration should be skipped in the const preference analysis. + * Specifically checks for Svelte's state management utilities ($props, $derived). + */ +function shouldSkipDeclaration(declaration: TSESTree.Expression | null) { + if (!declaration) { + return false; + } + + const callee = findDeclarationCallee(declaration); + if (!callee) { + return false; + } + + if (callee.type === 'Identifier' && ['$props', '$derived'].includes(callee.name)) { + return true; + } + + if (callee.type !== 'MemberExpression' || callee.object.type !== 'Identifier') { + return false; + } + + if ( + callee.object.name === '$derived' && + callee.property.type === 'Identifier' && + callee.property.name === 'by' + ) { + return true; + } + + return false; +} + +export default createRule('prefer-const', { + meta: { + ...coreRule.meta, + docs: { + description: coreRule.meta.docs.description, + category: 'Best Practices', + recommended: false, + extensionRule: 'prefer-const' + } + }, + create(context) { + return defineWrapperListener(coreRule, context, { + createListenerProxy(coreListener) { + return { + ...coreListener, + VariableDeclaration(node) { + for (const decl of node.declarations) { + if (shouldSkipDeclaration(decl.init)) { + return; + } + } + + coreListener.VariableDeclaration?.(node); + } + }; + } + }); + } +}); diff --git a/packages/eslint-plugin-svelte/src/types.ts b/packages/eslint-plugin-svelte/src/types.ts index 7b6452b52..96a6abd9b 100644 --- a/packages/eslint-plugin-svelte/src/types.ts +++ b/packages/eslint-plugin-svelte/src/types.ts @@ -232,6 +232,8 @@ export interface SourceCode { getLines(): string[]; + getDeclaredVariables(node: TSESTree.Node): Variable[]; + getAllComments(): AST.Comment[]; getComments(node: NodeOrToken): { diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts index 296a7700d..bba9a9975 100644 --- a/packages/eslint-plugin-svelte/src/utils/rules.ts +++ b/packages/eslint-plugin-svelte/src/utils/rules.ts @@ -52,6 +52,7 @@ import noUnusedClassName from '../rules/no-unused-class-name.js'; import noUnusedSvelteIgnore from '../rules/no-unused-svelte-ignore.js'; import noUselessMustaches from '../rules/no-useless-mustaches.js'; import preferClassDirective from '../rules/prefer-class-directive.js'; +import preferConst from '../rules/prefer-const.js'; import preferDestructuredStoreProps from '../rules/prefer-destructured-store-props.js'; import preferStyleDirective from '../rules/prefer-style-directive.js'; import requireEachKey from '../rules/require-each-key.js'; @@ -120,6 +121,7 @@ export const rules = [ noUnusedSvelteIgnore, noUselessMustaches, preferClassDirective, + preferConst, preferDestructuredStoreProps, preferStyleDirective, requireEachKey, diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-errors.yaml new file mode 100644 index 000000000..fed423c4a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-errors.yaml @@ -0,0 +1,20 @@ +- message: "'zero' is never reassigned. Use 'const' instead." + line: 3 + column: 6 + suggestions: null +- message: "'state' is never reassigned. Use 'const' instead." + line: 4 + column: 6 + suggestions: null +- message: "'raw' is never reassigned. Use 'const' instead." + line: 5 + column: 6 + suggestions: null +- message: "'doubled' is never reassigned. Use 'const' instead." + line: 6 + column: 6 + suggestions: null +- message: "'calculated' is never reassigned. Use 'const' instead." + line: 8 + column: 6 + suggestions: null diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-input.svelte new file mode 100644 index 000000000..2a9ff2bba --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-input.svelte @@ -0,0 +1,11 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-output.svelte new file mode 100644 index 000000000..fb6da26ca --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-output.svelte @@ -0,0 +1,11 @@ + diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/test01-input.svelte new file mode 100644 index 000000000..5c768eb34 --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/test01-input.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts b/packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts new file mode 100644 index 000000000..ffe176d0a --- /dev/null +++ b/packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts @@ -0,0 +1,12 @@ +import { RuleTester } from '../../utils/eslint-compat'; +import rule from '../../../src/rules/prefer-const'; +import { loadTestCases } from '../../utils/utils'; + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module' + }, +}); + +tester.run('prefer-const', rule as any, loadTestCases('prefer-const'));