UNPKG

7.85 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to require newlines before `return` statement
3 * @author Kai Cataldo
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Rule Definition
9//------------------------------------------------------------------------------
10
11module.exports = {
12 meta: {
13 docs: {
14 description: "require an empty line before `return` statements",
15 category: "Stylistic Issues",
16 recommended: false
17 },
18 fixable: "whitespace",
19 schema: []
20 },
21
22 create(context) {
23 const sourceCode = context.getSourceCode();
24
25 //--------------------------------------------------------------------------
26 // Helpers
27 //--------------------------------------------------------------------------
28
29 /**
30 * Tests whether node is preceded by supplied tokens
31 * @param {ASTNode} node - node to check
32 * @param {array} testTokens - array of tokens to test against
33 * @returns {boolean} Whether or not the node is preceded by one of the supplied tokens
34 * @private
35 */
36 function isPrecededByTokens(node, testTokens) {
37 const tokenBefore = sourceCode.getTokenBefore(node);
38
39 return testTokens.some(function(token) {
40 return tokenBefore.value === token;
41 });
42 }
43
44 /**
45 * Checks whether node is the first node after statement or in block
46 * @param {ASTNode} node - node to check
47 * @returns {boolean} Whether or not the node is the first node after statement or in block
48 * @private
49 */
50 function isFirstNode(node) {
51 const parentType = node.parent.type;
52
53 if (node.parent.body) {
54 return Array.isArray(node.parent.body)
55 ? node.parent.body[0] === node
56 : node.parent.body === node;
57 }
58
59 if (parentType === "IfStatement") {
60 return isPrecededByTokens(node, ["else", ")"]);
61 } else if (parentType === "DoWhileStatement") {
62 return isPrecededByTokens(node, ["do"]);
63 } else if (parentType === "SwitchCase") {
64 return isPrecededByTokens(node, [":"]);
65 } else {
66 return isPrecededByTokens(node, [")"]);
67 }
68 }
69
70 /**
71 * Returns the number of lines of comments that precede the node
72 * @param {ASTNode} node - node to check for overlapping comments
73 * @param {number} lineNumTokenBefore - line number of previous token, to check for overlapping comments
74 * @returns {number} Number of lines of comments that precede the node
75 * @private
76 */
77 function calcCommentLines(node, lineNumTokenBefore) {
78 const comments = sourceCode.getComments(node).leading;
79 let numLinesComments = 0;
80
81 if (!comments.length) {
82 return numLinesComments;
83 }
84
85 comments.forEach(function(comment) {
86 numLinesComments++;
87
88 if (comment.type === "Block") {
89 numLinesComments += comment.loc.end.line - comment.loc.start.line;
90 }
91
92 // avoid counting lines with inline comments twice
93 if (comment.loc.start.line === lineNumTokenBefore) {
94 numLinesComments--;
95 }
96
97 if (comment.loc.end.line === node.loc.start.line) {
98 numLinesComments--;
99 }
100 });
101
102 return numLinesComments;
103 }
104
105 /**
106 * Returns the line number of the token before the node that is passed in as an argument
107 * @param {ASTNode} node - The node to use as the start of the calculation
108 * @returns {number} Line number of the token before `node`
109 * @private
110 */
111 function getLineNumberOfTokenBefore(node) {
112 const tokenBefore = sourceCode.getTokenBefore(node);
113 let lineNumTokenBefore;
114
115 /**
116 * Global return (at the beginning of a script) is a special case.
117 * If there is no token before `return`, then we expect no line
118 * break before the return. Comments are allowed to occupy lines
119 * before the global return, just no blank lines.
120 * Setting lineNumTokenBefore to zero in that case results in the
121 * desired behavior.
122 */
123 if (tokenBefore) {
124 lineNumTokenBefore = tokenBefore.loc.end.line;
125 } else {
126 lineNumTokenBefore = 0; // global return at beginning of script
127 }
128
129 return lineNumTokenBefore;
130 }
131
132 /**
133 * Checks whether node is preceded by a newline
134 * @param {ASTNode} node - node to check
135 * @returns {boolean} Whether or not the node is preceded by a newline
136 * @private
137 */
138 function hasNewlineBefore(node) {
139 const lineNumNode = node.loc.start.line;
140 const lineNumTokenBefore = getLineNumberOfTokenBefore(node);
141 const commentLines = calcCommentLines(node, lineNumTokenBefore);
142
143 return (lineNumNode - lineNumTokenBefore - commentLines) > 1;
144 }
145
146 /**
147 * Checks whether it is safe to apply a fix to a given return statement.
148 *
149 * The fix is not considered safe if the given return statement has leading comments,
150 * as we cannot safely determine if the newline should be added before or after the comments.
151 * For more information, see: https://github.com/eslint/eslint/issues/5958#issuecomment-222767211
152 *
153 * @param {ASTNode} node - The return statement node to check.
154 * @returns {boolean} `true` if it can fix the node.
155 * @private
156 */
157 function canFix(node) {
158 const leadingComments = sourceCode.getComments(node).leading;
159 const lastLeadingComment = leadingComments[leadingComments.length - 1];
160 const tokenBefore = sourceCode.getTokenBefore(node);
161
162 if (leadingComments.length === 0) {
163 return true;
164 }
165
166 // if the last leading comment ends in the same line as the previous token and
167 // does not share a line with the `return` node, we can consider it safe to fix.
168 // Example:
169 // function a() {
170 // var b; //comment
171 // return;
172 // }
173 if (lastLeadingComment.loc.end.line === tokenBefore.loc.end.line &&
174 lastLeadingComment.loc.end.line !== node.loc.start.line) {
175 return true;
176 }
177
178 return false;
179 }
180
181 //--------------------------------------------------------------------------
182 // Public
183 //--------------------------------------------------------------------------
184
185 return {
186 ReturnStatement(node) {
187 if (!isFirstNode(node) && !hasNewlineBefore(node)) {
188 context.report({
189 node,
190 message: "Expected newline before return statement.",
191 fix(fixer) {
192 if (canFix(node)) {
193 const tokenBefore = sourceCode.getTokenBefore(node);
194 const newlines = node.loc.start.line === tokenBefore.loc.end.line ? "\n\n" : "\n";
195
196 return fixer.insertTextBefore(node, newlines);
197 }
198 return null;
199 }
200 });
201 }
202 }
203 };
204 }
205};