123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- /**
- * @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');
- const ast = require('../util/ast');
- // 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;
- }
- if (type === 'Literal') {
- return String(node.value);
- }
- if (type === 'TemplateLiteral' && node.expressions.length === 0) {
- return node.quasis[0].value.raw;
- }
- return null;
- }
- function isThisExpression(node) {
- return ast.unwrapTSAsExpression(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
- };
- }
- function isSetStateCall(node) {
- const unwrappedCalleeNode = ast.unwrapTSAsExpression(node.callee);
- return (
- unwrappedCalleeNode.type === 'MemberExpression'
- && isThisExpression(unwrappedCalleeNode.object)
- && getName(unwrappedCalleeNode.property) === 'setState'
- );
- }
- module.exports = {
- meta: {
- docs: {
- description: 'Prevent definition of unused state fields',
- category: 'Best Practices',
- recommended: false,
- url: docsUrl('no-unused-state')
- },
- messages: {
- unusedStateField: 'Unused state field: \'{{name}}\''
- },
- 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;
- function isStateParameterReference(node) {
- const classMethods = [
- 'shouldComponentUpdate',
- 'componentWillUpdate',
- 'UNSAFE_componentWillUpdate',
- 'getSnapshotBeforeUpdate',
- 'componentDidUpdate'
- ];
- let scope = context.getScope();
- while (scope) {
- const parent = scope.block && scope.block.parent;
- if (
- parent
- && parent.type === 'MethodDefinition' && (
- (parent.static && parent.key.name === 'getDerivedStateFromProps')
- || classMethods.indexOf(parent.key.name) !== -1
- )
- && parent.value.type === 'FunctionExpression'
- && parent.value.params[1]
- && parent.value.params[1].name === node.name
- ) {
- return true;
- }
- scope = scope.upper;
- }
- return false;
- }
- // Returns true if the given node is possibly a reference to `this.state` or the state parameter of
- // a lifecycle method.
- 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 || isStateParameterReference(node);
- }
- // Takes an ObjectExpression node and adds all named Property nodes to the
- // current set of state fields.
- function addStateFields(node) {
- node.properties.filter((prop) => (
- prop.type === 'Property'
- && (prop.key.type === 'Literal'
- || (prop.key.type === 'TemplateLiteral' && prop.key.expressions.length === 0)
- || (prop.computed === false && prop.key.type === 'Identifier'))
- && getName(prop.key) !== null
- )).forEach((prop) => {
- 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) {
- const unwrappedRight = ast.unwrapTSAsExpression(right);
- switch (left.type) {
- case 'Identifier':
- if (isStateReference(unwrappedRight) && classInfo.aliases) {
- classInfo.aliases.add(left.name);
- }
- break;
- case 'ObjectPattern':
- if (isStateReference(unwrappedRight)) {
- handleStateDestructuring(left);
- } else if (isThisExpression(unwrappedRight) && 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,
- messageId: 'unusedStateField',
- data: {
- name
- }
- });
- }
- }
- }
- function handleES6ComponentEnter(node) {
- if (utils.isES6Component(node)) {
- classInfo = getInitialClassInfo();
- }
- }
- function handleES6ComponentExit() {
- if (!classInfo) {
- return;
- }
- reportUnusedFields();
- classInfo = null;
- }
- return {
- ClassDeclaration: handleES6ComponentEnter,
- 'ClassDeclaration:exit': handleES6ComponentExit,
- ClassExpression: handleES6ComponentEnter,
- 'ClassExpression:exit': handleES6ComponentExit,
- ObjectExpression(node) {
- if (utils.isES5Component(node)) {
- classInfo = getInitialClassInfo();
- }
- },
- 'ObjectExpression:exit'(node) {
- if (!classInfo) {
- return;
- }
- if (utils.isES5Component(node)) {
- reportUnusedFields();
- classInfo = null;
- }
- },
- CallExpression(node) {
- if (!classInfo) {
- return;
- }
- const unwrappedNode = ast.unwrapTSAsExpression(node);
- const unwrappedArgumentNode = ast.unwrapTSAsExpression(unwrappedNode.arguments[0]);
- // If we're looking at a `this.setState({})` invocation, record all the
- // properties as state fields.
- if (
- isSetStateCall(unwrappedNode)
- && unwrappedNode.arguments.length > 0
- && unwrappedArgumentNode.type === 'ObjectExpression'
- ) {
- addStateFields(unwrappedArgumentNode);
- } else if (
- isSetStateCall(unwrappedNode)
- && unwrappedNode.arguments.length > 0
- && unwrappedArgumentNode.type === 'ArrowFunctionExpression'
- ) {
- const unwrappedBodyNode = ast.unwrapTSAsExpression(unwrappedArgumentNode.body);
- if (unwrappedBodyNode.type === 'ObjectExpression') {
- addStateFields(unwrappedBodyNode);
- }
- if (unwrappedArgumentNode.params.length > 0 && classInfo.aliases) {
- const firstParam = unwrappedArgumentNode.params[0];
- if (firstParam.type === 'ObjectPattern') {
- handleStateDestructuring(firstParam);
- } else {
- classInfo.aliases.add(getName(firstParam));
- }
- }
- }
- },
- 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.
- const unwrappedValueNode = ast.unwrapTSAsExpression(node.value);
- if (
- getName(node.key) === 'state'
- && !node.static
- && unwrappedValueNode
- && unwrappedValueNode.type === 'ObjectExpression'
- ) {
- addStateFields(unwrappedValueNode);
- }
- if (
- !node.static
- && unwrappedValueNode
- && unwrappedValueNode.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;
- }
- const unwrappedLeft = ast.unwrapTSAsExpression(node.left);
- const unwrappedRight = ast.unwrapTSAsExpression(node.right);
- // Check for assignments like `this.state = {}`
- if (
- unwrappedLeft.type === 'MemberExpression'
- && isThisExpression(unwrappedLeft.object)
- && getName(unwrappedLeft.property) === 'state'
- && unwrappedRight.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(unwrappedRight);
- }
- } else {
- // Check for assignments like `alias = this.state` and record the alias.
- handleAssignment(unwrappedLeft, unwrappedRight);
- }
- },
- VariableDeclarator(node) {
- if (!classInfo || !node.init) {
- return;
- }
- handleAssignment(node.id, node.init);
- },
- 'MemberExpression, OptionalMemberExpression'(node) {
- if (!classInfo) {
- return;
- }
- if (isStateReference(ast.unwrapTSAsExpression(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;
- }
- }
- };
- })
- };
|