UNPKG

12.3 kBJavaScriptView Raw
1/**
2 * @fileoverview Source code for spaced-comments rule
3 * @author Gyandeep Singh
4 */
5"use strict";
6
7const lodash = require("lodash");
8
9//------------------------------------------------------------------------------
10// Helpers
11//------------------------------------------------------------------------------
12
13/**
14 * Escapes the control characters of a given string.
15 * @param {string} s - A string to escape.
16 * @returns {string} An escaped string.
17 */
18function escape(s) {
19 const isOneChar = s.length === 1;
20
21 s = lodash.escapeRegExp(s);
22 return isOneChar ? s : `(?:${s})`;
23}
24
25/**
26 * Escapes the control characters of a given string.
27 * And adds a repeat flag.
28 * @param {string} s - A string to escape.
29 * @returns {string} An escaped string.
30 */
31function escapeAndRepeat(s) {
32 return `${escape(s)}+`;
33}
34
35/**
36 * Parses `markers` option.
37 * If markers don't include `"*"`, this adds `"*"` to allow JSDoc comments.
38 * @param {string[]} [markers] - A marker list.
39 * @returns {string[]} A marker list.
40 */
41function parseMarkersOption(markers) {
42 markers = markers ? markers.slice(0) : [];
43
44 // `*` is a marker for JSDoc comments.
45 if (markers.indexOf("*") === -1) {
46 markers.push("*");
47 }
48
49 return markers;
50}
51
52/**
53 * Creates string pattern for exceptions.
54 * Generated pattern:
55 *
56 * 1. A space or an exception pattern sequence.
57 *
58 * @param {string[]} exceptions - An exception pattern list.
59 * @returns {string} A regular expression string for exceptions.
60 */
61function createExceptionsPattern(exceptions) {
62 let pattern = "";
63
64 /*
65 * A space or an exception pattern sequence.
66 * [] ==> "\s"
67 * ["-"] ==> "(?:\s|\-+$)"
68 * ["-", "="] ==> "(?:\s|(?:\-+|=+)$)"
69 * ["-", "=", "--=="] ==> "(?:\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)
70 */
71 if (exceptions.length === 0) {
72
73 // a space.
74 pattern += "\\s";
75 } else {
76
77 // a space or...
78 pattern += "(?:\\s|";
79
80 if (exceptions.length === 1) {
81
82 // a sequence of the exception pattern.
83 pattern += escapeAndRepeat(exceptions[0]);
84 } else {
85
86 // a sequence of one of the exception patterns.
87 pattern += "(?:";
88 pattern += exceptions.map(escapeAndRepeat).join("|");
89 pattern += ")";
90 }
91
92 pattern += "(?:$|[\n\r]))";
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 },
163
164 fixable: "whitespace",
165
166 schema: [
167 {
168 enum: ["always", "never"]
169 },
170 {
171 type: "object",
172 properties: {
173 exceptions: {
174 type: "array",
175 items: {
176 type: "string"
177 }
178 },
179 markers: {
180 type: "array",
181 items: {
182 type: "string"
183 }
184 },
185 line: {
186 type: "object",
187 properties: {
188 exceptions: {
189 type: "array",
190 items: {
191 type: "string"
192 }
193 },
194 markers: {
195 type: "array",
196 items: {
197 type: "string"
198 }
199 }
200 },
201 additionalProperties: false
202 },
203 block: {
204 type: "object",
205 properties: {
206 exceptions: {
207 type: "array",
208 items: {
209 type: "string"
210 }
211 },
212 markers: {
213 type: "array",
214 items: {
215 type: "string"
216 }
217 },
218 balanced: {
219 type: "boolean"
220 }
221 },
222 additionalProperties: false
223 }
224 },
225 additionalProperties: false
226 }
227 ]
228 },
229
230 create(context) {
231
232 // Unless the first option is never, require a space
233 const requireSpace = context.options[0] !== "never";
234
235 /*
236 * Parse the second options.
237 * If markers don't include `"*"`, it's added automatically for JSDoc
238 * comments.
239 */
240 const config = context.options[1] || {};
241 const balanced = config.block && config.block.balanced;
242
243 const styleRules = ["block", "line"].reduce(function(rule, type) {
244 const markers = parseMarkersOption(config[type] && config[type].markers || config.markers);
245 const exceptions = config[type] && config[type].exceptions || config.exceptions || [];
246 const endNeverPattern = "[ \t]+$";
247
248 // Create RegExp object for valid patterns.
249 rule[type] = {
250 beginRegex: requireSpace ? createAlwaysStylePattern(markers, exceptions) : createNeverStylePattern(markers),
251 endRegex: balanced && requireSpace ? new RegExp(`${createExceptionsPattern(exceptions)}$`) : new RegExp(endNeverPattern),
252 hasExceptions: exceptions.length > 0,
253 markers: new RegExp(`^(${markers.map(escape).join("|")})`)
254 };
255
256 return rule;
257 }, {});
258
259 /**
260 * Reports a beginning spacing error with an appropriate message.
261 * @param {ASTNode} node - A comment node to check.
262 * @param {string} message - An error message to report.
263 * @param {Array} match - An array of match results for markers.
264 * @param {string} refChar - Character used for reference in the error message.
265 * @returns {void}
266 */
267 function reportBegin(node, message, match, refChar) {
268 const type = node.type.toLowerCase(),
269 commentIdentifier = type === "block" ? "/*" : "//";
270
271 context.report({
272 node,
273 fix(fixer) {
274 const start = node.range[0];
275 let end = start + 2;
276
277 if (requireSpace) {
278 if (match) {
279 end += match[0].length;
280 }
281 return fixer.insertTextAfterRange([start, end], " ");
282 } else {
283 end += match[0].length;
284 return fixer.replaceTextRange([start, end], commentIdentifier + (match[1] ? match[1] : ""));
285 }
286 },
287 message,
288 data: { refChar }
289 });
290 }
291
292 /**
293 * Reports an ending spacing error with an appropriate message.
294 * @param {ASTNode} node - A comment node to check.
295 * @param {string} message - An error message to report.
296 * @param {string} match - An array of the matched whitespace characters.
297 * @returns {void}
298 */
299 function reportEnd(node, message, match) {
300 context.report({
301 node,
302 fix(fixer) {
303 if (requireSpace) {
304 return fixer.insertTextAfterRange([node.start, node.end - 2], " ");
305 } else {
306 const end = node.end - 2,
307 start = end - match[0].length;
308
309 return fixer.replaceTextRange([start, end], "");
310 }
311 },
312 message
313 });
314 }
315
316 /**
317 * Reports a given comment if it's invalid.
318 * @param {ASTNode} node - a comment node to check.
319 * @returns {void}
320 */
321 function checkCommentForSpace(node) {
322 const type = node.type.toLowerCase(),
323 rule = styleRules[type],
324 commentIdentifier = type === "block" ? "/*" : "//";
325
326 // Ignores empty comments.
327 if (node.value.length === 0) {
328 return;
329 }
330
331 const beginMatch = rule.beginRegex.exec(node.value);
332 const endMatch = rule.endRegex.exec(node.value);
333
334 // Checks.
335 if (requireSpace) {
336 if (!beginMatch) {
337 const hasMarker = rule.markers.exec(node.value);
338 const marker = hasMarker ? commentIdentifier + hasMarker[0] : commentIdentifier;
339
340 if (rule.hasExceptions) {
341 reportBegin(node, "Expected exception block, space or tab after '{{refChar}}' in comment.", hasMarker, marker);
342 } else {
343 reportBegin(node, "Expected space or tab after '{{refChar}}' in comment.", hasMarker, marker);
344 }
345 }
346
347 if (balanced && type === "block" && !endMatch) {
348 reportEnd(node, "Expected space or tab before '*/' in comment.");
349 }
350 } else {
351 if (beginMatch) {
352 if (!beginMatch[1]) {
353 reportBegin(node, "Unexpected space or tab after '{{refChar}}' in comment.", beginMatch, commentIdentifier);
354 } else {
355 reportBegin(node, "Unexpected space or tab after marker ({{refChar}}) in comment.", beginMatch, beginMatch[1]);
356 }
357 }
358
359 if (balanced && type === "block" && endMatch) {
360 reportEnd(node, "Unexpected space or tab before '*/' in comment.", endMatch);
361 }
362 }
363 }
364
365 return {
366
367 LineComment: checkCommentForSpace,
368 BlockComment: checkCommentForSpace
369
370 };
371 }
372};