1 | /**
|
---|
2 | * @fileoverview Prevent declaring unused methods and properties of component class
|
---|
3 | * @author Paweł Nowak, Berton Zhu
|
---|
4 | */
|
---|
5 |
|
---|
6 | 'use strict';
|
---|
7 |
|
---|
8 | const docsUrl = require('../util/docsUrl');
|
---|
9 | const componentUtil = require('../util/componentUtil');
|
---|
10 | const report = require('../util/report');
|
---|
11 |
|
---|
12 | // ------------------------------------------------------------------------------
|
---|
13 | // Rule Definition
|
---|
14 | // ------------------------------------------------------------------------------
|
---|
15 |
|
---|
16 | const LIFECYCLE_METHODS = new Set([
|
---|
17 | 'constructor',
|
---|
18 | 'componentDidCatch',
|
---|
19 | 'componentDidMount',
|
---|
20 | 'componentDidUpdate',
|
---|
21 | 'componentWillMount',
|
---|
22 | 'componentWillReceiveProps',
|
---|
23 | 'componentWillUnmount',
|
---|
24 | 'componentWillUpdate',
|
---|
25 | 'getChildContext',
|
---|
26 | 'getSnapshotBeforeUpdate',
|
---|
27 | 'render',
|
---|
28 | 'shouldComponentUpdate',
|
---|
29 | 'UNSAFE_componentWillMount',
|
---|
30 | 'UNSAFE_componentWillReceiveProps',
|
---|
31 | 'UNSAFE_componentWillUpdate',
|
---|
32 | ]);
|
---|
33 |
|
---|
34 | const ES6_LIFECYCLE = new Set([
|
---|
35 | 'state',
|
---|
36 | ]);
|
---|
37 |
|
---|
38 | const ES5_LIFECYCLE = new Set([
|
---|
39 | 'getInitialState',
|
---|
40 | 'getDefaultProps',
|
---|
41 | 'mixins',
|
---|
42 | ]);
|
---|
43 |
|
---|
44 | function isKeyLiteralLike(node, property) {
|
---|
45 | return property.type === 'Literal'
|
---|
46 | || (property.type === 'TemplateLiteral' && property.expressions.length === 0)
|
---|
47 | || (node.computed === false && property.type === 'Identifier');
|
---|
48 | }
|
---|
49 |
|
---|
50 | // Descend through all wrapping TypeCastExpressions and return the expression
|
---|
51 | // that was cast.
|
---|
52 | function uncast(node) {
|
---|
53 | while (node.type === 'TypeCastExpression') {
|
---|
54 | node = node.expression;
|
---|
55 | }
|
---|
56 | return node;
|
---|
57 | }
|
---|
58 |
|
---|
59 | // Return the name of an identifier or the string value of a literal. Useful
|
---|
60 | // anywhere that a literal may be used as a key (e.g., member expressions,
|
---|
61 | // method definitions, ObjectExpression property keys).
|
---|
62 | function getName(node) {
|
---|
63 | node = uncast(node);
|
---|
64 | const type = node.type;
|
---|
65 |
|
---|
66 | if (type === 'Identifier') {
|
---|
67 | return node.name;
|
---|
68 | }
|
---|
69 | if (type === 'Literal') {
|
---|
70 | return String(node.value);
|
---|
71 | }
|
---|
72 | if (type === 'TemplateLiteral' && node.expressions.length === 0) {
|
---|
73 | return node.quasis[0].value.raw;
|
---|
74 | }
|
---|
75 | return null;
|
---|
76 | }
|
---|
77 |
|
---|
78 | function isThisExpression(node) {
|
---|
79 | return uncast(node).type === 'ThisExpression';
|
---|
80 | }
|
---|
81 |
|
---|
82 | function getInitialClassInfo(node, isClass) {
|
---|
83 | return {
|
---|
84 | classNode: node,
|
---|
85 | isClass,
|
---|
86 | // Set of nodes where properties were defined.
|
---|
87 | properties: new Set(),
|
---|
88 |
|
---|
89 | // Set of names of properties that we've seen used.
|
---|
90 | usedProperties: new Set(),
|
---|
91 |
|
---|
92 | inStatic: false,
|
---|
93 | };
|
---|
94 | }
|
---|
95 |
|
---|
96 | const messages = {
|
---|
97 | unused: 'Unused method or property "{{name}}"',
|
---|
98 | unusedWithClass: 'Unused method or property "{{name}}" of class "{{className}}"',
|
---|
99 | };
|
---|
100 |
|
---|
101 | module.exports = {
|
---|
102 | meta: {
|
---|
103 | docs: {
|
---|
104 | description: 'Disallow declaring unused methods of component class',
|
---|
105 | category: 'Best Practices',
|
---|
106 | recommended: false,
|
---|
107 | url: docsUrl('no-unused-class-component-methods'),
|
---|
108 | },
|
---|
109 | messages,
|
---|
110 | schema: [],
|
---|
111 | },
|
---|
112 |
|
---|
113 | create: ((context) => {
|
---|
114 | let classInfo = null;
|
---|
115 |
|
---|
116 | // Takes an ObjectExpression node and adds all named Property nodes to the
|
---|
117 | // current set of properties.
|
---|
118 | function addProperty(node) {
|
---|
119 | classInfo.properties.add(node);
|
---|
120 | }
|
---|
121 |
|
---|
122 | // Adds the name of the given node as a used property if the node is an
|
---|
123 | // Identifier or a Literal. Other node types are ignored.
|
---|
124 | function addUsedProperty(node) {
|
---|
125 | const name = getName(node);
|
---|
126 | if (name) {
|
---|
127 | classInfo.usedProperties.add(name);
|
---|
128 | }
|
---|
129 | }
|
---|
130 |
|
---|
131 | function reportUnusedProperties() {
|
---|
132 | // Report all unused properties.
|
---|
133 | for (const node of classInfo.properties) { // eslint-disable-line no-restricted-syntax
|
---|
134 | const name = getName(node);
|
---|
135 | if (
|
---|
136 | !classInfo.usedProperties.has(name)
|
---|
137 | && !LIFECYCLE_METHODS.has(name)
|
---|
138 | && (classInfo.isClass ? !ES6_LIFECYCLE.has(name) : !ES5_LIFECYCLE.has(name))
|
---|
139 | ) {
|
---|
140 | const className = (classInfo.classNode.id && classInfo.classNode.id.name) || '';
|
---|
141 |
|
---|
142 | const messageID = className ? 'unusedWithClass' : 'unused';
|
---|
143 | report(
|
---|
144 | context,
|
---|
145 | messages[messageID],
|
---|
146 | messageID,
|
---|
147 | {
|
---|
148 | node,
|
---|
149 | data: {
|
---|
150 | name,
|
---|
151 | className,
|
---|
152 | },
|
---|
153 | }
|
---|
154 | );
|
---|
155 | }
|
---|
156 | }
|
---|
157 | }
|
---|
158 |
|
---|
159 | function exitMethod() {
|
---|
160 | if (!classInfo || !classInfo.inStatic) {
|
---|
161 | return;
|
---|
162 | }
|
---|
163 |
|
---|
164 | classInfo.inStatic = false;
|
---|
165 | }
|
---|
166 |
|
---|
167 | return {
|
---|
168 | ClassDeclaration(node) {
|
---|
169 | if (componentUtil.isES6Component(node, context)) {
|
---|
170 | classInfo = getInitialClassInfo(node, true);
|
---|
171 | }
|
---|
172 | },
|
---|
173 |
|
---|
174 | ObjectExpression(node) {
|
---|
175 | if (componentUtil.isES5Component(node, context)) {
|
---|
176 | classInfo = getInitialClassInfo(node, false);
|
---|
177 | }
|
---|
178 | },
|
---|
179 |
|
---|
180 | 'ClassDeclaration:exit'() {
|
---|
181 | if (!classInfo) {
|
---|
182 | return;
|
---|
183 | }
|
---|
184 | reportUnusedProperties();
|
---|
185 | classInfo = null;
|
---|
186 | },
|
---|
187 |
|
---|
188 | 'ObjectExpression:exit'(node) {
|
---|
189 | if (!classInfo || classInfo.classNode !== node) {
|
---|
190 | return;
|
---|
191 | }
|
---|
192 | reportUnusedProperties();
|
---|
193 | classInfo = null;
|
---|
194 | },
|
---|
195 |
|
---|
196 | Property(node) {
|
---|
197 | if (!classInfo || classInfo.classNode !== node.parent) {
|
---|
198 | return;
|
---|
199 | }
|
---|
200 |
|
---|
201 | if (isKeyLiteralLike(node, node.key)) {
|
---|
202 | addProperty(node.key);
|
---|
203 | }
|
---|
204 | },
|
---|
205 |
|
---|
206 | 'ClassProperty, MethodDefinition, PropertyDefinition'(node) {
|
---|
207 | if (!classInfo) {
|
---|
208 | return;
|
---|
209 | }
|
---|
210 |
|
---|
211 | if (node.static) {
|
---|
212 | classInfo.inStatic = true;
|
---|
213 | return;
|
---|
214 | }
|
---|
215 |
|
---|
216 | if (isKeyLiteralLike(node, node.key)) {
|
---|
217 | addProperty(node.key);
|
---|
218 | }
|
---|
219 | },
|
---|
220 |
|
---|
221 | 'ClassProperty:exit': exitMethod,
|
---|
222 | 'MethodDefinition:exit': exitMethod,
|
---|
223 | 'PropertyDefinition:exit': exitMethod,
|
---|
224 |
|
---|
225 | MemberExpression(node) {
|
---|
226 | if (!classInfo || classInfo.inStatic) {
|
---|
227 | return;
|
---|
228 | }
|
---|
229 |
|
---|
230 | if (isThisExpression(node.object) && isKeyLiteralLike(node, node.property)) {
|
---|
231 | if (node.parent.type === 'AssignmentExpression' && node.parent.left === node) {
|
---|
232 | // detect `this.property = xxx`
|
---|
233 | addProperty(node.property);
|
---|
234 | } else {
|
---|
235 | // detect `this.property()`, `x = this.property`, etc.
|
---|
236 | addUsedProperty(node.property);
|
---|
237 | }
|
---|
238 | }
|
---|
239 | },
|
---|
240 |
|
---|
241 | VariableDeclarator(node) {
|
---|
242 | if (!classInfo || classInfo.inStatic) {
|
---|
243 | return;
|
---|
244 | }
|
---|
245 |
|
---|
246 | // detect `{ foo, bar: baz } = this`
|
---|
247 | if (node.init && isThisExpression(node.init) && node.id.type === 'ObjectPattern') {
|
---|
248 | node.id.properties
|
---|
249 | .filter((prop) => prop.type === 'Property' && isKeyLiteralLike(prop, prop.key))
|
---|
250 | .forEach((prop) => {
|
---|
251 | addUsedProperty(prop.key);
|
---|
252 | });
|
---|
253 | }
|
---|
254 | },
|
---|
255 | };
|
---|
256 | }),
|
---|
257 | };
|
---|