| /** |
| * @fileoverview Source code for spaced-comments rule |
| * @author Gyandeep Singh |
| */ |
| "use strict"; |
| |
| const lodash = require("lodash"); |
| const astUtils = require("./utils/ast-utils"); |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| /** |
| * Escapes the control characters of a given string. |
| * @param {string} s - A string to escape. |
| * @returns {string} An escaped string. |
| */ |
| function escape(s) { |
| return `(?:${lodash.escapeRegExp(s)})`; |
| } |
| |
| /** |
| * Escapes the control characters of a given string. |
| * And adds a repeat flag. |
| * @param {string} s - A string to escape. |
| * @returns {string} An escaped string. |
| */ |
| function escapeAndRepeat(s) { |
| return `${escape(s)}+`; |
| } |
| |
| /** |
| * Parses `markers` option. |
| * If markers don't include `"*"`, this adds `"*"` to allow JSDoc comments. |
| * @param {string[]} [markers] - A marker list. |
| * @returns {string[]} A marker list. |
| */ |
| function parseMarkersOption(markers) { |
| |
| // `*` is a marker for JSDoc comments. |
| if (markers.indexOf("*") === -1) { |
| return markers.concat("*"); |
| } |
| |
| return markers; |
| } |
| |
| /** |
| * Creates string pattern for exceptions. |
| * Generated pattern: |
| * |
| * 1. A space or an exception pattern sequence. |
| * |
| * @param {string[]} exceptions - An exception pattern list. |
| * @returns {string} A regular expression string for exceptions. |
| */ |
| function createExceptionsPattern(exceptions) { |
| let pattern = ""; |
| |
| /* |
| * A space or an exception pattern sequence. |
| * [] ==> "\s" |
| * ["-"] ==> "(?:\s|\-+$)" |
| * ["-", "="] ==> "(?:\s|(?:\-+|=+)$)" |
| * ["-", "=", "--=="] ==> "(?:\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) |
| */ |
| if (exceptions.length === 0) { |
| |
| // a space. |
| pattern += "\\s"; |
| } else { |
| |
| // a space or... |
| pattern += "(?:\\s|"; |
| |
| if (exceptions.length === 1) { |
| |
| // a sequence of the exception pattern. |
| pattern += escapeAndRepeat(exceptions[0]); |
| } else { |
| |
| // a sequence of one of the exception patterns. |
| pattern += "(?:"; |
| pattern += exceptions.map(escapeAndRepeat).join("|"); |
| pattern += ")"; |
| } |
| pattern += `(?:$|[${Array.from(astUtils.LINEBREAKS).join("")}]))`; |
| } |
| |
| return pattern; |
| } |
| |
| /** |
| * Creates RegExp object for `always` mode. |
| * Generated pattern for beginning of comment: |
| * |
| * 1. First, a marker or nothing. |
| * 2. Next, a space or an exception pattern sequence. |
| * |
| * @param {string[]} markers - A marker list. |
| * @param {string[]} exceptions - An exception pattern list. |
| * @returns {RegExp} A RegExp object for the beginning of a comment in `always` mode. |
| */ |
| function createAlwaysStylePattern(markers, exceptions) { |
| let pattern = "^"; |
| |
| /* |
| * A marker or nothing. |
| * ["*"] ==> "\*?" |
| * ["*", "!"] ==> "(?:\*|!)?" |
| * ["*", "/", "!<"] ==> "(?:\*|\/|(?:!<))?" ==> https://jex.im/regulex/#!embed=false&flags=&re=(%3F%3A%5C*%7C%5C%2F%7C(%3F%3A!%3C))%3F |
| */ |
| if (markers.length === 1) { |
| |
| // the marker. |
| pattern += escape(markers[0]); |
| } else { |
| |
| // one of markers. |
| pattern += "(?:"; |
| pattern += markers.map(escape).join("|"); |
| pattern += ")"; |
| } |
| |
| pattern += "?"; // or nothing. |
| pattern += createExceptionsPattern(exceptions); |
| |
| return new RegExp(pattern, "u"); |
| } |
| |
| /** |
| * Creates RegExp object for `never` mode. |
| * Generated pattern for beginning of comment: |
| * |
| * 1. First, a marker or nothing (captured). |
| * 2. Next, a space or a tab. |
| * |
| * @param {string[]} markers - A marker list. |
| * @returns {RegExp} A RegExp object for `never` mode. |
| */ |
| function createNeverStylePattern(markers) { |
| const pattern = `^(${markers.map(escape).join("|")})?[ \t]+`; |
| |
| return new RegExp(pattern, "u"); |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| type: "suggestion", |
| |
| docs: { |
| description: "enforce consistent spacing after the `//` or `/*` in a comment", |
| category: "Stylistic Issues", |
| recommended: false, |
| url: "https://eslint.org/docs/rules/spaced-comment" |
| }, |
| |
| fixable: "whitespace", |
| |
| schema: [ |
| { |
| enum: ["always", "never"] |
| }, |
| { |
| type: "object", |
| properties: { |
| exceptions: { |
| type: "array", |
| items: { |
| type: "string" |
| } |
| }, |
| markers: { |
| type: "array", |
| items: { |
| type: "string" |
| } |
| }, |
| line: { |
| type: "object", |
| properties: { |
| exceptions: { |
| type: "array", |
| items: { |
| type: "string" |
| } |
| }, |
| markers: { |
| type: "array", |
| items: { |
| type: "string" |
| } |
| } |
| }, |
| additionalProperties: false |
| }, |
| block: { |
| type: "object", |
| properties: { |
| exceptions: { |
| type: "array", |
| items: { |
| type: "string" |
| } |
| }, |
| markers: { |
| type: "array", |
| items: { |
| type: "string" |
| } |
| }, |
| balanced: { |
| type: "boolean", |
| default: false |
| } |
| }, |
| additionalProperties: false |
| } |
| }, |
| additionalProperties: false |
| } |
| ] |
| }, |
| |
| create(context) { |
| |
| const sourceCode = context.getSourceCode(); |
| |
| // Unless the first option is never, require a space |
| const requireSpace = context.options[0] !== "never"; |
| |
| /* |
| * Parse the second options. |
| * If markers don't include `"*"`, it's added automatically for JSDoc |
| * comments. |
| */ |
| const config = context.options[1] || {}; |
| const balanced = config.block && config.block.balanced; |
| |
| const styleRules = ["block", "line"].reduce((rule, type) => { |
| const markers = parseMarkersOption(config[type] && config[type].markers || config.markers || []); |
| const exceptions = config[type] && config[type].exceptions || config.exceptions || []; |
| const endNeverPattern = "[ \t]+$"; |
| |
| // Create RegExp object for valid patterns. |
| rule[type] = { |
| beginRegex: requireSpace ? createAlwaysStylePattern(markers, exceptions) : createNeverStylePattern(markers), |
| endRegex: balanced && requireSpace ? new RegExp(`${createExceptionsPattern(exceptions)}$`, "u") : new RegExp(endNeverPattern, "u"), |
| hasExceptions: exceptions.length > 0, |
| markers: new RegExp(`^(${markers.map(escape).join("|")})`, "u") |
| }; |
| |
| return rule; |
| }, {}); |
| |
| /** |
| * Reports a beginning spacing error with an appropriate message. |
| * @param {ASTNode} node - A comment node to check. |
| * @param {string} message - An error message to report. |
| * @param {Array} match - An array of match results for markers. |
| * @param {string} refChar - Character used for reference in the error message. |
| * @returns {void} |
| */ |
| function reportBegin(node, message, match, refChar) { |
| const type = node.type.toLowerCase(), |
| commentIdentifier = type === "block" ? "/*" : "//"; |
| |
| context.report({ |
| node, |
| fix(fixer) { |
| const start = node.range[0]; |
| let end = start + 2; |
| |
| if (requireSpace) { |
| if (match) { |
| end += match[0].length; |
| } |
| return fixer.insertTextAfterRange([start, end], " "); |
| } |
| end += match[0].length; |
| return fixer.replaceTextRange([start, end], commentIdentifier + (match[1] ? match[1] : "")); |
| |
| }, |
| message, |
| data: { refChar } |
| }); |
| } |
| |
| /** |
| * Reports an ending spacing error with an appropriate message. |
| * @param {ASTNode} node - A comment node to check. |
| * @param {string} message - An error message to report. |
| * @param {string} match - An array of the matched whitespace characters. |
| * @returns {void} |
| */ |
| function reportEnd(node, message, match) { |
| context.report({ |
| node, |
| fix(fixer) { |
| if (requireSpace) { |
| return fixer.insertTextAfterRange([node.range[0], node.range[1] - 2], " "); |
| } |
| const end = node.range[1] - 2, |
| start = end - match[0].length; |
| |
| return fixer.replaceTextRange([start, end], ""); |
| |
| }, |
| message |
| }); |
| } |
| |
| /** |
| * Reports a given comment if it's invalid. |
| * @param {ASTNode} node - a comment node to check. |
| * @returns {void} |
| */ |
| function checkCommentForSpace(node) { |
| const type = node.type.toLowerCase(), |
| rule = styleRules[type], |
| commentIdentifier = type === "block" ? "/*" : "//"; |
| |
| // Ignores empty comments. |
| if (node.value.length === 0) { |
| return; |
| } |
| |
| const beginMatch = rule.beginRegex.exec(node.value); |
| const endMatch = rule.endRegex.exec(node.value); |
| |
| // Checks. |
| if (requireSpace) { |
| if (!beginMatch) { |
| const hasMarker = rule.markers.exec(node.value); |
| const marker = hasMarker ? commentIdentifier + hasMarker[0] : commentIdentifier; |
| |
| if (rule.hasExceptions) { |
| reportBegin(node, "Expected exception block, space or tab after '{{refChar}}' in comment.", hasMarker, marker); |
| } else { |
| reportBegin(node, "Expected space or tab after '{{refChar}}' in comment.", hasMarker, marker); |
| } |
| } |
| |
| if (balanced && type === "block" && !endMatch) { |
| reportEnd(node, "Expected space or tab before '*/' in comment."); |
| } |
| } else { |
| if (beginMatch) { |
| if (!beginMatch[1]) { |
| reportBegin(node, "Unexpected space or tab after '{{refChar}}' in comment.", beginMatch, commentIdentifier); |
| } else { |
| reportBegin(node, "Unexpected space or tab after marker ({{refChar}}) in comment.", beginMatch, beginMatch[1]); |
| } |
| } |
| |
| if (balanced && type === "block" && endMatch) { |
| reportEnd(node, "Unexpected space or tab before '*/' in comment.", endMatch); |
| } |
| } |
| } |
| |
| return { |
| Program() { |
| const comments = sourceCode.getAllComments(); |
| |
| comments.filter(token => token.type !== "Shebang").forEach(checkCommentForSpace); |
| } |
| }; |
| } |
| }; |