1/** 2 * @fileoverview Rule to flag missing semicolons. 3 * @author Nicholas C. Zakas 4 */ 5"use strict"; 6 7//------------------------------------------------------------------------------ 8// Requirements 9//------------------------------------------------------------------------------ 10 11const FixTracker = require("./utils/fix-tracker"); 12const astUtils = require("./utils/ast-utils"); 13 14//------------------------------------------------------------------------------ 15// Rule Definition 16//------------------------------------------------------------------------------ 17 18module.exports = { 19 meta: { 20 type: "layout", 21 22 docs: { 23 description: "require or disallow semicolons instead of ASI", 24 category: "Stylistic Issues", 25 recommended: false, 26 url: "https://eslint.org/docs/rules/semi" 27 }, 28 29 fixable: "code", 30 31 schema: { 32 anyOf: [ 33 { 34 type: "array", 35 items: [ 36 { 37 enum: ["never"] 38 }, 39 { 40 type: "object", 41 properties: { 42 beforeStatementContinuationChars: { 43 enum: ["always", "any", "never"] 44 } 45 }, 46 additionalProperties: false 47 } 48 ], 49 minItems: 0, 50 maxItems: 2 51 }, 52 { 53 type: "array", 54 items: [ 55 { 56 enum: ["always"] 57 }, 58 { 59 type: "object", 60 properties: { 61 omitLastInOneLineBlock: { type: "boolean" } 62 }, 63 additionalProperties: false 64 } 65 ], 66 minItems: 0, 67 maxItems: 2 68 } 69 ] 70 }, 71 72 messages: { 73 missingSemi: "Missing semicolon.", 74 extraSemi: "Extra semicolon." 75 } 76 }, 77 78 create(context) { 79 80 const OPT_OUT_PATTERN = /^[-[(/+`]/u; // One of [(/+-` 81 const options = context.options[1]; 82 const never = context.options[0] === "never"; 83 const exceptOneLine = Boolean(options && options.omitLastInOneLineBlock); 84 const beforeStatementContinuationChars = options && options.beforeStatementContinuationChars || "any"; 85 const sourceCode = context.getSourceCode(); 86 87 //-------------------------------------------------------------------------- 88 // Helpers 89 //-------------------------------------------------------------------------- 90 91 /** 92 * Reports a semicolon error with appropriate location and message. 93 * @param {ASTNode} node The node with an extra or missing semicolon. 94 * @param {boolean} missing True if the semicolon is missing. 95 * @returns {void} 96 */ 97 function report(node, missing) { 98 const lastToken = sourceCode.getLastToken(node); 99 let messageId, 100 fix, 101 loc; 102 103 if (!missing) { 104 messageId = "missingSemi"; 105 loc = { 106 start: lastToken.loc.end, 107 end: astUtils.getNextLocation(sourceCode, lastToken.loc.end) 108 }; 109 fix = function(fixer) { 110 return fixer.insertTextAfter(lastToken, ";"); 111 }; 112 } else { 113 messageId = "extraSemi"; 114 loc = lastToken.loc; 115 fix = function(fixer) { 116 117 /* 118 * Expand the replacement range to include the surrounding 119 * tokens to avoid conflicting with no-extra-semi. 120 * https://github.com/eslint/eslint/issues/7928 121 */ 122 return new FixTracker(fixer, sourceCode) 123 .retainSurroundingTokens(lastToken) 124 .remove(lastToken); 125 }; 126 } 127 128 context.report({ 129 node, 130 loc, 131 messageId, 132 fix 133 }); 134 135 } 136 137 /** 138 * Check whether a given semicolon token is redundant. 139 * @param {Token} semiToken A semicolon token to check. 140 * @returns {boolean} `true` if the next token is `;` or `}`. 141 */ 142 function isRedundantSemi(semiToken) { 143 const nextToken = sourceCode.getTokenAfter(semiToken); 144 145 return ( 146 !nextToken || 147 astUtils.isClosingBraceToken(nextToken) || 148 astUtils.isSemicolonToken(nextToken) 149 ); 150 } 151 152 /** 153 * Check whether a given token is the closing brace of an arrow function. 154 * @param {Token} lastToken A token to check. 155 * @returns {boolean} `true` if the token is the closing brace of an arrow function. 156 */ 157 function isEndOfArrowBlock(lastToken) { 158 if (!astUtils.isClosingBraceToken(lastToken)) { 159 return false; 160 } 161 const node = sourceCode.getNodeByRangeIndex(lastToken.range[0]); 162 163 return ( 164 node.type === "BlockStatement" && 165 node.parent.type === "ArrowFunctionExpression" 166 ); 167 } 168 169 /** 170 * Check whether a given node is on the same line with the next token. 171 * @param {Node} node A statement node to check. 172 * @returns {boolean} `true` if the node is on the same line with the next token. 173 */ 174 function isOnSameLineWithNextToken(node) { 175 const prevToken = sourceCode.getLastToken(node, 1); 176 const nextToken = sourceCode.getTokenAfter(node); 177 178 return !!nextToken && astUtils.isTokenOnSameLine(prevToken, nextToken); 179 } 180 181 /** 182 * Check whether a given node can connect the next line if the next line is unreliable. 183 * @param {Node} node A statement node to check. 184 * @returns {boolean} `true` if the node can connect the next line. 185 */ 186 function maybeAsiHazardAfter(node) { 187 const t = node.type; 188 189 if (t === "DoWhileStatement" || 190 t === "BreakStatement" || 191 t === "ContinueStatement" || 192 t === "DebuggerStatement" || 193 t === "ImportDeclaration" || 194 t === "ExportAllDeclaration" 195 ) { 196 return false; 197 } 198 if (t === "ReturnStatement") { 199 return Boolean(node.argument); 200 } 201 if (t === "ExportNamedDeclaration") { 202 return Boolean(node.declaration); 203 } 204 if (isEndOfArrowBlock(sourceCode.getLastToken(node, 1))) { 205 return false; 206 } 207 208 return true; 209 } 210 211 /** 212 * Check whether a given token can connect the previous statement. 213 * @param {Token} token A token to check. 214 * @returns {boolean} `true` if the token is one of `[`, `(`, `/`, `+`, `-`, ```, `++`, and `--`. 215 */ 216 function maybeAsiHazardBefore(token) { 217 return ( 218 Boolean(token) && 219 OPT_OUT_PATTERN.test(token.value) && 220 token.value !== "++" && 221 token.value !== "--" 222 ); 223 } 224 225 /** 226 * Check if the semicolon of a given node is unnecessary, only true if: 227 * - next token is a valid statement divider (`;` or `}`). 228 * - next token is on a new line and the node is not connectable to the new line. 229 * @param {Node} node A statement node to check. 230 * @returns {boolean} whether the semicolon is unnecessary. 231 */ 232 function canRemoveSemicolon(node) { 233 if (isRedundantSemi(sourceCode.getLastToken(node))) { 234 return true; // `;;` or `;}` 235 } 236 if (isOnSameLineWithNextToken(node)) { 237 return false; // One liner. 238 } 239 if (beforeStatementContinuationChars === "never" && !maybeAsiHazardAfter(node)) { 240 return true; // ASI works. This statement doesn't connect to the next. 241 } 242 if (!maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) { 243 return true; // ASI works. The next token doesn't connect to this statement. 244 } 245 246 return false; 247 } 248 249 /** 250 * Checks a node to see if it's in a one-liner block statement. 251 * @param {ASTNode} node The node to check. 252 * @returns {boolean} whether the node is in a one-liner block statement. 253 */ 254 function isOneLinerBlock(node) { 255 const parent = node.parent; 256 const nextToken = sourceCode.getTokenAfter(node); 257 258 if (!nextToken || nextToken.value !== "}") { 259 return false; 260 } 261 return ( 262 !!parent && 263 parent.type === "BlockStatement" && 264 parent.loc.start.line === parent.loc.end.line 265 ); 266 } 267 268 /** 269 * Checks a node to see if it's followed by a semicolon. 270 * @param {ASTNode} node The node to check. 271 * @returns {void} 272 */ 273 function checkForSemicolon(node) { 274 const isSemi = astUtils.isSemicolonToken(sourceCode.getLastToken(node)); 275 276 if (never) { 277 if (isSemi && canRemoveSemicolon(node)) { 278 report(node, true); 279 } else if (!isSemi && beforeStatementContinuationChars === "always" && maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) { 280 report(node); 281 } 282 } else { 283 const oneLinerBlock = (exceptOneLine && isOneLinerBlock(node)); 284 285 if (isSemi && oneLinerBlock) { 286 report(node, true); 287 } else if (!isSemi && !oneLinerBlock) { 288 report(node); 289 } 290 } 291 } 292 293 /** 294 * Checks to see if there's a semicolon after a variable declaration. 295 * @param {ASTNode} node The node to check. 296 * @returns {void} 297 */ 298 function checkForSemicolonForVariableDeclaration(node) { 299 const parent = node.parent; 300 301 if ((parent.type !== "ForStatement" || parent.init !== node) && 302 (!/^For(?:In|Of)Statement/u.test(parent.type) || parent.left !== node) 303 ) { 304 checkForSemicolon(node); 305 } 306 } 307 308 //-------------------------------------------------------------------------- 309 // Public API 310 //-------------------------------------------------------------------------- 311 312 return { 313 VariableDeclaration: checkForSemicolonForVariableDeclaration, 314 ExpressionStatement: checkForSemicolon, 315 ReturnStatement: checkForSemicolon, 316 ThrowStatement: checkForSemicolon, 317 DoWhileStatement: checkForSemicolon, 318 DebuggerStatement: checkForSemicolon, 319 BreakStatement: checkForSemicolon, 320 ContinueStatement: checkForSemicolon, 321 ImportDeclaration: checkForSemicolon, 322 ExportAllDeclaration: checkForSemicolon, 323 ExportNamedDeclaration(node) { 324 if (!node.declaration) { 325 checkForSemicolon(node); 326 } 327 }, 328 ExportDefaultDeclaration(node) { 329 if (!/(?:Class|Function)Declaration/u.test(node.declaration.type)) { 330 checkForSemicolon(node); 331 } 332 } 333 }; 334 335 } 336}; 337