123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- const assert = require('assert')
- const convertSourceMap = require('convert-source-map')
- const { dirname, isAbsolute, join, resolve } = require('path')
- const { fileURLToPath } = require('url')
- const CovBranch = require('./branch')
- const CovFunction = require('./function')
- const CovSource = require('./source')
- const compatError = Error(`requires Node.js ${require('../package.json').engines.node}`)
- let readFile = () => { throw compatError }
- try {
- readFile = require('fs').promises.readFile
- } catch (_err) {
- // most likely we're on an older version of Node.js.
- }
- const { SourceMapConsumer } = require('source-map')
- const isOlderNode10 = /^v10\.(([0-9]\.)|(1[0-5]\.))/u.test(process.version)
- const isNode8 = /^v8\./.test(process.version)
-
- // Injected when Node.js is loading script into isolate pre Node 10.16.x.
- // see: https://github.com/nodejs/node/pull/21573.
- const cjsWrapperLength = isOlderNode10 ? require('module').wrapper[0].length : 0
-
- module.exports = class V8ToIstanbul {
- constructor (scriptPath, wrapperLength, sources, excludePath) {
- assert(typeof scriptPath === 'string', 'scriptPath must be a string')
- assert(!isNode8, 'This module does not support node 8 or lower, please upgrade to node 10')
- this.path = parsePath(scriptPath)
- this.wrapperLength = wrapperLength === undefined ? cjsWrapperLength : wrapperLength
- this.excludePath = excludePath || (() => false)
- this.sources = sources || {}
- this.generatedLines = []
- this.branches = {}
- this.functions = {}
- this.covSources = []
- this.rawSourceMap = undefined
- this.sourceMap = undefined
- this.sourceTranspiled = undefined
- // Indicate that this report was generated with placeholder data from
- // running --all:
- this.all = false
- }
-
- async load () {
- const rawSource = this.sources.source || await readFile(this.path, 'utf8')
- this.rawSourceMap = this.sources.sourceMap ||
- // if we find a source-map (either inline, or a .map file) we load
- // both the transpiled and original source, both of which are used during
- // the backflips we perform to remap absolute to relative positions.
- convertSourceMap.fromSource(rawSource) || convertSourceMap.fromMapFileSource(rawSource, dirname(this.path))
-
- if (this.rawSourceMap) {
- if (this.rawSourceMap.sourcemap.sources.length > 1) {
- this.sourceMap = await new SourceMapConsumer(this.rawSourceMap.sourcemap)
- this.covSources = this.sourceMap.sourcesContent.map((rawSource, i) => ({ source: new CovSource(rawSource, this.wrapperLength), path: this.sourceMap.sources[i] }))
- this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength)
- } else {
- const candidatePath = this.rawSourceMap.sourcemap.sources.length >= 1 ? this.rawSourceMap.sourcemap.sources[0] : this.rawSourceMap.sourcemap.file
- this.path = this._resolveSource(this.rawSourceMap, candidatePath || this.path)
- this.sourceMap = await new SourceMapConsumer(this.rawSourceMap.sourcemap)
-
- let originalRawSource
- if (this.sources.sourceMap && this.sources.sourceMap.sourcemap && this.sources.sourceMap.sourcemap.sourcesContent && this.sources.sourceMap.sourcemap.sourcesContent.length === 1) {
- // If the sourcesContent field has been provided, return it rather than attempting
- // to load the original source from disk.
- // TODO: investigate whether there's ever a case where we hit this logic with 1:many sources.
- originalRawSource = this.sources.sourceMap.sourcemap.sourcesContent[0]
- } else if (this.sources.originalSource) {
- // Original source may be populated on the sources object.
- originalRawSource = this.sources.originalSource
- } else if (this.sourceMap.sourcesContent && this.sourceMap.sourcesContent[0]) {
- // perhaps we loaded sourcesContent was populated by an inline source map, or .map file?
- // TODO: investigate whether there's ever a case where we hit this logic with 1:many sources.
- originalRawSource = this.sourceMap.sourcesContent[0]
- } else {
- // We fallback to reading the original source from disk.
- originalRawSource = await readFile(this.path, 'utf8')
- }
- this.covSources = [{ source: new CovSource(originalRawSource, this.wrapperLength), path: this.path }]
- this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength)
- }
- } else {
- this.covSources = [{ source: new CovSource(rawSource, this.wrapperLength), path: this.path }]
- }
- }
-
- destroy () {
- if (this.sourceMap) {
- this.sourceMap.destroy()
- this.sourceMap = undefined
- }
- }
-
- _resolveSource (rawSourceMap, sourcePath) {
- if (sourcePath.startsWith('file://')) {
- return fileURLToPath(sourcePath)
- }
- sourcePath = sourcePath.replace(/^webpack:\/\//, '')
- const sourceRoot = rawSourceMap.sourcemap.sourceRoot ? rawSourceMap.sourcemap.sourceRoot.replace('file://', '') : ''
- const candidatePath = join(sourceRoot, sourcePath)
-
- if (isAbsolute(candidatePath)) {
- return candidatePath
- } else {
- return resolve(dirname(this.path), candidatePath)
- }
- }
-
- applyCoverage (blocks) {
- blocks.forEach(block => {
- block.ranges.forEach((range, i) => {
- const { startCol, endCol, path, covSource } = this._maybeRemapStartColEndCol(range)
- if (this.excludePath(path)) {
- return
- }
- const lines = covSource.lines.filter(line => {
- // Upstream tooling can provide a block with the functionName
- // (empty-report), this will result in a report that has all
- // lines zeroed out.
- if (block.functionName === '(empty-report)') {
- line.count = 0
- this.all = true
- return true
- }
-
- return startCol < line.endCol && endCol >= line.startCol
- })
- const startLineInstance = lines[0]
- const endLineInstance = lines[lines.length - 1]
-
- if (block.isBlockCoverage && lines.length) {
- this.branches[path] = this.branches[path] || []
- // record branches.
- this.branches[path].push(new CovBranch(
- startLineInstance.line,
- startCol - startLineInstance.startCol,
- endLineInstance.line,
- endCol - endLineInstance.startCol,
- range.count
- ))
-
- // if block-level granularity is enabled, we we still create a single
- // CovFunction tracking object for each set of ranges.
- if (block.functionName && i === 0) {
- this.functions[path] = this.functions[path] || []
- this.functions[path].push(new CovFunction(
- block.functionName,
- startLineInstance.line,
- startCol - startLineInstance.startCol,
- endLineInstance.line,
- endCol - endLineInstance.startCol,
- range.count
- ))
- }
- } else if (block.functionName && lines.length) {
- this.functions[path] = this.functions[path] || []
- // record functions.
- this.functions[path].push(new CovFunction(
- block.functionName,
- startLineInstance.line,
- startCol - startLineInstance.startCol,
- endLineInstance.line,
- endCol - endLineInstance.startCol,
- range.count
- ))
- }
-
- // record the lines (we record these as statements, such that we're
- // compatible with Istanbul 2.0).
- lines.forEach(line => {
- // make sure branch spans entire line; don't record 'goodbye'
- // branch in `const foo = true ? 'hello' : 'goodbye'` as a
- // 0 for line coverage.
- //
- // All lines start out with coverage of 1, and are later set to 0
- // if they are not invoked; line.ignore prevents a line from being
- // set to 0, and is set if the special comment /* c8 ignore next */
- // is used.
-
- if (startCol <= line.startCol && endCol >= line.endCol && !line.ignore) {
- line.count = range.count
- }
- })
- })
- })
- }
-
- _maybeRemapStartColEndCol (range) {
- let covSource = this.covSources[0].source
- let startCol = Math.max(0, range.startOffset - covSource.wrapperLength)
- let endCol = Math.min(covSource.eof, range.endOffset - covSource.wrapperLength)
- let path = this.path
-
- if (this.sourceMap) {
- startCol = Math.max(0, range.startOffset - this.sourceTranspiled.wrapperLength)
- endCol = Math.min(this.sourceTranspiled.eof, range.endOffset - this.sourceTranspiled.wrapperLength)
-
- const { startLine, relStartCol, endLine, relEndCol, source } = this.sourceTranspiled.offsetToOriginalRelative(
- this.sourceMap,
- startCol,
- endCol
- )
-
- const matchingSource = this.covSources.find(covSource => covSource.path === source)
- covSource = matchingSource ? matchingSource.source : this.covSources[0].source
- path = matchingSource ? matchingSource.path : this.covSources[0].path
-
- // next we convert these relative positions back to absolute positions
- // in the original source (which is the format expected in the next step).
- startCol = covSource.relativeToOffset(startLine, relStartCol)
- endCol = covSource.relativeToOffset(endLine, relEndCol)
- }
-
- return {
- path,
- covSource,
- startCol,
- endCol
- }
- }
-
- getInnerIstanbul (source, path) {
- // We apply the "Resolving Sources" logic (as defined in
- // sourcemaps.info/spec.html) as a final step for 1:many source maps.
- // for 1:1 source maps, the resolve logic is applied while loading.
- //
- // TODO: could we move the resolving logic for 1:1 source maps to the final
- // step as well? currently this breaks some tests in c8.
- let resolvedPath = path
- if (this.rawSourceMap && this.rawSourceMap.sourcemap.sources.length > 1) {
- resolvedPath = this._resolveSource(this.rawSourceMap, path)
- }
-
- if (this.excludePath(resolvedPath)) {
- return
- }
-
- return {
- [resolvedPath]: {
- path: resolvedPath,
- all: this.all,
- ...this._statementsToIstanbul(source, path),
- ...this._branchesToIstanbul(source, path),
- ...this._functionsToIstanbul(source, path)
- }
- }
- }
-
- toIstanbul () {
- return this.covSources.reduce((istanbulOuter, { source, path }) => Object.assign(istanbulOuter, this.getInnerIstanbul(source, path)), {})
- }
-
- _statementsToIstanbul (source, path) {
- const statements = {
- statementMap: {},
- s: {}
- }
- source.lines.forEach((line, index) => {
- statements.statementMap[`${index}`] = line.toIstanbul()
- statements.s[`${index}`] = line.count
- })
- return statements
- }
-
- _branchesToIstanbul (source, path) {
- const branches = {
- branchMap: {},
- b: {}
- }
- this.branches[path] = this.branches[path] || []
- this.branches[path].forEach((branch, index) => {
- const srcLine = source.lines[branch.startLine - 1]
- const ignore = srcLine === undefined ? true : srcLine.ignore
- branches.branchMap[`${index}`] = branch.toIstanbul()
- branches.b[`${index}`] = [ignore ? 1 : branch.count]
- })
- return branches
- }
-
- _functionsToIstanbul (source, path) {
- const functions = {
- fnMap: {},
- f: {}
- }
- this.functions[path] = this.functions[path] || []
- this.functions[path].forEach((fn, index) => {
- const srcLine = source.lines[fn.startLine - 1]
- const ignore = srcLine === undefined ? true : srcLine.ignore
- functions.fnMap[`${index}`] = fn.toIstanbul()
- functions.f[`${index}`] = ignore ? 1 : fn.count
- })
- return functions
- }
- }
-
- function parsePath (scriptPath) {
- return scriptPath.startsWith('file://') ? fileURLToPath(scriptPath) : scriptPath
- }
|