diff --git a/index.d.ts b/index.d.ts index ee7b2868..2d286873 100644 --- a/index.d.ts +++ b/index.d.ts @@ -20,6 +20,6 @@ declare class V8ToIstanbul { toIstanbul(): CoverageMapData } -declare function v8ToIstanbul(scriptPath: string, wrapperLength?: number, sources?: Sources, excludePath?: (path: string) => boolean): V8ToIstanbul +declare function v8ToIstanbul(scriptPath: string, wrapperLength?: number, sources?: Sources, excludePath?: (path: string) => boolean, excludeEmptyLines?: boolean): V8ToIstanbul export = v8ToIstanbul diff --git a/index.js b/index.js index 4db27a7d..a5827cba 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ const V8ToIstanbul = require('./lib/v8-to-istanbul') -module.exports = function (path, wrapperLength, sources, excludePath) { - return new V8ToIstanbul(path, wrapperLength, sources, excludePath) +module.exports = function (path, wrapperLength, sources, excludePath, excludeEmptyLines) { + return new V8ToIstanbul(path, wrapperLength, sources, excludePath, excludeEmptyLines) } diff --git a/lib/source.js b/lib/source.js index d8ebc215..c9b0bf48 100644 --- a/lib/source.js +++ b/lib/source.js @@ -1,23 +1,31 @@ const CovLine = require('./line') const { sliceRange } = require('./range') -const { originalPositionFor, generatedPositionFor, GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND } = require('@jridgewell/trace-mapping') +const { originalPositionFor, generatedPositionFor, eachMapping, GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND } = require('@jridgewell/trace-mapping') module.exports = class CovSource { - constructor (sourceRaw, wrapperLength) { + constructor (sourceRaw, wrapperLength, traceMap) { sourceRaw = sourceRaw ? sourceRaw.trimEnd() : '' this.lines = [] this.eof = sourceRaw.length this.shebangLength = getShebangLength(sourceRaw) this.wrapperLength = wrapperLength - this.shebangLength - this._buildLines(sourceRaw) + this._buildLines(sourceRaw, traceMap) } - _buildLines (source) { + _buildLines (source, traceMap) { let position = 0 let ignoreCount = 0 let ignoreAll = false + const linesToCover = traceMap && this._parseLinesToCover(traceMap) + for (const [i, lineStr] of source.split(/(?<=\r?\n)/u).entries()) { - const line = new CovLine(i + 1, position, lineStr) + const lineNumber = i + 1 + const line = new CovLine(lineNumber, position, lineStr) + + if (linesToCover && !linesToCover.has(lineNumber)) { + line.ignore = true + } + if (ignoreCount > 0) { line.ignore = true ignoreCount-- @@ -125,6 +133,18 @@ module.exports = class CovSource { if (this.lines[line - 1] === undefined) return this.eof return Math.min(this.lines[line - 1].startCol + relCol, this.lines[line - 1].endCol) } + + _parseLinesToCover (traceMap) { + const linesToCover = new Set() + + eachMapping(traceMap, (mapping) => { + if (mapping.originalLine !== null) { + linesToCover.add(mapping.originalLine) + } + }) + + return linesToCover + } } // this implementation is pulled over from istanbul-lib-sourcemap: diff --git a/lib/v8-to-istanbul.js b/lib/v8-to-istanbul.js index 38e6a124..bd34bcbb 100644 --- a/lib/v8-to-istanbul.js +++ b/lib/v8-to-istanbul.js @@ -25,12 +25,13 @@ const isNode8 = /^v8\./.test(process.version) const cjsWrapperLength = isOlderNode10 ? require('module').wrapper[0].length : 0 module.exports = class V8ToIstanbul { - constructor (scriptPath, wrapperLength, sources, excludePath) { + constructor (scriptPath, wrapperLength, sources, excludePath, excludeEmptyLines) { 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.excludeEmptyLines = excludeEmptyLines === true this.sources = sources || {} this.generatedLines = [] this.branches = {} @@ -58,8 +59,8 @@ module.exports = class V8ToIstanbul { 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) + this.covSources = this.sourceMap.sourcesContent.map((rawSource, i) => ({ source: new CovSource(rawSource, this.wrapperLength, this.excludeEmptyLines ? this.sourceMap : null), path: this.sourceMap.sources[i] })) + this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength, this.excludeEmptyLines ? this.sourceMap : null) } 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) @@ -82,8 +83,8 @@ module.exports = class V8ToIstanbul { // 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) + this.covSources = [{ source: new CovSource(originalRawSource, this.wrapperLength, this.excludeEmptyLines ? this.sourceMap : null), path: this.path }] + this.sourceTranspiled = new CovSource(rawSource, this.wrapperLength, this.excludeEmptyLines ? this.sourceMap : null) } } else { this.covSources = [{ source: new CovSource(rawSource, this.wrapperLength), path: this.path }] diff --git a/test/source.js b/test/source.js index 2e5b877d..acf62d47 100644 --- a/test/source.js +++ b/test/source.js @@ -2,6 +2,7 @@ const CovSource = require('../lib/source') const { TraceMap } = require('@jridgewell/trace-mapping') +const assert = require('assert') require('tap').mochaGlobals() require('should') @@ -68,6 +69,101 @@ describe('Source', () => { }) source.offsetToOriginalRelative(sourceMap, 25, 97).should.deepEqual({}) }) + + it('ignores empty lines', () => { + const sourceRaw = `\ +function add(a: number, b: number) { + /** + * Multi + * line + * comment + */ + if (a === 2 && b === 3) { + // This line should NOT be covered + return 5; + } + + type TypescriptTypings = 1 | 2; + + if (a === 1 && b === 1) { + // Comment + return 2; + } + + interface MoreCompileTimeCode { + should: { + be: { + excluded: true; + }; + }; + } + + return a + b; +} + +add(2, 5); +` + const sourceMap = new TraceMap({ + "version": 3, + "file": "index.js", + "sourceRoot": "", + "sources": [ + "../src/index.ts" + ], + "names": [], + "mappings": ";AAAA,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS;IAM/B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAEvB,OAAO,CAAC,CAAC;IACX,CAAC;IAID,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAEvB,OAAO,CAAC,CAAC;IACX,CAAC;IAUD,OAAO,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC" + } + ) + + const source = new CovSource(sourceRaw, 0, sourceMap) + const lines = source.lines; + + assert.equal(lines[0].ignore, false) + + // Multiline comment + assert.equal(lines[1].ignore, true) + assert.equal(lines[2].ignore, true) + assert.equal(lines[3].ignore, true) + assert.equal(lines[4].ignore, true) + assert.equal(lines[5].ignore, true) + + // If-block with inline comment + assert.equal(lines[6].ignore, false) + assert.equal(lines[7].ignore, true) + assert.equal(lines[8].ignore, false) + assert.equal(lines[9].ignore, false) + assert.equal(lines[10].ignore, true) + + // Typescript Type + assert.equal(lines[11].ignore, true) + + // If-block with inline comment + assert.equal(lines[12].ignore, true) + assert.equal(lines[13].ignore, false) + assert.equal(lines[14].ignore, true) + assert.equal(lines[15].ignore, false) + assert.equal(lines[16].ignore, false) + assert.equal(lines[17].ignore, true) + + // Typescript interface + assert.equal(lines[18].ignore, true) + assert.equal(lines[19].ignore, true) + assert.equal(lines[20].ignore, true) + assert.equal(lines[21].ignore, true) + assert.equal(lines[22].ignore, true) + assert.equal(lines[23].ignore, true) + assert.equal(lines[24].ignore, true) + assert.equal(lines[25].ignore, true) + + // Return statement + function closing + assert.equal(lines[26].ignore, false) + assert.equal(lines[27].ignore, false) + + // Function call + assert.equal(lines[28].ignore, true) + assert.equal(lines[29].ignore, false) + + }) }) for (const prefix of ['c8', 'v8']) {