1
1
/**
2
2
* @fileoverview Enforce stateless components to be written as a pure function
3
3
* @author Yannick Croissant
4
+ * @author Alberto Rodríguez
5
+ * @copyright 2015 Alberto Rodríguez. All rights reserved.
4
6
*/
5
7
'use strict' ;
6
8
@@ -14,19 +16,6 @@ module.exports = Components.detect(function(context, components, utils) {
14
16
15
17
var sourceCode = context . getSourceCode ( ) ;
16
18
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
-
30
19
// --------------------------------------------------------------------------
31
20
// Public
32
21
// --------------------------------------------------------------------------
@@ -64,24 +53,87 @@ module.exports = Components.detect(function(context, components, utils) {
64
53
}
65
54
66
55
/**
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
68
111
* @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.
70
113
*/
71
- function hasLifecycleMethod ( node ) {
114
+ function hasOtherProperties ( node ) {
72
115
var properties = getComponentProperties ( node ) ;
73
116
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 ;
75
127
} ) ;
76
128
}
77
129
78
130
/**
79
131
* Mark a setState as used
80
132
* @param {ASTNode } node The AST node being checked.
81
133
*/
82
- function markSetStateAsUsed ( node ) {
134
+ function markThisAsUsed ( node ) {
83
135
components . set ( node , {
84
- useSetState : true
136
+ useThis : true
85
137
} ) ;
86
138
}
87
139
@@ -95,18 +147,48 @@ module.exports = Components.detect(function(context, components, utils) {
95
147
} ) ;
96
148
}
97
149
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
+
98
160
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' ) {
102
165
return ;
103
166
}
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
+ ) {
105
186
return ;
106
187
}
107
- markSetStateAsUsed ( node ) ;
188
+ markThisAsUsed ( node ) ;
108
189
} ,
109
190
191
+ // Mark `ref` usage
110
192
JSXAttribute : function ( node ) {
111
193
var name = sourceCode . getText ( node . name ) ;
112
194
if ( name !== 'ref' ) {
@@ -115,14 +197,32 @@ module.exports = Components.detect(function(context, components, utils) {
115
197
markRefAsUsed ( node ) ;
116
198
} ,
117
199
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
+
118
217
'Program:exit' : function ( ) {
119
218
var list = components . list ( ) ;
120
219
for ( var component in list ) {
121
220
if (
122
221
! list . hasOwnProperty ( component ) ||
123
- hasLifecycleMethod ( list [ component ] . node ) ||
124
- list [ component ] . useSetState ||
222
+ hasOtherProperties ( list [ component ] . node ) ||
223
+ list [ component ] . useThis ||
125
224
list [ component ] . useRef ||
225
+ list [ component ] . invalidReturn ||
126
226
( ! utils . isES5Component ( list [ component ] . node ) && ! utils . isES6Component ( list [ component ] . node ) )
127
227
) {
128
228
continue ;
0 commit comments