Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4d36b2f
commit 90969cf
Showing
8 changed files
with
852 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
packages/compiler-cli/ngcc/src/sourcemaps/cycle_tracker.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. 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 | ||
*/ | ||
|
||
/** | ||
* A utility class to track cycles when running a potentially recursive function. | ||
*/ | ||
export class CycleTracker<T> { | ||
private items: T[] = []; | ||
|
||
/** | ||
* Check that the `item` has not been tracked already; and run the `callback`. | ||
* | ||
* Use this function to wrap a potentially recursive function that needs to ensure that | ||
* it does not get stuck in an infinite recursion due to a cycle. | ||
*/ | ||
track<K>(item: T, callback: () => K): K { | ||
try { | ||
if (this.items.includes(item)) { | ||
this.items.push(item); | ||
throw new Error(`Illegal cycle found: ${this.items.join(' -> ')}`); | ||
} | ||
this.items.push(item); | ||
return callback(); | ||
} finally { | ||
this.items.pop(); | ||
} | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
packages/compiler-cli/ngcc/src/sourcemaps/raw_source_map.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. 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 | ||
*/ | ||
|
||
/** | ||
* This interface is the basic structure of the JSON in a raw source map that one might load from | ||
* disk. | ||
*/ | ||
export interface RawSourceMap { | ||
version: number|string; | ||
file?: string; | ||
sourceRoot?: string; | ||
sources: string[]; | ||
names: string[]; | ||
sourcesContent?: (string|null)[]; | ||
mappings: string; | ||
} |
309 changes: 309 additions & 0 deletions
309
packages/compiler-cli/ngcc/src/sourcemaps/source_file.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,309 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. 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 {SourceMapMappings, SourceMapSegment, decode, encode} from 'sourcemap-codec'; | ||
import {AbsoluteFsPath, dirname, relative} from '../../../src/ngtsc/file_system'; | ||
import {RawSourceMap} from './raw_source_map'; | ||
|
||
export class SourceFile { | ||
/** | ||
* The mappings parsed from the source file's source map. | ||
* If the file has no source map then mappings will be an empty array. | ||
*/ | ||
readonly mappings: Mapping[]; | ||
|
||
/** | ||
* A array original segments for each of the parsed `mappings`. | ||
* These are used to compute the ends of incoming segments. | ||
*/ | ||
readonly originalSegments: SegmentMarker[]; | ||
|
||
/** | ||
* The parsed mappings that have been flattened so that and 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[]; | ||
|
||
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) associated with this source file. */ | ||
readonly rawMap: RawSourceMap|null, | ||
/** Any source files referenced by the raw source map associated with this source file. */ | ||
readonly sources: (SourceFile|null)[]) { | ||
this.mappings = this.parseMappings(); | ||
this.originalSegments = this.mappings.map(mapping => mapping.originalSegment); | ||
this.originalSegments.sort(compareSegments); | ||
this.flattenedMappings = this.flattenMappings(); | ||
} | ||
|
||
/** | ||
* Render the raw source map generated from the flattened mappings. | ||
*/ | ||
renderFlattenedSourceMap(): RawSourceMap { | ||
const sources: SourceFile[] = []; | ||
const names: string[] = []; | ||
const mappings: SourceMapMappings = []; | ||
for (const mapping of this.flattenedMappings) { | ||
if (mappings[mapping.generatedSegment.line] === undefined) { | ||
mappings[mapping.generatedSegment.line] = []; | ||
} | ||
const mappingLine = mappings[mapping.generatedSegment.line]; | ||
const sourceIndex = findIndexOrAdd(sources, mapping.originalSegment.sourceFile); | ||
const mappingArray: SourceMapSegment = [ | ||
mapping.generatedSegment.column, | ||
sourceIndex, | ||
mapping.originalSegment.line, | ||
mapping.originalSegment.column, | ||
]; | ||
if (mapping.name !== undefined) { | ||
const nameIndex = findIndexOrAdd(names, mapping.name); | ||
mappingArray.push(nameIndex); | ||
} | ||
mappingLine.push(mappingArray); | ||
} | ||
const sourcePathDir = dirname(this.sourcePath); | ||
const sourceMap: RawSourceMap = { | ||
version: 3, | ||
sources: sources.map(sf => relative(sourcePathDir, sf.sourcePath)), names, | ||
mappings: encode(mappings), | ||
sourcesContent: sources.map(sf => sf.contents), | ||
}; | ||
return sourceMap; | ||
} | ||
|
||
/** | ||
* Parse the raw mappings into a collection of parsed mappings (stored in the `mappings`) and a | ||
* tree of `SourceFile`s (stored in the `sources` property). | ||
*/ | ||
private parseMappings(): Mapping[] { | ||
const mappings: Mapping[] = []; | ||
if (this.rawMap !== null) { | ||
const rawMappings = decode(this.rawMap.mappings); | ||
if (rawMappings !== null) { | ||
for (let generatedLine = 0; generatedLine < rawMappings.length; generatedLine++) { | ||
const generatedLineMappings = rawMappings[generatedLine]; | ||
for (const rawMapping of generatedLineMappings) { | ||
const generatedColumn = rawMapping[0]; | ||
const generatedSegment = { | ||
sourceFile: this, | ||
line: generatedLine, | ||
column: generatedColumn | ||
}; | ||
if (rawMapping.length >= 4) { | ||
const originalSegment = { | ||
sourceFile: this.sources[rawMapping[1] !] !, | ||
line: rawMapping[2] !, | ||
column: rawMapping[3] ! | ||
}; | ||
const name = rawMapping.length === 5 ? this.rawMap.names[rawMapping[4]] : undefined; | ||
const mapping = {generatedSegment, originalSegment, name}; | ||
mappings.push(mapping); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return mappings; | ||
} | ||
|
||
/** | ||
* Flatten the parsed mappings for this source file, so that all the mappings are to pure original | ||
* source files with no transitive source maps. | ||
* | ||
* TODO: expand of the algorithm... | ||
*/ | ||
private flattenMappings(): Mapping[] { | ||
const flattenedMappings: Mapping[] = []; | ||
debugger; | ||
for (let mappingIndex = 0; mappingIndex < this.mappings.length; mappingIndex++) { | ||
const aToBmapping = this.mappings[mappingIndex]; | ||
const bSource = aToBmapping.originalSegment.sourceFile; | ||
if (bSource.flattenedMappings.length === 0) { | ||
// The b source file has no mappings of its own (i.e. it a pure original file) | ||
// so just use the mapping as-is. | ||
flattenedMappings.push(aToBmapping); | ||
continue; | ||
} | ||
|
||
const incomingStart = aToBmapping.originalSegment; | ||
const lowerBoundOfStartIndex = | ||
findIndexOfLowerBound(bSource.flattenedMappings, incomingStart); | ||
|
||
const incomingEnd = this.originalSegments[this.originalSegments.indexOf(incomingStart) + 1]; | ||
const lowerBoundOfEndIndex = | ||
findIndexOfLowerBound(bSource.flattenedMappings, incomingEnd, lowerBoundOfStartIndex); | ||
|
||
for (let bToCmappingIndex = lowerBoundOfStartIndex; bToCmappingIndex <= lowerBoundOfEndIndex; | ||
bToCmappingIndex++) { | ||
const bToCmapping = bSource.flattenedMappings[bToCmappingIndex]; | ||
if (bToCmapping) { | ||
flattenedMappings.push(mergeMappings(aToBmapping, bToCmapping)); | ||
} | ||
} | ||
} | ||
return flattenedMappings; | ||
} | ||
} | ||
|
||
/** | ||
* 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 | ||
* 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 originalSegment: SegmentMarker; | ||
readonly name?: string; | ||
} | ||
|
||
/** | ||
* A marker that indicates the start of a segment in a mapping. | ||
* | ||
* The end of a segment is indicated by the the first segment-marker of another mapping whose start | ||
* is greater or equal to this one. | ||
*/ | ||
export interface SegmentMarker { | ||
readonly sourceFile: SourceFile; | ||
readonly line: number; | ||
readonly column: number; | ||
} | ||
|
||
/** | ||
* Compare two segment-markers, for use in a search or sorting algorithm. | ||
* | ||
* @returns a positive number of `a` is after `b`, a negative number if `b` is after `a` | ||
* and zero if they are at the same position. | ||
*/ | ||
function compareSegments(a: SegmentMarker, b: SegmentMarker | undefined): number { | ||
return b === undefined ? -1 : a.line === b.line ? a.column - b.column : a.line - b.line; | ||
} | ||
|
||
/** | ||
* Find the index of the lower bound mapping for `marker. | ||
* The lower bound is the mapping whose generated segment starts before the given segment `marker`. | ||
* | ||
* @param mappings The collection of mappings to search for the lower bound mapping. | ||
* @param marker The segment-marker whose lower bound we are searching for. | ||
* @param startIndexHint If we know that the mapping must start after a certain index then this can | ||
* be provided here to improve the search performance. | ||
* @returns The index of the lower bound mapping or -1 if not found. | ||
*/ | ||
function findIndexOfLowerBound( | ||
mappings: Mapping[], marker: SegmentMarker | undefined, startIndexHint: number = -1): number { | ||
if (startIndexHint === -1) { | ||
startIndexHint = 0; | ||
} | ||
for (let index = startIndexHint; index < mappings.length; index++) { | ||
if (compareSegments(mappings[index].generatedSegment, marker) >= 0) { | ||
return index - 1; | ||
} | ||
} | ||
return -1; | ||
} | ||
|
||
/** | ||
* Find `item` in `items` or, if it is not found, push it to the end of the array. | ||
* @param item the item to look for. | ||
* @param items the collection in which to look for `item`. | ||
* @returns the index of the `item` in `items`. | ||
*/ | ||
function findIndexOrAdd<T>(items: T[], item: T): number { | ||
const itemIndex = items.indexOf(item); | ||
if (itemIndex > -1) { | ||
return itemIndex; | ||
} else { | ||
items.push(item); | ||
return items.length - 1; | ||
} | ||
} | ||
|
||
|
||
/** | ||
* 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(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 = segmentDiff(ab.originalSegment, bc.generatedSegment); | ||
if (diff.negative) { | ||
return { | ||
name, | ||
generatedSegment: { | ||
sourceFile: ab.generatedSegment.sourceFile, | ||
line: ab.generatedSegment.line - diff.line, | ||
column: ab.generatedSegment.column - diff.column | ||
}, | ||
originalSegment: bc.originalSegment, | ||
}; | ||
} else { | ||
return { | ||
name, generatedSegment: ab.generatedSegment, originalSegment: { | ||
sourceFile: bc.originalSegment.sourceFile, | ||
line: bc.originalSegment.line + diff.line, | ||
column: bc.originalSegment.column + diff.column | ||
}, | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Compute the difference between two segments | ||
*/ | ||
function segmentDiff(a: SegmentMarker, b: SegmentMarker) { | ||
const line = a.line - b.line; | ||
const column = a.column - b.column; | ||
const negative = line !== 0 ? line < 0 : column < 0; | ||
return {line, column, negative}; | ||
} |
Oops, something went wrong.