source: frontend/node_modules/v8-to-istanbul/lib/v8-to-istanbul.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 2 weeks ago

Fix frontend appearance

  • Property mode set to 100644
File size: 12.2 KB
Line 
1const assert = require('assert')
2const convertSourceMap = require('convert-source-map')
3const util = require('util')
4const debuglog = util.debuglog('c8')
5const { dirname, isAbsolute, join, resolve } = require('path')
6const { fileURLToPath } = require('url')
7const CovBranch = require('./branch')
8const CovFunction = require('./function')
9const CovSource = require('./source')
10const { sliceRange } = require('./range')
11const compatError = Error(`requires Node.js ${require('../package.json').engines.node}`)
12let readFile = () => { throw compatError }
13try {
14 readFile = require('fs').promises.readFile
15} catch (_err) {
16 // most likely we're on an older version of Node.js.
17}
18const { SourceMapConsumer } = require('source-map')
19const isOlderNode10 = /^v10\.(([0-9]\.)|(1[0-5]\.))/u.test(process.version)
20const isNode8 = /^v8\./.test(process.version)
21
22// Injected when Node.js is loading script into isolate pre Node 10.16.x.
23// see: https://github.com/nodejs/node/pull/21573.
24const cjsWrapperLength = isOlderNode10 ? require('module').wrapper[0].length : 0
25
26module.exports = class V8ToIstanbul {
27 constructor (scriptPath, wrapperLength, sources, excludePath) {
28 assert(typeof scriptPath === 'string', 'scriptPath must be a string')
29 assert(!isNode8, 'This module does not support node 8 or lower, please upgrade to node 10')
30 this.path = parsePath(scriptPath)
31 this.wrapperLength = wrapperLength === undefined ? cjsWrapperLength : wrapperLength
32 this.excludePath = excludePath || (() => false)
33 this.sources = sources || {}
34 this.generatedLines = []
35 this.branches = {}
36 this.functions = {}
37 this.covSources = []
38 this.rawSourceMap = undefined
39 this.sourceMap = undefined
40 this.sourceTranspiled = undefined
41 // Indicate that this report was generated with placeholder data from
42 // running --all:
43 this.all = false
44 }
45
46 async load () {
47 const rawSource = this.sources.source || await readFile(this.path, 'utf8')
48 this.rawSourceMap = this.sources.sourceMap ||
49 // if we find a source-map (either inline, or a .map file) we load
50 // both the transpiled and original source, both of which are used during
51 // the backflips we perform to remap absolute to relative positions.
52 convertSourceMap.fromSource(rawSource) || convertSourceMap.fromMapFileSource(rawSource, dirname(this.path))
53
54 if (this.rawSourceMap) {
55 if (this.rawSourceMap.sourcemap.sources.length > 1) {
56 this.sourceMap = await new SourceMapConsumer(this.rawSourceMap.sourcemap)
57 if (!this.sourceMap.sourcesContent) {
58 this.sourceMap.sourcesContent = await this.sourcesContentFromSources()
59 }
60 this.covSources = this.sourceMap.sourcesContent.map((rawSource, i) => ({ source: new CovSource(rawSource, this.wrapperLength), path: this.sourceMap.sources[i] }))
61 this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength)
62 } else {
63 const candidatePath = this.rawSourceMap.sourcemap.sources.length >= 1 ? this.rawSourceMap.sourcemap.sources[0] : this.rawSourceMap.sourcemap.file
64 this.path = this._resolveSource(this.rawSourceMap, candidatePath || this.path)
65 this.sourceMap = await new SourceMapConsumer(this.rawSourceMap.sourcemap)
66
67 let originalRawSource
68 if (this.sources.sourceMap && this.sources.sourceMap.sourcemap && this.sources.sourceMap.sourcemap.sourcesContent && this.sources.sourceMap.sourcemap.sourcesContent.length === 1) {
69 // If the sourcesContent field has been provided, return it rather than attempting
70 // to load the original source from disk.
71 // TODO: investigate whether there's ever a case where we hit this logic with 1:many sources.
72 originalRawSource = this.sources.sourceMap.sourcemap.sourcesContent[0]
73 } else if (this.sources.originalSource) {
74 // Original source may be populated on the sources object.
75 originalRawSource = this.sources.originalSource
76 } else if (this.sourceMap.sourcesContent && this.sourceMap.sourcesContent[0]) {
77 // perhaps we loaded sourcesContent was populated by an inline source map, or .map file?
78 // TODO: investigate whether there's ever a case where we hit this logic with 1:many sources.
79 originalRawSource = this.sourceMap.sourcesContent[0]
80 } else {
81 // We fallback to reading the original source from disk.
82 originalRawSource = await readFile(this.path, 'utf8')
83 }
84 this.covSources = [{ source: new CovSource(originalRawSource, this.wrapperLength), path: this.path }]
85 this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength)
86 }
87 } else {
88 this.covSources = [{ source: new CovSource(rawSource, this.wrapperLength), path: this.path }]
89 }
90 }
91
92 async sourcesContentFromSources () {
93 const fileList = this.sourceMap.sources.map(relativePath => {
94 const realPath = this._resolveSource(this.rawSourceMap, relativePath)
95 return readFile(realPath, 'utf-8')
96 .then(result => result)
97 .catch(err => {
98 debuglog(`failed to load ${realPath}: ${err.message}`)
99 })
100 })
101 return await Promise.all(fileList)
102 }
103
104 destroy () {
105 if (this.sourceMap) {
106 this.sourceMap.destroy()
107 this.sourceMap = undefined
108 }
109 }
110
111 _resolveSource (rawSourceMap, sourcePath) {
112 if (sourcePath.startsWith('file://')) {
113 return fileURLToPath(sourcePath)
114 }
115 sourcePath = sourcePath.replace(/^webpack:\/\//, '')
116 const sourceRoot = rawSourceMap.sourcemap.sourceRoot ? rawSourceMap.sourcemap.sourceRoot.replace('file://', '') : ''
117 const candidatePath = join(sourceRoot, sourcePath)
118
119 if (isAbsolute(candidatePath)) {
120 return candidatePath
121 } else {
122 return resolve(dirname(this.path), candidatePath)
123 }
124 }
125
126 applyCoverage (blocks) {
127 blocks.forEach(block => {
128 block.ranges.forEach((range, i) => {
129 const { startCol, endCol, path, covSource } = this._maybeRemapStartColEndCol(range)
130 if (this.excludePath(path)) {
131 return
132 }
133 let lines
134 if (block.functionName === '(empty-report)') {
135 // (empty-report), this will result in a report that has all lines zeroed out.
136 lines = covSource.lines.filter((line) => {
137 line.count = 0
138 return true
139 })
140 this.all = lines.length > 0
141 } else {
142 lines = sliceRange(covSource.lines, startCol, endCol)
143 }
144 if (!lines.length) {
145 return
146 }
147
148 const startLineInstance = lines[0]
149 const endLineInstance = lines[lines.length - 1]
150
151 if (block.isBlockCoverage) {
152 this.branches[path] = this.branches[path] || []
153 // record branches.
154 this.branches[path].push(new CovBranch(
155 startLineInstance.line,
156 startCol - startLineInstance.startCol,
157 endLineInstance.line,
158 endCol - endLineInstance.startCol,
159 range.count
160 ))
161
162 // if block-level granularity is enabled, we still create a single
163 // CovFunction tracking object for each set of ranges.
164 if (block.functionName && i === 0) {
165 this.functions[path] = this.functions[path] || []
166 this.functions[path].push(new CovFunction(
167 block.functionName,
168 startLineInstance.line,
169 startCol - startLineInstance.startCol,
170 endLineInstance.line,
171 endCol - endLineInstance.startCol,
172 range.count
173 ))
174 }
175 } else if (block.functionName) {
176 this.functions[path] = this.functions[path] || []
177 // record functions.
178 this.functions[path].push(new CovFunction(
179 block.functionName,
180 startLineInstance.line,
181 startCol - startLineInstance.startCol,
182 endLineInstance.line,
183 endCol - endLineInstance.startCol,
184 range.count
185 ))
186 }
187
188 // record the lines (we record these as statements, such that we're
189 // compatible with Istanbul 2.0).
190 lines.forEach(line => {
191 // make sure branch spans entire line; don't record 'goodbye'
192 // branch in `const foo = true ? 'hello' : 'goodbye'` as a
193 // 0 for line coverage.
194 //
195 // All lines start out with coverage of 1, and are later set to 0
196 // if they are not invoked; line.ignore prevents a line from being
197 // set to 0, and is set if the special comment /* c8 ignore next */
198 // is used.
199
200 if (startCol <= line.startCol && endCol >= line.endCol && !line.ignore) {
201 line.count = range.count
202 }
203 })
204 })
205 })
206 }
207
208 _maybeRemapStartColEndCol (range) {
209 let covSource = this.covSources[0].source
210 let startCol = Math.max(0, range.startOffset - covSource.wrapperLength)
211 let endCol = Math.min(covSource.eof, range.endOffset - covSource.wrapperLength)
212 let path = this.path
213
214 if (this.sourceMap) {
215 startCol = Math.max(0, range.startOffset - this.sourceTranspiled.wrapperLength)
216 endCol = Math.min(this.sourceTranspiled.eof, range.endOffset - this.sourceTranspiled.wrapperLength)
217
218 const { startLine, relStartCol, endLine, relEndCol, source } = this.sourceTranspiled.offsetToOriginalRelative(
219 this.sourceMap,
220 startCol,
221 endCol
222 )
223
224 const matchingSource = this.covSources.find(covSource => covSource.path === source)
225 covSource = matchingSource ? matchingSource.source : this.covSources[0].source
226 path = matchingSource ? matchingSource.path : this.covSources[0].path
227
228 // next we convert these relative positions back to absolute positions
229 // in the original source (which is the format expected in the next step).
230 startCol = covSource.relativeToOffset(startLine, relStartCol)
231 endCol = covSource.relativeToOffset(endLine, relEndCol)
232 }
233
234 return {
235 path,
236 covSource,
237 startCol,
238 endCol
239 }
240 }
241
242 getInnerIstanbul (source, path) {
243 // We apply the "Resolving Sources" logic (as defined in
244 // sourcemaps.info/spec.html) as a final step for 1:many source maps.
245 // for 1:1 source maps, the resolve logic is applied while loading.
246 //
247 // TODO: could we move the resolving logic for 1:1 source maps to the final
248 // step as well? currently this breaks some tests in c8.
249 let resolvedPath = path
250 if (this.rawSourceMap && this.rawSourceMap.sourcemap.sources.length > 1) {
251 resolvedPath = this._resolveSource(this.rawSourceMap, path)
252 }
253
254 if (this.excludePath(resolvedPath)) {
255 return
256 }
257
258 return {
259 [resolvedPath]: {
260 path: resolvedPath,
261 all: this.all,
262 ...this._statementsToIstanbul(source, path),
263 ...this._branchesToIstanbul(source, path),
264 ...this._functionsToIstanbul(source, path)
265 }
266 }
267 }
268
269 toIstanbul () {
270 return this.covSources.reduce((istanbulOuter, { source, path }) => Object.assign(istanbulOuter, this.getInnerIstanbul(source, path)), {})
271 }
272
273 _statementsToIstanbul (source, path) {
274 const statements = {
275 statementMap: {},
276 s: {}
277 }
278 source.lines.forEach((line, index) => {
279 statements.statementMap[`${index}`] = line.toIstanbul()
280 statements.s[`${index}`] = line.count
281 })
282 return statements
283 }
284
285 _branchesToIstanbul (source, path) {
286 const branches = {
287 branchMap: {},
288 b: {}
289 }
290 this.branches[path] = this.branches[path] || []
291 this.branches[path].forEach((branch, index) => {
292 const srcLine = source.lines[branch.startLine - 1]
293 const ignore = srcLine === undefined ? true : srcLine.ignore
294 branches.branchMap[`${index}`] = branch.toIstanbul()
295 branches.b[`${index}`] = [ignore ? 1 : branch.count]
296 })
297 return branches
298 }
299
300 _functionsToIstanbul (source, path) {
301 const functions = {
302 fnMap: {},
303 f: {}
304 }
305 this.functions[path] = this.functions[path] || []
306 this.functions[path].forEach((fn, index) => {
307 const srcLine = source.lines[fn.startLine - 1]
308 const ignore = srcLine === undefined ? true : srcLine.ignore
309 functions.fnMap[`${index}`] = fn.toIstanbul()
310 functions.f[`${index}`] = ignore ? 1 : fn.count
311 })
312 return functions
313 }
314}
315
316function parsePath (scriptPath) {
317 return scriptPath.startsWith('file://') ? fileURLToPath(scriptPath) : scriptPath
318}
Note: See TracBrowser for help on using the repository browser.