From 835e036490d712162a1acbc5b732ba4e704b5426 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Fri, 5 Aug 2022 20:48:27 +0900 Subject: [PATCH 1/5] Add `svelte/require-stores-init` rule --- README.md | 1 + .../src/lib/components/ESLintCodeBlock.svelte | 8 ++- .../src/lib/eslint/ESLintEditor.svelte | 3 +- docs/rules.md | 1 + docs/rules/require-stores-init.md | 47 +++++++++++++ package.json | 1 + src/rules/require-stores-init.ts | 67 +++++++++++++++++++ src/utils/rules.ts | 2 + tests/fixtures/rules/.eslintrc.js | 6 +- .../invalid/no-init-in-js01-errors.yaml | 12 ++++ .../invalid/no-init-in-js01-input.js | 4 ++ .../invalid/no-init01-errors.yaml | 12 ++++ .../invalid/no-init01-input.svelte | 6 ++ .../valid/has-init-in-js01-input.js | 4 ++ .../valid/has-init01-input.svelte | 6 ++ .../valid/no-svelte-store01-input.svelte | 6 ++ .../valid/spread01-input.svelte | 5 ++ tests/src/rules/require-stores-init.ts | 16 +++++ tests/utils/utils.ts | 8 ++- typings/eslint-utils/index.d.ts | 4 ++ 20 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 docs/rules/require-stores-init.md create mode 100644 src/rules/require-stores-init.ts create mode 100644 tests/fixtures/rules/require-stores-init/invalid/no-init-in-js01-errors.yaml create mode 100644 tests/fixtures/rules/require-stores-init/invalid/no-init-in-js01-input.js create mode 100644 tests/fixtures/rules/require-stores-init/invalid/no-init01-errors.yaml create mode 100644 tests/fixtures/rules/require-stores-init/invalid/no-init01-input.svelte create mode 100644 tests/fixtures/rules/require-stores-init/valid/has-init-in-js01-input.js create mode 100644 tests/fixtures/rules/require-stores-init/valid/has-init01-input.svelte create mode 100644 tests/fixtures/rules/require-stores-init/valid/no-svelte-store01-input.svelte create mode 100644 tests/fixtures/rules/require-stores-init/valid/spread01-input.svelte create mode 100644 tests/src/rules/require-stores-init.ts diff --git a/README.md b/README.md index dabe16baf..2927f6793 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,7 @@ These rules relate to better ways of doing things to help you avoid problems: | [svelte/no-unused-svelte-ignore](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: | | [svelte/no-useless-mustaches](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: | | [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 | | +| [svelte/require-stores-init](https://ota-meshi.github.io/eslint-plugin-svelte/rules/require-stores-init/) | require initial value in store | | ## Stylistic Issues diff --git a/docs-svelte-kit/src/lib/components/ESLintCodeBlock.svelte b/docs-svelte-kit/src/lib/components/ESLintCodeBlock.svelte index 4f100e414..1ce361ae8 100644 --- a/docs-svelte-kit/src/lib/components/ESLintCodeBlock.svelte +++ b/docs-svelte-kit/src/lib/components/ESLintCodeBlock.svelte @@ -12,9 +12,10 @@ let code = "" export let rules = {} export let fix = false + export let language = "html" let time = "" - let options = { - filename: "example.svelte", + $: options = { + filename: language === "html" ? "example.svelte" : "example.js", preprocess, postprocess, } @@ -43,7 +44,7 @@ {linter} bind:code config={{ - parser: "svelte-eslint-parser", + parser: language === "html" ? "svelte-eslint-parser" : undefined, parserOptions: { ecmaVersion: 2020, sourceType: "module", @@ -54,6 +55,7 @@ es2021: true, }, }} + {language} {options} on:result={onLintedResult} showDiff={showDiff && fix} diff --git a/docs-svelte-kit/src/lib/eslint/ESLintEditor.svelte b/docs-svelte-kit/src/lib/eslint/ESLintEditor.svelte index 02644ef2c..cb047c701 100644 --- a/docs-svelte-kit/src/lib/eslint/ESLintEditor.svelte +++ b/docs-svelte-kit/src/lib/eslint/ESLintEditor.svelte @@ -11,6 +11,7 @@ export let options = {} export let fix = true export let showDiff = true + export let language = "html" let fixedValue = code let leftMarkers = [] @@ -221,7 +222,7 @@ bind:this={editor} bind:code bind:rightCode={fixedValue} - language="html" + {language} diffEditor={fix && showDiff} markers={leftMarkers} {rightMarkers} diff --git a/docs/rules.md b/docs/rules.md index 01451b026..040d07895 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -47,6 +47,7 @@ These rules relate to better ways of doing things to help you avoid problems: | [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md) | disallow unused svelte-ignore comments | :star: | | [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: | | [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | | +| [svelte/require-stores-init](./rules/require-stores-init.md) | require initial value in store | | ## Stylistic Issues diff --git a/docs/rules/require-stores-init.md b/docs/rules/require-stores-init.md new file mode 100644 index 000000000..97c849e38 --- /dev/null +++ b/docs/rules/require-stores-init.md @@ -0,0 +1,47 @@ +--- +pageClass: "rule-details" +sidebarDepth: 0 +title: "svelte/require-stores-init" +description: "require initial value in store" +--- + +# svelte/require-stores-init + +> require initial value in store + +- :exclamation: **_This rule has not been released yet._** + +## :book: Rule Details + +This rule is aimed to enforce initial values when initializing the Svelte stores. + + + + + +```js +/* eslint svelte/require-stores-init: "error" */ + +import { writable, readable, derived } from "svelte/store" + +/* ✓ GOOD */ +export const w1 = writable(false) +export const r1 = readable({}) +export const d1 = derived([a, b], () => {}, false) + +/* ✗ BAD */ +export const w2 = writable() +export const r2 = readable() +export const d2 = derived([a, b], () => {}) +``` + + + +## :wrench: Options + +Nothing. + +## :mag: Implementation + +- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/require-stores-init.ts) +- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/require-stores-init.ts) diff --git a/package.json b/package.json index 43387bf67..da52fa187 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "@types/escape-html": "^1.0.2", "@types/eslint": "^8.0.0", "@types/eslint-scope": "^3.7.0", + "@types/eslint-utils": "^3.0.1", "@types/eslint-visitor-keys": "^1.0.0", "@types/estree": "^1.0.0", "@types/less": "^3.0.3", diff --git a/src/rules/require-stores-init.ts b/src/rules/require-stores-init.ts new file mode 100644 index 000000000..5b2ac846c --- /dev/null +++ b/src/rules/require-stores-init.ts @@ -0,0 +1,67 @@ +import { createRule } from "../utils" +import type * as ESTree from "estree" +import { ReferenceTracker } from "eslint-utils" + +export default createRule("require-stores-init", { + meta: { + docs: { + description: "require initial value in store", + category: "Best Practices", + recommended: false, + }, + schema: [], + messages: { + storeDefaultValue: `Always set a default value for svelte stores.`, + }, + 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()) { + const minArgs = + name === "writable" || name === "readable" + ? 1 + : name === "derived" + ? 3 + : 0 + + if ( + node.arguments.length >= minArgs || + node.arguments.some((arg) => arg.type === "SpreadElement") + ) { + continue + } + context.report({ + node, + messageId: "storeDefaultValue", + }) + } + }, + } + }, +}) diff --git a/src/utils/rules.ts b/src/utils/rules.ts index f4e629813..e5776af24 100644 --- a/src/utils/rules.ts +++ b/src/utils/rules.ts @@ -28,6 +28,7 @@ import noUselessMustaches from "../rules/no-useless-mustaches" import preferClassDirective from "../rules/prefer-class-directive" import preferStyleDirective from "../rules/prefer-style-directive" import requireOptimizedStyleAttribute from "../rules/require-optimized-style-attribute" +import requireStoresInit from "../rules/require-stores-init" import shorthandAttribute from "../rules/shorthand-attribute" import shorthandDirective from "../rules/shorthand-directive" import sortAttributes from "../rules/sort-attributes" @@ -65,6 +66,7 @@ export const rules = [ preferClassDirective, preferStyleDirective, requireOptimizedStyleAttribute, + requireStoresInit, shorthandAttribute, shorthandDirective, sortAttributes, diff --git a/tests/fixtures/rules/.eslintrc.js b/tests/fixtures/rules/.eslintrc.js index c810e19f6..d4fc563ce 100644 --- a/tests/fixtures/rules/.eslintrc.js +++ b/tests/fixtures/rules/.eslintrc.js @@ -1,6 +1,7 @@ -"use strict" - module.exports = { + parserOptions: { + sourceType: "module", + }, overrides: [ { files: ["*output.svelte"], @@ -17,5 +18,6 @@ module.exports = { "no-empty-function": "off", "one-var": "off", "func-style": "off", + "node/no-unsupported-features/es-syntax": "off", }, } diff --git a/tests/fixtures/rules/require-stores-init/invalid/no-init-in-js01-errors.yaml b/tests/fixtures/rules/require-stores-init/invalid/no-init-in-js01-errors.yaml new file mode 100644 index 000000000..c0a036d13 --- /dev/null +++ b/tests/fixtures/rules/require-stores-init/invalid/no-init-in-js01-errors.yaml @@ -0,0 +1,12 @@ +- message: Always set a default value for svelte stores. + line: 2 + column: 18 + suggestions: null +- message: Always set a default value for svelte stores. + line: 3 + column: 18 + suggestions: null +- message: Always set a default value for svelte stores. + line: 4 + column: 18 + suggestions: null diff --git a/tests/fixtures/rules/require-stores-init/invalid/no-init-in-js01-input.js b/tests/fixtures/rules/require-stores-init/invalid/no-init-in-js01-input.js new file mode 100644 index 000000000..797262939 --- /dev/null +++ b/tests/fixtures/rules/require-stores-init/invalid/no-init-in-js01-input.js @@ -0,0 +1,4 @@ +import { writable, readable, derived } from "svelte/store" +export const w = writable() +export const r = readable() +export const d = derived([a, b], () => {}) diff --git a/tests/fixtures/rules/require-stores-init/invalid/no-init01-errors.yaml b/tests/fixtures/rules/require-stores-init/invalid/no-init01-errors.yaml new file mode 100644 index 000000000..e6f7ae5bc --- /dev/null +++ b/tests/fixtures/rules/require-stores-init/invalid/no-init01-errors.yaml @@ -0,0 +1,12 @@ +- message: Always set a default value for svelte stores. + line: 3 + column: 13 + suggestions: null +- message: Always set a default value for svelte stores. + line: 4 + column: 13 + suggestions: null +- message: Always set a default value for svelte stores. + line: 5 + column: 13 + suggestions: null diff --git a/tests/fixtures/rules/require-stores-init/invalid/no-init01-input.svelte b/tests/fixtures/rules/require-stores-init/invalid/no-init01-input.svelte new file mode 100644 index 000000000..3a6a2be45 --- /dev/null +++ b/tests/fixtures/rules/require-stores-init/invalid/no-init01-input.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/rules/require-stores-init/valid/has-init-in-js01-input.js b/tests/fixtures/rules/require-stores-init/valid/has-init-in-js01-input.js new file mode 100644 index 000000000..11a5a93c9 --- /dev/null +++ b/tests/fixtures/rules/require-stores-init/valid/has-init-in-js01-input.js @@ -0,0 +1,4 @@ +import { writable, readable, derived } from "svelte/store" +export const w = writable(false) +export const r = readable({}) +export const d = derived([a, b], () => {}, false) diff --git a/tests/fixtures/rules/require-stores-init/valid/has-init01-input.svelte b/tests/fixtures/rules/require-stores-init/valid/has-init01-input.svelte new file mode 100644 index 000000000..8b0770958 --- /dev/null +++ b/tests/fixtures/rules/require-stores-init/valid/has-init01-input.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/rules/require-stores-init/valid/no-svelte-store01-input.svelte b/tests/fixtures/rules/require-stores-init/valid/no-svelte-store01-input.svelte new file mode 100644 index 000000000..e14257b95 --- /dev/null +++ b/tests/fixtures/rules/require-stores-init/valid/no-svelte-store01-input.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/rules/require-stores-init/valid/spread01-input.svelte b/tests/fixtures/rules/require-stores-init/valid/spread01-input.svelte new file mode 100644 index 000000000..f51b331c6 --- /dev/null +++ b/tests/fixtures/rules/require-stores-init/valid/spread01-input.svelte @@ -0,0 +1,5 @@ + diff --git a/tests/src/rules/require-stores-init.ts b/tests/src/rules/require-stores-init.ts new file mode 100644 index 000000000..6dcbde52f --- /dev/null +++ b/tests/src/rules/require-stores-init.ts @@ -0,0 +1,16 @@ +import { RuleTester } from "eslint" +import rule from "../../../src/rules/require-stores-init" +import { loadTestCases } from "../../utils/utils" + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, +}) + +tester.run( + "require-stores-init", + rule as any, + loadTestCases("require-stores-init"), +) diff --git a/tests/utils/utils.ts b/tests/utils/utils.ts index 848d80580..f6572e74a 100644 --- a/tests/utils/utils.ts +++ b/tests/utils/utils.ts @@ -184,13 +184,17 @@ function writeFixtures( const config = getConfig(ruleName, inputFile) + const parser = + path.extname(inputFile) === ".svelte" + ? require.resolve("svelte-eslint-parser") + : undefined const result = linter.verify( config.code, { rules: { [ruleName]: ["error", ...(config.options || [])], }, - parser: "svelte-eslint-parser", + parser, parserOptions: { ecmaVersion: 2020, sourceType: "module", @@ -262,5 +266,5 @@ function getConfig(ruleName: string, inputFile: string) { ? require.resolve("svelte-eslint-parser") : undefined - return Object.assign({ parser }, config, { code, filename }) + return { parser, ...config, parserOptions: {}, code, filename } } diff --git a/typings/eslint-utils/index.d.ts b/typings/eslint-utils/index.d.ts index 5a4112cdc..fc14c75cc 100644 --- a/typings/eslint-utils/index.d.ts +++ b/typings/eslint-utils/index.d.ts @@ -1,6 +1,10 @@ import type { AST } from "svelte-eslint-parser" import type { Scope } from "eslint" import type * as ESTree from "estree" +export { + ReferenceTracker, + TrackedReferences, +} from "../../node_modules/@types/eslint-utils" type Token = { type: string; value: string } export function isArrowToken(token: Token): boolean export function isCommaToken(token: Token): boolean From 353391876be312c92ebbc57759265f7236a3a38f Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Fri, 5 Aug 2022 20:51:07 +0900 Subject: [PATCH 2/5] Update doc --- docs/rules/require-stores-init.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/rules/require-stores-init.md b/docs/rules/require-stores-init.md index 97c849e38..074c79af5 100644 --- a/docs/rules/require-stores-init.md +++ b/docs/rules/require-stores-init.md @@ -41,6 +41,13 @@ export const d2 = derived([a, b], () => {}) Nothing. +## :heart: Compatibility + +This rule was taken from [@tivac/eslint-plugin-svelte]. +This rule is compatible with `@tivac/svelte/stores-initial-value` rule. + +[@tivac/eslint-plugin-svelte]: https://github.com/tivac/eslint-plugin-svelte/ + ## :mag: Implementation - [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/require-stores-init.ts) From cb2ab3ba03604c6d677c2f8e11fadb25dd5d5687 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Fri, 5 Aug 2022 20:59:04 +0900 Subject: [PATCH 3/5] revert tests/utils.ts --- tests/utils/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/utils.ts b/tests/utils/utils.ts index f6572e74a..ca5807a88 100644 --- a/tests/utils/utils.ts +++ b/tests/utils/utils.ts @@ -266,5 +266,5 @@ function getConfig(ruleName: string, inputFile: string) { ? require.resolve("svelte-eslint-parser") : undefined - return { parser, ...config, parserOptions: {}, code, filename } + return Object.assign({ parser }, config, { code, filename }) } From 68fa61c334e1be9a229df536fb7f78336e360fb1 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Fri, 5 Aug 2022 21:00:02 +0900 Subject: [PATCH 4/5] fix lint errors --- tests/fixtures/rules/indent/invalid/.eslintrc.js | 2 -- tests/fixtures/rules/no-unused-svelte-ignore/valid/.eslintrc.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/tests/fixtures/rules/indent/invalid/.eslintrc.js b/tests/fixtures/rules/indent/invalid/.eslintrc.js index 496509f71..788e1c38d 100644 --- a/tests/fixtures/rules/indent/invalid/.eslintrc.js +++ b/tests/fixtures/rules/indent/invalid/.eslintrc.js @@ -1,5 +1,3 @@ -"use strict" - module.exports = { rules: { "no-sparse-arrays": "off", diff --git a/tests/fixtures/rules/no-unused-svelte-ignore/valid/.eslintrc.js b/tests/fixtures/rules/no-unused-svelte-ignore/valid/.eslintrc.js index ac738ac81..bb79284b9 100644 --- a/tests/fixtures/rules/no-unused-svelte-ignore/valid/.eslintrc.js +++ b/tests/fixtures/rules/no-unused-svelte-ignore/valid/.eslintrc.js @@ -1,5 +1,3 @@ -"use strict" - module.exports = { overrides: [ { From f9dbca9e1a763407d82dc98636122c88fe31f26d Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Fri, 5 Aug 2022 21:05:49 +0900 Subject: [PATCH 5/5] fix test case --- .../valid/no-svelte-store01-input.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/fixtures/rules/require-stores-init/valid/no-svelte-store01-input.svelte b/tests/fixtures/rules/require-stores-init/valid/no-svelte-store01-input.svelte index e14257b95..79db1fde0 100644 --- a/tests/fixtures/rules/require-stores-init/valid/no-svelte-store01-input.svelte +++ b/tests/fixtures/rules/require-stores-init/valid/no-svelte-store01-input.svelte @@ -1,6 +1,6 @@