diff --git a/conf/eslint.json b/conf/eslint.json index fdb7a1879a2..95279c5cf42 100755 --- a/conf/eslint.json +++ b/conf/eslint.json @@ -194,6 +194,7 @@ "space-unary-ops": 0, "spaced-comment": 0, "strict": 0, + "template-curly-spacing": 0, "use-isnan": 2, "valid-jsdoc": 0, "valid-typeof": 2, diff --git a/docs/rules/README.md b/docs/rules/README.md index 90890cbb30e..092d6e157fe 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -236,6 +236,7 @@ These rules are only relevant to ES6 environments. * [prefer-spread](prefer-spread.md) - suggest using the spread operator instead of `.apply()`. * [prefer-template](prefer-template.md) - suggest using template literals instead of strings concatenation * [require-yield](require-yield.md) - disallow generator functions that do not have `yield` +* [template-curly-spacing](template-curly-spacing.md) - enforce spacing around embedded expressions of template strings (fixable) * [yield-star-spacing](yield-star-spacing.md) - enforce spacing around the `*` in `yield*` expressions (fixable) diff --git a/docs/rules/template-curly-spacing.md b/docs/rules/template-curly-spacing.md new file mode 100644 index 00000000000..adba54db189 --- /dev/null +++ b/docs/rules/template-curly-spacing.md @@ -0,0 +1,79 @@ +# Enforce Usage of Spacing in Template Strings (template-curly-spacing) + +We can embed expressions in template strings with using a pair of `${` and `}`. +This rule can force usage of spacing inside of the curly brace pair according to style guides. + +```js +let hello = `hello, ${people.name}!`; +``` + +**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line. + +## Rule Details + +This rule aims to maintain consistency around the spacing inside of template literals. + +### Options + +```json +{ + "template-curly-spacing": [2, "never"] +} +``` + +This rule has one option which has either `"never"` or `"always"` as value. + +* `"never"` (by default) - Disallows spaces inside of the curly brace pair. +* `"always"` - Requires one or more spaces inside of the curly brace pair. + +The following patterns are considered problems when configured `"never"`: + +```js +/*eslint template-curly-spacing: 2*/ + +`hello, ${ people.name}!`; /*error Unexpected space(s) after '${'.*/ +`hello, ${people.name }!`; /*error Unexpected space(s) before '}'.*/ + +`hello, ${ people.name }!`; /*error Unexpected space(s) after '${'.*/ + /*error Unexpected space(s) before '}'.*/ +``` + +The following patterns are considered problems when configured `"always"`: + +```js +/*eslint template-curly-spacing: [2, "always"]*/ + +`hello, ${ people.name}!`; /*error Expected space(s) before '}'.*/ +`hello, ${people.name }!`; /*error Expected space(s) after '${'.*/ + +`hello, ${people.name}!`; /*error Expected space(s) after '${'.*/ + /*error Expected space(s) before '}'.*/ +``` + +The following patterns are not considered problems when configured `"never"`: + +```js +/*eslint template-curly-spacing: 2*/ + +`hello, ${people.name}!`; + +`hello, ${ + people.name +}!`; +``` + +The following patterns are not considered problems when configured `"always"`: + +```js +/*eslint template-curly-spacing: [2, "always"]*/ + +`hello, ${ people.name }!`; + +`hello, ${ + people.name +}!`; +``` + +## When Not To Use It + +If you don't want to be notified about usage of spacing inside of template strings, then it's safe to disable this rule. diff --git a/lib/rules/template-curly-spacing.js b/lib/rules/template-curly-spacing.js new file mode 100644 index 00000000000..972e2daa28f --- /dev/null +++ b/lib/rules/template-curly-spacing.js @@ -0,0 +1,102 @@ +/** + * @fileoverview Rule to enforce spacing around embedded expressions of template strings + * @author Toru Nagashima + * @copyright 2016 Toru Nagashima. All rights reserved. + * See LICENSE file in root directory for full license. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +var OPEN_PAREN = /\$\{$/; +var CLOSE_PAREN = /^\}/; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + var sourceCode = context.getSourceCode(); + var always = context.options[0] === "always"; + var prefix = always ? "Expected" : "Unexpected"; + + /** + * Checks spacing before `}` of a given token. + * @param {Token} token - A token to check. This is a Template token. + * @returns {void} + */ + function checkSpacingBefore(token) { + var prevToken = sourceCode.getTokenBefore(token); + if (prevToken && + CLOSE_PAREN.test(token.value) && + astUtils.isTokenOnSameLine(prevToken, token) && + sourceCode.isSpaceBetweenTokens(prevToken, token) !== always + ) { + context.report({ + loc: token.loc.start, + message: prefix + " space(s) before '}'.", + fix: function(fixer) { + if (always) { + return fixer.insertTextBefore(token, " "); + } + return fixer.removeRange([ + prevToken.range[1], + token.range[0] + ]); + } + }); + } + } + + /** + * Checks spacing after `${` of a given token. + * @param {Token} token - A token to check. This is a Template token. + * @returns {void} + */ + function checkSpacingAfter(token) { + var nextToken = sourceCode.getTokenAfter(token); + if (nextToken && + OPEN_PAREN.test(token.value) && + astUtils.isTokenOnSameLine(token, nextToken) && + sourceCode.isSpaceBetweenTokens(token, nextToken) !== always + ) { + context.report({ + loc: { + line: token.loc.end.line, + column: token.loc.end.column - 2 + }, + message: prefix + " space(s) after '${'.", + fix: function(fixer) { + if (always) { + return fixer.insertTextAfter(token, " "); + } + return fixer.removeRange([ + token.range[1], + nextToken.range[0] + ]); + } + }); + } + } + + return { + TemplateElement: function(node) { + var token = sourceCode.getFirstToken(node); + checkSpacingBefore(token); + checkSpacingAfter(token); + } + }; +}; + +module.exports.schema = [ + {enum: ["always", "never"]} +]; diff --git a/tests/lib/rules/template-curly-spacing.js b/tests/lib/rules/template-curly-spacing.js new file mode 100644 index 00000000000..80ffdcbca76 --- /dev/null +++ b/tests/lib/rules/template-curly-spacing.js @@ -0,0 +1,104 @@ +/** + * @fileoverview Tests for template-curly-spacing rule. + * @author Toru Nagashima + * @copyright 2016 Toru Nagashima. All rights reserved. + * See LICENSE file in root directory for full license. + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var rule = require("../../../lib/rules/template-curly-spacing"), + RuleTester = require("../../../lib/testers/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +var ruleTester = new RuleTester(); +ruleTester.run("template-curly-spacing", rule, { + valid: [ + {code: "{ foo }"}, + {code: "`${foo} ${bar}`", parserOptions: {ecmaVersion: 6}}, + {code: "`${foo} ${bar} ${\n baz\n}`", options: ["never"], parserOptions: {ecmaVersion: 6}}, + {code: "`${ foo } ${ bar } ${\n baz\n}`", options: ["always"], parserOptions: {ecmaVersion: 6}}, + {code: "tag`${foo} ${bar}`", parserOptions: {ecmaVersion: 6}}, + {code: "tag`${foo} ${bar} ${\n baz\n}`", options: ["never"], parserOptions: {ecmaVersion: 6}}, + {code: "tag`${ foo } ${ bar } ${\n baz\n}`", options: ["always"], parserOptions: {ecmaVersion: 6}} + ], + invalid: [ + { + code: "`${ foo } ${ bar }`", + output: "`${foo} ${bar}`", + errors: [ + {message: "Unexpected space(s) after '${'.", column: 2}, + {message: "Unexpected space(s) before '}'.", column: 9}, + {message: "Unexpected space(s) after '${'.", column: 11}, + {message: "Unexpected space(s) before '}'.", column: 18} + ], + parserOptions: {ecmaVersion: 6} + }, + { + code: "`${ foo } ${ bar }`", + output: "`${foo} ${bar}`", + errors: [ + {message: "Unexpected space(s) after '${'.", column: 2}, + {message: "Unexpected space(s) before '}'.", column: 9}, + {message: "Unexpected space(s) after '${'.", column: 11}, + {message: "Unexpected space(s) before '}'.", column: 18} + ], + options: ["never"], + parserOptions: {ecmaVersion: 6} + }, + { + code: "`${foo} ${bar}`", + output: "`${ foo } ${ bar }`", + errors: [ + {message: "Expected space(s) after '${'.", column: 2}, + {message: "Expected space(s) before '}'.", column: 7}, + {message: "Expected space(s) after '${'.", column: 9}, + {message: "Expected space(s) before '}'.", column: 14} + ], + options: ["always"], + parserOptions: {ecmaVersion: 6} + }, + { + code: "tag`${ foo } ${ bar }`", + output: "tag`${foo} ${bar}`", + errors: [ + {message: "Unexpected space(s) after '${'.", column: 5}, + {message: "Unexpected space(s) before '}'.", column: 12}, + {message: "Unexpected space(s) after '${'.", column: 14}, + {message: "Unexpected space(s) before '}'.", column: 21} + ], + parserOptions: {ecmaVersion: 6} + }, + { + code: "tag`${ foo } ${ bar }`", + output: "tag`${foo} ${bar}`", + errors: [ + {message: "Unexpected space(s) after '${'.", column: 5}, + {message: "Unexpected space(s) before '}'.", column: 12}, + {message: "Unexpected space(s) after '${'.", column: 14}, + {message: "Unexpected space(s) before '}'.", column: 21} + ], + options: ["never"], + parserOptions: {ecmaVersion: 6} + }, + { + code: "tag`${foo} ${bar}`", + output: "tag`${ foo } ${ bar }`", + errors: [ + {message: "Expected space(s) after '${'.", column: 5}, + {message: "Expected space(s) before '}'.", column: 10}, + {message: "Expected space(s) after '${'.", column: 12}, + {message: "Expected space(s) before '}'.", column: 17} + ], + options: ["always"], + parserOptions: {ecmaVersion: 6} + } + ] +});