UNPKG

chrome-devtools-frontend

Version:
540 lines (468 loc) • 15.3 kB
// 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 { GeneratedRangeFlags, type GeneratedRangeStartItem, OriginalScopeFlags, type OriginalScopeStartItem, Tag, } from "../codec.js"; import type { GeneratedRange, IndexSourceMapJson, OriginalScope, Position, ScopeInfo, SourceMap, SourceMapJson, SubRangeBinding, } from "../scopes.d.ts"; import { TokenIterator } from "../vlq.js"; /** * The mode decides how well-formed the encoded scopes have to be, to be accepted by the decoder. * * LAX is the default and is much more lenient. It's still best effort though and the decoder doesn't * implement any error recovery: e.g. superfluous "start" items can lead to whole trees being omitted. * * STRICT mode will throw in the following situations: * * - Encountering ORIGINAL_SCOPE_END, or GENERATED_RANGE_END items that don't have matching *_START items. * - Encountering ORIGINAL_SCOPE_VARIABLES items outside a surrounding scope START/END. * - Encountering GENERATED_RANGE_BINDINGS items outside a surrounding range START/END. * - Miss-matches between the number of variables in a scope vs the number of value expressions in the ranges. * - Out-of-bound indices into the "names" array. */ export const enum DecodeMode { STRICT = 1, LAX = 2, } export interface DecodeOptions { mode: DecodeMode; /** * Offsets `start` and `end` of all generated ranges by the specified amount. * Intended to be used when decoding sections of index source maps one-by-one. * * Has no effect when passing a {@link IndexSourceMapJson} directly to {@link decode}. */ generatedOffset: Position; } export const DEFAULT_DECODE_OPTIONS: DecodeOptions = { mode: DecodeMode.LAX, generatedOffset: { line: 0, column: 0 }, }; export function decode( sourceMap: SourceMap, options: Partial<DecodeOptions> = DEFAULT_DECODE_OPTIONS, ): ScopeInfo { const opts = { ...DEFAULT_DECODE_OPTIONS, ...options }; if ("sections" in sourceMap) { return decodeIndexMap(sourceMap, { ...opts, generatedOffset: { line: 0, column: 0 }, }); } return decodeMap(sourceMap, opts); } function decodeMap( sourceMap: SourceMapJson, options: DecodeOptions, ): ScopeInfo { if (!sourceMap.scopes || !sourceMap.names) return { scopes: [], ranges: [] }; return new Decoder(sourceMap.scopes, sourceMap.names, options).decode(); } function decodeIndexMap( sourceMap: IndexSourceMapJson, options: DecodeOptions, ): ScopeInfo { const scopeInfo: ScopeInfo = { scopes: [], ranges: [] }; for (const section of sourceMap.sections) { const { scopes, ranges } = decode(section.map, { ...options, generatedOffset: section.offset, }); for (const scope of scopes) scopeInfo.scopes.push(scope); for (const range of ranges) scopeInfo.ranges.push(range); } return scopeInfo; } const DEFAULT_SCOPE_STATE = { line: 0, column: 0, name: 0, kind: 0, variable: 0, }; const DEFAULT_RANGE_STATE = { line: 0, column: 0, defScopeIdx: 0, }; class Decoder { readonly #encodedScopes: string; readonly #names: string[]; readonly #mode: DecodeMode; #scopes: (OriginalScope | null)[] = []; #ranges: GeneratedRange[] = []; readonly #scopeState = { ...DEFAULT_SCOPE_STATE }; readonly #rangeState = { ...DEFAULT_RANGE_STATE }; readonly #scopeStack: OriginalScope[] = []; readonly #rangeStack: GeneratedRange[] = []; #flatOriginalScopes: OriginalScope[] = []; #subRangeBindingsForRange = new Map<number, [number, number, number][]>(); constructor(scopes: string, names: string[], options: DecodeOptions) { this.#encodedScopes = scopes; this.#names = names; this.#mode = options.mode; this.#rangeState.line = options.generatedOffset.line; this.#rangeState.column = options.generatedOffset.column; } decode(): ScopeInfo { const iter = new TokenIterator(this.#encodedScopes); while (iter.hasNext()) { if (iter.peek() === ",") { iter.nextChar(); // Consume ",". this.#scopes.push(null); // Add an EmptyItem; continue; } const tag = iter.nextUnsignedVLQ(); switch (tag) { case Tag.ORIGINAL_SCOPE_START: { const item: OriginalScopeStartItem = { flags: iter.nextUnsignedVLQ(), line: iter.nextUnsignedVLQ(), column: iter.nextUnsignedVLQ(), }; if (item.flags & OriginalScopeFlags.HAS_NAME) { item.nameIdx = iter.nextSignedVLQ(); } if (item.flags & OriginalScopeFlags.HAS_KIND) { item.kindIdx = iter.nextSignedVLQ(); } this.#handleOriginalScopeStartItem(item); break; } case Tag.ORIGINAL_SCOPE_VARIABLES: { const variableIdxs: number[] = []; while (iter.hasNext() && iter.peek() !== ",") { variableIdxs.push(iter.nextSignedVLQ()); } this.#handleOriginalScopeVariablesItem(variableIdxs); break; } case Tag.ORIGINAL_SCOPE_END: { this.#handleOriginalScopeEndItem( iter.nextUnsignedVLQ(), iter.nextUnsignedVLQ(), ); break; } case Tag.GENERATED_RANGE_START: { const flags = iter.nextUnsignedVLQ(); const line = flags & GeneratedRangeFlags.HAS_LINE ? iter.nextUnsignedVLQ() : undefined; const column = iter.nextUnsignedVLQ(); const definitionIdx = flags & GeneratedRangeFlags.HAS_DEFINITION ? iter.nextSignedVLQ() : undefined; this.#handleGeneratedRangeStartItem({ flags, line, column, definitionIdx, }); break; } case Tag.GENERATED_RANGE_END: { const lineOrColumn = iter.nextUnsignedVLQ(); const maybeColumn = iter.hasNext() && iter.peek() !== "," ? iter.nextUnsignedVLQ() : undefined; if (maybeColumn !== undefined) { this.#handleGeneratedRangeEndItem(lineOrColumn, maybeColumn); } else { this.#handleGeneratedRangeEndItem(0, lineOrColumn); } break; } case Tag.GENERATED_RANGE_BINDINGS: { const valueIdxs: number[] = []; while (iter.hasNext() && iter.peek() !== ",") { valueIdxs.push(iter.nextUnsignedVLQ()); } this.#handleGeneratedRangeBindingsItem(valueIdxs); break; } case Tag.GENERATED_RANGE_SUBRANGE_BINDING: { const variableIndex = iter.nextUnsignedVLQ(); const bindings: [number, number, number][] = []; while (iter.hasNext() && iter.peek() !== ",") { bindings.push([ iter.nextUnsignedVLQ(), iter.nextUnsignedVLQ(), iter.nextUnsignedVLQ(), ]); } this.#recordGeneratedSubRangeBindingItem(variableIndex, bindings); break; } case Tag.GENERATED_RANGE_CALL_SITE: { this.#handleGeneratedRangeCallSite( iter.nextUnsignedVLQ(), iter.nextUnsignedVLQ(), iter.nextUnsignedVLQ(), ); break; } } // Consume any trailing VLQ and the the "," while (iter.hasNext() && iter.peek() !== ",") iter.nextUnsignedVLQ(); if (iter.hasNext()) iter.nextChar(); } if (iter.currentChar() === ",") { // Handle trailing EmptyItem. this.#scopes.push(null); } if (this.#scopeStack.length > 0) { this.#throwInStrictMode( "Encountered ORIGINAL_SCOPE_START without matching END!", ); } if (this.#rangeStack.length > 0) { this.#throwInStrictMode( "Encountered GENERATED_RANGE_START without matching END!", ); } const info = { scopes: this.#scopes, ranges: this.#ranges }; this.#scopes = []; this.#ranges = []; this.#flatOriginalScopes = []; return info; } #throwInStrictMode(message: string) { if (this.#mode === DecodeMode.STRICT) throw new Error(message); } #handleOriginalScopeStartItem(item: OriginalScopeStartItem) { this.#scopeState.line += item.line; if (item.line === 0) { this.#scopeState.column += item.column; } else { this.#scopeState.column = item.column; } const scope: OriginalScope = { start: { line: this.#scopeState.line, column: this.#scopeState.column }, end: { line: this.#scopeState.line, column: this.#scopeState.column }, isStackFrame: false, variables: [], children: [], }; if (item.nameIdx !== undefined) { this.#scopeState.name += item.nameIdx; scope.name = this.#resolveName(this.#scopeState.name); } if (item.kindIdx !== undefined) { this.#scopeState.kind += item.kindIdx; scope.kind = this.#resolveName(this.#scopeState.kind); } scope.isStackFrame = Boolean( item.flags & OriginalScopeFlags.IS_STACK_FRAME, ); this.#scopeStack.push(scope); this.#flatOriginalScopes.push(scope); } #handleOriginalScopeVariablesItem(variableIdxs: number[]) { const scope = this.#scopeStack.at(-1); if (!scope) { this.#throwInStrictMode( "Encountered ORIGINAL_SCOPE_VARIABLES without surrounding ORIGINAL_SCOPE_START", ); return; } for (const variableIdx of variableIdxs) { this.#scopeState.variable += variableIdx; scope.variables.push(this.#resolveName(this.#scopeState.variable)); } } #handleOriginalScopeEndItem(line: number, column: number) { this.#scopeState.line += line; if (line === 0) { this.#scopeState.column += column; } else { this.#scopeState.column = column; } const scope = this.#scopeStack.pop(); if (!scope) { this.#throwInStrictMode( "Encountered ORIGINAL_SCOPE_END without matching ORIGINAL_SCOPE_START!", ); return; } scope.end = { line: this.#scopeState.line, column: this.#scopeState.column, }; if (this.#scopeStack.length > 0) { const parent = this.#scopeStack.at(-1)!; scope.parent = parent; parent.children.push(scope); } else { this.#scopes.push(scope); this.#scopeState.line = 0; this.#scopeState.column = 0; } } #handleGeneratedRangeStartItem(item: GeneratedRangeStartItem) { if (item.line !== undefined) { this.#rangeState.line += item.line; this.#rangeState.column = item.column; } else { this.#rangeState.column += item.column; } const range: GeneratedRange = { start: { line: this.#rangeState.line, column: this.#rangeState.column, }, end: { line: this.#rangeState.line, column: this.#rangeState.column, }, isStackFrame: Boolean( item.flags & GeneratedRangeFlags.IS_STACK_FRAME, ), isHidden: Boolean(item.flags & GeneratedRangeFlags.IS_HIDDEN), values: [], children: [], }; if (item.definitionIdx !== undefined) { this.#rangeState.defScopeIdx += item.definitionIdx; if ( this.#rangeState.defScopeIdx < 0 || this.#rangeState.defScopeIdx >= this.#flatOriginalScopes.length ) { this.#throwInStrictMode("Invalid definition scope index"); } else { range.originalScope = this.#flatOriginalScopes[this.#rangeState.defScopeIdx]; } } this.#rangeStack.push(range); this.#subRangeBindingsForRange.clear(); } #handleGeneratedRangeBindingsItem(valueIdxs: number[]) { const range = this.#rangeStack.at(-1); if (!range) { this.#throwInStrictMode( "Encountered GENERATED_RANGE_BINDINGS without surrounding GENERATED_RANGE_START", ); return; } for (const valueIdx of valueIdxs) { if (valueIdx === 0) { range.values.push(null); } else { range.values.push(this.#resolveName(valueIdx - 1)); } } } #recordGeneratedSubRangeBindingItem( variableIndex: number, bindings: [number, number, number][], ) { if (this.#subRangeBindingsForRange.has(variableIndex)) { this.#throwInStrictMode( "Encountered multiple GENERATED_RANGE_SUBRANGE_BINDING items for the same variable", ); return; } this.#subRangeBindingsForRange.set(variableIndex, bindings); } #handleGeneratedRangeCallSite( sourceIndex: number, line: number, column: number, ) { const range = this.#rangeStack.at(-1); if (!range) { this.#throwInStrictMode( "Encountered GENERATED_RANGE_CALL_SITE without surrounding GENERATED_RANGE_START", ); return; } range.callSite = { sourceIndex, line, column, }; } #handleGeneratedRangeEndItem(line: number, column: number) { if (line !== 0) { this.#rangeState.line += line; this.#rangeState.column = column; } else { this.#rangeState.column += column; } const range = this.#rangeStack.pop(); if (!range) { this.#throwInStrictMode( "Encountered GENERATED_RANGE_END without matching GENERATED_RANGE_START!", ); return; } range.end = { line: this.#rangeState.line, column: this.#rangeState.column, }; this.#handleGeneratedRangeSubRangeBindings(range); if (this.#rangeStack.length > 0) { const parent = this.#rangeStack.at(-1)!; range.parent = parent; parent.children.push(range); } else { this.#ranges.push(range); } } #handleGeneratedRangeSubRangeBindings(range: GeneratedRange) { for (const [variableIndex, bindings] of this.#subRangeBindingsForRange) { const value = range.values[variableIndex]; const subRanges: SubRangeBinding[] = []; range.values[variableIndex] = subRanges; let lastLine = range.start.line; let lastColumn = range.start.column; subRanges.push({ from: { line: lastLine, column: lastColumn }, to: { line: 0, column: 0 }, value: value as string | undefined, }); for (const [binding, line, column] of bindings) { lastLine += line; if (line === 0) { lastColumn += column; } else { lastColumn = column; } subRanges.push({ from: { line: lastLine, column: lastColumn }, to: { line: 0, column: 0 }, // This will be fixed in the post-processing step. value: binding === 0 ? undefined : this.#resolveName(binding - 1), }); } } for (const value of range.values) { if (Array.isArray(value)) { const subRanges = value; for (let i = 0; i < subRanges.length - 1; ++i) { subRanges[i].to = subRanges[i + 1].from; } subRanges[subRanges.length - 1].to = range.end; } } } #resolveName(index: number): string { if (index < 0 || index >= this.#names.length) { this.#throwInStrictMode("Illegal index into the 'names' array"); } return this.#names[index] ?? ""; } }