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