mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-15 03:23:08 -06:00
* fix(sourcemap): accept a sourceMappingURL that ends with a newline * Update src/compiler/sourcemap.ts Co-authored-by: Wesley Wigham <wwigham@gmail.com>
761 lines
32 KiB
TypeScript
761 lines
32 KiB
TypeScript
/* @internal */
|
|
namespace ts {
|
|
export interface SourceMapGeneratorOptions {
|
|
extendedDiagnostics?: boolean;
|
|
}
|
|
|
|
export function createSourceMapGenerator(host: EmitHost, file: string, sourceRoot: string, sourcesDirectoryPath: string, generatorOptions: SourceMapGeneratorOptions): SourceMapGenerator {
|
|
const { enter, exit } = generatorOptions.extendedDiagnostics
|
|
? performance.createTimer("Source Map", "beforeSourcemap", "afterSourcemap")
|
|
: performance.nullTimer;
|
|
|
|
// Current source map file and its index in the sources list
|
|
const rawSources: string[] = [];
|
|
const sources: string[] = [];
|
|
const sourceToSourceIndexMap = new Map<string, number>();
|
|
let sourcesContent: (string | null)[] | undefined;
|
|
|
|
const names: string[] = [];
|
|
let nameToNameIndexMap: ESMap<string, number> | undefined;
|
|
const mappingCharCodes: number[] = [];
|
|
let mappings = "";
|
|
|
|
// Last recorded and encoded mappings
|
|
let lastGeneratedLine = 0;
|
|
let lastGeneratedCharacter = 0;
|
|
let lastSourceIndex = 0;
|
|
let lastSourceLine = 0;
|
|
let lastSourceCharacter = 0;
|
|
let lastNameIndex = 0;
|
|
let hasLast = false;
|
|
|
|
let pendingGeneratedLine = 0;
|
|
let pendingGeneratedCharacter = 0;
|
|
let pendingSourceIndex = 0;
|
|
let pendingSourceLine = 0;
|
|
let pendingSourceCharacter = 0;
|
|
let pendingNameIndex = 0;
|
|
let hasPending = false;
|
|
let hasPendingSource = false;
|
|
let hasPendingName = false;
|
|
|
|
return {
|
|
getSources: () => rawSources,
|
|
addSource,
|
|
setSourceContent,
|
|
addName,
|
|
addMapping,
|
|
appendSourceMap,
|
|
toJSON,
|
|
toString: () => JSON.stringify(toJSON())
|
|
};
|
|
|
|
function addSource(fileName: string) {
|
|
enter();
|
|
const source = getRelativePathToDirectoryOrUrl(sourcesDirectoryPath,
|
|
fileName,
|
|
host.getCurrentDirectory(),
|
|
host.getCanonicalFileName,
|
|
/*isAbsolutePathAnUrl*/ true);
|
|
|
|
let sourceIndex = sourceToSourceIndexMap.get(source);
|
|
if (sourceIndex === undefined) {
|
|
sourceIndex = sources.length;
|
|
sources.push(source);
|
|
rawSources.push(fileName);
|
|
sourceToSourceIndexMap.set(source, sourceIndex);
|
|
}
|
|
exit();
|
|
return sourceIndex;
|
|
}
|
|
|
|
/* eslint-disable boolean-trivia, no-null/no-null */
|
|
function setSourceContent(sourceIndex: number, content: string | null) {
|
|
enter();
|
|
if (content !== null) {
|
|
if (!sourcesContent) sourcesContent = [];
|
|
while (sourcesContent.length < sourceIndex) {
|
|
sourcesContent.push(null);
|
|
}
|
|
sourcesContent[sourceIndex] = content;
|
|
}
|
|
exit();
|
|
}
|
|
/* eslint-enable boolean-trivia, no-null/no-null */
|
|
|
|
function addName(name: string) {
|
|
enter();
|
|
if (!nameToNameIndexMap) nameToNameIndexMap = new Map();
|
|
let nameIndex = nameToNameIndexMap.get(name);
|
|
if (nameIndex === undefined) {
|
|
nameIndex = names.length;
|
|
names.push(name);
|
|
nameToNameIndexMap.set(name, nameIndex);
|
|
}
|
|
exit();
|
|
return nameIndex;
|
|
}
|
|
|
|
function isNewGeneratedPosition(generatedLine: number, generatedCharacter: number) {
|
|
return !hasPending
|
|
|| pendingGeneratedLine !== generatedLine
|
|
|| pendingGeneratedCharacter !== generatedCharacter;
|
|
}
|
|
|
|
function isBacktrackingSourcePosition(sourceIndex: number | undefined, sourceLine: number | undefined, sourceCharacter: number | undefined) {
|
|
return sourceIndex !== undefined
|
|
&& sourceLine !== undefined
|
|
&& sourceCharacter !== undefined
|
|
&& pendingSourceIndex === sourceIndex
|
|
&& (pendingSourceLine > sourceLine
|
|
|| pendingSourceLine === sourceLine && pendingSourceCharacter > sourceCharacter);
|
|
}
|
|
|
|
function addMapping(generatedLine: number, generatedCharacter: number, sourceIndex?: number, sourceLine?: number, sourceCharacter?: number, nameIndex?: number) {
|
|
Debug.assert(generatedLine >= pendingGeneratedLine, "generatedLine cannot backtrack");
|
|
Debug.assert(generatedCharacter >= 0, "generatedCharacter cannot be negative");
|
|
Debug.assert(sourceIndex === undefined || sourceIndex >= 0, "sourceIndex cannot be negative");
|
|
Debug.assert(sourceLine === undefined || sourceLine >= 0, "sourceLine cannot be negative");
|
|
Debug.assert(sourceCharacter === undefined || sourceCharacter >= 0, "sourceCharacter cannot be negative");
|
|
enter();
|
|
// If this location wasn't recorded or the location in source is going backwards, record the mapping
|
|
if (isNewGeneratedPosition(generatedLine, generatedCharacter) ||
|
|
isBacktrackingSourcePosition(sourceIndex, sourceLine, sourceCharacter)) {
|
|
commitPendingMapping();
|
|
pendingGeneratedLine = generatedLine;
|
|
pendingGeneratedCharacter = generatedCharacter;
|
|
hasPendingSource = false;
|
|
hasPendingName = false;
|
|
hasPending = true;
|
|
}
|
|
|
|
if (sourceIndex !== undefined && sourceLine !== undefined && sourceCharacter !== undefined) {
|
|
pendingSourceIndex = sourceIndex;
|
|
pendingSourceLine = sourceLine;
|
|
pendingSourceCharacter = sourceCharacter;
|
|
hasPendingSource = true;
|
|
if (nameIndex !== undefined) {
|
|
pendingNameIndex = nameIndex;
|
|
hasPendingName = true;
|
|
}
|
|
}
|
|
exit();
|
|
}
|
|
|
|
function appendSourceMap(generatedLine: number, generatedCharacter: number, map: RawSourceMap, sourceMapPath: string, start?: LineAndCharacter, end?: LineAndCharacter) {
|
|
Debug.assert(generatedLine >= pendingGeneratedLine, "generatedLine cannot backtrack");
|
|
Debug.assert(generatedCharacter >= 0, "generatedCharacter cannot be negative");
|
|
enter();
|
|
// First, decode the old component sourcemap
|
|
const sourceIndexToNewSourceIndexMap: number[] = [];
|
|
let nameIndexToNewNameIndexMap: number[] | undefined;
|
|
const mappingIterator = decodeMappings(map.mappings);
|
|
for (let iterResult = mappingIterator.next(); !iterResult.done; iterResult = mappingIterator.next()) {
|
|
const raw = iterResult.value;
|
|
if (end && (
|
|
raw.generatedLine > end.line ||
|
|
(raw.generatedLine === end.line && raw.generatedCharacter > end.character))) {
|
|
break;
|
|
}
|
|
|
|
if (start && (
|
|
raw.generatedLine < start.line ||
|
|
(start.line === raw.generatedLine && raw.generatedCharacter < start.character))) {
|
|
continue;
|
|
}
|
|
// Then reencode all the updated mappings into the overall map
|
|
let newSourceIndex: number | undefined;
|
|
let newSourceLine: number | undefined;
|
|
let newSourceCharacter: number | undefined;
|
|
let newNameIndex: number | undefined;
|
|
if (raw.sourceIndex !== undefined) {
|
|
newSourceIndex = sourceIndexToNewSourceIndexMap[raw.sourceIndex];
|
|
if (newSourceIndex === undefined) {
|
|
// Apply offsets to each position and fixup source entries
|
|
const rawPath = map.sources[raw.sourceIndex];
|
|
const relativePath = map.sourceRoot ? combinePaths(map.sourceRoot, rawPath) : rawPath;
|
|
const combinedPath = combinePaths(getDirectoryPath(sourceMapPath), relativePath);
|
|
sourceIndexToNewSourceIndexMap[raw.sourceIndex] = newSourceIndex = addSource(combinedPath);
|
|
if (map.sourcesContent && typeof map.sourcesContent[raw.sourceIndex] === "string") {
|
|
setSourceContent(newSourceIndex, map.sourcesContent[raw.sourceIndex]);
|
|
}
|
|
}
|
|
|
|
newSourceLine = raw.sourceLine;
|
|
newSourceCharacter = raw.sourceCharacter;
|
|
if (map.names && raw.nameIndex !== undefined) {
|
|
if (!nameIndexToNewNameIndexMap) nameIndexToNewNameIndexMap = [];
|
|
newNameIndex = nameIndexToNewNameIndexMap[raw.nameIndex];
|
|
if (newNameIndex === undefined) {
|
|
nameIndexToNewNameIndexMap[raw.nameIndex] = newNameIndex = addName(map.names[raw.nameIndex]);
|
|
}
|
|
}
|
|
}
|
|
|
|
const rawGeneratedLine = raw.generatedLine - (start ? start.line : 0);
|
|
const newGeneratedLine = rawGeneratedLine + generatedLine;
|
|
const rawGeneratedCharacter = start && start.line === raw.generatedLine ? raw.generatedCharacter - start.character : raw.generatedCharacter;
|
|
const newGeneratedCharacter = rawGeneratedLine === 0 ? rawGeneratedCharacter + generatedCharacter : rawGeneratedCharacter;
|
|
addMapping(newGeneratedLine, newGeneratedCharacter, newSourceIndex, newSourceLine, newSourceCharacter, newNameIndex);
|
|
}
|
|
exit();
|
|
}
|
|
|
|
function shouldCommitMapping() {
|
|
return !hasLast
|
|
|| lastGeneratedLine !== pendingGeneratedLine
|
|
|| lastGeneratedCharacter !== pendingGeneratedCharacter
|
|
|| lastSourceIndex !== pendingSourceIndex
|
|
|| lastSourceLine !== pendingSourceLine
|
|
|| lastSourceCharacter !== pendingSourceCharacter
|
|
|| lastNameIndex !== pendingNameIndex;
|
|
}
|
|
|
|
function appendMappingCharCode(charCode: number) {
|
|
mappingCharCodes.push(charCode);
|
|
// String.fromCharCode accepts its arguments on the stack, so we have to chunk the input,
|
|
// otherwise we can get stack overflows for large source maps
|
|
if (mappingCharCodes.length >= 1024) {
|
|
flushMappingBuffer();
|
|
}
|
|
}
|
|
|
|
function commitPendingMapping() {
|
|
if (!hasPending || !shouldCommitMapping()) {
|
|
return;
|
|
}
|
|
|
|
enter();
|
|
|
|
// Line/Comma delimiters
|
|
if (lastGeneratedLine < pendingGeneratedLine) {
|
|
// Emit line delimiters
|
|
do {
|
|
appendMappingCharCode(CharacterCodes.semicolon);
|
|
lastGeneratedLine++;
|
|
}
|
|
while (lastGeneratedLine < pendingGeneratedLine);
|
|
// Only need to set this once
|
|
lastGeneratedCharacter = 0;
|
|
}
|
|
else {
|
|
Debug.assertEqual(lastGeneratedLine, pendingGeneratedLine, "generatedLine cannot backtrack");
|
|
// Emit comma to separate the entry
|
|
if (hasLast) {
|
|
appendMappingCharCode(CharacterCodes.comma);
|
|
}
|
|
}
|
|
|
|
// 1. Relative generated character
|
|
appendBase64VLQ(pendingGeneratedCharacter - lastGeneratedCharacter);
|
|
lastGeneratedCharacter = pendingGeneratedCharacter;
|
|
|
|
if (hasPendingSource) {
|
|
// 2. Relative sourceIndex
|
|
appendBase64VLQ(pendingSourceIndex - lastSourceIndex);
|
|
lastSourceIndex = pendingSourceIndex;
|
|
|
|
// 3. Relative source line
|
|
appendBase64VLQ(pendingSourceLine - lastSourceLine);
|
|
lastSourceLine = pendingSourceLine;
|
|
|
|
// 4. Relative source character
|
|
appendBase64VLQ(pendingSourceCharacter - lastSourceCharacter);
|
|
lastSourceCharacter = pendingSourceCharacter;
|
|
|
|
if (hasPendingName) {
|
|
// 5. Relative nameIndex
|
|
appendBase64VLQ(pendingNameIndex - lastNameIndex);
|
|
lastNameIndex = pendingNameIndex;
|
|
}
|
|
}
|
|
|
|
hasLast = true;
|
|
exit();
|
|
}
|
|
|
|
function flushMappingBuffer(): void {
|
|
if (mappingCharCodes.length > 0) {
|
|
mappings += String.fromCharCode.apply(undefined, mappingCharCodes);
|
|
mappingCharCodes.length = 0;
|
|
}
|
|
}
|
|
|
|
function toJSON(): RawSourceMap {
|
|
commitPendingMapping();
|
|
flushMappingBuffer();
|
|
return {
|
|
version: 3,
|
|
file,
|
|
sourceRoot,
|
|
sources,
|
|
names,
|
|
mappings,
|
|
sourcesContent,
|
|
};
|
|
}
|
|
|
|
function appendBase64VLQ(inValue: number): void {
|
|
// Add a new least significant bit that has the sign of the value.
|
|
// if negative number the least significant bit that gets added to the number has value 1
|
|
// else least significant bit value that gets added is 0
|
|
// eg. -1 changes to binary : 01 [1] => 3
|
|
// +1 changes to binary : 01 [0] => 2
|
|
if (inValue < 0) {
|
|
inValue = ((-inValue) << 1) + 1;
|
|
}
|
|
else {
|
|
inValue = inValue << 1;
|
|
}
|
|
|
|
// Encode 5 bits at a time starting from least significant bits
|
|
do {
|
|
let currentDigit = inValue & 31; // 11111
|
|
inValue = inValue >> 5;
|
|
if (inValue > 0) {
|
|
// There are still more digits to decode, set the msb (6th bit)
|
|
currentDigit = currentDigit | 32;
|
|
}
|
|
appendMappingCharCode(base64FormatEncode(currentDigit));
|
|
} while (inValue > 0);
|
|
}
|
|
}
|
|
|
|
// Sometimes tools can see the following line as a source mapping url comment, so we mangle it a bit (the [M])
|
|
const sourceMapCommentRegExp = /^\/\/[@#] source[M]appingURL=(.+)\r?\n?$/;
|
|
|
|
const whitespaceOrMapCommentRegExp = /^\s*(\/\/[@#] .*)?$/;
|
|
|
|
export interface LineInfo {
|
|
getLineCount(): number;
|
|
getLineText(line: number): string;
|
|
}
|
|
|
|
export function getLineInfo(text: string, lineStarts: readonly number[]): LineInfo {
|
|
return {
|
|
getLineCount: () => lineStarts.length,
|
|
getLineText: line => text.substring(lineStarts[line], lineStarts[line + 1])
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Tries to find the sourceMappingURL comment at the end of a file.
|
|
*/
|
|
export function tryGetSourceMappingURL(lineInfo: LineInfo) {
|
|
for (let index = lineInfo.getLineCount() - 1; index >= 0; index--) {
|
|
const line = lineInfo.getLineText(index);
|
|
const comment = sourceMapCommentRegExp.exec(line);
|
|
if (comment) {
|
|
return trimStringEnd(comment[1]);
|
|
}
|
|
// If we see a non-whitespace/map comment-like line, break, to avoid scanning up the entire file
|
|
else if (!line.match(whitespaceOrMapCommentRegExp)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* eslint-disable no-null/no-null */
|
|
function isStringOrNull(x: any) {
|
|
return typeof x === "string" || x === null;
|
|
}
|
|
|
|
export function isRawSourceMap(x: any): x is RawSourceMap {
|
|
return x !== null
|
|
&& typeof x === "object"
|
|
&& x.version === 3
|
|
&& typeof x.file === "string"
|
|
&& typeof x.mappings === "string"
|
|
&& isArray(x.sources) && every(x.sources, isString)
|
|
&& (x.sourceRoot === undefined || x.sourceRoot === null || typeof x.sourceRoot === "string")
|
|
&& (x.sourcesContent === undefined || x.sourcesContent === null || isArray(x.sourcesContent) && every(x.sourcesContent, isStringOrNull))
|
|
&& (x.names === undefined || x.names === null || isArray(x.names) && every(x.names, isString));
|
|
}
|
|
/* eslint-enable no-null/no-null */
|
|
|
|
export function tryParseRawSourceMap(text: string) {
|
|
try {
|
|
const parsed = JSON.parse(text);
|
|
if (isRawSourceMap(parsed)) {
|
|
return parsed;
|
|
}
|
|
}
|
|
catch {
|
|
// empty
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export interface MappingsDecoder extends Iterator<Mapping> {
|
|
readonly pos: number;
|
|
readonly error: string | undefined;
|
|
readonly state: Required<Mapping>;
|
|
}
|
|
|
|
export interface Mapping {
|
|
generatedLine: number;
|
|
generatedCharacter: number;
|
|
sourceIndex?: number;
|
|
sourceLine?: number;
|
|
sourceCharacter?: number;
|
|
nameIndex?: number;
|
|
}
|
|
|
|
export interface SourceMapping extends Mapping {
|
|
sourceIndex: number;
|
|
sourceLine: number;
|
|
sourceCharacter: number;
|
|
}
|
|
|
|
export function decodeMappings(mappings: string): MappingsDecoder {
|
|
let done = false;
|
|
let pos = 0;
|
|
let generatedLine = 0;
|
|
let generatedCharacter = 0;
|
|
let sourceIndex = 0;
|
|
let sourceLine = 0;
|
|
let sourceCharacter = 0;
|
|
let nameIndex = 0;
|
|
let error: string | undefined;
|
|
|
|
return {
|
|
get pos() { return pos; },
|
|
get error() { return error; },
|
|
get state() { return captureMapping(/*hasSource*/ true, /*hasName*/ true); },
|
|
next() {
|
|
while (!done && pos < mappings.length) {
|
|
const ch = mappings.charCodeAt(pos);
|
|
if (ch === CharacterCodes.semicolon) {
|
|
// new line
|
|
generatedLine++;
|
|
generatedCharacter = 0;
|
|
pos++;
|
|
continue;
|
|
}
|
|
|
|
if (ch === CharacterCodes.comma) {
|
|
// Next entry is on same line - no action needed
|
|
pos++;
|
|
continue;
|
|
}
|
|
|
|
let hasSource = false;
|
|
let hasName = false;
|
|
|
|
generatedCharacter += base64VLQFormatDecode();
|
|
if (hasReportedError()) return stopIterating();
|
|
if (generatedCharacter < 0) return setErrorAndStopIterating("Invalid generatedCharacter found");
|
|
|
|
if (!isSourceMappingSegmentEnd()) {
|
|
hasSource = true;
|
|
|
|
sourceIndex += base64VLQFormatDecode();
|
|
if (hasReportedError()) return stopIterating();
|
|
if (sourceIndex < 0) return setErrorAndStopIterating("Invalid sourceIndex found");
|
|
if (isSourceMappingSegmentEnd()) return setErrorAndStopIterating("Unsupported Format: No entries after sourceIndex");
|
|
|
|
sourceLine += base64VLQFormatDecode();
|
|
if (hasReportedError()) return stopIterating();
|
|
if (sourceLine < 0) return setErrorAndStopIterating("Invalid sourceLine found");
|
|
if (isSourceMappingSegmentEnd()) return setErrorAndStopIterating("Unsupported Format: No entries after sourceLine");
|
|
|
|
sourceCharacter += base64VLQFormatDecode();
|
|
if (hasReportedError()) return stopIterating();
|
|
if (sourceCharacter < 0) return setErrorAndStopIterating("Invalid sourceCharacter found");
|
|
|
|
if (!isSourceMappingSegmentEnd()) {
|
|
hasName = true;
|
|
nameIndex += base64VLQFormatDecode();
|
|
if (hasReportedError()) return stopIterating();
|
|
if (nameIndex < 0) return setErrorAndStopIterating("Invalid nameIndex found");
|
|
|
|
if (!isSourceMappingSegmentEnd()) return setErrorAndStopIterating("Unsupported Error Format: Entries after nameIndex");
|
|
}
|
|
}
|
|
|
|
return { value: captureMapping(hasSource, hasName), done };
|
|
}
|
|
|
|
return stopIterating();
|
|
}
|
|
};
|
|
|
|
function captureMapping(hasSource: true, hasName: true): Required<Mapping>;
|
|
function captureMapping(hasSource: boolean, hasName: boolean): Mapping;
|
|
function captureMapping(hasSource: boolean, hasName: boolean): Mapping {
|
|
return {
|
|
generatedLine,
|
|
generatedCharacter,
|
|
sourceIndex: hasSource ? sourceIndex : undefined,
|
|
sourceLine: hasSource ? sourceLine : undefined,
|
|
sourceCharacter: hasSource ? sourceCharacter : undefined,
|
|
nameIndex: hasName ? nameIndex : undefined
|
|
};
|
|
}
|
|
|
|
function stopIterating(): { value: never, done: true } {
|
|
done = true;
|
|
return { value: undefined!, done: true };
|
|
}
|
|
|
|
function setError(message: string) {
|
|
if (error === undefined) {
|
|
error = message;
|
|
}
|
|
}
|
|
|
|
function setErrorAndStopIterating(message: string) {
|
|
setError(message);
|
|
return stopIterating();
|
|
}
|
|
|
|
function hasReportedError() {
|
|
return error !== undefined;
|
|
}
|
|
|
|
function isSourceMappingSegmentEnd() {
|
|
return (pos === mappings.length ||
|
|
mappings.charCodeAt(pos) === CharacterCodes.comma ||
|
|
mappings.charCodeAt(pos) === CharacterCodes.semicolon);
|
|
}
|
|
|
|
function base64VLQFormatDecode(): number {
|
|
let moreDigits = true;
|
|
let shiftCount = 0;
|
|
let value = 0;
|
|
|
|
for (; moreDigits; pos++) {
|
|
if (pos >= mappings.length) return setError("Error in decoding base64VLQFormatDecode, past the mapping string"), -1;
|
|
|
|
// 6 digit number
|
|
const currentByte = base64FormatDecode(mappings.charCodeAt(pos));
|
|
if (currentByte === -1) return setError("Invalid character in VLQ"), -1;
|
|
|
|
// If msb is set, we still have more bits to continue
|
|
moreDigits = (currentByte & 32) !== 0;
|
|
|
|
// least significant 5 bits are the next msbs in the final value.
|
|
value = value | ((currentByte & 31) << shiftCount);
|
|
shiftCount += 5;
|
|
}
|
|
|
|
// Least significant bit if 1 represents negative and rest of the msb is actual absolute value
|
|
if ((value & 1) === 0) {
|
|
// + number
|
|
value = value >> 1;
|
|
}
|
|
else {
|
|
// - number
|
|
value = value >> 1;
|
|
value = -value;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
}
|
|
|
|
export function sameMapping<T extends Mapping>(left: T, right: T) {
|
|
return left === right
|
|
|| left.generatedLine === right.generatedLine
|
|
&& left.generatedCharacter === right.generatedCharacter
|
|
&& left.sourceIndex === right.sourceIndex
|
|
&& left.sourceLine === right.sourceLine
|
|
&& left.sourceCharacter === right.sourceCharacter
|
|
&& left.nameIndex === right.nameIndex;
|
|
}
|
|
|
|
export function isSourceMapping(mapping: Mapping): mapping is SourceMapping {
|
|
return mapping.sourceIndex !== undefined
|
|
&& mapping.sourceLine !== undefined
|
|
&& mapping.sourceCharacter !== undefined;
|
|
}
|
|
|
|
function base64FormatEncode(value: number) {
|
|
return value >= 0 && value < 26 ? CharacterCodes.A + value :
|
|
value >= 26 && value < 52 ? CharacterCodes.a + value - 26 :
|
|
value >= 52 && value < 62 ? CharacterCodes._0 + value - 52 :
|
|
value === 62 ? CharacterCodes.plus :
|
|
value === 63 ? CharacterCodes.slash :
|
|
Debug.fail(`${value}: not a base64 value`);
|
|
}
|
|
|
|
function base64FormatDecode(ch: number) {
|
|
return ch >= CharacterCodes.A && ch <= CharacterCodes.Z ? ch - CharacterCodes.A :
|
|
ch >= CharacterCodes.a && ch <= CharacterCodes.z ? ch - CharacterCodes.a + 26 :
|
|
ch >= CharacterCodes._0 && ch <= CharacterCodes._9 ? ch - CharacterCodes._0 + 52 :
|
|
ch === CharacterCodes.plus ? 62 :
|
|
ch === CharacterCodes.slash ? 63 :
|
|
-1;
|
|
}
|
|
|
|
interface MappedPosition {
|
|
generatedPosition: number;
|
|
source: string | undefined;
|
|
sourceIndex: number | undefined;
|
|
sourcePosition: number | undefined;
|
|
nameIndex: number | undefined;
|
|
}
|
|
|
|
interface SourceMappedPosition extends MappedPosition {
|
|
source: string;
|
|
sourceIndex: number;
|
|
sourcePosition: number;
|
|
}
|
|
|
|
function isSourceMappedPosition(value: MappedPosition): value is SourceMappedPosition {
|
|
return value.sourceIndex !== undefined
|
|
&& value.sourcePosition !== undefined;
|
|
}
|
|
|
|
function sameMappedPosition(left: MappedPosition, right: MappedPosition) {
|
|
return left.generatedPosition === right.generatedPosition
|
|
&& left.sourceIndex === right.sourceIndex
|
|
&& left.sourcePosition === right.sourcePosition;
|
|
}
|
|
|
|
function compareSourcePositions(left: SourceMappedPosition, right: SourceMappedPosition) {
|
|
// Compares sourcePosition without comparing sourceIndex
|
|
// since the mappings are grouped by sourceIndex
|
|
Debug.assert(left.sourceIndex === right.sourceIndex);
|
|
return compareValues(left.sourcePosition, right.sourcePosition);
|
|
}
|
|
|
|
function compareGeneratedPositions(left: MappedPosition, right: MappedPosition) {
|
|
return compareValues(left.generatedPosition, right.generatedPosition);
|
|
}
|
|
|
|
function getSourcePositionOfMapping(value: SourceMappedPosition) {
|
|
return value.sourcePosition;
|
|
}
|
|
|
|
function getGeneratedPositionOfMapping(value: MappedPosition) {
|
|
return value.generatedPosition;
|
|
}
|
|
|
|
export function createDocumentPositionMapper(host: DocumentPositionMapperHost, map: RawSourceMap, mapPath: string): DocumentPositionMapper {
|
|
const mapDirectory = getDirectoryPath(mapPath);
|
|
const sourceRoot = map.sourceRoot ? getNormalizedAbsolutePath(map.sourceRoot, mapDirectory) : mapDirectory;
|
|
const generatedAbsoluteFilePath = getNormalizedAbsolutePath(map.file, mapDirectory);
|
|
const generatedFile = host.getSourceFileLike(generatedAbsoluteFilePath);
|
|
const sourceFileAbsolutePaths = map.sources.map(source => getNormalizedAbsolutePath(source, sourceRoot));
|
|
const sourceToSourceIndexMap = new Map(sourceFileAbsolutePaths.map((source, i) => [host.getCanonicalFileName(source), i]));
|
|
let decodedMappings: readonly MappedPosition[] | undefined;
|
|
let generatedMappings: SortedReadonlyArray<MappedPosition> | undefined;
|
|
let sourceMappings: readonly SortedReadonlyArray<SourceMappedPosition>[] | undefined;
|
|
|
|
return {
|
|
getSourcePosition,
|
|
getGeneratedPosition
|
|
};
|
|
|
|
function processMapping(mapping: Mapping): MappedPosition {
|
|
const generatedPosition = generatedFile !== undefined
|
|
? getPositionOfLineAndCharacter(generatedFile, mapping.generatedLine, mapping.generatedCharacter, /*allowEdits*/ true)
|
|
: -1;
|
|
let source: string | undefined;
|
|
let sourcePosition: number | undefined;
|
|
if (isSourceMapping(mapping)) {
|
|
const sourceFile = host.getSourceFileLike(sourceFileAbsolutePaths[mapping.sourceIndex]);
|
|
source = map.sources[mapping.sourceIndex];
|
|
sourcePosition = sourceFile !== undefined
|
|
? getPositionOfLineAndCharacter(sourceFile, mapping.sourceLine, mapping.sourceCharacter, /*allowEdits*/ true)
|
|
: -1;
|
|
}
|
|
return {
|
|
generatedPosition,
|
|
source,
|
|
sourceIndex: mapping.sourceIndex,
|
|
sourcePosition,
|
|
nameIndex: mapping.nameIndex
|
|
};
|
|
}
|
|
|
|
function getDecodedMappings() {
|
|
if (decodedMappings === undefined) {
|
|
const decoder = decodeMappings(map.mappings);
|
|
const mappings = arrayFrom(decoder, processMapping);
|
|
if (decoder.error !== undefined) {
|
|
if (host.log) {
|
|
host.log(`Encountered error while decoding sourcemap: ${decoder.error}`);
|
|
}
|
|
decodedMappings = emptyArray;
|
|
}
|
|
else {
|
|
decodedMappings = mappings;
|
|
}
|
|
}
|
|
return decodedMappings;
|
|
}
|
|
|
|
function getSourceMappings(sourceIndex: number) {
|
|
if (sourceMappings === undefined) {
|
|
const lists: SourceMappedPosition[][] = [];
|
|
for (const mapping of getDecodedMappings()) {
|
|
if (!isSourceMappedPosition(mapping)) continue;
|
|
let list = lists[mapping.sourceIndex];
|
|
if (!list) lists[mapping.sourceIndex] = list = [];
|
|
list.push(mapping);
|
|
}
|
|
sourceMappings = lists.map(list => sortAndDeduplicate<SourceMappedPosition>(list, compareSourcePositions, sameMappedPosition));
|
|
}
|
|
return sourceMappings[sourceIndex];
|
|
}
|
|
|
|
function getGeneratedMappings() {
|
|
if (generatedMappings === undefined) {
|
|
const list: MappedPosition[] = [];
|
|
for (const mapping of getDecodedMappings()) {
|
|
list.push(mapping);
|
|
}
|
|
generatedMappings = sortAndDeduplicate(list, compareGeneratedPositions, sameMappedPosition);
|
|
}
|
|
return generatedMappings;
|
|
}
|
|
|
|
function getGeneratedPosition(loc: DocumentPosition): DocumentPosition {
|
|
const sourceIndex = sourceToSourceIndexMap.get(host.getCanonicalFileName(loc.fileName));
|
|
if (sourceIndex === undefined) return loc;
|
|
|
|
const sourceMappings = getSourceMappings(sourceIndex);
|
|
if (!some(sourceMappings)) return loc;
|
|
|
|
let targetIndex = binarySearchKey(sourceMappings, loc.pos, getSourcePositionOfMapping, compareValues);
|
|
if (targetIndex < 0) {
|
|
// if no exact match, closest is 2's complement of result
|
|
targetIndex = ~targetIndex;
|
|
}
|
|
|
|
const mapping = sourceMappings[targetIndex];
|
|
if (mapping === undefined || mapping.sourceIndex !== sourceIndex) {
|
|
return loc;
|
|
}
|
|
|
|
return { fileName: generatedAbsoluteFilePath, pos: mapping.generatedPosition }; // Closest pos
|
|
}
|
|
|
|
function getSourcePosition(loc: DocumentPosition): DocumentPosition {
|
|
const generatedMappings = getGeneratedMappings();
|
|
if (!some(generatedMappings)) return loc;
|
|
|
|
let targetIndex = binarySearchKey(generatedMappings, loc.pos, getGeneratedPositionOfMapping, compareValues);
|
|
if (targetIndex < 0) {
|
|
// if no exact match, closest is 2's complement of result
|
|
targetIndex = ~targetIndex;
|
|
}
|
|
|
|
const mapping = generatedMappings[targetIndex];
|
|
if (mapping === undefined || !isSourceMappedPosition(mapping)) {
|
|
return loc;
|
|
}
|
|
|
|
return { fileName: sourceFileAbsolutePaths[mapping.sourceIndex], pos: mapping.sourcePosition }; // Closest pos
|
|
}
|
|
}
|
|
|
|
export const identitySourceMapConsumer: DocumentPositionMapper = {
|
|
getSourcePosition: identity,
|
|
getGeneratedPosition: identity
|
|
};
|
|
}
|