1 | 'use strict';
|
2 | const getDocumentationUrl = require('./utils/get-documentation-url');
|
3 |
|
4 | const getDeclaratorOrPropertyValue = declaratorOrProperty => {
|
5 | return declaratorOrProperty.init || declaratorOrProperty.value;
|
6 | };
|
7 |
|
8 | const isMemberExpressionCall = memberExpression => {
|
9 | return (
|
10 | memberExpression.parent &&
|
11 | memberExpression.parent.type === 'CallExpression' &&
|
12 | memberExpression.parent.callee === memberExpression
|
13 | );
|
14 | };
|
15 |
|
16 | const isMemberExpressionAssignment = memberExpression => {
|
17 | return (
|
18 | memberExpression.parent &&
|
19 | memberExpression.parent.type === 'AssignmentExpression'
|
20 | );
|
21 | };
|
22 |
|
23 | const isMemberExpressionComputedBeyondPrediction = memberExpression => {
|
24 | return (
|
25 | memberExpression.computed && memberExpression.property.type !== 'Literal'
|
26 | );
|
27 | };
|
28 |
|
29 | const specialProtoPropertyKey = {
|
30 | type: 'Identifier',
|
31 | name: '__proto__'
|
32 | };
|
33 |
|
34 | const propertyKeysEqual = (keyA, keyB) => {
|
35 | if (keyA.type === 'Identifier') {
|
36 | if (keyB.type === 'Identifier') {
|
37 | return keyA.name === keyB.name;
|
38 | }
|
39 |
|
40 | if (keyB.type === 'Literal') {
|
41 | return keyA.name === keyB.value;
|
42 | }
|
43 | }
|
44 |
|
45 | if (keyA.type === 'Literal') {
|
46 | if (keyB.type === 'Identifier') {
|
47 | return keyA.value === keyB.name;
|
48 | }
|
49 |
|
50 | if (keyB.type === 'Literal') {
|
51 | return keyA.value === keyB.value;
|
52 | }
|
53 | }
|
54 |
|
55 | return false;
|
56 | };
|
57 |
|
58 | const objectPatternMatchesObjectExprPropertyKey = (pattern, key) => {
|
59 | return pattern.properties.some(property => {
|
60 | if (property.type === 'ExperimentalRestProperty' || property.type === 'RestElement') {
|
61 | return true;
|
62 | }
|
63 |
|
64 | return propertyKeysEqual(property.key, key);
|
65 | });
|
66 | };
|
67 |
|
68 | const isLeafDeclaratorOrProperty = declaratorOrProperty => {
|
69 | const value = getDeclaratorOrPropertyValue(declaratorOrProperty);
|
70 |
|
71 | if (!value) {
|
72 | return true;
|
73 | }
|
74 |
|
75 | if (value.type !== 'ObjectExpression') {
|
76 | return true;
|
77 | }
|
78 |
|
79 | return false;
|
80 | };
|
81 |
|
82 | const isUnusedVariable = variable => {
|
83 | const hasReadReference = variable.references.some(reference => reference.isRead());
|
84 | return !hasReadReference;
|
85 | };
|
86 |
|
87 | const create = context => {
|
88 | const getPropertyDisplayName = property => {
|
89 | if (property.key.type === 'Identifier') {
|
90 | return property.key.name;
|
91 | }
|
92 |
|
93 | if (property.key.type === 'Literal') {
|
94 | return property.key.value;
|
95 | }
|
96 |
|
97 | return context.getSource(property.key);
|
98 | };
|
99 |
|
100 | const checkProperty = (property, references, path) => {
|
101 | if (references.length === 0) {
|
102 | context.report({
|
103 | node: property,
|
104 | message: 'Property `{{name}}` is defined but never used.',
|
105 | data: {
|
106 | name: getPropertyDisplayName(property)
|
107 | }
|
108 | });
|
109 | return;
|
110 | }
|
111 |
|
112 | checkObject(property, references, path);
|
113 | };
|
114 |
|
115 | const checkProperties = (objectExpression, references, path = []) => {
|
116 | objectExpression.properties.forEach(property => {
|
117 | const {key} = property;
|
118 |
|
119 | if (!key) {
|
120 | return;
|
121 | }
|
122 |
|
123 | if (propertyKeysEqual(key, specialProtoPropertyKey)) {
|
124 | return;
|
125 | }
|
126 |
|
127 | const nextPath = path.concat(key);
|
128 |
|
129 | const nextReferences = references
|
130 | .map(reference => {
|
131 | const {parent} = reference.identifier;
|
132 |
|
133 | if (reference.init) {
|
134 | if (
|
135 | parent.type === 'VariableDeclarator' &&
|
136 | parent.parent.type === 'VariableDeclaration' &&
|
137 | parent.parent.parent.type === 'ExportNamedDeclaration'
|
138 | ) {
|
139 | return {identifier: parent};
|
140 | }
|
141 |
|
142 | return;
|
143 | }
|
144 |
|
145 | if (parent.type === 'MemberExpression') {
|
146 | if (
|
147 | isMemberExpressionAssignment(parent) ||
|
148 | isMemberExpressionCall(parent) ||
|
149 | isMemberExpressionComputedBeyondPrediction(parent) ||
|
150 | propertyKeysEqual(parent.property, key)
|
151 | ) {
|
152 | return {identifier: parent};
|
153 | }
|
154 |
|
155 | return;
|
156 | }
|
157 |
|
158 | if (
|
159 | parent.type === 'VariableDeclarator' &&
|
160 | parent.id.type === 'ObjectPattern'
|
161 | ) {
|
162 | if (objectPatternMatchesObjectExprPropertyKey(parent.id, key)) {
|
163 | return {identifier: parent};
|
164 | }
|
165 |
|
166 | return;
|
167 | }
|
168 |
|
169 | if (
|
170 | parent.type === 'AssignmentExpression' &&
|
171 | parent.left.type === 'ObjectPattern'
|
172 | ) {
|
173 | if (objectPatternMatchesObjectExprPropertyKey(parent.left, key)) {
|
174 | return {identifier: parent};
|
175 | }
|
176 |
|
177 | return;
|
178 | }
|
179 |
|
180 | return reference;
|
181 | })
|
182 | .filter(Boolean);
|
183 |
|
184 | checkProperty(property, nextReferences, nextPath);
|
185 | });
|
186 | };
|
187 |
|
188 | const checkObject = (declaratorOrProperty, references, path) => {
|
189 | if (isLeafDeclaratorOrProperty(declaratorOrProperty)) {
|
190 | return;
|
191 | }
|
192 |
|
193 | const value = getDeclaratorOrPropertyValue(declaratorOrProperty);
|
194 |
|
195 | checkProperties(value, references, path);
|
196 | };
|
197 |
|
198 | const checkVariable = variable => {
|
199 | if (variable.defs.length !== 1) {
|
200 | return;
|
201 | }
|
202 |
|
203 | if (isUnusedVariable(variable)) {
|
204 | return;
|
205 | }
|
206 |
|
207 | const [definition] = variable.defs;
|
208 |
|
209 | checkObject(definition.node, variable.references);
|
210 | };
|
211 |
|
212 | const checkVariables = scope => {
|
213 | scope.variables.forEach(variable => checkVariable(variable));
|
214 | };
|
215 |
|
216 | const checkChildScopes = scope => {
|
217 | scope.childScopes.forEach(scope => checkScope(scope));
|
218 | };
|
219 |
|
220 | const checkScope = scope => {
|
221 | if (scope.type === 'global') {
|
222 | return checkChildScopes(scope);
|
223 | }
|
224 |
|
225 | checkVariables(scope);
|
226 |
|
227 | return checkChildScopes(scope);
|
228 | };
|
229 |
|
230 | return {
|
231 | 'Program:exit'() {
|
232 | checkScope(context.getScope());
|
233 | }
|
234 | };
|
235 | };
|
236 |
|
237 | module.exports = {
|
238 | create,
|
239 | meta: {
|
240 | type: 'suggestion',
|
241 | docs: {
|
242 | url: getDocumentationUrl(__filename)
|
243 | }
|
244 | }
|
245 | };
|