UNPKG

6.75 kBJavaScriptView Raw
1/**
2 * @fileoverview Enforce React components to have a shouldComponentUpdate method
3 * @author Evgueni Naverniouk
4 */
5
6'use strict';
7
8const Components = require('../util/Components');
9const docsUrl = require('../util/docsUrl');
10
11module.exports = {
12 meta: {
13 docs: {
14 description: 'Enforce React components to have a shouldComponentUpdate method',
15 category: 'Best Practices',
16 recommended: false,
17 url: docsUrl('require-optimization')
18 },
19
20 schema: [{
21 type: 'object',
22 properties: {
23 allowDecorators: {
24 type: 'array',
25 items: {
26 type: 'string'
27 }
28 }
29 },
30 additionalProperties: false
31 }]
32 },
33
34 create: Components.detect((context, components, utils) => {
35 const MISSING_MESSAGE = 'Component is not optimized. Please add a shouldComponentUpdate method.';
36 const configuration = context.options[0] || {};
37 const allowDecorators = configuration.allowDecorators || [];
38
39 /**
40 * Checks to see if our component is decorated by PureRenderMixin via reactMixin
41 * @param {ASTNode} node The AST node being checked.
42 * @returns {Boolean} True if node is decorated with a PureRenderMixin, false if not.
43 */
44 function hasPureRenderDecorator(node) {
45 if (node.decorators && node.decorators.length) {
46 for (let i = 0, l = node.decorators.length; i < l; i++) {
47 if (
48 node.decorators[i].expression
49 && node.decorators[i].expression.callee
50 && node.decorators[i].expression.callee.object
51 && node.decorators[i].expression.callee.object.name === 'reactMixin'
52 && node.decorators[i].expression.callee.property
53 && node.decorators[i].expression.callee.property.name === 'decorate'
54 && node.decorators[i].expression.arguments
55 && node.decorators[i].expression.arguments.length
56 && node.decorators[i].expression.arguments[0].name === 'PureRenderMixin'
57 ) {
58 return true;
59 }
60 }
61 }
62
63 return false;
64 }
65
66 /**
67 * Checks to see if our component is custom decorated
68 * @param {ASTNode} node The AST node being checked.
69 * @returns {Boolean} True if node is decorated name with a custom decorated, false if not.
70 */
71 function hasCustomDecorator(node) {
72 const allowLength = allowDecorators.length;
73
74 if (allowLength && node.decorators && node.decorators.length) {
75 for (let i = 0; i < allowLength; i++) {
76 for (let j = 0, l = node.decorators.length; j < l; j++) {
77 if (
78 node.decorators[j].expression
79 && node.decorators[j].expression.name === allowDecorators[i]
80 ) {
81 return true;
82 }
83 }
84 }
85 }
86
87 return false;
88 }
89
90 /**
91 * Checks if we are declaring a shouldComponentUpdate method
92 * @param {ASTNode} node The AST node being checked.
93 * @returns {Boolean} True if we are declaring a shouldComponentUpdate method, false if not.
94 */
95 function isSCUDeclared(node) {
96 return Boolean(
97 node
98 && node.name === 'shouldComponentUpdate'
99 );
100 }
101
102 /**
103 * Checks if we are declaring a PureRenderMixin mixin
104 * @param {ASTNode} node The AST node being checked.
105 * @returns {Boolean} True if we are declaring a PureRenderMixin method, false if not.
106 */
107 function isPureRenderDeclared(node) {
108 let hasPR = false;
109 if (node.value && node.value.elements) {
110 for (let i = 0, l = node.value.elements.length; i < l; i++) {
111 if (node.value.elements[i] && node.value.elements[i].name === 'PureRenderMixin') {
112 hasPR = true;
113 break;
114 }
115 }
116 }
117
118 return Boolean(
119 node
120 && node.key.name === 'mixins'
121 && hasPR
122 );
123 }
124
125 /**
126 * Mark shouldComponentUpdate as declared
127 * @param {ASTNode} node The AST node being checked.
128 */
129 function markSCUAsDeclared(node) {
130 components.set(node, {
131 hasSCU: true
132 });
133 }
134
135 /**
136 * Reports missing optimization for a given component
137 * @param {Object} component The component to process
138 */
139 function reportMissingOptimization(component) {
140 context.report({
141 node: component.node,
142 message: MISSING_MESSAGE,
143 data: {
144 component: component.name
145 }
146 });
147 }
148
149 /**
150 * Checks if we are declaring function in class
151 * @returns {Boolean} True if we are declaring function in class, false if not.
152 */
153 function isFunctionInClass() {
154 let blockNode;
155 let scope = context.getScope();
156 while (scope) {
157 blockNode = scope.block;
158 if (blockNode && blockNode.type === 'ClassDeclaration') {
159 return true;
160 }
161 scope = scope.upper;
162 }
163
164 return false;
165 }
166
167 return {
168 ArrowFunctionExpression(node) {
169 // Skip if the function is declared in the class
170 if (isFunctionInClass()) {
171 return;
172 }
173 // Stateless Functional Components cannot be optimized (yet)
174 markSCUAsDeclared(node);
175 },
176
177 ClassDeclaration(node) {
178 if (!(hasPureRenderDecorator(node) || hasCustomDecorator(node) || utils.isPureComponent(node))) {
179 return;
180 }
181 markSCUAsDeclared(node);
182 },
183
184 FunctionDeclaration(node) {
185 // Skip if the function is declared in the class
186 if (isFunctionInClass()) {
187 return;
188 }
189 // Stateless Functional Components cannot be optimized (yet)
190 markSCUAsDeclared(node);
191 },
192
193 FunctionExpression(node) {
194 // Skip if the function is declared in the class
195 if (isFunctionInClass()) {
196 return;
197 }
198 // Stateless Functional Components cannot be optimized (yet)
199 markSCUAsDeclared(node);
200 },
201
202 MethodDefinition(node) {
203 if (!isSCUDeclared(node.key)) {
204 return;
205 }
206 markSCUAsDeclared(node);
207 },
208
209 ObjectExpression(node) {
210 // Search for the shouldComponentUpdate declaration
211 const found = node.properties.some((property) => (
212 property.key
213 && (isSCUDeclared(property.key) || isPureRenderDeclared(property))
214 ));
215 if (found) {
216 markSCUAsDeclared(node);
217 }
218 },
219
220 'Program:exit'() {
221 const list = components.list();
222
223 // Report missing shouldComponentUpdate for all components
224 Object.keys(list).filter((component) => !list[component].hasSCU).forEach((component) => {
225 reportMissingOptimization(list[component]);
226 });
227 }
228 };
229 })
230};