From 30d71d6d5b85fcd693b63f209e2f7366ed5e66fc Mon Sep 17 00:00:00 2001 From: Nicolas Froidure Date: Wed, 3 Aug 2016 21:34:29 +0200 Subject: [PATCH] Update: 'requireForBlockBody' modifier for 'arrow-parens' (fixes #6557) (#6558) --- docs/rules/arrow-parens.md | 58 +++++++++++++++++++++++++++++---- lib/rules/arrow-parens.js | 56 ++++++++++++++++++++++++++++++- tests/lib/rules/arrow-parens.js | 55 ++++++++++++++++++++++++++----- 3 files changed, 154 insertions(+), 15 deletions(-) diff --git a/docs/rules/arrow-parens.md b/docs/rules/arrow-parens.md index 229840021bd..075de1f0b46 100644 --- a/docs/rules/arrow-parens.md +++ b/docs/rules/arrow-parens.md @@ -49,13 +49,16 @@ a => {} ## Options -The rule takes one option, a string, which could be either `"always"` or `"as-needed"`. The default is `"always"`. +This rule has a string option and an object one. -You can set the option in configuration like this: +String options are: -```json -"arrow-parens": ["error", "always"] -``` +* `"always"` (default) requires parens around arguments in all cases. +* `"as-needed"` allows omitting parens when there is only one argument. + +Object properties for variants of the `"as-needed"` option: + +* `"requireForBlockBody": true` modifies the as-needed rule in order to require parens if the function body is in an intructions block (surrounded by braces). ### always @@ -176,4 +179,47 @@ a.then(foo => { if (true) {}; }); (a = 10) => a; ([a, b]) => a; ({a, b}) => a; -``` \ No newline at end of file +``` + +### requireForBlockBody + +Examples of **incorrect** code for the `{ "requireForBlockBody": true }` option: + +```js +/*eslint arrow-parens: [2, "as-needed", { "requireForBlockBody": true }]*/ +/*eslint-env es6*/ + +(a) => a; +a => {}; +a => {'\n'}; +a.map((x) => x * x); +a.map(x => { + return x * x; +}); +a.then(foo => {}); +``` + +Examples of **correct** code for the `{ "requireForBlockBody": true }` option: + +```js +/*eslint arrow-parens: [2, "as-needed", { "requireForBlockBody": true }]*/ +/*eslint-env es6*/ + +(a) => {}; +(a) => {'\n'}; +a => ({}); +() => {}; +a => a; +a.then((foo) => {}); +a.then((foo) => { if (true) {}; }); +a((foo) => { if (true) {}; }); +(a, b, c) => a; +(a = 10) => a; +([a, b]) => a; +({a, b}) => a; +``` + +## Further Reading + +* The `"as-needed", { "requireForBlockBody": true }` rule is directly inspired by the Airbnb + [JS Style Guide](https://github.com/airbnb/javascript#arrows--one-arg-parens). diff --git a/lib/rules/arrow-parens.js b/lib/rules/arrow-parens.js index e982a537ad8..60b683168eb 100644 --- a/lib/rules/arrow-parens.js +++ b/lib/rules/arrow-parens.js @@ -21,6 +21,15 @@ module.exports = { schema: [ { enum: ["always", "as-needed"] + }, + { + type: "object", + properties: { + requireForBlockBody: { + type: "boolean" + } + }, + additionalProperties: false } ] }, @@ -29,9 +38,13 @@ module.exports = { const message = "Expected parentheses around arrow function argument."; const asNeededMessage = "Unexpected parentheses around single function argument."; const asNeeded = context.options[0] === "as-needed"; + const requireForBlockBodyMessage = "Unexpected parentheses around single function argument having a body with no curly braces"; + const requireForBlockBodyNoParensMessage = "Expected parentheses around arrow function argument having a body with curly braces."; + const requireForBlockBody = asNeeded && context.options[1] && context.options[1].requireForBlockBody === true; const sourceCode = context.getSourceCode(); + /** * Determines whether a arrow function argument end with `)` * @param {ASTNode} node The arrow function node. @@ -40,7 +53,48 @@ module.exports = { function parens(node) { const token = sourceCode.getFirstToken(node); - // as-needed: x => x + // "as-needed", { "requireForBlockBody": true }: x => x + if ( + requireForBlockBody && + node.params.length === 1 && + node.params[0].type === "Identifier" && + node.body.type !== "BlockStatement" + ) { + if (token.type === "Punctuator" && token.value === "(") { + context.report({ + node: node, + message: requireForBlockBodyMessage, + fix: function(fixer) { + const paramToken = context.getTokenAfter(token); + const closingParenToken = context.getTokenAfter(paramToken); + + return fixer.replaceTextRange([ + token.range[0], + closingParenToken.range[1] + ], paramToken.value); + } + }); + } + return; + } + + if ( + requireForBlockBody && + node.body.type === "BlockStatement" + ) { + if (token.type !== "Punctuator" || token.value !== "(") { + context.report({ + node: node, + message: requireForBlockBodyNoParensMessage, + fix: function(fixer) { + return fixer.replaceText(token, "(" + token.value + ")"); + } + }); + } + return; + } + + // "as-needed": x => x if (asNeeded && node.params.length === 1 && node.params[0].type === "Identifier") { if (token.type === "Punctuator" && token.value === "(") { context.report({ diff --git a/tests/lib/rules/arrow-parens.js b/tests/lib/rules/arrow-parens.js index 3afbd75701b..713b325cc7c 100644 --- a/tests/lib/rules/arrow-parens.js +++ b/tests/lib/rules/arrow-parens.js @@ -18,6 +18,8 @@ const rule = require("../../../lib/rules/arrow-parens"), const ruleTester = new RuleTester(); const valid = [ + + // "always" (by default) { code: "() => {}", parserOptions: { ecmaVersion: 6 } }, { code: "(a) => {}", parserOptions: { ecmaVersion: 6 } }, { code: "(a) => a", parserOptions: { ecmaVersion: 6 } }, @@ -25,7 +27,15 @@ const valid = [ { code: "a.then((foo) => {});", parserOptions: { ecmaVersion: 6 } }, { code: "a.then((foo) => { if (true) {}; });", parserOptions: { ecmaVersion: 6 } }, - // as-needed + // "always" (explicit) + { code: "() => {}", options: ["always"], parserOptions: { ecmaVersion: 6 } }, + { code: "(a) => {}", options: ["always"], parserOptions: { ecmaVersion: 6 } }, + { code: "(a) => a", options: ["always"], parserOptions: { ecmaVersion: 6 } }, + { code: "(a) => {\n}", options: ["always"], parserOptions: { ecmaVersion: 6 } }, + { code: "a.then((foo) => {});", options: ["always"], parserOptions: { ecmaVersion: 6 } }, + { code: "a.then((foo) => { if (true) {}; });", options: ["always"], parserOptions: { ecmaVersion: 6 } }, + + // "as-needed" { code: "() => {}", options: ["as-needed"], parserOptions: { ecmaVersion: 6 } }, { code: "a => {}", options: ["as-needed"], parserOptions: { ecmaVersion: 6 } }, { code: "a => a", options: ["as-needed"], parserOptions: { ecmaVersion: 6 } }, @@ -33,15 +43,31 @@ const valid = [ { code: "({ a, b }) => {}", options: ["as-needed"], parserOptions: { ecmaVersion: 6 } }, { code: "(a = 10) => {}", options: ["as-needed"], parserOptions: { ecmaVersion: 6 } }, { code: "(...a) => a[0]", options: ["as-needed"], parserOptions: { ecmaVersion: 6 } }, - { code: "(a, b) => {}", options: ["as-needed"], parserOptions: { ecmaVersion: 6 } } + { code: "(a, b) => {}", options: ["as-needed"], parserOptions: { ecmaVersion: 6 } }, + + // "as-needed", { "requireForBlockBody": true } + { code: "() => {}", options: ["as-needed", {requireForBlockBody: true}], parserOptions: { ecmaVersion: 6 } }, + { code: "a => a", options: ["as-needed", {requireForBlockBody: true}], parserOptions: { ecmaVersion: 6 } }, + { code: "([a, b]) => {}", options: ["as-needed", {requireForBlockBody: true}], parserOptions: { ecmaVersion: 6 } }, + { code: "([a, b]) => a", options: ["as-needed", {requireForBlockBody: true}], parserOptions: { ecmaVersion: 6 } }, + { code: "({ a, b }) => {}", options: ["as-needed", {requireForBlockBody: true}], parserOptions: { ecmaVersion: 6 } }, + { code: "({ a, b }) => a + b", options: ["as-needed", {requireForBlockBody: true}], parserOptions: { ecmaVersion: 6 } }, + { code: "(a = 10) => {}", options: ["as-needed", {requireForBlockBody: true}], parserOptions: { ecmaVersion: 6 } }, + { code: "(...a) => a[0]", options: ["as-needed", {requireForBlockBody: true}], parserOptions: { ecmaVersion: 6 } }, + { code: "(a, b) => {}", options: ["as-needed", {requireForBlockBody: true}], parserOptions: { ecmaVersion: 6 } }, + { code: "a => ({})", options: ["as-needed", {requireForBlockBody: true}], parserOptions: { ecmaVersion: 6 } } ]; const message = "Expected parentheses around arrow function argument."; const asNeededMessage = "Unexpected parentheses around single function argument."; +const requireForBlockBodyMessage = "Unexpected parentheses around single function argument having a body with no curly braces"; +const requireForBlockBodyNoParensMessage = "Expected parentheses around arrow function argument having a body with curly braces."; const type = "ArrowFunctionExpression"; const invalid = [ + + // "always" (by default) { code: "a => {}", output: "(a) => {}", @@ -109,7 +135,7 @@ const invalid = [ }] }, - // as-needed + // "as-needed" { code: "(a) => a", output: "a => a", @@ -122,19 +148,32 @@ const invalid = [ type: type }] }, + + // "as-needed", { "requireForBlockBody": true } { - code: "(b) => b", - output: "b => b", - options: ["as-needed"], + code: "a => {}", + output: "(a) => {}", + options: ["as-needed", {requireForBlockBody: true}], parserOptions: { ecmaVersion: 6 }, errors: [{ line: 1, column: 1, - message: asNeededMessage, + message: requireForBlockBodyNoParensMessage, + type: type + }] + }, + { + code: "(a) => a", + output: "a => a", + options: ["as-needed", {requireForBlockBody: true}], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + line: 1, + column: 1, + message: requireForBlockBodyMessage, type: type }] } - ]; ruleTester.run("arrow-parens", rule, {