UNPKG

eslint-plugin-unicorn

Version:
173 lines (143 loc) 4.24 kB
'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}}`.' } } };