| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 | const assert = require('assert')const convertSourceMap = require('convert-source-map')const util = require('util')const debuglog = util.debuglog('c8')const { dirname, isAbsolute, join, resolve } = require('path')const { fileURLToPath } = require('url')const CovBranch = require('./branch')const CovFunction = require('./function')const CovSource = require('./source')const { sliceRange } = require('./range')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 : 0module.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)        if (!this.sourceMap.sourcesContent) {          this.sourceMap.sourcesContent = await this.sourcesContentFromSources()        }        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 }]    }  }  async sourcesContentFromSources () {    const fileList = this.sourceMap.sources.map(relativePath => {      const realPath = this._resolveSource(this.rawSourceMap, relativePath)      return readFile(realPath, 'utf-8')        .then(result => result)        .catch(err => {          debuglog(`failed to load ${realPath}: ${err.message}`)        })    })    return await Promise.all(fileList)  }  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        }        let lines        if (block.functionName === '(empty-report)') {          // (empty-report), this will result in a report that has all lines zeroed out.          lines = covSource.lines.filter((line) => {            line.count = 0            return true          })          this.all = lines.length > 0        } else {          lines = sliceRange(covSource.lines, startCol, endCol)        }        if (!lines.length) {          return        }        const startLineInstance = lines[0]        const endLineInstance = lines[lines.length - 1]        if (block.isBlockCoverage) {          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 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) {          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}
 |