Skip to content

Commit 119e4af

Browse files
authored
Add svelte/require-stores-init rule (#211)
* Add `svelte/require-stores-init` rule * Update doc * revert tests/utils.ts * fix lint errors * fix test case
1 parent 6f8ec65 commit 119e4af

22 files changed

+217
-11
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ These rules relate to better ways of doing things to help you avoid problems:
287287
| [svelte/no-unused-svelte-ignore](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
288288
| [svelte/no-useless-mustaches](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: |
289289
| [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 | |
290+
| [svelte/require-stores-init](https://ota-meshi.github.io/eslint-plugin-svelte/rules/require-stores-init/) | require initial value in store | |
290291

291292
## Stylistic Issues
292293

docs-svelte-kit/src/lib/components/ESLintCodeBlock.svelte

+5-3
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
let code = ""
1313
export let rules = {}
1414
export let fix = false
15+
export let language = "html"
1516
let time = ""
16-
let options = {
17-
filename: "example.svelte",
17+
$: options = {
18+
filename: language === "html" ? "example.svelte" : "example.js",
1819
preprocess,
1920
postprocess,
2021
}
@@ -43,7 +44,7 @@
4344
{linter}
4445
bind:code
4546
config={{
46-
parser: "svelte-eslint-parser",
47+
parser: language === "html" ? "svelte-eslint-parser" : undefined,
4748
parserOptions: {
4849
ecmaVersion: 2020,
4950
sourceType: "module",
@@ -54,6 +55,7 @@
5455
es2021: true,
5556
},
5657
}}
58+
{language}
5759
{options}
5860
on:result={onLintedResult}
5961
showDiff={showDiff && fix}

docs-svelte-kit/src/lib/eslint/ESLintEditor.svelte

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
export let options = {}
1212
export let fix = true
1313
export let showDiff = true
14+
export let language = "html"
1415
1516
let fixedValue = code
1617
let leftMarkers = []
@@ -221,7 +222,7 @@
221222
bind:this={editor}
222223
bind:code
223224
bind:rightCode={fixedValue}
224-
language="html"
225+
{language}
225226
diffEditor={fix && showDiff}
226227
markers={leftMarkers}
227228
{rightMarkers}

docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ These rules relate to better ways of doing things to help you avoid problems:
4747
| [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: |
4848
| [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
4949
| [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | |
50+
| [svelte/require-stores-init](./rules/require-stores-init.md) | require initial value in store | |
5051

5152
## Stylistic Issues
5253

docs/rules/require-stores-init.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "svelte/require-stores-init"
5+
description: "require initial value in store"
6+
---
7+
8+
# svelte/require-stores-init
9+
10+
> require initial value in store
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 is aimed to enforce initial values when initializing the Svelte stores.
17+
18+
<ESLintCodeBlock language="javascript">
19+
20+
<!--eslint-skip-->
21+
22+
```js
23+
/* eslint svelte/require-stores-init: "error" */
24+
25+
import { writable, readable, derived } from "svelte/store"
26+
27+
/* ✓ GOOD */
28+
export const w1 = writable(false)
29+
export const r1 = readable({})
30+
export const d1 = derived([a, b], () => {}, false)
31+
32+
/* ✗ BAD */
33+
export const w2 = writable()
34+
export const r2 = readable()
35+
export const d2 = derived([a, b], () => {})
36+
```
37+
38+
</ESLintCodeBlock>
39+
40+
## :wrench: Options
41+
42+
Nothing.
43+
44+
## :heart: Compatibility
45+
46+
This rule was taken from [@tivac/eslint-plugin-svelte].
47+
This rule is compatible with `@tivac/svelte/stores-initial-value` rule.
48+
49+
[@tivac/eslint-plugin-svelte]: https://github.com/tivac/eslint-plugin-svelte/
50+
51+
## :mag: Implementation
52+
53+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/require-stores-init.ts)
54+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/require-stores-init.ts)

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"@types/escape-html": "^1.0.2",
8484
"@types/eslint": "^8.0.0",
8585
"@types/eslint-scope": "^3.7.0",
86+
"@types/eslint-utils": "^3.0.1",
8687
"@types/eslint-visitor-keys": "^1.0.0",
8788
"@types/estree": "^1.0.0",
8889
"@types/less": "^3.0.3",

src/rules/require-stores-init.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { createRule } from "../utils"
2+
import type * as ESTree from "estree"
3+
import { ReferenceTracker } from "eslint-utils"
4+
5+
export default createRule("require-stores-init", {
6+
meta: {
7+
docs: {
8+
description: "require initial value in store",
9+
category: "Best Practices",
10+
recommended: false,
11+
},
12+
schema: [],
13+
messages: {
14+
storeDefaultValue: `Always set a default value for svelte stores.`,
15+
},
16+
type: "suggestion",
17+
},
18+
create(context) {
19+
/** Extract 'svelte/store' references */
20+
function* extractStoreReferences() {
21+
const referenceTracker = new ReferenceTracker(context.getScope())
22+
for (const { node, path } of referenceTracker.iterateEsmReferences({
23+
"svelte/store": {
24+
[ReferenceTracker.ESM]: true,
25+
writable: {
26+
[ReferenceTracker.CALL]: true,
27+
},
28+
readable: {
29+
[ReferenceTracker.CALL]: true,
30+
},
31+
derived: {
32+
[ReferenceTracker.CALL]: true,
33+
},
34+
},
35+
})) {
36+
yield {
37+
node: node as ESTree.CallExpression,
38+
name: path[path.length - 1],
39+
}
40+
}
41+
}
42+
43+
return {
44+
Program() {
45+
for (const { node, name } of extractStoreReferences()) {
46+
const minArgs =
47+
name === "writable" || name === "readable"
48+
? 1
49+
: name === "derived"
50+
? 3
51+
: 0
52+
53+
if (
54+
node.arguments.length >= minArgs ||
55+
node.arguments.some((arg) => arg.type === "SpreadElement")
56+
) {
57+
continue
58+
}
59+
context.report({
60+
node,
61+
messageId: "storeDefaultValue",
62+
})
63+
}
64+
},
65+
}
66+
},
67+
})

src/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import noUselessMustaches from "../rules/no-useless-mustaches"
2828
import preferClassDirective from "../rules/prefer-class-directive"
2929
import preferStyleDirective from "../rules/prefer-style-directive"
3030
import requireOptimizedStyleAttribute from "../rules/require-optimized-style-attribute"
31+
import requireStoresInit from "../rules/require-stores-init"
3132
import shorthandAttribute from "../rules/shorthand-attribute"
3233
import shorthandDirective from "../rules/shorthand-directive"
3334
import sortAttributes from "../rules/sort-attributes"
@@ -65,6 +66,7 @@ export const rules = [
6566
preferClassDirective,
6667
preferStyleDirective,
6768
requireOptimizedStyleAttribute,
69+
requireStoresInit,
6870
shorthandAttribute,
6971
shorthandDirective,
7072
sortAttributes,

tests/fixtures/rules/.eslintrc.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
"use strict"
2-
31
module.exports = {
2+
parserOptions: {
3+
sourceType: "module",
4+
},
45
overrides: [
56
{
67
files: ["*output.svelte"],
@@ -17,5 +18,6 @@ module.exports = {
1718
"no-empty-function": "off",
1819
"one-var": "off",
1920
"func-style": "off",
21+
"node/no-unsupported-features/es-syntax": "off",
2022
},
2123
}

tests/fixtures/rules/indent/invalid/.eslintrc.js

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
"use strict"
2-
31
module.exports = {
42
rules: {
53
"no-sparse-arrays": "off",

tests/fixtures/rules/no-unused-svelte-ignore/valid/.eslintrc.js

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
"use strict"
2-
31
module.exports = {
42
overrides: [
53
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
- message: Always set a default value for svelte stores.
2+
line: 2
3+
column: 18
4+
suggestions: null
5+
- message: Always set a default value for svelte stores.
6+
line: 3
7+
column: 18
8+
suggestions: null
9+
- message: Always set a default value for svelte stores.
10+
line: 4
11+
column: 18
12+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { writable, readable, derived } from "svelte/store"
2+
export const w = writable()
3+
export const r = readable()
4+
export const d = derived([a, b], () => {})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
- message: Always set a default value for svelte stores.
2+
line: 3
3+
column: 13
4+
suggestions: null
5+
- message: Always set a default value for svelte stores.
6+
line: 4
7+
column: 13
8+
suggestions: null
9+
- message: Always set a default value for svelte stores.
10+
line: 5
11+
column: 13
12+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
import { writable, readable, derived } from "svelte/store"
3+
const w = writable()
4+
const r = readable()
5+
const d = derived([a, b], () => {})
6+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { writable, readable, derived } from "svelte/store"
2+
export const w = writable(false)
3+
export const r = readable({})
4+
export const d = derived([a, b], () => {}, false)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
import { writable, readable, derived } from "svelte/store"
3+
const w = writable(false)
4+
const r = readable({})
5+
const d = derived([a, b], () => {}, false)
6+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
import { writable, readable, derived } from "./unknown"
3+
const w = writable()
4+
const r = readable()
5+
const d = derived([a, b], () => {})
6+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
import { derived } from "svelte/store"
3+
const args = [[a, b], () => {}, false]
4+
const d = derived(...args)
5+
</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/require-stores-init"
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+
"require-stores-init",
14+
rule as any,
15+
loadTestCases("require-stores-init"),
16+
)

tests/utils/utils.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,17 @@ function writeFixtures(
184184

185185
const config = getConfig(ruleName, inputFile)
186186

187+
const parser =
188+
path.extname(inputFile) === ".svelte"
189+
? require.resolve("svelte-eslint-parser")
190+
: undefined
187191
const result = linter.verify(
188192
config.code,
189193
{
190194
rules: {
191195
[ruleName]: ["error", ...(config.options || [])],
192196
},
193-
parser: "svelte-eslint-parser",
197+
parser,
194198
parserOptions: {
195199
ecmaVersion: 2020,
196200
sourceType: "module",

typings/eslint-utils/index.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { AST } from "svelte-eslint-parser"
22
import type { Scope } from "eslint"
33
import type * as ESTree from "estree"
4+
export {
5+
ReferenceTracker,
6+
TrackedReferences,
7+
} from "../../node_modules/@types/eslint-utils"
48
type Token = { type: string; value: string }
59
export function isArrowToken(token: Token): boolean
610
export function isCommaToken(token: Token): boolean

0 commit comments

Comments
 (0)