You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

263 lines
6.6 KiB

2 years ago
/**
* @fileoverview Prevent declaring unused methods and properties of component class
* @author Paweł Nowak, Berton Zhu
*/
'use strict';
const docsUrl = require('../util/docsUrl');
const componentUtil = require('../util/componentUtil');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const LIFECYCLE_METHODS = new Set([
'constructor',
'componentDidCatch',
'componentDidMount',
'componentDidUpdate',
'componentWillMount',
'componentWillReceiveProps',
'componentWillUnmount',
'componentWillUpdate',
'getChildContext',
'getSnapshotBeforeUpdate',
'render',
'shouldComponentUpdate',
'UNSAFE_componentWillMount',
'UNSAFE_componentWillReceiveProps',
'UNSAFE_componentWillUpdate',
]);
const ES6_LIFECYCLE = new Set([
'state',
]);
const ES5_LIFECYCLE = new Set([
'getInitialState',
'getDefaultProps',
'mixins',
]);
function isKeyLiteralLike(node, property) {
return property.type === 'Literal'
|| (property.type === 'TemplateLiteral' && property.expressions.length === 0)
|| (node.computed === false && property.type === 'Identifier');
}
// 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(node, isClass) {
return {
classNode: node,
isClass,
// Set of nodes where properties were defined.
properties: new Set(),
// Set of names of properties that we've seen used.
usedProperties: new Set(),
inStatic: false,
};
}
const messages = {
unused: 'Unused method or property "{{name}}"',
unusedWithClass: 'Unused method or property "{{name}}" of class "{{className}}"',
};
module.exports = {
meta: {
docs: {
description: 'Prevent declaring unused methods of component class',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-unused-class-component-methods'),
},
messages,
schema: [
{
type: 'object',
additionalProperties: false,
},
],
},
create: ((context) => {
let classInfo = null;
// Takes an ObjectExpression node and adds all named Property nodes to the
// current set of properties.
function addProperty(node) {
classInfo.properties.add(node);
}
// Adds the name of the given node as a used property if the node is an
// Identifier or a Literal. Other node types are ignored.
function addUsedProperty(node) {
const name = getName(node);
if (name) {
classInfo.usedProperties.add(name);
}
}
function reportUnusedProperties() {
// Report all unused properties.
for (const node of classInfo.properties) { // eslint-disable-line no-restricted-syntax
const name = getName(node);
if (
!classInfo.usedProperties.has(name)
&& !LIFECYCLE_METHODS.has(name)
&& (classInfo.isClass ? !ES6_LIFECYCLE.has(name) : !ES5_LIFECYCLE.has(name))
) {
const className = (classInfo.classNode.id && classInfo.classNode.id.name) || '';
const messageID = className ? 'unusedWithClass' : 'unused';
report(
context,
messages[messageID],
messageID,
{
node,
data: {
name,
className,
},
}
);
}
}
}
function exitMethod() {
if (!classInfo || !classInfo.inStatic) {
return;
}
classInfo.inStatic = false;
}
return {
ClassDeclaration(node) {
if (componentUtil.isES6Component(node, context)) {
classInfo = getInitialClassInfo(node, true);
}
},
ObjectExpression(node) {
if (componentUtil.isES5Component(node, context)) {
classInfo = getInitialClassInfo(node, false);
}
},
'ClassDeclaration:exit'() {
if (!classInfo) {
return;
}
reportUnusedProperties();
classInfo = null;
},
'ObjectExpression:exit'(node) {
if (!classInfo || classInfo.classNode !== node) {
return;
}
reportUnusedProperties();
classInfo = null;
},
Property(node) {
if (!classInfo || classInfo.classNode !== node.parent) {
return;
}
if (isKeyLiteralLike(node, node.key)) {
addProperty(node.key);
}
},
'ClassProperty, MethodDefinition, PropertyDefinition'(node) {
if (!classInfo) {
return;
}
if (node.static) {
classInfo.inStatic = true;
return;
}
if (isKeyLiteralLike(node, node.key)) {
addProperty(node.key);
}
},
'ClassProperty:exit': exitMethod,
'MethodDefinition:exit': exitMethod,
'PropertyDefinition:exit': exitMethod,
MemberExpression(node) {
if (!classInfo || classInfo.inStatic) {
return;
}
if (isThisExpression(node.object) && isKeyLiteralLike(node, node.property)) {
if (node.parent.type === 'AssignmentExpression' && node.parent.left === node) {
// detect `this.property = xxx`
addProperty(node.property);
} else {
// detect `this.property()`, `x = this.property`, etc.
addUsedProperty(node.property);
}
}
},
VariableDeclarator(node) {
if (!classInfo || classInfo.inStatic) {
return;
}
// detect `{ foo, bar: baz } = this`
if (node.init && isThisExpression(node.init) && node.id.type === 'ObjectPattern') {
node.id.properties.forEach((prop) => {
if (prop.type === 'Property' && isKeyLiteralLike(prop, prop.key)) {
addUsedProperty(prop.key);
}
});
}
},
};
}),
};