From fdad618c94a4ec71ee580661cabd761174660e90 Mon Sep 17 00:00:00 2001 From: milewski Date: Sun, 7 Oct 2018 19:23:31 +0800 Subject: [PATCH 1/2] add directive-interpolation-spacing rule --- README.md | 1 + docs/rules/directive-interpolation-spacing.md | 61 +++++ lib/configs/strongly-recommended.js | 1 + lib/index.js | 1 + lib/rules/directive-interpolation-spacing.js | 216 ++++++++++++++++++ .../rules/directive-interpolation-spacing.js | 149 ++++++++++++ 6 files changed, 429 insertions(+) create mode 100644 docs/rules/directive-interpolation-spacing.md create mode 100644 lib/rules/directive-interpolation-spacing.js create mode 100644 tests/lib/rules/directive-interpolation-spacing.js 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..3c7dcf8d9 --- /dev/null +++ b/docs/rules/directive-interpolation-spacing.md @@ -0,0 +1,61 @@ +# 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. + +:-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. + +:-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..2b788549e --- /dev/null +++ b/lib/rules/directive-interpolation-spacing.js @@ -0,0 +1,216 @@ +/** + * @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 === '{' +} + +function isCloseBrace (token) { + return token.type === 'Punctuator' && 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 (isOpenBrace(root)) { + openBrace = root + } else if (isCloseBrace(root)) { + 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 '{', but not found.`, + fix: fixer => fixer.insertTextAfter(openBrace, ' ') + }) + } + if (closeBrace.range[0] === previousToken.range[1]) { + context.report({ + node: closeBrace, + message: `Expected 1 space before '}', but not found.`, + 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 '{', but found.`, + 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 '}', but found.`, + 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 '{', but found.`, + 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 '{', but not found.`, + fix: fixer => fixer.insertTextAfter(firstToken, ' ') + }) + } + + if (lastToken.range[0] !== closeBrace.range[1] && lastToken.value === '"') { + context.report({ + node: lastToken, + message: `Expected no space after '}', but found.`, + 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 '}', but not found.`, + 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..48ba5ee8b --- /dev/null +++ b/tests/lib/rules/directive-interpolation-spacing.js @@ -0,0 +1,149 @@ +/** + * @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'] + } + ], + + 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.` + ] + }, + + /** + * 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.` + ] + } + ] +}) From 022846a178eed0f204bc8e6ea89e3ac937ce2f74 Mon Sep 17 00:00:00 2001 From: milewski Date: Sun, 7 Oct 2018 22:51:31 +0800 Subject: [PATCH 2/2] include support for :property="[]" --- docs/rules/directive-interpolation-spacing.md | 18 ++++-- lib/rules/directive-interpolation-spacing.js | 58 ++++++++++++++----- .../rules/directive-interpolation-spacing.js | 37 +++++++++++- 3 files changed, 93 insertions(+), 20 deletions(-) diff --git a/docs/rules/directive-interpolation-spacing.md b/docs/rules/directive-interpolation-spacing.md index 3c7dcf8d9..27b065790 100644 --- a/docs/rules/directive-interpolation-spacing.md +++ b/docs/rules/directive-interpolation-spacing.md @@ -14,6 +14,7 @@ This rule aims to enforce unified spacing in directive interpolations.
+
``` :+1: Examples of **correct** code for this rule: @@ -22,6 +23,7 @@ This rule aims to enforce unified spacing in directive interpolations.
+
``` ## :wrench: Options @@ -32,30 +34,34 @@ Default spacing is set to `always` 'vue/directive-interpolation-spacing': [2, 'always'|'never'] ``` -### `"always"` - Expect one space between expression and curly braces. +### `"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. +### `"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/rules/directive-interpolation-spacing.js b/lib/rules/directive-interpolation-spacing.js index 2b788549e..063ca094c 100644 --- a/lib/rules/directive-interpolation-spacing.js +++ b/lib/rules/directive-interpolation-spacing.js @@ -15,11 +15,19 @@ const utils = require('../utils') // ------------------------------------------------------------------------------ function isOpenBrace (token) { - return token.type === 'Punctuator' && token.value === '{' + return token.type === 'Punctuator' && (token.value === '{' || token.value === '[') } -function isCloseBrace (token) { - return token.type === 'Punctuator' && 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) { @@ -37,9 +45,9 @@ function getOpenAndCloseBraces (node, tokens) { return } - if (isOpenBrace(root)) { + if (!openBrace && isOpenBrace(root)) { openBrace = root - } else if (isCloseBrace(root)) { + } else if (isCloseBrace(root, openBrace)) { closeBrace = root } @@ -98,14 +106,20 @@ module.exports = { if (openBrace.range[0] === nextToken.range[0] - 1) { context.report({ node: nextToken, - message: `Expected 1 space after '{', but not found.`, + 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 '}', but not found.`, + message: `Expected 1 space before '{{ displayValue }}', but not found.`, + data: { + displayValue: closeBrace.value + }, fix: fixer => fixer.insertTextBefore(closeBrace, ' ') }) } @@ -117,7 +131,10 @@ module.exports = { start: openBrace.loc.end, end: openBrace.loc.start }, - message: `Expected no space after '{', but found.`, + message: `Expected no space after '{{ displayValue }}', but found.`, + data: { + displayValue: openBrace.value + }, fix: fixer => fixer.removeRange([openBrace.range[1], nextToken.range[0]]) }) } @@ -125,7 +142,10 @@ module.exports = { if (closeBrace.range[0] !== previousToken.range[1]) { context.report({ node: closeBrace, - message: `Expected no space before '}', but found.`, + message: `Expected no space before '{{ displayValue }}', but found.`, + data: { + displayValue: closeBrace.value + }, fix: fixer => fixer.removeRange([previousToken.range[1], closeBrace.range[0]]) }) } @@ -141,13 +161,19 @@ module.exports = { start: firstToken.loc.end, end: firstToken.loc.start }, - message: `Expected no space before '{', but found.`, + 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 '{', but not found.`, + message: `Expected 1 space before '{{ displayValue }}', but not found.`, + data: { + displayValue: openBrace.value + }, fix: fixer => fixer.insertTextAfter(firstToken, ' ') }) } @@ -155,13 +181,19 @@ module.exports = { if (lastToken.range[0] !== closeBrace.range[1] && lastToken.value === '"') { context.report({ node: lastToken, - message: `Expected no space after '}', but found.`, + 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 '}', but not found.`, + message: `Expected 1 space after '{{ displayValue }}', but not found.`, + data: { + displayValue: closeBrace.value + }, fix: fixer => fixer.insertTextBefore(lastToken, ' ') }) } diff --git a/tests/lib/rules/directive-interpolation-spacing.js b/tests/lib/rules/directive-interpolation-spacing.js index 48ba5ee8b..1d781f2f3 100644 --- a/tests/lib/rules/directive-interpolation-spacing.js +++ b/tests/lib/rules/directive-interpolation-spacing.js @@ -25,6 +25,9 @@ ruleTester.run('directive-interpolation-spacing', rule, { valid: [ ``, ``, + '', + '', + '', '', '', '', @@ -37,6 +40,10 @@ ruleTester.run('directive-interpolation-spacing', rule, { { code: ``, options: ['never'] + }, + { + code: ``, + options: ['never'] } ], @@ -111,7 +118,26 @@ ruleTester.run('directive-interpolation-spacing', rule, { `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 */ @@ -144,6 +170,15 @@ ruleTester.run('directive-interpolation-spacing', rule, { `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.` + ] } ] })