Repository: ota-meshi/eslint-plugin-svelte
Pull request #201 (2022-07-28 18:15:11)

add `svelte/no-extra-reactive-curlies` rule
Porting over a simple rule from https://github.com/tivac/eslint-plugin-svelte/ as per the conversation in https://github.com/ota-meshi/eslint-plugin-svelte/issues/198

I haven't been able to test that the documentation looks right, `yarn svelte-kit` is doing some funny path stuff on my windows machine. 🤷🏻‍♂️

ota-meshi: What do you think about rename the rule to `no-lone-reactive-blocks` or `no-extra-reactive-curlies`? I think it's similar to eslint's `no-lone-blocks` and `no-extra-parens`.
https://eslint.org/docs/latest/rules/no-lone-blocks
https://eslint.org/docs/latest/rules/no-extra-parens

ota-meshi: I didn't explain it, use the `yarn docs:watch` command to preview the document site. However, I'm not sure if there is a problem with windows.
tivac: > I didn't explain it, use the `yarn docs:watch` command to preview the document site. However, I'm not sure if there is a problem with windows.

This is what I see as output

```
C:\Users\pcavi\Documents\GitHub\eslint-svelte>yarn docs:watch
yarn run v1.22.19
$ yarn svelte-kit dev
$ node --experimental-loader ./svelte-kit-import-hook.mjs node_modules/vite/bin/vite.js dev
(node:9368) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:9368) DeprecationWarning: Obsolete loader hook(s) supplied and will be ignored: transformSource
build@ \C:\Users\pcavi\Documents\GitHub\eslint-svelte\docs-svelte-kit\build-system\src\eslint.mjs
X [ERROR] Could not read from file: C:\Users\pcavi\Documents\GitHub\eslint-svelte\C:\Users\pcavi\Documents\GitHub\eslint-svelte\docs-svelte-kit\build-system\src\process-shim.mjs

X [ERROR] Could not resolve "\\C:\\Users\\pcavi\\Documents\\GitHub\\eslint-svelte\\docs-svelte-kit\\build-system\\src\\eslint.mjs"

failed to load config from C:\Users\pcavi\Documents\GitHub\eslint-svelte\vite.config.mts
error when starting dev server:
Error: Build failed with 2 errors:
error: Could not read from file: C:\Users\pcavi\Documents\GitHub\eslint-svelte\C:\Users\pcavi\Documents\GitHub\eslint-svelte\docs-svelte-kit\build-system\src\process-shim.mjs
error: Could not resolve "\\C:\\Users\\pcavi\\Documents\\GitHub\\eslint-svelte\\docs-svelte-kit\\build-system\\src\\eslint.mjs"
    at failureErrorWithLog (C:\Users\pcavi\Documents\GitHub\eslint-svelte\node_modules\esbuild\lib\main.js:1621:15)
    at C:\Users\pcavi\Documents\GitHub\eslint-svelte\node_modules\esbuild\lib\main.js:1263:28
    at runOnEndCallbacks (C:\Users\pcavi\Documents\GitHub\eslint-svelte\node_modules\esbuild\lib\main.js:1176:65)
    at buildResponseToResult (C:\Users\pcavi\Documents\GitHub\eslint-svelte\node_modules\esbuild\lib\main.js:1261:7)
    at C:\Users\pcavi\Documents\GitHub\eslint-svelte\node_modules\esbuild\lib\main.js:1374:14
    at C:\Users\pcavi\Documents\GitHub\eslint-svelte\node_modules\esbuild\lib\main.js:675:9
    at handleIncomingPacket (C:\Users\pcavi\Documents\GitHub\eslint-svelte\node_modules\esbuild\lib\main.js:772:9)
    at Socket.readFromStdout (C:\Users\pcavi\Documents\GitHub\eslint-svelte\node_modules\esbuild\lib\main.js:641:7)
    at Socket.emit (node:events:527:28)
    at addChunk (node:internal/streams/readable:315:12)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
```

This one in particular seems like a likely culprit with the doubled-up folder structure.

`X [ERROR] Could not read from file: C:\Users\pcavi\Documents\GitHub\eslint-svelte\C:\Users\pcavi\Documents\GitHub\eslint-svelte\docs-svelte-kit\build-system\src\process-shim.mjs`
ota-meshi: > This one in particular seems like a likely culprit with the doubled-up folder structure.

Hmm. I don't know how to fix it. I think we giving esbuild the correct absolute path.

