chrome-devtools-frontend
Version:
Chrome DevTools UI
257 lines • 10 kB
JavaScript
// 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 { 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 {
#info;
#names;
// 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.
#namesToIndex = new Map();
#scopeState = {
...DEFAULT_SCOPE_STATE
};
#rangeState = {
...DEFAULT_RANGE_STATE
};
#encodedItems = [];
#currentItem = "";
#scopeToCount = new Map();
#scopeCounter = 0;
constructor(info, names){
this.#info = info;
this.#names = names;
for(let i = 0; i < names.length; ++i){
this.#namesToIndex.set(names[i], i);
}
}
encode() {
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) {
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) {
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;
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;
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) {
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) {
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) {
this.#encodeGeneratedRangeStart(range);
this.#encodeGeneratedRangeBindings(range);
this.#encodeGeneratedRangeSubRangeBindings(range);
this.#encodeGeneratedRangeCallSite(range);
range.children.forEach((child)=>this.#encodeGeneratedRange(child));
this.#encodeGeneratedRangeEnd(range);
}
#encodeGeneratedRangeStart(range) {
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) {
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) {
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) {
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) {
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) {
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, column) {
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, column) {
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) {
this.#currentItem += tag;
return this;
}
#encodeSigned(n) {
this.#currentItem += encodeSigned(n);
return this;
}
#encodeUnsigned(n) {
this.#currentItem += encodeUnsigned(n);
return this;
}
#finishItem() {
this.#encodedItems.push(this.#currentItem);
this.#currentItem = "";
}
}
//# sourceMappingURL=encoder.js.map