238 lines
5.1 KiB
JavaScript
238 lines
5.1 KiB
JavaScript
|
'use strict';
|
||
|
const getScopes = require('./utils/get-scopes.js');
|
||
|
|
||
|
const MESSAGE_ID = 'no-unused-properties';
|
||
|
const messages = {
|
||
|
[MESSAGE_ID]: 'Property `{{name}}` is defined but never used.',
|
||
|
};
|
||
|
|
||
|
const getDeclaratorOrPropertyValue = declaratorOrProperty =>
|
||
|
declaratorOrProperty.init
|
||
|
|| declaratorOrProperty.value;
|
||
|
|
||
|
const isMemberExpressionCall = memberExpression =>
|
||
|
memberExpression.parent.type === 'CallExpression'
|
||
|
&& memberExpression.parent.callee === memberExpression;
|
||
|
|
||
|
const isMemberExpressionAssignment = memberExpression =>
|
||
|
memberExpression.parent.type === 'AssignmentExpression';
|
||
|
|
||
|
const isMemberExpressionComputedBeyondPrediction = memberExpression =>
|
||
|
memberExpression.computed
|
||
|
&& memberExpression.property.type !== 'Literal';
|
||
|
|
||
|
const specialProtoPropertyKey = {
|
||
|
type: 'Identifier',
|
||
|
name: '__proto__',
|
||
|
};
|
||
|
|
||
|
const propertyKeysEqual = (keyA, keyB) => {
|
||
|
if (keyA.type === 'Identifier') {
|
||
|
if (keyB.type === 'Identifier') {
|
||
|
return keyA.name === keyB.name;
|
||
|
}
|
||
|
|
||
|
if (keyB.type === 'Literal') {
|
||
|
return keyA.name === keyB.value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (keyA.type === 'Literal') {
|
||
|
if (keyB.type === 'Identifier') {
|
||
|
return keyA.value === keyB.name;
|
||
|
}
|
||
|
|
||
|
if (keyB.type === 'Literal') {
|
||
|
return keyA.value === keyB.value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
};
|
||
|
|
||
|
const objectPatternMatchesObjectExprPropertyKey = (pattern, key) =>
|
||
|
pattern.properties.some(property => {
|
||
|
if (property.type === 'RestElement') {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return propertyKeysEqual(property.key, key);
|
||
|
});
|
||
|
|
||
|
const isLeafDeclaratorOrProperty = declaratorOrProperty => {
|
||
|
const value = getDeclaratorOrPropertyValue(declaratorOrProperty);
|
||
|
|
||
|
if (!value) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if (value.type !== 'ObjectExpression') {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
};
|
||
|
|
||
|
const isUnusedVariable = variable => {
|
||
|
const hasReadReference = variable.references.some(reference => reference.isRead());
|
||
|
return !hasReadReference;
|
||
|
};
|
||
|
|
||
|
/** @param {import('eslint').Rule.RuleContext} context */
|
||
|
const create = context => {
|
||
|
const getPropertyDisplayName = property => {
|
||
|
if (property.key.type === 'Identifier') {
|
||
|
return property.key.name;
|
||
|
}
|
||
|
|
||
|
if (property.key.type === 'Literal') {
|
||
|
return property.key.value;
|
||
|
}
|
||
|
|
||
|
return context.getSourceCode().getText(property.key);
|
||
|
};
|
||
|
|
||
|
const checkProperty = (property, references, path) => {
|
||
|
if (references.length === 0) {
|
||
|
context.report({
|
||
|
node: property,
|
||
|
messageId: MESSAGE_ID,
|
||
|
data: {
|
||
|
name: getPropertyDisplayName(property),
|
||
|
},
|
||
|
});
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
checkObject(property, references, path);
|
||
|
};
|
||
|
|
||
|
const checkProperties = (objectExpression, references, path = []) => {
|
||
|
for (const property of objectExpression.properties) {
|
||
|
const {key} = property;
|
||
|
|
||
|
if (!key) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (propertyKeysEqual(key, specialProtoPropertyKey)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const nextPath = [...path, key];
|
||
|
|
||
|
const nextReferences = references
|
||
|
.map(reference => {
|
||
|
const {parent} = reference.identifier;
|
||
|
|
||
|
if (reference.init) {
|
||
|
if (
|
||
|
parent.type === 'VariableDeclarator'
|
||
|
&& parent.parent.type === 'VariableDeclaration'
|
||
|
&& parent.parent.parent.type === 'ExportNamedDeclaration'
|
||
|
) {
|
||
|
return {identifier: parent};
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (parent.type === 'MemberExpression') {
|
||
|
if (
|
||
|
isMemberExpressionAssignment(parent)
|
||
|
|| isMemberExpressionCall(parent)
|
||
|
|| isMemberExpressionComputedBeyondPrediction(parent)
|
||
|
|| propertyKeysEqual(parent.property, key)
|
||
|
) {
|
||
|
return {identifier: parent};
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
parent.type === 'VariableDeclarator'
|
||
|
&& parent.id.type === 'ObjectPattern'
|
||
|
) {
|
||
|
if (objectPatternMatchesObjectExprPropertyKey(parent.id, key)) {
|
||
|
return {identifier: parent};
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
parent.type === 'AssignmentExpression'
|
||
|
&& parent.left.type === 'ObjectPattern'
|
||
|
) {
|
||
|
if (objectPatternMatchesObjectExprPropertyKey(parent.left, key)) {
|
||
|
return {identifier: parent};
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
return reference;
|
||
|
})
|
||
|
.filter(Boolean);
|
||
|
|
||
|
checkProperty(property, nextReferences, nextPath);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const checkObject = (declaratorOrProperty, references, path) => {
|
||
|
if (isLeafDeclaratorOrProperty(declaratorOrProperty)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const value = getDeclaratorOrPropertyValue(declaratorOrProperty);
|
||
|
|
||
|
checkProperties(value, references, path);
|
||
|
};
|
||
|
|
||
|
const checkVariable = variable => {
|
||
|
if (variable.defs.length !== 1) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (isUnusedVariable(variable)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const [definition] = variable.defs;
|
||
|
|
||
|
checkObject(definition.node, variable.references);
|
||
|
};
|
||
|
|
||
|
const checkVariables = scope => {
|
||
|
for (const variable of scope.variables) {
|
||
|
checkVariable(variable);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return {
|
||
|
'Program:exit'() {
|
||
|
const scopes = getScopes(context.getScope());
|
||
|
for (const scope of scopes) {
|
||
|
if (scope.type === 'global') {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
checkVariables(scope);
|
||
|
}
|
||
|
},
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/** @type {import('eslint').Rule.RuleModule} */
|
||
|
module.exports = {
|
||
|
create,
|
||
|
meta: {
|
||
|
type: 'suggestion',
|
||
|
docs: {
|
||
|
description: 'Disallow unused object properties.',
|
||
|
},
|
||
|
messages,
|
||
|
},
|
||
|
};
|