UNPKG

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