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