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"), +)