UNPKG

13.1 kBJavaScriptView Raw
1/**
2 * @fileoverview Source code for spaced-comments rule
3 * @author Gyandeep Singh
4 */
5"use strict";
6
7const lodash = require("lodash");
8const astUtils = require("./utils/ast-utils");
9
10//------------------------------------------------------------------------------
11// Helpers
12//------------------------------------------------------------------------------
13
14/**
15 * Escapes the control characters of a given string.
16 * @param {string} s A string to escape.
17 * @returns {string} An escaped string.
18 */
19function escape(s) {
20 return `(?:${lodash.escapeRegExp(s)})`;
21}
22
23/**
24 * Escapes the control characters of a given string.
25 * And adds a repeat flag.
26 * @param {string} s A string to escape.
27 * @returns {string} An escaped string.
28 */
29function escapeAndRepeat(s) {
30 return `${escape(s)}+`;
31}
32
33/**
34 * Parses `markers` option.
35 * If markers don't include `"*"`, this adds `"*"` to allow JSDoc comments.
36 * @param {string[]} [markers] A marker list.
37 * @returns {string[]} A marker list.
38 */
39function parseMarkersOption(markers) {
40
41 // `*` is a marker for JSDoc comments.
42 if (markers.indexOf("*") === -1) {
43 return markers.concat("*");
44 }
45
46 return markers;
47}
48
49/**
50 * Creates string pattern for exceptions.
51 * Generated pattern:
52 *
53 * 1. A space or an exception pattern sequence.
54 * @param {string[]} exceptions An exception pattern list.
55 * @returns {string} A regular expression string for exceptions.
56 */
57function createExceptionsPattern(exceptions) {
58 let pattern = "";
59
60 /*
61 * A space or an exception pattern sequence.
62 * [] ==> "\s"
63 * ["-"] ==> "(?:\s|\-+$)"
64 * ["-", "="] ==> "(?:\s|(?:\-+|=+)$)"
65 * ["-", "=", "--=="] ==> "(?:\s|(?:\-+|=+|(?:\-\-==)+)$)" ==> https://jex.im/regulex/#!embed=false&flags=&re=(%3F%3A%5Cs%7C(%3F%3A%5C-%2B%7C%3D%2B%7C(%3F%3A%5C-%5C-%3D%3D)%2B)%24)
66 */
67 if (exceptions.length === 0) {
68
69 // a space.
70 pattern += "\\s";
71 } else {
72
73 // a space or...
74 pattern += "(?:\\s|";
75
76 if (exceptions.length === 1) {
77
78 // a sequence of the exception pattern.
79 pattern += escapeAndRepeat(exceptions[0]);
80 } else {
81
82 // a sequence of one of the exception patterns.
83 pattern += "(?:";
84 pattern += exceptions.map(escapeAndRepeat).join("|");
85 pattern += ")";
86 }
87 pattern += `(?:$|[${Array.from(astUtils.LINEBREAKS).join("")}]))`;
88 }
89
90 return pattern;
91}
92
93/**
94 * Creates RegExp object for `always` mode.
95 * Generated pattern for beginning of comment:
96 *
97 * 1. First, a marker or nothing.
98 * 2. Next, a space or an exception pattern sequence.
99 * @param {string[]} markers A marker list.
100 * @param {string[]} exceptions An exception pattern list.
101 * @returns {RegExp} A RegExp object for the beginning of a comment in `always` mode.
102 */
103function createAlwaysStylePattern(markers, exceptions) {
104 let pattern = "^";
105
106 /*
107 * A marker or nothing.
108 * ["*"] ==> "\*?"
109 * ["*", "!"] ==> "(?:\*|!)?"
110 * ["*", "/", "!<"] ==> "(?:\*|\/|(?:!<))?" ==> https://jex.im/regulex/#!embed=false&flags=&re=(%3F%3A%5C*%7C%5C%2F%7C(%3F%3A!%3C))%3F
111 */
112 if (markers.length === 1) {
113
114 // the marker.
115 pattern += escape(markers[0]);
116 } else {
117
118 // one of markers.
119 pattern += "(?:";
120 pattern += markers.map(escape).join("|");
121 pattern += ")";
122 }
123
124 pattern += "?"; // or nothing.
125 pattern += createExceptionsPattern(exceptions);
126
127 return new RegExp(pattern, "u");
128}
129
130/**
131 * Creates RegExp object for `never` mode.
132 * Generated pattern for beginning of comment:
133 *
134 * 1. First, a marker or nothing (captured).
135 * 2. Next, a space or a tab.
136 * @param {string[]} markers A marker list.
137 * @returns {RegExp} A RegExp object for `never` mode.
138 */
139function createNeverStylePattern(markers) {
140 const pattern = `^(${markers.map(escape).join("|")})?[ \t]+`;
141
142 return new RegExp(pattern, "u");
143}
144
145//------------------------------------------------------------------------------
146// Rule Definition
147//------------------------------------------------------------------------------
148
149module.exports = {
150 meta: {
151 type: "suggestion",
152
153 docs: {
154 description: "enforce consistent spacing after the `//` or `/*` in a comment",
155 category: "Stylistic Issues",
156 recommended: false,
157 url: "https://eslint.org/docs/rules/spaced-comment"
158 },
159
160 fixable: "whitespace",
161
162 schema: [
163 {
164 enum: ["always", "never"]
165 },
166 {
167 type: "object",
168 properties: {
169 exceptions: {
170 type: "array",
171 items: {
172 type: "string"
173 }
174 },
175 markers: {
176 type: "array",
177 items: {
178 type: "string"
179 }
180 },
181 line: {
182 type: "object",
183 properties: {
184 exceptions: {
185 type: "array",
186 items: {
187 type: "string"
188 }
189 },
190 markers: {
191 type: "array",
192 items: {
193 type: "string"
194 }
195 }
196 },
197 additionalProperties: false
198 },
199 block: {
200 type: "object",
201 properties: {
202 exceptions: {
203 type: "array",
204 items: {
205 type: "string"
206 }
207 },
208 markers: {
209 type: "array",
210 items: {
211 type: "string"
212 }
213 },
214 balanced: {
215 type: "boolean",
216 default: false
217 }
218 },
219 additionalProperties: false
220 }
221 },
222 additionalProperties: false
223 }
224 ],
225
226 messages: {
227 unexpectedSpaceAfterMarker: "Unexpected space or tab after marker ({{refChar}}) in comment.",
228 expectedExceptionAfter: "Expected exception block, space or tab after '{{refChar}}' in comment.",
229 unexpectedSpaceBefore: "Unexpected space or tab before '*/' in comment.",
230 unexpectedSpaceAfter: "Unexpected space or tab after '{{refChar}}' in comment.",
231 expectedSpaceBefore: "Expected space or tab before '*/' in comment.",
232 expectedSpaceAfter: "Expected space or tab after '{{refChar}}' in comment."
233 }
234 },
235
236 create(context) {
237
238 const sourceCode = context.getSourceCode();
239
240 // Unless the first option is never, require a space
241 const requireSpace = context.options[0] !== "never";
242
243 /*
244 * Parse the second options.
245 * If markers don't include `"*"`, it's added automatically for JSDoc
246 * comments.
247 */
248 const config = context.options[1] || {};
249 const balanced = config.block && config.block.balanced;
250
251 const styleRules = ["block", "line"].reduce((rule, type) => {
252 const markers = parseMarkersOption(config[type] && config[type].markers || config.markers || []);
253 const exceptions = config[type] && config[type].exceptions || config.exceptions || [];
254 const endNeverPattern = "[ \t]+$";
255
256 // Create RegExp object for valid patterns.
257 rule[type] = {
258 beginRegex: requireSpace ? createAlwaysStylePattern(markers, exceptions) : createNeverStylePattern(markers),
259 endRegex: balanced && requireSpace ? new RegExp(`${createExceptionsPattern(exceptions)}$`, "u") : new RegExp(endNeverPattern, "u"),
260 hasExceptions: exceptions.length > 0,
261 captureMarker: new RegExp(`^(${markers.map(escape).join("|")})`, "u"),
262 markers: new Set(markers)
263 };
264
265 return rule;
266 }, {});
267
268 /**
269 * Reports a beginning spacing error with an appropriate message.
270 * @param {ASTNode} node A comment node to check.
271 * @param {string} messageId An error message to report.
272 * @param {Array} match An array of match results for markers.
273 * @param {string} refChar Character used for reference in the error message.
274 * @returns {void}
275 */
276 function reportBegin(node, messageId, match, refChar) {
277 const type = node.type.toLowerCase(),
278 commentIdentifier = type === "block" ? "/*" : "//";
279
280 context.report({
281 node,
282 fix(fixer) {
283 const start = node.range[0];
284 let end = start + 2;
285
286 if (requireSpace) {
287 if (match) {
288 end += match[0].length;
289 }
290 return fixer.insertTextAfterRange([start, end], " ");
291 }
292 end += match[0].length;
293 return fixer.replaceTextRange([start, end], commentIdentifier + (match[1] ? match[1] : ""));
294
295 },
296 messageId,
297 data: { refChar }
298 });
299 }
300
301 /**
302 * Reports an ending spacing error with an appropriate message.
303 * @param {ASTNode} node A comment node to check.
304 * @param {string} messageId An error message to report.
305 * @param {string} match An array of the matched whitespace characters.
306 * @returns {void}
307 */
308 function reportEnd(node, messageId, match) {
309 context.report({
310 node,
311 fix(fixer) {
312 if (requireSpace) {
313 return fixer.insertTextAfterRange([node.range[0], node.range[1] - 2], " ");
314 }
315 const end = node.range[1] - 2,
316 start = end - match[0].length;
317
318 return fixer.replaceTextRange([start, end], "");
319
320 },
321 messageId
322 });
323 }
324
325 /**
326 * Reports a given comment if it's invalid.
327 * @param {ASTNode} node a comment node to check.
328 * @returns {void}
329 */
330 function checkCommentForSpace(node) {
331 const type = node.type.toLowerCase(),
332 rule = styleRules[type],
333 commentIdentifier = type === "block" ? "/*" : "//";
334
335 // Ignores empty comments and comments that consist only of a marker.
336 if (node.value.length === 0 || rule.markers.has(node.value)) {
337 return;
338 }
339
340 const beginMatch = rule.beginRegex.exec(node.value);
341 const endMatch = rule.endRegex.exec(node.value);
342
343 // Checks.
344 if (requireSpace) {
345 if (!beginMatch) {
346 const hasMarker = rule.captureMarker.exec(node.value);
347 const marker = hasMarker ? commentIdentifier + hasMarker[0] : commentIdentifier;
348
349 if (rule.hasExceptions) {
350 reportBegin(node, "expectedExceptionAfter", hasMarker, marker);
351 } else {
352 reportBegin(node, "expectedSpaceAfter", hasMarker, marker);
353 }
354 }
355
356 if (balanced && type === "block" && !endMatch) {
357 reportEnd(node, "expectedSpaceBefore");
358 }
359 } else {
360 if (beginMatch) {
361 if (!beginMatch[1]) {
362 reportBegin(node, "unexpectedSpaceAfter", beginMatch, commentIdentifier);
363 } else {
364 reportBegin(node, "unexpectedSpaceAfterMarker", beginMatch, beginMatch[1]);
365 }
366 }
367
368 if (balanced && type === "block" && endMatch) {
369 reportEnd(node, "unexpectedSpaceBefore", endMatch);
370 }
371 }
372 }
373
374 return {
375 Program() {
376 const comments = sourceCode.getAllComments();
377
378 comments.filter(token => token.type !== "Shebang").forEach(checkCommentForSpace);
379 }
380 };
381 }
382};