Skip to content

Commit fdadf8f

Browse files
Evgueni Navernioukyannickcr
authored andcommitted
Add require-optimization rule (fixes #240)
2 parents ade9e77 + 94fa138 commit fdadf8f

File tree

5 files changed

+351
-1
lines changed

5 files changed

+351
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ The plugin has a [recommended configuration](#user-content-recommended-configura
9292
* [prop-types](docs/rules/prop-types.md): Prevent missing props validation in a React component definition
9393
* [react-in-jsx-scope](docs/rules/react-in-jsx-scope.md): Prevent missing `React` when using JSX
9494
* [require-extension](docs/rules/require-extension.md): Restrict file extensions that may be required
95+
* [require-optimization](docs/rules/require-optimization.md): Enforce React components to have a shouldComponentUpdate method
9596
* [require-render-return](docs/rules/require-render-return.md): Enforce ES5 or ES6 class for returning value in render function
9697
* [self-closing-comp](docs/rules/self-closing-comp.md): Prevent extra closing tags for components without children
9798
* [sort-comp](docs/rules/sort-comp.md): Enforce component methods order

docs/rules/require-optimization.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Enforce React components to have a shouldComponentUpdate method (require-optimization)
2+
3+
This rule prevents you from creating React components without declaring a `shouldComponentUpdate` method.
4+
5+
## Rule Details
6+
7+
The following patterns are considered warnings:
8+
9+
```js
10+
class YourComponent extends React.Component {
11+
12+
}
13+
```
14+
15+
```js
16+
React.createClass({
17+
});
18+
```
19+
20+
The following patterns are not considered warnings:
21+
22+
```js
23+
class YourComponent extends React.Component {
24+
shouldComponentUpdate () {
25+
return false;
26+
}
27+
}
28+
```
29+
30+
```js
31+
React.createClass({
32+
shouldComponentUpdate: function () {
33+
return false;
34+
}
35+
});
36+
```
37+
38+
```js
39+
React.createClass({
40+
mixins: [PureRenderMixin]
41+
});
42+
```
43+
44+
```js
45+
@reactMixin.decorate(PureRenderMixin)
46+
React.createClass({
47+
48+
});
49+
```
50+
51+
## Rule Options
52+
53+
```js
54+
...
55+
"require-optimization": [<enabled>]
56+
...
57+
```
58+
59+
### Example
60+
61+
```js
62+
...
63+
"require-optimization": 2
64+
...
65+
```

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ module.exports = {
4646
'require-render-return': require('./lib/rules/require-render-return'),
4747
'jsx-first-prop-new-line': require('./lib/rules/jsx-first-prop-new-line'),
4848
'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'),
49-
'jsx-filename-extension': require('./lib/rules/jsx-filename-extension')
49+
'jsx-filename-extension': require('./lib/rules/jsx-filename-extension'),
50+
'require-optimization': require('./lib/rules/require-optimization')
5051
},
5152
configs: {
5253
recommended: {

lib/rules/require-optimization.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* @fileoverview Enforce React components to have a shouldComponentUpdate method
3+
* @author Evgueni Naverniouk
4+
*/
5+
'use strict';
6+
7+
var Components = require('../util/Components');
8+
9+
module.exports = Components.detect(function (context, components) {
10+
var MISSING_MESSAGE = 'Component is not optimized. Please add a shouldComponentUpdate method.';
11+
12+
/**
13+
* Checks to see if our component is decorated by PureRenderMixin via reactMixin
14+
* @param {ASTNode} node The AST node being checked.
15+
* @returns {Boolean} True if node is decorated with a PureRenderMixin, false if not.
16+
*/
17+
var hasPureRenderDecorator = function (node) {
18+
if (node.decorators && node.decorators.length) {
19+
for (var i = 0, l = node.decorators.length; i < l; i++) {
20+
if (
21+
node.decorators[i].expression &&
22+
node.decorators[i].expression.callee &&
23+
node.decorators[i].expression.callee.object &&
24+
node.decorators[i].expression.callee.object.name === 'reactMixin' &&
25+
node.decorators[i].expression.callee.property &&
26+
node.decorators[i].expression.callee.property.name === 'decorate' &&
27+
node.decorators[i].expression.arguments &&
28+
node.decorators[i].expression.arguments.length &&
29+
node.decorators[i].expression.arguments[0].name === 'PureRenderMixin'
30+
) {
31+
return true;
32+
}
33+
}
34+
}
35+
36+
return false;
37+
};
38+
39+
/**
40+
* Checks if we are declaring a shouldComponentUpdate method
41+
* @param {ASTNode} node The AST node being checked.
42+
* @returns {Boolean} True if we are declaring a shouldComponentUpdate method, false if not.
43+
*/
44+
var isSCUDeclarеd = function (node) {
45+
return Boolean(
46+
node &&
47+
node.name === 'shouldComponentUpdate'
48+
);
49+
};
50+
51+
/**
52+
* Checks if we are declaring a PureRenderMixin mixin
53+
* @param {ASTNode} node The AST node being checked.
54+
* @returns {Boolean} True if we are declaring a PureRenderMixin method, false if not.
55+
*/
56+
var isPureRenderDeclared = function (node) {
57+
var hasPR = false;
58+
if (node.value && node.value.elements) {
59+
for (var i = 0, l = node.value.elements.length; i < l; i++) {
60+
if (node.value.elements[i].name === 'PureRenderMixin') {
61+
hasPR = true;
62+
break;
63+
}
64+
}
65+
}
66+
67+
return Boolean(
68+
node &&
69+
node.key.name === 'mixins' &&
70+
hasPR
71+
);
72+
};
73+
74+
/**
75+
* Mark shouldComponentUpdate as declared
76+
* @param {ASTNode} node The AST node being checked.
77+
*/
78+
var markSCUAsDeclared = function (node) {
79+
components.set(node, {
80+
hasSCU: true
81+
});
82+
};
83+
84+
/**
85+
* Reports missing optimization for a given component
86+
* @param {Object} component The component to process
87+
*/
88+
var reportMissingOptimization = function (component) {
89+
context.report({
90+
node: component.node,
91+
message: MISSING_MESSAGE,
92+
data: {
93+
component: component.name
94+
}
95+
});
96+
};
97+
98+
return {
99+
ArrowFunctionExpression: function (node) {
100+
// Stateless Functional Components cannot be optimized (yet)
101+
markSCUAsDeclared(node);
102+
},
103+
104+
ClassDeclaration: function (node) {
105+
if (!hasPureRenderDecorator(node)) {
106+
return;
107+
}
108+
markSCUAsDeclared(node);
109+
},
110+
111+
FunctionExpression: function (node) {
112+
// Stateless Functional Components cannot be optimized (yet)
113+
markSCUAsDeclared(node);
114+
},
115+
116+
MethodDefinition: function (node) {
117+
if (!isSCUDeclarеd(node.key)) {
118+
return;
119+
}
120+
markSCUAsDeclared(node);
121+
},
122+
123+
ObjectExpression: function (node) {
124+
// Search for the shouldComponentUpdate declaration
125+
for (var i = 0, l = node.properties.length; i < l; i++) {
126+
if (
127+
!node.properties[i].key || (
128+
!isSCUDeclarеd(node.properties[i].key) &&
129+
!isPureRenderDeclared(node.properties[i])
130+
)
131+
) {
132+
continue;
133+
}
134+
markSCUAsDeclared(node);
135+
}
136+
},
137+
138+
'Program:exit': function () {
139+
var list = components.list();
140+
141+
// Report missing shouldComponentUpdate for all components
142+
for (var component in list) {
143+
if (!list.hasOwnProperty(component) || list[component].hasSCU) {
144+
continue;
145+
}
146+
reportMissingOptimization(list[component]);
147+
}
148+
}
149+
};
150+
});
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* @fileoverview Enforce React components to have a shouldComponentUpdate method
3+
* @author Evgueni Naverniouk
4+
*/
5+
'use strict';
6+
7+
var rule = require('../../../lib/rules/require-optimization');
8+
var RuleTester = require('eslint').RuleTester;
9+
10+
var parserOptions = {
11+
ecmaVersion: 6,
12+
sourceType: 'module'
13+
};
14+
15+
var MESSAGE = 'Component is not optimized. Please add a shouldComponentUpdate method.';
16+
17+
var ruleTester = new RuleTester();
18+
ruleTester.run('react-require-optimization', rule, {
19+
valid: [{
20+
code: [
21+
'class A {}'
22+
].join('\n'),
23+
parserOptions: parserOptions
24+
}, {
25+
code: [
26+
'import React from "react";' +
27+
'class YourComponent extends React.Component {' +
28+
'shouldComponentUpdate () {}' +
29+
'}'
30+
].join('\n'),
31+
parserOptions: parserOptions
32+
}, {
33+
code: [
34+
'import React, {Component} from "react";' +
35+
'class YourComponent extends Component {' +
36+
'shouldComponentUpdate () {}' +
37+
'}'
38+
].join('\n'),
39+
parserOptions: parserOptions
40+
}, {
41+
code: [
42+
'import React from "react";' +
43+
'React.createClass({' +
44+
'shouldComponentUpdate: function () {}' +
45+
'})'
46+
].join('\n'),
47+
parserOptions: parserOptions
48+
}, {
49+
code: [
50+
'import React from "react";' +
51+
'React.createClass({' +
52+
'mixins: [PureRenderMixin]' +
53+
'})'
54+
].join('\n'),
55+
parserOptions: parserOptions
56+
}, {
57+
code: [
58+
'@reactMixin.decorate(PureRenderMixin)',
59+
'class DecoratedComponent extends Component {' +
60+
'}'
61+
].join('\n'),
62+
parser: 'babel-eslint',
63+
parserOptions: parserOptions
64+
}, {
65+
code: [
66+
'const FunctionalComponent = function (props) {' +
67+
'return <div />;' +
68+
'}'
69+
].join('\n'),
70+
parser: 'babel-eslint',
71+
parserOptions: parserOptions
72+
}, {
73+
code: [
74+
'const FunctionalComponent = (props) => {' +
75+
'return <div />;' +
76+
'}'
77+
].join('\n'),
78+
parser: 'babel-eslint',
79+
parserOptions: parserOptions
80+
}],
81+
82+
invalid: [{
83+
code: [
84+
'import React from "react";' +
85+
'class YourComponent extends React.Component {}'
86+
].join('\n'),
87+
errors: [{
88+
message: MESSAGE
89+
}],
90+
parserOptions: parserOptions
91+
}, {
92+
code: [
93+
'import React, {Component} from "react";' +
94+
'class YourComponent extends Component {}'
95+
].join('\n'),
96+
errors: [{
97+
message: MESSAGE
98+
}],
99+
parserOptions: parserOptions
100+
}, {
101+
code: [
102+
'import React from "react";' +
103+
'React.createClass({' +
104+
'})'
105+
].join('\n'),
106+
errors: [{
107+
message: MESSAGE
108+
}],
109+
parserOptions: parserOptions
110+
}, {
111+
code: [
112+
'import React from "react";' +
113+
'React.createClass({' +
114+
'mixins: [RandomMixin]' +
115+
'})'
116+
].join('\n'),
117+
errors: [{
118+
message: MESSAGE
119+
}],
120+
parserOptions: parserOptions
121+
}, {
122+
code: [
123+
'@reactMixin.decorate(SomeOtherMixin)',
124+
'class DecoratedComponent extends Component {' +
125+
'}'
126+
].join('\n'),
127+
errors: [{
128+
message: MESSAGE
129+
}],
130+
parser: 'babel-eslint',
131+
parserOptions: parserOptions
132+
}]
133+
});

0 commit comments

Comments
 (0)