semi.js 16 KB

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