UNPKG

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