• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview enforce or disallow capitalization of the first letter of a comment
3 * @author Kevin Partington
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Requirements
9//------------------------------------------------------------------------------
10
11const LETTER_PATTERN = require("./utils/patterns/letters");
12const astUtils = require("./utils/ast-utils");
13
14//------------------------------------------------------------------------------
15// Helpers
16//------------------------------------------------------------------------------
17
18const DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
19    WHITESPACE = /\s/gu,
20    MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/u; // TODO: Combine w/ max-len pattern?
21
22/*
23 * Base schema body for defining the basic capitalization rule, ignorePattern,
24 * and ignoreInlineComments values.
25 * This can be used in a few different ways in the actual schema.
26 */
27const SCHEMA_BODY = {
28    type: "object",
29    properties: {
30        ignorePattern: {
31            type: "string"
32        },
33        ignoreInlineComments: {
34            type: "boolean"
35        },
36        ignoreConsecutiveComments: {
37            type: "boolean"
38        }
39    },
40    additionalProperties: false
41};
42const DEFAULTS = {
43    ignorePattern: "",
44    ignoreInlineComments: false,
45    ignoreConsecutiveComments: false
46};
47
48/**
49 * Get normalized options for either block or line comments from the given
50 * user-provided options.
51 * - If the user-provided options is just a string, returns a normalized
52 *   set of options using default values for all other options.
53 * - If the user-provided options is an object, then a normalized option
54 *   set is returned. Options specified in overrides will take priority
55 *   over options specified in the main options object, which will in
56 *   turn take priority over the rule's defaults.
57 * @param {Object|string} rawOptions The user-provided options.
58 * @param {string} which Either "line" or "block".
59 * @returns {Object} The normalized options.
60 */
61function getNormalizedOptions(rawOptions, which) {
62    return Object.assign({}, DEFAULTS, rawOptions[which] || rawOptions);
63}
64
65/**
66 * Get normalized options for block and line comments.
67 * @param {Object|string} rawOptions The user-provided options.
68 * @returns {Object} An object with "Line" and "Block" keys and corresponding
69 * normalized options objects.
70 */
71function getAllNormalizedOptions(rawOptions = {}) {
72    return {
73        Line: getNormalizedOptions(rawOptions, "line"),
74        Block: getNormalizedOptions(rawOptions, "block")
75    };
76}
77
78/**
79 * Creates a regular expression for each ignorePattern defined in the rule
80 * options.
81 *
82 * This is done in order to avoid invoking the RegExp constructor repeatedly.
83 * @param {Object} normalizedOptions The normalized rule options.
84 * @returns {void}
85 */
86function createRegExpForIgnorePatterns(normalizedOptions) {
87    Object.keys(normalizedOptions).forEach(key => {
88        const ignorePatternStr = normalizedOptions[key].ignorePattern;
89
90        if (ignorePatternStr) {
91            const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`, "u");
92
93            normalizedOptions[key].ignorePatternRegExp = regExp;
94        }
95    });
96}
97
98//------------------------------------------------------------------------------
99// Rule Definition
100//------------------------------------------------------------------------------
101
102module.exports = {
103    meta: {
104        type: "suggestion",
105
106        docs: {
107            description: "enforce or disallow capitalization of the first letter of a comment",
108            category: "Stylistic Issues",
109            recommended: false,
110            url: "https://eslint.org/docs/rules/capitalized-comments"
111        },
112
113        fixable: "code",
114
115        schema: [
116            { enum: ["always", "never"] },
117            {
118                oneOf: [
119                    SCHEMA_BODY,
120                    {
121                        type: "object",
122                        properties: {
123                            line: SCHEMA_BODY,
124                            block: SCHEMA_BODY
125                        },
126                        additionalProperties: false
127                    }
128                ]
129            }
130        ],
131
132        messages: {
133            unexpectedLowercaseComment: "Comments should not begin with a lowercase character.",
134            unexpectedUppercaseComment: "Comments should not begin with an uppercase character."
135        }
136    },
137
138    create(context) {
139
140        const capitalize = context.options[0] || "always",
141            normalizedOptions = getAllNormalizedOptions(context.options[1]),
142            sourceCode = context.getSourceCode();
143
144        createRegExpForIgnorePatterns(normalizedOptions);
145
146        //----------------------------------------------------------------------
147        // Helpers
148        //----------------------------------------------------------------------
149
150        /**
151         * Checks whether a comment is an inline comment.
152         *
153         * For the purpose of this rule, a comment is inline if:
154         * 1. The comment is preceded by a token on the same line; and
155         * 2. The command is followed by a token on the same line.
156         *
157         * Note that the comment itself need not be single-line!
158         *
159         * Also, it follows from this definition that only block comments can
160         * be considered as possibly inline. This is because line comments
161         * would consume any following tokens on the same line as the comment.
162         * @param {ASTNode} comment The comment node to check.
163         * @returns {boolean} True if the comment is an inline comment, false
164         * otherwise.
165         */
166        function isInlineComment(comment) {
167            const previousToken = sourceCode.getTokenBefore(comment, { includeComments: true }),
168                nextToken = sourceCode.getTokenAfter(comment, { includeComments: true });
169
170            return Boolean(
171                previousToken &&
172                nextToken &&
173                comment.loc.start.line === previousToken.loc.end.line &&
174                comment.loc.end.line === nextToken.loc.start.line
175            );
176        }
177
178        /**
179         * Determine if a comment follows another comment.
180         * @param {ASTNode} comment The comment to check.
181         * @returns {boolean} True if the comment follows a valid comment.
182         */
183        function isConsecutiveComment(comment) {
184            const previousTokenOrComment = sourceCode.getTokenBefore(comment, { includeComments: true });
185
186            return Boolean(
187                previousTokenOrComment &&
188                ["Block", "Line"].indexOf(previousTokenOrComment.type) !== -1
189            );
190        }
191
192        /**
193         * Check a comment to determine if it is valid for this rule.
194         * @param {ASTNode} comment The comment node to process.
195         * @param {Object} options The options for checking this comment.
196         * @returns {boolean} True if the comment is valid, false otherwise.
197         */
198        function isCommentValid(comment, options) {
199
200            // 1. Check for default ignore pattern.
201            if (DEFAULT_IGNORE_PATTERN.test(comment.value)) {
202                return true;
203            }
204
205            // 2. Check for custom ignore pattern.
206            const commentWithoutAsterisks = comment.value
207                .replace(/\*/gu, "");
208
209            if (options.ignorePatternRegExp && options.ignorePatternRegExp.test(commentWithoutAsterisks)) {
210                return true;
211            }
212
213            // 3. Check for inline comments.
214            if (options.ignoreInlineComments && isInlineComment(comment)) {
215                return true;
216            }
217
218            // 4. Is this a consecutive comment (and are we tolerating those)?
219            if (options.ignoreConsecutiveComments && isConsecutiveComment(comment)) {
220                return true;
221            }
222
223            // 5. Does the comment start with a possible URL?
224            if (MAYBE_URL.test(commentWithoutAsterisks)) {
225                return true;
226            }
227
228            // 6. Is the initial word character a letter?
229            const commentWordCharsOnly = commentWithoutAsterisks
230                .replace(WHITESPACE, "");
231
232            if (commentWordCharsOnly.length === 0) {
233                return true;
234            }
235
236            const firstWordChar = commentWordCharsOnly[0];
237
238            if (!LETTER_PATTERN.test(firstWordChar)) {
239                return true;
240            }
241
242            // 7. Check the case of the initial word character.
243            const isUppercase = firstWordChar !== firstWordChar.toLocaleLowerCase(),
244                isLowercase = firstWordChar !== firstWordChar.toLocaleUpperCase();
245
246            if (capitalize === "always" && isLowercase) {
247                return false;
248            }
249            if (capitalize === "never" && isUppercase) {
250                return false;
251            }
252
253            return true;
254        }
255
256        /**
257         * Process a comment to determine if it needs to be reported.
258         * @param {ASTNode} comment The comment node to process.
259         * @returns {void}
260         */
261        function processComment(comment) {
262            const options = normalizedOptions[comment.type],
263                commentValid = isCommentValid(comment, options);
264
265            if (!commentValid) {
266                const messageId = capitalize === "always"
267                    ? "unexpectedLowercaseComment"
268                    : "unexpectedUppercaseComment";
269
270                context.report({
271                    node: null, // Intentionally using loc instead
272                    loc: comment.loc,
273                    messageId,
274                    fix(fixer) {
275                        const match = comment.value.match(LETTER_PATTERN);
276
277                        return fixer.replaceTextRange(
278
279                            // Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*)
280                            [comment.range[0] + match.index + 2, comment.range[0] + match.index + 3],
281                            capitalize === "always" ? match[0].toLocaleUpperCase() : match[0].toLocaleLowerCase()
282                        );
283                    }
284                });
285            }
286        }
287
288        //----------------------------------------------------------------------
289        // Public
290        //----------------------------------------------------------------------
291
292        return {
293            Program() {
294                const comments = sourceCode.getAllComments();
295
296                comments.filter(token => token.type !== "Shebang").forEach(processComment);
297            }
298        };
299    }
300};
301