Skip to content

⭐️New: Add directive-interpolation-spacing rule #599

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
| | Rule ID | Description |
|:---|:--------|:------------|
| :wrench: | [vue/attribute-hyphenation](./docs/rules/attribute-hyphenation.md) | enforce attribute naming style on custom components in template |
| :wrench: | [vue/directive-interpolation-spacing](./docs/rules/directive-interpolation-spacing.md) | enforce unified spacing in mustache interpolations within directive expressions |
| :wrench: | [vue/html-closing-bracket-newline](./docs/rules/html-closing-bracket-newline.md) | require or disallow a line break before tag's closing brackets |
| :wrench: | [vue/html-closing-bracket-spacing](./docs/rules/html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets |
| :wrench: | [vue/html-end-tags](./docs/rules/html-end-tags.md) | enforce end tag style |
Expand Down
67 changes: 67 additions & 0 deletions docs/rules/directive-interpolation-spacing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# enforce unified spacing in mustache interpolations within directive expressions (vue/directive-interpolation-spacing)

- :gear: This rule is included in `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.
- :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.

## :book: Rule Details

This rule aims to enforce unified spacing in directive interpolations.

:-1: Examples of **incorrect** code for this rule:

```html
<div :property="{key:value}"></div>
<div :property="{ key:value }"></div>
<div :property=" { key:value } "></div>
<div :property="{ [expression]:value,[expression]:value }"></div>
<div :property="[1,2,3]"></div>
```

:+1: Examples of **correct** code for this rule:

```html
<div :property="{ key: value }"></div>
<div :property="{ [expression]: value }"></div>
<div :property="{ [expression]: value, [expression]: value }"></div>
<div :property="[ 1, 2, 3 ]"></div>
```

## :wrench: Options

Default spacing is set to `always`

```
'vue/directive-interpolation-spacing': [2, 'always'|'never']
```

### `"always"` - Expect one space between expression and curly braces / brackets.

:-1: Examples of **incorrect** code for this rule:

```html
<div :class="{key:value,key:value}"></div>
<div :class="[1,2]"></div>
```

:+1: Examples of **correct** code for this rule:

```html
<div :class="{ key: value, key: value }"></div>
<div :class="[ 1, 2 ]"></div>
```

### `"never"` - Expect no spaces between expression and curly braces / brackets.

:-1: Examples of **incorrect** code for this rule:

```html
<div :class="{ key: value, key: value }"></div>
<div :class="[ 1, 2, 3 ]"></div>
```

:+1: Examples of **correct** code for this rule:

```html
<div :class="{key: value, key: value}"></div>
<div :class="[1, 2, 3]"></div>
```
1 change: 1 addition & 0 deletions lib/configs/strongly-recommended.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
extends: require.resolve('./essential'),
rules: {
'vue/attribute-hyphenation': 'error',
'vue/directive-interpolation-spacing': 'error',
'vue/html-closing-bracket-newline': 'error',
'vue/html-closing-bracket-spacing': 'error',
'vue/html-end-tags': 'error',
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = {
'attributes-order': require('./rules/attributes-order'),
'comment-directive': require('./rules/comment-directive'),
'component-name-in-template-casing': require('./rules/component-name-in-template-casing'),
'directive-interpolation-spacing': require('./rules/directive-interpolation-spacing'),
'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'),
'html-end-tags': require('./rules/html-end-tags'),
Expand Down
248 changes: 248 additions & 0 deletions lib/rules/directive-interpolation-spacing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
/**
* @fileoverview enforce unified spacing in directive interpolations.
* @author Rafael Milewski <https://github.com/milewski>
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('../utils')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

function isOpenBrace (token) {
return token.type === 'Punctuator' && (token.value === '{' || token.value === '[')
}

function isCloseBrace (token, openBrace) {
if (token.type !== 'Punctuator') {
return false
}

if (openBrace) {
return { '[': ']', '{': '}' }[openBrace.value] === token.value
}

return (token.value === '}' || token.value === ']')
}

function isEndOf (punctuator, token) {
return punctuator.value !== ',' && token.value !== ']' && token.type !== 'Identifier' && token.type !== 'Numeric'
}

function getOpenAndCloseBraces (node, tokens) {
let root = tokens.getFirstToken(node)
let openBrace, closeBrace

while (true) {
root = tokens.getTokenAfter(root)

if (!root) {
return
}

if (!openBrace && isOpenBrace(root)) {
openBrace = root
} else if (isCloseBrace(root, openBrace)) {
closeBrace = root
}

if (openBrace && closeBrace) {
return { openBrace, closeBrace }
}
}
}

module.exports = {
meta: {
docs: {
description: 'enforce unified spacing in directive interpolations',
category: 'strongly-recommended',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.3/docs/rules/directive-interpolation-spacing.md'
},
fixable: 'whitespace',
schema: [{ enum: ['always', 'never'] }]
},

create (context) {
const options = context.options[0] || 'always'
const template =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()

// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------

return utils.defineTemplateBodyVisitor(context, {
VDirectiveKey (node) {
const openAndCloseTokens = getOpenAndCloseBraces(node, template)

/**
* If these are not present,
* somewhat it is an invalid syntax not possible to continue
*/
if (!openAndCloseTokens) {
return
}

const { openBrace, closeBrace } = openAndCloseTokens
const nextToken = template.getTokenAfter(openBrace)
const previousToken = template.getTokenBefore(closeBrace)

const punctuators = template.getTokensBetween(nextToken, previousToken).filter(({ value }) => (value === ':' || value === '?' || value === ','))

const firstToken = template.getTokenBefore(openBrace)
const lastToken = template.getTokenAfter(closeBrace)

/**
* Space out inner braces :class="{[+x][expression][+x]}"
*/
if (options === 'always') {
if (openBrace.range[0] === nextToken.range[0] - 1) {
context.report({
node: nextToken,
message: `Expected 1 space after '{{ displayValue }}', but not found.`,
data: {
displayValue: openBrace.value
},
fix: fixer => fixer.insertTextAfter(openBrace, ' ')
})
}
if (closeBrace.range[0] === previousToken.range[1]) {
context.report({
node: closeBrace,
message: `Expected 1 space before '{{ displayValue }}', but not found.`,
data: {
displayValue: closeBrace.value
},
fix: fixer => fixer.insertTextBefore(closeBrace, ' ')
})
}
} else {
if (openBrace.range[1] !== nextToken.range[0]) {
context.report({
node: openBrace,
loc: {
start: openBrace.loc.end,
end: openBrace.loc.start
},
message: `Expected no space after '{{ displayValue }}', but found.`,
data: {
displayValue: openBrace.value
},
fix: fixer => fixer.removeRange([openBrace.range[1], nextToken.range[0]])
})
}

if (closeBrace.range[0] !== previousToken.range[1]) {
context.report({
node: closeBrace,
message: `Expected no space before '{{ displayValue }}', but found.`,
data: {
displayValue: closeBrace.value
},
fix: fixer => fixer.removeRange([previousToken.range[1], closeBrace.range[0]])
})
}
}

/**
* Remove spaces from outer braces :class="[-x]{ [expression] }[-x]"
*/
if (firstToken.range[1] !== openBrace.range[0] && firstToken.value === '"') {
context.report({
node: firstToken,
loc: {
start: firstToken.loc.end,
end: firstToken.loc.start
},
message: `Expected no space before '{{ displayValue }}', but found.`,
data: {
displayValue: openBrace.value
},
fix: fixer => fixer.removeRange([firstToken.range[1], openBrace.range[0]])
})
} else if (firstToken.range[1] === openBrace.range[0] && firstToken.value !== '"') {
context.report({
node: openBrace,
message: `Expected 1 space before '{{ displayValue }}', but not found.`,
data: {
displayValue: openBrace.value
},
fix: fixer => fixer.insertTextAfter(firstToken, ' ')
})
}

if (lastToken.range[0] !== closeBrace.range[1] && lastToken.value === '"') {
context.report({
node: lastToken,
message: `Expected no space after '{{ displayValue }}', but found.`,
data: {
displayValue: closeBrace.value
},
fix: fixer => fixer.removeRange([closeBrace.range[1], lastToken.range[0]])
})
} else if (lastToken.range[0] === closeBrace.range[1] && lastToken.value !== '"') {
context.report({
node: lastToken,
message: `Expected 1 space after '{{ displayValue }}', but not found.`,
data: {
displayValue: closeBrace.value
},
fix: fixer => fixer.insertTextBefore(lastToken, ' ')
})
}

/**
* Space out every Punctuator[:?] :class="{ [key][-x]:[+x][expression] }"
*/
for (const punctuator of punctuators) {
const nextToken = template.getTokenAfter(punctuator)
const previousToken = template.getTokenBefore(punctuator)

if (punctuator.range[1] === nextToken.range[0]) {
context.report({
node: punctuator,
loc: {
start: punctuator.loc.end,
end: punctuator.loc.start
},
message: `Expected 1 space after '{{ displayValue }}', but not found.`,
data: {
displayValue: punctuator.value
},
fix: fixer => fixer.insertTextAfter(punctuator, ' ')
})
}

if (punctuator.range[0] === previousToken.range[1] && isEndOf(punctuator, previousToken)) {
context.report({
node: punctuator,
message: `Expected 1 space before '{{ displayValue }}', but not found.`,
data: {
displayValue: punctuator.value
},
fix: fixer => fixer.insertTextBefore(punctuator, ' ')
})
}

if (previousToken.range[1] !== punctuator.range[0] && !isEndOf(punctuator, previousToken)) {
context.report({
node: punctuator,
message: `Expected no space before '{{ displayValue }}', but found.`,
data: {
displayValue: punctuator.value
},
fix: fixer => fixer.removeRange([previousToken.range[1], punctuator.range[0]])
})
}
}
}
})
}
}
Loading