| // 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; |
| } |