UNPKG

10.7 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("./utils/patterns/letters");
12const astUtils = require("./utils/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 * @param {Object|string} rawOptions The user-provided options.
58 * @param {string} which Either "line" or "block".
59 * @returns {Object} The normalized options.
60 */
61function 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 */
71function 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 */
86function 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
102module.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};