feat(compiler-cli): add `SourceFile.getOriginalLocation()` to sourcemaps package (#32912)

This method will allow us to find the original location given a
generated location, which is useful in fine grained work with
source-mapping. E.g. in `$localize` tooling.

PR Close #32912
This commit is contained in:
Pete Bacon Darwin 2020-05-18 18:27:49 +01:00 committed by Andrew Kushnir
parent 2b2146bc58
commit 6abb8d0d91
2 changed files with 185 additions and 0 deletions

View File

@ -87,6 +87,48 @@ export class SourceFile {
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.

View File

@ -518,6 +518,149 @@ runInEachFileSystem(() => {
expect(aTocSourceMap.mappings).toEqual(aToBSourceMap.mappings);
});
});
describe('getOriginalLocation()', () => {
it('should return null for source files with no flattened mappings', () => {
const sourceFile =
new SourceFile(_('/foo/src/index.js'), 'index contents', null, false, []);
expect(sourceFile.getOriginalLocation(1, 1)).toEqual(null);
});
it('should return offset locations in multiple flattened original source files', () => {
const cSource = new SourceFile(_('/foo/src/c.js'), 'bcd123', null, false, []);
const dSource = new SourceFile(_('/foo/src/d.js'), 'aef', null, false, []);
const bSourceMap: RawSourceMap = {
mappings: encode([
[
[0, 1, 0, 0], // "a" is in d.js [source 1]
[1, 0, 0, 0], // "bcd" are in c.js [source 0]
[4, 1, 0, 1], // "ef" are in d.js [source 1]
],
]),
names: [],
sources: ['c.js', 'd.js'],
version: 3
};
const bSource =
new SourceFile(_('/foo/src/b.js'), 'abcdef', bSourceMap, false, [cSource, dSource]);
const aSourceMap: RawSourceMap = {
mappings: encode([
[
[0, 0, 0, 0], [2, 0, 0, 3], // "c" is missing from first line
],
[
[4, 0, 0, 2], // second line has new indentation, and starts with "c"
[5, 0, 0, 5], // "f" is here
],
]),
names: [],
sources: ['b.js'],
version: 3
};
const aSource =
new SourceFile(_('/foo/src/a.js'), 'abde\n cf', aSourceMap, false, [bSource]);
// Line 0
expect(aSource.getOriginalLocation(0, 0)) // a
.toEqual({file: dSource.sourcePath, line: 0, column: 0});
expect(aSource.getOriginalLocation(0, 1)) // b
.toEqual({file: cSource.sourcePath, line: 0, column: 0});
expect(aSource.getOriginalLocation(0, 2)) // d
.toEqual({file: cSource.sourcePath, line: 0, column: 2});
expect(aSource.getOriginalLocation(0, 3)) // e
.toEqual({file: dSource.sourcePath, line: 0, column: 1});
expect(aSource.getOriginalLocation(0, 4)) // off the end of the line
.toEqual({file: dSource.sourcePath, line: 0, column: 2});
// Line 1
expect(aSource.getOriginalLocation(1, 0)) // indent
.toEqual({file: dSource.sourcePath, line: 0, column: 3});
expect(aSource.getOriginalLocation(1, 1)) // indent
.toEqual({file: dSource.sourcePath, line: 0, column: 4});
expect(aSource.getOriginalLocation(1, 2)) // indent
.toEqual({file: dSource.sourcePath, line: 0, column: 5});
expect(aSource.getOriginalLocation(1, 3)) // indent
.toEqual({file: dSource.sourcePath, line: 0, column: 6});
expect(aSource.getOriginalLocation(1, 4)) // c
.toEqual({file: cSource.sourcePath, line: 0, column: 1});
expect(aSource.getOriginalLocation(1, 5)) // f
.toEqual({file: dSource.sourcePath, line: 0, column: 2});
expect(aSource.getOriginalLocation(1, 6)) // off the end of the line
.toEqual({file: dSource.sourcePath, line: 0, column: 3});
});
it('should return offset locations across multiple lines', () => {
const originalSource =
new SourceFile(_('/foo/src/original.js'), 'abcdef\nghijk\nlmnop', null, false, []);
const generatedSourceMap: RawSourceMap = {
mappings: encode([
[
[0, 0, 0, 0], // "ABC" [0,0] => [0,0]
],
[
[0, 0, 1, 0], // "GHIJ" [1, 0] => [1,0]
[4, 0, 0, 3], // "DEF" [1, 4] => [0,3]
[7, 0, 1, 4], // "K" [1, 7] => [1,4]
],
[
[0, 0, 2, 0], // "LMNOP" [2,0] => [2,0]
],
]),
names: [],
sources: ['original.js'],
version: 3
};
const generatedSource = new SourceFile(
_('/foo/src/generated.js'), 'ABC\nGHIJDEFK\nLMNOP', generatedSourceMap, false,
[originalSource]);
// Line 0
expect(generatedSource.getOriginalLocation(0, 0)) // A
.toEqual({file: originalSource.sourcePath, line: 0, column: 0});
expect(generatedSource.getOriginalLocation(0, 1)) // B
.toEqual({file: originalSource.sourcePath, line: 0, column: 1});
expect(generatedSource.getOriginalLocation(0, 2)) // C
.toEqual({file: originalSource.sourcePath, line: 0, column: 2});
expect(generatedSource.getOriginalLocation(0, 3)) // off the end of line 0
.toEqual({file: originalSource.sourcePath, line: 0, column: 3});
// Line 1
expect(generatedSource.getOriginalLocation(1, 0)) // G
.toEqual({file: originalSource.sourcePath, line: 1, column: 0});
expect(generatedSource.getOriginalLocation(1, 1)) // H
.toEqual({file: originalSource.sourcePath, line: 1, column: 1});
expect(generatedSource.getOriginalLocation(1, 2)) // I
.toEqual({file: originalSource.sourcePath, line: 1, column: 2});
expect(generatedSource.getOriginalLocation(1, 3)) // J
.toEqual({file: originalSource.sourcePath, line: 1, column: 3});
expect(generatedSource.getOriginalLocation(1, 4)) // D
.toEqual({file: originalSource.sourcePath, line: 0, column: 3});
expect(generatedSource.getOriginalLocation(1, 5)) // E
.toEqual({file: originalSource.sourcePath, line: 0, column: 4});
expect(generatedSource.getOriginalLocation(1, 6)) // F
.toEqual({file: originalSource.sourcePath, line: 0, column: 5});
expect(generatedSource.getOriginalLocation(1, 7)) // K
.toEqual({file: originalSource.sourcePath, line: 1, column: 4});
expect(generatedSource.getOriginalLocation(1, 8)) // off the end of line 1
.toEqual({file: originalSource.sourcePath, line: 1, column: 5});
// Line 2
expect(generatedSource.getOriginalLocation(2, 0)) // L
.toEqual({file: originalSource.sourcePath, line: 2, column: 0});
expect(generatedSource.getOriginalLocation(2, 1)) // M
.toEqual({file: originalSource.sourcePath, line: 2, column: 1});
expect(generatedSource.getOriginalLocation(2, 2)) // N
.toEqual({file: originalSource.sourcePath, line: 2, column: 2});
expect(generatedSource.getOriginalLocation(2, 3)) // O
.toEqual({file: originalSource.sourcePath, line: 2, column: 3});
expect(generatedSource.getOriginalLocation(2, 4)) // P
.toEqual({file: originalSource.sourcePath, line: 2, column: 4});
expect(generatedSource.getOriginalLocation(2, 5)) // off the end of line 2
.toEqual({file: originalSource.sourcePath, line: 2, column: 5});
});
});
});
describe('computeStartOfLinePositions()', () => {