forked from daren.hsu/line_push
246 lines
5.1 KiB
JavaScript
246 lines
5.1 KiB
JavaScript
'use strict';
|
|
const getDocumentationUrl = require('./utils/get-documentation-url');
|
|
|
|
const getDeclaratorOrPropertyValue = declaratorOrProperty => {
|
|
return declaratorOrProperty.init || declaratorOrProperty.value;
|
|
};
|
|
|
|
const isMemberExpressionCall = memberExpression => {
|
|
return (
|
|
memberExpression.parent &&
|
|
memberExpression.parent.type === 'CallExpression' &&
|
|
memberExpression.parent.callee === memberExpression
|
|
);
|
|
};
|
|
|
|
const isMemberExpressionAssignment = memberExpression => {
|
|
return (
|
|
memberExpression.parent &&
|
|
memberExpression.parent.type === 'AssignmentExpression'
|
|
);
|
|
};
|
|
|
|
const isMemberExpressionComputedBeyondPrediction = memberExpression => {
|
|
return (
|
|
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) => {
|
|
return pattern.properties.some(property => {
|
|
if (property.type === 'ExperimentalRestProperty') {
|
|
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;
|
|
};
|
|
|
|
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.getSource(property.key);
|
|
};
|
|
|
|
const checkProperty = (property, references, path) => {
|
|
if (references.length === 0) {
|
|
context.report({
|
|
node: property,
|
|
message: 'Property `{{name}}` is defined but never used.',
|
|
data: {
|
|
name: getPropertyDisplayName(property)
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
checkObject(property, references, path);
|
|
};
|
|
|
|
const checkProperties = (objectExpression, references, path = []) => {
|
|
objectExpression.properties.forEach(property => {
|
|
const {key} = property;
|
|
|
|
if (!key) {
|
|
return;
|
|
}
|
|
|
|
if (propertyKeysEqual(key, specialProtoPropertyKey)) {
|
|
return;
|
|
}
|
|
|
|
const nextPath = path.concat(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 undefined;
|
|
}
|
|
|
|
if (parent.type === 'MemberExpression') {
|
|
if (
|
|
isMemberExpressionAssignment(parent) ||
|
|
isMemberExpressionCall(parent) ||
|
|
isMemberExpressionComputedBeyondPrediction(parent) ||
|
|
propertyKeysEqual(parent.property, key)
|
|
) {
|
|
return {identifier: parent};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
if (
|
|
parent.type === 'VariableDeclarator' &&
|
|
parent.id.type === 'ObjectPattern'
|
|
) {
|
|
if (objectPatternMatchesObjectExprPropertyKey(parent.id, key)) {
|
|
return {identifier: parent};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
if (
|
|
parent.type === 'AssignmentExpression' &&
|
|
parent.left.type === 'ObjectPattern'
|
|
) {
|
|
if (objectPatternMatchesObjectExprPropertyKey(parent.left, key)) {
|
|
return {identifier: parent};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
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 => {
|
|
scope.variables.forEach(variable => checkVariable(variable));
|
|
};
|
|
|
|
const checkChildScopes = scope => {
|
|
scope.childScopes.forEach(scope => checkScope(scope));
|
|
};
|
|
|
|
const checkScope = scope => {
|
|
if (scope.type === 'global') {
|
|
return checkChildScopes(scope);
|
|
}
|
|
|
|
checkVariables(scope);
|
|
|
|
return checkChildScopes(scope);
|
|
};
|
|
|
|
return {
|
|
'Program:exit'() {
|
|
checkScope(context.getScope());
|
|
}
|
|
};
|
|
};
|
|
|
|
module.exports = {
|
|
create,
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
url: getDocumentationUrl(__filename)
|
|
}
|
|
}
|
|
};
|