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