Skip to content

Commit 7024678

Browse files
teamehyannickcr
authored andcommitted
Add PureComponent support to prefer-stateless-function
1 parent d4dee48 commit 7024678

File tree

6 files changed

+163
-31
lines changed

6 files changed

+163
-31
lines changed

docs/rules/prefer-stateless-function.md

+44-7
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,33 @@ This rule will check your class based React components for
88

99
* methods/properties other than `displayName`, `propTypes`, `render` and useless constructor (same detection as ESLint [no-useless-constructor rule](http://eslint.org/docs/rules/no-useless-constructor))
1010
* instance property other than `this.props` and `this.context`
11+
* extension of `React.PureComponent` ()
1112
* presence of `ref` attribute in JSX
1213
* `render` method that return anything but JSX: `undefined`, `null`, etc. (only in React <15.0.0, see [shared settings](https://github.com/yannickcr/eslint-plugin-react/blob/master/README.md#configuration) for React version configuration)
1314

14-
If none of these 4 elements are found, the rule will warn you to write this component as a pure function.
15+
If none of these elements are found, the rule will warn you to write this component as a pure function.
1516

16-
The following pattern is considered warnings:
17+
The following pattern is considered a warning:
1718

18-
```js
19+
```jsx
1920
var Hello = React.createClass({
2021
render: function() {
2122
return <div>Hello {this.props.name}</div>;
2223
}
2324
});
2425
```
2526

26-
The following pattern is not considered warnings:
27+
The following pattern is not considered a warning:
2728

28-
```js
29+
```jsx
2930
const Foo = function(props) {
3031
return <div>{props.foo}</div>;
3132
};
3233
```
3334

34-
The following pattern is not considered warning in React <15.0.0:
35+
The following pattern is not considered a warning in React <15.0.0:
3536

36-
```js
37+
```jsx
3738
class Foo extends React.Component {
3839
render() {
3940
if (!this.props.foo) {
@@ -43,3 +44,39 @@ class Foo extends React.Component {
4344
}
4445
}
4546
```
47+
48+
49+
## Rule Options
50+
51+
```js
52+
...
53+
"prefer-stateless-function": [<enabled>, { "ignorePureComponent": <ignorePureComponent> }]
54+
...
55+
```
56+
57+
* `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0.
58+
* `ignorePureComponent`: optional boolean set to `true` to ignore components extending from `React.PureComponent` (default to `false`).
59+
60+
### `ignorePureComponent`
61+
62+
When `true` the rule will ignore Components extending from `React.PureComponent` that use `this.props` or `this.context`.
63+
64+
The following patterns is considered okay and does not cause warnings:
65+
66+
```jsx
67+
class Foo extends React.PureComponent {
68+
render() {
69+
return <div>{this.props.foo}</div>;
70+
}
71+
}
72+
```
73+
74+
The following pattern is considered a warning because it's not using props or context:
75+
76+
```jsx
77+
class Foo extends React.PureComponent {
78+
render() {
79+
return <div>Bar</div>;
80+
}
81+
}
82+
```

lib/rules/no-multi-comp.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ module.exports = {
4343
* @returns {Boolean} True if the component is ignored, false if not.
4444
*/
4545
function isIgnored(component) {
46-
return ignoreStateless === true && /Function/.test(component.node.type);
46+
return ignoreStateless && /Function/.test(component.node.type);
4747
}
4848

4949
// --------------------------------------------------------------------------

lib/rules/prefer-stateless-function.js

+48-3
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,23 @@ module.exports = {
2020
category: 'Stylistic Issues',
2121
recommended: false
2222
},
23-
schema: []
23+
schema: [{
24+
type: 'object',
25+
properties: {
26+
ignorePureComponents: {
27+
default: false,
28+
type: 'boolean'
29+
}
30+
},
31+
additionalProperties: false
32+
}]
2433
},
2534

2635
create: Components.detect(function(context, components, utils) {
2736

37+
var configuration = context.options[0] || {};
38+
var ignorePureComponents = configuration.ignorePureComponents || false;
39+
2840
var sourceCode = context.getSourceCode();
2941

3042
// --------------------------------------------------------------------------
@@ -213,6 +225,16 @@ module.exports = {
213225
});
214226
}
215227

228+
/**
229+
* Mark component as pure as declared
230+
* @param {ASTNode} node The AST node being checked.
231+
*/
232+
var markSCUAsDeclared = function (node) {
233+
components.set(node, {
234+
hasSCU: true
235+
});
236+
};
237+
216238
/**
217239
* Mark a setState as used
218240
* @param {ASTNode} node The AST node being checked.
@@ -223,6 +245,16 @@ module.exports = {
223245
});
224246
}
225247

248+
/**
249+
* Mark a props or context as used
250+
* @param {ASTNode} node The AST node being checked.
251+
*/
252+
function markPropsOrContextAsUsed(node) {
253+
components.set(node, {
254+
usePropsOrContext: true
255+
});
256+
}
257+
226258
/**
227259
* Mark a ref as used
228260
* @param {ASTNode} node The AST node being checked.
@@ -244,6 +276,12 @@ module.exports = {
244276
}
245277

246278
return {
279+
ClassDeclaration: function (node) {
280+
if (ignorePureComponents && utils.isPureComponent(node)) {
281+
markSCUAsDeclared(node);
282+
}
283+
},
284+
247285
// Mark `this` destructuring as a usage of `this`
248286
VariableDeclarator: function(node) {
249287
// Ignore destructuring on other than `this`
@@ -256,6 +294,7 @@ module.exports = {
256294
return name !== 'props' && name !== 'context';
257295
});
258296
if (!useThis) {
297+
markPropsOrContextAsUsed(node);
259298
return;
260299
}
261300
markThisAsUsed(node);
@@ -264,11 +303,13 @@ module.exports = {
264303
// Mark `this` usage
265304
MemberExpression: function(node) {
266305
// Ignore calls to `this.props` and `this.context`
267-
if (
268-
node.object.type !== 'ThisExpression' ||
306+
if (node.object.type !== 'ThisExpression') {
307+
return;
308+
} else if (
269309
(node.property.name || node.property.value) === 'props' ||
270310
(node.property.name || node.property.value) === 'context'
271311
) {
312+
markPropsOrContextAsUsed(node);
272313
return;
273314
}
274315
markThisAsUsed(node);
@@ -322,6 +363,10 @@ module.exports = {
322363
continue;
323364
}
324365

366+
if (list[component].hasSCU && list[component].usePropsOrContext) {
367+
continue;
368+
}
369+
325370
context.report({
326371
node: list[component].node,
327372
message: 'Component should be written as a pure function'

lib/rules/require-optimization.js

+2-20
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
'use strict';
66

77
var Components = require('../util/Components');
8-
var pragmaUtil = require('../util/pragma');
98

109
module.exports = {
1110
meta: {
@@ -29,15 +28,11 @@ module.exports = {
2928
}]
3029
},
3130

32-
create: Components.detect(function (context, components) {
31+
create: Components.detect(function (context, components, utils) {
3332
var MISSING_MESSAGE = 'Component is not optimized. Please add a shouldComponentUpdate method.';
3433
var configuration = context.options[0] || {};
3534
var allowDecorators = configuration.allowDecorators || [];
3635

37-
var pragma = pragmaUtil.getFromContext(context);
38-
var pureComponentRegExp = new RegExp('^(' + pragma + '\\.)?PureComponent$');
39-
var sourceCode = context.getSourceCode();
40-
4136
/**
4237
* Checks to see if our component is decorated by PureRenderMixin via reactMixin
4338
* @param {ASTNode} node The AST node being checked.
@@ -89,19 +84,6 @@ module.exports = {
8984
return false;
9085
};
9186

92-
/**
93-
* Checks to see if our component extends React.PureComponent
94-
* @param {ASTNode} node The AST node being checked.
95-
* @returns {Boolean} True if node extends React.PureComponent, false if not.
96-
*/
97-
var isPureComponent = function (node) {
98-
if (node.superClass) {
99-
return pureComponentRegExp.test(sourceCode.getText(node.superClass));
100-
}
101-
102-
return false;
103-
};
104-
10587
/**
10688
* Checks if we are declaring a shouldComponentUpdate method
10789
* @param {ASTNode} node The AST node being checked.
@@ -186,7 +168,7 @@ module.exports = {
186168
},
187169

188170
ClassDeclaration: function (node) {
189-
if (!(hasPureRenderDecorator(node) || hasCustomDecorator(node) || isPureComponent(node))) {
171+
if (!(hasPureRenderDecorator(node) || hasCustomDecorator(node) || utils.isPureComponent(node))) {
190172
return;
191173
}
192174
markSCUAsDeclared(node);

lib/util/Components.js

+13
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,19 @@ function componentRule(rule, context) {
199199
return relevantTags.length > 0;
200200
},
201201

202+
/**
203+
* Checks to see if our component extends React.PureComponent
204+
*
205+
* @param {ASTNode} node The AST node being checked.
206+
* @returns {Boolean} True if node extends React.PureComponent, false if not
207+
*/
208+
isPureComponent: function (node) {
209+
if (node.superClass) {
210+
return new RegExp('^(' + pragma + '\\.)?PureComponent$').test(sourceCode.getText(node.superClass));
211+
}
212+
return false;
213+
},
214+
202215
/**
203216
* Check if the node is returning JSX
204217
*

tests/lib/rules/prefer-stateless-function.js

+55
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,32 @@ ruleTester.run('prefer-stateless-function', rule, {
3838
// Already a stateless (arrow) function
3939
code: 'const Foo = ({foo}) => <div>{foo}</div>;',
4040
parserOptions: parserOptions
41+
}, {
42+
// Extends from PureComponent and uses props
43+
code: [
44+
'class Foo extends React.PureComponent {',
45+
' render() {',
46+
' return <div>{this.props.foo}</div>;',
47+
' }',
48+
'}'
49+
].join('\n'),
50+
parserOptions: parserOptions,
51+
options: [{
52+
ignorePureComponents: true
53+
}]
54+
}, {
55+
// Extends from PureComponent and uses context
56+
code: [
57+
'class Foo extends React.PureComponent {',
58+
' render() {',
59+
' return <div>{this.context.foo}</div>;',
60+
' }',
61+
'}'
62+
].join('\n'),
63+
parserOptions: parserOptions,
64+
options: [{
65+
ignorePureComponents: true
66+
}]
4167
}, {
4268
// Has a lifecyle method
4369
code: [
@@ -259,6 +285,35 @@ ruleTester.run('prefer-stateless-function', rule, {
259285
errors: [{
260286
message: 'Component should be written as a pure function'
261287
}]
288+
}, {
289+
// Only extend PureComponent without use of props or context
290+
code: [
291+
'class Foo extends React.PureComponent {',
292+
' render() {',
293+
' return <div>foo</div>;',
294+
' }',
295+
'}'
296+
].join('\n'),
297+
parserOptions: parserOptions,
298+
options: [{
299+
ignorePureComponents: true
300+
}],
301+
errors: [{
302+
message: 'Component should be written as a pure function'
303+
}]
304+
}, {
305+
// Extends from PureComponent but no ignorePureComponents option
306+
code: [
307+
'class Foo extends React.PureComponent {',
308+
' render() {',
309+
' return <div>{this.props.foo}</div>;',
310+
' }',
311+
'}'
312+
].join('\n'),
313+
parserOptions: parserOptions,
314+
errors: [{
315+
message: 'Component should be written as a pure function'
316+
}]
262317
}, {
263318
// Has only displayName (method) and render
264319
code: [

0 commit comments

Comments
 (0)