• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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};
383