no-empty-collection.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  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/S4158
  22. const utils_1 = require("../utils");
  23. const docs_url_1 = require("../utils/docs-url");
  24. // Methods that mutate the collection but can't add elements
  25. const nonAdditiveMutatorMethods = [
  26. // array methods
  27. 'copyWithin',
  28. 'pop',
  29. 'reverse',
  30. 'shift',
  31. 'sort',
  32. // map, set methods
  33. 'clear',
  34. 'delete',
  35. ];
  36. const accessorMethods = [
  37. // array methods
  38. 'concat',
  39. 'flat',
  40. 'flatMap',
  41. 'includes',
  42. 'indexOf',
  43. 'join',
  44. 'lastIndexOf',
  45. 'slice',
  46. 'toSource',
  47. 'toString',
  48. 'toLocaleString',
  49. // map, set methods
  50. 'get',
  51. 'has',
  52. ];
  53. const iterationMethods = [
  54. 'entries',
  55. 'every',
  56. 'filter',
  57. 'find',
  58. 'findIndex',
  59. 'forEach',
  60. 'keys',
  61. 'map',
  62. 'reduce',
  63. 'reduceRight',
  64. 'some',
  65. 'values',
  66. ];
  67. const strictlyReadingMethods = new Set([
  68. ...nonAdditiveMutatorMethods,
  69. ...accessorMethods,
  70. ...iterationMethods,
  71. ]);
  72. const rule = {
  73. meta: {
  74. messages: {
  75. reviewUsageOfIdentifier: 'Review this usage of "{{identifierName}}" as it can only be empty here.',
  76. },
  77. schema: [],
  78. type: 'problem',
  79. docs: {
  80. description: 'Empty collections should not be accessed or iterated',
  81. recommended: 'error',
  82. url: (0, docs_url_1.default)(__filename),
  83. },
  84. },
  85. create(context) {
  86. return {
  87. 'Program:exit': () => {
  88. reportEmptyCollectionsUsage(context.getScope(), context);
  89. },
  90. };
  91. },
  92. };
  93. function reportEmptyCollectionsUsage(scope, context) {
  94. if (scope.type !== 'global') {
  95. scope.variables.forEach(v => {
  96. reportEmptyCollectionUsage(v, context);
  97. });
  98. }
  99. scope.childScopes.forEach(childScope => {
  100. reportEmptyCollectionsUsage(childScope, context);
  101. });
  102. }
  103. function reportEmptyCollectionUsage(variable, context) {
  104. if (variable.references.length <= 1) {
  105. return;
  106. }
  107. if (variable.defs.some(d => d.type === 'Parameter' || d.type === 'ImportBinding')) {
  108. // Bound value initialized elsewhere, could be non-empty.
  109. return;
  110. }
  111. const readingUsages = [];
  112. let hasAssignmentOfEmptyCollection = false;
  113. for (const ref of variable.references) {
  114. if (ref.isWriteOnly()) {
  115. if (isReferenceAssigningEmptyCollection(ref)) {
  116. hasAssignmentOfEmptyCollection = true;
  117. }
  118. else {
  119. // There is at least one operation that might make the collection non-empty.
  120. // We ignore the order of usages, and consider all reads to be safe.
  121. return;
  122. }
  123. }
  124. else if (isReadingCollectionUsage(ref)) {
  125. readingUsages.push(ref);
  126. }
  127. else {
  128. // some unknown operation on the collection.
  129. // To avoid any FPs, we assume that it could make the collection non-empty.
  130. return;
  131. }
  132. }
  133. if (hasAssignmentOfEmptyCollection) {
  134. readingUsages.forEach(ref => {
  135. context.report({
  136. messageId: 'reviewUsageOfIdentifier',
  137. data: {
  138. identifierName: ref.identifier.name,
  139. },
  140. node: ref.identifier,
  141. });
  142. });
  143. }
  144. }
  145. function isReferenceAssigningEmptyCollection(ref) {
  146. const declOrExprStmt = (0, utils_1.findFirstMatchingAncestor)(ref.identifier, n => n.type === 'VariableDeclarator' || n.type === 'ExpressionStatement');
  147. if (declOrExprStmt) {
  148. if (declOrExprStmt.type === 'VariableDeclarator' && declOrExprStmt.init) {
  149. return isEmptyCollectionType(declOrExprStmt.init);
  150. }
  151. if (declOrExprStmt.type === 'ExpressionStatement') {
  152. const { expression } = declOrExprStmt;
  153. return (expression.type === 'AssignmentExpression' &&
  154. (0, utils_1.isReferenceTo)(ref, expression.left) &&
  155. isEmptyCollectionType(expression.right));
  156. }
  157. }
  158. return false;
  159. }
  160. function isEmptyCollectionType(node) {
  161. if (node && node.type === 'ArrayExpression') {
  162. return node.elements.length === 0;
  163. }
  164. else if (node && (node.type === 'CallExpression' || node.type === 'NewExpression')) {
  165. return (0, utils_1.isIdentifier)(node.callee, ...utils_1.collectionConstructor) && node.arguments.length === 0;
  166. }
  167. return false;
  168. }
  169. function isReadingCollectionUsage(ref) {
  170. return isStrictlyReadingMethodCall(ref) || isForIterationPattern(ref) || isElementRead(ref);
  171. }
  172. function isStrictlyReadingMethodCall(usage) {
  173. const { parent } = usage.identifier;
  174. if (parent && parent.type === 'MemberExpression') {
  175. const memberExpressionParent = parent.parent;
  176. if (memberExpressionParent && memberExpressionParent.type === 'CallExpression') {
  177. return (0, utils_1.isIdentifier)(parent.property, ...strictlyReadingMethods);
  178. }
  179. }
  180. return false;
  181. }
  182. function isForIterationPattern(ref) {
  183. const forInOrOfStatement = (0, utils_1.findFirstMatchingAncestor)(ref.identifier, n => n.type === 'ForOfStatement' || n.type === 'ForInStatement');
  184. return forInOrOfStatement && forInOrOfStatement.right === ref.identifier;
  185. }
  186. function isElementRead(ref) {
  187. const { parent } = ref.identifier;
  188. return parent && parent.type === 'MemberExpression' && parent.computed && !isElementWrite(parent);
  189. }
  190. function isElementWrite(memberExpression) {
  191. const ancestors = (0, utils_1.ancestorsChain)(memberExpression, new Set());
  192. const assignment = ancestors.find(n => n.type === 'AssignmentExpression');
  193. if (assignment && assignment.operator === '=') {
  194. return [memberExpression, ...ancestors].includes(assignment.left);
  195. }
  196. return false;
  197. }
  198. module.exports = rule;
  199. //# sourceMappingURL=no-empty-collection.js.map