.gitignore added
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / eslint / lib / rules / capitalized-comments.js
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
11 const LETTER_PATTERN = require("./utils/patterns/letters");
12 const astUtils = require("./utils/ast-utils");
13
14 //------------------------------------------------------------------------------
15 // Helpers
16 //------------------------------------------------------------------------------
17
18 const 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  */
27 const 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 };
42 const 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  */
61 function 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  */
71 function 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  */
86 function 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
102 module.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 };