/** * @fileoverview Rule to require newlines before `return` statement * @author Kai Cataldo * @deprecated */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { type: "layout", docs: { description: "require an empty line before `return` statements", category: "Stylistic Issues", recommended: false, url: "https://eslint.org/docs/rules/newline-before-return" }, fixable: "whitespace", schema: [], messages: { expected: "Expected newline before return statement." }, deprecated: true, replacedBy: ["padding-line-between-statements"] }, create(context) { const sourceCode = context.getSourceCode(); //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- /** * Tests whether node is preceded by supplied tokens * @param {ASTNode} node node to check * @param {Array} testTokens array of tokens to test against * @returns {boolean} Whether or not the node is preceded by one of the supplied tokens * @private */ function isPrecededByTokens(node, testTokens) { const tokenBefore = sourceCode.getTokenBefore(node); return testTokens.some(token => tokenBefore.value === token); } /** * Checks whether node is the first node after statement or in block * @param {ASTNode} node node to check * @returns {boolean} Whether or not the node is the first node after statement or in block * @private */ function isFirstNode(node) { const parentType = node.parent.type; if (node.parent.body) { return Array.isArray(node.parent.body) ? node.parent.body[0] === node : node.parent.body === node; } if (parentType === "IfStatement") { return isPrecededByTokens(node, ["else", ")"]); } if (parentType === "DoWhileStatement") { return isPrecededByTokens(node, ["do"]); } if (parentType === "SwitchCase") { return isPrecededByTokens(node, [":"]); } return isPrecededByTokens(node, [")"]); } /** * Returns the number of lines of comments that precede the node * @param {ASTNode} node node to check for overlapping comments * @param {number} lineNumTokenBefore line number of previous token, to check for overlapping comments * @returns {number} Number of lines of comments that precede the node * @private */ function calcCommentLines(node, lineNumTokenBefore) { const comments = sourceCode.getCommentsBefore(node); let numLinesComments = 0; if (!comments.length) { return numLinesComments; } comments.forEach(comment => { numLinesComments++; if (comment.type === "Block") { numLinesComments += comment.loc.end.line - comment.loc.start.line; } // avoid counting lines with inline comments twice if (comment.loc.start.line === lineNumTokenBefore) { numLinesComments--; } if (comment.loc.end.line === node.loc.start.line) { numLinesComments--; } }); return numLinesComments; } /** * Returns the line number of the token before the node that is passed in as an argument * @param {ASTNode} node The node to use as the start of the calculation * @returns {number} Line number of the token before `node` * @private */ function getLineNumberOfTokenBefore(node) { const tokenBefore = sourceCode.getTokenBefore(node); let lineNumTokenBefore; /** * Global return (at the beginning of a script) is a special case. * If there is no token before `return`, then we expect no line * break before the return. Comments are allowed to occupy lines * before the global return, just no blank lines. * Setting lineNumTokenBefore to zero in that case results in the * desired behavior. */ if (tokenBefore) { lineNumTokenBefore = tokenBefore.loc.end.line; } else { lineNumTokenBefore = 0; // global return at beginning of script } return lineNumTokenBefore; } /** * Checks whether node is preceded by a newline * @param {ASTNode} node node to check * @returns {boolean} Whether or not the node is preceded by a newline * @private */ function hasNewlineBefore(node) { const lineNumNode = node.loc.start.line; const lineNumTokenBefore = getLineNumberOfTokenBefore(node); const commentLines = calcCommentLines(node, lineNumTokenBefore); return (lineNumNode - lineNumTokenBefore - commentLines) > 1; } /** * Checks whether it is safe to apply a fix to a given return statement. * * The fix is not considered safe if the given return statement has leading comments, * as we cannot safely determine if the newline should be added before or after the comments. * For more information, see: https://github.com/eslint/eslint/issues/5958#issuecomment-222767211 * @param {ASTNode} node The return statement node to check. * @returns {boolean} `true` if it can fix the node. * @private */ function canFix(node) { const leadingComments = sourceCode.getCommentsBefore(node); const lastLeadingComment = leadingComments[leadingComments.length - 1]; const tokenBefore = sourceCode.getTokenBefore(node); if (leadingComments.length === 0) { return true; } /* * if the last leading comment ends in the same line as the previous token and * does not share a line with the `return` node, we can consider it safe to fix. * Example: * function a() { * var b; //comment * return; * } */ if (lastLeadingComment.loc.end.line === tokenBefore.loc.end.line && lastLeadingComment.loc.end.line !== node.loc.start.line) { return true; } return false; } //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- return { ReturnStatement(node) { if (!isFirstNode(node) && !hasNewlineBefore(node)) { context.report({ node, messageId: "expected", fix(fixer) { if (canFix(node)) { const tokenBefore = sourceCode.getTokenBefore(node); const newlines = node.loc.start.line === tokenBefore.loc.end.line ? "\n\n" : "\n"; return fixer.insertTextBefore(node, newlines); } return null; } }); } } }; } };