mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-10 06:41:59 -06:00
190 lines
7.1 KiB
TypeScript
190 lines
7.1 KiB
TypeScript
import * as Harness from "./_namespaces/Harness";
|
|
import * as ts from "./_namespaces/ts";
|
|
|
|
// NOTE: The contents of this file are all exported from the namespace 'documents'. This is to
|
|
// support the eventual conversion of harness into a modular system.
|
|
|
|
export class TextDocument {
|
|
public readonly meta: Map<string, string>;
|
|
public readonly file: string;
|
|
public readonly text: string;
|
|
|
|
private _lineStarts: readonly number[] | undefined;
|
|
private _testFile: Harness.Compiler.TestFile | undefined;
|
|
|
|
constructor(file: string, text: string, meta?: Map<string, string>) {
|
|
this.file = file;
|
|
this.text = text;
|
|
this.meta = meta || new Map<string, string>();
|
|
}
|
|
|
|
public get lineStarts(): readonly number[] {
|
|
return this._lineStarts || (this._lineStarts = ts.computeLineStarts(this.text));
|
|
}
|
|
|
|
public static fromTestFile(file: Harness.Compiler.TestFile) {
|
|
return new TextDocument(
|
|
file.unitName,
|
|
file.content,
|
|
file.fileOptions && Object.keys(file.fileOptions)
|
|
.reduce((meta, key) => meta.set(key, file.fileOptions[key]), new Map<string, string>()),
|
|
);
|
|
}
|
|
|
|
public asTestFile() {
|
|
return this._testFile || (this._testFile = {
|
|
unitName: this.file,
|
|
content: this.text,
|
|
fileOptions: ts.arrayFrom(this.meta)
|
|
.reduce((obj, [key, value]) => (obj[key] = value, obj), {} as Record<string, string>),
|
|
});
|
|
}
|
|
}
|
|
|
|
export interface RawSourceMap {
|
|
version: number;
|
|
file: string;
|
|
sourceRoot?: string;
|
|
sources: string[];
|
|
sourcesContent?: string[];
|
|
names: string[];
|
|
mappings: string;
|
|
}
|
|
|
|
export interface Mapping {
|
|
mappingIndex: number;
|
|
emittedLine: number;
|
|
emittedColumn: number;
|
|
sourceIndex: number;
|
|
sourceLine: number;
|
|
sourceColumn: number;
|
|
nameIndex?: number;
|
|
}
|
|
|
|
export class SourceMap {
|
|
public readonly raw: RawSourceMap;
|
|
public readonly mapFile: string | undefined;
|
|
public readonly version: number;
|
|
public readonly file: string;
|
|
public readonly sourceRoot: string | undefined;
|
|
public readonly sources: readonly string[] = [];
|
|
public readonly sourcesContent: readonly string[] | undefined;
|
|
public readonly mappings: readonly Mapping[] = [];
|
|
public readonly names: readonly string[] | undefined;
|
|
|
|
private static readonly _mappingRegExp = /([A-Za-z0-9+/]+),?|(;)|./g;
|
|
private static readonly _sourceMappingURLRegExp = /^\/\/[#@]\s*sourceMappingURL\s*=\s*(.*?)\s*$/mig;
|
|
private static readonly _dataURLRegExp = /^data:application\/json;base64,([a-z0-9+/=]+)$/i;
|
|
private static readonly _base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
|
|
private _emittedLineMappings: Mapping[][] = [];
|
|
private _sourceLineMappings: Mapping[][][] = [];
|
|
|
|
constructor(mapFile: string | undefined, data: string | RawSourceMap) {
|
|
this.raw = typeof data === "string" ? JSON.parse(data) as RawSourceMap : data;
|
|
this.mapFile = mapFile;
|
|
this.version = this.raw.version;
|
|
this.file = this.raw.file;
|
|
this.sourceRoot = this.raw.sourceRoot;
|
|
this.sources = this.raw.sources;
|
|
this.sourcesContent = this.raw.sourcesContent;
|
|
this.names = this.raw.names;
|
|
|
|
// populate mappings
|
|
const mappings: Mapping[] = [];
|
|
let emittedLine = 0;
|
|
let emittedColumn = 0;
|
|
let sourceIndex = 0;
|
|
let sourceLine = 0;
|
|
let sourceColumn = 0;
|
|
let nameIndex = 0;
|
|
let match: RegExpExecArray | null;
|
|
while (match = SourceMap._mappingRegExp.exec(this.raw.mappings)) {
|
|
if (match[1]) {
|
|
const segment = SourceMap._decodeVLQ(match[1]);
|
|
if (segment.length !== 1 && segment.length !== 4 && segment.length !== 5) {
|
|
throw new Error("Invalid VLQ");
|
|
}
|
|
|
|
emittedColumn += segment[0];
|
|
if (segment.length >= 4) {
|
|
sourceIndex += segment[1];
|
|
sourceLine += segment[2];
|
|
sourceColumn += segment[3];
|
|
}
|
|
|
|
const mapping: Mapping = { mappingIndex: mappings.length, emittedLine, emittedColumn, sourceIndex, sourceLine, sourceColumn };
|
|
if (segment.length === 5) {
|
|
nameIndex += segment[4];
|
|
mapping.nameIndex = nameIndex;
|
|
}
|
|
|
|
mappings.push(mapping);
|
|
|
|
const mappingsForEmittedLine = this._emittedLineMappings[mapping.emittedLine] || (this._emittedLineMappings[mapping.emittedLine] = []);
|
|
mappingsForEmittedLine.push(mapping);
|
|
|
|
const mappingsForSource = this._sourceLineMappings[mapping.sourceIndex] || (this._sourceLineMappings[mapping.sourceIndex] = []);
|
|
const mappingsForSourceLine = mappingsForSource[mapping.sourceLine] || (mappingsForSource[mapping.sourceLine] = []);
|
|
mappingsForSourceLine.push(mapping);
|
|
}
|
|
else if (match[2]) {
|
|
emittedLine++;
|
|
emittedColumn = 0;
|
|
}
|
|
else {
|
|
throw new Error(`Unrecognized character '${match[0]}'.`);
|
|
}
|
|
}
|
|
|
|
this.mappings = mappings;
|
|
}
|
|
|
|
public static getUrl(text: string) {
|
|
let match: RegExpExecArray | null;
|
|
let lastMatch: RegExpExecArray | undefined;
|
|
while (match = SourceMap._sourceMappingURLRegExp.exec(text)) {
|
|
lastMatch = match;
|
|
}
|
|
return lastMatch ? lastMatch[1] : undefined;
|
|
}
|
|
|
|
public static fromUrl(url: string) {
|
|
const match = SourceMap._dataURLRegExp.exec(url);
|
|
return match ? new SourceMap(/*mapFile*/ undefined, ts.sys.base64decode!(match[1])) : undefined;
|
|
}
|
|
|
|
public static fromSource(text: string): SourceMap | undefined {
|
|
const url = this.getUrl(text);
|
|
return url === undefined ? undefined : this.fromUrl(url);
|
|
}
|
|
|
|
public getMappingsForEmittedLine(emittedLine: number): readonly Mapping[] | undefined {
|
|
return this._emittedLineMappings[emittedLine];
|
|
}
|
|
|
|
public getMappingsForSourceLine(sourceIndex: number, sourceLine: number): readonly Mapping[] | undefined {
|
|
const mappingsForSource = this._sourceLineMappings[sourceIndex];
|
|
return mappingsForSource && mappingsForSource[sourceLine];
|
|
}
|
|
|
|
private static _decodeVLQ(text: string): number[] {
|
|
const vlq: number[] = [];
|
|
let shift = 0;
|
|
let value = 0;
|
|
for (let i = 0; i < text.length; i++) {
|
|
const currentByte = SourceMap._base64Chars.indexOf(text.charAt(i));
|
|
value += (currentByte & 31) << shift;
|
|
if ((currentByte & 32) === 0) {
|
|
vlq.push(value & 1 ? -(value >>> 1) : value >>> 1);
|
|
shift = 0;
|
|
value = 0;
|
|
}
|
|
else {
|
|
shift += 5;
|
|
}
|
|
}
|
|
return vlq;
|
|
}
|
|
}
|