Skip to content

Commit

Permalink
Merge pull request #1103 from wbinnssmith/wbinnssmith/no-unused-state
Browse files Browse the repository at this point in the history
Add no-unused-state
  • Loading branch information
ljharb committed Jul 21, 2017
2 parents 067882b + ba1dfc1 commit 8bfafe1
Show file tree
Hide file tree
Showing 3 changed files with 786 additions and 1 deletion.
3 changes: 2 additions & 1 deletion index.js
Expand Up @@ -66,7 +66,8 @@ const allRules = {
'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing'),
'no-redundant-should-component-update': require('./lib/rules/no-redundant-should-component-update'),
'boolean-prop-naming': require('./lib/rules/boolean-prop-naming'),
'no-typos': require('./lib/rules/no-typos')
'no-typos': require('./lib/rules/no-typos'),
'no-unused-state': require('./lib/rules/no-unused-state')
};

function filterRules(rules, predicate) {
Expand Down
338 changes: 338 additions & 0 deletions lib/rules/no-unused-state.js
@@ -0,0 +1,338 @@
/**
* @fileoverview Attempts to discover all state fields in a React component and
* warn if any of them are never read.
*
* State field definitions are collected from `this.state = {}` assignments in
* the constructor, objects passed to `this.setState()`, and `state = {}` class
* property assignments.
*/

'use strict';

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

// Descend through all wrapping TypeCastExpressions and return the expression
// that was cast.
function uncast(node) {
while (node.type === 'TypeCastExpression') {
node = node.expression;
}
return node;
}

// Return the name of an identifier or the string value of a literal. Useful
// anywhere that a literal may be used as a key (e.g., member expressions,
// method definitions, ObjectExpression property keys).
function getName(node) {
node = uncast(node);
if (node.type === 'Identifier') {
return node.name;
} else if (node.type === 'Literal') {
return String(node.value);
}
return null;
}

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

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

// Set of names of state fields that we've seen used.
usedStateFields: new Set(),

// 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: {
description: 'Prevent definition of unused state fields',
category: 'Best Practices',
recommended: false
},
schema: []
},

create: Components.detect((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
// JSX attributes), then this is again set to null.
let classInfo = null;

// Returns true if the given node is possibly a reference to `this.state`.
function isStateReference(node) {
node = uncast(node);

const isDirectStateReference =
node.type === 'MemberExpression' &&
isThisExpression(node.object) &&
node.property.name === 'state';

const isAliasedStateReference =
node.type === 'Identifier' &&
classInfo.aliases &&
classInfo.aliases.has(node.name);

return isDirectStateReference || isAliasedStateReference;
}

// Takes an ObjectExpression node and adds all named Property nodes to the
// current set of state fields.
function addStateFields(node) {
for (const prop of node.properties) {
if (prop.type === 'Property' && getName(prop.key) !== null) {
classInfo.stateFields.add(prop);
}
}
}

// Adds the name of the given node as a used state field if the node is an
// Identifier or a Literal. Other node types are ignored.
function addUsedStateField(node) {
const name = getName(node);
if (name) {
classInfo.usedStateFields.add(name);
}
}

// Records used state fields and new aliases for an ObjectPattern which
// destructures `this.state`.
function handleStateDestructuring(node) {
for (const prop of node.properties) {
if (prop.type === 'Property') {
addUsedStateField(prop.key);
} else if (
prop.type === 'ExperimentalRestProperty' &&
classInfo.aliases
) {
classInfo.aliases.add(getName(prop.argument));
}
}
}

// Used to record used state fields and new aliases for both
// AssignmentExpressions and VariableDeclarators.
function handleAssignment(left, right) {
switch (left.type) {
case 'Identifier':
if (isStateReference(right) && classInfo.aliases) {
classInfo.aliases.add(left.name);
}
break;
case 'ObjectPattern':
if (isStateReference(right)) {
handleStateDestructuring(left);
} else if (isThisExpression(right) && classInfo.aliases) {
for (const prop of left.properties) {
if (prop.type === 'Property' && getName(prop.key) === 'state') {
const name = getName(prop.value);
if (name) {
classInfo.aliases.add(name);
} else if (prop.value.type === 'ObjectPattern') {
handleStateDestructuring(prop.value);
}
}
}
}
break;
default:
// pass
}
}

function reportUnusedFields() {
// Report all unused state fields.
for (const node of classInfo.stateFields) {
const name = getName(node.key);
if (!classInfo.usedStateFields.has(name)) {
context.report(node, `Unused state field: '${name}'`);
}
}
}

return {
ClassDeclaration(node) {
if (utils.isES6Component(node)) {
classInfo = getInitialClassInfo();
}
},

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

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

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

'ClassDeclaration:exit'() {
if (!classInfo) {
return;
}
reportUnusedFields();
classInfo = null;
},

CallExpression(node) {
if (!classInfo) {
return;
}
// If we're looking at a `this.setState({})` invocation, record all the
// properties as state fields.
if (
node.callee.type === 'MemberExpression' &&
isThisExpression(node.callee.object) &&
getName(node.callee.property) === 'setState' &&
node.arguments.length > 0 &&
node.arguments[0].type === 'ObjectExpression'
) {
addStateFields(node.arguments[0]);
}
},

ClassProperty(node) {
if (!classInfo) {
return;
}
// If we see state being assigned as a class property using an object
// expression, record all the fields of that object as state fields.
if (
getName(node.key) === 'state' &&
!node.static &&
node.value &&
node.value.type === 'ObjectExpression'
) {
addStateFields(node.value);
}
},

MethodDefinition() {
if (!classInfo) {
return;
}
// Create a new set for this.state aliases local to this method.
classInfo.aliases = new Set();
},

'MethodDefinition:exit'() {
if (!classInfo) {
return;
}
// Forget our set of local aliases.
classInfo.aliases = null;
},

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

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

if (parent.key.name === 'getInitialState') {
const body = node.body.body;
const 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 = new Set();
}
},

AssignmentExpression(node) {
if (!classInfo) {
return;
}
// Check for assignments like `this.state = {}`
if (
node.left.type === 'MemberExpression' &&
isThisExpression(node.left.object) &&
getName(node.left.property) === 'state' &&
node.right.type === 'ObjectExpression'
) {
// Find the nearest function expression containing this assignment.
let fn = node;
while (fn.type !== 'FunctionExpression' && fn.parent) {
fn = fn.parent;
}
// If the nearest containing function is the constructor, then we want
// to record all the assigned properties as state fields.
if (
fn.parent &&
fn.parent.type === 'MethodDefinition' &&
fn.parent.kind === 'constructor'
) {
addStateFields(node.right);
}
} else {
// Check for assignments like `alias = this.state` and record the alias.
handleAssignment(node.left, node.right);
}
},

VariableDeclarator(node) {
if (!classInfo || !node.init) {
return;
}
handleAssignment(node.id, node.init);
},

MemberExpression(node) {
if (!classInfo) {
return;
}
if (isStateReference(node.object)) {
// If we see this.state[foo] access, give up.
if (node.computed && node.property.type !== 'Literal') {
classInfo = null;
return;
}
// Otherwise, record that we saw this property being accessed.
addUsedStateField(node.property);
}
},

JSXSpreadAttribute(node) {
if (classInfo && isStateReference(node.argument)) {
classInfo = null;
}
},

ExperimentalSpreadProperty(node) {
if (classInfo && isStateReference(node.argument)) {
classInfo = null;
}
}
};
})
};

0 comments on commit 8bfafe1

Please sign in to comment.