Skip to content

Commit cfca370

Browse files
committed
Rewrite prefer-stateless-function rule (fixes #491)
1 parent 426b228 commit cfca370

File tree

2 files changed

+361
-30
lines changed

2 files changed

+361
-30
lines changed

lib/rules/prefer-stateless-function.js

Lines changed: 126 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
22
* @fileoverview Enforce stateless components to be written as a pure function
33
* @author Yannick Croissant
4+
* @author Alberto Rodríguez
5+
* @copyright 2015 Alberto Rodríguez. All rights reserved.
46
*/
57
'use strict';
68

@@ -14,19 +16,6 @@ module.exports = Components.detect(function(context, components, utils) {
1416

1517
var sourceCode = context.getSourceCode();
1618

17-
var lifecycleMethods = [
18-
'state',
19-
'getInitialState',
20-
'getChildContext',
21-
'componentWillMount',
22-
'componentDidMount',
23-
'componentWillReceiveProps',
24-
'shouldComponentUpdate',
25-
'componentWillUpdate',
26-
'componentDidUpdate',
27-
'componentWillUnmount'
28-
];
29-
3019
// --------------------------------------------------------------------------
3120
// Public
3221
// --------------------------------------------------------------------------
@@ -64,24 +53,87 @@ module.exports = Components.detect(function(context, components, utils) {
6453
}
6554

6655
/**
67-
* Check if a given AST node have any lifecycle method
56+
* Checks whether the constructor body is a redundant super call.
57+
* @see ESLint no-useless-constructor rule
58+
* @param {Array} body - constructor body content.
59+
* @param {Array} ctorParams - The params to check against super call.
60+
* @returns {boolean} true if the construtor body is redundant
61+
*/
62+
function isRedundantSuperCall(body, ctorParams) {
63+
if (
64+
body.length !== 1 ||
65+
body[0].type !== 'ExpressionStatement' ||
66+
body[0].expression.callee.type !== 'Super'
67+
) {
68+
return false;
69+
}
70+
71+
var superArgs = body[0].expression.arguments;
72+
var firstSuperArg = superArgs[0];
73+
var lastSuperArgIndex = superArgs.length - 1;
74+
var lastSuperArg = superArgs[lastSuperArgIndex];
75+
var isSimpleParameterList = ctorParams.every(function(param) {
76+
return param.type === 'Identifier' || param.type === 'RestElement';
77+
});
78+
79+
/**
80+
* Checks if a super argument is the same with constructor argument
81+
* @param {ASTNode} arg argument node
82+
* @param {number} index argument index
83+
* @returns {boolean} true if the arguments are same, false otherwise
84+
*/
85+
function isSameIdentifier(arg, index) {
86+
return (
87+
arg.type === 'Identifier' &&
88+
arg.name === ctorParams[index].name
89+
);
90+
}
91+
92+
var spreadsArguments =
93+
superArgs.length === 1 &&
94+
firstSuperArg.type === 'SpreadElement' &&
95+
firstSuperArg.argument.name === 'arguments';
96+
97+
var passesParamsAsArgs =
98+
superArgs.length === ctorParams.length &&
99+
superArgs.every(isSameIdentifier) ||
100+
superArgs.length <= ctorParams.length &&
101+
superArgs.slice(0, -1).every(isSameIdentifier) &&
102+
lastSuperArg.type === 'SpreadElement' &&
103+
ctorParams[lastSuperArgIndex].type === 'RestElement' &&
104+
lastSuperArg.argument.name === ctorParams[lastSuperArgIndex].argument.name;
105+
106+
return isSimpleParameterList && (spreadsArguments || passesParamsAsArgs);
107+
}
108+
109+
/**
110+
* Check if a given AST node have any other properties the ones available in stateless components
68111
* @param {ASTNode} node The AST node being checked.
69-
* @returns {Boolean} True if the node has at least one lifecycle method, false if not.
112+
* @returns {Boolean} True if the node has at least one other property, false if not.
70113
*/
71-
function hasLifecycleMethod(node) {
114+
function hasOtherProperties(node) {
72115
var properties = getComponentProperties(node);
73116
return properties.some(function(property) {
74-
return lifecycleMethods.indexOf(getPropertyName(property)) !== -1;
117+
var name = getPropertyName(property);
118+
var isDisplayName = name === 'displayName';
119+
var isPropTypes = name === 'propTypes' || name === 'props' && property.typeAnnotation;
120+
var contextTypes = name === 'contextTypes';
121+
var isUselessConstructor =
122+
property.kind === 'constructor' &&
123+
isRedundantSuperCall(property.value.body.body, property.value.params)
124+
;
125+
var isRender = name === 'render';
126+
return !isDisplayName && !isPropTypes && !contextTypes && !isUselessConstructor && !isRender;
75127
});
76128
}
77129

78130
/**
79131
* Mark a setState as used
80132
* @param {ASTNode} node The AST node being checked.
81133
*/
82-
function markSetStateAsUsed(node) {
134+
function markThisAsUsed(node) {
83135
components.set(node, {
84-
useSetState: true
136+
useThis: true
85137
});
86138
}
87139

@@ -95,18 +147,48 @@ module.exports = Components.detect(function(context, components, utils) {
95147
});
96148
}
97149

150+
/**
151+
* Mark return as invalid
152+
* @param {ASTNode} node The AST node being checked.
153+
*/
154+
function markReturnAsInvalid(node) {
155+
components.set(node, {
156+
invalidReturn: true
157+
});
158+
}
159+
98160
return {
99-
CallExpression: function(node) {
100-
var callee = node.callee;
101-
if (callee.type !== 'MemberExpression') {
161+
// Mark `this` destructuring as a usage of `this`
162+
VariableDeclarator: function(node) {
163+
// Ignore destructuring on other than `this`
164+
if (!node.id || node.id.type !== 'ObjectPattern' || !node.init || node.init.type !== 'ThisExpression') {
102165
return;
103166
}
104-
if (callee.object.type !== 'ThisExpression' || callee.property.name !== 'setState') {
167+
// Ignore `props` and `context`
168+
var useThis = node.id.properties.some(function(property) {
169+
var name = getPropertyName(property);
170+
return name !== 'props' && name !== 'context';
171+
});
172+
if (!useThis) {
173+
return;
174+
}
175+
markThisAsUsed(node);
176+
},
177+
178+
// Mark `this` usage
179+
MemberExpression: function(node) {
180+
// Ignore calls to `this.props` and `this.context`
181+
if (
182+
node.object.type !== 'ThisExpression' ||
183+
(node.property.name || node.property.value) === 'props' ||
184+
(node.property.name || node.property.value) === 'context'
185+
) {
105186
return;
106187
}
107-
markSetStateAsUsed(node);
188+
markThisAsUsed(node);
108189
},
109190

191+
// Mark `ref` usage
110192
JSXAttribute: function(node) {
111193
var name = sourceCode.getText(node.name);
112194
if (name !== 'ref') {
@@ -115,14 +197,32 @@ module.exports = Components.detect(function(context, components, utils) {
115197
markRefAsUsed(node);
116198
},
117199

200+
// Mark `render` that do not return some JSX
201+
ReturnStatement: function(node) {
202+
var blockNode;
203+
var scope = context.getScope();
204+
while (scope) {
205+
blockNode = scope.block && scope.block.parent;
206+
if (blockNode && (blockNode.type === 'MethodDefinition' || blockNode.type === 'Property')) {
207+
break;
208+
}
209+
scope = scope.upper;
210+
}
211+
if (!blockNode || !blockNode.key || blockNode.key.name !== 'render' || utils.isReturningJSX(node)) {
212+
return;
213+
}
214+
markReturnAsInvalid(node);
215+
},
216+
118217
'Program:exit': function() {
119218
var list = components.list();
120219
for (var component in list) {
121220
if (
122221
!list.hasOwnProperty(component) ||
123-
hasLifecycleMethod(list[component].node) ||
124-
list[component].useSetState ||
222+
hasOtherProperties(list[component].node) ||
223+
list[component].useThis ||
125224
list[component].useRef ||
225+
list[component].invalidReturn ||
126226
(!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node))
127227
) {
128228
continue;

0 commit comments

Comments
 (0)