diff --git a/README.md b/README.md
index 1592d02a3..bcb32c91c 100644
--- a/README.md
+++ b/README.md
@@ -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 |
diff --git a/docs/rules/directive-interpolation-spacing.md b/docs/rules/directive-interpolation-spacing.md
new file mode 100644
index 000000000..27b065790
--- /dev/null
+++ b/docs/rules/directive-interpolation-spacing.md
@@ -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
+
+
+
+
+
+```
+
+:+1: Examples of **correct** code for this rule:
+
+```html
+
+
+
+
+```
+
+## :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
+
+
+```
+
+:+1: Examples of **correct** code for this rule:
+
+```html
+
+
+```
+
+### `"never"` - Expect no spaces between expression and curly braces / brackets.
+
+:-1: Examples of **incorrect** code for this rule:
+
+```html
+
+
+```
+
+:+1: Examples of **correct** code for this rule:
+
+```html
+
+
+```
diff --git a/lib/configs/strongly-recommended.js b/lib/configs/strongly-recommended.js
index cdd9d1528..4bba19b06 100644
--- a/lib/configs/strongly-recommended.js
+++ b/lib/configs/strongly-recommended.js
@@ -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',
diff --git a/lib/index.js b/lib/index.js
index fff64d396..0d72fa615 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -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'),
diff --git a/lib/rules/directive-interpolation-spacing.js b/lib/rules/directive-interpolation-spacing.js
new file mode 100644
index 000000000..063ca094c
--- /dev/null
+++ b/lib/rules/directive-interpolation-spacing.js
@@ -0,0 +1,248 @@
+/**
+ * @fileoverview enforce unified spacing in directive interpolations.
+ * @author Rafael 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]])
+ })
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/tests/lib/rules/directive-interpolation-spacing.js b/tests/lib/rules/directive-interpolation-spacing.js
new file mode 100644
index 000000000..1d781f2f3
--- /dev/null
+++ b/tests/lib/rules/directive-interpolation-spacing.js
@@ -0,0 +1,184 @@
+/**
+ * @fileoverview enforce unified spacing in directive interpolations.
+ * @author Rafael Milewski
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/directive-interpolation-spacing')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+ parser: 'vue-eslint-parser',
+ parserOptions: { ecmaVersion: 2015 }
+})
+
+ruleTester.run('directive-interpolation-spacing', rule, {
+
+ valid: [
+ ``,
+ ``,
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ ``,
+ ``,
+ ``,
+ ``,
+ {
+ code: ``,
+ options: ['never']
+ },
+ {
+ code: ``,
+ options: ['never']
+ }
+ ],
+
+ invalid: [
+ {
+ code: ``,
+ output: ``,
+ options: ['always'],
+ errors: [
+ `Expected 1 space before '}', but not found.`
+ ]
+ },
+ {
+ code: '',
+ output: '',
+ options: ['always'],
+ errors: [
+ `Expected 1 space after '{', but not found.`,
+ `Expected 1 space before '}', but not found.`
+ ]
+ },
+ {
+ code: '',
+ output: '',
+ options: ['always'],
+ errors: [
+ `Expected no space before '{', but found.`,
+ `Expected no space after '}', but found.`
+ ]
+ },
+ {
+ code: '',
+ output: '',
+ options: ['always'],
+ errors: [
+ `Expected no space before ',', but found.`,
+ `Expected 1 space after ':', but not found.`
+ ]
+ },
+ {
+ code: '',
+ output: '',
+ options: ['always'],
+ errors: [
+ `Expected 1 space after ',', but not found.`
+ ]
+ },
+ {
+ code: '',
+ output: '',
+ options: ['always'],
+ errors: [
+ `Expected 1 space before '?', but not found.`,
+ `Expected 1 space after '?', but not found.`,
+ `Expected 1 space after ':', but not found.`
+ ]
+ },
+ {
+ code: '',
+ output: '',
+ options: ['always'],
+ errors: [
+ `Expected 1 space before '{', but not found.`
+ ]
+ },
+ {
+ code: '',
+ output: '',
+ options: ['always'],
+ errors: [
+ `Expected 1 space before '{', but not found.`,
+ `Expected 1 space after '}', but not found.`
+ ]
+ },
+ {
+ code: '',
+ output: '',
+ options: ['always'],
+ errors: [
+ `Expected no space before '[', but found.`,
+ `Expected no space after ']', but found.`
+ ]
+ },
+ {
+ code: '',
+ output: '',
+ options: ['always'],
+ errors: [
+ `Expected 1 space after '[', but not found.`,
+ `Expected 1 space after ',', but not found.`,
+ `Expected 1 space after ',', but not found.`,
+ `Expected 1 space before ']', but not found.`
+ ]
+ },
+ /**
+ * Options: never
+ */
+ {
+ code: '',
+ output: '',
+ options: ['never'],
+ errors: [
+ `Expected no space after '{', but found.`,
+ `Expected no space before '}', but found.`
+ ]
+ },
+ {
+ code: '',
+ output: '',
+ options: ['never'],
+ errors: [
+ `Expected 1 space after ':', but not found.`,
+ `Expected 1 space after ',', but not found.`,
+ `Expected 1 space after ':', but not found.`
+ ]
+ },
+ {
+ code: '',
+ output: '',
+ options: ['never'],
+ errors: [
+ `Expected 1 space before '{', but not found.`,
+ `Expected no space after '{', but found.`,
+ `Expected no space before '}', but found.`,
+ `Expected 1 space after '}', but not found.`
+ ]
+ },
+ {
+ code: '',
+ output: '',
+ options: ['never'],
+ errors: [
+ `Expected no space after '[', but found.`,
+ `Expected no space before ']', but found.`
+ ]
+ }
+ ]
+})