Skip to content

Commit

Permalink
Implement React.createClass support and use Component util
Browse files Browse the repository at this point in the history
  • Loading branch information
wbinnssmith committed Mar 13, 2017
1 parent 4ddb0ff commit 17f9566
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 38 deletions.
115 changes: 77 additions & 38 deletions lib/rules/no-unused-state.js
Expand Up @@ -9,6 +9,8 @@

'use strict';

var Components = require('../util/Components');

// Descend through all wrapping TypeCastExpressions and return the expression
// that was cast.
function uncast(node) {
Expand All @@ -31,19 +33,27 @@ function getName(node) {
return null;
}

function isMethodDefinitionWithName(node, name, isStatic) {
isStatic = isStatic || false;
return (
node.type === 'MethodDefinition' &&
node.static === isStatic &&
getName(node.key) === name
);
}

function isThisExpression(node) {
return uncast(node).type === 'ThisExpression';
}

function getInitialClassInfo() {
return {
// Set of nodes where state fields were defined.
stateFields: [],

// Set of names of state fields that we've seen used.
usedStateFields: [],

// Names of local variables that may be pointing to this.state. To
// track this properly, we would need to keep track of all locals,
// shadowing, assignments, etc. To keep things simple, we only
// maintain one set of aliases per method and accept that it will
// produce some false negatives.
aliases: null
};
}

module.exports = {
meta: {
docs: {
Expand All @@ -54,7 +64,7 @@ module.exports = {
schema: []
},

create: function(context) {
create: Components.detect(function(context, components, utils) {
// Non-null when we are inside a React component ClassDeclaration and we have
// not yet encountered any use of this.state which we have chosen not to
// analyze. If we encounter any such usage (like this.state being spread as
Expand Down Expand Up @@ -142,42 +152,45 @@ module.exports = {
}
}

function reportUnusedFields() {
// Report all unused state fields.
classInfo.stateFields.forEach(function(node) {
var name = getName(node.key);
if (classInfo.usedStateFields.indexOf(name) < 0) {
context.report(node, 'Unused state field: \'' + name + '\'');
}
});
}

return {
ClassDeclaration: function(node) {
// Simple heuristic for determining whether we're in a React component.
var isReactComponent = node.body.body.some(function(child) {
return isMethodDefinitionWithName(child, 'render');
});

if (isReactComponent) {
classInfo = {
// Set of nodes where state fields were defined.
stateFields: [],

// Set of names of state fields that we've seen used.
usedStateFields: [],

// Names of local variables that may be pointing to this.state. To
// track this properly, we would need to keep track of all locals,
// shadowing, assignments, etc. To keep things simple, we only
// maintain one set of aliases per method and accept that it will
// produce some false negatives.
aliases: null
};
if (utils.isES6Component(node)) {
classInfo = getInitialClassInfo();
}
},

ObjectExpression: function(node) {
if (utils.isES5Component(node)) {
classInfo = getInitialClassInfo();
}
},

'ObjectExpression:exit': function(node) {
if (!classInfo) {
return;
}

if (utils.isES5Component(node)) {
reportUnusedFields();
classInfo = null;
}
},

'ClassDeclaration:exit': function() {
if (!classInfo) {
return;
}
// Report all unused state fields.
classInfo.stateFields.forEach(function(node) {
var name = getName(node.key);
if (classInfo.usedStateFields.indexOf(name) < 0) {
context.report(node, 'Unused state field: \'' + name + '\'');
}
});
reportUnusedFields();
classInfo = null;
},

Expand Down Expand Up @@ -230,6 +243,32 @@ module.exports = {
classInfo.aliases = null;
},

FunctionExpression: function(node) {
if (!classInfo) {
return;
}

var parent = node.parent;
if (!utils.isES5Component(parent.parent)) {
return;
}

if (parent.key.name === 'getInitialState') {
var body = node.body.body;
var lastBodyNode = body[body.length - 1];

if (
lastBodyNode.type === 'ReturnStatement' &&
lastBodyNode.argument.type === 'ObjectExpression'
) {
addStateFields(lastBodyNode.argument);
}
} else {
// Create a new set for this.state aliases local to this method.
classInfo.aliases = [];
}
},

AssignmentExpression: function(node) {
if (!classInfo) {
return;
Expand Down Expand Up @@ -295,5 +334,5 @@ module.exports = {
}
}
};
}
})
};
101 changes: 101 additions & 0 deletions tests/lib/rules/no-unused-state.js
Expand Up @@ -29,6 +29,68 @@ function getErrorMessages(unusedFields) {

eslintTester.run('no-unused-state', rule, {
valid: [
[
'function StatelessFnUnaffectedTest(props) {',
' return <SomeComponent foo={props.foo} />;',
'};'
].join('\n'),
[
'var NoStateTest = React.createClass({',
' render: function() {',
' return <SomeComponent />;',
' }',
'});'
].join('\n'),
[
'var NoStateMethodTest = React.createClass({',
' render() {',
' return <SomeComponent />;',
' }',
'});'
].join('\n'),
[
'var GetInitialStateTest = React.createClass({',
' getInitialState: function() {',
' return { foo: 0 };',
' },',
' render: function() {',
' return <SomeComponent foo={this.state.foo} />;',
' }',
'});'
].join('\n'),
[
'var GetInitialStateMethodTest = React.createClass({',
' getInitialState() {',
' return { foo: 0 };',
' },',
' render() {',
' return <SomeComponent foo={this.state.foo} />;',
' }',
'});'
].join('\n'),
[
'var SetStateTest = React.createClass({',
' onFooChange(newFoo) {',
' this.setState({ foo: newFoo });',
' },',
' render() {',
' return <SomeComponent foo={this.state.foo} />;',
' }',
'});'
].join('\n'),
[
'var MultipleSetState = React.createClass({',
' getInitialState() {',
' return { foo: 0 };',
' },',
' update() {',
' this.setState({foo: 1});',
' },',
' render() {',
' return <SomeComponent onClick={this.update} foo={this.state.foo} />;',
' }',
'});'
].join('\n'),
[
'class NoStateTest extends React.Component {',
' render() {',
Expand Down Expand Up @@ -262,6 +324,45 @@ eslintTester.run('no-unused-state', rule, {
],

invalid: [
{
code: [
'var UnusedGetInitialStateTest = React.createClass({',
' getInitialState: function() {',
' return { foo: 0 };',
' },',
' render: function() {',
' return <SomeComponent />;',
' }',
'})'
].join('\n'),
errors: getErrorMessages(['foo'])
},
{
code: [
'var UnusedGetInitialStateMethodTest = React.createClass({',
' getInitialState() {',
' return { foo: 0 };',
' },',
' render() {',
' return <SomeComponent />;',
' }',
'})'
].join('\n'),
errors: getErrorMessages(['foo'])
},
{
code: [
'var UnusedSetStateTest = React.createClass({',
' onFooChange(newFoo) {',
' this.setState({ foo: newFoo });',
' },',
' render() {',
' return <SomeComponent />;',
' }',
'});'
].join('\n'),
errors: getErrorMessages(['foo'])
},
{
code: [
'class UnusedCtorStateTest extends React.Component {',
Expand Down

0 comments on commit 17f9566

Please sign in to comment.