semi.js 13 KB

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