//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///
namespace Harness.SourceMapRecorder {
interface SourceMapSpanWithDecodeErrors {
sourceMapSpan: ts.SourceMapSpan;
decodeErrors: string[];
}
namespace SourceMapDecoder {
let sourceMapMappings: string;
let sourceMapNames: string[];
let decodingIndex: number;
let prevNameIndex: number;
let decodeOfEncodedMapping: ts.SourceMapSpan;
let errorDecodeOfEncodedMapping: string;
export function initializeSourceMapDecoding(sourceMapData: ts.SourceMapData) {
sourceMapMappings = sourceMapData.sourceMapMappings;
sourceMapNames = sourceMapData.sourceMapNames;
decodingIndex = 0;
prevNameIndex = 0;
decodeOfEncodedMapping = {
emittedLine: 1,
emittedColumn: 1,
sourceLine: 1,
sourceColumn: 1,
sourceIndex: 0,
};
errorDecodeOfEncodedMapping = undefined;
}
function isSourceMappingSegmentEnd() {
if (decodingIndex === sourceMapMappings.length) {
return true;
}
if (sourceMapMappings.charAt(decodingIndex) === ",") {
return true;
}
if (sourceMapMappings.charAt(decodingIndex) === ";") {
return true;
}
return false;
}
export function decodeNextEncodedSourceMapSpan() {
errorDecodeOfEncodedMapping = undefined;
function createErrorIfCondition(condition: boolean, errormsg: string) {
if (errorDecodeOfEncodedMapping) {
// there was existing error:
return true;
}
if (condition) {
errorDecodeOfEncodedMapping = errormsg;
}
return condition;
}
function base64VLQFormatDecode() {
function base64FormatDecode() {
return "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(sourceMapMappings.charAt(decodingIndex));
}
let moreDigits = true;
let shiftCount = 0;
let value = 0;
for (; moreDigits; decodingIndex++) {
if (createErrorIfCondition(decodingIndex >= sourceMapMappings.length, "Error in decoding base64VLQFormatDecode, past the mapping string")) {
return;
}
// 6 digit number
const currentByte = base64FormatDecode();
// 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;
}
while (decodingIndex < sourceMapMappings.length) {
if (sourceMapMappings.charAt(decodingIndex) === ";") {
// New line
decodeOfEncodedMapping.emittedLine++;
decodeOfEncodedMapping.emittedColumn = 1;
decodingIndex++;
continue;
}
if (sourceMapMappings.charAt(decodingIndex) === ",") {
// Next entry is on same line - no action needed
decodingIndex++;
continue;
}
// Read the current span
// 1. Column offset from prev read jsColumn
decodeOfEncodedMapping.emittedColumn += base64VLQFormatDecode();
// Incorrect emittedColumn dont support this map
if (createErrorIfCondition(decodeOfEncodedMapping.emittedColumn < 1, "Invalid emittedColumn found")) {
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
}
// Dont support reading mappings that dont have information about original source and its line numbers
if (createErrorIfCondition(isSourceMappingSegmentEnd(), "Unsupported Error Format: No entries after emitted column")) {
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
}
// 2. Relative sourceIndex
decodeOfEncodedMapping.sourceIndex += base64VLQFormatDecode();
// Incorrect sourceIndex dont support this map
if (createErrorIfCondition(decodeOfEncodedMapping.sourceIndex < 0, "Invalid sourceIndex found")) {
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
}
// Dont support reading mappings that dont have information about original source span
if (createErrorIfCondition(isSourceMappingSegmentEnd(), "Unsupported Error Format: No entries after sourceIndex")) {
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
}
// 3. Relative sourceLine 0 based
decodeOfEncodedMapping.sourceLine += base64VLQFormatDecode();
// Incorrect sourceLine dont support this map
if (createErrorIfCondition(decodeOfEncodedMapping.sourceLine < 1, "Invalid sourceLine found")) {
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
}
// Dont support reading mappings that dont have information about original source and its line numbers
if (createErrorIfCondition(isSourceMappingSegmentEnd(), "Unsupported Error Format: No entries after emitted Line")) {
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
}
// 4. Relative sourceColumn 0 based
decodeOfEncodedMapping.sourceColumn += base64VLQFormatDecode();
// Incorrect sourceColumn dont support this map
if (createErrorIfCondition(decodeOfEncodedMapping.sourceColumn < 1, "Invalid sourceLine found")) {
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
}
// 5. Check if there is name:
if (!isSourceMappingSegmentEnd()) {
prevNameIndex += base64VLQFormatDecode();
decodeOfEncodedMapping.nameIndex = prevNameIndex;
// Incorrect nameIndex dont support this map
if (createErrorIfCondition(decodeOfEncodedMapping.nameIndex < 0 || decodeOfEncodedMapping.nameIndex >= sourceMapNames.length, "Invalid name index for the source map entry")) {
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
}
}
// Dont support reading mappings that dont have information about original source and its line numbers
if (createErrorIfCondition(!isSourceMappingSegmentEnd(), "Unsupported Error Format: There are more entries after " + (decodeOfEncodedMapping.nameIndex === -1 ? "sourceColumn" : "nameIndex"))) {
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
}
// Populated the entry
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
}
createErrorIfCondition(/*condition*/ true, "No encoded entry found");
}
export function hasCompletedDecoding() {
return decodingIndex === sourceMapMappings.length;
}
export function getRemainingDecodeString() {
return sourceMapMappings.substr(decodingIndex);
}
}
namespace SourceMapSpanWriter {
let sourceMapRecorder: Compiler.WriterAggregator;
let sourceMapSources: string[];
let sourceMapNames: string[];
let jsFile: documents.TextDocument;
let jsLineMap: ReadonlyArray;
let tsCode: string;
let tsLineMap: number[];
let spansOnSingleLine: SourceMapSpanWithDecodeErrors[];
let prevWrittenSourcePos: number;
let prevWrittenJsLine: number;
let spanMarkerContinues: boolean;
export function initializeSourceMapSpanWriter(sourceMapRecordWriter: Compiler.WriterAggregator, sourceMapData: ts.SourceMapData, currentJsFile: documents.TextDocument) {
sourceMapRecorder = sourceMapRecordWriter;
sourceMapSources = sourceMapData.sourceMapSources;
sourceMapNames = sourceMapData.sourceMapNames;
jsFile = currentJsFile;
jsLineMap = jsFile.lineStarts;
spansOnSingleLine = [];
prevWrittenSourcePos = 0;
prevWrittenJsLine = 0;
spanMarkerContinues = false;
SourceMapDecoder.initializeSourceMapDecoding(sourceMapData);
sourceMapRecorder.WriteLine("===================================================================");
sourceMapRecorder.WriteLine("JsFile: " + sourceMapData.sourceMapFile);
sourceMapRecorder.WriteLine("mapUrl: " + sourceMapData.jsSourceMappingURL);
sourceMapRecorder.WriteLine("sourceRoot: " + sourceMapData.sourceMapSourceRoot);
sourceMapRecorder.WriteLine("sources: " + sourceMapData.sourceMapSources);
if (sourceMapData.sourceMapSourcesContent) {
sourceMapRecorder.WriteLine("sourcesContent: " + JSON.stringify(sourceMapData.sourceMapSourcesContent));
}
sourceMapRecorder.WriteLine("===================================================================");
}
function getSourceMapSpanString(mapEntry: ts.SourceMapSpan, getAbsentNameIndex?: boolean) {
let mapString = "Emitted(" + mapEntry.emittedLine + ", " + mapEntry.emittedColumn + ") Source(" + mapEntry.sourceLine + ", " + mapEntry.sourceColumn + ") + SourceIndex(" + mapEntry.sourceIndex + ")";
if (mapEntry.nameIndex >= 0 && mapEntry.nameIndex < sourceMapNames.length) {
mapString += " name (" + sourceMapNames[mapEntry.nameIndex] + ")";
}
else {
if ((mapEntry.nameIndex && mapEntry.nameIndex !== -1) || getAbsentNameIndex) {
mapString += " nameIndex (" + mapEntry.nameIndex + ")";
}
}
return mapString;
}
export function recordSourceMapSpan(sourceMapSpan: ts.SourceMapSpan) {
// verify the decoded span is same as the new span
const decodeResult = SourceMapDecoder.decodeNextEncodedSourceMapSpan();
let decodeErrors: string[];
if (decodeResult.error
|| decodeResult.sourceMapSpan.emittedLine !== sourceMapSpan.emittedLine
|| decodeResult.sourceMapSpan.emittedColumn !== sourceMapSpan.emittedColumn
|| decodeResult.sourceMapSpan.sourceLine !== sourceMapSpan.sourceLine
|| decodeResult.sourceMapSpan.sourceColumn !== sourceMapSpan.sourceColumn
|| decodeResult.sourceMapSpan.sourceIndex !== sourceMapSpan.sourceIndex
|| decodeResult.sourceMapSpan.nameIndex !== sourceMapSpan.nameIndex) {
if (decodeResult.error) {
decodeErrors = ["!!^^ !!^^ There was decoding error in the sourcemap at this location: " + decodeResult.error];
}
else {
decodeErrors = ["!!^^ !!^^ The decoded span from sourcemap's mapping entry does not match what was encoded for this span:"];
}
decodeErrors.push("!!^^ !!^^ Decoded span from sourcemap's mappings entry: " + getSourceMapSpanString(decodeResult.sourceMapSpan, /*getAbsentNameIndex*/ true) + " Span encoded by the emitter:" + getSourceMapSpanString(sourceMapSpan, /*getAbsentNameIndex*/ true));
}
if (spansOnSingleLine.length && spansOnSingleLine[0].sourceMapSpan.emittedLine !== sourceMapSpan.emittedLine) {
// On different line from the one that we have been recording till now,
writeRecordedSpans();
spansOnSingleLine = [];
}
spansOnSingleLine.push({ sourceMapSpan, decodeErrors });
}
export function recordNewSourceFileSpan(sourceMapSpan: ts.SourceMapSpan, newSourceFileCode: string) {
assert.isTrue(spansOnSingleLine.length === 0 || spansOnSingleLine[0].sourceMapSpan.emittedLine !== sourceMapSpan.emittedLine, "new file source map span should be on new line. We currently handle only that scenario");
recordSourceMapSpan(sourceMapSpan);
assert.isTrue(spansOnSingleLine.length === 1);
sourceMapRecorder.WriteLine("-------------------------------------------------------------------");
sourceMapRecorder.WriteLine("emittedFile:" + jsFile.file);
sourceMapRecorder.WriteLine("sourceFile:" + sourceMapSources[spansOnSingleLine[0].sourceMapSpan.sourceIndex]);
sourceMapRecorder.WriteLine("-------------------------------------------------------------------");
tsLineMap = ts.computeLineStarts(newSourceFileCode);
tsCode = newSourceFileCode;
prevWrittenSourcePos = 0;
}
export function close() {
// Write the lines pending on the single line
writeRecordedSpans();
if (!SourceMapDecoder.hasCompletedDecoding()) {
sourceMapRecorder.WriteLine("!!!! **** There are more source map entries in the sourceMap's mapping than what was encoded");
sourceMapRecorder.WriteLine("!!!! **** Remaining decoded string: " + SourceMapDecoder.getRemainingDecodeString());
}
// write remaining js lines
writeJsFileLines(jsLineMap.length);
}
function getTextOfLine(line: number, lineMap: ReadonlyArray, code: string) {
const startPos = lineMap[line];
const endPos = lineMap[line + 1];
const text = code.substring(startPos, endPos);
return line === 0 ? utils.removeByteOrderMark(text) : text;
}
function writeJsFileLines(endJsLine: number) {
for (; prevWrittenJsLine < endJsLine; prevWrittenJsLine++) {
sourceMapRecorder.Write(">>>" + getTextOfLine(prevWrittenJsLine, jsLineMap, jsFile.text));
}
}
function writeRecordedSpans() {
const markerIds: string[] = [];
function getMarkerId(markerIndex: number) {
let markerId = "";
if (spanMarkerContinues) {
assert.isTrue(markerIndex === 0);
markerId = "1->";
}
else {
markerId = "" + (markerIndex + 1);
if (markerId.length < 2) {
markerId = markerId + " ";
}
markerId += ">";
}
return markerId;
}
let prevEmittedCol: number;
function iterateSpans(fn: (currentSpan: SourceMapSpanWithDecodeErrors, index: number) => void) {
prevEmittedCol = 1;
for (let i = 0; i < spansOnSingleLine.length; i++) {
fn(spansOnSingleLine[i], i);
prevEmittedCol = spansOnSingleLine[i].sourceMapSpan.emittedColumn;
}
}
function writeSourceMapIndent(indentLength: number, indentPrefix: string) {
sourceMapRecorder.Write(indentPrefix);
for (let i = 1; i < indentLength; i++) {
sourceMapRecorder.Write(" ");
}
}
function writeSourceMapMarker(currentSpan: SourceMapSpanWithDecodeErrors, index: number, endColumn = currentSpan.sourceMapSpan.emittedColumn, endContinues?: boolean) {
const markerId = getMarkerId(index);
markerIds.push(markerId);
writeSourceMapIndent(prevEmittedCol, markerId);
for (let i = prevEmittedCol; i < endColumn; i++) {
sourceMapRecorder.Write("^");
}
if (endContinues) {
sourceMapRecorder.Write("->");
}
sourceMapRecorder.WriteLine("");
spanMarkerContinues = endContinues;
}
function writeSourceMapSourceText(currentSpan: SourceMapSpanWithDecodeErrors, index: number) {
const sourcePos = tsLineMap[currentSpan.sourceMapSpan.sourceLine - 1] + (currentSpan.sourceMapSpan.sourceColumn - 1);
let sourceText = "";
if (prevWrittenSourcePos < sourcePos) {
// Position that goes forward, get text
sourceText = tsCode.substring(prevWrittenSourcePos, sourcePos);
}
if (currentSpan.decodeErrors) {
// If there are decode errors, write
for (const decodeError of currentSpan.decodeErrors) {
writeSourceMapIndent(prevEmittedCol, markerIds[index]);
sourceMapRecorder.WriteLine(decodeError);
}
}
const tsCodeLineMap = ts.computeLineStarts(sourceText);
for (let i = 0; i < tsCodeLineMap.length; i++) {
writeSourceMapIndent(prevEmittedCol, i === 0 ? markerIds[index] : " >");
sourceMapRecorder.Write(getTextOfLine(i, tsCodeLineMap, sourceText));
if (i === tsCodeLineMap.length - 1) {
sourceMapRecorder.WriteLine("");
}
}
prevWrittenSourcePos = sourcePos;
}
function writeSpanDetails(currentSpan: SourceMapSpanWithDecodeErrors, index: number) {
sourceMapRecorder.WriteLine(markerIds[index] + getSourceMapSpanString(currentSpan.sourceMapSpan));
}
if (spansOnSingleLine.length) {
const currentJsLine = spansOnSingleLine[0].sourceMapSpan.emittedLine;
// Write js line
writeJsFileLines(currentJsLine);
// Emit markers
iterateSpans(writeSourceMapMarker);
const jsFileText = getTextOfLine(currentJsLine, jsLineMap, jsFile.text);
if (prevEmittedCol < jsFileText.length) {
// There is remaining text on this line that will be part of next source span so write marker that continues
writeSourceMapMarker(/*currentSpan*/ undefined, spansOnSingleLine.length, /*endColumn*/ jsFileText.length, /*endContinues*/ true);
}
// Emit Source text
iterateSpans(writeSourceMapSourceText);
// Emit column number etc
iterateSpans(writeSpanDetails);
sourceMapRecorder.WriteLine("---");
}
}
}
export function getSourceMapRecord(sourceMapDataList: ReadonlyArray, program: ts.Program, jsFiles: ReadonlyArray, declarationFiles: ReadonlyArray) {
const sourceMapRecorder = new Compiler.WriterAggregator();
for (let i = 0; i < sourceMapDataList.length; i++) {
const sourceMapData = sourceMapDataList[i];
let prevSourceFile: ts.SourceFile;
let currentFile: documents.TextDocument;
if (ts.endsWith(sourceMapData.sourceMapFile, ts.Extension.Dts)) {
if (sourceMapDataList.length > jsFiles.length) {
currentFile = declarationFiles[Math.floor(i / 2)]; // When both kinds of source map are present, they alternate js/dts
}
else {
currentFile = declarationFiles[i];
}
}
else {
if (sourceMapDataList.length > jsFiles.length) {
currentFile = jsFiles[Math.floor(i / 2)];
}
else {
currentFile = jsFiles[i];
}
}
SourceMapSpanWriter.initializeSourceMapSpanWriter(sourceMapRecorder, sourceMapData, currentFile);
for (const decodedSourceMapping of sourceMapData.sourceMapDecodedMappings) {
const currentSourceFile = program.getSourceFile(sourceMapData.inputSourceFileNames[decodedSourceMapping.sourceIndex]);
if (currentSourceFile !== prevSourceFile) {
SourceMapSpanWriter.recordNewSourceFileSpan(decodedSourceMapping, currentSourceFile.text);
prevSourceFile = currentSourceFile;
}
else {
SourceMapSpanWriter.recordSourceMapSpan(decodedSourceMapping);
}
}
SourceMapSpanWriter.close(); // If the last spans werent emitted, emit them
}
sourceMapRecorder.Close();
return sourceMapRecorder.lines.join("\r\n");
}
}