Skip to content

Commit 547cb5f

Browse files
author
Evgueni Naverniouk
committed
Fixes #240. Adds new require-optimization rule.
1 parent 750f979 commit 547cb5f

File tree

5 files changed

+325
-1
lines changed

5 files changed

+325
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ The plugin has a [recommended configuration](#user-content-recommended-configura
9191
* [prop-types](docs/rules/prop-types.md): Prevent missing props validation in a React component definition
9292
* [react-in-jsx-scope](docs/rules/react-in-jsx-scope.md): Prevent missing `React` when using JSX
9393
* [require-extension](docs/rules/require-extension.md): Restrict file extensions that may be required
94+
* [require-optimization](docs/rules/require-optimization.md): Enforce React components to have a shouldComponentUpdate method
9495
* [require-render-return](docs/rules/require-render-return.md): Enforce ES5 or ES6 class for returning value in render function
9596
* [self-closing-comp](docs/rules/self-closing-comp.md): Prevent extra closing tags for components without children
9697
* [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
@@ -44,7 +44,8 @@ module.exports = {
4444
'prefer-stateless-function': require('./lib/rules/prefer-stateless-function'),
4545
'require-render-return': require('./lib/rules/require-render-return'),
4646
'jsx-first-prop-new-line': require('./lib/rules/jsx-first-prop-new-line'),
47-
'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank')
47+
'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'),
48+
'require-optimization': require('./lib/rules/require-optimization')
4849
},
4950
configs: {
5051
recommended: {

lib/rules/require-optimization.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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+
ClassDeclaration: function (node) {
100+
if (!hasPureRenderDecorator(node)) {
101+
return;
102+
}
103+
markSCUAsDeclared(node);
104+
},
105+
106+
MethodDefinition: function (node) {
107+
if (!isSCUDeclarеd(node.key)) {
108+
return;
109+
}
110+
markSCUAsDeclared(node);
111+
},
112+
113+
ObjectExpression: function (node) {
114+
// Search for the shouldComponentUpdate declaration
115+
for (var i = 0, l = node.properties.length; i < l; i++) {
116+
if (
117+
!node.properties[i].key || (
118+
!isSCUDeclarеd(node.properties[i].key) &&
119+
!isPureRenderDeclared(node.properties[i])
120+
)
121+
) {
122+
continue;
123+
}
124+
markSCUAsDeclared(node);
125+
}
126+
},
127+
128+
'Program:exit': function () {
129+
var list = components.list();
130+
131+
// Report missing shouldComponentUpdate for all components
132+
for (var component in list) {
133+
if (!list.hasOwnProperty(component) || list[component].hasSCU) {
134+
continue;
135+
}
136+
reportMissingOptimization(list[component]);
137+
}
138+
}
139+
};
140+
});
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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+
66+
invalid: [{
67+
code: [
68+
'import React from "react";' +
69+
'class YourComponent extends React.Component {}'
70+
].join('\n'),
71+
errors: [{
72+
message: MESSAGE
73+
}],
74+
parserOptions: parserOptions
75+
}, {
76+
code: [
77+
'import React, {Component} from "react";' +
78+
'class YourComponent extends Component {}'
79+
].join('\n'),
80+
errors: [{
81+
message: MESSAGE
82+
}],
83+
parserOptions: parserOptions
84+
}, {
85+
code: [
86+
'import React from "react";' +
87+
'React.createClass({' +
88+
'})'
89+
].join('\n'),
90+
errors: [{
91+
message: MESSAGE
92+
}],
93+
parserOptions: parserOptions
94+
}, {
95+
code: [
96+
'import React from "react";' +
97+
'React.createClass({' +
98+
'mixins: [RandomMixin]' +
99+
'})'
100+
].join('\n'),
101+
errors: [{
102+
message: MESSAGE
103+
}],
104+
parserOptions: parserOptions
105+
}, {
106+
code: [
107+
'@reactMixin.decorate(SomeOtherMixin)',
108+
'class DecoratedComponent extends Component {' +
109+
'}'
110+
].join('\n'),
111+
errors: [{
112+
message: MESSAGE
113+
}],
114+
parser: 'babel-eslint',
115+
parserOptions: parserOptions
116+
}]
117+
});

0 commit comments

Comments
 (0)