]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Enforces empty lines around comments. | |
3 | * @author Jamund Ferguson | |
4 | */ | |
5 | "use strict"; | |
6 | ||
7 | //------------------------------------------------------------------------------ | |
8 | // Requirements | |
9 | //------------------------------------------------------------------------------ | |
10 | ||
5422a9cc | 11 | const astUtils = require("./utils/ast-utils"); |
eb39fafa DC |
12 | |
13 | //------------------------------------------------------------------------------ | |
14 | // Helpers | |
15 | //------------------------------------------------------------------------------ | |
16 | ||
17 | /** | |
18 | * Return an array with with any line numbers that are empty. | |
19 | * @param {Array} lines An array of each line of the file. | |
20 | * @returns {Array} An array of line numbers. | |
21 | */ | |
22 | function getEmptyLineNums(lines) { | |
23 | const emptyLines = lines.map((line, i) => ({ | |
24 | code: line.trim(), | |
25 | num: i + 1 | |
26 | })).filter(line => !line.code).map(line => line.num); | |
27 | ||
28 | return emptyLines; | |
29 | } | |
30 | ||
31 | /** | |
32 | * Return an array with with any line numbers that contain comments. | |
33 | * @param {Array} comments An array of comment tokens. | |
34 | * @returns {Array} An array of line numbers. | |
35 | */ | |
36 | function getCommentLineNums(comments) { | |
37 | const lines = []; | |
38 | ||
39 | comments.forEach(token => { | |
40 | const start = token.loc.start.line; | |
41 | const end = token.loc.end.line; | |
42 | ||
43 | lines.push(start, end); | |
44 | }); | |
45 | return lines; | |
46 | } | |
47 | ||
48 | //------------------------------------------------------------------------------ | |
49 | // Rule Definition | |
50 | //------------------------------------------------------------------------------ | |
51 | ||
52 | module.exports = { | |
53 | meta: { | |
54 | type: "layout", | |
55 | ||
56 | docs: { | |
57 | description: "require empty lines around comments", | |
58 | category: "Stylistic Issues", | |
59 | recommended: false, | |
60 | url: "https://eslint.org/docs/rules/lines-around-comment" | |
61 | }, | |
62 | ||
63 | fixable: "whitespace", | |
64 | ||
65 | schema: [ | |
66 | { | |
67 | type: "object", | |
68 | properties: { | |
69 | beforeBlockComment: { | |
70 | type: "boolean", | |
71 | default: true | |
72 | }, | |
73 | afterBlockComment: { | |
74 | type: "boolean", | |
75 | default: false | |
76 | }, | |
77 | beforeLineComment: { | |
78 | type: "boolean", | |
79 | default: false | |
80 | }, | |
81 | afterLineComment: { | |
82 | type: "boolean", | |
83 | default: false | |
84 | }, | |
85 | allowBlockStart: { | |
86 | type: "boolean", | |
87 | default: false | |
88 | }, | |
89 | allowBlockEnd: { | |
90 | type: "boolean", | |
91 | default: false | |
92 | }, | |
93 | allowClassStart: { | |
94 | type: "boolean" | |
95 | }, | |
96 | allowClassEnd: { | |
97 | type: "boolean" | |
98 | }, | |
99 | allowObjectStart: { | |
100 | type: "boolean" | |
101 | }, | |
102 | allowObjectEnd: { | |
103 | type: "boolean" | |
104 | }, | |
105 | allowArrayStart: { | |
106 | type: "boolean" | |
107 | }, | |
108 | allowArrayEnd: { | |
109 | type: "boolean" | |
110 | }, | |
111 | ignorePattern: { | |
112 | type: "string" | |
113 | }, | |
114 | applyDefaultIgnorePatterns: { | |
115 | type: "boolean" | |
116 | } | |
117 | }, | |
118 | additionalProperties: false | |
119 | } | |
120 | ], | |
121 | messages: { | |
122 | after: "Expected line after comment.", | |
123 | before: "Expected line before comment." | |
124 | } | |
125 | }, | |
126 | ||
127 | create(context) { | |
128 | ||
129 | const options = Object.assign({}, context.options[0]); | |
130 | const ignorePattern = options.ignorePattern; | |
131 | const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN; | |
132 | const customIgnoreRegExp = new RegExp(ignorePattern, "u"); | |
133 | const applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns !== false; | |
134 | ||
135 | options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true; | |
136 | ||
137 | const sourceCode = context.getSourceCode(); | |
138 | ||
139 | const lines = sourceCode.lines, | |
140 | numLines = lines.length + 1, | |
141 | comments = sourceCode.getAllComments(), | |
142 | commentLines = getCommentLineNums(comments), | |
143 | emptyLines = getEmptyLineNums(lines), | |
144 | commentAndEmptyLines = commentLines.concat(emptyLines); | |
145 | ||
146 | /** | |
147 | * Returns whether or not comments are on lines starting with or ending with code | |
148 | * @param {token} token The comment token to check. | |
149 | * @returns {boolean} True if the comment is not alone. | |
150 | */ | |
151 | function codeAroundComment(token) { | |
152 | let currentToken = token; | |
153 | ||
154 | do { | |
155 | currentToken = sourceCode.getTokenBefore(currentToken, { includeComments: true }); | |
156 | } while (currentToken && astUtils.isCommentToken(currentToken)); | |
157 | ||
158 | if (currentToken && astUtils.isTokenOnSameLine(currentToken, token)) { | |
159 | return true; | |
160 | } | |
161 | ||
162 | currentToken = token; | |
163 | do { | |
164 | currentToken = sourceCode.getTokenAfter(currentToken, { includeComments: true }); | |
165 | } while (currentToken && astUtils.isCommentToken(currentToken)); | |
166 | ||
167 | if (currentToken && astUtils.isTokenOnSameLine(token, currentToken)) { | |
168 | return true; | |
169 | } | |
170 | ||
171 | return false; | |
172 | } | |
173 | ||
174 | /** | |
175 | * Returns whether or not comments are inside a node type or not. | |
176 | * @param {ASTNode} parent The Comment parent node. | |
177 | * @param {string} nodeType The parent type to check against. | |
178 | * @returns {boolean} True if the comment is inside nodeType. | |
179 | */ | |
180 | function isParentNodeType(parent, nodeType) { | |
181 | return parent.type === nodeType || | |
182 | (parent.body && parent.body.type === nodeType) || | |
183 | (parent.consequent && parent.consequent.type === nodeType); | |
184 | } | |
185 | ||
186 | /** | |
187 | * Returns the parent node that contains the given token. | |
188 | * @param {token} token The token to check. | |
189 | * @returns {ASTNode} The parent node that contains the given token. | |
190 | */ | |
191 | function getParentNodeOfToken(token) { | |
192 | return sourceCode.getNodeByRangeIndex(token.range[0]); | |
193 | } | |
194 | ||
195 | /** | |
196 | * Returns whether or not comments are at the parent start or not. | |
197 | * @param {token} token The Comment token. | |
198 | * @param {string} nodeType The parent type to check against. | |
199 | * @returns {boolean} True if the comment is at parent start. | |
200 | */ | |
201 | function isCommentAtParentStart(token, nodeType) { | |
202 | const parent = getParentNodeOfToken(token); | |
203 | ||
204 | return parent && isParentNodeType(parent, nodeType) && | |
205 | token.loc.start.line - parent.loc.start.line === 1; | |
206 | } | |
207 | ||
208 | /** | |
209 | * Returns whether or not comments are at the parent end or not. | |
210 | * @param {token} token The Comment token. | |
211 | * @param {string} nodeType The parent type to check against. | |
212 | * @returns {boolean} True if the comment is at parent end. | |
213 | */ | |
214 | function isCommentAtParentEnd(token, nodeType) { | |
215 | const parent = getParentNodeOfToken(token); | |
216 | ||
217 | return parent && isParentNodeType(parent, nodeType) && | |
218 | parent.loc.end.line - token.loc.end.line === 1; | |
219 | } | |
220 | ||
221 | /** | |
222 | * Returns whether or not comments are at the block start or not. | |
223 | * @param {token} token The Comment token. | |
224 | * @returns {boolean} True if the comment is at block start. | |
225 | */ | |
226 | function isCommentAtBlockStart(token) { | |
227 | return isCommentAtParentStart(token, "ClassBody") || isCommentAtParentStart(token, "BlockStatement") || isCommentAtParentStart(token, "SwitchCase"); | |
228 | } | |
229 | ||
230 | /** | |
231 | * Returns whether or not comments are at the block end or not. | |
232 | * @param {token} token The Comment token. | |
233 | * @returns {boolean} True if the comment is at block end. | |
234 | */ | |
235 | function isCommentAtBlockEnd(token) { | |
236 | return isCommentAtParentEnd(token, "ClassBody") || isCommentAtParentEnd(token, "BlockStatement") || isCommentAtParentEnd(token, "SwitchCase") || isCommentAtParentEnd(token, "SwitchStatement"); | |
237 | } | |
238 | ||
239 | /** | |
240 | * Returns whether or not comments are at the class start or not. | |
241 | * @param {token} token The Comment token. | |
242 | * @returns {boolean} True if the comment is at class start. | |
243 | */ | |
244 | function isCommentAtClassStart(token) { | |
245 | return isCommentAtParentStart(token, "ClassBody"); | |
246 | } | |
247 | ||
248 | /** | |
249 | * Returns whether or not comments are at the class end or not. | |
250 | * @param {token} token The Comment token. | |
251 | * @returns {boolean} True if the comment is at class end. | |
252 | */ | |
253 | function isCommentAtClassEnd(token) { | |
254 | return isCommentAtParentEnd(token, "ClassBody"); | |
255 | } | |
256 | ||
257 | /** | |
258 | * Returns whether or not comments are at the object start or not. | |
259 | * @param {token} token The Comment token. | |
260 | * @returns {boolean} True if the comment is at object start. | |
261 | */ | |
262 | function isCommentAtObjectStart(token) { | |
263 | return isCommentAtParentStart(token, "ObjectExpression") || isCommentAtParentStart(token, "ObjectPattern"); | |
264 | } | |
265 | ||
266 | /** | |
267 | * Returns whether or not comments are at the object end or not. | |
268 | * @param {token} token The Comment token. | |
269 | * @returns {boolean} True if the comment is at object end. | |
270 | */ | |
271 | function isCommentAtObjectEnd(token) { | |
272 | return isCommentAtParentEnd(token, "ObjectExpression") || isCommentAtParentEnd(token, "ObjectPattern"); | |
273 | } | |
274 | ||
275 | /** | |
276 | * Returns whether or not comments are at the array start or not. | |
277 | * @param {token} token The Comment token. | |
278 | * @returns {boolean} True if the comment is at array start. | |
279 | */ | |
280 | function isCommentAtArrayStart(token) { | |
281 | return isCommentAtParentStart(token, "ArrayExpression") || isCommentAtParentStart(token, "ArrayPattern"); | |
282 | } | |
283 | ||
284 | /** | |
285 | * Returns whether or not comments are at the array end or not. | |
286 | * @param {token} token The Comment token. | |
287 | * @returns {boolean} True if the comment is at array end. | |
288 | */ | |
289 | function isCommentAtArrayEnd(token) { | |
290 | return isCommentAtParentEnd(token, "ArrayExpression") || isCommentAtParentEnd(token, "ArrayPattern"); | |
291 | } | |
292 | ||
293 | /** | |
294 | * Checks if a comment token has lines around it (ignores inline comments) | |
295 | * @param {token} token The Comment token. | |
296 | * @param {Object} opts Options to determine the newline. | |
297 | * @param {boolean} opts.after Should have a newline after this line. | |
298 | * @param {boolean} opts.before Should have a newline before this line. | |
299 | * @returns {void} | |
300 | */ | |
301 | function checkForEmptyLine(token, opts) { | |
302 | if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(token.value)) { | |
303 | return; | |
304 | } | |
305 | ||
306 | if (ignorePattern && customIgnoreRegExp.test(token.value)) { | |
307 | return; | |
308 | } | |
309 | ||
310 | let after = opts.after, | |
311 | before = opts.before; | |
312 | ||
313 | const prevLineNum = token.loc.start.line - 1, | |
314 | nextLineNum = token.loc.end.line + 1, | |
315 | commentIsNotAlone = codeAroundComment(token); | |
316 | ||
317 | const blockStartAllowed = options.allowBlockStart && | |
318 | isCommentAtBlockStart(token) && | |
319 | !(options.allowClassStart === false && | |
320 | isCommentAtClassStart(token)), | |
321 | blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(token) && !(options.allowClassEnd === false && isCommentAtClassEnd(token)), | |
322 | classStartAllowed = options.allowClassStart && isCommentAtClassStart(token), | |
323 | classEndAllowed = options.allowClassEnd && isCommentAtClassEnd(token), | |
324 | objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(token), | |
325 | objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(token), | |
326 | arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(token), | |
327 | arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(token); | |
328 | ||
329 | const exceptionStartAllowed = blockStartAllowed || classStartAllowed || objectStartAllowed || arrayStartAllowed; | |
330 | const exceptionEndAllowed = blockEndAllowed || classEndAllowed || objectEndAllowed || arrayEndAllowed; | |
331 | ||
332 | // ignore top of the file and bottom of the file | |
333 | if (prevLineNum < 1) { | |
334 | before = false; | |
335 | } | |
336 | if (nextLineNum >= numLines) { | |
337 | after = false; | |
338 | } | |
339 | ||
340 | // we ignore all inline comments | |
341 | if (commentIsNotAlone) { | |
342 | return; | |
343 | } | |
344 | ||
345 | const previousTokenOrComment = sourceCode.getTokenBefore(token, { includeComments: true }); | |
346 | const nextTokenOrComment = sourceCode.getTokenAfter(token, { includeComments: true }); | |
347 | ||
348 | // check for newline before | |
5422a9cc | 349 | if (!exceptionStartAllowed && before && !commentAndEmptyLines.includes(prevLineNum) && |
eb39fafa DC |
350 | !(astUtils.isCommentToken(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, token))) { |
351 | const lineStart = token.range[0] - token.loc.start.column; | |
352 | const range = [lineStart, lineStart]; | |
353 | ||
354 | context.report({ | |
355 | node: token, | |
356 | messageId: "before", | |
357 | fix(fixer) { | |
358 | return fixer.insertTextBeforeRange(range, "\n"); | |
359 | } | |
360 | }); | |
361 | } | |
362 | ||
363 | // check for newline after | |
5422a9cc | 364 | if (!exceptionEndAllowed && after && !commentAndEmptyLines.includes(nextLineNum) && |
eb39fafa DC |
365 | !(astUtils.isCommentToken(nextTokenOrComment) && astUtils.isTokenOnSameLine(token, nextTokenOrComment))) { |
366 | context.report({ | |
367 | node: token, | |
368 | messageId: "after", | |
369 | fix(fixer) { | |
370 | return fixer.insertTextAfter(token, "\n"); | |
371 | } | |
372 | }); | |
373 | } | |
374 | ||
375 | } | |
376 | ||
377 | //-------------------------------------------------------------------------- | |
378 | // Public | |
379 | //-------------------------------------------------------------------------- | |
380 | ||
381 | return { | |
382 | Program() { | |
383 | comments.forEach(token => { | |
384 | if (token.type === "Line") { | |
385 | if (options.beforeLineComment || options.afterLineComment) { | |
386 | checkForEmptyLine(token, { | |
387 | after: options.afterLineComment, | |
388 | before: options.beforeLineComment | |
389 | }); | |
390 | } | |
391 | } else if (token.type === "Block") { | |
392 | if (options.beforeBlockComment || options.afterBlockComment) { | |
393 | checkForEmptyLine(token, { | |
394 | after: options.afterBlockComment, | |
395 | before: options.beforeBlockComment | |
396 | }); | |
397 | } | |
398 | } | |
399 | }); | |
400 | } | |
401 | }; | |
402 | } | |
403 | }; |