Skip to content

Commit a3888b3

Browse files
authored
feat: implement stores-no-async (#225)
* feat: implement stores-no-async rule * test: add tests * docs: add docs * chore: add changeset * fix: remove since * fix: changeset * fix: use ReferenceTracker * fix: handled if fn is undefined * fix: handle FunctionExpression also
1 parent dcb5f48 commit a3888b3

18 files changed

+263
-27
lines changed

.changeset/curvy-ants-admire.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-svelte": minor
3+
---
4+
5+
Add svelte/stores-no-async rule

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
269269
| [svelte/no-not-function-handler](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-not-function-handler/) | disallow use of not function in event handler | :star: |
270270
| [svelte/no-object-in-text-mustaches](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-object-in-text-mustaches/) | disallow objects in text mustache interpolation | :star: |
271271
| [svelte/no-shorthand-style-property-overrides](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-shorthand-style-property-overrides/) | disallow shorthand style properties that override related longhand properties | :star: |
272+
| [svelte/no-store-async](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-store-async.md) | disallow using async/await inside svelte stores | :star: |
272273
| [svelte/no-unknown-style-directive-property](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-unknown-style-directive-property/) | disallow unknown `style:property` | :star: |
273274
| [svelte/valid-compile](https://ota-meshi.github.io/eslint-plugin-svelte/rules/valid-compile/) | disallow warnings when compiling. | :star: |
274275

docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
2222
| [svelte/no-not-function-handler](./rules/no-not-function-handler.md) | disallow use of not function in event handler | :star: |
2323
| [svelte/no-object-in-text-mustaches](./rules/no-object-in-text-mustaches.md) | disallow objects in text mustache interpolation | :star: |
2424
| [svelte/no-shorthand-style-property-overrides](./rules/no-shorthand-style-property-overrides.md) | disallow shorthand style properties that override related longhand properties | :star: |
25+
| [svelte/no-store-async](./rules/no-store-async.md) | disallow using async/await inside svelte stores | :star: |
2526
| [svelte/no-unknown-style-directive-property](./rules/no-unknown-style-directive-property.md) | disallow unknown `style:property` | :star: |
2627
| [svelte/valid-compile](./rules/valid-compile.md) | disallow warnings when compiling. | :star: |
2728

docs/rules/no-store-async.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "svelte/no-store-async"
5+
description: "disallow using async/await inside svelte stores"
6+
---
7+
8+
# svelte/no-store-async
9+
10+
> disallow using async/await inside svelte stores
11+
12+
- :gear: This rule is included in `"plugin:svelte/recommended"`.
13+
14+
## :book: Rule Details
15+
16+
This rule reports all uses of async/await inside svelte stores.
17+
Because it causes issues with the auto-unsubscribing features.
18+
19+
<ESLintCodeBlock language="javascript">
20+
21+
<!--eslint-skip-->
22+
23+
```js
24+
/* eslint svelte/no-store-async: "error" */
25+
26+
import { writable, readable, derived } from "svelte/store"
27+
28+
/* ✓ GOOD */
29+
const w1 = writable(false, () => {})
30+
const r1 = readable(false, () => {})
31+
const d1 = derived(a1, ($a1) => {})
32+
33+
/* ✗ BAD */
34+
const w2 = writable(false, async () => {})
35+
const r2 = readable(false, async () => {})
36+
const d2 = derived(a1, async ($a1) => {})
37+
```
38+
39+
</ESLintCodeBlock>
40+
41+
## :wrench: Options
42+
43+
Nothing.
44+
45+
## :books: Further Reading
46+
47+
- [Svelte - Docs > 4. Prefix stores with $ to access their values / Store contract](https://svelte.dev/docs#component-format-script-4-prefix-stores-with-$-to-access-their-values-store-contract)
48+
49+
## :rocket: Version
50+
51+
This rule was introduced in eslint-plugin-svelte v3.1.0
52+
53+
## :mag: Implementation
54+
55+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-store-async.ts)
56+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-store-async.ts)

src/rules/no-store-async.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { createRule } from "../utils"
2+
import { extractStoreReferences } from "./reference-helpers/svelte-store"
3+
4+
export default createRule("no-store-async", {
5+
meta: {
6+
docs: {
7+
description:
8+
"disallow using async/await inside svelte stores because it causes issues with the auto-unsubscribing features",
9+
category: "Possible Errors",
10+
recommended: true,
11+
default: "error",
12+
},
13+
schema: [],
14+
messages: {
15+
unexpected: "Do not pass async functions to svelte stores.",
16+
},
17+
type: "problem",
18+
},
19+
create(context) {
20+
return {
21+
Program() {
22+
for (const { node } of extractStoreReferences(context)) {
23+
const [, fn] = node.arguments
24+
if (
25+
!fn ||
26+
(fn.type !== "ArrowFunctionExpression" &&
27+
fn.type !== "FunctionExpression") ||
28+
!fn.async
29+
) {
30+
continue
31+
}
32+
33+
const start = fn.loc?.start ?? { line: 1, column: 0 }
34+
context.report({
35+
node: fn,
36+
loc: {
37+
start,
38+
end: {
39+
line: start.line,
40+
column: start.column + 5,
41+
},
42+
},
43+
messageId: "unexpected",
44+
})
45+
}
46+
},
47+
}
48+
},
49+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type * as ESTree from "estree"
2+
import { ReferenceTracker } from "eslint-utils"
3+
import type { RuleContext } from "../../types"
4+
5+
/** Extract 'svelte/store' references */
6+
export function* extractStoreReferences(
7+
context: RuleContext,
8+
): Generator<{ node: ESTree.CallExpression; name: string }, void> {
9+
const referenceTracker = new ReferenceTracker(context.getScope())
10+
for (const { node, path } of referenceTracker.iterateEsmReferences({
11+
"svelte/store": {
12+
[ReferenceTracker.ESM]: true,
13+
writable: {
14+
[ReferenceTracker.CALL]: true,
15+
},
16+
readable: {
17+
[ReferenceTracker.CALL]: true,
18+
},
19+
derived: {
20+
[ReferenceTracker.CALL]: true,
21+
},
22+
},
23+
})) {
24+
yield {
25+
node: node as ESTree.CallExpression,
26+
name: path[path.length - 1],
27+
}
28+
}
29+
}

src/rules/require-stores-init.ts

+2-27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createRule } from "../utils"
2-
import type * as ESTree from "estree"
3-
import { ReferenceTracker } from "eslint-utils"
2+
import { extractStoreReferences } from "./reference-helpers/svelte-store"
43

54
export default createRule("require-stores-init", {
65
meta: {
@@ -16,33 +15,9 @@ export default createRule("require-stores-init", {
1615
type: "suggestion",
1716
},
1817
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-
4318
return {
4419
Program() {
45-
for (const { node, name } of extractStoreReferences()) {
20+
for (const { node, name } of extractStoreReferences(context)) {
4621
const minArgs =
4722
name === "writable" || name === "readable"
4823
? 1

src/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import noReactiveFunctions from "../rules/no-reactive-functions"
2121
import noReactiveLiterals from "../rules/no-reactive-literals"
2222
import noShorthandStylePropertyOverrides from "../rules/no-shorthand-style-property-overrides"
2323
import noSpacesAroundEqualSignsInAttribute from "../rules/no-spaces-around-equal-signs-in-attribute"
24+
import noStoreAsync from "../rules/no-store-async"
2425
import noTargetBlank from "../rules/no-target-blank"
2526
import noUnknownStyleDirectiveProperty from "../rules/no-unknown-style-directive-property"
2627
import noUnusedSvelteIgnore from "../rules/no-unused-svelte-ignore"
@@ -59,6 +60,7 @@ export const rules = [
5960
noReactiveLiterals,
6061
noShorthandStylePropertyOverrides,
6162
noSpacesAroundEqualSignsInAttribute,
63+
noStoreAsync,
6264
noTargetBlank,
6365
noUnknownStyleDirectiveProperty,
6466
noUnusedSvelteIgnore,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
- message: Do not pass async functions to svelte stores.
2+
line: 3
3+
column: 28
4+
suggestions: null
5+
- message: Do not pass async functions to svelte stores.
6+
line: 6
7+
column: 28
8+
suggestions: null
9+
- message: Do not pass async functions to svelte stores.
10+
line: 9
11+
column: 24
12+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { writable, readable, derived } from "svelte/store"
2+
3+
const w2 = writable(false, async () => {
4+
/** do nothing */
5+
})
6+
const r2 = readable(false, async () => {
7+
/** do nothing */
8+
})
9+
const d2 = derived(a1, async ($a1) => {
10+
/** do nothing */
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
- message: Do not pass async functions to svelte stores.
2+
line: 3
3+
column: 35
4+
suggestions: null
5+
- message: Do not pass async functions to svelte stores.
6+
line: 6
7+
column: 35
8+
suggestions: null
9+
- message: Do not pass async functions to svelte stores.
10+
line: 9
11+
column: 31
12+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as stores from "svelte/store"
2+
3+
const w2 = stores.writable(false, async () => {
4+
/** do nothing */
5+
})
6+
const r2 = stores.readable(false, async () => {
7+
/** do nothing */
8+
})
9+
const d2 = stores.derived(a1, async ($a1) => {
10+
/** do nothing */
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
- message: Do not pass async functions to svelte stores.
2+
line: 3
3+
column: 21
4+
suggestions: null
5+
- message: Do not pass async functions to svelte stores.
6+
line: 6
7+
column: 21
8+
suggestions: null
9+
- message: Do not pass async functions to svelte stores.
10+
line: 9
11+
column: 18
12+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { writable as A, readable as B, derived as C } from "svelte/store"
2+
3+
const w2 = A(false, async () => {
4+
/** do nothing */
5+
})
6+
const r2 = B(false, async () => {
7+
/** do nothing */
8+
})
9+
const d2 = C(a1, async ($a1) => {
10+
/** do nothing */
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
- message: Do not pass async functions to svelte stores.
2+
line: 3
3+
column: 28
4+
suggestions: null
5+
- message: Do not pass async functions to svelte stores.
6+
line: 6
7+
column: 28
8+
suggestions: null
9+
- message: Do not pass async functions to svelte stores.
10+
line: 9
11+
column: 24
12+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { writable, readable, derived } from "svelte/store"
2+
3+
const w2 = writable(false, async function () {
4+
/** do nothing */
5+
})
6+
const r2 = readable(false, async function () {
7+
/** do nothing */
8+
})
9+
const d2 = derived(a1, async function ($a1) {
10+
/** do nothing */
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { writable, readable, derived } from "svelte/store"
2+
3+
const w1 = writable(false, () => {
4+
/** do nothing */
5+
})
6+
const w2 = writable(false)
7+
const r1 = readable(false, () => {
8+
/** do nothing */
9+
})
10+
const r2 = readable(false)
11+
const d1 = derived(a1, ($a1) => {
12+
/** do nothing */
13+
})
14+
const d2 = derived(a1)

tests/src/rules/no-store-async.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../src/rules/no-store-async"
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("no-store-async", rule as any, loadTestCases("no-store-async"))

0 commit comments

Comments
 (0)