1 | "use strict";
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 | const utils_1 = require("../utils");
|
23 | const docs_url_1 = require("../utils/docs-url");
|
24 |
|
25 | const nonAdditiveMutatorMethods = [
|
26 |
|
27 | 'copyWithin',
|
28 | 'pop',
|
29 | 'reverse',
|
30 | 'shift',
|
31 | 'sort',
|
32 |
|
33 | 'clear',
|
34 | 'delete',
|
35 | ];
|
36 | const accessorMethods = [
|
37 |
|
38 | 'concat',
|
39 | 'flat',
|
40 | 'flatMap',
|
41 | 'includes',
|
42 | 'indexOf',
|
43 | 'join',
|
44 | 'lastIndexOf',
|
45 | 'slice',
|
46 | 'toSource',
|
47 | 'toString',
|
48 | 'toLocaleString',
|
49 |
|
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 | 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 | };
|
90 | function 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 | }
|
100 | function 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 |
|
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 |
|
117 |
|
118 | return;
|
119 | }
|
120 | }
|
121 | else if (isReadingCollectionUsage(ref)) {
|
122 | readingUsages.push(ref);
|
123 | }
|
124 | else {
|
125 |
|
126 |
|
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 | }
|
139 | function 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 | }
|
154 | function 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 | }
|
163 | function isReadingCollectionUsage(ref) {
|
164 | return isStrictlyReadingMethodCall(ref) || isForIterationPattern(ref) || isElementRead(ref);
|
165 | }
|
166 | function 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 | }
|
176 | function 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 | }
|
180 | function isElementRead(ref) {
|
181 | const { parent } = ref.identifier;
|
182 | return parent && parent.type === 'MemberExpression' && parent.computed && !isElementWrite(parent);
|
183 | }
|
184 | function 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 | }
|
192 | module.exports = rule;
|
193 |
|
\ | No newline at end of file |