Skip to content

Commit

Permalink
feat: implement stores-no-async (#225)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
baseballyama committed Aug 23, 2022
1 parent dcb5f48 commit a3888b3
Show file tree
Hide file tree
Showing 18 changed files with 263 additions and 27 deletions.
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"))

0 comments on commit a3888b3

Please sign in to comment.