Skip to content

Commit bed9624

Browse files
authored
Add no-object-in-text-mustaches rule (#34)
1 parent fbaa1e4 commit bed9624

20 files changed

+245
-26
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
246246
| Rule ID | Description | |
247247
|:--------|:------------|:---|
248248
| [@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: |
249+
| [@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: |
249250

250251
## Security Vulnerability
251252

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ These rules relate to possible syntax or logic errors in Svelte code:
1616
| Rule ID | Description | |
1717
|:--------|:------------|:---|
1818
| [@ota-meshi/svelte/no-dupe-else-if-blocks](./no-dupe-else-if-blocks.md) | disallow duplicate conditions in `{#if}` / `{:else if}` chains | :star: |
19+
| [@ota-meshi/svelte/no-object-in-text-mustaches](./no-object-in-text-mustaches.md) | disallow objects in text mustache interpolation | :star: |
1920

2021
## Security Vulnerability
2122

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "@ota-meshi/svelte/no-object-in-text-mustaches"
5+
description: "disallow objects in text mustache interpolation"
6+
---
7+
8+
# @ota-meshi/svelte/no-object-in-text-mustaches
9+
10+
> disallow objects in text mustache interpolation
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
- :gear: This rule is included in `"plugin:@ota-meshi/svelte/recommended"`.
14+
15+
## :book: Rule Details
16+
17+
This rule disallows the use of objects in text mustache interpolation.
18+
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.
19+
20+
<eslint-code-block>
21+
22+
<!--eslint-skip-->
23+
24+
```svelte
25+
<script>
26+
/* eslint @ota-meshi/svelte/no-object-in-text-mustaches: "error" */
27+
</script>
28+
29+
<!-- ✓ GOOD -->
30+
{foo}
31+
<input class="{foo} bar" />
32+
<MyComponent prop={{ foo }} />
33+
34+
<!-- ✗ BAD -->
35+
{{ foo }}
36+
<input class="{{ foo }} bar" />
37+
```
38+
39+
</eslint-code-block>
40+
41+
## :wrench: Options
42+
43+
Nothing.
44+
45+
## :mag: Implementation
46+
47+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/no-object-in-text-mustaches.ts)
48+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/no-object-in-text-mustaches.ts)

src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export = {
1111
"@ota-meshi/svelte/no-at-html-tags": "error",
1212
"@ota-meshi/svelte/no-dupe-else-if-blocks": "error",
1313
"@ota-meshi/svelte/no-inner-declarations": "error",
14+
"@ota-meshi/svelte/no-object-in-text-mustaches": "error",
1415
"@ota-meshi/svelte/system": "error",
1516
},
1617
}

src/rules/indent-helpers/ast.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ type AnyToken = AST.Token | AST.Comment
77
export function isWhitespace(
88
token: AnyToken | ESTree.Comment | null | undefined,
99
): boolean {
10-
return token != null && token.type === "HTMLText" && !token.value.trim()
10+
return (
11+
token != null &&
12+
((token.type === "HTMLText" && !token.value.trim()) ||
13+
(token.type === "JSXText" && !token.value.trim()))
14+
)
1115
}
1216

