tor-browser

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

breakpointPositions.js (12773B)


      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  getBreakpointPositionsForSource,
      7  getSourceActorsForSource,
      8 } from "../../selectors/index";
      9 
     10 import { makeBreakpointId } from "../../utils/breakpoint/index";
     11 import { memoizeableAction } from "../../utils/memoizableAction";
     12 import { fulfilled } from "../../utils/async-value";
     13 import {
     14  sourceMapToDebuggerLocation,
     15  createLocation,
     16 } from "../../utils/location";
     17 import { validateSource } from "../../utils/context";
     18 
     19 /**
     20 * Helper function which consumes breakpoints positions sent by the server
     21 * and map them to location objects.
     22 * During this process, the SourceMapLoader will be queried to map the positions from generated to original locations.
     23 *
     24 * @param {object} breakpointPositions
     25 *        The positions to map related to the generated source:
     26 *          {
     27 *            1: [ 2, 6 ], // Line 1 is breakable on column 2 and 6
     28 *            2: [ 2 ], // Line 2 is only breakable on column 2
     29 *          }
     30 * @param {object} generatedSource
     31 * @param {object} location
     32 *        The current location we are computing breakable positions.
     33 * @param {object} thunk arguments
     34 * @return {object}
     35 *         The mapped breakable locations in the original source:
     36 *          {
     37 *            1: [ { source, line: 1, column: 2} , { source, line: 1, column 6 } ], // Line 1 is not mapped as location are same as breakpointPositions.
     38 *            10: [ { source, line: 10, column: 28 } ], // Line 2 is mapped and locations and line key refers to the original source positions.
     39 *          }
     40 */
     41 async function mapToLocations(
     42  breakpointPositions,
     43  generatedSource,
     44  mappedLocation,
     45  { getState, sourceMapLoader }
     46 ) {
     47  // Map breakable positions from generated to original locations.
     48  let mappedBreakpointPositions = await sourceMapLoader.getOriginalLocations(
     49    breakpointPositions,
     50    generatedSource.id
     51  );
     52  // The Source Map Loader will return null when there is no source map for that generated source.
     53  // Consider the map as unrelated to source map and process the source actor positions as-is.
     54  if (!mappedBreakpointPositions) {
     55    mappedBreakpointPositions = breakpointPositions;
     56  }
     57 
     58  const positions = {};
     59 
     60  // Ensure that we have an entry for the line fetched
     61  if (typeof mappedLocation.line === "number") {
     62    positions[mappedLocation.line] = [];
     63  }
     64 
     65  const handledBreakpointIds = new Set();
     66  const isOriginal = mappedLocation.source.isOriginal;
     67  const originalSourceId = mappedLocation.source.id;
     68 
     69  for (let line in mappedBreakpointPositions) {
     70    // createLocation expects a number and not a string.
     71    line = parseInt(line, 10);
     72    for (const columnOrSourceMapLocation of mappedBreakpointPositions[line]) {
     73      let location, generatedLocation;
     74 
     75      // When processing a source unrelated to source map, `mappedBreakpointPositions` will be equal to `breakpointPositions`.
     76      // and columnOrSourceMapLocation will always be a number.
     77      // But it will also be a number if we process a source mapped file and SourceMapLoader didn't find any valid mapping
     78      // for the current position (line and column).
     79      // When this happen to be a number it means it isn't mapped and columnOrSourceMapLocation refers to the column index.
     80      if (typeof columnOrSourceMapLocation == "number") {
     81        // If columnOrSourceMapLocation is a number, it means that this location doesn't mapped to an original source.
     82        // So if we are currently computation positions for an original source, we can skip this breakable positions.
     83        if (isOriginal) {
     84          continue;
     85        }
     86        location = generatedLocation = createLocation({
     87          line,
     88          column: columnOrSourceMapLocation,
     89          source: generatedSource,
     90        });
     91      } else {
     92        // Otherwise, for sources which are mapped. `columnOrSourceMapLocation` will be a SourceMapLoader location object.
     93        // This location object will refer to the location where the current column (columnOrSourceMapLocation.generatedColumn)
     94        // mapped in the original file.
     95 
     96        // When computing positions for an original source, ignore the location if that mapped to another original source.
     97        if (
     98          isOriginal &&
     99          columnOrSourceMapLocation.sourceId != originalSourceId
    100        ) {
    101          continue;
    102        }
    103 
    104        location = sourceMapToDebuggerLocation(
    105          getState(),
    106          columnOrSourceMapLocation
    107        );
    108 
    109        // Merge positions that refer to duplicated positions.
    110        // Some sourcemaped positions might refer to the exact same source/line/column triple.
    111        const breakpointId = makeBreakpointId(location);
    112        if (handledBreakpointIds.has(breakpointId)) {
    113          continue;
    114        }
    115        handledBreakpointIds.add(breakpointId);
    116 
    117        generatedLocation = createLocation({
    118          line,
    119          column: columnOrSourceMapLocation.generatedColumn,
    120          source: generatedSource,
    121        });
    122      }
    123 
    124      // The positions stored in redux will be keyed by original source's line (if we are
    125      // computing the original source positions), or the generated source line.
    126      // Note that when we compute the bundle positions, location may refer to the original source,
    127      // but we still want to use the generated location as key.
    128      const keyLocation = isOriginal ? location : generatedLocation;
    129      const keyLine = keyLocation.line;
    130      if (!positions[keyLine]) {
    131        positions[keyLine] = [];
    132      }
    133      positions[keyLine].push({ location, generatedLocation });
    134    }
    135  }
    136 
    137  return positions;
    138 }
    139 
    140 async function _setBreakpointPositions(location, thunkArgs) {
    141  const { client, dispatch, getState, sourceMapLoader } = thunkArgs;
    142  const results = {};
    143  let generatedSource = location.source;
    144  if (location.source.isOriginal) {
    145    const ranges = await sourceMapLoader.getGeneratedRangesForOriginal(
    146      location.source.id,
    147      true
    148    );
    149    generatedSource = location.source.generatedSource;
    150 
    151    // Note: While looping here may not look ideal, in the vast majority of
    152    // cases, the number of ranges here should be very small, and is quite
    153    // likely to only be a single range.
    154    for (const range of ranges) {
    155      // Wrap infinite end positions to the next line to keep things simple
    156      // and because we know we don't care about the end-line whitespace
    157      // in this case.
    158      if (range.end.column === Infinity) {
    159        range.end = {
    160          line: range.end.line + 1,
    161          column: 0,
    162        };
    163      }
    164 
    165      // Retrieve the positions for all the source actors for the related generated source.
    166      // There might be many if it is loaded many times.
    167      // We limit the retrieval of positions within the given range, so that we don't
    168      // retrieve the whole bundle positions.
    169      const allActorsPositions = await Promise.all(
    170        getSourceActorsForSource(getState(), generatedSource.id).map(actor =>
    171          client.getSourceActorBreakpointPositions(actor, range)
    172        )
    173      );
    174 
    175      // `allActorsPositions` looks like this:
    176      // [
    177      //   { // Positions for the first source actor
    178      //     1: [ 2, 6 ], // Line 1 is breakable on column 2 and 6
    179      //     2: [ 2 ], // Line 2 is only breakable on column 2
    180      //   },
    181      //   {...} // Positions for another source actor
    182      // ]
    183      for (const actorPositions of allActorsPositions) {
    184        for (const rangeLine in actorPositions) {
    185          const columns = actorPositions[rangeLine];
    186 
    187          // Merge all actors's breakable columns and avoid duplication of columns reported as breakable
    188          const existing = results[rangeLine];
    189          if (existing) {
    190            for (const column of columns) {
    191              if (!existing.includes(column)) {
    192                existing.push(column);
    193              }
    194            }
    195          } else {
    196            results[rangeLine] = columns;
    197          }
    198        }
    199      }
    200    }
    201  } else {
    202    const { line } = location;
    203    if (typeof line !== "number") {
    204      throw new Error("Line is required for generated sources");
    205    }
    206 
    207    // We only retrieve the positions for the given requested line, that, for each source actor.
    208    // There might be many source actor, if it is loaded many times.
    209    // Or if this is an html page, with many inline scripts.
    210    const allActorsBreakableColumns = await Promise.all(
    211      getSourceActorsForSource(getState(), location.source.id).map(
    212        async actor => {
    213          const positions = await client.getSourceActorBreakpointPositions(
    214            actor,
    215            {
    216              // Only retrieve positions for the given line
    217              start: { line, column: 0 },
    218              end: { line: line + 1, column: 0 },
    219            }
    220          );
    221          return positions[line] || [];
    222        }
    223      )
    224    );
    225 
    226    for (const columns of allActorsBreakableColumns) {
    227      // Merge all actors's breakable columns and avoid duplication of columns reported as breakable
    228      const existing = results[line];
    229      if (existing) {
    230        for (const column of columns) {
    231          if (!existing.includes(column)) {
    232            existing.push(column);
    233          }
    234        }
    235      } else {
    236        results[line] = columns;
    237      }
    238    }
    239  }
    240 
    241  const positions = await mapToLocations(
    242    results,
    243    generatedSource,
    244    location,
    245    thunkArgs
    246  );
    247  // `mapToLocations` may compute for a little while asynchronously,
    248  // ensure that the location is still valid before continuing.
    249  validateSource(getState(), location.source);
    250 
    251  dispatch({
    252    type: "ADD_BREAKPOINT_POSITIONS",
    253    source: location.source,
    254    positions,
    255  });
    256 }
    257 
    258 function generatedSourceActorKey(state, source) {
    259  const generatedSource = source.isOriginal ? source.generatedSource : source;
    260  const actors = generatedSource
    261    ? getSourceActorsForSource(state, generatedSource.id).map(
    262        ({ actor }) => actor
    263      )
    264    : [];
    265  return [source.id, ...actors].join(":");
    266 }
    267 
    268 /**
    269 * This method will force retrieving the breakable positions for a given source, on a given line.
    270 * If this data has already been computed, it will returned the cached data.
    271 *
    272 * For original sources, this will query the SourceMap worker.
    273 * For generated sources, this will query the DevTools server and the related source actors.
    274 *
    275 * @param Object options
    276 *        Dictionary object with many arguments:
    277 * @param String options.sourceId
    278 *        The source we want to fetch breakable positions
    279 * @param Number options.line
    280 *        The line we want to know which columns are breakable.
    281 *        (note that this seems to be optional for original sources)
    282 * @return Array<Object>
    283 *         The list of all breakable positions, each object of this array will be like this:
    284 *         {
    285 *           line: Number
    286 *           column: Number
    287 *           source: Source object
    288 *         }
    289 */
    290 export const setBreakpointPositions = memoizeableAction(
    291  "setBreakpointPositions",
    292  {
    293    getValue: (location, { getState }) => {
    294      const positions = getBreakpointPositionsForSource(
    295        getState(),
    296        location.source.id
    297      );
    298      if (!positions) {
    299        return null;
    300      }
    301 
    302      if (
    303        !location.source.isOriginal &&
    304        location.line &&
    305        !positions[location.line]
    306      ) {
    307        // We always return the full position dataset, but if a given line is
    308        // not available, we treat the whole set as loading.
    309        return null;
    310      }
    311 
    312      return fulfilled(positions);
    313    },
    314    createKey(location, { getState }) {
    315      const key = generatedSourceActorKey(getState(), location.source);
    316      return !location.source.isOriginal && location.line
    317        ? `${key}-${location.line}`
    318        : key;
    319    },
    320    action: async (location, thunkArgs) =>
    321      _setBreakpointPositions(location, thunkArgs),
    322  }
    323 );
    324 
    325 export function updateBreakpointPositionsForNewPrettyPrintedSource(
    326  minifiedSource
    327 ) {
    328  return async ({ dispatch, getState }) => {
    329    const oldPositions = getBreakpointPositionsForSource(
    330      getState(),
    331      minifiedSource.id
    332    );
    333    if (!oldPositions) {
    334      return;
    335    }
    336 
    337    // gather the lines for which we have breakpointPositions
    338    const lines = [...Object.keys(oldPositions)].map(lineString =>
    339      Number(lineString)
    340    );
    341 
    342    dispatch({ type: "CLEAR_BREAKPOINT_POSITIONS", source: minifiedSource });
    343 
    344    // recompute the breakpoint positions for all lines for which we had breakpointPositions before
    345    await Promise.all(
    346      lines.map(line =>
    347        dispatch(setBreakpointPositions({ source: minifiedSource, line }))
    348      )
    349    );
    350  };
    351 }