Skip to content

Commit 4947459

Browse files
authored
Add svelte/no-reactive-literals rule (#203)
* wip: add no reactive literals rule * feat: add snapshot support for suggestions * wip: feedback & test updates * chore: fix lint issue * docs: yarn update output
1 parent a7073c7 commit 4947459

File tree

11 files changed

+195
-0
lines changed

11 files changed

+195
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ These rules relate to better ways of doing things to help you avoid problems:
281281
|:--------|:------------|:---|
282282
| [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 | |
283283
| [svelte/no-at-debug-tags](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/) | disallow the use of `{@debug}` | :star: |
284+
| [svelte/no-reactive-literals](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-reactive-literals/) | Don't assign literal values in reactive statements | |
284285
| [svelte/no-unused-svelte-ignore](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
285286
| [svelte/no-useless-mustaches](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: |
286287
| [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 | |

docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ These rules relate to better ways of doing things to help you avoid problems:
4141
|:--------|:------------|:---|
4242
| [svelte/button-has-type](./rules/button-has-type.md) | disallow usage of button without an explicit type attribute | |
4343
| [svelte/no-at-debug-tags](./rules/no-at-debug-tags.md) | disallow the use of `{@debug}` | :star: |
44+
| [svelte/no-reactive-literals](./rules/no-reactive-literals.md) | Don't assign literal values in reactive statements | |
4445
| [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
4546
| [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
4647
| [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | |

docs/rules/no-reactive-literals.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "svelte/no-reactive-literals"
5+
description: "Don't assign literal values in reactive statements"
6+
---
7+
8+
# svelte/no-reactive-literals
9+
10+
> Don't assign literal values in reactive statements
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
14+
## :book: Rule Details
15+
16+
This rule reports on any assignment of a static, unchanging value within a reactive statement because it's not necessary.
17+
18+
<ESLintCodeBlock>
19+
20+
<!--eslint-skip-->
21+
22+
```svelte
23+
<script>
24+
/* eslint svelte/no-reactive-literals: "error" */
25+
/* ✓ GOOD */
26+
let foo = "bar";
27+
28+
/* ✗ BAD */
29+
$: foo = "bar";
30+
</script>
31+
```
32+
33+
</ESLintCodeBlock>
34+
35+
## :wrench: Options
36+
37+
Nothing
38+
39+
## :mag: Implementation
40+
41+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-reactive-literals.ts)
42+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-reactive-literals.ts)

src/rules/no-reactive-literals.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { TSESTree } from "@typescript-eslint/types"
2+
import { createRule } from "../utils"
3+
4+
export default createRule("no-reactive-literals", {
5+
meta: {
6+
docs: {
7+
description: "Don't assign literal values in reactive statements",
8+
category: "Best Practices",
9+
recommended: false,
10+
},
11+
hasSuggestions: true,
12+
schema: [],
13+
messages: {
14+
noReactiveLiterals: `Do not assign literal values inside reactive statements unless absolutely necessary.`,
15+
fixReactiveLiteral: `Move the literal out of the reactive statement into an assignment`,
16+
},
17+
type: "suggestion",
18+
},
19+
create(context) {
20+
return {
21+
[`SvelteReactiveStatement > ExpressionStatement > AssignmentExpression${[
22+
// $: foo = "foo";
23+
// $: foo = 1;
24+
`[right.type="Literal"]`,
25+
26+
// $: foo = [];
27+
`[right.type="ArrayExpression"][right.elements.length=0]`,
28+
29+
// $: foo = {};
30+
`[right.type="ObjectExpression"][right.properties.length=0]`,
31+
].join(",")}`](node: TSESTree.AssignmentExpression) {
32+
// Move upwards to include the entire reactive statement
33+
const parent = node.parent?.parent
34+
35+
if (!parent) {
36+
return false
37+
}
38+
39+
const source = context.getSourceCode()
40+
41+
return context.report({
42+
node: parent,
43+
loc: parent.loc,
44+
messageId: "noReactiveLiterals",
45+
suggest: [
46+
{
47+
messageId: "fixReactiveLiteral",
48+
fix(fixer) {
49+
return [
50+
// Insert "let" + whatever was in there
51+
fixer.insertTextBefore(parent, `let ${source.getText(node)}`),
52+
53+
// Remove the original reactive statement
54+
fixer.remove(parent),
55+
]
56+
},
57+
},
58+
],
59+
})
60+
},
61+
}
62+
},
63+
})

src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export interface RuleMetaData {
6969
}
7070
messages: { [messageId: string]: string }
7171
fixable?: "code" | "whitespace"
72+
hasSuggestions?: boolean
7273
schema: JSONSchema4 | JSONSchema4[]
7374
deprecated?: boolean
7475
replacedBy?: string[]
@@ -98,6 +99,7 @@ export interface PartialRuleMetaData {
9899
)
99100
messages: { [messageId: string]: string }
100101
fixable?: "code" | "whitespace"
102+
hasSuggestions?: boolean
101103
schema: JSONSchema4 | JSONSchema4[]
102104
deprecated?: boolean
103105
replacedBy?: string[]

src/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import noDynamicSlotName from "../rules/no-dynamic-slot-name"
1515
import noInnerDeclarations from "../rules/no-inner-declarations"
1616
import noNotFunctionHandler from "../rules/no-not-function-handler"
1717
import noObjectInTextMustaches from "../rules/no-object-in-text-mustaches"
18+
import noReactiveLiterals from "../rules/no-reactive-literals"
1819
import noShorthandStylePropertyOverrides from "../rules/no-shorthand-style-property-overrides"
1920
import noSpacesAroundEqualSignsInAttribute from "../rules/no-spaces-around-equal-signs-in-attribute"
2021
import noTargetBlank from "../rules/no-target-blank"
@@ -47,6 +48,7 @@ export const rules = [
4748
noInnerDeclarations,
4849
noNotFunctionHandler,
4950
noObjectInTextMustaches,
51+
noReactiveLiterals,
5052
noShorthandStylePropertyOverrides,
5153
noSpacesAroundEqualSignsInAttribute,
5254
noTargetBlank,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[
2+
{
3+
"message": "Do not assign literal values inside reactive statements unless absolutely necessary.",
4+
"line": 3,
5+
"column": 5,
6+
"suggestions": [
7+
{
8+
"desc": "Move the literal out of the reactive statement into an assignment",
9+
"messageId": "fixReactiveLiteral",
10+
"output": "<!-- prettier-ignore -->\n<script>\n let foo = \"foo\"\n $: bar = [];\n $: baz = {};\n</script>\n"
11+
}
12+
]
13+
},
14+
{
15+
"message": "Do not assign literal values inside reactive statements unless absolutely necessary.",
16+
"line": 4,
17+
"column": 5,
18+
"suggestions": [
19+
{
20+
"desc": "Move the literal out of the reactive statement into an assignment",
21+
"messageId": "fixReactiveLiteral",
22+
"output": "<!-- prettier-ignore -->\n<script>\n $: foo = \"foo\";\n let bar = []\n $: baz = {};\n</script>\n"
23+
}
24+
]
25+
},
26+
{
27+
"message": "Do not assign literal values inside reactive statements unless absolutely necessary.",
28+
"line": 5,
29+
"column": 5,
30+
"suggestions": [
31+
{
32+
"desc": "Move the literal out of the reactive statement into an assignment",
33+
"messageId": "fixReactiveLiteral",
34+
"output": "<!-- prettier-ignore -->\n<script>\n $: foo = \"foo\";\n $: bar = [];\n let baz = {}\n</script>\n"
35+
}
36+
]
37+
}
38+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<!-- prettier-ignore -->
2+
<script>
3+
$: foo = "foo";
4+
$: bar = [];
5+
$: baz = {};
6+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<!-- prettier-ignore -->
2+
<script>
3+
$: foo = `${"bar"}baz`
4+
$: bar = [ "bar" ]
5+
$: baz = { qux : true }
6+
</script>
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../src/rules/no-reactive-literals"
3+
import { loadTestCases } from "../../utils/utils"
4+
5+
const tester = new RuleTester({
6+
parserOptions: {
7+
ecmaVersion: 2020,
8+
sourceType: "module",
9+
},
10+
})
11+
12+
tester.run(
13+
"no-reactive-literals",
14+
rule as any,
15+
loadTestCases("no-reactive-literals"),
16+
)

tests/utils/utils.ts

+18
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export function loadTestCases(
130130
throw new Error(`Empty code: ${test.filename}`)
131131
}
132132
}
133+
133134
return {
134135
valid,
135136
invalid,
@@ -155,6 +156,14 @@ function* itrListupInput(rootDir: string): IterableIterator<string> {
155156
}
156157
}
157158

159+
// Necessary because of this:
160+
// https://github.com/eslint/eslint/issues/14936#issuecomment-906746754
161+
function applySuggestion(code: string, suggestion: Linter.LintSuggestion) {
162+
const { fix } = suggestion
163+
164+
return `${code.slice(0, fix.range[0])}${fix.text}${code.slice(fix.range[1])}`
165+
}
166+
158167
function writeFixtures(
159168
ruleName: string,
160169
inputFile: string,
@@ -184,6 +193,7 @@ function writeFixtures(
184193
},
185194
config.filename,
186195
)
196+
187197
if (force || !fs.existsSync(errorFile)) {
188198
fs.writeFileSync(
189199
errorFile,
@@ -192,6 +202,14 @@ function writeFixtures(
192202
message: m.message,
193203
line: m.line,
194204
column: m.column,
205+
suggestions: m.suggestions
206+
? m.suggestions.map((s) => ({
207+
desc: s.desc,
208+
messageId: s.messageId,
209+
// Need to have this be the *fixed* output, not just the fix content or anything
210+
output: applySuggestion(config.code, s),
211+
}))
212+
: null,
195213
})),
196214
null,
197215
2,

0 commit comments

Comments
 (0)