Patch:
diff --git a/README.md b/README.md
index 5dbdf55..d557ee9 100644
--- a/README.md
+++ b/README.md
@@ -298,6 +298,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
 | [svelte/indent](https://ota-meshi.github.io/eslint-plugin-svelte/rules/indent/) | enforce consistent indentation | :wrench: |
 | [svelte/max-attributes-per-line](https://ota-meshi.github.io/eslint-plugin-svelte/rules/max-attributes-per-line/) | enforce the maximum number of attributes per line | :wrench: |
 | [svelte/mustache-spacing](https://ota-meshi.github.io/eslint-plugin-svelte/rules/mustache-spacing/) | enforce unified spacing in mustache | :wrench: |
+| [svelte/no-extra-reactive-curlies](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-extra-reactive-curlies/) | disallow wrapping single reactive statements in curly braces |  |
 | [svelte/no-spaces-around-equal-signs-in-attribute](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-spaces-around-equal-signs-in-attribute/) | disallow spaces around equal signs in attribute | :wrench: |
 | [svelte/prefer-class-directive](https://ota-meshi.github.io/eslint-plugin-svelte/rules/prefer-class-directive/) | require class directives instead of ternary expressions | :wrench: |
 | [svelte/prefer-style-directive](https://ota-meshi.github.io/eslint-plugin-svelte/rules/prefer-style-directive/) | require style directives instead of style attribute | :wrench: |
diff --git a/docs/rules.md b/docs/rules.md
index 9581c79..55a3fbf 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -58,6 +58,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
 | [svelte/indent](./rules/indent.md) | enforce consistent indentation | :wrench: |
 | [svelte/max-attributes-per-line](./rules/max-attributes-per-line.md) | enforce the maximum number of attributes per line | :wrench: |
 | [svelte/mustache-spacing](./rules/mustache-spacing.md) | enforce unified spacing in mustache | :wrench: |
+| [svelte/no-extra-reactive-curlies](./rules/no-extra-reactive-curlies.md) | disallow wrapping single reactive statements in curly braces |  |
 | [svelte/no-spaces-around-equal-signs-in-attribute](./rules/no-spaces-around-equal-signs-in-attribute.md) | disallow spaces around equal signs in attribute | :wrench: |
 | [svelte/prefer-class-directive](./rules/prefer-class-directive.md) | require class directives instead of ternary expressions | :wrench: |
 | [svelte/prefer-style-directive](./rules/prefer-style-directive.md) | require style directives instead of style attribute | :wrench: |
diff --git a/docs/rules/no-extra-reactive-curlies.md b/docs/rules/no-extra-reactive-curlies.md
new file mode 100644
index 0000000..7b3bf37
--- /dev/null
+++ b/docs/rules/no-extra-reactive-curlies.md
@@ -0,0 +1,52 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "svelte/no-extra-reactive-curlies"
+description: "disallow wrapping single reactive statements in curly braces"
+---
+
+# svelte/no-extra-reactive-curlies
+
+> disallow wrapping single reactive statements in curly braces
+
+- :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 reports if curly brackets (`{` and `}`) are used unnecessarily around a reactive statement body containing only a single expression.
+
+<ESLintCodeBlock>
+
+<!--eslint-skip-->
+
+```svelte
+<script>
+  /* eslint svelte/no-extra-reactive-curlies: "error" */
+
+  /* ✓ GOOD */
+  $: foo = "red"
+
+  /* ✗ BAD */
+  $: {
+    foo = "red"
+  }
+</script>
+```
+
+</ESLintCodeBlock>
+
+## :wrench: Options
+
+Nothing.
+
+## :heart: Compatibility
+
+This rule was taken from [@tivac/eslint-plugin-svelte].  
+This rule is compatible with `@tivac/svelte/reactive-curlies` 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/no-extra-reactive-curlies.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-extra-reactive-curlies.ts)
diff --git a/src/rules/no-extra-reactive-curlies.ts b/src/rules/no-extra-reactive-curlies.ts
new file mode 100644
index 0000000..ba9db7d
--- /dev/null
+++ b/src/rules/no-extra-reactive-curlies.ts
@@ -0,0 +1,56 @@
+import type { TSESTree } from "@typescript-eslint/types"
+import { createRule } from "../utils"
+
+export default createRule("no-extra-reactive-curlies", {
+  meta: {
+    docs: {
+      description:
+        "disallow wrapping single reactive statements in curly braces",
+      category: "Stylistic Issues",
+      recommended: false,
+      conflictWithPrettier: false,
+    },
+    hasSuggestions: true,
+    schema: [],
+    messages: {
+      extraCurlies: `Do not wrap reactive statements in curly braces unless necessary.`,
+      removeExtraCurlies: `Remove the unnecessary curly braces.`,
+    },
+    type: "suggestion",
+  },
+  create(context) {
+    return {
+      // $: { foo = "bar"; }
+      [`SvelteReactiveStatement > BlockStatement[body.length=1]`]: (
+        node: TSESTree.BlockStatement,
+      ) => {
+        const source = context.getSourceCode()
+
+        return context.report({
+          node,
+          loc: node.loc,
+          messageId: "extraCurlies",
+          suggest: [
+            {
+              messageId: "removeExtraCurlies",
+              fix(fixer) {
+                const tokens = source.getTokens(node, { includeComments: true })
+
+                // Remove everything up to the second token, and the entire last token since
+                // those are known to be "{" and "}"
+                return [
+                  fixer.removeRange([tokens[0].range[0], tokens[1].range[0]]),
+
+                  fixer.removeRange([
+                    tokens[tokens.length - 2].range[1],
+                    tokens[tokens.length - 1].range[1],
+                  ]),
+                ]
+              },
+            },
+          ],
+        })
+      },
+    }
+  },
+})
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index c08ae0f..e46182b 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -12,6 +12,7 @@ import noAtHtmlTags from "../rules/no-at-html-tags"
 import noDupeElseIfBlocks from "../rules/no-dupe-else-if-blocks"
 import noDupeStyleProperties from "../rules/no-dupe-style-properties"
 import noDynamicSlotName from "../rules/no-dynamic-slot-name"
+import noExtraReactiveCurlies from "../rules/no-extra-reactive-curlies"
 import noInnerDeclarations from "../rules/no-inner-declarations"
 import noNotFunctionHandler from "../rules/no-not-function-handler"
 import noObjectInTextMustaches from "../rules/no-object-in-text-mustaches"
@@ -45,6 +46,7 @@ export const rules = [
   noDupeElseIfBlocks,
   noDupeStyleProperties,
   noDynamicSlotName,
+  noExtraReactiveCurlies,
   noInnerDeclarations,
   noNotFunctionHandler,
   noObjectInTextMustaches,


Test patch:
diff --git a/tests/fixtures/rules/no-extra-reactive-curlies/invalid/test01-errors.json b/tests/fixtures/rules/no-extra-reactive-curlies/invalid/test01-errors.json
new file mode 100644
index 0000000..6a0b3a4
--- /dev/null
+++ b/tests/fixtures/rules/no-extra-reactive-curlies/invalid/test01-errors.json
@@ -0,0 +1,14 @@
+[
+  {
+    "message": "Do not wrap reactive statements in curly braces unless necessary.",
+    "line": 2,
+    "column": 6,
+    "suggestions": [
+      {
+        "desc": "Remove the unnecessary curly braces.",
+        "messageId": "removeExtraCurlies",
+        "output": "<script>\n  $: foo = info\n</script>\n"
+      }
+    ]
+  }
+]
diff --git a/tests/fixtures/rules/no-extra-reactive-curlies/invalid/test01-input.svelte b/tests/fixtures/rules/no-extra-reactive-curlies/invalid/test01-input.svelte
new file mode 100644
index 0000000..e65c3ab
--- /dev/null
+++ b/tests/fixtures/rules/no-extra-reactive-curlies/invalid/test01-input.svelte
@@ -0,0 +1,5 @@
+<script>
+  $: {
+    foo = info
+  }
+</script>
diff --git a/tests/fixtures/rules/no-extra-reactive-curlies/invalid/test02-errors.json b/tests/fixtures/rules/no-extra-reactive-curlies/invalid/test02-errors.json
new file mode 100644
index 0000000..cc41e64
--- /dev/null
+++ b/tests/fixtures/rules/no-extra-reactive-curlies/invalid/test02-errors.json
@@ -0,0 +1,14 @@
+[
+  {
+    "message": "Do not wrap reactive statements in curly braces unless necessary.",
+    "line": 3,
+    "column": 6,
+    "suggestions": [
+      {
+        "desc": "Remove the unnecessary curly braces.",
+        "messageId": "removeExtraCurlies",
+        "output": "<!-- prettier-ignore -->\n<script>\n  $: foo = info;\n</script>\n"
+      }
+    ]
+  }
+]
diff --git a/tests/fixtures/rules/no-extra-reactive-curlies/invalid/test02-input.svelte b/tests/fixtures/rules/no-extra-reactive-curlies/invalid/test02-input.svelte
new file mode 100644
index 0000000..6074c3c
--- /dev/null
+++ b/tests/fixtures/rules/no-extra-reactive-curlies/invalid/test02-input.svelte
@@ -0,0 +1,4 @@
+<!-- prettier-ignore -->
+<script>
+  $: { foo = info; }
+</script>
diff --git a/tests/fixtures/rules/no-extra-reactive-curlies/valid/test01-input.svelte b/tests/fixtures/rules/no-extra-reactive-curlies/valid/test01-input.svelte
new file mode 100644
index 0000000..630f715
--- /dev/null
+++ b/tests/fixtures/rules/no-extra-reactive-curlies/valid/test01-input.svelte
@@ -0,0 +1,5 @@
+<!-- prettier-ignore -->
+<script>
+  $: foo1 = "bar"
+  $: foo2 = "bar";
+</script>
diff --git a/tests/fixtures/rules/no-extra-reactive-curlies/valid/test02-input.svelte b/tests/fixtures/rules/no-extra-reactive-curlies/valid/test02-input.svelte
new file mode 100644
index 0000000..0bd0c0d
--- /dev/null
+++ b/tests/fixtures/rules/no-extra-reactive-curlies/valid/test02-input.svelte
@@ -0,0 +1,6 @@
+<script>
+  $: {
+    foo = info
+    bar = something
+  }
+</script>
diff --git a/tests/src/rules/no-extra-reactive-curlies.ts b/tests/src/rules/no-extra-reactive-curlies.ts
new file mode 100644
index 0000000..ed9674d
--- /dev/null
+++ b/tests/src/rules/no-extra-reactive-curlies.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/no-extra-reactive-curlies"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: "module",
+  },
+})
+
+tester.run(
+  "no-extra-reactive-curlies",
+  rule as any,
+  loadTestCases("no-extra-reactive-curlies"),
+)