Skip to content

Commit fdad618

Browse files
committed
add directive-interpolation-spacing rule
1 parent 3ccf3ef commit fdad618

File tree

6 files changed

+429
-0
lines changed

6 files changed

+429
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
193193
| | Rule ID | Description |
194194
|:---|:--------|:------------|
195195
| :wrench: | [vue/attribute-hyphenation](./docs/rules/attribute-hyphenation.md) | enforce attribute naming style on custom components in template |
196+
| :wrench: | [vue/directive-interpolation-spacing](./docs/rules/directive-interpolation-spacing.md) | enforce unified spacing in mustache interpolations within directive expressions |
196197
| :wrench: | [vue/html-closing-bracket-newline](./docs/rules/html-closing-bracket-newline.md) | require or disallow a line break before tag's closing brackets |
197198
| :wrench: | [vue/html-closing-bracket-spacing](./docs/rules/html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets |
198199
| :wrench: | [vue/html-end-tags](./docs/rules/html-end-tags.md) | enforce end tag style |
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# enforce unified spacing in mustache interpolations within directive expressions (vue/directive-interpolation-spacing)
2+
3+
- :gear: This rule is included in `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.
4+
- :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.
5+
6+
## :book: Rule Details
7+
8+
This rule aims to enforce unified spacing in directive interpolations.
9+
10+
:-1: Examples of **incorrect** code for this rule:
11+
12+
```html
13+
<div :property="{key:value}"></div>
14+
<div :property="{ key:value }"></div>
15+
<div :property=" { key:value } "></div>
16+
<div :property="{ [expression]:value,[expression]:value }"></div>
17+
```
18+
19+
:+1: Examples of **correct** code for this rule:
20+
21+
```html
22+
<div :property="{ key: value }"></div>
23+
<div :property="{ [expression]: value }"></div>
24+
<div :property="{ [expression]: value, [expression]: value }"></div>
25+
```
26+
27+
## :wrench: Options
28+
29+
Default spacing is set to `always`
30+
31+
```
32+
'vue/directive-interpolation-spacing': [2, 'always'|'never']
33+
```
34+
35+
### `"always"` - Expect one space between expression and curly braces.
36+
37+
:-1: Examples of **incorrect** code for this rule:
38+
39+
```html
40+
<div :class="{key:value}"></div>
41+
```
42+
43+
:+1: Examples of **correct** code for this rule:
44+
45+
```html
46+
<div :class="{ key: value }"></div>
47+
```
48+
49+
### `"never"` - Expect no spaces between expression and curly braces.
50+
51+
:-1: Examples of **incorrect** code for this rule:
52+
53+
```html
54+
<div :class="{ key: value }"></div>
55+
```
56+
57+
:+1: Examples of **correct** code for this rule:
58+
59+
```html
60+
<div :class="{key: value}"></div>
61+
```

lib/configs/strongly-recommended.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module.exports = {
77
extends: require.resolve('./essential'),
88
rules: {
99
'vue/attribute-hyphenation': 'error',
10+
'vue/directive-interpolation-spacing': 'error',
1011
'vue/html-closing-bracket-newline': 'error',
1112
'vue/html-closing-bracket-spacing': 'error',
1213
'vue/html-end-tags': 'error',

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = {
1111
'attributes-order': require('./rules/attributes-order'),
1212
'comment-directive': require('./rules/comment-directive'),
1313
'component-name-in-template-casing': require('./rules/component-name-in-template-casing'),
14+
'directive-interpolation-spacing': require('./rules/directive-interpolation-spacing'),
1415
'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
1516
'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'),
1617
'html-end-tags': require('./rules/html-end-tags'),
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/**
2+
* @fileoverview enforce unified spacing in directive interpolations.
3+
* @author Rafael Milewski <https://github.com/milewski>
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
13+
// ------------------------------------------------------------------------------
14+
// Rule Definition
15+
// ------------------------------------------------------------------------------
16+
17+
function isOpenBrace (token) {
18+
return token.type === 'Punctuator' && token.value === '{'
19+
}
20+
21+
function isCloseBrace (token) {
22+
return token.type === 'Punctuator' && token.value === '}'
23+
}
24+
25+
function isEndOf (punctuator, token) {
26+
return punctuator.value !== ',' && token.value !== ']' && token.type !== 'Identifier' && token.type !== 'Numeric'
27+
}
28+
29+
function getOpenAndCloseBraces (node, tokens) {
30+
let root = tokens.getFirstToken(node)
31+
let openBrace, closeBrace
32+
33+
while (true) {
34+
root = tokens.getTokenAfter(root)
35+
36+
if (!root) {
37+
return
38+
}
39+
40+
if (isOpenBrace(root)) {
41+
openBrace = root
42+
} else if (isCloseBrace(root)) {
43+
closeBrace = root
44+
}
45+
46+
if (openBrace && closeBrace) {
47+
return { openBrace, closeBrace }
48+
}
49+
}
50+
}
51+
52+
module.exports = {
53+
meta: {
54+
docs: {
55+
description: 'enforce unified spacing in directive interpolations',
56+
category: 'strongly-recommended',
57+
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.3/docs/rules/directive-interpolation-spacing.md'
58+
},
59+
fixable: 'whitespace',
60+
schema: [{ enum: ['always', 'never'] }]
61+
},
62+
63+
create (context) {
64+
const options = context.options[0] || 'always'
65+
const template =
66+
context.parserServices.getTemplateBodyTokenStore &&
67+
context.parserServices.getTemplateBodyTokenStore()
68+
69+
// ----------------------------------------------------------------------
70+
// Public
71+
// ----------------------------------------------------------------------
72+
73+
return utils.defineTemplateBodyVisitor(context, {
74+
VDirectiveKey (node) {
75+
const openAndCloseTokens = getOpenAndCloseBraces(node, template)
76+
77+
/**
78+
* If these are not present,
79+
* somewhat it is an invalid syntax not possible to continue
80+
*/
81+
if (!openAndCloseTokens) {
82+
return
83+
}
84+
85+
const { openBrace, closeBrace } = openAndCloseTokens
86+
const nextToken = template.getTokenAfter(openBrace)
87+
const previousToken = template.getTokenBefore(closeBrace)
88+
89+
const punctuators = template.getTokensBetween(nextToken, previousToken).filter(({ value }) => (value === ':' || value === '?' || value === ','))
90+
91+
const firstToken = template.getTokenBefore(openBrace)
92+
const lastToken = template.getTokenAfter(closeBrace)
93+
94+
/**
95+
* Space out inner braces :class="{[+x][expression][+x]}"
96+
*/
97+
if (options === 'always') {
98+
if (openBrace.range[0] === nextToken.range[0] - 1) {
99+
context.report({
100+
node: nextToken,
101+
message: `Expected 1 space after '{', but not found.`,
102+
fix: fixer => fixer.insertTextAfter(openBrace, ' ')
103+
})
104+
}
105+
if (closeBrace.range[0] === previousToken.range[1]) {
106+
context.report({
107+
node: closeBrace,
108+
message: `Expected 1 space before '}', but not found.`,
109+
fix: fixer => fixer.insertTextBefore(closeBrace, ' ')
110+
})
111+
}
112+
} else {
113+
if (openBrace.range[1] !== nextToken.range[0]) {
114+
context.report({
115+
node: openBrace,
116+
loc: {
117+
start: openBrace.loc.end,
118+
end: openBrace.loc.start
119+
},
120+
message: `Expected no space after '{', but found.`,
121+
fix: fixer => fixer.removeRange([openBrace.range[1], nextToken.range[0]])
122+
})
123+
}
124+
125+
if (closeBrace.range[0] !== previousToken.range[1]) {
126+
context.report({
127+
node: closeBrace,
128+
message: `Expected no space before '}', but found.`,
129+
fix: fixer => fixer.removeRange([previousToken.range[1], closeBrace.range[0]])
130+
})
131+
}
132+
}
133+
134+
/**
135+
* Remove spaces from outer braces :class="[-x]{ [expression] }[-x]"
136+
*/
137+
if (firstToken.range[1] !== openBrace.range[0] && firstToken.value === '"') {
138+
context.report({
139+
node: firstToken,
140+
loc: {
141+
start: firstToken.loc.end,
142+
end: firstToken.loc.start
143+
},
144+
message: `Expected no space before '{', but found.`,
145+
fix: fixer => fixer.removeRange([firstToken.range[1], openBrace.range[0]])
146+
})
147+
} else if (firstToken.range[1] === openBrace.range[0] && firstToken.value !== '"') {
148+
context.report({
149+
node: openBrace,
150+
message: `Expected 1 space before '{', but not found.`,
151+
fix: fixer => fixer.insertTextAfter(firstToken, ' ')
152+
})
153+
}
154+
155+
if (lastToken.range[0] !== closeBrace.range[1] && lastToken.value === '"') {
156+
context.report({
157+
node: lastToken,
158+
message: `Expected no space after '}', but found.`,
159+
fix: fixer => fixer.removeRange([closeBrace.range[1], lastToken.range[0]])
160+
})
161+
} else if (lastToken.range[0] === closeBrace.range[1] && lastToken.value !== '"') {
162+
context.report({
163+
node: lastToken,
164+
message: `Expected 1 space after '}', but not found.`,
165+
fix: fixer => fixer.insertTextBefore(lastToken, ' ')
166+
})
167+
}
168+
169+
/**
170+
* Space out every Punctuator[:?] :class="{ [key][-x]:[+x][expression] }"
171+
*/
172+
for (const punctuator of punctuators) {
173+
const nextToken = template.getTokenAfter(punctuator)
174+
const previousToken = template.getTokenBefore(punctuator)
175+
176+
if (punctuator.range[1] === nextToken.range[0]) {
177+
context.report({
178+
node: punctuator,
179+
loc: {
180+
start: punctuator.loc.end,
181+
end: punctuator.loc.start
182+
},
183+
message: `Expected 1 space after '{{ displayValue }}', but not found.`,
184+
data: {
185+
displayValue: punctuator.value
186+
},
187+
fix: fixer => fixer.insertTextAfter(punctuator, ' ')
188+
})
189+
}
190+
191+
if (punctuator.range[0] === previousToken.range[1] && isEndOf(punctuator, previousToken)) {
192+
context.report({
193+
node: punctuator,
194+
message: `Expected 1 space before '{{ displayValue }}', but not found.`,
195+
data: {
196+
displayValue: punctuator.value
197+
},
198+
fix: fixer => fixer.insertTextBefore(punctuator, ' ')
199+
})
200+
}
201+
202+
if (previousToken.range[1] !== punctuator.range[0] && !isEndOf(punctuator, previousToken)) {
203+
context.report({
204+
node: punctuator,
205+
message: `Expected no space before '{{ displayValue }}', but found.`,
206+
data: {
207+
displayValue: punctuator.value
208+
},
209+
fix: fixer => fixer.removeRange([previousToken.range[1], punctuator.range[0]])
210+
})
211+
}
212+
}
213+
}
214+
})
215+
}
216+
}

0 commit comments

Comments
 (0)