curly.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. /**
  2. * @fileoverview Rule to flag statements without curly braces
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Rule Definition
  12. //------------------------------------------------------------------------------
  13. /** @type {import('../types').Rule.RuleModule} */
  14. module.exports = {
  15. meta: {
  16. type: "suggestion",
  17. docs: {
  18. description:
  19. "Enforce consistent brace style for all control statements",
  20. recommended: false,
  21. frozen: true,
  22. url: "https://eslint.org/docs/latest/rules/curly",
  23. },
  24. schema: {
  25. anyOf: [
  26. {
  27. type: "array",
  28. items: [
  29. {
  30. enum: ["all"],
  31. },
  32. ],
  33. minItems: 0,
  34. maxItems: 1,
  35. },
  36. {
  37. type: "array",
  38. items: [
  39. {
  40. enum: ["multi", "multi-line", "multi-or-nest"],
  41. },
  42. {
  43. enum: ["consistent"],
  44. },
  45. ],
  46. minItems: 0,
  47. maxItems: 2,
  48. },
  49. ],
  50. },
  51. defaultOptions: ["all"],
  52. fixable: "code",
  53. messages: {
  54. missingCurlyAfter: "Expected { after '{{name}}'.",
  55. missingCurlyAfterCondition:
  56. "Expected { after '{{name}}' condition.",
  57. unexpectedCurlyAfter: "Unnecessary { after '{{name}}'.",
  58. unexpectedCurlyAfterCondition:
  59. "Unnecessary { after '{{name}}' condition.",
  60. },
  61. },
  62. create(context) {
  63. const multiOnly = context.options[0] === "multi";
  64. const multiLine = context.options[0] === "multi-line";
  65. const multiOrNest = context.options[0] === "multi-or-nest";
  66. const consistent = context.options[1] === "consistent";
  67. const sourceCode = context.sourceCode;
  68. //--------------------------------------------------------------------------
  69. // Helpers
  70. //--------------------------------------------------------------------------
  71. /**
  72. * Determines if a given node is a one-liner that's on the same line as it's preceding code.
  73. * @param {ASTNode} node The node to check.
  74. * @returns {boolean} True if the node is a one-liner that's on the same line as it's preceding code.
  75. * @private
  76. */
  77. function isCollapsedOneLiner(node) {
  78. const before = sourceCode.getTokenBefore(node);
  79. const last = sourceCode.getLastToken(node);
  80. const lastExcludingSemicolon = astUtils.isSemicolonToken(last)
  81. ? sourceCode.getTokenBefore(last)
  82. : last;
  83. return (
  84. before.loc.start.line === lastExcludingSemicolon.loc.end.line
  85. );
  86. }
  87. /**
  88. * Determines if a given node is a one-liner.
  89. * @param {ASTNode} node The node to check.
  90. * @returns {boolean} True if the node is a one-liner.
  91. * @private
  92. */
  93. function isOneLiner(node) {
  94. if (node.type === "EmptyStatement") {
  95. return true;
  96. }
  97. const first = sourceCode.getFirstToken(node);
  98. const last = sourceCode.getLastToken(node);
  99. const lastExcludingSemicolon = astUtils.isSemicolonToken(last)
  100. ? sourceCode.getTokenBefore(last)
  101. : last;
  102. return first.loc.start.line === lastExcludingSemicolon.loc.end.line;
  103. }
  104. /**
  105. * Determines if a semicolon needs to be inserted after removing a set of curly brackets, in order to avoid a SyntaxError.
  106. * @param {Token} closingBracket The } token
  107. * @returns {boolean} `true` if a semicolon needs to be inserted after the last statement in the block.
  108. */
  109. function needsSemicolon(closingBracket) {
  110. const tokenBefore = sourceCode.getTokenBefore(closingBracket);
  111. const tokenAfter = sourceCode.getTokenAfter(closingBracket);
  112. const lastBlockNode = sourceCode.getNodeByRangeIndex(
  113. tokenBefore.range[0],
  114. );
  115. if (astUtils.isSemicolonToken(tokenBefore)) {
  116. // If the last statement already has a semicolon, don't add another one.
  117. return false;
  118. }
  119. if (!tokenAfter) {
  120. // If there are no statements after this block, there is no need to add a semicolon.
  121. return false;
  122. }
  123. if (
  124. lastBlockNode.type === "BlockStatement" &&
  125. lastBlockNode.parent.type !== "FunctionExpression" &&
  126. lastBlockNode.parent.type !== "ArrowFunctionExpression"
  127. ) {
  128. /*
  129. * If the last node surrounded by curly brackets is a BlockStatement (other than a FunctionExpression or an ArrowFunctionExpression),
  130. * don't insert a semicolon. Otherwise, the semicolon would be parsed as a separate statement, which would cause
  131. * a SyntaxError if it was followed by `else`.
  132. */
  133. return false;
  134. }
  135. if (tokenBefore.loc.end.line === tokenAfter.loc.start.line) {
  136. // If the next token is on the same line, insert a semicolon.
  137. return true;
  138. }
  139. if (/^[([/`+-]/u.test(tokenAfter.value)) {
  140. // If the next token starts with a character that would disrupt ASI, insert a semicolon.
  141. return true;
  142. }
  143. if (
  144. tokenBefore.type === "Punctuator" &&
  145. (tokenBefore.value === "++" || tokenBefore.value === "--")
  146. ) {
  147. // If the last token is ++ or --, insert a semicolon to avoid disrupting ASI.
  148. return true;
  149. }
  150. // Otherwise, do not insert a semicolon.
  151. return false;
  152. }
  153. /**
  154. * Prepares to check the body of a node to see if it's a block statement.
  155. * @param {ASTNode} node The node to report if there's a problem.
  156. * @param {ASTNode} body The body node to check for blocks.
  157. * @param {string} name The name to report if there's a problem.
  158. * @param {{ condition: boolean }} opts Options to pass to the report functions
  159. * @returns {Object} a prepared check object, with "actual", "expected", "check" properties.
  160. * "actual" will be `true` or `false` whether the body is already a block statement.
  161. * "expected" will be `true` or `false` if the body should be a block statement or not, or
  162. * `null` if it doesn't matter, depending on the rule options. It can be modified to change
  163. * the final behavior of "check".
  164. * "check" will be a function reporting appropriate problems depending on the other
  165. * properties.
  166. */
  167. function prepareCheck(node, body, name, opts) {
  168. const hasBlock = body.type === "BlockStatement";
  169. let expected = null;
  170. if (
  171. hasBlock &&
  172. (body.body.length !== 1 ||
  173. astUtils.areBracesNecessary(body, sourceCode))
  174. ) {
  175. expected = true;
  176. } else if (multiOnly) {
  177. expected = false;
  178. } else if (multiLine) {
  179. if (!isCollapsedOneLiner(body)) {
  180. expected = true;
  181. }
  182. // otherwise, the body is allowed to have braces or not to have braces
  183. } else if (multiOrNest) {
  184. if (hasBlock) {
  185. const statement = body.body[0];
  186. const leadingCommentsInBlock =
  187. sourceCode.getCommentsBefore(statement);
  188. expected =
  189. !isOneLiner(statement) ||
  190. leadingCommentsInBlock.length > 0;
  191. } else {
  192. expected = !isOneLiner(body);
  193. }
  194. } else {
  195. // default "all"
  196. expected = true;
  197. }
  198. return {
  199. actual: hasBlock,
  200. expected,
  201. check() {
  202. if (
  203. this.expected !== null &&
  204. this.expected !== this.actual
  205. ) {
  206. if (this.expected) {
  207. context.report({
  208. node,
  209. loc: body.loc,
  210. messageId:
  211. opts && opts.condition
  212. ? "missingCurlyAfterCondition"
  213. : "missingCurlyAfter",
  214. data: {
  215. name,
  216. },
  217. fix: fixer =>
  218. fixer.replaceText(
  219. body,
  220. `{${sourceCode.getText(body)}}`,
  221. ),
  222. });
  223. } else {
  224. context.report({
  225. node,
  226. loc: body.loc,
  227. messageId:
  228. opts && opts.condition
  229. ? "unexpectedCurlyAfterCondition"
  230. : "unexpectedCurlyAfter",
  231. data: {
  232. name,
  233. },
  234. fix(fixer) {
  235. /*
  236. * `do while` expressions sometimes need a space to be inserted after `do`.
  237. * e.g. `do{foo()} while (bar)` should be corrected to `do foo() while (bar)`
  238. */
  239. const needsPrecedingSpace =
  240. node.type === "DoWhileStatement" &&
  241. sourceCode.getTokenBefore(body)
  242. .range[1] === body.range[0] &&
  243. !astUtils.canTokensBeAdjacent(
  244. "do",
  245. sourceCode.getFirstToken(body, {
  246. skip: 1,
  247. }),
  248. );
  249. const openingBracket =
  250. sourceCode.getFirstToken(body);
  251. const closingBracket =
  252. sourceCode.getLastToken(body);
  253. const lastTokenInBlock =
  254. sourceCode.getTokenBefore(
  255. closingBracket,
  256. );
  257. if (needsSemicolon(closingBracket)) {
  258. /*
  259. * If removing braces would cause a SyntaxError due to multiple statements on the same line (or
  260. * change the semantics of the code due to ASI), don't perform a fix.
  261. */
  262. return null;
  263. }
  264. const resultingBodyText =
  265. sourceCode
  266. .getText()
  267. .slice(
  268. openingBracket.range[1],
  269. lastTokenInBlock.range[0],
  270. ) +
  271. sourceCode.getText(lastTokenInBlock) +
  272. sourceCode
  273. .getText()
  274. .slice(
  275. lastTokenInBlock.range[1],
  276. closingBracket.range[0],
  277. );
  278. return fixer.replaceText(
  279. body,
  280. (needsPrecedingSpace ? " " : "") +
  281. resultingBodyText,
  282. );
  283. },
  284. });
  285. }
  286. }
  287. },
  288. };
  289. }
  290. /**
  291. * Prepares to check the bodies of a "if", "else if" and "else" chain.
  292. * @param {ASTNode} node The first IfStatement node of the chain.
  293. * @returns {Object[]} prepared checks for each body of the chain. See `prepareCheck` for more
  294. * information.
  295. */
  296. function prepareIfChecks(node) {
  297. const preparedChecks = [];
  298. for (
  299. let currentNode = node;
  300. currentNode;
  301. currentNode = currentNode.alternate
  302. ) {
  303. preparedChecks.push(
  304. prepareCheck(currentNode, currentNode.consequent, "if", {
  305. condition: true,
  306. }),
  307. );
  308. if (
  309. currentNode.alternate &&
  310. currentNode.alternate.type !== "IfStatement"
  311. ) {
  312. preparedChecks.push(
  313. prepareCheck(
  314. currentNode,
  315. currentNode.alternate,
  316. "else",
  317. ),
  318. );
  319. break;
  320. }
  321. }
  322. if (consistent) {
  323. /*
  324. * If any node should have or already have braces, make sure they
  325. * all have braces.
  326. * If all nodes shouldn't have braces, make sure they don't.
  327. */
  328. const expected = preparedChecks.some(preparedCheck => {
  329. if (preparedCheck.expected !== null) {
  330. return preparedCheck.expected;
  331. }
  332. return preparedCheck.actual;
  333. });
  334. preparedChecks.forEach(preparedCheck => {
  335. preparedCheck.expected = expected;
  336. });
  337. }
  338. return preparedChecks;
  339. }
  340. //--------------------------------------------------------------------------
  341. // Public
  342. //--------------------------------------------------------------------------
  343. return {
  344. IfStatement(node) {
  345. const parent = node.parent;
  346. const isElseIf =
  347. parent.type === "IfStatement" && parent.alternate === node;
  348. if (!isElseIf) {
  349. // This is a top `if`, check the whole `if-else-if` chain
  350. prepareIfChecks(node).forEach(preparedCheck => {
  351. preparedCheck.check();
  352. });
  353. }
  354. // Skip `else if`, it's already checked (when the top `if` was visited)
  355. },
  356. WhileStatement(node) {
  357. prepareCheck(node, node.body, "while", {
  358. condition: true,
  359. }).check();
  360. },
  361. DoWhileStatement(node) {
  362. prepareCheck(node, node.body, "do").check();
  363. },
  364. ForStatement(node) {
  365. prepareCheck(node, node.body, "for", {
  366. condition: true,
  367. }).check();
  368. },
  369. ForInStatement(node) {
  370. prepareCheck(node, node.body, "for-in").check();
  371. },
  372. ForOfStatement(node) {
  373. prepareCheck(node, node.body, "for-of").check();
  374. },
  375. };
  376. },
  377. };