| /** | |
|  * @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/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 | |
|         }; | |
|     } | |
| };
 |