--- /dev/null
+/**
+ * @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
+//------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: "suggestion",
+
+ docs: {
+ description: "require destructuring from arrays and/or objects",
+ category: "ECMAScript 6",
+ 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
+ //--------------------------------------------------------------------------
+
+ // eslint-disable-next-line jsdoc/require-description
+ /**
+ * @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") {
+ 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
+ };
+ }
+};