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 | /** @type {import('../shared/types').Rule} */
|
---|
103 | module.exports = {
|
---|
104 | meta: {
|
---|
105 | type: "suggestion",
|
---|
106 |
|
---|
107 | docs: {
|
---|
108 | description: "Enforce or disallow capitalization of the first letter of a comment",
|
---|
109 | recommended: false,
|
---|
110 | url: "https://eslint.org/docs/latest/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.sourceCode;
|
---|
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"].includes(previousTokenOrComment.type)
|
---|
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 | };
|
---|