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("../util/patterns/letters");
|
12 | const astUtils = require("../ast-utils");
|
13 |
|
14 | //------------------------------------------------------------------------------
|
15 | // Helpers
|
16 | //------------------------------------------------------------------------------
|
17 |
|
18 | const DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
|
19 | WHITESPACE = /\s/g,
|
20 | MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/, // TODO: Combine w/ max-len pattern?
|
21 | DEFAULTS = {
|
22 | ignorePattern: null,
|
23 | ignoreInlineComments: false,
|
24 | ignoreConsecutiveComments: false
|
25 | };
|
26 |
|
27 | /*
|
28 | * Base schema body for defining the basic capitalization rule, ignorePattern,
|
29 | * and ignoreInlineComments values.
|
30 | * This can be used in a few different ways in the actual schema.
|
31 | */
|
32 | const SCHEMA_BODY = {
|
33 | type: "object",
|
34 | properties: {
|
35 | ignorePattern: {
|
36 | type: "string"
|
37 | },
|
38 | ignoreInlineComments: {
|
39 | type: "boolean"
|
40 | },
|
41 | ignoreConsecutiveComments: {
|
42 | type: "boolean"
|
43 | }
|
44 | },
|
45 | additionalProperties: 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 | */
|
62 | function getNormalizedOptions(rawOptions, which) {
|
63 | if (!rawOptions) {
|
64 | return Object.assign({}, DEFAULTS);
|
65 | }
|
66 |
|
67 | return Object.assign({}, DEFAULTS, rawOptions[which] || rawOptions);
|
68 | }
|
69 |
|
70 | /**
|
71 | * Get normalized options for block and line comments.
|
72 | *
|
73 | * @param {Object|string} rawOptions The user-provided options.
|
74 | * @returns {Object} An object with "Line" and "Block" keys and corresponding
|
75 | * normalized options objects.
|
76 | */
|
77 | function getAllNormalizedOptions(rawOptions) {
|
78 | return {
|
79 | Line: getNormalizedOptions(rawOptions, "line"),
|
80 | Block: getNormalizedOptions(rawOptions, "block")
|
81 | };
|
82 | }
|
83 |
|
84 | /**
|
85 | * Creates a regular expression for each ignorePattern defined in the rule
|
86 | * options.
|
87 | *
|
88 | * This is done in order to avoid invoking the RegExp constructor repeatedly.
|
89 | *
|
90 | * @param {Object} normalizedOptions The normalized rule options.
|
91 | * @returns {void}
|
92 | */
|
93 | function createRegExpForIgnorePatterns(normalizedOptions) {
|
94 | Object.keys(normalizedOptions).forEach(key => {
|
95 | const ignorePatternStr = normalizedOptions[key].ignorePattern;
|
96 |
|
97 | if (ignorePatternStr) {
|
98 | const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`);
|
99 |
|
100 | normalizedOptions[key].ignorePatternRegExp = regExp;
|
101 | }
|
102 | });
|
103 | }
|
104 |
|
105 | //------------------------------------------------------------------------------
|
106 | // Rule Definition
|
107 | //------------------------------------------------------------------------------
|
108 |
|
109 | module.exports = {
|
110 | meta: {
|
111 | docs: {
|
112 | description: "enforce or disallow capitalization of the first letter of a comment",
|
113 | category: "Stylistic Issues",
|
114 | recommended: false,
|
115 | url: "https://eslint.org/docs/rules/capitalized-comments"
|
116 | },
|
117 | fixable: "code",
|
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(/\*/g, "");
|
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 | };
|