/
source_file.ts
531 lines (487 loc) Β· 18.7 KB
/
source_file.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import mapHelpers from 'convert-source-map';
import {decode, encode, SourceMapMappings, SourceMapSegment} from 'sourcemap-codec';
import {AbsoluteFsPath, PathManipulation} from '../../file_system';
import {RawSourceMap, SourceMapInfo} from './raw_source_map';
import {compareSegments, offsetSegment, SegmentMarker} from './segment_marker';
export function removeSourceMapComments(contents: string): string {
return mapHelpers.removeMapFileComments(mapHelpers.removeComments(contents))
.replace(/\n\n$/, '\n');
}
export class SourceFile {
/**
* The parsed mappings that have been flattened so that any intermediate source mappings have been
* flattened.
*
* The result is that any source file mentioned in the flattened mappings have no source map (are
* pure original source files).
*/
readonly flattenedMappings: Mapping[];
readonly startOfLinePositions: number[];
constructor(
/** The path to this source file. */
readonly sourcePath: AbsoluteFsPath,
/** The contents of this source file. */
readonly contents: string,
/** The raw source map (if any) referenced by this source file. */
readonly rawMap: SourceMapInfo|null,
/** Any source files referenced by the raw source map associated with this source file. */
readonly sources: (SourceFile|null)[],
private fs: PathManipulation,
) {
this.contents = removeSourceMapComments(contents);
this.startOfLinePositions = computeStartOfLinePositions(this.contents);
this.flattenedMappings = this.flattenMappings();
}
/**
* Render the raw source map generated from the flattened mappings.
*/
renderFlattenedSourceMap(): RawSourceMap {
const sources = new IndexedMap<string, string>();
const names = new IndexedSet<string>();
const mappings: SourceMapMappings = [];
const sourcePathDir = this.fs.dirname(this.sourcePath);
// Computing the relative path can be expensive, and we are likely to have the same path for
// many (if not all!) mappings.
const relativeSourcePathCache =
new Cache<string, string>(input => this.fs.relative(sourcePathDir, input));
for (const mapping of this.flattenedMappings) {
const sourceIndex = sources.set(
relativeSourcePathCache.get(mapping.originalSource.sourcePath),
mapping.originalSource.contents);
const mappingArray: SourceMapSegment = [
mapping.generatedSegment.column,
sourceIndex,
mapping.originalSegment.line,
mapping.originalSegment.column,
];
if (mapping.name !== undefined) {
const nameIndex = names.add(mapping.name);
mappingArray.push(nameIndex);
}
// Ensure a mapping line array for this mapping.
const line = mapping.generatedSegment.line;
while (line >= mappings.length) {
mappings.push([]);
}
// Add this mapping to the line
mappings[line].push(mappingArray);
}
const sourceMap: RawSourceMap = {
version: 3,
file: this.fs.relative(sourcePathDir, this.sourcePath),
sources: sources.keys,
names: names.values,
mappings: encode(mappings),
sourcesContent: sources.values,
};
return sourceMap;
}
/**
* Find the original mapped location for the given `line` and `column` in the generated file.
*
* First we search for a mapping whose generated segment is at or directly before the given
* location. Then we compute the offset between the given location and the matching generated
* segment. Finally we apply this offset to the original source segment to get the desired
* original location.
*/
getOriginalLocation(line: number, column: number):
{file: AbsoluteFsPath, line: number, column: number}|null {
if (this.flattenedMappings.length === 0) {
return null;
}
let position: number;
if (line < this.startOfLinePositions.length) {
position = this.startOfLinePositions[line] + column;
} else {
// The line is off the end of the file, so just assume we are at the end of the file.
position = this.contents.length;
}
const locationSegment: SegmentMarker = {line, column, position, next: undefined};
let mappingIndex =
findLastMappingIndexBefore(this.flattenedMappings, locationSegment, false, 0);
if (mappingIndex < 0) {
mappingIndex = 0;
}
const {originalSegment, originalSource, generatedSegment} =
this.flattenedMappings[mappingIndex];
const offset = locationSegment.position - generatedSegment.position;
const offsetOriginalSegment =
offsetSegment(originalSource.startOfLinePositions, originalSegment, offset);
return {
file: originalSource.sourcePath,
line: offsetOriginalSegment.line,
column: offsetOriginalSegment.column,
};
}
/**
* Flatten the parsed mappings for this source file, so that all the mappings are to pure original
* source files with no transitive source maps.
*/
private flattenMappings(): Mapping[] {
const mappings =
parseMappings(this.rawMap && this.rawMap.map, this.sources, this.startOfLinePositions);
ensureOriginalSegmentLinks(mappings);
const flattenedMappings: Mapping[] = [];
for (let mappingIndex = 0; mappingIndex < mappings.length; mappingIndex++) {
const aToBmapping = mappings[mappingIndex];
const bSource = aToBmapping.originalSource;
if (bSource.flattenedMappings.length === 0) {
// The b source file has no mappings of its own (i.e. it is a pure original file)
// so just use the mapping as-is.
flattenedMappings.push(aToBmapping);
continue;
}
// The `incomingStart` and `incomingEnd` are the `SegmentMarker`s in `B` that represent the
// section of `B` source file that is being mapped to by the current `aToBmapping`.
//
// For example, consider the mappings from A to B:
//
// src A src B mapping
//
// a ----- a [0, 0]
// b b
// f - /- c [4, 2]
// g \ / d
// c -/\ e
// d \- f [2, 5]
// e
//
// For mapping [0,0] the incoming start and end are 0 and 2 (i.e. the range a, b, c)
// For mapping [4,2] the incoming start and end are 2 and 5 (i.e. the range c, d, e, f)
//
const incomingStart = aToBmapping.originalSegment;
const incomingEnd = incomingStart.next;
// The `outgoingStartIndex` and `outgoingEndIndex` are the indices of the range of mappings
// that leave `b` that we are interested in merging with the aToBmapping.
// We actually care about all the markers from the last bToCmapping directly before the
// `incomingStart` to the last bToCmaping directly before the `incomingEnd`, inclusive.
//
// For example, if we consider the range 2 to 5 from above (i.e. c, d, e, f) with the
// following mappings from B to C:
//
// src B src C mapping
// a
// b ----- b [1, 0]
// - c c
// | d d
// | e ----- 1 [4, 3]
// - f \ 2
// \ 3
// \- e [4, 6]
//
// The range with `incomingStart` at 2 and `incomingEnd` at 5 has outgoing start mapping of
// [1,0] and outgoing end mapping of [4, 6], which also includes [4, 3].
//
let outgoingStartIndex =
findLastMappingIndexBefore(bSource.flattenedMappings, incomingStart, false, 0);
if (outgoingStartIndex < 0) {
outgoingStartIndex = 0;
}
const outgoingEndIndex = incomingEnd !== undefined ?
findLastMappingIndexBefore(
bSource.flattenedMappings, incomingEnd, true, outgoingStartIndex) :
bSource.flattenedMappings.length - 1;
for (let bToCmappingIndex = outgoingStartIndex; bToCmappingIndex <= outgoingEndIndex;
bToCmappingIndex++) {
const bToCmapping: Mapping = bSource.flattenedMappings[bToCmappingIndex];
flattenedMappings.push(mergeMappings(this, aToBmapping, bToCmapping));
}
}
return flattenedMappings;
}
}
/**
*
* @param mappings The collection of mappings whose segment-markers we are searching.
* @param marker The segment-marker to match against those of the given `mappings`.
* @param exclusive If exclusive then we must find a mapping with a segment-marker that is
* exclusively earlier than the given `marker`.
* If not exclusive then we can return the highest mappings with an equivalent segment-marker to the
* given `marker`.
* @param lowerIndex If provided, this is used as a hint that the marker we are searching for has an
* index that is no lower than this.
*/
export function findLastMappingIndexBefore(
mappings: Mapping[], marker: SegmentMarker, exclusive: boolean, lowerIndex: number): number {
let upperIndex = mappings.length - 1;
const test = exclusive ? -1 : 0;
if (compareSegments(mappings[lowerIndex].generatedSegment, marker) > test) {
// Exit early since the marker is outside the allowed range of mappings.
return -1;
}
let matchingIndex = -1;
while (lowerIndex <= upperIndex) {
const index = (upperIndex + lowerIndex) >> 1;
if (compareSegments(mappings[index].generatedSegment, marker) <= test) {
matchingIndex = index;
lowerIndex = index + 1;
} else {
upperIndex = index - 1;
}
}
return matchingIndex;
}
/**
* A Mapping consists of two segment markers: one in the generated source and one in the original
* source, which indicate the start of each segment. The end of a segment is indicated by the first
* segment marker of another mapping whose start is greater or equal to this one.
*
* It may also include a name associated with the segment being mapped.
*/
export interface Mapping {
readonly generatedSegment: SegmentMarker;
readonly originalSource: SourceFile;
readonly originalSegment: SegmentMarker;
readonly name?: string;
}
/**
* Merge two mappings that go from A to B and B to C, to result in a mapping that goes from A to C.
*/
export function mergeMappings(generatedSource: SourceFile, ab: Mapping, bc: Mapping): Mapping {
const name = bc.name || ab.name;
// We need to modify the segment-markers of the new mapping to take into account the shifts that
// occur due to the combination of the two mappings.
// For example:
// * Simple map where the B->C starts at the same place the A->B ends:
//
// ```
// A: 1 2 b c d
// | A->B [2,0]
// | |
// B: b c d A->C [2,1]
// | |
// | B->C [0,1]
// C: a b c d e
// ```
// * More complicated case where diffs of segment-markers is needed:
//
// ```
// A: b 1 2 c d
// \
// | A->B [0,1*] [0,1*]
// | | |+3
// B: a b 1 2 c d A->C [0,1] [3,2]
// | / |+1 |
// | / B->C [0*,0] [4*,2]
// | /
// C: a b c d e
// ```
//
// `[0,1]` mapping from A->C:
// The difference between the "original segment-marker" of A->B (1*) and the "generated
// segment-marker of B->C (0*): `1 - 0 = +1`.
// Since it is positive we must increment the "original segment-marker" with `1` to give [0,1].
//
// `[3,2]` mapping from A->C:
// The difference between the "original segment-marker" of A->B (1*) and the "generated
// segment-marker" of B->C (4*): `1 - 4 = -3`.
// Since it is negative we must increment the "generated segment-marker" with `3` to give [3,2].
const diff = compareSegments(bc.generatedSegment, ab.originalSegment);
if (diff > 0) {
return {
name,
generatedSegment:
offsetSegment(generatedSource.startOfLinePositions, ab.generatedSegment, diff),
originalSource: bc.originalSource,
originalSegment: bc.originalSegment,
};
} else {
return {
name,
generatedSegment: ab.generatedSegment,
originalSource: bc.originalSource,
originalSegment:
offsetSegment(bc.originalSource.startOfLinePositions, bc.originalSegment, -diff),
};
}
}
/**
* Parse the `rawMappings` into an array of parsed mappings, which reference source-files provided
* in the `sources` parameter.
*/
export function parseMappings(
rawMap: RawSourceMap|null, sources: (SourceFile|null)[],
generatedSourceStartOfLinePositions: number[]): Mapping[] {
if (rawMap === null) {
return [];
}
const rawMappings = decode(rawMap.mappings);
if (rawMappings === null) {
return [];
}
const mappings: Mapping[] = [];
for (let generatedLine = 0; generatedLine < rawMappings.length; generatedLine++) {
const generatedLineMappings = rawMappings[generatedLine];
for (const rawMapping of generatedLineMappings) {
if (rawMapping.length >= 4) {
const originalSource = sources[rawMapping[1]!];
if (originalSource === null || originalSource === undefined) {
// the original source is missing so ignore this mapping
continue;
}
const generatedColumn = rawMapping[0];
const name = rawMapping.length === 5 ? rawMap.names[rawMapping[4]] : undefined;
const line = rawMapping[2]!;
const column = rawMapping[3]!;
const generatedSegment: SegmentMarker = {
line: generatedLine,
column: generatedColumn,
position: generatedSourceStartOfLinePositions[generatedLine] + generatedColumn,
next: undefined,
};
const originalSegment: SegmentMarker = {
line,
column,
position: originalSource.startOfLinePositions[line] + column,
next: undefined,
};
mappings.push({name, generatedSegment, originalSegment, originalSource});
}
}
}
return mappings;
}
/**
* Extract the segment markers from the original source files in each mapping of an array of
* `mappings`.
*
* @param mappings The mappings whose original segments we want to extract
* @returns Return a map from original source-files (referenced in the `mappings`) to arrays of
* segment-markers sorted by their order in their source file.
*/
export function extractOriginalSegments(mappings: Mapping[]): Map<SourceFile, SegmentMarker[]> {
const originalSegments = new Map<SourceFile, SegmentMarker[]>();
for (const mapping of mappings) {
const originalSource = mapping.originalSource;
if (!originalSegments.has(originalSource)) {
originalSegments.set(originalSource, []);
}
const segments = originalSegments.get(originalSource)!;
segments.push(mapping.originalSegment);
}
originalSegments.forEach(segmentMarkers => segmentMarkers.sort(compareSegments));
return originalSegments;
}
/**
* Update the original segments of each of the given `mappings` to include a link to the next
* segment in the source file.
*
* @param mappings the mappings whose segments should be updated
*/
export function ensureOriginalSegmentLinks(mappings: Mapping[]): void {
const segmentsBySource = extractOriginalSegments(mappings);
segmentsBySource.forEach(markers => {
for (let i = 0; i < markers.length - 1; i++) {
markers[i].next = markers[i + 1];
}
});
}
export function computeStartOfLinePositions(str: string) {
// The `1` is to indicate a newline character between the lines.
// Note that in the actual contents there could be more than one character that indicates a
// newline
// - e.g. \r\n - but that is not important here since segment-markers are in line/column pairs and
// so differences in length due to extra `\r` characters do not affect the algorithms.
const NEWLINE_MARKER_OFFSET = 1;
const lineLengths = computeLineLengths(str);
const startPositions = [0]; // First line starts at position 0
for (let i = 0; i < lineLengths.length - 1; i++) {
startPositions.push(startPositions[i] + lineLengths[i] + NEWLINE_MARKER_OFFSET);
}
return startPositions;
}
function computeLineLengths(str: string): number[] {
return (str.split(/\n/)).map(s => s.length);
}
/**
* A collection of mappings between `keys` and `values` stored in the order in which the keys are
* first seen.
*
* The difference between this and a standard `Map` is that when you add a key-value pair the index
* of the `key` is returned.
*/
class IndexedMap<K, V> {
private map = new Map<K, number>();
/**
* An array of keys added to this map.
*
* This array is guaranteed to be in the order of the first time the key was added to the map.
*/
readonly keys: K[] = [];
/**
* An array of values added to this map.
*
* This array is guaranteed to be in the order of the first time the associated key was added to
* the map.
*/
readonly values: V[] = [];
/**
* Associate the `value` with the `key` and return the index of the key in the collection.
*
* If the `key` already exists then the `value` is not set and the index of that `key` is
* returned; otherwise the `key` and `value` are stored and the index of the new `key` is
* returned.
*
* @param key the key to associated with the `value`.
* @param value the value to associated with the `key`.
* @returns the index of the `key` in the `keys` array.
*/
set(key: K, value: V): number {
if (this.map.has(key)) {
return this.map.get(key)!;
}
const index = this.values.push(value) - 1;
this.keys.push(key);
this.map.set(key, index);
return index;
}
}
/**
* A collection of `values` stored in the order in which they were added.
*
* The difference between this and a standard `Set` is that when you add a value the index of that
* item is returned.
*/
class IndexedSet<V> {
private map = new Map<V, number>();
/**
* An array of values added to this set.
* This array is guaranteed to be in the order of the first time the value was added to the set.
*/
readonly values: V[] = [];
/**
* Add the `value` to the `values` array, if it doesn't already exist; returning the index of the
* `value` in the `values` array.
*
* If the `value` already exists then the index of that `value` is returned, otherwise the new
* `value` is stored and the new index returned.
*
* @param value the value to add to the set.
* @returns the index of the `value` in the `values` array.
*/
add(value: V): number {
if (this.map.has(value)) {
return this.map.get(value)!;
}
const index = this.values.push(value) - 1;
this.map.set(value, index);
return index;
}
}
class Cache<Input, Cached> {
private map = new Map<Input, Cached>();
constructor(private computeFn: (input: Input) => Cached) {}
get(input: Input): Cached {
if (!this.map.has(input)) {
this.map.set(input, this.computeFn(input));
}
return this.map.get(input)!;
}
}