eslint-plugin-unicorn
Version:
Various awesome ESLint rules
173 lines (143 loc) • 4.24 kB
JavaScript
'use strict';
const avoidCapture = require('./utils/avoid-capture');
const getDocumentationUrl = require('./utils/get-documentation-url');
const MESSAGE_ID = 'consistentDestructuring';
const MESSAGE_ID_SUGGEST = 'consistentDestructuringSuggest';
const declaratorSelector = [
'VariableDeclarator',
'[id.type="ObjectPattern"]',
'[init]',
'[init.type!="Literal"]'
].join('');
const memberSelector = [
'MemberExpression',
'[computed=false]',
':not(',
'AssignmentExpression > MemberExpression.left,',
'CallExpression > MemberExpression.callee,',
'NewExpression > MemberExpression.callee,',
'UpdateExpression > MemberExpression.argument,',
'UnaryExpression[operator="delete"] > MemberExpression.argument',
')'
].join('');
const isSimpleExpression = expression => {
while (expression) {
if (expression.computed) {
return false;
}
if (expression.type !== 'MemberExpression') {
break;
}
expression = expression.object;
}
return expression.type === 'Identifier' ||
expression.type === 'ThisExpression';
};
const isChildInParentScope = (child, parent) => {
while (child) {
if (child === parent) {
return true;
}
child = child.upper;
}
return false;
};
const create = context => {
const {ecmaVersion} = context.parserOptions;
const source = context.getSourceCode();
const declarations = new Map();
return {
[declaratorSelector]: node => {
// Ignore any complex expressions (e.g. arrays, functions)
if (!isSimpleExpression(node.init)) {
return;
}
declarations.set(source.getText(node.init), {
scope: context.getScope(),
variables: context.getDeclaredVariables(node),
objectPattern: node.id
});
},
[memberSelector]: node => {
const declaration = declarations.get(source.getText(node.object));
if (!declaration) {
return;
}
const {scope, objectPattern} = declaration;
const memberScope = context.getScope();
// Property is destructured outside the current scope
if (!isChildInParentScope(memberScope, scope)) {
return;
}
const destructurings = objectPattern.properties.filter(property =>
property.type === 'Property' &&
property.key.type === 'Identifier' &&
property.value.type === 'Identifier'
);
const lastProperty = objectPattern.properties[objectPattern.properties.length - 1];
// TODO: Remove `ExperimentalRestProperty` check when we drop support for `babel-eslint` #1040
const hasRest = lastProperty &&
(lastProperty.type === 'RestElement' || lastProperty.type === 'ExperimentalRestProperty');
const expression = source.getText(node);
const member = source.getText(node.property);
// Member might already be destructured
const destructuredMember = destructurings.find(property =>
property.key.name === member
);
if (!destructuredMember) {
// Don't destructure additional members when rest is used
if (hasRest) {
return;
}
// Destructured member collides with an existing identifier
if (avoidCapture(member, [memberScope], ecmaVersion) !== member) {
return;
}
}
// Don't try to fix nested member expressions
if (node.parent.type === 'MemberExpression') {
context.report({
node,
messageId: MESSAGE_ID
});
return;
}
const newMember = destructuredMember ? destructuredMember.value.name : member;
context.report({
node,
messageId: MESSAGE_ID,
suggest: [{
messageId: MESSAGE_ID_SUGGEST,
data: {
expression,
property: newMember
},
* fix(fixer) {
const {properties} = objectPattern;
const lastProperty = properties[properties.length - 1];
yield fixer.replaceText(node, newMember);
if (!destructuredMember) {
yield lastProperty ?
fixer.insertTextAfter(lastProperty, `, ${newMember}`) :
fixer.replaceText(objectPattern, `{${newMember}}`);
}
}
}]
});
}
};
};
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
fixable: 'code',
messages: {
[MESSAGE_ID]: 'Use destructured variables over properties.',
[MESSAGE_ID_SUGGEST]: 'Replace `{{expression}}` with destructured property `{{property}}`.'
}
}
};