302 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			302 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @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
 | |
|         };
 | |
|     }
 | |
| };
 |