1317
/**

src/rules/indent-helpers/es.ts

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -464,42 +464,41 @@ export function defineVisitor(context: IndentContext): NodeListener {
464464
node: ESTree.FunctionDeclaration | ESTree.FunctionExpression,
465465
) {
466466
const firstToken = sourceCode.getFirstToken(node)
467-
let leftParenToken, bodyBaseToken
467+
468+
const leftParenToken = sourceCode.getTokenBefore(
469+
node.params[0] ||
470+
(node as TSESTree.FunctionExpression).returnType ||
471+
sourceCode.getTokenBefore(node.body),
472+
{
473+
filter: isOpeningParenToken,
474+
includeComments: false,
475+
},
476+
)!
477+
let bodyBaseToken
468478
if (firstToken.type === "Punctuator") {
469479
// method
470-
leftParenToken = firstToken
471480
bodyBaseToken = sourceCode.getFirstToken(getParent(node)!)
472481
} else {
473-
let nextToken = sourceCode.getTokenAfter(firstToken)
474-
let nextTokenOffset = 0
475-
while (
476-
nextToken &&
477-
!isOpeningParenToken(nextToken) &&
478-
nextToken.value !== "<"
479-
) {
482+
let tokenOffset = 0
483+
for (const token of sourceCode.getTokensBetween(
484+
firstToken,
485+
leftParenToken,
486+
)) {
487+
if (token.value === "<") {
488+
break
489+
}
480490
if (
481-
nextToken.value === "*" ||
482-
(node.id && nextToken.range[0] === node.id.range![0])
491+
token.value === "*" ||
492+
(node.id && token.range[0] === node.id.range![0])
483493
) {
484-
nextTokenOffset = 1
494+
tokenOffset = 1
485495
}
486-
offsets.setOffsetToken(nextToken, nextTokenOffset, firstToken)
487-
nextToken = sourceCode.getTokenAfter(nextToken)
496+
offsets.setOffsetToken(token, tokenOffset, firstToken)
488497
}
489498

490-
leftParenToken = nextToken!
491499
bodyBaseToken = firstToken
492500
}
493501

494-
if (
495-
!isOpeningParenToken(leftParenToken) &&
496-
(node as TSESTree.FunctionExpression).typeParameters
497-
) {
498-
leftParenToken = sourceCode.getTokenAfter(
499-
(node as TSESTree.FunctionExpression).typeParameters!,
500-
)!
501-
}
502-
503502
const rightParenToken = sourceCode.getTokenAfter(
504503
node.params[node.params.length - 1] || leftParenToken,
505504
{ filter: isClosingParenToken, includeComments: false },
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { createRule } from "../utils"
2+
3+
const PHRASES = {
4+
ObjectExpression: "object",
5+
ArrayExpression: "array",
6+
ArrowFunctionExpression: "function",
7+
FunctionExpression: "function",
8+
ClassExpression: "class",
9+
}
10+
11+
export default createRule("no-object-in-text-mustaches", {
12+
meta: {
13+
docs: {
14+
description: "disallow objects in text mustache interpolation",
15+
category: "Possible Errors",
16+
recommended: true,
17+
},
18+
schema: [],
19+
messages: {
20+
unexpected: "Unexpected {{phrase}} in text mustache interpolation.",
21+
},
22+
type: "problem", // "problem", or "layout",
23+
},
24+
create(context) {
25+
return {
26+
SvelteMustacheTag(node) {
27+
const { expression } = node
28+
if (
29+
expression.type !== "ObjectExpression" &&
30+
expression.type !== "ArrayExpression" &&
31+
expression.type !== "ArrowFunctionExpression" &&
32+
expression.type !== "FunctionExpression" &&
33+
expression.type !== "ClassExpression"
34+
) {
35+
return
36+
}
37+
if (node.parent.type === "SvelteAttribute") {
38+
if (node.parent.value.length === 1) {
39+
// Maybe props
40+
return
41+
}
42+
}
43+
44+
context.report({
45+
node,
46+
messageId: "unexpected",
47+
data: {
48+
phrase: PHRASES[expression.type],
49+
},
50+
})
51+
},
52+
}
53+
},
54+
})

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type ASTNodeListenerMap<T extends ASTNodeWithParent = ASTNodeWithParent> = {
2020
[key in ASTNodeWithParent["type"]]: T extends { type: key } ? T : never
2121
}
2222

23-
type ASTNodeListener = {
23+
export type ASTNodeListener = {
2424
[T in keyof ASTNodeListenerMap]?: (node: ASTNodeListenerMap[T]) => void
2525
}
2626
export interface RuleListener extends ASTNodeListener {

src/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import noAtDebugTags from "../rules/no-at-debug-tags"
88
import noAtHtmlTags from "../rules/no-at-html-tags"
99
import noDupeElseIfBlocks from "../rules/no-dupe-else-if-blocks"
1010
import noInnerDeclarations from "../rules/no-inner-declarations"
11+
import noObjectInTextMustaches from "../rules/no-object-in-text-mustaches"
1112
import noTargetBlank from "../rules/no-target-blank"
1213
import noUselessMustaches from "../rules/no-useless-mustaches"
1314
import preferClassDirective from "../rules/prefer-class-directive"
@@ -25,6 +26,7 @@ export const rules = [
2526
noAtHtmlTags,
2627
noDupeElseIfBlocks,
2728
noInnerDeclarations,
29+
noObjectInTextMustaches,
2830
noTargetBlank,
2931
noUselessMustaches,
3032
preferClassDirective,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"message": "Unexpected array in text mustache interpolation.",
4+
"line": 5,
5+
"column": 1
6+
},
7+
{
8+
"message": "Unexpected array in text mustache interpolation.",
9+
"line": 6,
10+
"column": 15
11+
}
12+
]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
let a = "hello!"
3+
</script>
4+
5+
{[a]}
6+
<input class="{[a]} a" />
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"message": "Unexpected class in text mustache interpolation.",
4+
"line": 4,
5+
"column": 1
6+
},
7+
{
8+
"message": "Unexpected class in text mustache interpolation.",
9+
"line": 6,
10+
"column": 15
11+
}
12+
]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
</script>
3+
4+
{class A {}}
5+
6+
<input class="{class B {}} a" />
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[
2+
{
3+
"message": "Unexpected function in text mustache interpolation.",
4+
"line": 5,
5+
"column": 1
6+
},
7+
{
8+
"message": "Unexpected function in text mustache interpolation.",
9+
"line": 6,
10+
"column": 1
11+
},
12+
{
13+
"message": "Unexpected function in text mustache interpolation.",
14+
"line": 9,
15+
"column": 15
16+
}
17+
]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
let a = "hello!"
3+
</script>
4+
5+
{() => a}
6+
{function () {
7+
return a
8+
}}
9+
<input class="{() => a} a" />
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"message": "Unexpected object in text mustache interpolation.",
4+
"line": 5,
5+
"column": 1
6+
},
7+
{
8+
"message": "Unexpected object in text mustache interpolation.",
9+
"line": 6,
10+
"column": 15
11+
}
12+
]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
let a = "hello!"
3+
</script>
4+
5+
{{ a }}
6+
<input class="{{ a }} a" />
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
import MyComponent from "./MyComponent.svelte"
3+
let a = "hello!"
4+
</script>
5+
6+
<MyComponent prop={{ a }} />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let a = "hello!"
3+
</script>
4+
5+
{a}
6+
<input class="{a} a" />
7+
<input class:foo={{ a }} />
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../src/rules/no-object-in-text-mustaches"
3+
import { loadTestCases } from "../../utils/utils"
4+
5+
const tester = new RuleTester({
6+
parserOptions: {
7+
ecmaVersion: 2020,
8+
sourceType: "module",
9+
},
10+
})
11+
12+
tester.run(
13+
"no-object-in-text-mustaches",
14+
rule as any,
15+
loadTestCases("no-object-in-text-mustaches"),
16+
)

0 commit comments

Comments
 (0)