/** * @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('../shared/types').Rule} */ module.exports = { meta: { type: "suggestion", docs: { description: "Require destructuring from arrays and/or objects", recommended: false, url: "https://eslint.org/docs/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.getSourceCode(); // 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 }; } };