semi.js 12 KB

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