semi.js 12 KB

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