/** * @fileoverview Prefer destructuring from arrays and objects * @author Alex LaFroscia */ 'use strict'; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const astUtils = require('./utils/ast-utils'); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ const PRECEDENCE_OF_ASSIGNMENT_EXPR = astUtils.getPrecedence({ type: 'AssignmentExpression', }); //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../types').Rule.RuleModule} */ module.exports = { meta: { type: 'suggestion', docs: { description: 'Require destructuring from arrays and/or objects', recommended: false, frozen: true, url: 'https://eslint.org/docs/latest/rules/prefer-destructuring', }, fixable: 'code', schema: [ { /* * old support {array: Boolean, object: Boolean} * new support {VariableDeclarator: {}, AssignmentExpression: {}} */ oneOf: [ { type: 'object', properties: { VariableDeclarator: { type: 'object', properties: { array: { type: 'boolean', }, object: { type: 'boolean', }, }, additionalProperties: false, }, AssignmentExpression: { type: 'object', properties: { array: { type: 'boolean', }, object: { type: 'boolean', }, }, additionalProperties: false, }, }, additionalProperties: false, }, { type: 'object', properties: { array: { type: 'boolean', }, object: { type: 'boolean', }, }, additionalProperties: false, }, ], }, { type: 'object', properties: { enforceForRenamedProperties: { type: 'boolean', }, }, additionalProperties: false, }, ], messages: { preferDestructuring: 'Use {{type}} destructuring.', }, }, create(context) { const enabledTypes = context.options[0]; const enforceForRenamedProperties = context.options[1] && context.options[1].enforceForRenamedProperties; let normalizedOptions = { VariableDeclarator: { array: true, object: true }, AssignmentExpression: { array: true, object: true }, }; if (enabledTypes) { normalizedOptions = ( typeof enabledTypes.array !== 'undefined' || typeof enabledTypes.object !== 'undefined' ) ? { VariableDeclarator: enabledTypes, AssignmentExpression: enabledTypes, } : enabledTypes; } //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- /** * Checks if destructuring type should be checked. * @param {string} nodeType "AssignmentExpression" or "VariableDeclarator" * @param {string} destructuringType "array" or "object" * @returns {boolean} `true` if the destructuring type should be checked for the given node */ function shouldCheck(nodeType, destructuringType) { return ( normalizedOptions && normalizedOptions[nodeType] && normalizedOptions[nodeType][destructuringType] ); } /** * Determines if the given node is accessing an array index * * This is used to differentiate array index access from object property * access. * @param {ASTNode} node the node to evaluate * @returns {boolean} whether or not the node is an integer */ function isArrayIndexAccess(node) { return Number.isInteger(node.property.value); } /** * Report that the given node should use destructuring * @param {ASTNode} reportNode the node to report * @param {string} type the type of destructuring that should have been done * @param {Function|null} fix the fix function or null to pass to context.report * @returns {void} */ function report(reportNode, type, fix) { context.report({ node: reportNode, messageId: 'preferDestructuring', data: { type }, fix, }); } /** * Determines if a node should be fixed into object destructuring * * The fixer only fixes the simplest case of object destructuring, * like: `let x = a.x`; * * Assignment expression is not fixed. * Array destructuring is not fixed. * Renamed property is not fixed. * @param {ASTNode} node the node to evaluate * @returns {boolean} whether or not the node should be fixed */ function shouldFix(node) { return ( node.type === 'VariableDeclarator' && node.id.type === 'Identifier' && node.init.type === 'MemberExpression' && !node.init.computed && node.init.property.type === 'Identifier' && node.id.name === node.init.property.name ); } /** * Fix a node into object destructuring. * This function only handles the simplest case of object destructuring, * see {@link shouldFix}. * @param {SourceCodeFixer} fixer the fixer object * @param {ASTNode} node the node to be fixed. * @returns {Object} a fix for the node */ function fixIntoObjectDestructuring(fixer, node) { const rightNode = node.init; const sourceCode = context.sourceCode; // Don't fix if that would remove any comments. Only comments inside `rightNode.object` can be preserved. if ( sourceCode.getCommentsInside(node).length > sourceCode.getCommentsInside(rightNode.object).length ) { return null; } let objectText = sourceCode.getText(rightNode.object); if ( astUtils.getPrecedence(rightNode.object) < PRECEDENCE_OF_ASSIGNMENT_EXPR ) { objectText = `(${objectText})`; } return fixer.replaceText( node, `{${rightNode.property.name}} = ${objectText}` ); } /** * Check that the `prefer-destructuring` rules are followed based on the * given left- and right-hand side of the assignment. * * Pulled out into a separate method so that VariableDeclarators and * AssignmentExpressions can share the same verification logic. * @param {ASTNode} leftNode the left-hand side of the assignment * @param {ASTNode} rightNode the right-hand side of the assignment * @param {ASTNode} reportNode the node to report the error on * @returns {void} */ function performCheck(leftNode, rightNode, reportNode) { if ( rightNode.type !== 'MemberExpression' || rightNode.object.type === 'Super' || rightNode.property.type === 'PrivateIdentifier' ) { return; } if (isArrayIndexAccess(rightNode)) { if (shouldCheck(reportNode.type, 'array')) { report(reportNode, 'array', null); } return; } const fix = shouldFix(reportNode) ? (fixer) => fixIntoObjectDestructuring(fixer, reportNode) : null; if ( shouldCheck(reportNode.type, 'object') && enforceForRenamedProperties ) { report(reportNode, 'object', fix); return; } if (shouldCheck(reportNode.type, 'object')) { const property = rightNode.property; if ( (property.type === 'Literal' && leftNode.name === property.value) || (property.type === 'Identifier' && leftNode.name === property.name && !rightNode.computed) ) { report(reportNode, 'object', fix); } } } /** * Check if a given variable declarator is coming from an property access * that should be using destructuring instead * @param {ASTNode} node the variable declarator to check * @returns {void} */ function checkVariableDeclarator(node) { // Skip if variable is declared without assignment if (!node.init) { return; } // We only care about member expressions past this point if (node.init.type !== 'MemberExpression') { return; } performCheck(node.id, node.init, node); } /** * Run the `prefer-destructuring` check on an AssignmentExpression * @param {ASTNode} node the AssignmentExpression node * @returns {void} */ function checkAssignmentExpression(node) { if (node.operator === '=') { performCheck(node.left, node.right, node); } } //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- return { VariableDeclarator: checkVariableDeclarator, AssignmentExpression: checkAssignmentExpression, }; }, };