|
- /**
- * @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');
- const docsUrl = require('../util/docsUrl');
-
- // 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);
- const type = node.type;
-
- if (type === 'Identifier') {
- return node.name;
- } else if (type === 'Literal') {
- return String(node.value);
- } else if (type === 'TemplateLiteral' && node.expressions.length === 0) {
- return node.quasis[0].value.raw;
- }
- 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,
- url: docsUrl('no-unused-state')
- },
- 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`, `prevState` or `nextState`.
- 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);
-
- const isPrevStateReference =
- node.type === 'Identifier' &&
- node.name === 'prevState';
-
- const isNextStateReference =
- node.type === 'Identifier' &&
- node.name === 'nextState';
-
- return isDirectStateReference || isAliasedStateReference || isPrevStateReference || isNextStateReference;
- }
-
- // 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) {
- const key = prop.key;
-
- if (
- prop.type === 'Property' &&
- (key.type === 'Literal' ||
- (key.type === 'TemplateLiteral' && key.expressions.length === 0) ||
- (prop.computed === false && key.type === 'Identifier')) &&
- 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' || prop.type === 'RestElement') &&
- 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);
- }
-
- if (
- !node.static &&
- node.value &&
- node.value.type === 'ArrowFunctionExpression'
- ) {
- // Create a new set for this.state aliases local to this method.
- classInfo.aliases = new Set();
- }
- },
-
- 'ClassProperty:exit'(node) {
- if (
- classInfo &&
- !node.static &&
- node.value &&
- node.value.type === 'ArrowFunctionExpression'
- ) {
- // Forget our set of local aliases.
- classInfo.aliases = null;
- }
- },
-
- 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);
- // If we see a `this.state` access in a CallExpression, give up.
- } else if (isStateReference(node) && node.parent.type === 'CallExpression') {
- classInfo = null;
- }
- },
-
- JSXSpreadAttribute(node) {
- if (classInfo && isStateReference(node.argument)) {
- classInfo = null;
- }
- },
-
- 'ExperimentalSpreadProperty, SpreadElement'(node) {
- if (classInfo && isStateReference(node.argument)) {
- classInfo = null;
- }
- }
- };
- })
- };
|