259 lines
6.6 KiB
JavaScript
259 lines
6.6 KiB
JavaScript
|
/**
|
||
|
* @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}}"',
|
||
|
};
|
||
|
|
||
|
/** @type {import('eslint').Rule.RuleModule} */
|
||
|
module.exports = {
|
||
|
meta: {
|
||
|
docs: {
|
||
|
description: 'Disallow declaring unused methods of component class',
|
||
|
category: 'Best Practices',
|
||
|
recommended: false,
|
||
|
url: docsUrl('no-unused-class-component-methods'),
|
||
|
},
|
||
|
messages,
|
||
|
schema: [],
|
||
|
},
|
||
|
|
||
|
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
|
||
|
.filter((prop) => prop.type === 'Property' && isKeyLiteralLike(prop, prop.key))
|
||
|
.forEach((prop) => {
|
||
|
addUsedProperty('key' in prop ? prop.key : undefined);
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
};
|
||
|
}),
|
||
|
};
|