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