chrome-devtools-frontend
Version:
Chrome DevTools UI
349 lines (285 loc) • 10.9 kB
text/typescript
// Copyright 2025 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {
EncodedTag,
GeneratedRangeFlags,
OriginalScopeFlags,
} from "../codec.js";
import type { GeneratedRange, OriginalScope, ScopeInfo } from "../scopes.d.ts";
import { encodeSigned, encodeUnsigned } from "../vlq.js";
import { comparePositions } from "../util.js";
const DEFAULT_SCOPE_STATE = {
line: 0,
column: 0,
name: 0,
kind: 0,
variable: 0,
};
const DEFAULT_RANGE_STATE = {
line: 0,
column: 0,
defScopeIdx: 0,
};
export class Encoder {
readonly #info: ScopeInfo;
readonly #names: string[];
// Hash map to resolve indices of strings in the "names" array. Otherwise we'd have
// to use 'indexOf' for every name we want to encode.
readonly #namesToIndex = new Map<string, number>();
readonly #scopeState = { ...DEFAULT_SCOPE_STATE };
readonly #rangeState = { ...DEFAULT_RANGE_STATE };
#encodedItems: string[] = [];
#currentItem: string = "";
#scopeToCount = new Map<OriginalScope, number>();
#scopeCounter = 0;
constructor(info: ScopeInfo, names: string[]) {
this.#info = info;
this.#names = names;
for (let i = 0; i < names.length; ++i) {
this.#namesToIndex.set(names[i], i);
}
}
encode(): string {
this.#encodedItems = [];
this.#info.scopes.forEach((scope) => {
this.#scopeState.line = 0;
this.#scopeState.column = 0;
this.#encodeOriginalScope(scope);
});
this.#info.ranges.forEach((range) => {
this.#encodeGeneratedRange(range);
});
return this.#encodedItems.join(",");
}
#encodeOriginalScope(scope: OriginalScope | null): void {
if (scope === null) {
this.#encodedItems.push("");
return;
}
this.#encodeOriginalScopeStart(scope);
this.#encodeOriginalScopeVariables(scope);
scope.children.forEach((child) => this.#encodeOriginalScope(child));
this.#encodeOriginalScopeEnd(scope);
}
#encodeOriginalScopeStart(scope: OriginalScope) {
const { line, column } = scope.start;
this.#verifyPositionWithScopeState(line, column);
let flags = 0;
const encodedLine = line - this.#scopeState.line;
const encodedColumn = encodedLine === 0
? column - this.#scopeState.column
: column;
this.#scopeState.line = line;
this.#scopeState.column = column;
let encodedName: number | undefined;
if (scope.name !== undefined) {
flags |= OriginalScopeFlags.HAS_NAME;
const nameIdx = this.#resolveNamesIdx(scope.name);
encodedName = nameIdx - this.#scopeState.name;
this.#scopeState.name = nameIdx;
}
let encodedKind: number | undefined;
if (scope.kind !== undefined) {
flags |= OriginalScopeFlags.HAS_KIND;
const kindIdx = this.#resolveNamesIdx(scope.kind);
encodedKind = kindIdx - this.#scopeState.kind;
this.#scopeState.kind = kindIdx;
}
if (scope.isStackFrame) flags |= OriginalScopeFlags.IS_STACK_FRAME;
this.#encodeTag(EncodedTag.ORIGINAL_SCOPE_START).#encodeUnsigned(flags)
.#encodeUnsigned(encodedLine).#encodeUnsigned(encodedColumn);
if (encodedName !== undefined) this.#encodeSigned(encodedName);
if (encodedKind !== undefined) this.#encodeSigned(encodedKind);
this.#finishItem();
this.#scopeToCount.set(scope, this.#scopeCounter++);
}
#encodeOriginalScopeVariables(scope: OriginalScope) {
if (scope.variables.length === 0) return;
this.#encodeTag(EncodedTag.ORIGINAL_SCOPE_VARIABLES);
for (const variable of scope.variables) {
const idx = this.#resolveNamesIdx(variable);
this.#encodeSigned(idx - this.#scopeState.variable);
this.#scopeState.variable = idx;
}
this.#finishItem();
}
#encodeOriginalScopeEnd(scope: OriginalScope) {
const { line, column } = scope.end;
this.#verifyPositionWithScopeState(line, column);
const encodedLine = line - this.#scopeState.line;
const encodedColumn = encodedLine === 0
? column - this.#scopeState.column
: column;
this.#scopeState.line = line;
this.#scopeState.column = column;
this.#encodeTag(EncodedTag.ORIGINAL_SCOPE_END).#encodeUnsigned(encodedLine)
.#encodeUnsigned(encodedColumn).#finishItem();
}
#encodeGeneratedRange(range: GeneratedRange): void {
this.#encodeGeneratedRangeStart(range);
this.#encodeGeneratedRangeBindings(range);
this.#encodeGeneratedRangeSubRangeBindings(range);
this.#encodeGeneratedRangeCallSite(range);
range.children.forEach((child) => this.#encodeGeneratedRange(child));
this.#encodeGeneratedRangeEnd(range);
}
#encodeGeneratedRangeStart(range: GeneratedRange) {
const { line, column } = range.start;
this.#verifyPositionWithRangeState(line, column);
let flags = 0;
const encodedLine = line - this.#rangeState.line;
let encodedColumn = column - this.#rangeState.column;
if (encodedLine > 0) {
flags |= GeneratedRangeFlags.HAS_LINE;
encodedColumn = column;
}
this.#rangeState.line = line;
this.#rangeState.column = column;
let encodedDefinition;
if (range.originalScope) {
const definitionIdx = this.#scopeToCount.get(range.originalScope);
if (definitionIdx === undefined) {
throw new Error("Unknown OriginalScope for definition!");
}
flags |= GeneratedRangeFlags.HAS_DEFINITION;
encodedDefinition = definitionIdx - this.#rangeState.defScopeIdx;
this.#rangeState.defScopeIdx = definitionIdx;
}
if (range.isStackFrame) flags |= GeneratedRangeFlags.IS_STACK_FRAME;
if (range.isHidden) flags |= GeneratedRangeFlags.IS_HIDDEN;
this.#encodeTag(EncodedTag.GENERATED_RANGE_START).#encodeUnsigned(flags);
if (encodedLine > 0) this.#encodeUnsigned(encodedLine);
this.#encodeUnsigned(encodedColumn);
if (encodedDefinition !== undefined) this.#encodeSigned(encodedDefinition);
this.#finishItem();
}
#encodeGeneratedRangeSubRangeBindings(range: GeneratedRange) {
if (range.values.length === 0) return;
for (let i = 0; i < range.values.length; ++i) {
const value = range.values[i];
if (!Array.isArray(value) || value.length <= 1) {
continue;
}
this.#encodeTag(EncodedTag.GENERATED_RANGE_SUBRANGE_BINDING)
.#encodeUnsigned(i);
let lastLine = range.start.line;
let lastColumn = range.start.column;
for (let j = 1; j < value.length; ++j) {
const subRange = value[j];
const prevSubRange = value[j - 1];
if (comparePositions(prevSubRange.to, subRange.from) !== 0) {
throw new Error("Sub-range bindings must not have gaps");
}
const encodedLine = subRange.from.line - lastLine;
const encodedColumn = encodedLine === 0
? subRange.from.column - lastColumn
: subRange.from.column;
if (encodedLine < 0 || encodedColumn < 0) {
throw new Error("Sub-range bindings must be sorted");
}
lastLine = subRange.from.line;
lastColumn = subRange.from.column;
const binding = subRange.value === undefined
? 0
: this.#resolveNamesIdx(subRange.value) + 1;
this.#encodeUnsigned(binding).#encodeUnsigned(encodedLine)
.#encodeUnsigned(encodedColumn);
}
this.#finishItem();
}
}
#encodeGeneratedRangeBindings(range: GeneratedRange) {
if (range.values.length === 0) return;
if (!range.originalScope) {
throw new Error("Range has binding expressions but no OriginalScope");
} else if (range.originalScope.variables.length !== range.values.length) {
throw new Error(
"Range's binding expressions don't match OriginalScopes' variables",
);
}
this.#encodeTag(EncodedTag.GENERATED_RANGE_BINDINGS);
for (const val of range.values) {
if (val === null || val === undefined) {
this.#encodeUnsigned(0);
} else if (typeof val === "string") {
this.#encodeUnsigned(this.#resolveNamesIdx(val) + 1);
} else {
const initialValue = val[0];
const binding = initialValue.value === undefined
? 0
: this.#resolveNamesIdx(initialValue.value) + 1;
this.#encodeUnsigned(binding);
}
}
this.#finishItem();
}
#encodeGeneratedRangeCallSite(range: GeneratedRange) {
if (!range.callSite) return;
const { sourceIndex, line, column } = range.callSite;
// TODO: Throw if stackFrame flag is set or OriginalScope index is invalid or no generated range is here.
this.#encodeTag(EncodedTag.GENERATED_RANGE_CALL_SITE).#encodeUnsigned(
sourceIndex,
).#encodeUnsigned(line).#encodeUnsigned(column).#finishItem();
}
#encodeGeneratedRangeEnd(range: GeneratedRange) {
const { line, column } = range.end;
this.#verifyPositionWithRangeState(line, column);
let flags = 0;
const encodedLine = line - this.#rangeState.line;
let encodedColumn = column - this.#rangeState.column;
if (encodedLine > 0) {
flags |= GeneratedRangeFlags.HAS_LINE;
encodedColumn = column;
}
this.#rangeState.line = line;
this.#rangeState.column = column;
this.#encodeTag(EncodedTag.GENERATED_RANGE_END);
if (encodedLine > 0) this.#encodeUnsigned(encodedLine);
this.#encodeUnsigned(encodedColumn).#finishItem();
}
#resolveNamesIdx(name: string): number {
const index = this.#namesToIndex.get(name);
if (index !== undefined) return index;
const addedIndex = this.#names.length;
this.#names.push(name);
this.#namesToIndex.set(name, addedIndex);
return addedIndex;
}
#verifyPositionWithScopeState(line: number, column: number) {
if (
this.#scopeState.line > line ||
(this.#scopeState.line === line && this.#scopeState.column > column)
) {
throw new Error(
`Attempting to encode scope item (${line}, ${column}) that precedes the last encoded scope item (${this.#scopeState.line}, ${this.#scopeState.column})`,
);
}
}
#verifyPositionWithRangeState(line: number, column: number) {
if (
this.#rangeState.line > line ||
(this.#rangeState.line === line && this.#rangeState.column > column)
) {
throw new Error(
`Attempting to encode range item that precedes the last encoded range item (${line}, ${column})`,
);
}
}
#encodeTag(tag: EncodedTag): this {
this.#currentItem += tag;
return this;
}
#encodeSigned(n: number): this {
this.#currentItem += encodeSigned(n);
return this;
}
#encodeUnsigned(n: number): this {
this.#currentItem += encodeUnsigned(n);
return this;
}
#finishItem(): void {
this.#encodedItems.push(this.#currentItem);
this.#currentItem = "";
}
}