1 | /**
|
2 | * @fileoverview enforce or disallow capitalization of the first letter of a comment
|
3 | * @author Kevin Partington
|
4 | */
|
5 | ;
|
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 | };
|