diff --git a/docs/rules/function-paren-newline.md b/docs/rules/function-paren-newline.md new file mode 100644 index 00000000000..83912b7b283 --- /dev/null +++ b/docs/rules/function-paren-newline.md @@ -0,0 +1,225 @@ +# enforce consistent line breaks inside function parentheses (function-paren-newline) + +(fixable) The `--fix` option on the [command line](../user-guide/command-line-interface#fix) automatically fixes problems reported by this rule. + +Many styleguides require or disallow newlines inside of function parentheses. + +## Rule Details + +This rule enforces consistent line breaks inside parentheses of function parameters or arguments. + +### Options + +This rule has a single option, which can either be a string or an object. + +* `"always"` requires line breaks inside all function parentheses. +* `"never"` disallows line breaks inside all function parentheses. +* `"multiline"` (default) requires linebreaks inside function parentheses if any of the parameters/arguments have a line break between them. Otherwise, it disallows linebreaks. +* `{ "minItems": value }` requires linebreaks inside function parentheses if the number of parameters/arguments is at least `value`. Otherwise, it disallows linebreaks. + +Example configurations: + +```json +{ + "rules": { + "function-paren-newline": ["error", "never"] + } +} +``` + +```json +{ + "rules": { + "function-paren-newline": ["error", { "minItems": 3 }] + } +} +``` + +Examples of **incorrect** code for this rule with the `"always"` option: + +```js +/* eslint function-paren-newline: ["error", "always"] */ + +function foo(bar, baz) {} + +var foo = function(bar, baz) {}; + +var foo = (bar, baz) => {}; + +foo(bar, baz); +``` + +Examples of **correct** code for this rule with the `"always"` option: + +```js +/* eslint function-paren-newline: ["error", "always"] */ + +function foo( + bar, + baz +) {} + +var foo = function( + bar, baz +) {}; + +var foo = ( + bar, + baz +) => {}; + +foo( + bar, + baz +); +``` + +Examples of **incorrect** code for this rule with the `"never"` option: + +```js +/* eslint function-paren-newline: ["error", "never"] */ + +function foo( + bar, + baz +) {} + +var foo = function( + bar, baz +) {}; + +var foo = ( + bar, + baz +) => {}; + +foo( + bar, + baz +); +``` + +Examples of **correct** code for this rule with the `"never"` option: + +```js +/* eslint function-paren-newline: ["error", "never"] */ + +function foo(bar, baz) {} + +function foo(bar, + baz) {} + +var foo = function(bar, baz) {}; + +var foo = (bar, baz) => {}; + +foo(bar, baz); + +foo(bar, + baz); +``` + +Examples of **incorrect** code for this rule with the default `"multiline"` option: + +```js +/* eslint function-paren-newline: ["error", "multiline"] */ + +function foo(bar, + baz +) {} + +var foo = function( + bar, baz +) {}; + +var foo = ( + bar, + baz) => {}; + +foo(bar, + baz); + +foo( + function() { + return baz; + } +); +``` + +Examples of **correct** code for this rule with the default `"multiline"` option: + +```js +/* eslint function-paren-newline: ["error", "multiline"] */ + +function foo(bar, baz) {} + +var foo = function( + bar, + baz +) {}; + +var foo = (bar, baz) => {}; + +foo(bar, baz, qux); + +foo( + bar, + baz, + qux +); + +foo(function() { + return baz; +}); +``` + +Examples of **incorrect** code for this rule with the `{ "minItems": 3 }` option: + +```js +/* eslint function-paren-newline: ["error", { "minItems": 3 }] */ + +function foo( + bar, + baz +) {} + +function foo(bar, baz, qux) {} + +var foo = function( + bar, baz +) {}; + +var foo = (bar, + baz) => {}; + +foo(bar, + baz); +``` + +Examples of **correct** code for this rule with the `{ "minItems": 3 }` option: + +```js +/* eslint function-paren-newline: ["error", { "minItems": 3 }] */ + +function foo(bar, baz) {} + +var foo = function( + bar, + baz, + qux +) {}; + +var foo = ( + bar, baz, qux +) => {}; + +foo(bar, baz); + +foo( + bar, baz, qux +); +``` + +## When Not To Use It + +If don't want to enforce consistent linebreaks inside function parentheses, do not turn on this rule. diff --git a/lib/rules/function-paren-newline.js b/lib/rules/function-paren-newline.js new file mode 100644 index 00000000000..6ac1fc655c3 --- /dev/null +++ b/lib/rules/function-paren-newline.js @@ -0,0 +1,216 @@ +/** + * @fileoverview enforce consistent line breaks inside function parentheses + * @author Teddy Katz + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("../ast-utils"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "enforce consistent line breaks inside function parentheses", + category: "Stylistic Issues", + recommended: false + }, + fixable: "whitespace", + schema: [ + { + oneOf: [ + { + enum: ["always", "never", "multiline"] + }, + { + type: "object", + properties: { + minItems: { + type: "integer", + minimum: 0 + } + }, + additionalProperties: false + } + ] + } + ] + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const rawOption = context.options[0] || "multiline"; + const multilineOption = rawOption === "multiline"; + let minItems; + + if (typeof rawOption === "object") { + minItems = rawOption.minItems; + } else if (rawOption === "always") { + minItems = 0; + } else if (rawOption === "never") { + minItems = Infinity; + } else { + minItems = null; + } + + //---------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------- + + /** + * Determines whether there should be newlines inside function parens + * @param {ASTNode[]} elements The arguments or parameters in the list + * @returns {boolean} `true` if there should be newlines inside the function parens + */ + function shouldHaveNewlines(elements) { + if (multilineOption) { + return elements.some((element, index) => index !== elements.length - 1 && element.loc.end.line !== elements[index + 1].loc.start.line); + } + return elements.length >= minItems; + } + + /** + * Validates a list of arguments or parameters + * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token + * @param {ASTNode[]} elements The arguments or parameters in the list + * @returns {void} + */ + function validateParens(parens, elements) { + const leftParen = parens.leftParen; + const rightParen = parens.rightParen; + const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen); + const tokenBeforeRightParen = sourceCode.getTokenBefore(rightParen); + const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen); + const hasRightNewline = !astUtils.isTokenOnSameLine(tokenBeforeRightParen, rightParen); + const needsNewlines = shouldHaveNewlines(elements); + + if (hasLeftNewline && !needsNewlines) { + context.report({ + node: leftParen, + message: "Unexpected newline after '('.", + fix(fixer) { + return sourceCode.getText().slice(leftParen.range[1], tokenAfterLeftParen.range[0]).trim() + + // If there is a comment between the ( and the first element, don't do a fix. + ? null + : fixer.removeRange([leftParen.range[1], tokenAfterLeftParen.range[0]]); + } + }); + } else if (!hasLeftNewline && needsNewlines) { + context.report({ + node: leftParen, + message: "Expected a newline after '('.", + fix: fixer => fixer.insertTextAfter(leftParen, "\n") + }); + } + + if (hasRightNewline && !needsNewlines) { + context.report({ + node: rightParen, + message: "Unexpected newline before ')'.", + fix(fixer) { + return sourceCode.getText().slice(tokenBeforeRightParen.range[1], rightParen.range[0]).trim() + + // If there is a comment between the last element and the ), don't do a fix. + ? null + : fixer.removeRange([tokenBeforeRightParen.range[1], rightParen.range[0]]); + } + }); + } else if (!hasRightNewline && needsNewlines) { + context.report({ + node: rightParen, + message: "Expected a newline before ')'.", + fix: fixer => fixer.insertTextBefore(rightParen, "\n") + }); + } + } + + /** + * Gets the left paren and right paren tokens of a node. + * @param {ASTNode} node The node with parens + * @returns {Object} An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token. + * Can also return `null` if an expression has no parens (e.g. a NewExpression with no arguments, or an ArrowFunctionExpression + * with a single parameter) + */ + function getParenTokens(node) { + switch (node.type) { + case "NewExpression": + if (!node.arguments.length && !( + astUtils.isOpeningParenToken(sourceCode.getLastToken(node, { skip: 1 })) && + astUtils.isClosingParenToken(sourceCode.getLastToken(node)) + )) { + + // If the NewExpression does not have parens (e.g. `new Foo`), return null. + return null; + } + + // falls through + + case "CallExpression": + return { + leftParen: sourceCode.getTokenAfter(node.callee, astUtils.isOpeningParenToken), + rightParen: sourceCode.getLastToken(node) + }; + + case "FunctionDeclaration": + case "FunctionExpression": { + const leftParen = sourceCode.getFirstToken(node, astUtils.isOpeningParenToken); + const rightParen = node.params.length + ? sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isClosingParenToken) + : sourceCode.getTokenAfter(leftParen); + + return { leftParen, rightParen }; + } + + case "ArrowFunctionExpression": { + const firstToken = sourceCode.getFirstToken(node); + + if (!astUtils.isOpeningParenToken(firstToken)) { + + // If the ArrowFunctionExpression has a single param without parens, return null. + return null; + } + + return { + leftParen: firstToken, + rightParen: sourceCode.getTokenBefore(node.body, astUtils.isClosingParenToken) + }; + } + + default: + return null; + } + } + + /** + * Validates the parentheses for a node + * @param {ASTNode} node The node with parens + * @returns {void} + */ + function validateNode(node) { + const parens = getParenTokens(node); + + if (parens) { + validateParens(parens, astUtils.isFunction(node) ? node.params : node.arguments); + } + } + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + ArrowFunctionExpression: validateNode, + CallExpression: validateNode, + FunctionDeclaration: validateNode, + FunctionExpression: validateNode, + NewExpression: validateNode + }; + } +}; diff --git a/tests/lib/rules/function-paren-newline.js b/tests/lib/rules/function-paren-newline.js new file mode 100644 index 00000000000..abc9a865369 --- /dev/null +++ b/tests/lib/rules/function-paren-newline.js @@ -0,0 +1,552 @@ +/** + * @fileoverview enforce consistent line breaks inside function parentheses + * @author Teddy Katz + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/function-paren-newline"); +const RuleTester = require("../../../lib/testers/rule-tester"); + + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const LEFT_MISSING_ERROR = { message: "Expected a newline after '('.", type: "Punctuator" }; +const LEFT_UNEXPECTED_ERROR = { message: "Unexpected newline after '('.", type: "Punctuator" }; +const RIGHT_MISSING_ERROR = { message: "Expected a newline before ')'.", type: "Punctuator" }; +const RIGHT_UNEXPECTED_ERROR = { message: "Unexpected newline before ')'.", type: "Punctuator" }; + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); + +ruleTester.run("function-paren-newline", rule, { + + valid: [ + + // multiline option (default) + "function baz(foo, bar) {}", + "(function(foo, bar) {});", + "(function baz(foo, bar) {});", + "(foo, bar) => {};", + "foo => {};", + "baz(foo, bar);", + "function baz() {}", + ` + function baz( + foo, + bar + ) {} + `, + ` + (function( + foo, + bar + ) {}); + `, + ` + (function baz( + foo, + bar + ) {}); + `, + ` + ( + foo, + bar + ) => {}; + `, + ` + baz( + foo, + bar + ); + `, + ` + baz(\`foo + bar\`) + `, + "new Foo(bar, baz)", + "new Foo", + "new (Foo)", + + ` + (foo) + (bar) + `, + ` + foo.map(value => { + return value; + }) + `, + + // always option + { + code: "function baz(foo, bar) {}", + options: ["multiline"] + }, + { + code: ` + function baz( + foo, + bar + ) {} + `, + options: ["always"] + }, + { + code: ` + (function( + foo, + bar + ) {}); + `, + options: ["always"] + }, + { + code: ` + (function baz( + foo, + bar + ) {}); + `, + options: ["always"] + }, + { + code: ` + ( + foo, + bar + ) => {}; + `, + options: ["always"] + }, + { + code: ` + baz( + foo, + bar + ); + `, + options: ["always"] + }, + { + code: ` + function baz( + ) {} + `, + options: ["always"] + }, + + // never option + { + code: "function baz(foo, bar) {}", + options: ["never"] + }, + { + code: "(function(foo, bar) {});", + options: ["never"] + }, + { + code: "(function baz(foo, bar) {});", + options: ["never"] + }, + { + code: "(foo, bar) => {};", + options: ["never"] + }, + { + code: "baz(foo, bar);", + options: ["never"] + }, + { + code: "function baz() {}", + options: ["never"] + }, + + // minItems option + { + code: "function baz(foo, bar) {}", + options: [{ minItems: 3 }] + }, + { + code: ` + function baz( + foo, bar, qux + ) {} + `, + options: [{ minItems: 3 }] + }, + { + code: ` + baz( + foo, bar, qux + ); + `, + options: [{ minItems: 3 }] + }, + { + code: "baz(foo, bar);", + options: [{ minItems: 3 }] + } + ], + + invalid: [ + + // multiline option (default) + { + code: ` + function baz(foo, + bar + ) {} + `, + output: ` + function baz(\nfoo, + bar + ) {} + `, + errors: [LEFT_MISSING_ERROR] + }, + { + code: ` + (function( + foo, + bar) {}) + `, + output: ` + (function( + foo, + bar\n) {}) + `, + errors: [RIGHT_MISSING_ERROR] + }, + { + code: ` + (function baz(foo, + bar) {}) + `, + output: ` + (function baz(\nfoo, + bar\n) {}) + `, + errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR] + }, + { + code: ` + baz( + foo, bar); + `, + output: ` + baz(foo, bar); + `, + errors: [LEFT_UNEXPECTED_ERROR] + }, + { + code: ` + (foo, bar + ) => {}; + `, + output: ` + (foo, bar) => {}; + `, + errors: [RIGHT_UNEXPECTED_ERROR] + }, + { + code: ` + function baz( + foo, bar + ) {} + `, + output: ` + function baz(foo, bar) {} + `, + errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR] + }, + { + code: ` + function baz( + foo = + 1 + ) {} + `, + output: ` + function baz(foo = + 1) {} + `, + errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR] + }, + { + code: ` + function baz( + ) {} + `, + output: ` + function baz() {} + `, + errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR] + }, + { + code: ` + new Foo(bar, + baz); + `, + output: ` + new Foo(\nbar, + baz\n); + `, + errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR] + }, + { + code: ` + function baz(/* not fixed due to comment */ + foo) {} + `, + output: ` + function baz(/* not fixed due to comment */ + foo) {} + `, + errors: [LEFT_UNEXPECTED_ERROR] + }, + { + code: ` + function baz(foo + /* not fixed due to comment */) {} + `, + output: ` + function baz(foo + /* not fixed due to comment */) {} + `, + errors: [RIGHT_UNEXPECTED_ERROR] + }, + + // always option + { + code: ` + function baz(foo, + bar + ) {} + `, + output: ` + function baz(\nfoo, + bar + ) {} + `, + options: ["always"], + errors: [LEFT_MISSING_ERROR] + }, + { + code: ` + (function( + foo, + bar) {}) + `, + output: ` + (function( + foo, + bar\n) {}) + `, + options: ["always"], + errors: [RIGHT_MISSING_ERROR] + }, + { + code: ` + (function baz(foo, + bar) {}) + `, + output: ` + (function baz(\nfoo, + bar\n) {}) + `, + options: ["always"], + errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR] + }, + { + code: "function baz(foo, bar) {}", + output: "function baz(\nfoo, bar\n) {}", + options: ["always"], + errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR] + }, + { + code: "(function(foo, bar) {});", + output: "(function(\nfoo, bar\n) {});", + options: ["always"], + errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR] + }, + { + code: "(function baz(foo, bar) {});", + output: "(function baz(\nfoo, bar\n) {});", + options: ["always"], + errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR] + }, + { + code: "(foo, bar) => {};", + output: "(\nfoo, bar\n) => {};", + options: ["always"], + errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR] + }, + { + code: "baz(foo, bar);", + output: "baz(\nfoo, bar\n);", + options: ["always"], + errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR] + }, + { + code: "function baz() {}", + output: "function baz(\n) {}", + options: ["always"], + errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR] + }, + + // never option + { + code: ` + function baz(foo, + bar + ) {} + `, + output: ` + function baz(foo, + bar) {} + `, + options: ["never"], + errors: [RIGHT_UNEXPECTED_ERROR] + }, + { + code: ` + (function( + foo, + bar) {}) + `, + output: ` + (function(foo, + bar) {}) + `, + options: ["never"], + errors: [LEFT_UNEXPECTED_ERROR] + }, + { + code: ` + function baz( + foo, + bar + ) {} + `, + output: ` + function baz(foo, + bar) {} + `, + options: ["never"], + errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR] + }, + { + code: ` + (function( + foo, + bar + ) {}); + `, + output: ` + (function(foo, + bar) {}); + `, + options: ["never"], + errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR] + }, + { + code: ` + (function baz( + foo, + bar + ) {}); + `, + output: ` + (function baz(foo, + bar) {}); + `, + options: ["never"], + errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR] + }, + { + code: ` + ( + foo, + bar + ) => {}; + `, + output: ` + (foo, + bar) => {}; + `, + options: ["never"], + errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR] + }, + { + code: ` + baz( + foo, + bar + ); + `, + output: ` + baz(foo, + bar); + `, + options: ["never"], + errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR] + }, + { + code: ` + function baz( + ) {} + `, + output: ` + function baz() {} + `, + options: ["never"], + errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR] + }, + + // minItems option + { + code: "function baz(foo, bar, qux) {}", + output: "function baz(\nfoo, bar, qux\n) {}", + options: [{ minItems: 3 }], + errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR] + }, + { + code: ` + function baz( + foo, bar + ) {} + `, + output: ` + function baz(foo, bar) {} + `, + options: [{ minItems: 3 }], + errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR] + }, + { + code: "baz(foo, bar, qux);", + output: "baz(\nfoo, bar, qux\n);", + options: [{ minItems: 3 }], + errors: [LEFT_MISSING_ERROR, RIGHT_MISSING_ERROR] + }, + { + code: ` + baz( + foo, + bar + ); + `, + output: ` + baz(foo, + bar); + `, + options: [{ minItems: 3 }], + errors: [LEFT_UNEXPECTED_ERROR, RIGHT_UNEXPECTED_ERROR] + } + ] +});