blob: f8c1742e769f533507a7d51604099f47330ee2e4 [file] [log] [blame]
// Copyright 2021 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 * as SDK from '../core/sdk/sdk.js';
const base64Digits = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
export function encodeVlq(n: number): string {
// Set the sign bit as the least significant bit.
n = n >= 0 ? 2 * n : 1 - 2 * n;
// Encode into a base64 run.
let result = '';
while (true) {
// Extract the lowest 5 bits and remove them from the number.
const digit = n & 0x1f;
n >>= 5;
// Is there anything more left to encode?
if (n === 0) {
// We are done encoding, finish the run.
result += base64Digits[digit];
break;
} else {
// There is still more encode, so add the digit and the continuation bit.
result += base64Digits[0x20 + digit];
}
}
return result;
}
export function encodeVlqList(list: number[]) {
return list.map(encodeVlq).join('');
}
// Encode array mappings of the form "compiledLine:compiledColumn => srcFile:srcLine:srcColumn@name"
// as a source map.
export function encodeSourceMap(textMap: string[], sourceRoot?: string): SDK.SourceMap.SourceMapV3Object {
let mappings = '';
const sources: string[] = [];
const names: string[] = [];
let sourcesContent: Array<null|string>|undefined;
const state = {
line: -1,
column: 0,
srcFile: 0,
srcLine: 0,
srcColumn: 0,
srcName: 0,
};
for (const mapping of textMap) {
let match = mapping.match(/^(\d+):(\d+)(?:\s*=>\s*([^:]+):(\d+):(\d+)(?:@(\S+))?)?$/);
if (!match) {
match = mapping.match(/^([^:]+):\s*(.+)$/);
if (!match) {
throw new Error(`Cannot parse mapping "${mapping}"`);
}
(sourcesContent = sourcesContent ?? [])[getOrAddString(sources, match[1])] = match[2];
continue;
}
const lastState = Object.assign({}, state);
state.line = Number(match[1]);
state.column = Number(match[2]);
const hasSource = match[3] !== undefined;
const hasName = hasSource && (match[6] !== undefined);
if (hasSource) {
state.srcFile = getOrAddString(sources, match[3]);
state.srcLine = Number(match[4]);
state.srcColumn = Number(match[5]);
if (hasName) {
state.srcName = getOrAddString(names, match[6]);
}
}
if (state.line < lastState.line) {
throw new Error('Line numbers must be increasing');
}
const isNewLine = state.line !== lastState.line;
if (isNewLine) {
// Fixup for the first line mapping.
if (lastState.line === -1) {
lastState.line = 0;
}
// Insert semicolons for all the new lines.
mappings += ';'.repeat(state.line - lastState.line);
// Reset the compiled code column counter.
lastState.column = 0;
} else {
mappings += ',';
}
// Encode the mapping and add it to the list of mappings.
const toEncode = [state.column - lastState.column];
if (hasSource) {
toEncode.push(
state.srcFile - lastState.srcFile, state.srcLine - lastState.srcLine, state.srcColumn - lastState.srcColumn);
if (hasName) {
toEncode.push(state.srcName - lastState.srcName);
}
}
mappings += encodeVlqList(toEncode);
}
const sourceMapV3: SDK.SourceMap.SourceMapV3 = {version: 3, mappings, sources, names};
if (sourceRoot !== undefined) {
sourceMapV3.sourceRoot = sourceRoot;
}
if (sourcesContent !== undefined) {
for (let i = 0; i < sources.length; ++i) {
if (typeof sourcesContent[i] !== 'string') {
sourcesContent[i] = null;
}
}
sourceMapV3.sourcesContent = sourcesContent;
}
return sourceMapV3;
function getOrAddString(array: string[], s: string) {
const index = array.indexOf(s);
if (index >= 0) {
return index;
}
array.push(s);
return array.length - 1;
}
}
export class OriginalScopeBuilder {
#encodedScope = '';
#lastLine = 0;
#lastKind = 0;
readonly #names: string[];
/** The 'names' field of the SourceMap. The builder will modify it. */
constructor(names: string[]) {
this.#names = names;
}
start(
line: number, column: number,
options?: {name?: string, kind?: string, isStackFrame?: boolean, variables?: string[]}): this {
if (this.#encodedScope !== '') {
this.#encodedScope += ',';
}
const lineDiff = line - this.#lastLine;
this.#lastLine = line;
let flags = 0;
const nameIdxAndKindIdx: number[] = [];
if (options?.name) {
flags |= SDK.SourceMapScopes.EncodedOriginalScopeFlag.HAS_NAME;
nameIdxAndKindIdx.push(this.#nameIdx(options.name));
}
if (options?.kind) {
flags |= SDK.SourceMapScopes.EncodedOriginalScopeFlag.HAS_KIND;
nameIdxAndKindIdx.push(this.#encodeKind(options?.kind));
}
if (options?.isStackFrame) {
flags |= SDK.SourceMapScopes.EncodedOriginalScopeFlag.IS_STACK_FRAME;
}
this.#encodedScope += encodeVlqList([lineDiff, column, flags, ...nameIdxAndKindIdx]);
if (options?.variables) {
this.#encodedScope += encodeVlqList(options.variables.map(variable => this.#nameIdx(variable)));
}
return this;
}
end(line: number, column: number): this {
if (this.#encodedScope !== '') {
this.#encodedScope += ',';
}
const lineDiff = line - this.#lastLine;
this.#lastLine = line;
this.#encodedScope += encodeVlqList([lineDiff, column]);
return this;
}
build(): string {
const result = this.#encodedScope;
this.#lastLine = 0;
this.#encodedScope = '';
return result;
}
#encodeKind(kind: string): number {
const kindIdx = this.#nameIdx(kind);
const encodedIdx = kindIdx - this.#lastKind;
this.#lastKind = kindIdx;
return encodedIdx;
}
#nameIdx(name: string): number {
let idx = this.#names.indexOf(name);
if (idx < 0) {
idx = this.#names.length;
this.#names.push(name);
}
return idx;
}
}
export class GeneratedRangeBuilder {
#encodedRange = '';
#state = {
line: 0,
column: 0,
defSourceIdx: 0,
defScopeIdx: 0,
callsiteSourceIdx: 0,
callsiteLine: 0,
callsiteColumn: 0,
};
readonly #names: string[];
/** The 'names' field of the SourceMap. The builder will modify it. */
constructor(names: string[]) {
this.#names = names;
}
start(line: number, column: number, options?: {
isStackFrame?: boolean,
isHidden?: boolean,
definition?: {sourceIdx: number, scopeIdx: number},
callsite?: {sourceIdx: number, line: number, column: number},
bindings?: Array<string|undefined|Array<{line: number, column: number, name: string|undefined}>>,
}): this {
this.#emitLineSeparator(line);
this.#emitItemSepratorIfRequired();
const emittedColumn = column - (this.#state.line === line ? this.#state.column : 0);
this.#encodedRange += encodeVlq(emittedColumn);
this.#state.line = line;
this.#state.column = column;
let flags = 0;
if (options?.definition) {
flags |= SDK.SourceMapScopes.EncodedGeneratedRangeFlag.HAS_DEFINITION;
}
if (options?.callsite) {
flags |= SDK.SourceMapScopes.EncodedGeneratedRangeFlag.HAS_CALLSITE;
}
if (options?.isStackFrame) {
flags |= SDK.SourceMapScopes.EncodedGeneratedRangeFlag.IS_STACK_FRAME;
}
if (options?.isHidden) {
flags |= SDK.SourceMapScopes.EncodedGeneratedRangeFlag.IS_HIDDEN;
}
this.#encodedRange += encodeVlq(flags);
if (options?.definition) {
const {sourceIdx, scopeIdx} = options.definition;
this.#encodedRange += encodeVlq(sourceIdx - this.#state.defSourceIdx);
const emittedScopeIdx = scopeIdx - (this.#state.defSourceIdx === sourceIdx ? this.#state.defScopeIdx : 0);
this.#encodedRange += encodeVlq(emittedScopeIdx);
this.#state.defSourceIdx = sourceIdx;
this.#state.defScopeIdx = scopeIdx;
}
if (options?.callsite) {
const {sourceIdx, line, column} = options.callsite;
this.#encodedRange += encodeVlq(sourceIdx - this.#state.callsiteSourceIdx);
const emittedLine = line - (this.#state.callsiteSourceIdx === sourceIdx ? this.#state.callsiteLine : 0);
this.#encodedRange += encodeVlq(emittedLine);
const emittedColumn = column - (this.#state.callsiteLine === line ? this.#state.callsiteColumn : 0);
this.#encodedRange += encodeVlq(emittedColumn);
this.#state.callsiteSourceIdx = sourceIdx;
this.#state.callsiteLine = line;
this.#state.callsiteColumn = column;
}
for (const bindings of options?.bindings ?? []) {
if (bindings === undefined || typeof bindings === 'string') {
this.#encodedRange += encodeVlq(this.#nameIdx(bindings));
continue;
}
this.#encodedRange += encodeVlq(-bindings.length);
this.#encodedRange += encodeVlq(this.#nameIdx(bindings[0].name));
if (bindings[0].line !== line || bindings[0].column !== column) {
throw new Error('First binding line/column must match the range start line/column');
}
for (let i = 1; i < bindings.length; ++i) {
const {line, column, name} = bindings[i];
const emittedLine = line - bindings[i - 1].line;
const emittedColumn = column - (line === bindings[i - 1].line ? bindings[i - 1].column : 0);
this.#encodedRange += encodeVlq(emittedLine);
this.#encodedRange += encodeVlq(emittedColumn);
this.#encodedRange += encodeVlq(this.#nameIdx(name));
}
}
return this;
}
end(line: number, column: number): this {
this.#emitLineSeparator(line);
this.#emitItemSepratorIfRequired();
const emittedColumn = column - (this.#state.line === line ? this.#state.column : 0);
this.#encodedRange += encodeVlq(emittedColumn);
this.#state.line = line;
this.#state.column = column;
return this;
}
#emitLineSeparator(line: number): void {
for (let i = this.#state.line; i < line; ++i) {
this.#encodedRange += ';';
}
}
#emitItemSepratorIfRequired(): void {
if (this.#encodedRange !== '' && this.#encodedRange[this.#encodedRange.length - 1] !== ';') {
this.#encodedRange += ',';
}
}
#nameIdx(name?: string): number {
if (name === undefined) {
return -1;
}
let idx = this.#names.indexOf(name);
if (idx < 0) {
idx = this.#names.length;
this.#names.push(name);
}
return idx;
}
build(): string {
const result = this.#encodedRange;
this.#state = {
line: 0,
column: 0,
defSourceIdx: 0,
defScopeIdx: 0,
callsiteSourceIdx: 0,
callsiteLine: 0,
callsiteColumn: 0,
};
this.#encodedRange = '';
return result;
}
}