• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace Harness.SourceMapRecorder {
2
3    interface SourceMapSpanWithDecodeErrors {
4        sourceMapSpan: ts.Mapping;
5        decodeErrors: string[] | undefined;
6    }
7
8    namespace SourceMapDecoder {
9        let sourceMapMappings: string;
10        let decodingIndex: number;
11        let mappings: ts.MappingsDecoder | undefined;
12
13        export interface DecodedMapping {
14            sourceMapSpan: ts.Mapping;
15            error?: string;
16        }
17
18        export function initializeSourceMapDecoding(sourceMap: ts.RawSourceMap) {
19            decodingIndex = 0;
20            sourceMapMappings = sourceMap.mappings;
21            mappings = ts.decodeMappings(sourceMap.mappings);
22        }
23
24        export function decodeNextEncodedSourceMapSpan(): DecodedMapping {
25            if (!mappings) return ts.Debug.fail("not initialized");
26            const result = mappings.next();
27            if (result.done) return { error: mappings.error || "No encoded entry found", sourceMapSpan: mappings.state };
28            return { sourceMapSpan: result.value };
29        }
30
31        export function hasCompletedDecoding() {
32            if (!mappings) return ts.Debug.fail("not initialized");
33            return mappings.pos === sourceMapMappings.length;
34        }
35
36        export function getRemainingDecodeString() {
37            return sourceMapMappings.substr(decodingIndex);
38        }
39    }
40
41    namespace SourceMapSpanWriter {
42        let sourceMapRecorder: Compiler.WriterAggregator;
43        let sourceMapSources: string[];
44        let sourceMapNames: string[] | null | undefined;
45
46        let jsFile: documents.TextDocument;
47        let jsLineMap: readonly number[];
48        let tsCode: string;
49        let tsLineMap: number[];
50
51        let spansOnSingleLine: SourceMapSpanWithDecodeErrors[];
52        let prevWrittenSourcePos: number;
53        let nextJsLineToWrite: number;
54        let spanMarkerContinues: boolean;
55
56        export function initializeSourceMapSpanWriter(sourceMapRecordWriter: Compiler.WriterAggregator, sourceMap: ts.RawSourceMap, currentJsFile: documents.TextDocument) {
57            sourceMapRecorder = sourceMapRecordWriter;
58            sourceMapSources = sourceMap.sources;
59            sourceMapNames = sourceMap.names;
60
61            jsFile = currentJsFile;
62            jsLineMap = jsFile.lineStarts;
63
64            spansOnSingleLine = [];
65            prevWrittenSourcePos = 0;
66            nextJsLineToWrite = 0;
67            spanMarkerContinues = false;
68
69            SourceMapDecoder.initializeSourceMapDecoding(sourceMap);
70            sourceMapRecorder.WriteLine("===================================================================");
71            sourceMapRecorder.WriteLine("JsFile: " + sourceMap.file);
72            sourceMapRecorder.WriteLine("mapUrl: " + ts.tryGetSourceMappingURL(ts.getLineInfo(jsFile.text, jsLineMap)));
73            sourceMapRecorder.WriteLine("sourceRoot: " + sourceMap.sourceRoot);
74            sourceMapRecorder.WriteLine("sources: " + sourceMap.sources);
75            if (sourceMap.sourcesContent) {
76                sourceMapRecorder.WriteLine("sourcesContent: " + JSON.stringify(sourceMap.sourcesContent));
77            }
78            sourceMapRecorder.WriteLine("===================================================================");
79        }
80
81        function getSourceMapSpanString(mapEntry: ts.Mapping, getAbsentNameIndex?: boolean) {
82            let mapString = "Emitted(" + (mapEntry.generatedLine + 1) + ", " + (mapEntry.generatedCharacter + 1) + ")";
83            if (ts.isSourceMapping(mapEntry)) {
84                mapString += " Source(" + (mapEntry.sourceLine + 1) + ", " + (mapEntry.sourceCharacter + 1) + ") + SourceIndex(" + mapEntry.sourceIndex + ")";
85                if (mapEntry.nameIndex! >= 0 && mapEntry.nameIndex! < sourceMapNames!.length) {
86                    mapString += " name (" + sourceMapNames![mapEntry.nameIndex!] + ")";
87                }
88                else {
89                    if ((mapEntry.nameIndex && mapEntry.nameIndex !== -1) || getAbsentNameIndex) {
90                        mapString += " nameIndex (" + mapEntry.nameIndex + ")";
91                    }
92                }
93            }
94
95            return mapString;
96        }
97
98        export function recordSourceMapSpan(sourceMapSpan: ts.Mapping) {
99            // verify the decoded span is same as the new span
100            const decodeResult = SourceMapDecoder.decodeNextEncodedSourceMapSpan();
101            let decodeErrors: string[] | undefined;
102            if (typeof decodeResult.error === "string" || !ts.sameMapping(decodeResult.sourceMapSpan, sourceMapSpan)) {
103                if (decodeResult.error) {
104                    decodeErrors = ["!!^^ !!^^ There was decoding error in the sourcemap at this location: " + decodeResult.error];
105                }
106                else {
107                    decodeErrors = ["!!^^ !!^^ The decoded span from sourcemap's mapping entry does not match what was encoded for this span:"];
108                }
109                decodeErrors.push("!!^^ !!^^ Decoded span from sourcemap's mappings entry: " + getSourceMapSpanString(decodeResult.sourceMapSpan, /*getAbsentNameIndex*/ true) + " Span encoded by the emitter:" + getSourceMapSpanString(sourceMapSpan, /*getAbsentNameIndex*/ true));
110            }
111
112            if (spansOnSingleLine.length && spansOnSingleLine[0].sourceMapSpan.generatedLine !== sourceMapSpan.generatedLine) {
113                // On different line from the one that we have been recording till now,
114                writeRecordedSpans();
115                spansOnSingleLine = [];
116            }
117            spansOnSingleLine.push({ sourceMapSpan, decodeErrors });
118        }
119
120        export function recordNewSourceFileSpan(sourceMapSpan: ts.Mapping, newSourceFileCode: string) {
121            let continuesLine = false;
122            if (spansOnSingleLine.length > 0 && spansOnSingleLine[0].sourceMapSpan.generatedCharacter === sourceMapSpan.generatedLine) {
123                writeRecordedSpans();
124                spansOnSingleLine = [];
125                nextJsLineToWrite--; // walk back one line to reprint the line
126                continuesLine = true;
127            }
128
129            recordSourceMapSpan(sourceMapSpan);
130
131            assert.isTrue(spansOnSingleLine.length === 1);
132            sourceMapRecorder.WriteLine("-------------------------------------------------------------------");
133            sourceMapRecorder.WriteLine("emittedFile:" + jsFile.file + (continuesLine ? ` (${sourceMapSpan.generatedLine + 1}, ${sourceMapSpan.generatedCharacter + 1})` : ""));
134            sourceMapRecorder.WriteLine("sourceFile:" + sourceMapSources[spansOnSingleLine[0].sourceMapSpan.sourceIndex!]);
135            sourceMapRecorder.WriteLine("-------------------------------------------------------------------");
136
137            tsLineMap = ts.computeLineStarts(newSourceFileCode);
138            tsCode = newSourceFileCode;
139            prevWrittenSourcePos = 0;
140        }
141
142        export function close() {
143            // Write the lines pending on the single line
144            writeRecordedSpans();
145
146            if (!SourceMapDecoder.hasCompletedDecoding()) {
147                sourceMapRecorder.WriteLine("!!!! **** There are more source map entries in the sourceMap's mapping than what was encoded");
148                sourceMapRecorder.WriteLine("!!!! **** Remaining decoded string: " + SourceMapDecoder.getRemainingDecodeString());
149
150            }
151
152            // write remaining js lines
153            writeJsFileLines(jsLineMap.length);
154        }
155
156        function getTextOfLine(line: number, lineMap: readonly number[], code: string) {
157            const startPos = lineMap[line];
158            const endPos = lineMap[line + 1];
159            const text = code.substring(startPos, endPos);
160            return line === 0 ? Utils.removeByteOrderMark(text) : text;
161        }
162
163        function writeJsFileLines(endJsLine: number) {
164            for (; nextJsLineToWrite < endJsLine; nextJsLineToWrite++) {
165                sourceMapRecorder.Write(">>>" + getTextOfLine(nextJsLineToWrite, jsLineMap, jsFile.text));
166            }
167        }
168
169        function writeRecordedSpans() {
170            const markerIds: string[] = [];
171
172            function getMarkerId(markerIndex: number) {
173                let markerId = "";
174                if (spanMarkerContinues) {
175                    assert.isTrue(markerIndex === 0);
176                    markerId = "1->";
177                }
178                else {
179                    markerId = "" + (markerIndex + 1);
180                    if (markerId.length < 2) {
181                        markerId = markerId + " ";
182                    }
183                    markerId += ">";
184                }
185                return markerId;
186            }
187
188            let prevEmittedCol!: number;
189            function iterateSpans(fn: (currentSpan: SourceMapSpanWithDecodeErrors, index: number) => void) {
190                prevEmittedCol = 0;
191                for (let i = 0; i < spansOnSingleLine.length; i++) {
192                    fn(spansOnSingleLine[i], i);
193                    prevEmittedCol = spansOnSingleLine[i].sourceMapSpan.generatedCharacter;
194                }
195            }
196
197            function writeSourceMapIndent(indentLength: number, indentPrefix: string) {
198                sourceMapRecorder.Write(indentPrefix);
199                for (let i = 0; i < indentLength; i++) {
200                    sourceMapRecorder.Write(" ");
201                }
202            }
203
204            function writeSourceMapMarker(currentSpan: SourceMapSpanWithDecodeErrors, index: number, endColumn = currentSpan.sourceMapSpan.generatedCharacter, endContinues = false) {
205                const markerId = getMarkerId(index);
206                markerIds.push(markerId);
207
208                writeSourceMapIndent(prevEmittedCol, markerId);
209
210                for (let i = prevEmittedCol; i < endColumn; i++) {
211                    sourceMapRecorder.Write("^");
212                }
213                if (endContinues) {
214                    sourceMapRecorder.Write("->");
215                }
216                sourceMapRecorder.WriteLine("");
217                spanMarkerContinues = endContinues;
218            }
219
220            function writeSourceMapSourceText(currentSpan: SourceMapSpanWithDecodeErrors, index: number) {
221                const sourcePos = tsLineMap[currentSpan.sourceMapSpan.sourceLine!] + (currentSpan.sourceMapSpan.sourceCharacter!);
222                let sourceText = "";
223                if (prevWrittenSourcePos < sourcePos) {
224                    // Position that goes forward, get text
225                    sourceText = tsCode.substring(prevWrittenSourcePos, sourcePos);
226                }
227
228                if (currentSpan.decodeErrors) {
229                    // If there are decode errors, write
230                    for (const decodeError of currentSpan.decodeErrors) {
231                        writeSourceMapIndent(prevEmittedCol, markerIds[index]);
232                        sourceMapRecorder.WriteLine(decodeError);
233                    }
234                }
235
236                const tsCodeLineMap = ts.computeLineStarts(sourceText);
237                for (let i = 0; i < tsCodeLineMap.length; i++) {
238                    writeSourceMapIndent(prevEmittedCol, i === 0 ? markerIds[index] : "  >");
239                    sourceMapRecorder.Write(getTextOfLine(i, tsCodeLineMap, sourceText));
240                    if (i === tsCodeLineMap.length - 1) {
241                        sourceMapRecorder.WriteLine("");
242                    }
243                }
244
245                prevWrittenSourcePos = sourcePos;
246            }
247
248            function writeSpanDetails(currentSpan: SourceMapSpanWithDecodeErrors, index: number) {
249                sourceMapRecorder.WriteLine(markerIds[index] + getSourceMapSpanString(currentSpan.sourceMapSpan));
250            }
251
252            if (spansOnSingleLine.length) {
253                const currentJsLine = spansOnSingleLine[0].sourceMapSpan.generatedLine;
254
255                // Write js line
256                writeJsFileLines(currentJsLine + 1);
257
258                // Emit markers
259                iterateSpans(writeSourceMapMarker);
260
261                const jsFileText = getTextOfLine(currentJsLine + 1, jsLineMap, jsFile.text);
262                if (prevEmittedCol < jsFileText.length - 1) {
263                    // There is remaining text on this line that will be part of next source span so write marker that continues
264                    writeSourceMapMarker(/*currentSpan*/ undefined!, spansOnSingleLine.length, /*endColumn*/ jsFileText.length - 1, /*endContinues*/ true); // TODO: GH#18217
265                }
266
267                // Emit Source text
268                iterateSpans(writeSourceMapSourceText);
269
270                // Emit column number etc
271                iterateSpans(writeSpanDetails);
272
273                sourceMapRecorder.WriteLine("---");
274            }
275        }
276    }
277
278    export function getSourceMapRecord(sourceMapDataList: readonly ts.SourceMapEmitResult[], program: ts.Program, jsFiles: readonly documents.TextDocument[], declarationFiles: readonly documents.TextDocument[]) {
279        const sourceMapRecorder = new Compiler.WriterAggregator();
280
281        for (let i = 0; i < sourceMapDataList.length; i++) {
282            const sourceMapData = sourceMapDataList[i];
283            let prevSourceFile: ts.SourceFile | undefined;
284            let currentFile: documents.TextDocument;
285            if (ts.isDeclarationFileName(sourceMapData.sourceMap.file)) {
286                if (sourceMapDataList.length > jsFiles.length) {
287                    currentFile = declarationFiles[Math.floor(i / 2)]; // When both kinds of source map are present, they alternate js/dts
288                }
289                else {
290                    currentFile = declarationFiles[i];
291                }
292            }
293            else {
294                if (sourceMapDataList.length > jsFiles.length) {
295                    currentFile = jsFiles[Math.floor(i / 2)];
296                }
297                else {
298                    currentFile = jsFiles[i];
299                }
300            }
301
302            SourceMapSpanWriter.initializeSourceMapSpanWriter(sourceMapRecorder, sourceMapData.sourceMap, currentFile);
303            const mapper = ts.decodeMappings(sourceMapData.sourceMap.mappings);
304            for (let iterResult = mapper.next(); !iterResult.done; iterResult = mapper.next()) {
305                const decodedSourceMapping = iterResult.value;
306                const currentSourceFile = ts.isSourceMapping(decodedSourceMapping)
307                    ? program.getSourceFile(sourceMapData.inputSourceFileNames[decodedSourceMapping.sourceIndex])
308                    : undefined;
309                if (currentSourceFile !== prevSourceFile) {
310                    if (currentSourceFile) {
311                        SourceMapSpanWriter.recordNewSourceFileSpan(decodedSourceMapping, currentSourceFile.text);
312                    }
313                    prevSourceFile = currentSourceFile;
314                }
315                else {
316                    SourceMapSpanWriter.recordSourceMapSpan(decodedSourceMapping);
317                }
318            }
319            SourceMapSpanWriter.close(); // If the last spans werent emitted, emit them
320        }
321        sourceMapRecorder.Close();
322        return sourceMapRecorder.lines.join("\r\n");
323    }
324
325    export function getSourceMapRecordWithSystem(sys: ts.System, sourceMapFile: string) {
326        const sourceMapRecorder = new Compiler.WriterAggregator();
327        let prevSourceFile: documents.TextDocument | undefined;
328        const files = new ts.Map<string, documents.TextDocument>();
329        const sourceMap = ts.tryParseRawSourceMap(sys.readFile(sourceMapFile, "utf8")!);
330        if (sourceMap) {
331            const mapDirectory = ts.getDirectoryPath(sourceMapFile);
332            const sourceRoot = sourceMap.sourceRoot ? ts.getNormalizedAbsolutePath(sourceMap.sourceRoot, mapDirectory) : mapDirectory;
333            const generatedAbsoluteFilePath = ts.getNormalizedAbsolutePath(sourceMap.file, mapDirectory);
334            const sourceFileAbsolutePaths = sourceMap.sources.map(source => ts.getNormalizedAbsolutePath(source, sourceRoot));
335            const currentFile = getFile(generatedAbsoluteFilePath);
336
337            SourceMapSpanWriter.initializeSourceMapSpanWriter(sourceMapRecorder, sourceMap, currentFile);
338            const mapper = ts.decodeMappings(sourceMap.mappings);
339            for (let iterResult = mapper.next(); !iterResult.done; iterResult = mapper.next()) {
340                const decodedSourceMapping = iterResult.value;
341                const currentSourceFile = ts.isSourceMapping(decodedSourceMapping)
342                    ? getFile(sourceFileAbsolutePaths[decodedSourceMapping.sourceIndex])
343                    : undefined;
344                if (currentSourceFile !== prevSourceFile) {
345                    if (currentSourceFile) {
346                        SourceMapSpanWriter.recordNewSourceFileSpan(decodedSourceMapping, currentSourceFile.text);
347                    }
348                    prevSourceFile = currentSourceFile;
349                }
350                else {
351                    SourceMapSpanWriter.recordSourceMapSpan(decodedSourceMapping);
352                }
353            }
354            SourceMapSpanWriter.close(); // If the last spans werent emitted, emit them
355        }
356        sourceMapRecorder.Close();
357        return sourceMapRecorder.lines.join("\r\n");
358
359        function getFile(path: string) {
360            const existing = files.get(path);
361            if (existing) return existing;
362            const value = new documents.TextDocument(path, sys.readFile(path, "utf8")!);
363            files.set(path, value);
364            return value;
365        }
366    }
367}
368