findGeneratedBindingFromPosition.js (9296B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 import { locColumn } from "./locColumn"; 6 import { mappingContains } from "./mappingContains"; 7 8 // eslint-disable-next-line max-len 9 10 import { clientCommands } from "../../../client/firefox"; 11 12 /** 13 * Given a mapped range over the generated source, attempt to resolve a real 14 * binding descriptor that can be used to access the value. 15 */ 16 export async function findGeneratedReference(applicableBindings) { 17 // We can adjust this number as we go, but these are a decent start as a 18 // general heuristic to assume the bindings were bad or just map a chunk of 19 // whole line or something. 20 if (applicableBindings.length > 4) { 21 // Babel's for..of generates at least 3 bindings inside one range for 22 // block-scoped loop variables, so we shouldn't go below that. 23 applicableBindings = []; 24 } 25 26 for (const applicable of applicableBindings) { 27 const result = await mapBindingReferenceToDescriptor(applicable); 28 if (result) { 29 return result; 30 } 31 } 32 return null; 33 } 34 35 export async function findGeneratedImportReference(applicableBindings) { 36 // When wrapped, for instance as `Object(ns.default)`, the `Object` binding 37 // will be the first in the list. To avoid resolving `Object` as the 38 // value of the import itself, we potentially skip the first binding. 39 applicableBindings = applicableBindings.filter((applicable, i) => { 40 if ( 41 !applicable.firstInRange || 42 applicable.binding.loc.type !== "ref" || 43 applicable.binding.loc.meta 44 ) { 45 return true; 46 } 47 48 const next = 49 i + 1 < applicableBindings.length ? applicableBindings[i + 1] : null; 50 51 return !next || next.binding.loc.type !== "ref" || !next.binding.loc.meta; 52 }); 53 54 // We can adjust this number as we go, but these are a decent start as a 55 // general heuristic to assume the bindings were bad or just map a chunk of 56 // whole line or something. 57 if (applicableBindings.length > 2) { 58 // Babel's for..of generates at least 3 bindings inside one range for 59 // block-scoped loop variables, so we shouldn't go below that. 60 applicableBindings = []; 61 } 62 63 for (const applicable of applicableBindings) { 64 const result = await mapImportReferenceToDescriptor(applicable); 65 if (result) { 66 return result; 67 } 68 } 69 70 return null; 71 } 72 73 /** 74 * Given a mapped range over the generated source and the name of the imported 75 * value that is referenced, attempt to resolve a binding descriptor for 76 * the import's value. 77 */ 78 export async function findGeneratedImportDeclaration( 79 applicableBindings, 80 importName 81 ) { 82 // We can adjust this number as we go, but these are a decent start as a 83 // general heuristic to assume the bindings were bad or just map a chunk of 84 // whole line or something. 85 if (applicableBindings.length > 10) { 86 // Import declarations tend to have a large number of bindings for 87 // for things like 'require' and 'interop', so this number is larger 88 // than other binding count checks. 89 applicableBindings = []; 90 } 91 92 let result = null; 93 94 for (const { binding } of applicableBindings) { 95 if (binding.loc.type === "ref") { 96 continue; 97 } 98 99 const namespaceDesc = await binding.desc(); 100 if (isPrimitiveValue(namespaceDesc)) { 101 continue; 102 } 103 if (!isObjectValue(namespaceDesc)) { 104 // We want to handle cases like 105 // 106 // var _mod = require(...); 107 // var _mod2 = _interopRequire(_mod); 108 // 109 // where "_mod" is optimized out because it is only referenced once. To 110 // allow that, we track the optimized-out value as a possible result, 111 // but allow later binding values to overwrite the result. 112 result = { 113 name: binding.name, 114 desc: namespaceDesc, 115 expression: binding.name, 116 }; 117 continue; 118 } 119 120 const desc = await readDescriptorProperty(namespaceDesc, importName); 121 const expression = `${binding.name}.${importName}`; 122 123 if (desc) { 124 result = { 125 name: binding.name, 126 desc, 127 expression, 128 }; 129 break; 130 } 131 } 132 133 return result; 134 } 135 136 /** 137 * Given a generated binding, and a range over the generated code, statically 138 * check if the given binding matches the range. 139 */ 140 async function mapBindingReferenceToDescriptor({ 141 binding, 142 range, 143 firstInRange, 144 firstOnLine, 145 }) { 146 // Allow the mapping to point anywhere within the generated binding 147 // location to allow for less than perfect sourcemaps. Since you also 148 // need at least one character between identifiers, we also give one 149 // characters of space at the front the generated binding in order 150 // to increase the probability of finding the right mapping. 151 if ( 152 range.start.line === binding.loc.start.line && 153 // If a binding is the first on a line, Babel will extend the mapping to 154 // include the whitespace between the newline and the binding. To handle 155 // that, we skip the range requirement for starting location. 156 (firstInRange || 157 firstOnLine || 158 locColumn(range.start) >= locColumn(binding.loc.start)) && 159 locColumn(range.start) <= locColumn(binding.loc.end) 160 ) { 161 return { 162 name: binding.name, 163 desc: await binding.desc(), 164 expression: binding.name, 165 }; 166 } 167 168 return null; 169 } 170 171 /** 172 * Given an generated binding, and a range over the generated code, statically 173 * evaluate accessed properties within the mapped range to resolve the actual 174 * imported value. 175 */ 176 async function mapImportReferenceToDescriptor({ binding, range }) { 177 if (binding.loc.type !== "ref") { 178 return null; 179 } 180 181 // Expression matches require broader searching because sourcemaps usage 182 // varies in how they map certain things. For instance given 183 // 184 // import { bar } from "mod"; 185 // bar(); 186 // 187 // The "bar()" expression is generally expanded into one of two possibly 188 // forms, both of which map the "bar" identifier in different ways. See 189 // the "^^" markers below for the ranges. 190 // 191 // (0, foo.bar)() // Babel 192 // ^^^^^^^ // mapping 193 // ^^^ // binding 194 // vs 195 // 196 // __webpack_require__.i(foo.bar)() // Webpack 2 197 // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // mapping 198 // ^^^ // binding 199 // vs 200 // 201 // Object(foo.bar)() // Webpack >= 3 202 // ^^^^^^^^^^^^^^^ // mapping 203 // ^^^ // binding 204 // 205 // Unfortunately, Webpack also has a tendancy to over-map past the call 206 // expression to the start of the next line, at least when there isn't 207 // anything else on that line that is mapped, e.g. 208 // 209 // Object(foo.bar)() 210 // ^^^^^^^^^^^^^^^^^ 211 // ^ // wrapped to column 0 of next line 212 213 if (!mappingContains(range, binding.loc)) { 214 return null; 215 } 216 217 // Webpack 2's import declarations wrap calls with an identity fn, so we 218 // need to make sure to skip that binding because it is mapped to the 219 // location of the original binding usage. 220 if ( 221 binding.name === "__webpack_require__" && 222 binding.loc.meta && 223 binding.loc.meta.type === "member" && 224 binding.loc.meta.property === "i" 225 ) { 226 return null; 227 } 228 229 let expression = binding.name; 230 let desc = await binding.desc(); 231 232 if (binding.loc.type === "ref") { 233 const { meta } = binding.loc; 234 235 // Limit to 2 simple property or inherits operartions, since it would 236 // just be more work to search more and it is very unlikely that 237 // bindings would be mapped to more than a single member + inherits 238 // wrapper. 239 for ( 240 let op = meta, index = 0; 241 op && mappingContains(range, op) && desc && index < 2; 242 index++, op = op?.parent 243 ) { 244 // Calling could potentially trigger side-effects, which would not 245 // be ideal for this case. 246 if (op.type === "call") { 247 return null; 248 } 249 250 if (op.type === "inherit") { 251 continue; 252 } 253 254 desc = await readDescriptorProperty(desc, op.property); 255 expression += `.${op.property}`; 256 } 257 } 258 259 return desc 260 ? { 261 name: binding.name, 262 desc, 263 expression, 264 } 265 : null; 266 } 267 268 function isPrimitiveValue(desc) { 269 return desc && (!desc.value || typeof desc.value !== "object"); 270 } 271 function isObjectValue(desc) { 272 return ( 273 desc && 274 !isPrimitiveValue(desc) && 275 desc.value.type === "object" && 276 // Note: The check for `.type` might already cover the optimizedOut case 277 // but not 100% sure, so just being cautious. 278 !desc.value.optimizedOut 279 ); 280 } 281 282 async function readDescriptorProperty(desc, property) { 283 if (!desc) { 284 return null; 285 } 286 287 if (typeof desc.value !== "object" || !desc.value) { 288 // If accessing a property on a primitive type, just return 'undefined' 289 // as the value. 290 return { 291 value: { 292 type: "undefined", 293 }, 294 }; 295 } 296 297 if (!isObjectValue(desc)) { 298 // If we got a non-primitive descriptor but it isn't an object, then 299 // it's definitely not the namespace and it is probably an error. 300 return desc; 301 } 302 303 const objectFront = clientCommands.createObjectFront(desc.value); 304 return (await objectFront.getProperty(property)).descriptor; 305 }