TypeScript/src/harness/sourceMapRecorder.ts
2014-07-12 17:30:19 -07:00

461 lines
22 KiB
TypeScript

//
// 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.
//
///<reference path='harness.ts'/>
module Harness.SourceMapRecoder {
interface SourceMapSpanWithDecodeErrors {
sourceMapSpan: ts.SourceMapSpan;
decodeErrors: string[];
}
module SourceMapDecoder {
var sourceMapMappings: string;
var sourceMapNames: string[];
var decodingIndex: number;
var prevNameIndex: number;
var decodeOfEncodedMapping: ts.SourceMapSpan;
var 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));
}
var moreDigits = true;
var shiftCount = 0;
var value = 0;
for (; moreDigits; decodingIndex++) {
if (createErrorIfCondition(decodingIndex >= sourceMapMappings.length, "Error in decoding base64VLQFormatDecode, past the mapping string")) {
return;
}
// 6 digit number
var 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:
decodeOfEncodedMapping.nameIndex = -1;
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(true, "No encoded entry found");
}
export function hasCompletedDecoding() {
return decodingIndex === sourceMapMappings.length;
}
export function getRemainingDecodeString() {
return sourceMapMappings.substr(decodingIndex);
}
}
module SourceMapSpanWriter {
var sourceMapRecoder: Compiler.WriterAggregator;
var sourceMapSources: string[];
var sourceMapNames: string[];
var jsFile: Compiler.GeneratedFile;
var jsLineMap: number[];
var tsCode: string;
var tsLineMap: number[];
var spansOnSingleLine: SourceMapSpanWithDecodeErrors[];
var prevWrittenSourcePos: number;
var prevWrittenJsLine: number;
var spanMarkerContinues: boolean;
export function intializeSourceMapSpanWriter(sourceMapRecordWriter: Compiler.WriterAggregator, sourceMapData: ts.SourceMapData, currentJsFile: Compiler.GeneratedFile) {
sourceMapRecoder = sourceMapRecordWriter;
sourceMapSources = sourceMapData.sourceMapSources;
sourceMapNames = sourceMapData.sourceMapNames;
jsFile = currentJsFile;
jsLineMap = ts.getLineStarts(jsFile.code);
spansOnSingleLine = [];
prevWrittenSourcePos = 0;
prevWrittenJsLine = 0;
spanMarkerContinues = false;
SourceMapDecoder.initializeSourceMapDecoding(sourceMapData);
sourceMapRecoder.WriteLine("===================================================================");
sourceMapRecoder.WriteLine("JsFile: " + sourceMapData.sourceMapFile);
sourceMapRecoder.WriteLine("mapUrl: " + sourceMapData.jsSourceMappingURL);
sourceMapRecoder.WriteLine("sourceRoot: " + sourceMapData.sourceMapSourceRoot);
sourceMapRecoder.WriteLine("sources: " + sourceMapData.sourceMapSources);
sourceMapRecoder.WriteLine("===================================================================");
}
function getSourceMapSpanString(mapEntry: ts.SourceMapSpan, getAbsentNameIndex?: boolean) {
var 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 != -1 || getAbsentNameIndex) {
mapString += " nameIndex (" + mapEntry.nameIndex + ")";
}
}
return mapString;
}
export function recordSourceMapSpan(sourceMapSpan: ts.SourceMapSpan) {
// verify the decoded span is same as the new span
var decodeResult = SourceMapDecoder.decodeNextEncodedSourceMapSpan();
var decodedErrors: 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) {
decodedErrors = ["!!^^ !!^^ There was decoding error in the sourcemap at this location: " + decodeResult.error];
}
else {
decodedErrors = ["!!^^ !!^^ The decoded span from sourcemap's mapping entry does not match what was encoded for this span:"];
}
decodedErrors.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 = [{ sourceMapSpan: sourceMapSpan, decodeErrors: decodedErrors }];
}
else {
spansOnSingleLine.push({ sourceMapSpan: sourceMapSpan, decodeErrors: decodedErrors });
}
}
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);
sourceMapRecoder.WriteLine("-------------------------------------------------------------------");
sourceMapRecoder.WriteLine("emittedFile:" + jsFile.fileName);
sourceMapRecoder.WriteLine("sourceFile:" + sourceMapSources[spansOnSingleLine[0].sourceMapSpan.sourceIndex]);
sourceMapRecoder.WriteLine("-------------------------------------------------------------------");
tsLineMap = ts.getLineStarts(newSourceFileCode);
tsCode = newSourceFileCode;
prevWrittenSourcePos = 0;
}
export function close() {
// Write the lines pending on the single line
writeRecordedSpans();
if (!SourceMapDecoder.hasCompletedDecoding()) {
sourceMapRecoder.WriteLine("!!!! **** There are more source map entries in the sourceMap's mapping than what was encoded");
sourceMapRecoder.WriteLine("!!!! **** Remaining decoded string: " + SourceMapDecoder.getRemainingDecodeString());
}
// write remaining js lines
writeJsFileLines(jsLineMap.length);
}
function getTextOfLine(line: number, lineMap: number[], code: string) {
var startPos = lineMap[line];
var endPos = lineMap[line + 1];
return code.substring(startPos, endPos);
}
function writeJsFileLines(endJsLine: number) {
for (; prevWrittenJsLine < endJsLine; prevWrittenJsLine++) {
sourceMapRecoder.Write(">>>" + getTextOfLine(prevWrittenJsLine, jsLineMap, jsFile.code));
}
}
function writeRecordedSpans() {
function getMarkerId(markerIndex: number) {
var markerId = "";
if (spanMarkerContinues) {
assert.isTrue(markerIndex === 0);
markerId = "1->";
}
else {
var markerId = "" + (markerIndex + 1);
if (markerId.length < 2) {
markerId = markerId + " ";
}
markerId += ">";
}
return markerId;
}
var prevEmittedCol: number;
function iterateSpans(fn: (currentSpan: SourceMapSpanWithDecodeErrors, index: number) => void) {
prevEmittedCol = 1;
for (var i = 0; i < spansOnSingleLine.length; i++) {
fn(spansOnSingleLine[i], i);
prevEmittedCol = spansOnSingleLine[i].sourceMapSpan.emittedColumn;
}
}
function writeSourceMapIndent(indentLength: number, indentPrefix: string) {
sourceMapRecoder.Write(indentPrefix);
for (var i = 1; i < indentLength; i++) {
sourceMapRecoder.Write(" ");
}
}
function writeSourceMapMarker(currentSpan: SourceMapSpanWithDecodeErrors, index: number, endColumn = currentSpan.sourceMapSpan.emittedColumn, endContinues?: boolean) {
var markerId = getMarkerId(index);
markerIds.push(markerId);
writeSourceMapIndent(prevEmittedCol, markerId);
for (var i = prevEmittedCol; i < endColumn; i++) {
sourceMapRecoder.Write("^");
}
if (endContinues) {
sourceMapRecoder.Write("->");
}
sourceMapRecoder.WriteLine("");
spanMarkerContinues = endContinues;
}
function writeSourceMapSourceText(currentSpan: SourceMapSpanWithDecodeErrors, index: number) {
var sourcePos = tsLineMap[currentSpan.sourceMapSpan.sourceLine - 1] + (currentSpan.sourceMapSpan.sourceColumn - 1);
var 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 (var i = 0; i < currentSpan.decodeErrors.length; i++) {
writeSourceMapIndent(prevEmittedCol, markerIds[index]);
sourceMapRecoder.WriteLine(currentSpan.decodeErrors[i]);
}
}
var tsCodeLineMap = ts.getLineStarts(sourceText);
for (var i = 0; i < tsCodeLineMap.length; i++) {
writeSourceMapIndent(prevEmittedCol, i == 0 ? markerIds[index] : " >");
sourceMapRecoder.Write(getTextOfLine(i, tsCodeLineMap, sourceText));
if (i == tsCodeLineMap.length - 1) {
sourceMapRecoder.WriteLine("");
}
}
prevWrittenSourcePos = sourcePos;
}
function writeSpanDetails(currentSpan: SourceMapSpanWithDecodeErrors, index: number) {
sourceMapRecoder.WriteLine(markerIds[index] + getSourceMapSpanString(currentSpan.sourceMapSpan));
}
if (spansOnSingleLine.length) {
var currentJsLine = spansOnSingleLine[0].sourceMapSpan.emittedLine;
// Write js line
writeJsFileLines(currentJsLine);
// Emit markers
var markerIds: string[] = [];
iterateSpans(writeSourceMapMarker);
var jsFileText = getTextOfLine(currentJsLine, jsLineMap, jsFile.code);
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(undefined, spansOnSingleLine.length, /*endColumn*/ jsFileText.length, /*endContinues*/ true);
}
// Emit Source text
iterateSpans(writeSourceMapSourceText);
// Emit column number etc
iterateSpans(writeSpanDetails);
sourceMapRecoder.WriteLine("---");
}
}
}
export function getSourceMapRecord(sourceMapDataList: ts.SourceMapData[], program: ts.Program, jsFiles: Compiler.GeneratedFile[]) {
var sourceMapRecoder = new Compiler.WriterAggregator();
for (var i = 0; i < sourceMapDataList.length; i++) {
var sourceMapData = sourceMapDataList[i];
var prevSourceFile: ts.SourceFile = null;
SourceMapSpanWriter.intializeSourceMapSpanWriter(sourceMapRecoder, sourceMapData, jsFiles[i]);
for (var j = 0; j < sourceMapData.sourceMapDecodedMappings.length; j++) {
var decodedSourceMapping = sourceMapData.sourceMapDecodedMappings[j];
var 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
}
sourceMapRecoder.Close();
return sourceMapRecoder.lines.join('\r\n');
}
}