1 | /**
|
2 | * @fileoverview Rule to require newlines before `return` statement
|
3 | * @author Kai Cataldo
|
4 | */
|
5 | ;
|
6 |
|
7 | //------------------------------------------------------------------------------
|
8 | // Rule Definition
|
9 | //------------------------------------------------------------------------------
|
10 |
|
11 | module.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 | };
|