• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Prefer destructuring from arrays and objects
3 * @author Alex LaFroscia
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Rule Definition
9//------------------------------------------------------------------------------
10
11module.exports = {
12    meta: {
13        type: "suggestion",
14
15        docs: {
16            description: "require destructuring from arrays and/or objects",
17            category: "ECMAScript 6",
18            recommended: false,
19            url: "https://eslint.org/docs/rules/prefer-destructuring"
20        },
21
22        fixable: "code",
23
24        schema: [
25            {
26
27                /*
28                 * old support {array: Boolean, object: Boolean}
29                 * new support {VariableDeclarator: {}, AssignmentExpression: {}}
30                 */
31                oneOf: [
32                    {
33                        type: "object",
34                        properties: {
35                            VariableDeclarator: {
36                                type: "object",
37                                properties: {
38                                    array: {
39                                        type: "boolean"
40                                    },
41                                    object: {
42                                        type: "boolean"
43                                    }
44                                },
45                                additionalProperties: false
46                            },
47                            AssignmentExpression: {
48                                type: "object",
49                                properties: {
50                                    array: {
51                                        type: "boolean"
52                                    },
53                                    object: {
54                                        type: "boolean"
55                                    }
56                                },
57                                additionalProperties: false
58                            }
59                        },
60                        additionalProperties: false
61                    },
62                    {
63                        type: "object",
64                        properties: {
65                            array: {
66                                type: "boolean"
67                            },
68                            object: {
69                                type: "boolean"
70                            }
71                        },
72                        additionalProperties: false
73                    }
74                ]
75            },
76            {
77                type: "object",
78                properties: {
79                    enforceForRenamedProperties: {
80                        type: "boolean"
81                    }
82                },
83                additionalProperties: false
84            }
85        ],
86
87        messages: {
88            preferDestructuring: "Use {{type}} destructuring."
89        }
90    },
91    create(context) {
92
93        const enabledTypes = context.options[0];
94        const enforceForRenamedProperties = context.options[1] && context.options[1].enforceForRenamedProperties;
95        let normalizedOptions = {
96            VariableDeclarator: { array: true, object: true },
97            AssignmentExpression: { array: true, object: true }
98        };
99
100        if (enabledTypes) {
101            normalizedOptions = typeof enabledTypes.array !== "undefined" || typeof enabledTypes.object !== "undefined"
102                ? { VariableDeclarator: enabledTypes, AssignmentExpression: enabledTypes }
103                : enabledTypes;
104        }
105
106        //--------------------------------------------------------------------------
107        // Helpers
108        //--------------------------------------------------------------------------
109
110        // eslint-disable-next-line jsdoc/require-description
111        /**
112         * @param {string} nodeType "AssignmentExpression" or "VariableDeclarator"
113         * @param {string} destructuringType "array" or "object"
114         * @returns {boolean} `true` if the destructuring type should be checked for the given node
115         */
116        function shouldCheck(nodeType, destructuringType) {
117            return normalizedOptions &&
118                normalizedOptions[nodeType] &&
119                normalizedOptions[nodeType][destructuringType];
120        }
121
122        /**
123         * Determines if the given node is accessing an array index
124         *
125         * This is used to differentiate array index access from object property
126         * access.
127         * @param {ASTNode} node the node to evaluate
128         * @returns {boolean} whether or not the node is an integer
129         */
130        function isArrayIndexAccess(node) {
131            return Number.isInteger(node.property.value);
132        }
133
134        /**
135         * Report that the given node should use destructuring
136         * @param {ASTNode} reportNode the node to report
137         * @param {string} type the type of destructuring that should have been done
138         * @param {Function|null} fix the fix function or null to pass to context.report
139         * @returns {void}
140         */
141        function report(reportNode, type, fix) {
142            context.report({
143                node: reportNode,
144                messageId: "preferDestructuring",
145                data: { type },
146                fix
147            });
148        }
149
150        /**
151         * Determines if a node should be fixed into object destructuring
152         *
153         * The fixer only fixes the simplest case of object destructuring,
154         * like: `let x = a.x`;
155         *
156         * Assignment expression is not fixed.
157         * Array destructuring is not fixed.
158         * Renamed property is not fixed.
159         * @param {ASTNode} node the the node to evaluate
160         * @returns {boolean} whether or not the node should be fixed
161         */
162        function shouldFix(node) {
163            return node.type === "VariableDeclarator" &&
164                node.id.type === "Identifier" &&
165                node.init.type === "MemberExpression" &&
166                !node.init.computed &&
167                node.init.property.type === "Identifier" &&
168                node.id.name === node.init.property.name;
169        }
170
171        /**
172         * Fix a node into object destructuring.
173         * This function only handles the simplest case of object destructuring,
174         * see {@link shouldFix}.
175         * @param {SourceCodeFixer} fixer the fixer object
176         * @param {ASTNode} node the node to be fixed.
177         * @returns {Object} a fix for the node
178         */
179        function fixIntoObjectDestructuring(fixer, node) {
180            const rightNode = node.init;
181            const sourceCode = context.getSourceCode();
182
183            // Don't fix if that would remove any comments. Only comments inside `rightNode.object` can be preserved.
184            if (sourceCode.getCommentsInside(node).length > sourceCode.getCommentsInside(rightNode.object).length) {
185                return null;
186            }
187
188            return fixer.replaceText(
189                node,
190                `{${rightNode.property.name}} = ${sourceCode.getText(rightNode.object)}`
191            );
192        }
193
194        /**
195         * Check that the `prefer-destructuring` rules are followed based on the
196         * given left- and right-hand side of the assignment.
197         *
198         * Pulled out into a separate method so that VariableDeclarators and
199         * AssignmentExpressions can share the same verification logic.
200         * @param {ASTNode} leftNode the left-hand side of the assignment
201         * @param {ASTNode} rightNode the right-hand side of the assignment
202         * @param {ASTNode} reportNode the node to report the error on
203         * @returns {void}
204         */
205        function performCheck(leftNode, rightNode, reportNode) {
206            if (rightNode.type !== "MemberExpression" || rightNode.object.type === "Super") {
207                return;
208            }
209
210            if (isArrayIndexAccess(rightNode)) {
211                if (shouldCheck(reportNode.type, "array")) {
212                    report(reportNode, "array", null);
213                }
214                return;
215            }
216
217            const fix = shouldFix(reportNode)
218                ? fixer => fixIntoObjectDestructuring(fixer, reportNode)
219                : null;
220
221            if (shouldCheck(reportNode.type, "object") && enforceForRenamedProperties) {
222                report(reportNode, "object", fix);
223                return;
224            }
225
226            if (shouldCheck(reportNode.type, "object")) {
227                const property = rightNode.property;
228
229                if (
230                    (property.type === "Literal" && leftNode.name === property.value) ||
231                    (property.type === "Identifier" && leftNode.name === property.name && !rightNode.computed)
232                ) {
233                    report(reportNode, "object", fix);
234                }
235            }
236        }
237
238        /**
239         * Check if a given variable declarator is coming from an property access
240         * that should be using destructuring instead
241         * @param {ASTNode} node the variable declarator to check
242         * @returns {void}
243         */
244        function checkVariableDeclarator(node) {
245
246            // Skip if variable is declared without assignment
247            if (!node.init) {
248                return;
249            }
250
251            // We only care about member expressions past this point
252            if (node.init.type !== "MemberExpression") {
253                return;
254            }
255
256            performCheck(node.id, node.init, node);
257        }
258
259        /**
260         * Run the `prefer-destructuring` check on an AssignmentExpression
261         * @param {ASTNode} node the AssignmentExpression node
262         * @returns {void}
263         */
264        function checkAssigmentExpression(node) {
265            if (node.operator === "=") {
266                performCheck(node.left, node.right, node);
267            }
268        }
269
270        //--------------------------------------------------------------------------
271        // Public
272        //--------------------------------------------------------------------------
273
274        return {
275            VariableDeclarator: checkVariableDeclarator,
276            AssignmentExpression: checkAssigmentExpression
277        };
278    }
279};
280