logical-assignment-operators.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. /**
  2. * @fileoverview Rule to replace assignment expressions with logical operator assignment
  3. * @author Daniel Martens
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils.js");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. const baseTypes = new Set(["Identifier", "Super", "ThisExpression"]);
  14. /**
  15. * Returns true iff either "undefined" or a void expression (eg. "void 0")
  16. * @param {ASTNode} expression Expression to check
  17. * @param {import('eslint-scope').Scope} scope Scope of the expression
  18. * @returns {boolean} True iff "undefined" or "void ..."
  19. */
  20. function isUndefined(expression, scope) {
  21. if (expression.type === "Identifier" && expression.name === "undefined") {
  22. return astUtils.isReferenceToGlobalVariable(scope, expression);
  23. }
  24. return (
  25. expression.type === "UnaryExpression" &&
  26. expression.operator === "void" &&
  27. expression.argument.type === "Literal" &&
  28. expression.argument.value === 0
  29. );
  30. }
  31. /**
  32. * Returns true iff the reference is either an identifier or member expression
  33. * @param {ASTNode} expression Expression to check
  34. * @returns {boolean} True for identifiers and member expressions
  35. */
  36. function isReference(expression) {
  37. return (
  38. (expression.type === "Identifier" && expression.name !== "undefined") ||
  39. expression.type === "MemberExpression"
  40. );
  41. }
  42. /**
  43. * Returns true iff the expression checks for nullish with loose equals.
  44. * Examples: value == null, value == void 0
  45. * @param {ASTNode} expression Test condition
  46. * @param {import('eslint-scope').Scope} scope Scope of the expression
  47. * @returns {boolean} True iff implicit nullish comparison
  48. */
  49. function isImplicitNullishComparison(expression, scope) {
  50. if (
  51. expression.type !== "BinaryExpression" ||
  52. expression.operator !== "=="
  53. ) {
  54. return false;
  55. }
  56. const reference = isReference(expression.left) ? "left" : "right";
  57. const nullish = reference === "left" ? "right" : "left";
  58. return (
  59. isReference(expression[reference]) &&
  60. (astUtils.isNullLiteral(expression[nullish]) ||
  61. isUndefined(expression[nullish], scope))
  62. );
  63. }
  64. /**
  65. * Condition with two equal comparisons.
  66. * @param {ASTNode} expression Condition
  67. * @returns {boolean} True iff matches ? === ? || ? === ?
  68. */
  69. function isDoubleComparison(expression) {
  70. return (
  71. expression.type === "LogicalExpression" &&
  72. expression.operator === "||" &&
  73. expression.left.type === "BinaryExpression" &&
  74. expression.left.operator === "===" &&
  75. expression.right.type === "BinaryExpression" &&
  76. expression.right.operator === "==="
  77. );
  78. }
  79. /**
  80. * Returns true iff the expression checks for undefined and null.
  81. * Example: value === null || value === undefined
  82. * @param {ASTNode} expression Test condition
  83. * @param {import('eslint-scope').Scope} scope Scope of the expression
  84. * @returns {boolean} True iff explicit nullish comparison
  85. */
  86. function isExplicitNullishComparison(expression, scope) {
  87. if (!isDoubleComparison(expression)) {
  88. return false;
  89. }
  90. const leftReference = isReference(expression.left.left) ? "left" : "right";
  91. const leftNullish = leftReference === "left" ? "right" : "left";
  92. const rightReference = isReference(expression.right.left)
  93. ? "left"
  94. : "right";
  95. const rightNullish = rightReference === "left" ? "right" : "left";
  96. return (
  97. astUtils.isSameReference(
  98. expression.left[leftReference],
  99. expression.right[rightReference],
  100. ) &&
  101. ((astUtils.isNullLiteral(expression.left[leftNullish]) &&
  102. isUndefined(expression.right[rightNullish], scope)) ||
  103. (isUndefined(expression.left[leftNullish], scope) &&
  104. astUtils.isNullLiteral(expression.right[rightNullish])))
  105. );
  106. }
  107. /**
  108. * Returns true for Boolean(arg) calls
  109. * @param {ASTNode} expression Test condition
  110. * @param {import('eslint-scope').Scope} scope Scope of the expression
  111. * @returns {boolean} Whether the expression is a boolean cast
  112. */
  113. function isBooleanCast(expression, scope) {
  114. return (
  115. expression.type === "CallExpression" &&
  116. expression.callee.name === "Boolean" &&
  117. expression.arguments.length === 1 &&
  118. astUtils.isReferenceToGlobalVariable(scope, expression.callee)
  119. );
  120. }
  121. /**
  122. * Returns true for:
  123. * truthiness checks: value, Boolean(value), !!value
  124. * falsiness checks: !value, !Boolean(value)
  125. * nullish checks: value == null, value === undefined || value === null
  126. * @param {ASTNode} expression Test condition
  127. * @param {import('eslint-scope').Scope} scope Scope of the expression
  128. * @returns {?{ reference: ASTNode, operator: '??'|'||'|'&&'}} Null if not a known existence
  129. */
  130. function getExistence(expression, scope) {
  131. const isNegated =
  132. expression.type === "UnaryExpression" && expression.operator === "!";
  133. const base = isNegated ? expression.argument : expression;
  134. switch (true) {
  135. case isReference(base):
  136. return { reference: base, operator: isNegated ? "||" : "&&" };
  137. case base.type === "UnaryExpression" &&
  138. base.operator === "!" &&
  139. isReference(base.argument):
  140. return { reference: base.argument, operator: "&&" };
  141. case isBooleanCast(base, scope) && isReference(base.arguments[0]):
  142. return {
  143. reference: base.arguments[0],
  144. operator: isNegated ? "||" : "&&",
  145. };
  146. case isImplicitNullishComparison(expression, scope):
  147. return {
  148. reference: isReference(expression.left)
  149. ? expression.left
  150. : expression.right,
  151. operator: "??",
  152. };
  153. case isExplicitNullishComparison(expression, scope):
  154. return {
  155. reference: isReference(expression.left.left)
  156. ? expression.left.left
  157. : expression.left.right,
  158. operator: "??",
  159. };
  160. default:
  161. return null;
  162. }
  163. }
  164. /**
  165. * Returns true iff the node is inside a with block
  166. * @param {ASTNode} node Node to check
  167. * @returns {boolean} True iff passed node is inside a with block
  168. */
  169. function isInsideWithBlock(node) {
  170. if (node.type === "Program") {
  171. return false;
  172. }
  173. return node.parent.type === "WithStatement" && node.parent.body === node
  174. ? true
  175. : isInsideWithBlock(node.parent);
  176. }
  177. /**
  178. * Gets the leftmost operand of a consecutive logical expression.
  179. * @param {SourceCode} sourceCode The ESLint source code object
  180. * @param {LogicalExpression} node LogicalExpression
  181. * @returns {Expression} Leftmost operand
  182. */
  183. function getLeftmostOperand(sourceCode, node) {
  184. let left = node.left;
  185. while (
  186. left.type === "LogicalExpression" &&
  187. left.operator === node.operator
  188. ) {
  189. if (astUtils.isParenthesised(sourceCode, left)) {
  190. /*
  191. * It should have associativity,
  192. * but ignore it if use parentheses to make the evaluation order clear.
  193. */
  194. return left;
  195. }
  196. left = left.left;
  197. }
  198. return left;
  199. }
  200. //------------------------------------------------------------------------------
  201. // Rule Definition
  202. //------------------------------------------------------------------------------
  203. /** @type {import('../types').Rule.RuleModule} */
  204. module.exports = {
  205. meta: {
  206. type: "suggestion",
  207. docs: {
  208. description:
  209. "Require or disallow logical assignment operator shorthand",
  210. recommended: false,
  211. frozen: true,
  212. url: "https://eslint.org/docs/latest/rules/logical-assignment-operators",
  213. },
  214. schema: {
  215. type: "array",
  216. oneOf: [
  217. {
  218. items: [
  219. { const: "always" },
  220. {
  221. type: "object",
  222. properties: {
  223. enforceForIfStatements: {
  224. type: "boolean",
  225. },
  226. },
  227. additionalProperties: false,
  228. },
  229. ],
  230. minItems: 0, // 0 for allowing passing no options
  231. maxItems: 2,
  232. },
  233. {
  234. items: [{ const: "never" }],
  235. minItems: 1,
  236. maxItems: 1,
  237. },
  238. ],
  239. },
  240. fixable: "code",
  241. hasSuggestions: true,
  242. messages: {
  243. assignment:
  244. "Assignment (=) can be replaced with operator assignment ({{operator}}).",
  245. useLogicalOperator:
  246. "Convert this assignment to use the operator {{ operator }}.",
  247. logical:
  248. "Logical expression can be replaced with an assignment ({{ operator }}).",
  249. convertLogical:
  250. "Replace this logical expression with an assignment with the operator {{ operator }}.",
  251. if: "'if' statement can be replaced with a logical operator assignment with operator {{ operator }}.",
  252. convertIf:
  253. "Replace this 'if' statement with a logical assignment with operator {{ operator }}.",
  254. unexpected:
  255. "Unexpected logical operator assignment ({{operator}}) shorthand.",
  256. separate:
  257. "Separate the logical assignment into an assignment with a logical operator.",
  258. },
  259. },
  260. create(context) {
  261. const mode = context.options[0] === "never" ? "never" : "always";
  262. const checkIf =
  263. mode === "always" &&
  264. context.options.length > 1 &&
  265. context.options[1].enforceForIfStatements;
  266. const sourceCode = context.sourceCode;
  267. const isStrict = sourceCode.getScope(sourceCode.ast).isStrict;
  268. /**
  269. * Returns false if the access could be a getter
  270. * @param {ASTNode} node Assignment expression
  271. * @returns {boolean} True iff the fix is safe
  272. */
  273. function cannotBeGetter(node) {
  274. return (
  275. node.type === "Identifier" &&
  276. (isStrict || !isInsideWithBlock(node))
  277. );
  278. }
  279. /**
  280. * Check whether only a single property is accessed
  281. * @param {ASTNode} node reference
  282. * @returns {boolean} True iff a single property is accessed
  283. */
  284. function accessesSingleProperty(node) {
  285. if (!isStrict && isInsideWithBlock(node)) {
  286. return node.type === "Identifier";
  287. }
  288. return (
  289. node.type === "MemberExpression" &&
  290. baseTypes.has(node.object.type) &&
  291. (!node.computed ||
  292. (node.property.type !== "MemberExpression" &&
  293. node.property.type !== "ChainExpression"))
  294. );
  295. }
  296. /**
  297. * Adds a fixer or suggestion whether on the fix is safe.
  298. * @param {{ messageId: string, node: ASTNode }} descriptor Report descriptor without fix or suggest
  299. * @param {{ messageId: string, fix: Function }} suggestion Adds the fix or the whole suggestion as only element in "suggest" to suggestion
  300. * @param {boolean} shouldBeFixed Fix iff the condition is true
  301. * @returns {Object} Descriptor with either an added fix or suggestion
  302. */
  303. function createConditionalFixer(descriptor, suggestion, shouldBeFixed) {
  304. if (shouldBeFixed) {
  305. return {
  306. ...descriptor,
  307. fix: suggestion.fix,
  308. };
  309. }
  310. return {
  311. ...descriptor,
  312. suggest: [suggestion],
  313. };
  314. }
  315. /**
  316. * Returns the operator token for assignments and binary expressions
  317. * @param {ASTNode} node AssignmentExpression or BinaryExpression
  318. * @returns {import('eslint').AST.Token} Operator token between the left and right expression
  319. */
  320. function getOperatorToken(node) {
  321. return sourceCode.getFirstTokenBetween(
  322. node.left,
  323. node.right,
  324. token => token.value === node.operator,
  325. );
  326. }
  327. if (mode === "never") {
  328. return {
  329. // foo ||= bar
  330. AssignmentExpression(assignment) {
  331. if (
  332. !astUtils.isLogicalAssignmentOperator(
  333. assignment.operator,
  334. )
  335. ) {
  336. return;
  337. }
  338. const descriptor = {
  339. messageId: "unexpected",
  340. node: assignment,
  341. data: { operator: assignment.operator },
  342. };
  343. const suggestion = {
  344. messageId: "separate",
  345. *fix(ruleFixer) {
  346. if (
  347. sourceCode.getCommentsInside(assignment)
  348. .length > 0
  349. ) {
  350. return;
  351. }
  352. const operatorToken = getOperatorToken(assignment);
  353. // -> foo = bar
  354. yield ruleFixer.replaceText(operatorToken, "=");
  355. const assignmentText = sourceCode.getText(
  356. assignment.left,
  357. );
  358. const operator = assignment.operator.slice(0, -1);
  359. // -> foo = foo || bar
  360. yield ruleFixer.insertTextAfter(
  361. operatorToken,
  362. ` ${assignmentText} ${operator}`,
  363. );
  364. const precedence =
  365. astUtils.getPrecedence(assignment.right) <=
  366. astUtils.getPrecedence({
  367. type: "LogicalExpression",
  368. operator,
  369. });
  370. // ?? and || / && cannot be mixed but have same precedence
  371. const mixed =
  372. assignment.operator === "??=" &&
  373. astUtils.isLogicalExpression(assignment.right);
  374. if (
  375. !astUtils.isParenthesised(
  376. sourceCode,
  377. assignment.right,
  378. ) &&
  379. (precedence || mixed)
  380. ) {
  381. // -> foo = foo || (bar)
  382. yield ruleFixer.insertTextBefore(
  383. assignment.right,
  384. "(",
  385. );
  386. yield ruleFixer.insertTextAfter(
  387. assignment.right,
  388. ")",
  389. );
  390. }
  391. },
  392. };
  393. context.report(
  394. createConditionalFixer(
  395. descriptor,
  396. suggestion,
  397. cannotBeGetter(assignment.left),
  398. ),
  399. );
  400. },
  401. };
  402. }
  403. return {
  404. // foo = foo || bar
  405. "AssignmentExpression[operator='='][right.type='LogicalExpression']"(
  406. assignment,
  407. ) {
  408. const leftOperand = getLeftmostOperand(
  409. sourceCode,
  410. assignment.right,
  411. );
  412. if (!astUtils.isSameReference(assignment.left, leftOperand)) {
  413. return;
  414. }
  415. const descriptor = {
  416. messageId: "assignment",
  417. node: assignment,
  418. data: { operator: `${assignment.right.operator}=` },
  419. };
  420. const suggestion = {
  421. messageId: "useLogicalOperator",
  422. data: { operator: `${assignment.right.operator}=` },
  423. *fix(ruleFixer) {
  424. if (
  425. sourceCode.getCommentsInside(assignment).length > 0
  426. ) {
  427. return;
  428. }
  429. // No need for parenthesis around the assignment based on precedence as the precedence stays the same even with changed operator
  430. const assignmentOperatorToken =
  431. getOperatorToken(assignment);
  432. // -> foo ||= foo || bar
  433. yield ruleFixer.insertTextBefore(
  434. assignmentOperatorToken,
  435. assignment.right.operator,
  436. );
  437. // -> foo ||= bar
  438. const logicalOperatorToken = getOperatorToken(
  439. leftOperand.parent,
  440. );
  441. const firstRightOperandToken =
  442. sourceCode.getTokenAfter(logicalOperatorToken);
  443. yield ruleFixer.removeRange([
  444. leftOperand.parent.range[0],
  445. firstRightOperandToken.range[0],
  446. ]);
  447. },
  448. };
  449. context.report(
  450. createConditionalFixer(
  451. descriptor,
  452. suggestion,
  453. cannotBeGetter(assignment.left),
  454. ),
  455. );
  456. },
  457. // foo || (foo = bar)
  458. 'LogicalExpression[right.type="AssignmentExpression"][right.operator="="]'(
  459. logical,
  460. ) {
  461. // Right side has to be parenthesized, otherwise would be parsed as (foo || foo) = bar which is illegal
  462. if (
  463. isReference(logical.left) &&
  464. astUtils.isSameReference(logical.left, logical.right.left)
  465. ) {
  466. const descriptor = {
  467. messageId: "logical",
  468. node: logical,
  469. data: { operator: `${logical.operator}=` },
  470. };
  471. const suggestion = {
  472. messageId: "convertLogical",
  473. data: { operator: `${logical.operator}=` },
  474. *fix(ruleFixer) {
  475. if (
  476. sourceCode.getCommentsInside(logical).length > 0
  477. ) {
  478. return;
  479. }
  480. const parentPrecedence = astUtils.getPrecedence(
  481. logical.parent,
  482. );
  483. const requiresOuterParenthesis =
  484. logical.parent.type !== "ExpressionStatement" &&
  485. (parentPrecedence === -1 ||
  486. astUtils.getPrecedence({
  487. type: "AssignmentExpression",
  488. }) < parentPrecedence);
  489. if (
  490. !astUtils.isParenthesised(
  491. sourceCode,
  492. logical,
  493. ) &&
  494. requiresOuterParenthesis
  495. ) {
  496. yield ruleFixer.insertTextBefore(logical, "(");
  497. yield ruleFixer.insertTextAfter(logical, ")");
  498. }
  499. // Also removes all opening parenthesis
  500. yield ruleFixer.removeRange([
  501. logical.range[0],
  502. logical.right.range[0],
  503. ]); // -> foo = bar)
  504. // Also removes all ending parenthesis
  505. yield ruleFixer.removeRange([
  506. logical.right.range[1],
  507. logical.range[1],
  508. ]); // -> foo = bar
  509. const operatorToken = getOperatorToken(
  510. logical.right,
  511. );
  512. yield ruleFixer.insertTextBefore(
  513. operatorToken,
  514. logical.operator,
  515. ); // -> foo ||= bar
  516. },
  517. };
  518. const fix =
  519. cannotBeGetter(logical.left) ||
  520. accessesSingleProperty(logical.left);
  521. context.report(
  522. createConditionalFixer(descriptor, suggestion, fix),
  523. );
  524. }
  525. },
  526. // if (foo) foo = bar
  527. "IfStatement[alternate=null]"(ifNode) {
  528. if (!checkIf) {
  529. return;
  530. }
  531. const hasBody = ifNode.consequent.type === "BlockStatement";
  532. if (hasBody && ifNode.consequent.body.length !== 1) {
  533. return;
  534. }
  535. const body = hasBody
  536. ? ifNode.consequent.body[0]
  537. : ifNode.consequent;
  538. const scope = sourceCode.getScope(ifNode);
  539. const existence = getExistence(ifNode.test, scope);
  540. if (
  541. body.type === "ExpressionStatement" &&
  542. body.expression.type === "AssignmentExpression" &&
  543. body.expression.operator === "=" &&
  544. existence !== null &&
  545. astUtils.isSameReference(
  546. existence.reference,
  547. body.expression.left,
  548. )
  549. ) {
  550. const descriptor = {
  551. messageId: "if",
  552. node: ifNode,
  553. data: { operator: `${existence.operator}=` },
  554. };
  555. const suggestion = {
  556. messageId: "convertIf",
  557. data: { operator: `${existence.operator}=` },
  558. *fix(ruleFixer) {
  559. if (
  560. sourceCode.getCommentsInside(ifNode).length > 0
  561. ) {
  562. return;
  563. }
  564. const firstBodyToken =
  565. sourceCode.getFirstToken(body);
  566. const prevToken = sourceCode.getTokenBefore(ifNode);
  567. if (
  568. prevToken !== null &&
  569. prevToken.value !== ";" &&
  570. prevToken.value !== "{" &&
  571. firstBodyToken.type !== "Identifier" &&
  572. firstBodyToken.type !== "Keyword"
  573. ) {
  574. // Do not fix if the fixed statement could be part of the previous statement (eg. fn() if (a == null) (a) = b --> fn()(a) ??= b)
  575. return;
  576. }
  577. const operatorToken = getOperatorToken(
  578. body.expression,
  579. );
  580. yield ruleFixer.insertTextBefore(
  581. operatorToken,
  582. existence.operator,
  583. ); // -> if (foo) foo ||= bar
  584. yield ruleFixer.removeRange([
  585. ifNode.range[0],
  586. body.range[0],
  587. ]); // -> foo ||= bar
  588. yield ruleFixer.removeRange([
  589. body.range[1],
  590. ifNode.range[1],
  591. ]); // -> foo ||= bar, only present if "if" had a body
  592. const nextToken = sourceCode.getTokenAfter(
  593. body.expression,
  594. );
  595. if (
  596. hasBody &&
  597. nextToken !== null &&
  598. nextToken.value !== ";"
  599. ) {
  600. yield ruleFixer.insertTextAfter(ifNode, ";");
  601. }
  602. },
  603. };
  604. const shouldBeFixed =
  605. cannotBeGetter(existence.reference) ||
  606. (ifNode.test.type !== "LogicalExpression" &&
  607. accessesSingleProperty(existence.reference));
  608. context.report(
  609. createConditionalFixer(
  610. descriptor,
  611. suggestion,
  612. shouldBeFixed,
  613. ),
  614. );
  615. }
  616. },
  617. };
  618. },
  619. };