WIP - add extractor, generate snippet_data
This commit is contained in:
432
node_modules/eslint-plugin-react/lib/rules/no-unused-state.js
generated
vendored
Executable file
432
node_modules/eslint-plugin-react/lib/rules/no-unused-state.js
generated
vendored
Executable file
@ -0,0 +1,432 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
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 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) {
|
||||
return (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
isThisExpression(node.callee.object) &&
|
||||
getName(node.callee.property) === 'setState'
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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) {
|
||||
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,
|
||||
message: `Unused state field: '${name}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ClassDeclaration(node) {
|
||||
if (utils.isES6Component(node)) {
|
||||
classInfo = getInitialClassInfo();
|
||||
}
|
||||
},
|
||||
|
||||
ObjectExpression(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;
|
||||
}
|
||||
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 (
|
||||
isSetStateCall(node) &&
|
||||
node.arguments.length > 0 &&
|
||||
node.arguments[0].type === 'ObjectExpression'
|
||||
) {
|
||||
addStateFields(node.arguments[0]);
|
||||
} else if (
|
||||
isSetStateCall(node) &&
|
||||
node.arguments.length > 0 &&
|
||||
node.arguments[0].type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
if (node.arguments[0].body.type === 'ObjectExpression') {
|
||||
addStateFields(node.arguments[0].body);
|
||||
}
|
||||
if (node.arguments[0].params.length > 0 && classInfo.aliases) {
|
||||
const firstParam = node.arguments[0].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.
|
||||
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': function (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': function () {
|
||||
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': function (node) {
|
||||
if (classInfo && isStateReference(node.argument)) {
|
||||
classInfo = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
Reference in New Issue
Block a user