UNPKG

7.95 kBPlain TextView Raw
1import libCoverage from "istanbul-lib-coverage";
2import sourceMap from "source-map";
3import url from "url";
4import { CoverageMapBuilder } from "./builder";
5import { SourceStore } from "./source-store";
6
7/**
8 * A range in a source text identified by its `url`.
9 */
10export interface SourceLocationWithUrl extends SourceLocation {
11 /**
12 * URL of the file containing this location.
13 */
14 readonly url: string;
15}
16
17/**
18 * A (possibly empty) range in the source text.
19 */
20export interface SourceLocation {
21 /**
22 * Start position.
23 */
24 readonly start: Position;
25
26 /**
27 * End position.
28 */
29 readonly end: Position;
30}
31
32/**
33 * A position in the source text.
34 */
35export interface Position {
36 /**
37 * 1-based line index.
38 *
39 * Must satisfy `line >= 1`.
40 */
41 readonly line: number;
42
43 /**
44 * 0-based column index.
45 *
46 * Must satisfy `column >= 0`.
47 */
48 readonly column: number;
49}
50
51/**
52 * Adds content from a generated-file coverage using source maps.
53 *
54 * @param covMapBuilder Coverage map builder to use for the original coverages.
55 * @param sourceStore Store where inlined source texts will be added.
56 * @param generatedFileCov File coverage to process.
57 * @param rawSourceMap Raw source map corresponding to the file to process.
58 * @param sourceMapUrl Optional URL of the source map.
59 */
60export async function addFromGeneratedFileCov(
61 covMapBuilder: CoverageMapBuilder,
62 sourceStore: SourceStore,
63 generatedFileCov: libCoverage.FileCoverageData,
64 rawSourceMap: sourceMap.RawSourceMap,
65 sourceMapUrl?: string,
66): Promise<boolean> {
67 return sourceMap.SourceMapConsumer.with(
68 rawSourceMap,
69 undefined, // sourceMapUrl,
70 // TODO: Fix `souce-map.d.ts` type definitions
71 <(smc: any) => Promise<boolean>> (async (smc: sourceMap.BasicSourceMapConsumer): Promise<boolean> => {
72 const didUpdateSources: boolean = addSourceTexts(sourceStore, smc, generatedFileCov.path);
73 const didUpdateCovMap: boolean = addOriginalFileCovs(covMapBuilder, smc, generatedFileCov);
74 return didUpdateSources || didUpdateCovMap;
75 }),
76 );
77}
78
79/**
80 * Adds the source texts embedded in the source map to the source store.
81 *
82 * @param sourceStore Source store where the source texts will be added.
83 * @param smc Source map consumer to use for the extraction.
84 * @param generatedUrl URL of the generated file.
85 * @returns Boolean indicating of the source store was updated.
86 */
87export function addSourceTexts(
88 sourceStore: SourceStore,
89 smc: sourceMap.BasicSourceMapConsumer,
90 generatedUrl: string,
91): boolean {
92 let didUpdate: boolean = false;
93 for (const [url, sourceText] of getSourceTexts(smc, generatedUrl)) {
94 const curDidUpdate: boolean = sourceStore.set(url, sourceText);
95 didUpdate = didUpdate || curDidUpdate;
96 }
97 return didUpdate;
98}
99
100/**
101 * Extracts the source texts embedded in the source map.
102 *
103 * This function can be used to extract inlined source text for example.
104 *
105 * @param smc Source map consumer to use for the extraction.
106 * @param generatedUrl URL of the generated file.
107 * @returns Iterator of `[url, sourceText]`. `url` is absolute.
108 */
109export function* getSourceTexts(
110 smc: sourceMap.BasicSourceMapConsumer,
111 generatedUrl: string,
112): IterableIterator<[string, string]> {
113 for (const source of smc.sources) {
114 const sourceText: string | null = smc.sourceContentFor(source);
115 if (sourceText === null) {
116 continue;
117 }
118 const originalUrl: string = url.resolve(generatedUrl, source);
119 yield [originalUrl, sourceText];
120 }
121}
122
123/**
124 * Adds the original file coverages to the provided builders map.
125 *
126 * @param covMapBuilder Coverage map builder to use for the original coverages.
127 * @param smc Source map consumer to use.
128 * @param generatedFileCov File coverage for the generated file.
129 * @returns Boolean indicating if the builder was updated.
130 */
131export function addOriginalFileCovs(
132 covMapBuilder: CoverageMapBuilder,
133 smc: sourceMap.BasicSourceMapConsumer,
134 generatedFileCov: libCoverage.FileCoverageData,
135): boolean {
136 let didUpdate: boolean = false;
137 const generatedUrl: string = generatedFileCov.path;
138
139 for (const [key, loc] of Object.entries(generatedFileCov.statementMap)) {
140 const count: number = generatedFileCov.s[key];
141 const originalLoc: SourceLocationWithUrl | undefined = tryGetOriginalLocation(smc, generatedUrl, loc);
142 if (originalLoc === undefined) {
143 continue;
144 }
145 covMapBuilder.addStatement(originalLoc.url, originalLoc, count);
146 didUpdate = true;
147 }
148
149 for (const [key, funcMapping] of Object.entries(generatedFileCov.fnMap)) {
150 const count: number = generatedFileCov.f[key];
151 const originalDecl: SourceLocationWithUrl | undefined = tryGetOriginalLocation(smc, generatedUrl, funcMapping.decl);
152 const originalLoc: SourceLocationWithUrl | undefined = tryGetOriginalLocation(smc, generatedUrl, funcMapping.loc);
153 if (originalDecl === undefined || originalLoc === undefined || originalDecl.url !== originalLoc.url) {
154 continue;
155 }
156 covMapBuilder.addFunction(originalDecl.url, originalDecl, originalLoc, count, funcMapping.name);
157 didUpdate = true;
158 }
159
160 branches: for (const [key, branchMapping] of Object.entries(generatedFileCov.branchMap)) {
161 const counts: ReadonlyArray<number> = generatedFileCov.b[key];
162 const mainLoc: SourceLocation = branchMapping.loc;
163 const originalMainLoc: SourceLocationWithUrl | undefined = tryGetOriginalLocation(smc, generatedUrl, mainLoc);
164 if (originalMainLoc === undefined) {
165 continue;
166 }
167 const arms: [SourceLocation, number][] = [];
168 for (const [i, loc] of branchMapping.locations.entries()) {
169 const count: number = counts[i];
170 const originalLoc: SourceLocationWithUrl | undefined = tryGetOriginalLocation(smc, generatedUrl, loc);
171 if (originalLoc === undefined || originalLoc.url !== originalMainLoc.url) {
172 continue branches;
173 }
174 arms.push([originalLoc, count]);
175 }
176 covMapBuilder.addBranch(originalMainLoc.url, originalMainLoc, arms, branchMapping.type);
177 didUpdate = true;
178 }
179
180 return didUpdate;
181}
182
183/**
184 * Tries to return the corresponding original range.
185 *
186 * The function returns `undefined` in the `start` or `end` positions cannot
187 * be fully resolved (`source`, `line` and `column`) or the `source` does not
188 * match.
189 *
190 * @param smc Source map consumer to use.
191 * @param generatedUrl Absolute URL of the generated file.
192 * @param generatedLocation Source location from the generated file.
193 */
194function tryGetOriginalLocation(
195 smc: sourceMap.BasicSourceMapConsumer,
196 generatedUrl: string,
197 generatedLocation: SourceLocation,
198): SourceLocationWithUrl | undefined {
199 // `oStart`: `originalStart`
200 const oStart: sourceMap.NullableMappedPosition = smc.originalPositionFor({
201 ...generatedLocation.start,
202 bias: sourceMap.SourceMapConsumer.LEAST_UPPER_BOUND,
203 });
204
205 if (oStart.source === null || oStart.line === null || oStart.column === null) {
206 // Unable to fully resolve the start position.
207 return undefined;
208 }
209
210 // `oEnd`: `originalEnd`
211 let oEnd: sourceMap.NullableMappedPosition = smc.originalPositionFor({
212 ...generatedLocation.end,
213 bias: sourceMap.SourceMapConsumer.LEAST_UPPER_BOUND,
214 });
215
216 if (oEnd.source === oStart.source && oEnd.line === oStart.line && oEnd.column === oStart.column) {
217 // `oEnd` === `oStart`, try to use the other bias
218 oEnd = smc.originalPositionFor({
219 ...generatedLocation.end,
220 bias: sourceMap.SourceMapConsumer.GREATEST_LOWER_BOUND,
221 });
222 }
223
224 if (oEnd.source === null || oEnd.line === null || oEnd.column === null) {
225 // Unable to fully resolve the end position.
226 return undefined;
227 }
228
229 if (oEnd.source !== oStart.source) {
230 return undefined;
231 }
232
233 const originalUrl: string = url.resolve(generatedUrl, oStart.source);
234
235 return {
236 url: originalUrl,
237 start: {line: oStart.line, column: oStart.column},
238 end: {line: oEnd.line, column: oEnd.column},
239 };
240}