'use strict'; const avoidCapture = require('./utils/avoid-capture.js'); const {not, notLeftHandSideSelector} = require('./selectors/index.js'); const MESSAGE_ID = 'consistentDestructuring'; const MESSAGE_ID_SUGGEST = 'consistentDestructuringSuggest'; const declaratorSelector = [ 'VariableDeclarator', '[id.type="ObjectPattern"]', '[init]', '[init.type!="Literal"]', ].join(''); const memberSelector = [ 'MemberExpression', '[computed!=true]', notLeftHandSideSelector(), not([ 'CallExpression > .callee', 'NewExpression> .callee', ]), ].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; }; /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { 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]; const hasRest = lastProperty && lastProperty.type === 'RestElement'; 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]) !== member) { return; } } // Don't try to fix nested member expressions if (node.parent.type === 'MemberExpression') { return { node, messageId: MESSAGE_ID, }; } const newMember = destructuredMember ? destructuredMember.value.name : member; return { 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}}`); } }, }], }; }, }; }; /** @type {import('eslint').Rule.RuleModule} */ module.exports = { create, meta: { type: 'suggestion', docs: { description: 'Use destructured variables over properties.', }, fixable: 'code', hasSuggestions: true, messages: { [MESSAGE_ID]: 'Use destructured variables over properties.', [MESSAGE_ID_SUGGEST]: 'Replace `{{expression}}` with destructured property `{{property}}`.', }, }, };