tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }