Skip to content

feat: implement stores-no-async #225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curvy-ants-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-svelte": minor
---

Add svelte/stores-no-async rule
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
| [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: |
| [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: |
| [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: |
| [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: |
| [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: |
| [svelte/valid-compile](https://ota-meshi.github.io/eslint-plugin-svelte/rules/valid-compile/) | disallow warnings when compiling. | :star: |

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

Expand Down
56 changes: 56 additions & 0 deletions docs/rules/no-store-async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
pageClass: "rule-details"
sidebarDepth: 0
title: "svelte/no-store-async"
description: "disallow using async/await inside svelte stores"
---

# svelte/no-store-async

> disallow using async/await inside svelte stores

- :gear: This rule is included in `"plugin:svelte/recommended"`.

## :book: Rule Details

This rule reports all uses of async/await inside svelte stores.
Because it causes issues with the auto-unsubscribing features.

<ESLintCodeBlock language="javascript">

<!--eslint-skip-->

```js
/* eslint svelte/no-store-async: "error" */

import { writable, readable, derived } from "svelte/store"

/* ✓ GOOD */
const w1 = writable(false, () => {})
const r1 = readable(false, () => {})
const d1 = derived(a1, ($a1) => {})

/* ✗ BAD */
const w2 = writable(false, async () => {})
const r2 = readable(false, async () => {})
const d2 = derived(a1, async ($a1) => {})
```

</ESLintCodeBlock>

## :wrench: Options

Nothing.

## :books: Further Reading

- [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)

## :rocket: Version

This rule was introduced in eslint-plugin-svelte v3.1.0

## :mag: Implementation

- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-store-async.ts)
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-store-async.ts)
49 changes: 49 additions & 0 deletions src/rules/no-store-async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createRule } from "../utils"
import { extractStoreReferences } from "./reference-helpers/svelte-store"

export default createRule("no-store-async", {
meta: {
docs: {
description:
"disallow using async/await inside svelte stores because it causes issues with the auto-unsubscribing features",
category: "Possible Errors",
recommended: true,
default: "error",
},
schema: [],
messages: {
unexpected: "Do not pass async functions to svelte stores.",
},
type: "problem",
},
create(context) {
return {
Program() {
for (const { node } of extractStoreReferences(context)) {
const [, fn] = node.arguments
if (
!fn ||
(fn.type !== "ArrowFunctionExpression" &&
fn.type !== "FunctionExpression") ||
!fn.async
) {
continue
}

const start = fn.loc?.start ?? { line: 1, column: 0 }
context.report({
node: fn,
loc: {
start,
end: {
line: start.line,
column: start.column + 5,
},
},
messageId: "unexpected",
})
}
},
}
},
})
29 changes: 29 additions & 0 deletions src/rules/reference-helpers/svelte-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type * as ESTree from "estree"
import { ReferenceTracker } from "eslint-utils"
import type { RuleContext } from "../../types"

/** Extract 'svelte/store' references */
export function* extractStoreReferences(
context: RuleContext,
): Generator<{ node: ESTree.CallExpression; name: string }, void> {
const referenceTracker = new ReferenceTracker(context.getScope())
for (const { node, path } of referenceTracker.iterateEsmReferences({
"svelte/store": {
[ReferenceTracker.ESM]: true,
writable: {
[ReferenceTracker.CALL]: true,
},
readable: {
[ReferenceTracker.CALL]: true,
},
derived: {
[ReferenceTracker.CALL]: true,
},
},
})) {
yield {
node: node as ESTree.CallExpression,
name: path[path.length - 1],
}
}
}
29 changes: 2 additions & 27 deletions src/rules/require-stores-init.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createRule } from "../utils"
import type * as ESTree from "estree"
import { ReferenceTracker } from "eslint-utils"
import { extractStoreReferences } from "./reference-helpers/svelte-store"

export default createRule("require-stores-init", {
meta: {
Expand All @@ -16,33 +15,9 @@ export default createRule("require-stores-init", {
type: "suggestion",
},
create(context) {
/** Extract 'svelte/store' references */
function* extractStoreReferences() {
const referenceTracker = new ReferenceTracker(context.getScope())
for (const { node, path } of referenceTracker.iterateEsmReferences({
"svelte/store": {
[ReferenceTracker.ESM]: true,
writable: {
[ReferenceTracker.CALL]: true,
},
readable: {
[ReferenceTracker.CALL]: true,
},
derived: {
[ReferenceTracker.CALL]: true,
},
},
})) {
yield {
node: node as ESTree.CallExpression,
name: path[path.length - 1],
}
}
}

return {
Program() {
for (const { node, name } of extractStoreReferences()) {
for (const { node, name } of extractStoreReferences(context)) {
const minArgs =
name === "writable" || name === "readable"
? 1
Expand Down
2 changes: 2 additions & 0 deletions src/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import noReactiveFunctions from "../rules/no-reactive-functions"
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 noStoreAsync from "../rules/no-store-async"
import noTargetBlank from "../rules/no-target-blank"
import noUnknownStyleDirectiveProperty from "../rules/no-unknown-style-directive-property"
import noUnusedSvelteIgnore from "../rules/no-unused-svelte-ignore"
Expand Down Expand Up @@ -59,6 +60,7 @@ export const rules = [
noReactiveLiterals,
noShorthandStylePropertyOverrides,
noSpacesAroundEqualSignsInAttribute,
noStoreAsync,
noTargetBlank,
noUnknownStyleDirectiveProperty,
noUnusedSvelteIgnore,
Expand Down
12 changes: 12 additions & 0 deletions tests/fixtures/rules/no-store-async/invalid/test01-errors.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- message: Do not pass async functions to svelte stores.
line: 3
column: 28
suggestions: null
- message: Do not pass async functions to svelte stores.
line: 6
column: 28
suggestions: null
- message: Do not pass async functions to svelte stores.
line: 9
column: 24
suggestions: null
11 changes: 11 additions & 0 deletions tests/fixtures/rules/no-store-async/invalid/test01-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { writable, readable, derived } from "svelte/store"

const w2 = writable(false, async () => {
/** do nothing */
})
const r2 = readable(false, async () => {
/** do nothing */
})
const d2 = derived(a1, async ($a1) => {
/** do nothing */
})
12 changes: 12 additions & 0 deletions tests/fixtures/rules/no-store-async/invalid/test02-errors.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- message: Do not pass async functions to svelte stores.
line: 3
column: 35
suggestions: null
- message: Do not pass async functions to svelte stores.
line: 6
column: 35
suggestions: null
- message: Do not pass async functions to svelte stores.
line: 9
column: 31
suggestions: null
11 changes: 11 additions & 0 deletions tests/fixtures/rules/no-store-async/invalid/test02-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as stores from "svelte/store"

const w2 = stores.writable(false, async () => {
/** do nothing */
})
const r2 = stores.readable(false, async () => {
/** do nothing */
})
const d2 = stores.derived(a1, async ($a1) => {
/** do nothing */
})
12 changes: 12 additions & 0 deletions tests/fixtures/rules/no-store-async/invalid/test03-errors.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- message: Do not pass async functions to svelte stores.
line: 3
column: 21
suggestions: null
- message: Do not pass async functions to svelte stores.
line: 6
column: 21
suggestions: null
- message: Do not pass async functions to svelte stores.
line: 9
column: 18
suggestions: null
11 changes: 11 additions & 0 deletions tests/fixtures/rules/no-store-async/invalid/test03-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { writable as A, readable as B, derived as C } from "svelte/store"

const w2 = A(false, async () => {
/** do nothing */
})
const r2 = B(false, async () => {
/** do nothing */
})
const d2 = C(a1, async ($a1) => {
/** do nothing */
})
12 changes: 12 additions & 0 deletions tests/fixtures/rules/no-store-async/invalid/test04-errors.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- message: Do not pass async functions to svelte stores.
line: 3
column: 28
suggestions: null
- message: Do not pass async functions to svelte stores.
line: 6
column: 28
suggestions: null
- message: Do not pass async functions to svelte stores.
line: 9
column: 24
suggestions: null
11 changes: 11 additions & 0 deletions tests/fixtures/rules/no-store-async/invalid/test04-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { writable, readable, derived } from "svelte/store"

const w2 = writable(false, async function () {
/** do nothing */
})
const r2 = readable(false, async function () {
/** do nothing */
})
const d2 = derived(a1, async function ($a1) {
/** do nothing */
})
14 changes: 14 additions & 0 deletions tests/fixtures/rules/no-store-async/valid/test01-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { writable, readable, derived } from "svelte/store"

const w1 = writable(false, () => {
/** do nothing */
})
const w2 = writable(false)
const r1 = readable(false, () => {
/** do nothing */
})
const r2 = readable(false)
const d1 = derived(a1, ($a1) => {
/** do nothing */
})
const d2 = derived(a1)
12 changes: 12 additions & 0 deletions tests/src/rules/no-store-async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { RuleTester } from "eslint"
import rule from "../../../src/rules/no-store-async"
import { loadTestCases } from "../../utils/utils"

const tester = new RuleTester({
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
},
})

tester.run("no-store-async", rule as any, loadTestCases("no-store-async"))