diff --git a/.changeset/curvy-ants-admire.md b/.changeset/curvy-ants-admire.md new file mode 100644 index 000000000..db5aec1ec --- /dev/null +++ b/.changeset/curvy-ants-admire.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-svelte": minor +--- + +Add svelte/stores-no-async rule diff --git a/README.md b/README.md index 596f71ac1..b73f81a0a 100644 --- a/README.md +++ b/README.md @@ -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: | diff --git a/docs/rules.md b/docs/rules.md index 223d78dca..cec9eb373 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -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: | diff --git a/docs/rules/no-store-async.md b/docs/rules/no-store-async.md new file mode 100644 index 000000000..29dd29c64 --- /dev/null +++ b/docs/rules/no-store-async.md @@ -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. + + + + + +```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) => {}) +``` + + + +## :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) diff --git a/src/rules/no-store-async.ts b/src/rules/no-store-async.ts new file mode 100644 index 000000000..d805f1854 --- /dev/null +++ b/src/rules/no-store-async.ts @@ -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", + }) + } + }, + } + }, +}) diff --git a/src/rules/reference-helpers/svelte-store.ts b/src/rules/reference-helpers/svelte-store.ts new file mode 100644 index 000000000..3e9edc975 --- /dev/null +++ b/src/rules/reference-helpers/svelte-store.ts @@ -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], + } + } +} diff --git a/src/rules/require-stores-init.ts b/src/rules/require-stores-init.ts index 5b2ac846c..8245ef5ca 100644 --- a/src/rules/require-stores-init.ts +++ b/src/rules/require-stores-init.ts @@ -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: { @@ -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 diff --git a/src/utils/rules.ts b/src/utils/rules.ts index e5776af24..06382d328 100644 --- a/src/utils/rules.ts +++ b/src/utils/rules.ts @@ -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" @@ -59,6 +60,7 @@ export const rules = [ noReactiveLiterals, noShorthandStylePropertyOverrides, noSpacesAroundEqualSignsInAttribute, + noStoreAsync, noTargetBlank, noUnknownStyleDirectiveProperty, noUnusedSvelteIgnore, diff --git a/tests/fixtures/rules/no-store-async/invalid/test01-errors.yaml b/tests/fixtures/rules/no-store-async/invalid/test01-errors.yaml new file mode 100644 index 000000000..932632015 --- /dev/null +++ b/tests/fixtures/rules/no-store-async/invalid/test01-errors.yaml @@ -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 diff --git a/tests/fixtures/rules/no-store-async/invalid/test01-input.js b/tests/fixtures/rules/no-store-async/invalid/test01-input.js new file mode 100644 index 000000000..3ad801903 --- /dev/null +++ b/tests/fixtures/rules/no-store-async/invalid/test01-input.js @@ -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 */ +}) diff --git a/tests/fixtures/rules/no-store-async/invalid/test02-errors.yaml b/tests/fixtures/rules/no-store-async/invalid/test02-errors.yaml new file mode 100644 index 000000000..b65aa407e --- /dev/null +++ b/tests/fixtures/rules/no-store-async/invalid/test02-errors.yaml @@ -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 diff --git a/tests/fixtures/rules/no-store-async/invalid/test02-input.js b/tests/fixtures/rules/no-store-async/invalid/test02-input.js new file mode 100644 index 000000000..a327fa985 --- /dev/null +++ b/tests/fixtures/rules/no-store-async/invalid/test02-input.js @@ -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 */ +}) diff --git a/tests/fixtures/rules/no-store-async/invalid/test03-errors.yaml b/tests/fixtures/rules/no-store-async/invalid/test03-errors.yaml new file mode 100644 index 000000000..3c3ed27e9 --- /dev/null +++ b/tests/fixtures/rules/no-store-async/invalid/test03-errors.yaml @@ -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 diff --git a/tests/fixtures/rules/no-store-async/invalid/test03-input.js b/tests/fixtures/rules/no-store-async/invalid/test03-input.js new file mode 100644 index 000000000..394382d9c --- /dev/null +++ b/tests/fixtures/rules/no-store-async/invalid/test03-input.js @@ -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 */ +}) diff --git a/tests/fixtures/rules/no-store-async/invalid/test04-errors.yaml b/tests/fixtures/rules/no-store-async/invalid/test04-errors.yaml new file mode 100644 index 000000000..932632015 --- /dev/null +++ b/tests/fixtures/rules/no-store-async/invalid/test04-errors.yaml @@ -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 diff --git a/tests/fixtures/rules/no-store-async/invalid/test04-input.js b/tests/fixtures/rules/no-store-async/invalid/test04-input.js new file mode 100644 index 000000000..f6ca81718 --- /dev/null +++ b/tests/fixtures/rules/no-store-async/invalid/test04-input.js @@ -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 */ +}) diff --git a/tests/fixtures/rules/no-store-async/valid/test01-input.js b/tests/fixtures/rules/no-store-async/valid/test01-input.js new file mode 100644 index 000000000..f6e2c1aa6 --- /dev/null +++ b/tests/fixtures/rules/no-store-async/valid/test01-input.js @@ -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) diff --git a/tests/src/rules/no-store-async.ts b/tests/src/rules/no-store-async.ts new file mode 100644 index 000000000..90d4bcbca --- /dev/null +++ b/tests/src/rules/no-store-async.ts @@ -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"))