no-extra-boolean-cast.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. /**
  2. * @fileoverview Rule to flag unnecessary double negation in Boolean contexts
  3. * @author Brandon Mills
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. const eslintUtils = require("@eslint-community/eslint-utils");
  11. const precedence = astUtils.getPrecedence;
  12. //------------------------------------------------------------------------------
  13. // Rule Definition
  14. //------------------------------------------------------------------------------
  15. /** @type {import('../types').Rule.RuleModule} */
  16. module.exports = {
  17. meta: {
  18. type: "suggestion",
  19. defaultOptions: [{}],
  20. docs: {
  21. description: "Disallow unnecessary boolean casts",
  22. recommended: true,
  23. frozen: true,
  24. url: "https://eslint.org/docs/latest/rules/no-extra-boolean-cast",
  25. },
  26. schema: [
  27. {
  28. anyOf: [
  29. {
  30. type: "object",
  31. properties: {
  32. enforceForInnerExpressions: {
  33. type: "boolean",
  34. },
  35. },
  36. additionalProperties: false,
  37. },
  38. // deprecated
  39. {
  40. type: "object",
  41. properties: {
  42. enforceForLogicalOperands: {
  43. type: "boolean",
  44. },
  45. },
  46. additionalProperties: false,
  47. },
  48. ],
  49. },
  50. ],
  51. fixable: "code",
  52. messages: {
  53. unexpectedCall: "Redundant Boolean call.",
  54. unexpectedNegation: "Redundant double negation.",
  55. },
  56. },
  57. create(context) {
  58. const sourceCode = context.sourceCode;
  59. const [{ enforceForLogicalOperands, enforceForInnerExpressions }] =
  60. context.options;
  61. // Node types which have a test which will coerce values to booleans.
  62. const BOOLEAN_NODE_TYPES = new Set([
  63. "IfStatement",
  64. "DoWhileStatement",
  65. "WhileStatement",
  66. "ConditionalExpression",
  67. "ForStatement",
  68. ]);
  69. /**
  70. * Check if a node is a Boolean function or constructor.
  71. * @param {ASTNode} node the node
  72. * @returns {boolean} If the node is Boolean function or constructor
  73. */
  74. function isBooleanFunctionOrConstructorCall(node) {
  75. // Boolean(<bool>) and new Boolean(<bool>)
  76. return (
  77. (node.type === "CallExpression" ||
  78. node.type === "NewExpression") &&
  79. node.callee.type === "Identifier" &&
  80. node.callee.name === "Boolean"
  81. );
  82. }
  83. /**
  84. * Check if a node is in a context where its value would be coerced to a boolean at runtime.
  85. * @param {ASTNode} node The node
  86. * @returns {boolean} If it is in a boolean context
  87. */
  88. function isInBooleanContext(node) {
  89. return (
  90. (isBooleanFunctionOrConstructorCall(node.parent) &&
  91. node === node.parent.arguments[0]) ||
  92. (BOOLEAN_NODE_TYPES.has(node.parent.type) &&
  93. node === node.parent.test) ||
  94. // !<bool>
  95. (node.parent.type === "UnaryExpression" &&
  96. node.parent.operator === "!")
  97. );
  98. }
  99. /**
  100. * Checks whether the node is a context that should report an error
  101. * Acts recursively if it is in a logical context
  102. * @param {ASTNode} node the node
  103. * @returns {boolean} If the node is in one of the flagged contexts
  104. */
  105. function isInFlaggedContext(node) {
  106. if (node.parent.type === "ChainExpression") {
  107. return isInFlaggedContext(node.parent);
  108. }
  109. /*
  110. * legacy behavior - enforceForLogicalOperands will only recurse on
  111. * logical expressions, not on other contexts.
  112. * enforceForInnerExpressions will recurse on logical expressions
  113. * as well as the other recursive syntaxes.
  114. */
  115. if (enforceForLogicalOperands || enforceForInnerExpressions) {
  116. if (node.parent.type === "LogicalExpression") {
  117. if (
  118. node.parent.operator === "||" ||
  119. node.parent.operator === "&&"
  120. ) {
  121. return isInFlaggedContext(node.parent);
  122. }
  123. // Check the right hand side of a `??` operator.
  124. if (
  125. enforceForInnerExpressions &&
  126. node.parent.operator === "??" &&
  127. node.parent.right === node
  128. ) {
  129. return isInFlaggedContext(node.parent);
  130. }
  131. }
  132. }
  133. if (enforceForInnerExpressions) {
  134. if (
  135. node.parent.type === "ConditionalExpression" &&
  136. (node.parent.consequent === node ||
  137. node.parent.alternate === node)
  138. ) {
  139. return isInFlaggedContext(node.parent);
  140. }
  141. /*
  142. * Check last expression only in a sequence, i.e. if ((1, 2, Boolean(3))) {}, since
  143. * the others don't affect the result of the expression.
  144. */
  145. if (
  146. node.parent.type === "SequenceExpression" &&
  147. node.parent.expressions.at(-1) === node
  148. ) {
  149. return isInFlaggedContext(node.parent);
  150. }
  151. }
  152. return isInBooleanContext(node);
  153. }
  154. /**
  155. * Check if a node has comments inside.
  156. * @param {ASTNode} node The node to check.
  157. * @returns {boolean} `true` if it has comments inside.
  158. */
  159. function hasCommentsInside(node) {
  160. return Boolean(sourceCode.getCommentsInside(node).length);
  161. }
  162. /**
  163. * Checks if the given node is wrapped in grouping parentheses. Parentheses for constructs such as if() don't count.
  164. * @param {ASTNode} node The node to check.
  165. * @returns {boolean} `true` if the node is parenthesized.
  166. * @private
  167. */
  168. function isParenthesized(node) {
  169. return eslintUtils.isParenthesized(1, node, sourceCode);
  170. }
  171. /**
  172. * Determines whether the given node needs to be parenthesized when replacing the previous node.
  173. * It assumes that `previousNode` is the node to be reported by this rule, so it has a limited list
  174. * of possible parent node types. By the same assumption, the node's role in a particular parent is already known.
  175. * @param {ASTNode} previousNode Previous node.
  176. * @param {ASTNode} node The node to check.
  177. * @throws {Error} (Unreachable.)
  178. * @returns {boolean} `true` if the node needs to be parenthesized.
  179. */
  180. function needsParens(previousNode, node) {
  181. if (previousNode.parent.type === "ChainExpression") {
  182. return needsParens(previousNode.parent, node);
  183. }
  184. if (isParenthesized(previousNode)) {
  185. // parentheses around the previous node will stay, so there is no need for an additional pair
  186. return false;
  187. }
  188. // parent of the previous node will become parent of the replacement node
  189. const parent = previousNode.parent;
  190. switch (parent.type) {
  191. case "CallExpression":
  192. case "NewExpression":
  193. return node.type === "SequenceExpression";
  194. case "IfStatement":
  195. case "DoWhileStatement":
  196. case "WhileStatement":
  197. case "ForStatement":
  198. case "SequenceExpression":
  199. return false;
  200. case "ConditionalExpression":
  201. if (previousNode === parent.test) {
  202. return precedence(node) <= precedence(parent);
  203. }
  204. if (
  205. previousNode === parent.consequent ||
  206. previousNode === parent.alternate
  207. ) {
  208. return (
  209. precedence(node) <
  210. precedence({ type: "AssignmentExpression" })
  211. );
  212. }
  213. /* c8 ignore next */
  214. throw new Error(
  215. "Ternary child must be test, consequent, or alternate.",
  216. );
  217. case "UnaryExpression":
  218. return precedence(node) < precedence(parent);
  219. case "LogicalExpression":
  220. if (
  221. astUtils.isMixedLogicalAndCoalesceExpressions(
  222. node,
  223. parent,
  224. )
  225. ) {
  226. return true;
  227. }
  228. if (previousNode === parent.left) {
  229. return precedence(node) < precedence(parent);
  230. }
  231. return precedence(node) <= precedence(parent);
  232. /* c8 ignore next */
  233. default:
  234. throw new Error(`Unexpected parent type: ${parent.type}`);
  235. }
  236. }
  237. return {
  238. UnaryExpression(node) {
  239. const parent = node.parent;
  240. // Exit early if it's guaranteed not to match
  241. if (
  242. node.operator !== "!" ||
  243. parent.type !== "UnaryExpression" ||
  244. parent.operator !== "!"
  245. ) {
  246. return;
  247. }
  248. if (isInFlaggedContext(parent)) {
  249. context.report({
  250. node: parent,
  251. messageId: "unexpectedNegation",
  252. fix(fixer) {
  253. if (hasCommentsInside(parent)) {
  254. return null;
  255. }
  256. if (needsParens(parent, node.argument)) {
  257. return fixer.replaceText(
  258. parent,
  259. `(${sourceCode.getText(node.argument)})`,
  260. );
  261. }
  262. let prefix = "";
  263. const tokenBefore =
  264. sourceCode.getTokenBefore(parent);
  265. const firstReplacementToken =
  266. sourceCode.getFirstToken(node.argument);
  267. if (
  268. tokenBefore &&
  269. tokenBefore.range[1] === parent.range[0] &&
  270. !astUtils.canTokensBeAdjacent(
  271. tokenBefore,
  272. firstReplacementToken,
  273. )
  274. ) {
  275. prefix = " ";
  276. }
  277. return fixer.replaceText(
  278. parent,
  279. prefix + sourceCode.getText(node.argument),
  280. );
  281. },
  282. });
  283. }
  284. },
  285. CallExpression(node) {
  286. if (
  287. node.callee.type !== "Identifier" ||
  288. node.callee.name !== "Boolean"
  289. ) {
  290. return;
  291. }
  292. if (isInFlaggedContext(node)) {
  293. context.report({
  294. node,
  295. messageId: "unexpectedCall",
  296. fix(fixer) {
  297. const parent = node.parent;
  298. if (node.arguments.length === 0) {
  299. if (
  300. parent.type === "UnaryExpression" &&
  301. parent.operator === "!"
  302. ) {
  303. /*
  304. * !Boolean() -> true
  305. */
  306. if (hasCommentsInside(parent)) {
  307. return null;
  308. }
  309. const replacement = "true";
  310. let prefix = "";
  311. const tokenBefore =
  312. sourceCode.getTokenBefore(parent);
  313. if (
  314. tokenBefore &&
  315. tokenBefore.range[1] ===
  316. parent.range[0] &&
  317. !astUtils.canTokensBeAdjacent(
  318. tokenBefore,
  319. replacement,
  320. )
  321. ) {
  322. prefix = " ";
  323. }
  324. return fixer.replaceText(
  325. parent,
  326. prefix + replacement,
  327. );
  328. }
  329. /*
  330. * Boolean() -> false
  331. */
  332. if (hasCommentsInside(node)) {
  333. return null;
  334. }
  335. return fixer.replaceText(node, "false");
  336. }
  337. if (node.arguments.length === 1) {
  338. const argument = node.arguments[0];
  339. if (
  340. argument.type === "SpreadElement" ||
  341. hasCommentsInside(node)
  342. ) {
  343. return null;
  344. }
  345. /*
  346. * Boolean(expression) -> expression
  347. */
  348. if (needsParens(node, argument)) {
  349. return fixer.replaceText(
  350. node,
  351. `(${sourceCode.getText(argument)})`,
  352. );
  353. }
  354. return fixer.replaceText(
  355. node,
  356. sourceCode.getText(argument),
  357. );
  358. }
  359. // two or more arguments
  360. return null;
  361. },
  362. });
  363. }
  364. },
  365. };
  366. },
  367. };