chrome-devtools-frontend
Version:
Chrome DevTools UI
157 lines (135 loc) • 4.83 kB
text/typescript
// 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;
}