File size: 10,733 Bytes
4a51346
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
/**
 * @fileoverview enforce or disallow capitalization of the first letter of a comment
 * @author Kevin Partington
 */
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const LETTER_PATTERN = require("./utils/patterns/letters");
const astUtils = require("./utils/ast-utils");

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

const DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
    WHITESPACE = /\s/gu,
    MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/u; // TODO: Combine w/ max-len pattern?

/*
 * Base schema body for defining the basic capitalization rule, ignorePattern,
 * and ignoreInlineComments values.
 * This can be used in a few different ways in the actual schema.
 */
const SCHEMA_BODY = {
    type: "object",
    properties: {
        ignorePattern: {
            type: "string"
        },
        ignoreInlineComments: {
            type: "boolean"
        },
        ignoreConsecutiveComments: {
            type: "boolean"
        }
    },
    additionalProperties: false
};
const DEFAULTS = {
    ignorePattern: "",
    ignoreInlineComments: false,
    ignoreConsecutiveComments: false
};

/**
 * Get normalized options for either block or line comments from the given
 * user-provided options.
 * - If the user-provided options is just a string, returns a normalized
 *   set of options using default values for all other options.
 * - If the user-provided options is an object, then a normalized option
 *   set is returned. Options specified in overrides will take priority
 *   over options specified in the main options object, which will in
 *   turn take priority over the rule's defaults.
 * @param {Object|string} rawOptions The user-provided options.
 * @param {string} which Either "line" or "block".
 * @returns {Object} The normalized options.
 */
function getNormalizedOptions(rawOptions, which) {
    return Object.assign({}, DEFAULTS, rawOptions[which] || rawOptions);
}

/**
 * Get normalized options for block and line comments.
 * @param {Object|string} rawOptions The user-provided options.
 * @returns {Object} An object with "Line" and "Block" keys and corresponding
 * normalized options objects.
 */
function getAllNormalizedOptions(rawOptions = {}) {
    return {
        Line: getNormalizedOptions(rawOptions, "line"),
        Block: getNormalizedOptions(rawOptions, "block")
    };
}

/**
 * Creates a regular expression for each ignorePattern defined in the rule
 * options.
 *
 * This is done in order to avoid invoking the RegExp constructor repeatedly.
 * @param {Object} normalizedOptions The normalized rule options.
 * @returns {void}
 */
