no-gratuitous-expressions.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. "use strict";
  2. /*
  3. * eslint-plugin-sonarjs
  4. * Copyright (C) 2018-2021 SonarSource SA
  5. * mailto:info AT sonarsource DOT com
  6. *
  7. * This program is free software; you can redistribute it and/or
  8. * modify it under the terms of the GNU Lesser General Public
  9. * License as published by the Free Software Foundation; either
  10. * version 3 of the License, or (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  15. * Lesser General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Lesser General Public License
  18. * along with this program; if not, write to the Free Software Foundation,
  19. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  20. */
  21. // https://sonarsource.github.io/rspec/#/rspec/S2589
  22. const locations_1 = require("../utils/locations");
  23. const nodes_1 = require("../utils/nodes");
  24. const docs_url_1 = require("../utils/docs-url");
  25. const message = 'This always evaluates to {{value}}. Consider refactoring this code.';
  26. const rule = {
  27. meta: {
  28. messages: {
  29. refactorBooleanExpression: message,
  30. sonarRuntime: '{{sonarRuntimeData}}',
  31. },
  32. type: 'suggestion',
  33. docs: {
  34. description: 'Boolean expressions should not be gratuitous',
  35. recommended: 'error',
  36. url: (0, docs_url_1.default)(__filename),
  37. },
  38. schema: [
  39. {
  40. // internal parameter for rules having secondary locations
  41. enum: ['sonar-runtime'],
  42. },
  43. ],
  44. },
  45. create(context) {
  46. const truthyMap = new Map();
  47. const falsyMap = new Map();
  48. function isInsideJSX() {
  49. const ancestors = context.getAncestors();
  50. return !!ancestors.find(ancestor => ancestor.type === 'JSXExpressionContainer');
  51. }
  52. return {
  53. IfStatement: (node) => {
  54. const { test } = node;
  55. if (test.type === 'Literal' && typeof test.value === 'boolean') {
  56. reportIssue(test, undefined, context, test.value);
  57. }
  58. },
  59. ':statement': (node) => {
  60. const { parent } = node;
  61. if ((0, nodes_1.isIfStatement)(parent)) {
  62. // we visit 'consequent' and 'alternate' and not if-statement directly in order to get scope for 'consequent'
  63. const currentScope = context.getScope();
  64. if (parent.consequent === node) {
  65. const { truthy, falsy } = collectKnownIdentifiers(parent.test);
  66. truthyMap.set(parent.consequent, transformAndFilter(truthy, currentScope));
  67. falsyMap.set(parent.consequent, transformAndFilter(falsy, currentScope));
  68. }
  69. else if (parent.alternate === node && (0, nodes_1.isIdentifier)(parent.test)) {
  70. falsyMap.set(parent.alternate, transformAndFilter([parent.test], currentScope));
  71. }
  72. }
  73. },
  74. ':statement:exit': (node) => {
  75. const stmt = node;
  76. truthyMap.delete(stmt);
  77. falsyMap.delete(stmt);
  78. },
  79. Identifier: (node) => {
  80. const id = node;
  81. const symbol = getSymbol(id, context.getScope());
  82. const { parent } = node;
  83. if (!symbol || !parent || (isInsideJSX() && isLogicalAndRhs(id, parent))) {
  84. return;
  85. }
  86. if (!isLogicalAnd(parent) &&
  87. !isLogicalOrLhs(id, parent) &&
  88. !(0, nodes_1.isIfStatement)(parent) &&
  89. !isLogicalNegation(parent)) {
  90. return;
  91. }
  92. const checkIfKnownAndReport = (map, truthy) => {
  93. map.forEach(references => {
  94. const ref = references.find(ref => ref.resolved === symbol);
  95. if (ref) {
  96. reportIssue(id, ref, context, truthy);
  97. }
  98. });
  99. };
  100. checkIfKnownAndReport(truthyMap, true);
  101. checkIfKnownAndReport(falsyMap, false);
  102. },
  103. Program: () => {
  104. truthyMap.clear();
  105. falsyMap.clear();
  106. },
  107. };
  108. },
  109. };
  110. function collectKnownIdentifiers(expression) {
  111. const truthy = [];
  112. const falsy = [];
  113. const checkExpr = (expr) => {
  114. if ((0, nodes_1.isIdentifier)(expr)) {
  115. truthy.push(expr);
  116. }
  117. else if (isLogicalNegation(expr)) {
  118. if ((0, nodes_1.isIdentifier)(expr.argument)) {
  119. falsy.push(expr.argument);
  120. }
  121. else if (isLogicalNegation(expr.argument) && (0, nodes_1.isIdentifier)(expr.argument.argument)) {
  122. truthy.push(expr.argument.argument);
  123. }
  124. }
  125. };
  126. let current = expression;
  127. checkExpr(current);
  128. while (isLogicalAnd(current)) {
  129. checkExpr(current.right);
  130. current = current.left;
  131. }
  132. checkExpr(current);
  133. return { truthy, falsy };
  134. }
  135. function isLogicalAnd(expression) {
  136. return expression.type === 'LogicalExpression' && expression.operator === '&&';
  137. }
  138. function isLogicalOrLhs(id, expression) {
  139. return (expression.type === 'LogicalExpression' &&
  140. expression.operator === '||' &&
  141. expression.left === id);
  142. }
  143. function isLogicalAndRhs(id, expression) {
  144. var _a;
  145. return (((_a = expression.parent) === null || _a === void 0 ? void 0 : _a.type) !== 'LogicalExpression' &&
  146. expression.type === 'LogicalExpression' &&
  147. expression.operator === '&&' &&
  148. expression.right === id);
  149. }
  150. function isLogicalNegation(expression) {
  151. return expression.type === 'UnaryExpression' && expression.operator === '!';
  152. }
  153. function isDefined(x) {
  154. return x != null;
  155. }
  156. function getSymbol(id, scope) {
  157. const ref = scope.references.find(r => r.identifier === id);
  158. if (ref) {
  159. return ref.resolved;
  160. }
  161. return null;
  162. }
  163. function getFunctionScope(scope) {
  164. if (scope.type === 'function') {
  165. return scope;
  166. }
  167. else if (!scope.upper) {
  168. return null;
  169. }
  170. return getFunctionScope(scope.upper);
  171. }
  172. function mightBeWritten(symbol, currentScope) {
  173. return symbol.references
  174. .filter(ref => ref.isWrite())
  175. .find(ref => {
  176. const refScope = ref.from;
  177. let cur = refScope;
  178. while (cur) {
  179. if (cur === currentScope) {
  180. return true;
  181. }
  182. cur = cur.upper;
  183. }
  184. const currentFunc = getFunctionScope(currentScope);
  185. const refFunc = getFunctionScope(refScope);
  186. return refFunc !== currentFunc;
  187. });
  188. }
  189. function transformAndFilter(ids, currentScope) {
  190. return ids
  191. .map(id => { var _a; return (_a = currentScope.upper) === null || _a === void 0 ? void 0 : _a.references.find(r => r.identifier === id); })
  192. .filter(isDefined)
  193. .filter(ref => isDefined(ref.resolved))
  194. .filter(ref => !mightBeWritten(ref.resolved, currentScope));
  195. }
  196. function reportIssue(id, ref, context, truthy) {
  197. const value = truthy ? 'truthy' : 'falsy';
  198. (0, locations_1.report)(context, {
  199. messageId: 'refactorBooleanExpression',
  200. data: {
  201. value,
  202. },
  203. node: id,
  204. }, getSecondaryLocations(ref, value), message);
  205. }
  206. function getSecondaryLocations(ref, truthy) {
  207. if (ref) {
  208. const secLoc = ref.identifier.loc;
  209. return [
  210. {
  211. message: `Evaluated here to be ${truthy}`,
  212. line: secLoc.start.line,
  213. column: secLoc.start.column,
  214. endLine: secLoc.end.line,
  215. endColumn: secLoc.end.column,
  216. },
  217. ];
  218. }
  219. else {
  220. return [];
  221. }
  222. }
  223. module.exports = rule;
  224. //# sourceMappingURL=no-gratuitous-expressions.js.map