UNPKG

12.5 kBJavaScriptView Raw
1/**
2 * @fileoverview Enforce stateless components to be written as a pure function
3 * @author Yannick Croissant
4 * @author Alberto Rodríguez
5 * @copyright 2015 Alberto Rodríguez. All rights reserved.
6 */
7
8'use strict';
9
10const Components = require('../util/Components');
11const versionUtil = require('../util/version');
12const astUtil = require('../util/ast');
13const docsUrl = require('../util/docsUrl');
14
15// ------------------------------------------------------------------------------
16// Rule Definition
17// ------------------------------------------------------------------------------
18
19module.exports = {
20 meta: {
21 docs: {
22 description: 'Enforce stateless components to be written as a pure function',
23 category: 'Stylistic Issues',
24 recommended: false,
25 url: docsUrl('prefer-stateless-function')
26 },
27 schema: [{
28 type: 'object',
29 properties: {
30 ignorePureComponents: {
31 default: false,
32 type: 'boolean'
33 }
34 },
35 additionalProperties: false
36 }]
37 },
38
39 create: Components.detect((context, components, utils) => {
40 const configuration = context.options[0] || {};
41 const ignorePureComponents = configuration.ignorePureComponents || false;
42
43 // --------------------------------------------------------------------------
44 // Public
45 // --------------------------------------------------------------------------
46
47 /**
48 * Checks whether a given array of statements is a single call of `super`.
49 * @see ESLint no-useless-constructor rule
50 * @param {ASTNode[]} body - An array of statements to check.
51 * @returns {boolean} `true` if the body is a single call of `super`.
52 */
53 function isSingleSuperCall(body) {
54 return (
55 body.length === 1
56 && body[0].type === 'ExpressionStatement'
57 && body[0].expression.type === 'CallExpression'
58 && body[0].expression.callee.type === 'Super'
59 );
60 }
61
62 /**
63 * Checks whether a given node is a pattern which doesn't have any side effects.
64 * Default parameters and Destructuring parameters can have side effects.
65 * @see ESLint no-useless-constructor rule
66 * @param {ASTNode} node - A pattern node.
67 * @returns {boolean} `true` if the node doesn't have any side effects.
68 */
69 function isSimple(node) {
70 return node.type === 'Identifier' || node.type === 'RestElement';
71 }
72
73 /**
74 * Checks whether a given array of expressions is `...arguments` or not.
75 * `super(...arguments)` passes all arguments through.
76 * @see ESLint no-useless-constructor rule
77 * @param {ASTNode[]} superArgs - An array of expressions to check.
78 * @returns {boolean} `true` if the superArgs is `...arguments`.
79 */
80 function isSpreadArguments(superArgs) {
81 return (
82 superArgs.length === 1
83 && superArgs[0].type === 'SpreadElement'
84 && superArgs[0].argument.type === 'Identifier'
85 && superArgs[0].argument.name === 'arguments'
86 );
87 }
88
89 /**
90 * Checks whether given 2 nodes are identifiers which have the same name or not.
91 * @see ESLint no-useless-constructor rule
92 * @param {ASTNode} ctorParam - A node to check.
93 * @param {ASTNode} superArg - A node to check.
94 * @returns {boolean} `true` if the nodes are identifiers which have the same
95 * name.
96 */
97 function isValidIdentifierPair(ctorParam, superArg) {
98 return (
99 ctorParam.type === 'Identifier'
100 && superArg.type === 'Identifier'
101 && ctorParam.name === superArg.name
102 );
103 }
104
105 /**
106 * Checks whether given 2 nodes are a rest/spread pair which has the same values.
107 * @see ESLint no-useless-constructor rule
108 * @param {ASTNode} ctorParam - A node to check.
109 * @param {ASTNode} superArg - A node to check.
110 * @returns {boolean} `true` if the nodes are a rest/spread pair which has the
111 * same values.
112 */
113 function isValidRestSpreadPair(ctorParam, superArg) {
114 return (
115 ctorParam.type === 'RestElement'
116 && superArg.type === 'SpreadElement'
117 && isValidIdentifierPair(ctorParam.argument, superArg.argument)
118 );
119 }
120
121 /**
122 * Checks whether given 2 nodes have the same value or not.
123 * @see ESLint no-useless-constructor rule
124 * @param {ASTNode} ctorParam - A node to check.
125 * @param {ASTNode} superArg - A node to check.
126 * @returns {boolean} `true` if the nodes have the same value or not.
127 */
128 function isValidPair(ctorParam, superArg) {
129 return (
130 isValidIdentifierPair(ctorParam, superArg)
131 || isValidRestSpreadPair(ctorParam, superArg)
132 );
133 }
134
135 /**
136 * Checks whether the parameters of a constructor and the arguments of `super()`
137 * have the same values or not.
138 * @see ESLint no-useless-constructor rule
139 * @param {ASTNode[]} ctorParams - The parameters of a constructor to check.
140 * @param {ASTNode} superArgs - The arguments of `super()` to check.
141 * @returns {boolean} `true` if those have the same values.
142 */
143 function isPassingThrough(ctorParams, superArgs) {
144 if (ctorParams.length !== superArgs.length) {
145 return false;
146 }
147
148 for (let i = 0; i < ctorParams.length; ++i) {
149 if (!isValidPair(ctorParams[i], superArgs[i])) {
150 return false;
151 }
152 }
153
154 return true;
155 }
156
157 /**
158 * Checks whether the constructor body is a redundant super call.
159 * @see ESLint no-useless-constructor rule
160 * @param {Array} body - constructor body content.
161 * @param {Array} ctorParams - The params to check against super call.
162 * @returns {boolean} true if the construtor body is redundant
163 */
164 function isRedundantSuperCall(body, ctorParams) {
165 return (
166 isSingleSuperCall(body)
167 && ctorParams.every(isSimple)
168 && (
169 isSpreadArguments(body[0].expression.arguments)
170 || isPassingThrough(ctorParams, body[0].expression.arguments)
171 )
172 );
173 }
174
175 /**
176 * Check if a given AST node have any other properties the ones available in stateless components
177 * @param {ASTNode} node The AST node being checked.
178 * @returns {Boolean} True if the node has at least one other property, false if not.
179 */
180 function hasOtherProperties(node) {
181 const properties = astUtil.getComponentProperties(node);
182 return properties.some((property) => {
183 const name = astUtil.getPropertyName(property);
184 const isDisplayName = name === 'displayName';
185 const isPropTypes = name === 'propTypes' || name === 'props' && property.typeAnnotation;
186 const contextTypes = name === 'contextTypes';
187 const defaultProps = name === 'defaultProps';
188 const isUselessConstructor = property.kind === 'constructor'
189 && !!property.value.body
190 && isRedundantSuperCall(property.value.body.body, property.value.params);
191 const isRender = name === 'render';
192 return !isDisplayName && !isPropTypes && !contextTypes && !defaultProps && !isUselessConstructor && !isRender;
193 });
194 }
195
196 /**
197 * Mark component as pure as declared
198 * @param {ASTNode} node The AST node being checked.
199 */
200 function markSCUAsDeclared(node) {
201 components.set(node, {
202 hasSCU: true
203 });
204 }
205
206 /**
207 * Mark childContextTypes as declared
208 * @param {ASTNode} node The AST node being checked.
209 */
210 function markChildContextTypesAsDeclared(node) {
211 components.set(node, {
212 hasChildContextTypes: true
213 });
214 }
215
216 /**
217 * Mark a setState as used
218 * @param {ASTNode} node The AST node being checked.
219 */
220 function markThisAsUsed(node) {
221 components.set(node, {
222 useThis: true
223 });
224 }
225
226 /**
227 * Mark a props or context as used
228 * @param {ASTNode} node The AST node being checked.
229 */
230 function markPropsOrContextAsUsed(node) {
231 components.set(node, {
232 usePropsOrContext: true
233 });
234 }
235
236 /**
237 * Mark a ref as used
238 * @param {ASTNode} node The AST node being checked.
239 */
240 function markRefAsUsed(node) {
241 components.set(node, {
242 useRef: true
243 });
244 }
245
246 /**
247 * Mark return as invalid
248 * @param {ASTNode} node The AST node being checked.
249 */
250 function markReturnAsInvalid(node) {
251 components.set(node, {
252 invalidReturn: true
253 });
254 }
255
256 /**
257 * Mark a ClassDeclaration as having used decorators
258 * @param {ASTNode} node The AST node being checked.
259 */
260 function markDecoratorsAsUsed(node) {
261 components.set(node, {
262 useDecorators: true
263 });
264 }
265
266 function visitClass(node) {
267 if (ignorePureComponents && utils.isPureComponent(node)) {
268 markSCUAsDeclared(node);
269 }
270
271 if (node.decorators && node.decorators.length) {
272 markDecoratorsAsUsed(node);
273 }
274 }
275
276 return {
277 ClassDeclaration: visitClass,
278 ClassExpression: visitClass,
279
280 // Mark `this` destructuring as a usage of `this`
281 VariableDeclarator(node) {
282 // Ignore destructuring on other than `this`
283 if (!node.id || node.id.type !== 'ObjectPattern' || !node.init || node.init.type !== 'ThisExpression') {
284 return;
285 }
286 // Ignore `props` and `context`
287 const useThis = node.id.properties.some((property) => {
288 const name = astUtil.getPropertyName(property);
289 return name !== 'props' && name !== 'context';
290 });
291 if (!useThis) {
292 markPropsOrContextAsUsed(node);
293 return;
294 }
295 markThisAsUsed(node);
296 },
297
298 // Mark `this` usage
299 MemberExpression(node) {
300 if (node.object.type !== 'ThisExpression') {
301 if (node.property && node.property.name === 'childContextTypes') {
302 const component = utils.getRelatedComponent(node);
303 if (!component) {
304 return;
305 }
306 markChildContextTypesAsDeclared(component.node);
307 }
308 return;
309 // Ignore calls to `this.props` and `this.context`
310 }
311 if (
312 (node.property.name || node.property.value) === 'props'
313 || (node.property.name || node.property.value) === 'context'
314 ) {
315 markPropsOrContextAsUsed(node);
316 return;
317 }
318 markThisAsUsed(node);
319 },
320
321 // Mark `ref` usage
322 JSXAttribute(node) {
323 const name = context.getSourceCode().getText(node.name);
324 if (name !== 'ref') {
325 return;
326 }
327 markRefAsUsed(node);
328 },
329
330 // Mark `render` that do not return some JSX
331 ReturnStatement(node) {
332 let blockNode;
333 let scope = context.getScope();
334 while (scope) {
335 blockNode = scope.block && scope.block.parent;
336 if (blockNode && (blockNode.type === 'MethodDefinition' || blockNode.type === 'Property')) {
337 break;
338 }
339 scope = scope.upper;
340 }
341 const isRender = blockNode && blockNode.key && blockNode.key.name === 'render';
342 const allowNull = versionUtil.testReactVersion(context, '15.0.0'); // Stateless components can return null since React 15
343 const isReturningJSX = utils.isReturningJSX(node, !allowNull);
344 const isReturningNull = node.argument && (node.argument.value === null || node.argument.value === false);
345 if (
346 !isRender
347 || (allowNull && (isReturningJSX || isReturningNull))
348 || (!allowNull && isReturningJSX)
349 ) {
350 return;
351 }
352 markReturnAsInvalid(node);
353 },
354
355 'Program:exit'() {
356 const list = components.list();
357 Object.keys(list).forEach((component) => {
358 if (
359 hasOtherProperties(list[component].node)
360 || list[component].useThis
361 || list[component].useRef
362 || list[component].invalidReturn
363 || list[component].hasChildContextTypes
364 || list[component].useDecorators
365 || (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node))
366 ) {
367 return;
368 }
369
370 if (list[component].hasSCU) {
371 return;
372 }
373 context.report({
374 node: list[component].node,
375 message: 'Component should be written as a pure function'
376 });
377 });
378 }
379 };
380 })
381};