UNPKG

15.9 kBJavaScriptView Raw
1/**
2 * @fileoverview Enforces empty lines around comments.
3 * @author Jamund Ferguson
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Requirements
9//------------------------------------------------------------------------------
10
11const astUtils = require("./utils/ast-utils");
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 */
22function 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 */
36function 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
52module.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
349 if (!exceptionStartAllowed && before && !commentAndEmptyLines.includes(prevLineNum) &&
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
364 if (!exceptionEndAllowed && after && !commentAndEmptyLines.includes(nextLineNum) &&
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};