Repository: ota-meshi/eslint-plugin-svelte
Pull request #211 (2022-08-05 11:55:21)

Add `svelte/require-stores-init` rule
This PR adds `svelte/require-stores-init` rule that require initial value in store.

This rule was taken from  `@tivac/svelte/stores-initial-value` rule.
However, no suggestions are provided.
The original suggestion is to give `false` as the argument, but I think the least code-breaking suggestion is to give `undefined` as the argument. But for most users it makes no sense to give `undefined`.



Patch:
diff --git a/README.md b/README.md
index dabe16b..2927f67 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 4f100e4..1ce361a 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 02644ef..cb047c7 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 01451b0..040d078 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 0000000..074c79a
--- /dev/null
+++ b/docs/rules/require-stores-init.md
@@ -0,0 +1,54 @@
+---
+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: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
+
+## :book: Rule Details
+
+This rule is aimed to enforce initial values when initializing the Svelte stores.
+
+<ESLintCodeBlock language="javascript">
+
+<!--eslint-skip-->
+
+```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], () => {})
+```
+
+</ESLintCodeBlock>
+
+## :wrench: Options
+
+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)
+- [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 43387bf..da52fa1 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 0000000..5b2ac84
--- /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 f4e6298..e5776af 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/typings/eslint-utils/index.d.ts b/typings/eslint-utils/index.d.ts
index 5a4112c..fc14c75 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


Test patch:
diff --git a/tests/fixtures/rules/.eslintrc.js b/tests/fixtures/rules/.eslintrc.js
index c810e19..d4fc563 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/indent/invalid/.eslintrc.js b/tests/fixtures/rules/indent/invalid/.eslintrc.js
index 496509f..788e1c3 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 ac738ac..bb79284 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: [
     {
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 0000000..c0a036d
--- /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 0000000..7972629
--- /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 0000000..e6f7ae5
--- /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 0000000..3a6a2be
--- /dev/null
+++ b/tests/fixtures/rules/require-stores-init/invalid/no-init01-input.svelte
@@ -0,0 +1,6 @@
+<script>
+  import { writable, readable, derived } from "svelte/store"
+  const w = writable()
+  const r = readable()
+  const d = derived([a, b], () => {})
+</script>
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 0000000..11a5a93
--- /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 0000000..8b07709
--- /dev/null
+++ b/tests/fixtures/rules/require-stores-init/valid/has-init01-input.svelte
@@ -0,0 +1,6 @@
+<script>
+  import { writable, readable, derived } from "svelte/store"
+  const w = writable(false)
+  const r = readable({})
+  const d = derived([a, b], () => {}, false)
+</script>
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 0000000..79db1fd
--- /dev/null
+++ b/tests/fixtures/rules/require-stores-init/valid/no-svelte-store01-input.svelte
@@ -0,0 +1,6 @@
+<script>
+  import { writable, readable, derived } from "./unknown"
+  const w = writable()
+  const r = readable()
+  const d = derived([a, b], () => {})
+</script>
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 0000000..f51b331
--- /dev/null
+++ b/tests/fixtures/rules/require-stores-init/valid/spread01-input.svelte
@@ -0,0 +1,5 @@
+<script>
+  import { derived } from "svelte/store"
+  const args = [[a, b], () => {}, false]
+  const d = derived(...args)
+</script>
diff --git a/tests/src/rules/require-stores-init.ts b/tests/src/rules/require-stores-init.ts
new file mode 100644
index 0000000..6dcbde5
--- /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 848d805..ca5807a 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",