Skip to content

Commit

Permalink
Make prefer-stateless-function PureComponent aware
Browse files Browse the repository at this point in the history
  • Loading branch information
teameh committed Aug 21, 2016
1 parent 4be546a commit 6eabed5
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 6 deletions.
20 changes: 16 additions & 4 deletions docs/rules/prefer-stateless-function.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ This rule will check your class based React components for

* 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))
* instance property other than `this.props` and `this.context`
* extension of `React.PureComponent` ()
* presence of `ref` attribute in JSX
* `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)

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

The following pattern is considered warnings:
The following pattern is considered a warning:

```js
var Hello = React.createClass({
Expand All @@ -23,15 +24,15 @@ var Hello = React.createClass({
});
```

The following pattern is not considered warnings:
The following pattern is not considered a warning:

```js
const Foo = function(props) {
return <div>{props.foo}</div>;
};
```

The following pattern is not considered warning in React <15.0.0:
The following pattern is not considered a warning in React <15.0.0:

```js
class Foo extends React.Component {
Expand All @@ -43,3 +44,14 @@ class Foo extends React.Component {
}
}
```

The following pattern is not considered a warning:

```js
class Foo extends React.PureComponent {
render() {
return <div>{this.props.foo}</div>;
}
}
```

55 changes: 53 additions & 2 deletions lib/rules/prefer-stateless-function.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
'use strict';

var Components = require('../util/Components');
var pragmaUtil = require('../util/pragma');
var versionUtil = require('../util/version');

// ------------------------------------------------------------------------------
Expand All @@ -25,6 +26,9 @@ module.exports = {

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

var pragma = pragmaUtil.getFromContext(context);
var pureComponentRegExp = new RegExp('^(' + pragma + '\\.)?PureComponent$');

var sourceCode = context.getSourceCode();

// --------------------------------------------------------------------------
Expand Down Expand Up @@ -64,6 +68,19 @@ module.exports = {
}
}

/**
* Checks to see if our component extends React.PureComponent
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if node extends React.PureComponent, false if not.
*/
var isPureComponent = function (node) {
if (node.superClass) {
return pureComponentRegExp.test(sourceCode.getText(node.superClass));
}

return false;
};

/**
* Checks whether a given array of statements is a single call of `super`.
* @see ESLint no-useless-constructor rule
Expand Down Expand Up @@ -213,6 +230,16 @@ module.exports = {
});
}

/**
* Mark component as pure as declared
* @param {ASTNode} node The AST node being checked.
*/
var markSCUAsDeclared = function (node) {
components.set(node, {
hasSCU: true
});
};

/**
* Mark a setState as used
* @param {ASTNode} node The AST node being checked.
Expand All @@ -223,6 +250,16 @@ module.exports = {
});
}

/**
* Mark a props or context as used
* @param {ASTNode} node The AST node being checked.
*/
function markPropsOrContextAsUsed(node) {
components.set(node, {
usePropsOrContext: true
});
}

/**
* Mark a ref as used
* @param {ASTNode} node The AST node being checked.
Expand All @@ -244,6 +281,13 @@ module.exports = {
}

return {
ClassDeclaration: function (node) {
if (!isPureComponent(node)) {
return;
}
markSCUAsDeclared(node);
},

// Mark `this` destructuring as a usage of `this`
VariableDeclarator: function(node) {
// Ignore destructuring on other than `this`
Expand All @@ -256,6 +300,7 @@ module.exports = {
return name !== 'props' && name !== 'context';
});
if (!useThis) {
markPropsOrContextAsUsed(node);
return;
}
markThisAsUsed(node);
Expand All @@ -264,11 +309,13 @@ module.exports = {
// Mark `this` usage
MemberExpression: function(node) {
// Ignore calls to `this.props` and `this.context`
if (
node.object.type !== 'ThisExpression' ||
if (node.object.type !== 'ThisExpression') {
return;
} else if (
(node.property.name || node.property.value) === 'props' ||
(node.property.name || node.property.value) === 'context'
) {
markPropsOrContextAsUsed(node);
return;
}
markThisAsUsed(node);
Expand Down Expand Up @@ -322,6 +369,10 @@ module.exports = {
continue;
}

if (list[component].hasSCU && list[component].usePropsOrContext) {
continue;
}

context.report({
node: list[component].node,
message: 'Component should be written as a pure function'
Expand Down
33 changes: 33 additions & 0 deletions tests/lib/rules/prefer-stateless-function.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,26 @@ ruleTester.run('prefer-stateless-function', rule, {
// Already a stateless (arrow) function
code: 'const Foo = ({foo}) => <div>{foo}</div>;',
parserOptions: parserOptions
}, {
// Extends from PureComponent and uses props
code: [
'class Foo extends React.PureComponent {',
' render() {',
' return <div>{this.props.foo}</div>;',
' }',
'}'
].join('\n'),
parserOptions: parserOptions
}, {
// Extends from PureComponent and uses context
code: [
'class Foo extends React.PureComponent {',
' render() {',
' return <div>{this.context.foo}</div>;',
' }',
'}'
].join('\n'),
parserOptions: parserOptions
}, {
// Has a lifecyle method
code: [
Expand Down Expand Up @@ -259,6 +279,19 @@ ruleTester.run('prefer-stateless-function', rule, {
errors: [{
message: 'Component should be written as a pure function'
}]
}, {
// Only extend PureComponent without use of props or context
code: [
'class Foo extends React.PureComponent {',
' render() {',
' return <div>foo</div>;',
' }',
'}'
].join('\n'),
parserOptions: parserOptions,
errors: [{
message: 'Component should be written as a pure function'
}]
}, {
// Has only displayName (method) and render
code: [
Expand Down

0 comments on commit 6eabed5

Please sign in to comment.