diff --git a/lib/rules/implicit-arrow-linebreak.js b/lib/rules/implicit-arrow-linebreak.js index a883b226471..fb7d603a35a 100644 --- a/lib/rules/implicit-arrow-linebreak.js +++ b/lib/rules/implicit-arrow-linebreak.js @@ -4,6 +4,12 @@ */ "use strict"; +const { + isArrowToken, + isParenthesised, + isOpeningParenToken +} = require("../util/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -41,6 +47,142 @@ module.exports = { return context.options[0] || "beside"; } + /** + * Formats the comments depending on whether it's a line or block comment. + * @param {Comment[]} comments The array of comments between the arrow and body + * @param {Integer} column The column number of the first token + * @returns {string} A string of comment text joined by line breaks + */ + function formatComments(comments, column) { + const whiteSpaces = " ".repeat(column); + + return `${comments.map(comment => { + + if (comment.type === "Line") { + return `//${comment.value}`; + } + + return `/*${comment.value}*/`; + }).join(`\n${whiteSpaces}`)}\n${whiteSpaces}`; + } + + /** + * Finds the first token to prepend comments to depending on the parent type + * @param {Node} node The validated node + * @returns {Token|Node} The node to prepend comments to + */ + function findFirstToken(node) { + switch (node.parent.type) { + case "VariableDeclarator": + + // If the parent is first or only declarator, return the declaration, else, declarator + return sourceCode.getFirstToken( + node.parent.parent.declarations.length === 1 || + node.parent.parent.declarations[0].id.name === node.parent.id.name + ? node.parent.parent : node.parent + ); + case "CallExpression": + case "Property": + + // find the object key + return sourceCode.getFirstToken(node.parent); + default: + return node; + } + } + + /** + * Helper function for adding parentheses fixes for nodes containing nested arrow functions + * @param {Fixer} fixer Fixer + * @param {Token} arrow - The arrow token + * @param {ASTNode} arrowBody - The arrow function body + * @returns {Function[]} autofixer -- wraps function bodies with parentheses + */ + function addParentheses(fixer, arrow, arrowBody) { + const parenthesesFixes = []; + let closingParentheses = ""; + + let followingBody = arrowBody; + let currentArrow = arrow; + + while (currentArrow) { + if (!isParenthesised(sourceCode, followingBody)) { + parenthesesFixes.push( + fixer.insertTextAfter(currentArrow, " (") + ); + + const paramsToken = sourceCode.getTokenBefore(currentArrow, token => + isOpeningParenToken(token) || token.type === "Identifier"); + + const whiteSpaces = " ".repeat(paramsToken.loc.start.column); + + closingParentheses = `\n${whiteSpaces})${closingParentheses}`; + } + + currentArrow = sourceCode.getTokenAfter(currentArrow, isArrowToken); + + if (currentArrow) { + followingBody = sourceCode.getTokenAfter(currentArrow, token => !isOpeningParenToken(token)); + } + } + + return [...parenthesesFixes, + fixer.insertTextAfter(arrowBody, closingParentheses) + ]; + } + + /** + * Autofixes the function body to collapse onto the same line as the arrow. + * If comments exist, prepends the comments before the arrow function. + * If the function body contains arrow functions, appends the function bodies with parentheses. + * @param {Token} arrowToken The arrow token. + * @param {ASTNode} arrowBody the function body + * @param {ASTNode} node The evaluated node + * @returns {Function} autofixer -- validates the node to adhere to besides + */ + function autoFixBesides(arrowToken, arrowBody, node) { + return fixer => { + const placeBesides = fixer.replaceTextRange([arrowToken.range[1], arrowBody.range[0]], " "); + + const comments = sourceCode.getCommentsInside(node).filter(comment => + comment.loc.start.line < arrowBody.loc.start.line); + + if (comments.length) { + + // If the grandparent is not a variable declarator + if ( + arrowBody.parent && + arrowBody.parent.parent && + arrowBody.parent.parent.type !== "VariableDeclarator" + ) { + + // If any arrow functions follow, return the necessary parens fixes. + if (sourceCode.getTokenAfter(arrowToken, isArrowToken) && arrowBody.parent.parent.type !== "VariableDeclarator") { + return addParentheses(fixer, arrowToken, arrowBody); + } + + // If any arrow functions precede, the necessary fixes have already been returned, so return null. + if (sourceCode.getTokenBefore(arrowToken, isArrowToken) && arrowBody.parent.parent.type !== "VariableDeclarator") { + return null; + } + } + + const firstToken = findFirstToken(node); + + const commentText = formatComments(comments, firstToken.loc.start.column); + + const commentBeforeExpression = fixer.insertTextBeforeRange( + firstToken.range, + commentText + ); + + return [placeBesides, commentBeforeExpression]; + } + + return placeBesides; + }; + } + /** * Validates the location of an arrow function body * @param {ASTNode} node The arrow function body @@ -75,7 +217,7 @@ module.exports = { context.report({ node: fixerTarget, message: "Expected no linebreak before this expression.", - fix: fixer => fixer.replaceTextRange([tokenBefore.range[1], fixerTarget.range[0]], " ") + fix: autoFixBesides(tokenBefore, fixerTarget, node) }); } } diff --git a/tests/lib/rules/implicit-arrow-linebreak.js b/tests/lib/rules/implicit-arrow-linebreak.js index 7212e083f1b..f07182c1b5c 100644 --- a/tests/lib/rules/implicit-arrow-linebreak.js +++ b/tests/lib/rules/implicit-arrow-linebreak.js @@ -37,9 +37,76 @@ ruleTester.run("implicit-arrow-linebreak", rule, { `(foo) => ( bar )`, + "(foo) => bar();", + ` + //comment + foo => bar; + `, + ` + foo => ( + // comment + bar => ( + // another comment + baz + ) + ) + `, + ` + foo => ( + // comment + bar => baz + ) + `, + ` + /* text */ + () => bar; + `, + ` + /* foo */ + const bar = () => baz; + `, + ` + (foo) => ( + //comment + bar + ) + `, + ` + [ // comment + foo => 'bar' + ] + `, + ` + /* + One two three four + Five six seven nine. + */ + (foo) => bar + `, + ` + const foo = { + id: 'bar', + // comment + prop: (foo1) => 'returning this string', + } + `, + ` + // comment + "foo".split('').map((char) => char + ) + `, { - code: "(foo) => bar();", - options: ["beside"] + code: ` + async foo => () => bar; + `, + parserOptions: { ecmaVersion: 8 } + }, + { + code: ` + // comment + async foo => 'string' + `, + parserOptions: { ecmaVersion: 8 } }, // 'below' option @@ -132,6 +199,371 @@ ruleTester.run("implicit-arrow-linebreak", rule, { ) `, errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + (foo) => + // test comment + bar + `, + output: ` + // test comment + (foo) => bar + `, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + const foo = () => + // comment + [] + `, + output: ` + // comment + const foo = () => [] + `, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + (foo) => + ( + //comment + bar + ) + `, + output: ` + (foo) => ( + //comment + bar + ) + `, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + (foo) => + ( + bar + //comment + ) + + `, + output: ` + (foo) => ( + bar + //comment + ) + + `, + errors: [UNEXPECTED_LINEBREAK] + + }, { + code: ` + (foo) => + // comment + // another comment + bar`, + output: ` + // comment + // another comment + (foo) => bar`, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + (foo) => + // comment + ( + // another comment + bar + )`, + output: ` + // comment + (foo) => ( + // another comment + bar + )`, + errors: [UNEXPECTED_LINEBREAK] + }, + { + code: "() => // comment \n bar", + output: "// comment \n() => bar", + errors: [UNEXPECTED_LINEBREAK] + }, { + code: "(foo) => //comment \n bar", + output: "//comment \n(foo) => bar", + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + (foo) => + /* test comment */ + bar + `, + output: ` + /* test comment */ + (foo) => bar + `, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + (foo) => + // hi + bar => + // there + baz; + `, + output: ` + (foo) => ( + // hi + bar => ( + // there + baz + ) + ); + `, + errors: [UNEXPECTED_LINEBREAK, UNEXPECTED_LINEBREAK] + }, { + code: ` + (foo) => + // hi + bar => ( + // there + baz + ) + `, + output: ` + (foo) => ( + // hi + bar => ( + // there + baz + ) + ) + `, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + const foo = { + id: 'bar', + prop: (foo1) => + // comment + 'returning this string', + } + `, + output: ` + const foo = { + id: 'bar', + // comment + prop: (foo1) => 'returning this string', + } + `, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + [ foo => + // comment + 'bar' + ] + `, + output: ` + [ // comment + foo => 'bar' + ] + `, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + "foo".split('').map((char) => + // comment + char + ) + `, + output: ` + // comment + "foo".split('').map((char) => char + ) + `, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + new Promise((resolve, reject) => + // comment + resolve() + ) + `, + output: ` + new Promise(// comment + (resolve, reject) => resolve() + ) + `, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + () => + /* + succinct + explanation + of code + */ + bar + `, + output: ` + /* + succinct + explanation + of code + */ + () => bar + `, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + stepOne => + /* + here is + what is + happening + */ + stepTwo => + // then this happens + stepThree`, + output: ` + stepOne => ( + /* + here is + what is + happening + */ + stepTwo => ( + // then this happens + stepThree + ) + )`, + errors: [UNEXPECTED_LINEBREAK, UNEXPECTED_LINEBREAK] + }, { + code: ` + () => + /* + multi + line + */ + bar => + /* + many + lines + */ + baz + `, + output: ` + () => ( + /* + multi + line + */ + bar => ( + /* + many + lines + */ + baz + ) + ) + `, + errors: [UNEXPECTED_LINEBREAK, UNEXPECTED_LINEBREAK] + }, { + code: ` + foo('', boo => + // comment + bar + ) + `, + output: ` + // comment + foo('', boo => bar + ) + `, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + async foo => + // comment + 'string' + `, + output: ` + // comment + async foo => 'string' + `, + parserOptions: { ecmaVersion: 8 }, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + async foo => + // comment + // another + bar; + `, + output: ` + // comment + // another + async foo => bar; + `, + parserOptions: { ecmaVersion: 8 }, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + async (foo) => + // comment + 'string' + `, + output: ` + // comment + async (foo) => 'string' + `, + parserOptions: { ecmaVersion: 8 }, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + const foo = 1, + bar = 2, + baz = () => // comment + qux + `, + output: ` + const foo = 1, + bar = 2, + // comment + baz = () => qux + `, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + const foo = () => + //comment + qux, + bar = 2, + baz = 3 + `, + output: ` + //comment + const foo = () => qux, + bar = 2, + baz = 3 + `, + errors: [UNEXPECTED_LINEBREAK] + }, { + code: ` + const foo = () => + //two + 1, + boo = () => + //comment + 2, + bop = "what" + `, + output: ` + //two + const foo = () => 1, + //comment + boo = () => 2, + bop = "what" + `, + errors: [UNEXPECTED_LINEBREAK, UNEXPECTED_LINEBREAK] }, // 'below' option