no-unused-collection.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  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/S4030
  22. const collections_1 = require("../utils/collections");
  23. const docs_url_1 = require("../utils/docs-url");
  24. const rule = {
  25. meta: {
  26. messages: {
  27. unusedCollection: "Either use this collection's contents or remove the collection.",
  28. },
  29. schema: [],
  30. type: 'problem',
  31. docs: {
  32. description: 'Collection and array contents should be used',
  33. recommended: 'error',
  34. url: (0, docs_url_1.default)(__filename),
  35. },
  36. },
  37. create(context) {
  38. return {
  39. 'Program:exit': () => {
  40. const unusedArrays = [];
  41. collectUnusedCollections(context.getScope(), unusedArrays);
  42. unusedArrays.forEach(unusedArray => {
  43. context.report({
  44. messageId: 'unusedCollection',
  45. node: unusedArray.identifiers[0],
  46. });
  47. });
  48. },
  49. };
  50. },
  51. };
  52. function collectUnusedCollections(scope, unusedArray) {
  53. if (scope.type !== 'global') {
  54. scope.variables.filter(isUnusedCollection).forEach(v => {
  55. unusedArray.push(v);
  56. });
  57. }
  58. scope.childScopes.forEach(childScope => {
  59. collectUnusedCollections(childScope, unusedArray);
  60. });
  61. }
  62. function isExported(variable) {
  63. var _a, _b;
  64. const definition = variable.defs[0];
  65. return definition && ((_b = (_a = definition.node.parent) === null || _a === void 0 ? void 0 : _a.parent) === null || _b === void 0 ? void 0 : _b.type.startsWith('Export'));
  66. }
  67. function isUnusedCollection(variable) {
  68. if (isExported(variable)) {
  69. return false;
  70. }
  71. if (variable.references.length <= 1) {
  72. return false;
  73. }
  74. let assignCollection = false;
  75. for (const ref of variable.references) {
  76. if (ref.isWriteOnly()) {
  77. if (isReferenceAssigningCollection(ref)) {
  78. assignCollection = true;
  79. }
  80. else {
  81. //One assignment is not a collection, we don't go further
  82. return false;
  83. }
  84. }
  85. else if (isRead(ref)) {
  86. //Unfortunately, isRead (!isWrite) from Scope.Reference consider A[1] = 1; and A.xxx(); as a read operation, we need to filter further
  87. return false;
  88. }
  89. }
  90. return assignCollection;
  91. }
  92. function isReferenceAssigningCollection(ref) {
  93. const declOrExprStmt = findFirstMatchingAncestor(ref.identifier, n => n.type === 'VariableDeclarator' || n.type === 'ExpressionStatement');
  94. if (declOrExprStmt) {
  95. if (declOrExprStmt.type === 'VariableDeclarator' && declOrExprStmt.init) {
  96. return isCollectionType(declOrExprStmt.init);
  97. }
  98. if (declOrExprStmt.type === 'ExpressionStatement') {
  99. const { expression } = declOrExprStmt;
  100. return (expression.type === 'AssignmentExpression' &&
  101. isReferenceTo(ref, expression.left) &&
  102. isCollectionType(expression.right));
  103. }
  104. }
  105. return false;
  106. }
  107. function isCollectionType(node) {
  108. if (node && node.type === 'ArrayExpression') {
  109. return true;
  110. }
  111. else if (node && (node.type === 'CallExpression' || node.type === 'NewExpression')) {
  112. return isIdentifier(node.callee, ...collections_1.collectionConstructor);
  113. }
  114. return false;
  115. }
  116. function isRead(ref) {
  117. const expressionStatement = findFirstMatchingAncestor(ref.identifier, n => n.type === 'ExpressionStatement');
  118. if (expressionStatement) {
  119. return !(isElementWrite(expressionStatement, ref) || isWritingMethodCall(expressionStatement, ref));
  120. }
  121. //All the write statement that we search are part of ExpressionStatement, if there is none, it's a read
  122. return true;
  123. }
  124. /**
  125. * Detect expression statements like the following:
  126. * myArray.push(1);
  127. */
  128. function isWritingMethodCall(statement, ref) {
  129. if (statement.expression.type === 'CallExpression') {
  130. const { callee } = statement.expression;
  131. if (isMemberExpression(callee)) {
  132. const { property } = callee;
  133. return isReferenceTo(ref, callee.object) && isIdentifier(property, ...collections_1.writingMethods);
  134. }
  135. }
  136. return false;
  137. }
  138. function isMemberExpression(node) {
  139. return node.type === 'MemberExpression';
  140. }
  141. /**
  142. * Detect expression statements like the following:
  143. * myArray[1] = 42;
  144. * myArray[1] += 42;
  145. * myObj.prop1 = 3;
  146. * myObj.prop1 += 3;
  147. */
  148. function isElementWrite(statement, ref) {
  149. if (statement.expression.type === 'AssignmentExpression') {
  150. const assignmentExpression = statement.expression;
  151. const lhs = assignmentExpression.left;
  152. return isMemberExpressionReference(lhs, ref);
  153. }
  154. return false;
  155. }
  156. function isMemberExpressionReference(lhs, ref) {
  157. return (lhs.type === 'MemberExpression' &&
  158. (isReferenceTo(ref, lhs.object) || isMemberExpressionReference(lhs.object, ref)));
  159. }
  160. function isIdentifier(node, ...values) {
  161. return node.type === 'Identifier' && values.some(value => value === node.name);
  162. }
  163. function isReferenceTo(ref, node) {
  164. return node.type === 'Identifier' && node === ref.identifier;
  165. }
  166. function findFirstMatchingAncestor(node, predicate) {
  167. return ancestorsChain(node, new Set()).find(predicate);
  168. }
  169. function ancestorsChain(node, boundaryTypes) {
  170. const chain = [];
  171. let currentNode = node.parent;
  172. while (currentNode) {
  173. chain.push(currentNode);
  174. if (boundaryTypes.has(currentNode.type)) {
  175. break;
  176. }
  177. currentNode = currentNode.parent;
  178. }
  179. return chain;
  180. }
  181. module.exports = rule;
  182. //# sourceMappingURL=no-unused-collection.js.map