UNPKG

chrome-devtools-frontend

Version:
157 lines (135 loc) 4.83 kB
// Copyright 2025 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import type * as ScopesCodec from '../../third_party/source-map-scopes-codec/source-map-scopes-codec.js'; import {TokenIterator} from './SourceMap.js'; export interface NamedFunctionRange { start: ScopesCodec.Position; end: ScopesCodec.Position; name: string; } /** * Turns a list of {@link NamedFunctionRange}s into a single {@link OriginalScope} tree nested * according to the start/end position. Each range is turned into a OriginalScope with the `isStackFrame` * bit set to denote it as a function and a generic "Function" label. * * We nest all these function scopes underneath a single global scope that always starts at (0, 0) and * reaches to the largest end position. * * `ranges` can be unsorted but will be sorted in-place. * * @throws if the ranges are not nested properly. Concretely: start < end for each range, and no * "straddling" (i.e. partially overlapping ranges). */ export function buildOriginalScopes(ranges: NamedFunctionRange[]): ScopesCodec.OriginalScope { validateStartBeforeEnd(ranges); // 1. Sort ranges by ascending start position. // If two ranges have the same start position, we sort the one // with the higher end position first, because it's the parent. ranges.sort((a, b) => comparePositions(a.start, b.start) || comparePositions(b.end, a.end)); const root: ScopesCodec.OriginalScope = { start: {line: 0, column: 0}, end: {line: Number.POSITIVE_INFINITY, column: Number.POSITIVE_INFINITY}, kind: 'Global', isStackFrame: false, children: [], variables: [], }; // 2. Build the tree from the ranges. const stack: ScopesCodec.OriginalScope[] = [root]; for (const range of ranges) { // Pop all scopes that precede the current entry (to find the right parent). let stackTop = stack.at(-1) as ScopesCodec.OriginalScope; while (true) { if (comparePositions(stackTop.end, range.start) <= 0) { stack.pop(); stackTop = stack.at(-1) as ScopesCodec.OriginalScope; } else { break; } } /* * Check for partially overlapping ranges: * * --- A * | --- B * --- | * --- * *i.e.: B.start < A.end < B.end */ if (comparePositions(range.start, stackTop.end) < 0 && comparePositions(stackTop.end, range.end) < 0) { throw new Error(`Range ${JSON.stringify(range)} and ${JSON.stringify(stackTop)} partially overlap.`); } const scope = createScopeFrom(range); stackTop.children.push(scope); stack.push(scope); } // 3. Update root.end. const lastChild = root.children.at(-1); if (lastChild) { root.end = lastChild.end; } return root; } function validateStartBeforeEnd(ranges: NamedFunctionRange[]): void { for (const range of ranges) { if (comparePositions(range.start, range.end) >= 0) { throw new Error(`Invalid range. End before start: ${JSON.stringify(range)}`); } } } function createScopeFrom(range: NamedFunctionRange): ScopesCodec.OriginalScope { return { ...range, kind: 'Function', isStackFrame: true, children: [], variables: [], }; } /** * Implements decoding of the pasta source map specification. * * See https://github.com/bloomberg/pasta-sourcemaps/blob/main/spec.md */ export function decodePastaRanges(encodedRanges: string, names: string[]): NamedFunctionRange[] { const result: NamedFunctionRange[] = []; let nameIndex = 0; let startLineNumber = 0; let startColumnNumber = 0; let endLineNumber = 0; let endColumnNumber = 0; const tokenIter = new TokenIterator(encodedRanges); let atStart = true; while (tokenIter.hasNext()) { if (atStart) { atStart = false; } else if (tokenIter.peek() === ',') { tokenIter.next(); } else { // Unexpected character. Return what we have up until now. break; } nameIndex += tokenIter.nextVLQ(); startLineNumber = endLineNumber + tokenIter.nextVLQ(); startColumnNumber += tokenIter.nextVLQ(); endLineNumber = startLineNumber + tokenIter.nextVLQ(); endColumnNumber += tokenIter.nextVLQ(); const name = names[nameIndex]; if (name === undefined) { // If the range doesn't have a valid name, ignore it. continue; } result.push({ start: {line: startLineNumber, column: startColumnNumber}, end: {line: endLineNumber, column: endColumnNumber}, name, }); } return result; } /** @returns 0 if both positions are equal, a negative number if a < b and a positive number if a > b */ function comparePositions(a: ScopesCodec.Position, b: ScopesCodec.Position): number { return a.line - b.line || a.column - b.column; }