index.js (17229B)
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 { 6 debuggerToSourceMapLocation, 7 sourceMapToDebuggerLocation, 8 } from "../../location"; 9 import { locColumn } from "./locColumn"; 10 import { loadRangeMetadata, findMatchingRange } from "./rangeMetadata"; 11 12 // eslint-disable-next-line max-len 13 import { 14 findGeneratedReference, 15 findGeneratedImportReference, 16 findGeneratedImportDeclaration, 17 } from "./findGeneratedBindingFromPosition"; 18 import { 19 buildGeneratedBindingList, 20 buildFakeBindingList, 21 } from "./buildGeneratedBindingList"; 22 import { 23 originalRangeStartsInside, 24 getApplicableBindingsForOriginalPosition, 25 } from "./getApplicableBindingsForOriginalPosition"; 26 import { getOptimizedOutGrip } from "./optimizedOut"; 27 28 import { log } from "../../log"; 29 30 // Create real location objects for all location start and end. 31 // 32 // Parser worker returns scopes with location having a sourceId 33 // instead of a source object as it doesn't know about main thread source objects. 34 function updateLocationsInScopes(state, scopes) { 35 for (const item of scopes) { 36 for (const name of Object.keys(item.bindings)) { 37 for (const ref of item.bindings[name].refs) { 38 const locs = [ref]; 39 if (ref.type !== "ref") { 40 locs.push(ref.declaration); 41 } 42 for (const loc of locs) { 43 loc.start = sourceMapToDebuggerLocation(state, loc.start); 44 loc.end = sourceMapToDebuggerLocation(state, loc.end); 45 } 46 } 47 } 48 } 49 } 50 51 export async function buildMappedScopes( 52 source, 53 content, 54 frame, 55 generatedScopes, 56 thunkArgs 57 ) { 58 const { getState, parserWorker } = thunkArgs; 59 if (frame.location.source.isWasm) { 60 return null; 61 } 62 const originalAstScopes = await parserWorker.getScopes(frame.location); 63 updateLocationsInScopes(getState(), originalAstScopes); 64 const generatedAstScopes = await parserWorker.getScopes( 65 frame.generatedLocation 66 ); 67 updateLocationsInScopes(getState(), generatedAstScopes); 68 69 if (!originalAstScopes || !generatedAstScopes) { 70 return null; 71 } 72 73 const originalRanges = await loadRangeMetadata( 74 frame.location, 75 originalAstScopes, 76 thunkArgs 77 ); 78 79 if (hasLineMappings(originalRanges)) { 80 // Fallback to generated scopes as there are no clear mappings to original scopes 81 // This means the scope variable names are likely the same for both the original 82 // generated sources. 83 return { scope: generatedScopes }; 84 } 85 86 let generatedAstBindings; 87 if (generatedScopes) { 88 generatedAstBindings = buildGeneratedBindingList( 89 generatedScopes, 90 generatedAstScopes, 91 frame.this 92 ); 93 } else { 94 generatedAstBindings = buildFakeBindingList(generatedAstScopes); 95 } 96 97 const { mappedOriginalScopes, expressionLookup } = 98 await mapOriginalBindingsToGenerated( 99 source, 100 content, 101 originalRanges, 102 originalAstScopes, 103 generatedAstBindings, 104 thunkArgs 105 ); 106 107 const globalLexicalScope = generatedScopes 108 ? getGlobalFromScope(generatedScopes) 109 : generateGlobalFromAst(generatedAstScopes); 110 const mappedGeneratedScopes = generateClientScope( 111 globalLexicalScope, 112 mappedOriginalScopes 113 ); 114 115 return isReliableScope(mappedGeneratedScopes) 116 ? { mappings: expressionLookup, scope: mappedGeneratedScopes } 117 : { scope: generatedScopes }; 118 } 119 120 async function mapOriginalBindingsToGenerated( 121 source, 122 content, 123 originalRanges, 124 originalAstScopes, 125 generatedAstBindings, 126 thunkArgs 127 ) { 128 const expressionLookup = {}; 129 const mappedOriginalScopes = []; 130 131 const cachedSourceMaps = batchScopeMappings( 132 originalAstScopes, 133 source, 134 thunkArgs 135 ); 136 // Override sourceMapLoader attribute with the special cached SourceMapLoader instance 137 // in order to make it used by all functions used in this method. 138 thunkArgs = { ...thunkArgs, sourceMapLoader: cachedSourceMaps }; 139 140 for (const item of originalAstScopes) { 141 const generatedBindings = {}; 142 143 for (const name of Object.keys(item.bindings)) { 144 const binding = item.bindings[name]; 145 146 const result = await findGeneratedBinding( 147 source, 148 content, 149 name, 150 binding, 151 originalRanges, 152 generatedAstBindings, 153 thunkArgs 154 ); 155 156 if (result) { 157 generatedBindings[name] = result.grip; 158 159 if ( 160 binding.refs.length !== 0 && 161 // These are assigned depth-first, so we don't want shadowed 162 // bindings in parent scopes overwriting the expression. 163 !Object.prototype.hasOwnProperty.call(expressionLookup, name) 164 ) { 165 expressionLookup[name] = result.expression; 166 } 167 } 168 } 169 170 mappedOriginalScopes.push({ 171 ...item, 172 generatedBindings, 173 }); 174 } 175 176 return { 177 mappedOriginalScopes, 178 expressionLookup, 179 }; 180 } 181 182 /** 183 * Consider a scope and its parents reliable if the vast majority of its 184 * bindings were successfully mapped to generated scope bindings. 185 */ 186 function isReliableScope(scope) { 187 let totalBindings = 0; 188 let unknownBindings = 0; 189 190 for (let s = scope; s; s = s.parent) { 191 const vars = s.bindings?.variables || {}; 192 for (const key of Object.keys(vars)) { 193 const binding = vars[key]; 194 195 totalBindings += 1; 196 if ( 197 binding.value && 198 typeof binding.value === "object" && 199 (binding.value.type === "unscoped" || binding.value.type === "unmapped") 200 ) { 201 unknownBindings += 1; 202 } 203 } 204 } 205 206 // As determined by fair dice roll. 207 return totalBindings === 0 || unknownBindings / totalBindings < 0.25; 208 } 209 210 function hasLineMappings(ranges) { 211 return ranges.every( 212 range => range.columnStart === 0 && range.columnEnd === Infinity 213 ); 214 } 215 216 /** 217 * Build a special SourceMapLoader instance, based on the one passed in thunkArgs, 218 * which will both: 219 * - preload generated ranges/locations for original locations mentioned 220 * in originalAstScopes 221 * - cache the requests to fetch these genereated ranges/locations 222 */ 223 function batchScopeMappings(originalAstScopes, source, thunkArgs) { 224 const { sourceMapLoader } = thunkArgs; 225 const precalculatedRanges = new Map(); 226 const precalculatedLocations = new Map(); 227 228 // Explicitly dispatch all of the sourcemap requests synchronously up front so 229 // that they will be batched into a single request for the worker to process. 230 for (const item of originalAstScopes) { 231 for (const name of Object.keys(item.bindings)) { 232 for (const ref of item.bindings[name].refs) { 233 const locs = [ref]; 234 if (ref.type !== "ref") { 235 locs.push(ref.declaration); 236 } 237 238 for (const loc of locs) { 239 precalculatedRanges.set( 240 buildLocationKey(loc.start), 241 sourceMapLoader.getGeneratedRanges( 242 debuggerToSourceMapLocation(loc.start) 243 ) 244 ); 245 precalculatedLocations.set( 246 buildLocationKey(loc.start), 247 sourceMapLoader.getGeneratedLocation( 248 debuggerToSourceMapLocation(loc.start) 249 ) 250 ); 251 precalculatedLocations.set( 252 buildLocationKey(loc.end), 253 sourceMapLoader.getGeneratedLocation( 254 debuggerToSourceMapLocation(loc.end) 255 ) 256 ); 257 } 258 } 259 } 260 } 261 262 return { 263 async getGeneratedRanges(pos) { 264 const key = buildLocationKey(pos); 265 266 if (!precalculatedRanges.has(key)) { 267 log("Bad precalculated mapping"); 268 return sourceMapLoader.getGeneratedRanges( 269 debuggerToSourceMapLocation(pos) 270 ); 271 } 272 return precalculatedRanges.get(key); 273 }, 274 275 async getGeneratedLocation(pos) { 276 const key = buildLocationKey(pos); 277 278 if (!precalculatedLocations.has(key)) { 279 log("Bad precalculated mapping"); 280 return sourceMapLoader.getGeneratedLocation( 281 debuggerToSourceMapLocation(pos) 282 ); 283 } 284 return precalculatedLocations.get(key); 285 }, 286 }; 287 } 288 function buildLocationKey(loc) { 289 return `${loc.line}:${locColumn(loc)}`; 290 } 291 292 function generateClientScope(globalLexicalScope, originalScopes) { 293 // Build a structure similar to the client's linked scope object using 294 // the original AST scopes, but pulling in the generated bindings 295 // linked to each scope. 296 const result = originalScopes 297 .slice(0, -2) 298 .reverse() 299 .reduce((acc, orig, i) => { 300 const { 301 // The 'this' binding data we have is handled independently, so 302 // the binding data is not included here. 303 // eslint-disable-next-line no-unused-vars 304 this: _this, 305 ...variables 306 } = orig.generatedBindings; 307 308 return { 309 parent: acc, 310 actor: `originalActor${i}`, 311 type: orig.type, 312 scopeKind: orig.scopeKind, 313 bindings: { 314 arguments: [], 315 variables, 316 }, 317 ...(orig.type === "function" 318 ? { 319 function: { 320 displayName: orig.displayName, 321 }, 322 } 323 : null), 324 ...(orig.type === "block" 325 ? { 326 block: { 327 displayName: orig.displayName, 328 }, 329 } 330 : null), 331 }; 332 }, globalLexicalScope); 333 334 // The rendering logic in getScope 'this' bindings only runs on the current 335 // selected frame scope, so we pluck out the 'this' binding that was mapped, 336 // and put it in a special location 337 const thisScope = originalScopes.find(scope => scope.bindings.this); 338 if (result.bindings && thisScope) { 339 result.bindings.this = thisScope.generatedBindings.this || null; 340 } 341 342 return result; 343 } 344 345 function getGlobalFromScope(scopes) { 346 // Pull the root object scope and root lexical scope to reuse them in 347 // our mapped scopes. This assumes that file being processed is 348 // a CommonJS or ES6 module, which might not be ideal. Potentially 349 // should add some logic to try to detect those cases? 350 let globalLexicalScope = null; 351 for (let s = scopes; s.parent; s = s.parent) { 352 globalLexicalScope = s; 353 } 354 if (!globalLexicalScope) { 355 throw new Error("Assertion failure - there should always be a scope"); 356 } 357 return globalLexicalScope; 358 } 359 360 function generateGlobalFromAst(generatedScopes) { 361 const globalLexicalAst = generatedScopes[generatedScopes.length - 2]; 362 if (!globalLexicalAst) { 363 throw new Error("Assertion failure - there should always be a scope"); 364 } 365 return { 366 actor: "generatedActor1", 367 type: "block", 368 scopeKind: "", 369 bindings: { 370 arguments: [], 371 variables: Object.fromEntries( 372 Object.keys(globalLexicalAst).map(key => [key, getOptimizedOutGrip()]) 373 ), 374 }, 375 parent: { 376 actor: "generatedActor0", 377 object: getOptimizedOutGrip(), 378 scopeKind: "", 379 type: "object", 380 }, 381 }; 382 } 383 384 function hasValidIdent(range, pos) { 385 return ( 386 range.type === "match" || 387 // For declarations, we allow the range on the identifier to be a 388 // more general "contains" to increase the chances of a match. 389 (pos.type !== "ref" && range.type === "contains") 390 ); 391 } 392 393 // eslint-disable-next-line complexity 394 async function findGeneratedBinding( 395 source, 396 content, 397 name, 398 originalBinding, 399 originalRanges, 400 generatedAstBindings, 401 thunkArgs 402 ) { 403 // If there are no references to the implicits, then we have no way to 404 // even attempt to map it back to the original since there is no location 405 // data to use. Bail out instead of just showing it as unmapped. 406 if ( 407 originalBinding.type === "implicit" && 408 !originalBinding.refs.some(item => item.type === "ref") 409 ) { 410 return null; 411 } 412 413 const loadApplicableBindings = async (pos, locationType) => { 414 let applicableBindings = await getApplicableBindingsForOriginalPosition( 415 generatedAstBindings, 416 source, 417 pos, 418 originalBinding.type, 419 locationType, 420 thunkArgs 421 ); 422 if (applicableBindings.length) { 423 hadApplicableBindings = true; 424 } 425 if (locationType === "ref") { 426 // Some tooling creates ranges that map a line as a whole, which is useful 427 // for step-debugging, but can easily lead to finding the wrong binding. 428 // To avoid these false-positives, we entirely ignore bindings matched 429 // by ranges that cover full lines. 430 applicableBindings = applicableBindings.filter( 431 ({ range }) => 432 !(range.start.column === 0 && range.end.column === Infinity) 433 ); 434 } 435 if ( 436 locationType !== "ref" && 437 !(await originalRangeStartsInside(pos, thunkArgs)) 438 ) { 439 applicableBindings = []; 440 } 441 return applicableBindings; 442 }; 443 444 const { refs } = originalBinding; 445 446 let hadApplicableBindings = false; 447 let genContent = null; 448 for (const pos of refs) { 449 const applicableBindings = await loadApplicableBindings(pos, pos.type); 450 451 const range = findMatchingRange(originalRanges, pos); 452 if (range && hasValidIdent(range, pos)) { 453 if (originalBinding.type === "import") { 454 genContent = await findGeneratedImportReference(applicableBindings); 455 } else { 456 genContent = await findGeneratedReference(applicableBindings); 457 } 458 } 459 460 if ( 461 (pos.type === "class-decl" || pos.type === "class-inner") && 462 content.contentType && 463 content.contentType.match(/\/typescript/) 464 ) { 465 const declRange = findMatchingRange(originalRanges, pos.declaration); 466 if (declRange && declRange.type !== "multiple") { 467 const applicableDeclBindings = await loadApplicableBindings( 468 pos.declaration, 469 pos.type 470 ); 471 472 // Resolve to first binding in the range 473 const declContent = await findGeneratedReference( 474 applicableDeclBindings 475 ); 476 477 if (declContent) { 478 // Prefer the declaration mapping in this case because TS sometimes 479 // maps class declaration names to "export.Foo = Foo;" or to 480 // the decorator logic itself 481 genContent = declContent; 482 } 483 } 484 } 485 486 if ( 487 !genContent && 488 pos.type === "import-decl" && 489 typeof pos.importName === "string" 490 ) { 491 const { importName } = pos; 492 const declRange = findMatchingRange(originalRanges, pos.declaration); 493 494 // The import declaration should have an original position mapping, 495 // but otherwise we don't really have preferences on the range type 496 // because it can have multiple bindings, but we do want to make sure 497 // that all of the bindings that match the range are part of the same 498 // import declaration. 499 if (declRange?.singleDeclaration) { 500 const applicableDeclBindings = await loadApplicableBindings( 501 pos.declaration, 502 pos.type 503 ); 504 505 // match the import declaration location 506 genContent = await findGeneratedImportDeclaration( 507 applicableDeclBindings, 508 importName 509 ); 510 } 511 } 512 513 if (genContent) { 514 break; 515 } 516 } 517 518 if (genContent && genContent.desc) { 519 return { 520 grip: genContent.desc, 521 expression: genContent.expression, 522 }; 523 } else if (genContent) { 524 // If there is no descriptor for 'this', then this is not the top-level 525 // 'this' that the server gave us a binding for, and we can just ignore it. 526 if (name === "this") { 527 return null; 528 } 529 530 // If the location is found but the descriptor is not, then it 531 // means that the server scope information didn't match the scope 532 // information from the DevTools parsed scopes. 533 return { 534 grip: { 535 configurable: false, 536 enumerable: true, 537 writable: false, 538 value: { 539 type: "unscoped", 540 unscoped: true, 541 542 // HACK: Until support for "unscoped" lands in devtools-reps, 543 // this will make these show as (unavailable). 544 missingArguments: true, 545 }, 546 }, 547 expression: null, 548 }; 549 } else if (!hadApplicableBindings && name !== "this") { 550 // If there were no applicable bindings to consider while searching for 551 // matching bindings, then the source map for this file didn't make any 552 // attempt to map the binding, and that most likely means that the 553 // code was entirely emitted from the output code. 554 return { 555 grip: getOptimizedOutGrip(), 556 expression: ` 557 (() => { 558 throw new Error('"' + ${JSON.stringify( 559 name 560 )} + '" has been optimized out.'); 561 })() 562 `, 563 }; 564 } 565 566 // If no location mapping is found, then the map is bad, or 567 // the map is okay but it original location is inside 568 // of some scope, but the generated location is outside, leading 569 // us to search for bindings that don't technically exist. 570 return { 571 grip: { 572 configurable: false, 573 enumerable: true, 574 writable: false, 575 value: { 576 type: "unmapped", 577 unmapped: true, 578 579 // HACK: Until support for "unmapped" lands in devtools-reps, 580 // this will make these show as (unavailable). 581 missingArguments: true, 582 }, 583 }, 584 expression: null, 585 }; 586 }