diff --git a/README.md b/README.md
index 6a766fd48..fb31df855 100644
--- a/README.md
+++ b/README.md
@@ -246,6 +246,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
| Rule ID | Description | |
|:--------|:------------|:---|
| [@ota-meshi/svelte/no-dupe-else-if-blocks](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-dupe-else-if-blocks.html) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
+| [@ota-meshi/svelte/no-object-in-text-mustaches](https://ota-meshi.github.io/eslint-plugin-svelte/rules/no-object-in-text-mustaches.html) | disallow objects in text mustache interpolation | :star: |
## Security Vulnerability
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 209ce5231..d3baf4bdc 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -16,6 +16,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
| Rule ID | Description | |
|:--------|:------------|:---|
| [@ota-meshi/svelte/no-dupe-else-if-blocks](./no-dupe-else-if-blocks.md) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
+| [@ota-meshi/svelte/no-object-in-text-mustaches](./no-object-in-text-mustaches.md) | disallow objects in text mustache interpolation | :star: |
## Security Vulnerability
diff --git a/docs/rules/no-object-in-text-mustaches.md b/docs/rules/no-object-in-text-mustaches.md
new file mode 100644
index 000000000..8cd8f0c6a
--- /dev/null
+++ b/docs/rules/no-object-in-text-mustaches.md
@@ -0,0 +1,48 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "@ota-meshi/svelte/no-object-in-text-mustaches"
+description: "disallow objects in text mustache interpolation"
+---
+
+# @ota-meshi/svelte/no-object-in-text-mustaches
+
+> disallow objects in text mustache interpolation
+
+- :exclamation: **_This rule has not been released yet._**
+- :gear: This rule is included in `"plugin:@ota-meshi/svelte/recommended"`.
+
+## :book: Rule Details
+
+This rule disallows the use of objects in text mustache interpolation.
+When you use an object for text interpolation, it is drawn as `[object Object]`. It's almost always a mistake. You may have written a lot of unnecessary curly braces.
+
+
+
+
+
+```svelte
+
+
+
+{foo}
+
+
+
+
+{{ foo }}
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-object-in-text-mustaches.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-object-in-text-mustaches.ts)
diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts
index 74482b93b..870a83f21 100644
--- a/src/configs/recommended.ts
+++ b/src/configs/recommended.ts
@@ -11,6 +11,7 @@ export = {
"@ota-meshi/svelte/no-at-html-tags": "error",
"@ota-meshi/svelte/no-dupe-else-if-blocks": "error",
"@ota-meshi/svelte/no-inner-declarations": "error",
+ "@ota-meshi/svelte/no-object-in-text-mustaches": "error",
"@ota-meshi/svelte/system": "error",
},
}
diff --git a/src/rules/indent-helpers/ast.ts b/src/rules/indent-helpers/ast.ts
index c6c6bcffa..ddf107b8a 100644
--- a/src/rules/indent-helpers/ast.ts
+++ b/src/rules/indent-helpers/ast.ts
@@ -7,7 +7,11 @@ type AnyToken = AST.Token | AST.Comment
export function isWhitespace(
token: AnyToken | ESTree.Comment | null | undefined,
): boolean {
- return token != null && token.type === "HTMLText" && !token.value.trim()
+ return (
+ token != null &&
+ ((token.type === "HTMLText" && !token.value.trim()) ||
+ (token.type === "JSXText" && !token.value.trim()))
+ )
}
/**
diff --git a/src/rules/indent-helpers/es.ts b/src/rules/indent-helpers/es.ts
index 3d1249113..4f2dcf99f 100644
--- a/src/rules/indent-helpers/es.ts
+++ b/src/rules/indent-helpers/es.ts
@@ -464,42 +464,41 @@ export function defineVisitor(context: IndentContext): NodeListener {
node: ESTree.FunctionDeclaration | ESTree.FunctionExpression,
) {
const firstToken = sourceCode.getFirstToken(node)
- let leftParenToken, bodyBaseToken
+
+ const leftParenToken = sourceCode.getTokenBefore(
+ node.params[0] ||
+ (node as TSESTree.FunctionExpression).returnType ||
+ sourceCode.getTokenBefore(node.body),
+ {
+ filter: isOpeningParenToken,
+ includeComments: false,
+ },
+ )!
+ let bodyBaseToken
if (firstToken.type === "Punctuator") {
// method
- leftParenToken = firstToken
bodyBaseToken = sourceCode.getFirstToken(getParent(node)!)
} else {
- let nextToken = sourceCode.getTokenAfter(firstToken)
- let nextTokenOffset = 0
- while (
- nextToken &&
- !isOpeningParenToken(nextToken) &&
- nextToken.value !== "<"
- ) {
+ let tokenOffset = 0
+ for (const token of sourceCode.getTokensBetween(
+ firstToken,
+ leftParenToken,
+ )) {
+ if (token.value === "<") {
+ break
+ }
if (
- nextToken.value === "*" ||
- (node.id && nextToken.range[0] === node.id.range![0])
+ token.value === "*" ||
+ (node.id && token.range[0] === node.id.range![0])
) {
- nextTokenOffset = 1
+ tokenOffset = 1
}
- offsets.setOffsetToken(nextToken, nextTokenOffset, firstToken)
- nextToken = sourceCode.getTokenAfter(nextToken)
+ offsets.setOffsetToken(token, tokenOffset, firstToken)
}
- leftParenToken = nextToken!
bodyBaseToken = firstToken
}
- if (
- !isOpeningParenToken(leftParenToken) &&
- (node as TSESTree.FunctionExpression).typeParameters
- ) {
- leftParenToken = sourceCode.getTokenAfter(
- (node as TSESTree.FunctionExpression).typeParameters!,
- )!
- }
-
const rightParenToken = sourceCode.getTokenAfter(
node.params[node.params.length - 1] || leftParenToken,
{ filter: isClosingParenToken, includeComments: false },
diff --git a/src/rules/no-object-in-text-mustaches.ts b/src/rules/no-object-in-text-mustaches.ts
new file mode 100644
index 000000000..06601b2bd
--- /dev/null
+++ b/src/rules/no-object-in-text-mustaches.ts
@@ -0,0 +1,54 @@
+import { createRule } from "../utils"
+
+const PHRASES = {
+ ObjectExpression: "object",
+ ArrayExpression: "array",
+ ArrowFunctionExpression: "function",
+ FunctionExpression: "function",
+ ClassExpression: "class",
+}
+
+export default createRule("no-object-in-text-mustaches", {
+ meta: {
+ docs: {
+ description: "disallow objects in text mustache interpolation",
+ category: "Possible Errors",
+ recommended: true,
+ },
+ schema: [],
+ messages: {
+ unexpected: "Unexpected {{phrase}} in text mustache interpolation.",
+ },
+ type: "problem", // "problem", or "layout",
+ },
+ create(context) {
+ return {
+ SvelteMustacheTag(node) {
+ const { expression } = node
+ if (
+ expression.type !== "ObjectExpression" &&
+ expression.type !== "ArrayExpression" &&
+ expression.type !== "ArrowFunctionExpression" &&
+ expression.type !== "FunctionExpression" &&
+ expression.type !== "ClassExpression"
+ ) {
+ return
+ }
+ if (node.parent.type === "SvelteAttribute") {
+ if (node.parent.value.length === 1) {
+ // Maybe props
+ return
+ }
+ }
+
+ context.report({
+ node,
+ messageId: "unexpected",
+ data: {
+ phrase: PHRASES[expression.type],
+ },
+ })
+ },
+ }
+ },
+})
diff --git a/src/types.ts b/src/types.ts
index 23fba6442..33d1a83c6 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -20,7 +20,7 @@ type ASTNodeListenerMap = {
[key in ASTNodeWithParent["type"]]: T extends { type: key } ? T : never
}
-type ASTNodeListener = {
+export type ASTNodeListener = {
[T in keyof ASTNodeListenerMap]?: (node: ASTNodeListenerMap[T]) => void
}
export interface RuleListener extends ASTNodeListener {
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index a593b578b..4ec015b87 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -8,6 +8,7 @@ import noAtDebugTags from "../rules/no-at-debug-tags"
import noAtHtmlTags from "../rules/no-at-html-tags"
import noDupeElseIfBlocks from "../rules/no-dupe-else-if-blocks"
import noInnerDeclarations from "../rules/no-inner-declarations"
+import noObjectInTextMustaches from "../rules/no-object-in-text-mustaches"
import noTargetBlank from "../rules/no-target-blank"
import noUselessMustaches from "../rules/no-useless-mustaches"
import preferClassDirective from "../rules/prefer-class-directive"
@@ -25,6 +26,7 @@ export const rules = [
noAtHtmlTags,
noDupeElseIfBlocks,
noInnerDeclarations,
+ noObjectInTextMustaches,
noTargetBlank,
noUselessMustaches,
preferClassDirective,
diff --git a/tests/fixtures/rules/no-object-in-text-mustaches/invalid/array01-errors.json b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/array01-errors.json
new file mode 100644
index 000000000..11af7d85e
--- /dev/null
+++ b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/array01-errors.json
@@ -0,0 +1,12 @@
+[
+ {
+ "message": "Unexpected array in text mustache interpolation.",
+ "line": 5,
+ "column": 1
+ },
+ {
+ "message": "Unexpected array in text mustache interpolation.",
+ "line": 6,
+ "column": 15
+ }
+]
diff --git a/tests/fixtures/rules/no-object-in-text-mustaches/invalid/array01-input.svelte b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/array01-input.svelte
new file mode 100644
index 000000000..1d319c8a4
--- /dev/null
+++ b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/array01-input.svelte
@@ -0,0 +1,6 @@
+
+
+{[a]}
+
diff --git a/tests/fixtures/rules/no-object-in-text-mustaches/invalid/class01-errors.json b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/class01-errors.json
new file mode 100644
index 000000000..51283455c
--- /dev/null
+++ b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/class01-errors.json
@@ -0,0 +1,12 @@
+[
+ {
+ "message": "Unexpected class in text mustache interpolation.",
+ "line": 4,
+ "column": 1
+ },
+ {
+ "message": "Unexpected class in text mustache interpolation.",
+ "line": 6,
+ "column": 15
+ }
+]
diff --git a/tests/fixtures/rules/no-object-in-text-mustaches/invalid/class01-input.svelte b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/class01-input.svelte
new file mode 100644
index 000000000..529282a50
--- /dev/null
+++ b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/class01-input.svelte
@@ -0,0 +1,6 @@
+
+
+{class A {}}
+
+
diff --git a/tests/fixtures/rules/no-object-in-text-mustaches/invalid/function01-errors.json b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/function01-errors.json
new file mode 100644
index 000000000..31e6c0516
--- /dev/null
+++ b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/function01-errors.json
@@ -0,0 +1,17 @@
+[
+ {
+ "message": "Unexpected function in text mustache interpolation.",
+ "line": 5,
+ "column": 1
+ },
+ {
+ "message": "Unexpected function in text mustache interpolation.",
+ "line": 6,
+ "column": 1
+ },
+ {
+ "message": "Unexpected function in text mustache interpolation.",
+ "line": 9,
+ "column": 15
+ }
+]
diff --git a/tests/fixtures/rules/no-object-in-text-mustaches/invalid/function01-input.svelte b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/function01-input.svelte
new file mode 100644
index 000000000..ea343fcbf
--- /dev/null
+++ b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/function01-input.svelte
@@ -0,0 +1,9 @@
+
+
+{() => a}
+{function () {
+ return a
+}}
+
diff --git a/tests/fixtures/rules/no-object-in-text-mustaches/invalid/object01-errors.json b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/object01-errors.json
new file mode 100644
index 000000000..d6817f558
--- /dev/null
+++ b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/object01-errors.json
@@ -0,0 +1,12 @@
+[
+ {
+ "message": "Unexpected object in text mustache interpolation.",
+ "line": 5,
+ "column": 1
+ },
+ {
+ "message": "Unexpected object in text mustache interpolation.",
+ "line": 6,
+ "column": 15
+ }
+]
diff --git a/tests/fixtures/rules/no-object-in-text-mustaches/invalid/object01-input.svelte b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/object01-input.svelte
new file mode 100644
index 000000000..c8bbd775f
--- /dev/null
+++ b/tests/fixtures/rules/no-object-in-text-mustaches/invalid/object01-input.svelte
@@ -0,0 +1,6 @@
+
+
+{{ a }}
+
diff --git a/tests/fixtures/rules/no-object-in-text-mustaches/valid/object01-input.svelte b/tests/fixtures/rules/no-object-in-text-mustaches/valid/object01-input.svelte
new file mode 100644
index 000000000..22604e10d
--- /dev/null
+++ b/tests/fixtures/rules/no-object-in-text-mustaches/valid/object01-input.svelte
@@ -0,0 +1,6 @@
+
+
+
diff --git a/tests/fixtures/rules/no-object-in-text-mustaches/valid/string01-input.svelte b/tests/fixtures/rules/no-object-in-text-mustaches/valid/string01-input.svelte
new file mode 100644
index 000000000..3d300e52f
--- /dev/null
+++ b/tests/fixtures/rules/no-object-in-text-mustaches/valid/string01-input.svelte
@@ -0,0 +1,7 @@
+
+
+{a}
+
+
diff --git a/tests/src/rules/no-object-in-text-mustaches.ts b/tests/src/rules/no-object-in-text-mustaches.ts
new file mode 100644
index 000000000..66b42ce8f
--- /dev/null
+++ b/tests/src/rules/no-object-in-text-mustaches.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/no-object-in-text-mustaches"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run(
+ "no-object-in-text-mustaches",
+ rule as any,
+ loadTestCases("no-object-in-text-mustaches"),
+)