diff --git a/README.md b/README.md index 3f346b4a6..5dbdf553f 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,7 @@ These rules relate to better ways of doing things to help you avoid problems: |:--------|:------------|:---| | [svelte/button-has-type](https://ota-meshi.github.io/eslint-plugin-svelte/rules/button-has-type/) | disallow usage of button without an explicit type attribute | | | [svelte/no-at-debug-tags](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/) | disallow the use of `{@debug}` | :star: | +| [svelte/no-reactive-literals](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-reactive-literals/) | Don't assign literal values in reactive statements | | | [svelte/no-unused-svelte-ignore](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: | | [svelte/no-useless-mustaches](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: | | [svelte/require-optimized-style-attribute](https://ota-meshi.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | | diff --git a/docs/rules.md b/docs/rules.md index 900cdcc55..9581c794f 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -41,6 +41,7 @@ These rules relate to better ways of doing things to help you avoid problems: |:--------|:------------|:---| | [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: | +| [svelte/no-reactive-literals](./rules/no-reactive-literals.md) | Don't assign literal values in reactive statements | | | [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/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | | diff --git a/docs/rules/no-reactive-literals.md b/docs/rules/no-reactive-literals.md new file mode 100644 index 000000000..37a297d42 --- /dev/null +++ b/docs/rules/no-reactive-literals.md @@ -0,0 +1,42 @@ +--- +pageClass: "rule-details" +sidebarDepth: 0 +title: "svelte/no-reactive-literals" +description: "Don't assign literal values in reactive statements" +--- + +# svelte/no-reactive-literals + +> Don't assign literal values in reactive statements + +- :exclamation: **_This rule has not been released yet._** + +## :book: Rule Details + +This rule reports on any assignment of a static, unchanging value within a reactive statement because it's not necessary. + + + + + +```svelte + +``` + + + +## :wrench: Options + +Nothing + +## :mag: Implementation + +- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-reactive-literals.ts) +- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-reactive-literals.ts) diff --git a/src/rules/no-reactive-literals.ts b/src/rules/no-reactive-literals.ts new file mode 100644 index 000000000..ba69034a1 --- /dev/null +++ b/src/rules/no-reactive-literals.ts @@ -0,0 +1,63 @@ +import type { TSESTree } from "@typescript-eslint/types" +import { createRule } from "../utils" + +export default createRule("no-reactive-literals", { + meta: { + docs: { + description: "Don't assign literal values in reactive statements", + category: "Best Practices", + recommended: false, + }, + hasSuggestions: true, + schema: [], + messages: { + noReactiveLiterals: `Do not assign literal values inside reactive statements unless absolutely necessary.`, + fixReactiveLiteral: `Move the literal out of the reactive statement into an assignment`, + }, + type: "suggestion", + }, + create(context) { + return { + [`SvelteReactiveStatement > ExpressionStatement > AssignmentExpression${[ + // $: foo = "foo"; + // $: foo = 1; + `[right.type="Literal"]`, + + // $: foo = []; + `[right.type="ArrayExpression"][right.elements.length=0]`, + + // $: foo = {}; + `[right.type="ObjectExpression"][right.properties.length=0]`, + ].join(",")}`](node: TSESTree.AssignmentExpression) { + // Move upwards to include the entire reactive statement + const parent = node.parent?.parent + + if (!parent) { + return false + } + + const source = context.getSourceCode() + + return context.report({ + node: parent, + loc: parent.loc, + messageId: "noReactiveLiterals", + suggest: [ + { + messageId: "fixReactiveLiteral", + fix(fixer) { + return [ + // Insert "let" + whatever was in there + fixer.insertTextBefore(parent, `let ${source.getText(node)}`), + + // Remove the original reactive statement + fixer.remove(parent), + ] + }, + }, + ], + }) + }, + } + }, +}) diff --git a/src/types.ts b/src/types.ts index 4a8b32452..fc14c2701 100644 --- a/src/types.ts +++ b/src/types.ts @@ -69,6 +69,7 @@ export interface RuleMetaData { } messages: { [messageId: string]: string } fixable?: "code" | "whitespace" + hasSuggestions?: boolean schema: JSONSchema4 | JSONSchema4[] deprecated?: boolean replacedBy?: string[] @@ -98,6 +99,7 @@ export interface PartialRuleMetaData { ) messages: { [messageId: string]: string } fixable?: "code" | "whitespace" + hasSuggestions?: boolean schema: JSONSchema4 | JSONSchema4[] deprecated?: boolean replacedBy?: string[] diff --git a/src/utils/rules.ts b/src/utils/rules.ts index 8a2b068fc..c08ae0f1d 100644 --- a/src/utils/rules.ts +++ b/src/utils/rules.ts @@ -15,6 +15,7 @@ import noDynamicSlotName from "../rules/no-dynamic-slot-name" import noInnerDeclarations from "../rules/no-inner-declarations" import noNotFunctionHandler from "../rules/no-not-function-handler" import noObjectInTextMustaches from "../rules/no-object-in-text-mustaches" +import noReactiveLiterals from "../rules/no-reactive-literals" import noShorthandStylePropertyOverrides from "../rules/no-shorthand-style-property-overrides" import noSpacesAroundEqualSignsInAttribute from "../rules/no-spaces-around-equal-signs-in-attribute" import noTargetBlank from "../rules/no-target-blank" @@ -47,6 +48,7 @@ export const rules = [ noInnerDeclarations, noNotFunctionHandler, noObjectInTextMustaches, + noReactiveLiterals, noShorthandStylePropertyOverrides, noSpacesAroundEqualSignsInAttribute, noTargetBlank, diff --git a/tests/fixtures/rules/no-reactive-literals/invalid/test01-errors.json b/tests/fixtures/rules/no-reactive-literals/invalid/test01-errors.json new file mode 100644 index 000000000..c09a9bba6 --- /dev/null +++ b/tests/fixtures/rules/no-reactive-literals/invalid/test01-errors.json @@ -0,0 +1,38 @@ +[ + { + "message": "Do not assign literal values inside reactive statements unless absolutely necessary.", + "line": 3, + "column": 5, + "suggestions": [ + { + "desc": "Move the literal out of the reactive statement into an assignment", + "messageId": "fixReactiveLiteral", + "output": "\n\n" + } + ] + }, + { + "message": "Do not assign literal values inside reactive statements unless absolutely necessary.", + "line": 4, + "column": 5, + "suggestions": [ + { + "desc": "Move the literal out of the reactive statement into an assignment", + "messageId": "fixReactiveLiteral", + "output": "\n\n" + } + ] + }, + { + "message": "Do not assign literal values inside reactive statements unless absolutely necessary.", + "line": 5, + "column": 5, + "suggestions": [ + { + "desc": "Move the literal out of the reactive statement into an assignment", + "messageId": "fixReactiveLiteral", + "output": "\n\n" + } + ] + } +] diff --git a/tests/fixtures/rules/no-reactive-literals/invalid/test01-input.svelte b/tests/fixtures/rules/no-reactive-literals/invalid/test01-input.svelte new file mode 100644 index 000000000..0267fe56b --- /dev/null +++ b/tests/fixtures/rules/no-reactive-literals/invalid/test01-input.svelte @@ -0,0 +1,6 @@ + + diff --git a/tests/fixtures/rules/no-reactive-literals/valid/test01-input.svelte b/tests/fixtures/rules/no-reactive-literals/valid/test01-input.svelte new file mode 100644 index 000000000..67e41d3e0 --- /dev/null +++ b/tests/fixtures/rules/no-reactive-literals/valid/test01-input.svelte @@ -0,0 +1,6 @@ + + diff --git a/tests/src/rules/no-reactive-literals.ts b/tests/src/rules/no-reactive-literals.ts new file mode 100644 index 000000000..d78739107 --- /dev/null +++ b/tests/src/rules/no-reactive-literals.ts @@ -0,0 +1,16 @@ +import { RuleTester } from "eslint" +import rule from "../../../src/rules/no-reactive-literals" +import { loadTestCases } from "../../utils/utils" + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, +}) + +tester.run( + "no-reactive-literals", + rule as any, + loadTestCases("no-reactive-literals"), +) diff --git a/tests/utils/utils.ts b/tests/utils/utils.ts index 56b70694d..644ff8ee7 100644 --- a/tests/utils/utils.ts +++ b/tests/utils/utils.ts @@ -130,6 +130,7 @@ export function loadTestCases( throw new Error(`Empty code: ${test.filename}`) } } + return { valid, invalid, @@ -155,6 +156,14 @@ function* itrListupInput(rootDir: string): IterableIterator { } } +// Necessary because of this: +// https://github.com/eslint/eslint/issues/14936#issuecomment-906746754 +function applySuggestion(code: string, suggestion: Linter.LintSuggestion) { + const { fix } = suggestion + + return `${code.slice(0, fix.range[0])}${fix.text}${code.slice(fix.range[1])}` +} + function writeFixtures( ruleName: string, inputFile: string, @@ -184,6 +193,7 @@ function writeFixtures( }, config.filename, ) + if (force || !fs.existsSync(errorFile)) { fs.writeFileSync( errorFile, @@ -192,6 +202,14 @@ function writeFixtures( message: m.message, line: m.line, column: m.column, + suggestions: m.suggestions + ? m.suggestions.map((s) => ({ + desc: s.desc, + messageId: s.messageId, + // Need to have this be the *fixed* output, not just the fix content or anything + output: applySuggestion(config.code, s), + })) + : null, })), null, 2,