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