UNPKG

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