function createRegExpForIgnorePatterns(normalizedOptions) {
    Object.keys(normalizedOptions).forEach(key => {
        const ignorePatternStr = normalizedOptions[key].ignorePattern;

        if (ignorePatternStr) {
            const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`, "u");

            normalizedOptions[key].ignorePatternRegExp = regExp;
        }
    });
}

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = {
    meta: {
        type: "suggestion",

        docs: {
            description: "enforce or disallow capitalization of the first letter of a comment",
            category: "Stylistic Issues",
            recommended: false,
            url: "https://eslint.org/docs/rules/capitalized-comments"
        },

        fixable: "code",

        schema: [
            { enum: ["always", "never"] },
            {
                oneOf: [
                    SCHEMA_BODY,
                    {
                        type: "object",
                        properties: {
                            line: SCHEMA_BODY,
                            block: SCHEMA_BODY
                        },
                        additionalProperties: false
                    }
                ]
            }
        ],

        messages: {
            unexpectedLowercaseComment: "Comments should not begin with a lowercase character.",
            unexpectedUppercaseComment: "Comments should not begin with an uppercase character."
        }
    },

    create(context) {

        const capitalize = context.options[0] || "always",
            normalizedOptions = getAllNormalizedOptions(context.options[1]),
            sourceCode = context.getSourceCode();

        createRegExpForIgnorePatterns(normalizedOptions);

        //----------------------------------------------------------------------
        // Helpers
        //----------------------------------------------------------------------

        /**
         * Checks whether a comment is an inline comment.
         *
         * For the purpose of this rule, a comment is inline if:
         * 1. The comment is preceded by a token on the same line; and
         * 2. The command is followed by a token on the same line.
         *
         * Note that the comment itself need not be single-line!
         *
         * Also, it follows from this definition that only block comments can
         * be considered as possibly inline. This is because line comments
         * would consume any following tokens on the same line as the comment.
         * @param {ASTNode} comment The comment node to check.
         * @returns {boolean} True if the comment is an inline comment, false
         * otherwise.
         */
        function isInlineComment(comment) {
            const previousToken = sourceCode.getTokenBefore(comment, { includeComments: true }),
                nextToken = sourceCode.getTokenAfter(comment, { includeComments: true });

            return Boolean(
                previousToken &&
                nextToken &&
                comment.loc.start.line === previousToken.loc.end.line &&
                comment.loc.end.line === nextToken.loc.start.line
            );
        }

        /**
         * Determine if a comment follows another comment.
         * @param {ASTNode} comment The comment to check.
         * @returns {boolean} True if the comment follows a valid comment.
         */
        function isConsecutiveComment(comment) {
            const previousTokenOrComment = sourceCode.getTokenBefore(comment, { includeComments: true });

            return Boolean(
                previousTokenOrComment &&
                ["Block", "Line"].indexOf(previousTokenOrComment.type) !== -1
            );
        }

        /**
         * Check a comment to determine if it is valid for this rule.
         * @param {ASTNode} comment The comment node to process.
         * @param {Object} options The options for checking this comment.
         * @returns {boolean} True if the comment is valid, false otherwise.
         */
        function isCommentValid(comment, options) {

            // 1. Check for default ignore pattern.
            if (DEFAULT_IGNORE_PATTERN.test(comment.value)) {
                return true;
            }

            // 2. Check for custom ignore pattern.
            const commentWithoutAsterisks = comment.value
                .replace(/\*/gu, "");

            if (options.ignorePatternRegExp && options.ignorePatternRegExp.test(commentWithoutAsterisks)) {
                return true;
            }

            // 3. Check for inline comments.
            if (options.ignoreInlineComments && isInlineComment(comment)) {
                return true;
            }

            // 4. Is this a consecutive comment (and are we tolerating those)?
            if (options.ignoreConsecutiveComments && isConsecutiveComment(comment)) {
                return true;
            }

            // 5. Does the comment start with a possible URL?
            if (MAYBE_URL.test(commentWithoutAsterisks)) {
                return true;
            }

            // 6. Is the initial word character a letter?
            const commentWordCharsOnly = commentWithoutAsterisks
                .replace(WHITESPACE, "");

            if (commentWordCharsOnly.length === 0) {
                return true;
            }

            const firstWordChar = commentWordCharsOnly[0];

            if (!LETTER_PATTERN.test(firstWordChar)) {
                return true;
            }

            // 7. Check the case of the initial word character.
            const isUppercase = firstWordChar !== firstWordChar.toLocaleLowerCase(),
                isLowercase = firstWordChar !== firstWordChar.toLocaleUpperCase();

            if (capitalize === "always" && isLowercase) {
                return false;
            }
            if (capitalize === "never" && isUppercase) {
                return false;
            }

            return true;
        }

        /**
         * Process a comment to determine if it needs to be reported.
         * @param {ASTNode} comment The comment node to process.
         * @returns {void}
         */
        function processComment(comment) {
            const options = normalizedOptions[comment.type],
                commentValid = isCommentValid(comment, options);

            if (!commentValid) {
                const messageId = capitalize === "always"
                    ? "unexpectedLowercaseComment"
                    : "unexpectedUppercaseComment";

                context.report({
                    node: null, // Intentionally using loc instead
                    loc: comment.loc,
                    messageId,
                    fix(fixer) {
                        const match = comment.value.match(LETTER_PATTERN);

                        return fixer.replaceTextRange(

                            // Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*)
                            [comment.range[0] + match.index + 2, comment.range[0] + match.index + 3],
                            capitalize === "always" ? match[0].toLocaleUpperCase() : match[0].toLocaleLowerCase()
                        );
                    }
                });
            }
        }

        //----------------------------------------------------------------------
        // Public
        //----------------------------------------------------------------------

        return {
            Program() {
                const comments = sourceCode.getAllComments();

                comments.filter(token => token.type !== "Shebang").forEach(processComment);
            }
        };
    }
};