From 8cd00b308987e0db0bdb2e242bf13b2b07b350bd Mon Sep 17 00:00:00 2001 From: finico Date: Sun, 18 Aug 2019 22:57:42 +0300 Subject: [PATCH] New: function-call-argument-newline (#12024) --- docs/rules/function-call-argument-newline.md | 202 +++++++++ lib/rules/function-call-argument-newline.js | 120 ++++++ lib/rules/index.js | 1 + .../rules/function-call-argument-newline.js | 386 ++++++++++++++++++ tools/rule-types.json | 1 + 5 files changed, 710 insertions(+) create mode 100644 docs/rules/function-call-argument-newline.md create mode 100644 lib/rules/function-call-argument-newline.js create mode 100644 tests/lib/rules/function-call-argument-newline.js diff --git a/docs/rules/function-call-argument-newline.md b/docs/rules/function-call-argument-newline.md new file mode 100644 index 00000000000..bdb98807f72 --- /dev/null +++ b/docs/rules/function-call-argument-newline.md @@ -0,0 +1,202 @@ +# enforce line breaks between arguments of a function call (function-call-argument-newline) + +A number of style guides require or disallow line breaks between arguments of a function call. + +## Rule Details + +This rule enforces line breaks between arguments of a function call. + +## Options + +This rule has a string option: + +* `"always"` (default) requires line breaks between arguments +* `"never"` disallows line breaks between arguments +* `"consistent"` requires consistent usage of line breaks between arguments + + +### always + +Examples of **incorrect** code for this rule with the default `"always"` option: + +```js +/*eslint function-call-argument-newline: ["error", "always"]*/ + +foo("one", "two", "three"); + +bar("one", "two", { + one: 1, + two: 2 +}); + +baz("one", "two", (x) => { + console.log(x); +}); +``` + +Examples of **correct** code for this rule with the default `"always"` option: + +```js +/*eslint function-call-argument-newline: ["error", "always"]*/ + +foo( + "one", + "two", + "three" +); + +bar( + "one", + "two", + { one: 1, two: 2 } +); +// or +bar( + "one", + "two", + { + one: 1, + two: 2 + } +); + +baz( + "one", + "two", + (x) => { + console.log(x); + } +); +``` + + +### never + +Examples of **incorrect** code for this rule with the default `"never"` option: + +```js +/*eslint function-call-argument-newline: ["error", "never"]*/ + +foo( + "one", + "two", "three" +); + +bar( + "one", + "two", { + one: 1, + two: 2 + } +); + +baz( + "one", + "two", (x) => { + console.log(x); + } +); +``` + +Examples of **correct** code for this rule with the `"never"` option: + +```js +/*eslint function-call-argument-newline: ["error", "never"]*/ + +foo("one", "two", "three"); +// or +foo( + "one", "two", "three" +); + +bar("one", "two", { one: 1, two: 2 }); +// or +bar("one", "two", { + one: 1, + two: 2 +}); + +baz("one", "two", (x) => { + console.log(x); +}); +``` + +### consistent + +Examples of **incorrect** code for this rule with the default `"consistent"` option: + +```js +/*eslint function-call-argument-newline: ["error", "consistent"]*/ + +foo("one", "two", + "three"); +//or +foo("one", + "two", "three"); + +bar("one", "two", + { one: 1, two: 2} +); + +baz("one", "two", + (x) => { console.log(x); } +); +``` + +Examples of **correct** code for this rule with the default `"consistent"` option: + +```js +/*eslint function-call-argument-newline: ["error", "consistent"]*/ + +foo("one", "two", "three"); +// or +foo( + "one", + "two", + "three" +); + +bar("one", "two", { + one: 1, + two: 2 +}); +// or +bar( + "one", + "two", + { one: 1, two: 2 } +); +// or +bar( + "one", + "two", + { + one: 1, + two: 2 + } +); + +baz("one", "two", (x) => { + console.log(x); +}); +// or +baz( + "one", + "two", + (x) => { + console.log(x); + } +); +``` + + +## When Not To Use It + +If you don't want to enforce line breaks between arguments, don't enable this rule. + +## Related Rules + +* [function-paren-newline](function-paren-newline.md) +* [func-call-spacing](func-call-spacing.md) +* [object-property-newline](object-property-newline.md) +* [array-element-newline](array-element-newline.md) diff --git a/lib/rules/function-call-argument-newline.js b/lib/rules/function-call-argument-newline.js new file mode 100644 index 00000000000..8bf31f7c713 --- /dev/null +++ b/lib/rules/function-call-argument-newline.js @@ -0,0 +1,120 @@ +/** + * @fileoverview Rule to enforce line breaks between arguments of a function call + * @author Alexey Gonchar + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: "layout", + + docs: { + description: "enforce line breaks between arguments of a function call", + category: "Stylistic Issues", + recommended: false, + url: "https://eslint.org/docs/rules/function-call-argument-newline" + }, + + fixable: "whitespace", + + schema: [ + { + enum: ["always", "never", "consistent"] + } + ], + + messages: { + unexpectedLineBreak: "There should be no line break here.", + missingLineBreak: "There should be a line break after this argument." + } + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + const checkers = { + unexpected: { + messageId: "unexpectedLineBreak", + check: (prevToken, currentToken) => prevToken.loc.start.line !== currentToken.loc.start.line, + createFix: (token, tokenBefore) => fixer => + fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], " ") + }, + missing: { + messageId: "missingLineBreak", + check: (prevToken, currentToken) => prevToken.loc.start.line === currentToken.loc.start.line, + createFix: (token, tokenBefore) => fixer => + fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], "\n") + } + }; + + /** + * Check all arguments for line breaks in the CallExpression + * @param {CallExpression} node node to evaluate + * @param {{ messageId: string, check: Function }} checker selected checker + * @returns {void} + * @private + */ + function checkArguments(node, checker) { + for (let i = 1; i < node.arguments.length; i++) { + const prevArgToken = sourceCode.getFirstToken(node.arguments[i - 1]); + const currentArgToken = sourceCode.getFirstToken(node.arguments[i]); + + if (checker.check(prevArgToken, currentArgToken)) { + const tokenBefore = sourceCode.getTokenBefore( + currentArgToken, + { includeComments: true } + ); + + context.report({ + node, + loc: { + start: tokenBefore.loc.end, + end: currentArgToken.loc.start + }, + messageId: checker.messageId, + fix: checker.createFix(currentArgToken, tokenBefore) + }); + } + } + } + + /** + * Check if open space is present in a function name + * @param {CallExpression} node node to evaluate + * @returns {void} + * @private + */ + function check(node) { + if (node.arguments.length < 2) { + return; + } + + const option = context.options[0] || "always"; + + if (option === "never") { + checkArguments(node, checkers.unexpected); + } else if (option === "always") { + checkArguments(node, checkers.missing); + } else if (option === "consistent") { + const firstArgToken = sourceCode.getFirstToken(node.arguments[0]); + const secondArgToken = sourceCode.getFirstToken(node.arguments[1]); + + if (firstArgToken.loc.start.line === secondArgToken.loc.start.line) { + checkArguments(node, checkers.unexpected); + } else { + checkArguments(node, checkers.missing); + } + } + } + + return { + CallExpression: check, + NewExpression: check + }; + } +}; diff --git a/lib/rules/index.js b/lib/rules/index.js index 45045904bb4..c42ae41d6cb 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -46,6 +46,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({ "func-name-matching": () => require("./func-name-matching"), "func-names": () => require("./func-names"), "func-style": () => require("./func-style"), + "function-call-argument-newline": () => require("./function-call-argument-newline"), "function-paren-newline": () => require("./function-paren-newline"), "generator-star-spacing": () => require("./generator-star-spacing"), "getter-return": () => require("./getter-return"), diff --git a/tests/lib/rules/function-call-argument-newline.js b/tests/lib/rules/function-call-argument-newline.js new file mode 100644 index 00000000000..2871534e79d --- /dev/null +++ b/tests/lib/rules/function-call-argument-newline.js @@ -0,0 +1,386 @@ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/function-call-argument-newline"), + { RuleTester } = require("../../../lib/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); + +ruleTester.run("function-call-argument-newline", rule, { + valid: [ + + /* early return */ + "fn()", + "fn(a)", + "new Foo()", + "new Foo(b)", + + /* default ("always") */ + "fn(a,\n\tb)", + + /* "always" */ + { code: "fn(a,\n\tb)", options: ["always"] }, + { code: "fn(\n\ta,\n\tb\n)", options: ["always"] }, + { code: "fn(\n\ta,\n\tb,\n\tc\n)", options: ["always"] }, + { + code: "fn(\n\ta,\n\tb,\n\t[\n\t\t1,\n\t\t2\n\t]\n)", + options: ["always"] + }, + { + code: "fn(\n\ta,\n\tb,\n\t{\n\t\ta: 1,\n\t\tb: 2\n\t}\n)", + options: ["always"] + }, + { + code: "fn(\n\ta,\n\tb,\n\tfunction (x) {\n\t\tx()\n\t}\n)", + options: ["always"] + }, + { + code: "fn(\n\ta,\n\tb,\n\tx => {\n\t\tx()\n\t}\n)", + options: ["always"], + parserOptions: { ecmaVersion: 6 } + }, + + /* "never" */ + { code: "fn(a, b)", options: ["never"] }, + { code: "fn(\n\ta, b\n)", options: ["never"] }, + { code: "fn(a, b, c)", options: ["never"] }, + { code: "fn(a, b, [\n\t1,\n\t2\n])", options: ["never"] }, + { code: "fn(a, b, {\n\ta: 1,\n\tb: 2\n})", options: ["never"] }, + { code: "fn(a, b, function (x) {\n\tx()\n})", options: ["never"] }, + { + code: "fn(a, b, x => {\n\tx()\n})", + options: ["never"], + parserOptions: { ecmaVersion: 6 } + }, + + /* "consistent" */ + { code: "fn(a, b, c)", options: ["consistent"] }, + { code: "fn(a,\n\tb,\n\tc)", options: ["consistent"] } + ], + invalid: [ + + /* default ("always") */ + { + code: "fn(a, b)", + output: "fn(a,\nb)", + errors: [ + { + messageId: "missingLineBreak", + line: 1, + column: 6, + endLine: 1, + endColumn: 7 + } + ] + }, + + /* "always" */ + { + code: "fn(a, b)", + output: "fn(a,\nb)", + options: ["always"], + errors: [ + { + messageId: "missingLineBreak", + line: 1, + column: 6, + endLine: 1, + endColumn: 7 + } + ] + }, + { + code: "fn(a, b, c)", + output: "fn(a,\nb,\nc)", + options: ["always"], + errors: [ + { + messageId: "missingLineBreak", + line: 1, + column: 6, + endLine: 1, + endColumn: 7 + }, + { + messageId: "missingLineBreak", + line: 1, + column: 9, + endLine: 1, + endColumn: 10 + } + ] + }, + { + code: "fn(a, b, [\n\t1,\n\t2\n])", + output: "fn(a,\nb,\n[\n\t1,\n\t2\n])", + options: ["always"], + errors: [ + { + messageId: "missingLineBreak", + line: 1, + column: 6, + endLine: 1, + endColumn: 7 + }, + { + messageId: "missingLineBreak", + line: 1, + column: 9, + endLine: 1, + endColumn: 10 + } + ] + }, + { + code: "fn(a, b, {\n\ta: 1,\n\tb: 2\n})", + output: "fn(a,\nb,\n{\n\ta: 1,\n\tb: 2\n})", + options: ["always"], + errors: [ + { + messageId: "missingLineBreak", + line: 1, + column: 6, + endLine: 1, + endColumn: 7 + }, + { + messageId: "missingLineBreak", + line: 1, + column: 9, + endLine: 1, + endColumn: 10 + } + ] + }, + { + code: "fn(a, b, function (x) {\n\tx()\n})", + output: "fn(a,\nb,\nfunction (x) {\n\tx()\n})", + options: ["always"], + errors: [ + { + messageId: "missingLineBreak", + line: 1, + column: 6, + endLine: 1, + endColumn: 7 + }, + { + messageId: "missingLineBreak", + line: 1, + column: 9, + endLine: 1, + endColumn: 10 + } + ] + }, + { + code: "fn(a, b, x => {\n\tx()\n})", + output: "fn(a,\nb,\nx => {\n\tx()\n})", + options: ["always"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "missingLineBreak", + line: 1, + column: 6, + endLine: 1, + endColumn: 7 + }, + { + messageId: "missingLineBreak", + line: 1, + column: 9, + endLine: 1, + endColumn: 10 + } + ] + }, + + /* "never" */ + { + code: "fn(a,\n\tb)", + output: "fn(a, b)", + options: ["never"], + errors: [ + { + messageId: "unexpectedLineBreak", + line: 1, + column: 6, + endLine: 2, + endColumn: 2 + } + ] + }, + { + code: "fn(a,\n\tb,\n\tc)", + output: "fn(a, b, c)", + options: ["never"], + errors: [ + { + messageId: "unexpectedLineBreak", + line: 1, + column: 6, + endLine: 2, + endColumn: 2 + }, + { + messageId: "unexpectedLineBreak", + line: 2, + column: 4, + endLine: 3, + endColumn: 2 + } + ] + }, + { + code: "fn(a,\n\tb,\n\t[\n\t\t1,\n\t\t2\n])", + output: "fn(a, b, [\n\t\t1,\n\t\t2\n])", + options: ["never"], + errors: [ + { + messageId: "unexpectedLineBreak", + line: 1, + column: 6, + endLine: 2, + endColumn: 2 + }, + { + messageId: "unexpectedLineBreak", + line: 2, + column: 4, + endLine: 3, + endColumn: 2 + } + ] + }, + { + code: "fn(a,\n\tb,\n\t{\n\t\ta: 1,\n\t\tb: 2\n})", + output: "fn(a, b, {\n\t\ta: 1,\n\t\tb: 2\n})", + options: ["never"], + errors: [ + { + messageId: "unexpectedLineBreak", + line: 1, + column: 6, + endLine: 2, + endColumn: 2 + }, + { + messageId: "unexpectedLineBreak", + line: 2, + column: 4, + endLine: 3, + endColumn: 2 + } + ] + }, + { + code: "fn(a,\n\tb,\n\tfunction (x) {\n\t\tx()\n})", + output: "fn(a, b, function (x) {\n\t\tx()\n})", + options: ["never"], + errors: [ + { + messageId: "unexpectedLineBreak", + line: 1, + column: 6, + endLine: 2, + endColumn: 2 + }, + { + messageId: "unexpectedLineBreak", + line: 2, + column: 4, + endLine: 3, + endColumn: 2 + } + ] + }, + { + code: "fn(a,\n\tb,\n\tx => {\n\t\tx()\n})", + output: "fn(a, b, x => {\n\t\tx()\n})", + options: ["never"], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "unexpectedLineBreak", + line: 1, + column: 6, + endLine: 2, + endColumn: 2 + }, + { + messageId: "unexpectedLineBreak", + line: 2, + column: 4, + endLine: 3, + endColumn: 2 + } + ] + }, + + /* "consistent" */ + { + code: "fn(a, b,\n\tc)", + output: "fn(a, b, c)", + options: ["consistent"], + errors: [ + { + messageId: "unexpectedLineBreak", + line: 1, + column: 9, + endLine: 2, + endColumn: 2 + } + ] + }, + { + code: "fn(a,\n\tb, c)", + output: "fn(a,\n\tb,\nc)", + options: ["consistent"], + errors: [ + { + messageId: "missingLineBreak", + line: 2, + column: 4, + endLine: 2, + endColumn: 5 + } + ] + }, + { + code: "fn(a,\n\tb /* comment */, c)", + output: "fn(a,\n\tb /* comment */,\nc)", + options: ["consistent"], + errors: [ + { + messageId: "missingLineBreak", + line: 2, + column: 18, + endLine: 2, + endColumn: 19 + } + ] + }, + { + code: "fn(a,\n\tb, /* comment */ c)", + output: "fn(a,\n\tb, /* comment */\nc)", + options: ["consistent"], + errors: [ + { + messageId: "missingLineBreak", + line: 2, + column: 18, + endLine: 2, + endColumn: 19 + } + ] + } + ] +}); diff --git a/tools/rule-types.json b/tools/rule-types.json index 1239e5f3ffa..eef001a20d5 100644 --- a/tools/rule-types.json +++ b/tools/rule-types.json @@ -33,6 +33,7 @@ "func-name-matching": "suggestion", "func-names": "suggestion", "func-style": "suggestion", + "function-call-argument-newline": "layout", "function-paren-newline": "layout", "generator-star-spacing": "layout", "getter-return": "problem",