Skip to content

Commit 9c4e207

Browse files
authored
Add shorthand-directive rule (#99)
1 parent 970bee7 commit 9c4e207

24 files changed

+466
-4
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
286286
| [@ota-meshi/svelte/prefer-class-directive](https://ota-meshi.github.io/eslint-plugin-svelte/rules/prefer-class-directive/) | require class directives instead of ternary expressions | :wrench: |
287287
| [@ota-meshi/svelte/prefer-style-directive](https://ota-meshi.github.io/eslint-plugin-svelte/rules/prefer-style-directive/) | require style directives instead of style attribute | :wrench: |
288288
| [@ota-meshi/svelte/shorthand-attribute](https://ota-meshi.github.io/eslint-plugin-svelte/rules/shorthand-attribute/) | enforce use of shorthand syntax in attribute | :wrench: |
289+
| [@ota-meshi/svelte/shorthand-directive](https://ota-meshi.github.io/eslint-plugin-svelte/rules/shorthand-directive/) | enforce use of shorthand syntax in directives | :wrench: |
289290
| [@ota-meshi/svelte/spaced-html-comment](https://ota-meshi.github.io/eslint-plugin-svelte/rules/spaced-html-comment/) | enforce consistent spacing after the `<!--` and before the `-->` in a HTML comment | :wrench: |
290291

291292
## Extension Rules

docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
5555
| [@ota-meshi/svelte/prefer-class-directive](./rules/prefer-class-directive.md) | require class directives instead of ternary expressions | :wrench: |
5656
| [@ota-meshi/svelte/prefer-style-directive](./rules/prefer-style-directive.md) | require style directives instead of style attribute | :wrench: |
5757
| [@ota-meshi/svelte/shorthand-attribute](./rules/shorthand-attribute.md) | enforce use of shorthand syntax in attribute | :wrench: |
58+
| [@ota-meshi/svelte/shorthand-directive](./rules/shorthand-directive.md) | enforce use of shorthand syntax in directives | :wrench: |
5859
| [@ota-meshi/svelte/spaced-html-comment](./rules/spaced-html-comment.md) | enforce consistent spacing after the `<!--` and before the `-->` in a HTML comment | :wrench: |
5960

6061
## Extension Rules

docs/rules/shorthand-attribute.md

+6
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ This rule enforces the use of the shorthand syntax in attribute.
5454
- `"always"` ... Expects that the shorthand will be used whenever possible. This is default.
5555
- `"never"` ... Ensures that no shorthand is used in any attribute.
5656

57+
## :couple: Related Rules
58+
59+
- [@ota-meshi/svelte/shorthand-directive]
60+
61+
[@ota-meshi/svelte/shorthand-directive]: ./shorthand-directive.md
62+
5763
## :rocket: Version
5864

5965
This rule was introduced in @ota-meshi/eslint-plugin-svelte v0.5.0

docs/rules/shorthand-directive.md

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "@ota-meshi/svelte/shorthand-directive"
5+
description: "enforce use of shorthand syntax in directives"
6+
---
7+
8+
# @ota-meshi/svelte/shorthand-directive
9+
10+
> enforce use of shorthand syntax in directives
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+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
14+
15+
## :book: Rule Details
16+
17+
This rule enforces the use of the shorthand syntax in directives.
18+
19+
<ESLintCodeBlock fix>
20+
21+
<!-- prettier-ignore-start -->
22+
<!--eslint-skip-->
23+
24+
```svelte
25+
<script>
26+
/* eslint @ota-meshi/svelte/shorthand-directive: "error" */
27+
let value = 'hello!'
28+
let active = true
29+
let color = 'red'
30+
</script>
31+
32+
<!-- ✓ GOOD -->
33+
<input bind:value>
34+
<div class:active>...</div>
35+
<div style:color>...</div>
36+
37+
<!-- ✗ BAD -->
38+
<input bind:value={value}>
39+
<div class:active={active}>...</div>
40+
<div style:color={color}>...</div>
41+
```
42+
43+
<!-- prettier-ignore-end -->
44+
45+
</ESLintCodeBlock>
46+
47+
## :wrench: Options
48+
49+
```json
50+
{
51+
"@ota-meshi/svelte/shorthand-directive": [
52+
"error",
53+
{
54+
"prefer": "always" // "never"
55+
}
56+
]
57+
}
58+
```
59+
60+
- `prefer`
61+
- `"always"` ... Expects that the shorthand will be used whenever possible. This is default.
62+
- `"never"` ... Ensures that no shorthand is used in any directive.
63+
64+
## :couple: Related Rules
65+
66+
- [@ota-meshi/svelte/shorthand-attribute]
67+
68+
[@ota-meshi/svelte/shorthand-attribute]: ./shorthand-directive.md
69+
70+
## :mag: Implementation
71+
72+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/shorthand-directive.ts)
73+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/shorthand-directive.ts)

src/rules/shorthand-directive.ts

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type { AST } from "svelte-eslint-parser"
2+
import { createRule } from "../utils"
3+
import { getAttributeValueQuoteAndRange } from "../utils/ast-utils"
4+
5+
export default createRule("shorthand-directive", {
6+
meta: {
7+
docs: {
8+
description: "enforce use of shorthand syntax in directives",
9+
category: "Stylistic Issues",
10+
recommended: false,
11+
},
12+
fixable: "code",
13+
schema: [
14+
{
15+
type: "object",
16+
properties: {
17+
prefer: { enum: ["always", "never"] },
18+
},
19+
additionalProperties: false,
20+
},
21+
],
22+
messages: {
23+
expectedShorthand: "Expected shorthand directive.",
24+
expectedRegular: "Expected regular directive syntax.",
25+
},
26+
type: "layout",
27+
},
28+
create(context) {
29+
const sourceCode = context.getSourceCode()
30+
const always: boolean = context.options[0]?.prefer !== "never"
31+
32+
/** Report for always */
33+
function reportForAlways(
34+
node: AST.SvelteDirective | AST.SvelteStyleDirective,
35+
) {
36+
context.report({
37+
node,
38+
messageId: "expectedShorthand",
39+
*fix(fixer) {
40+
const quoteAndRange = getAttributeValueQuoteAndRange(node, sourceCode)
41+
if (quoteAndRange) {
42+
yield fixer.remove(
43+
sourceCode.getTokenBefore(quoteAndRange.firstToken)!,
44+
)
45+
yield fixer.removeRange(quoteAndRange.range)
46+
}
47+
},
48+
})
49+
}
50+
51+
/** Report for never */
52+
function reportForNever(
53+
node: AST.SvelteDirective | AST.SvelteStyleDirective,
54+
) {
55+
context.report({
56+
node,
57+
messageId: "expectedRegular",
58+
*fix(fixer) {
59+
yield fixer.insertTextAfter(node.key.name, `={${node.key.name.name}}`)
60+
},
61+
})
62+
}
63+
64+
return {
65+
SvelteDirective(node) {
66+
if (node.kind !== "Binding" && node.kind !== "Class") {
67+
return
68+
}
69+
70+
const expression = node.expression
71+
if (!expression) {
72+
// Invalid ?
73+
return
74+
}
75+
if (
76+
expression.type !== "Identifier" ||
77+
node.key.name.name !== expression.name
78+
) {
79+
// Cannot use shorthand
80+
return
81+
}
82+
if (always) {
83+
if (node.key.name.range![0] === expression.range![0]) {
84+
// Use shorthand
85+
return
86+
}
87+
reportForAlways(node)
88+
} else {
89+
if (node.key.name.range![1] < expression.range![0]) {
90+
// Use longform
91+
return
92+
}
93+
reportForNever(node)
94+
}
95+
},
96+
SvelteStyleDirective(node) {
97+
if (always) {
98+
if (node.shorthand) {
99+
// Use shorthand
100+
return
101+
}
102+
if (node.value.length !== 1) {
103+
// Cannot use shorthand
104+
return
105+
}
106+
const expression = node.value[0]
107+
if (
108+
expression.type !== "SvelteMustacheTag" ||
109+
expression.expression.type !== "Identifier" ||
110+
expression.expression.name !== node.key.name.name
111+
) {
112+
// Cannot use shorthand
113+
return
114+
}
115+
reportForAlways(node)
116+
} else {
117+
if (!node.shorthand) {
118+
// Use longform
119+
return
120+
}
121+
reportForNever(node)
122+
}
123+
},
124+
}
125+
},
126+
})

src/utils/ast-utils.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ export function getScope(
233233
export type QuoteAndRange = {
234234
quote: "unquoted" | "double" | "single"
235235
range: [number, number]
236+
firstToken: SvAST.Token | SvAST.Comment
237+
lastToken: SvAST.Token | SvAST.Comment
236238
}
237239
/** Get the quote and range from given attribute values */
238240
export function getAttributeValueQuoteAndRange(
@@ -262,6 +264,8 @@ export function getAttributeValueQuoteAndRange(
262264
return {
263265
quote: "unquoted",
264266
range: [valueFirstToken.range[0], valueLastToken.range[1]],
267+
firstToken: valueFirstToken,
268+
lastToken: valueLastToken,
265269
}
266270
} else if (
267271
beforeTokens.length > 1 ||
@@ -280,6 +284,8 @@ export function getAttributeValueQuoteAndRange(
280284
return {
281285
quote: beforeToken.value === '"' ? "double" : "single",
282286
range: [beforeToken.range[0], afterToken.range[1]],
287+
firstToken: beforeToken,
288+
lastToken: afterToken,
283289
}
284290
}
285291
export function getMustacheTokens(
@@ -381,11 +387,11 @@ function getAttributeValueRangeTokens(
381387
if (!attr.value.length) {
382388
return null
383389
}
384-
const firstToken = attr.value[0]
385-
const lastToken = attr.value[attr.value.length - 1]
390+
const first = attr.value[0]
391+
const last = attr.value[attr.value.length - 1]
386392
return {
387-
firstToken,
388-
lastToken,
393+
firstToken: sourceCode.getFirstToken(first),
394+
lastToken: sourceCode.getLastToken(last),
389395
}
390396
}
391397
const tokens = getMustacheTokens(attr, sourceCode)

src/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import noUselessMustaches from "../rules/no-useless-mustaches"
2020
import preferClassDirective from "../rules/prefer-class-directive"
2121
import preferStyleDirective from "../rules/prefer-style-directive"
2222
import shorthandAttribute from "../rules/shorthand-attribute"
23+
import shorthandDirective from "../rules/shorthand-directive"
2324
import spacedHtmlComment from "../rules/spaced-html-comment"
2425
import system from "../rules/system"
2526
import validCompile from "../rules/valid-compile"
@@ -46,6 +47,7 @@ export const rules = [
4647
preferClassDirective,
4748
preferStyleDirective,
4849
shorthandAttribute,
50+
shorthandDirective,
4951
spacedHtmlComment,
5052
system,
5153
validCompile,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"options": [{ "prefer": "always" }]
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[
2+
{
3+
"message": "Expected shorthand directive.",
4+
"line": 14,
5+
"column": 8
6+
},
7+
{
8+
"message": "Expected shorthand directive.",
9+
"line": 16,
10+
"column": 6
11+
},
12+
{
13+
"message": "Expected shorthand directive.",
14+
"line": 18,
15+
"column": 6
16+
},
17+
{
18+
"message": "Expected shorthand directive.",
19+
"line": 21,
20+
"column": 6
21+
},
22+
{
23+
"message": "Expected shorthand directive.",
24+
"line": 23,
25+
"column": 6
26+
}
27+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script>
2+
let value = "hello!"
3+
let active = true
4+
let color = "red"
5+
</script>
6+
7+
<!-- ✓ GOOD -->
8+
<input bind:value />
9+
<div class:active>...</div>
10+
<div style:color>...</div>
11+
12+
<!-- ✗ BAD -->
13+
<!-- prettier-ignore -->
14+
<input bind:value={value}>
15+
<!-- prettier-ignore -->
16+
<div class:active={active}>...</div>
17+
<!-- prettier-ignore -->
18+
<div style:color={color}>...</div>
19+
20+
<!-- prettier-ignore -->
21+
<div class:active="{active}">...</div>
22+
<!-- prettier-ignore -->
23+
<div style:color="{color}">...</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script>
2+
let value = "hello!"
3+
let active = true
4+
let color = "red"
5+
</script>
6+
7+
<!-- ✓ GOOD -->
8+
<input bind:value />
9+
<div class:active>...</div>
10+
<div style:color>...</div>
11+
12+
<!-- ✗ BAD -->
13+
<!-- prettier-ignore -->
14+
<input bind:value>
15+
<!-- prettier-ignore -->
16+
<div class:active>...</div>
17+
<!-- prettier-ignore -->
18+
<div style:color>...</div>
19+
20+
<!-- prettier-ignore -->
21+
<div class:active>...</div>
22+
<!-- prettier-ignore -->
23+
<div style:color>...</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"options": [{ "prefer": "never" }]
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[
2+
{
3+
"message": "Expected regular directive syntax.",
4+
"line": 8,
5+
"column": 8
6+
},
7+
{
8+
"message": "Expected regular directive syntax.",
9+
"line": 9,
10+
"column": 6
11+
},
12+
{
13+
"message": "Expected regular directive syntax.",
14+
"line": 10,
15+
"column": 6
16+
}
17+
]

0 commit comments

Comments
 (0)