From 89839dbc6f0412bb631edb3b8e108711f7d8d93c Mon Sep 17 00:00:00 2001 From: Joe Mordetsky Date: Fri, 27 Mar 2020 14:25:14 -0400 Subject: [PATCH] fix: v8 coverage ranges that fall on \n characters cause exceptions When a file has dos line endings \r\n we found a few instances in a large codebase where v8 would report the position of the \n as startCol for a range. This would cause the starting column supplied to sourceMap.originalPositionFor to be a -1 because the startColumns of the transpiled source lines were calculated as endCol + length of line break sequence. To replicate this condition, revert the fix but run the new unit test. The call to `applyCoverage` will crash. While the unit test range was hand crafted by me for this particular example, we saw this coming up in the wild. This change clamps the supplied value to max 0 and adds a test fixture (force encoded as \r\n) to test for it. --- .gitattributes | 3 ++ .gitignore | 1 + lib/source.js | 2 +- .../scripts/needs-compile.compiled.js | 51 +++++++++++++++++++ .../scripts/needs-compile.compiled.js.map | 1 + test/fixtures/scripts/needs-compile.js | 32 ++++++++++++ test/v8-to-istanbul.js | 31 +++++++++++ 7 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 test/fixtures/scripts/needs-compile.compiled.js create mode 100644 test/fixtures/scripts/needs-compile.compiled.js.map create mode 100644 test/fixtures/scripts/needs-compile.js diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6aaf3007 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +test/fixtures/scripts/needs-compile.js eol=crlf +test/fixtures/scripts/needs-compile.compiled.js eol=crlf +test/fixtures/scripts/needs-compile.compiled.js.map eol=crlf \ No newline at end of file diff --git a/.gitignore b/.gitignore index 950be1d1..da8673f3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ coverage .nyc_output node_modules +.idea diff --git a/lib/source.js b/lib/source.js index f592f52e..60569da2 100644 --- a/lib/source.js +++ b/lib/source.js @@ -56,7 +56,7 @@ module.exports = class CovSource { const start = originalPositionTryBoth( sourceMap, lines[0].line, - startCol - lines[0].startCol + Math.max(0, startCol - lines[0].startCol) ) let end = originalEndPositionFor( sourceMap, diff --git a/test/fixtures/scripts/needs-compile.compiled.js b/test/fixtures/scripts/needs-compile.compiled.js new file mode 100644 index 00000000..680de596 --- /dev/null +++ b/test/fixtures/scripts/needs-compile.compiled.js @@ -0,0 +1,51 @@ +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +function _classPrivateFieldGet(receiver, privateMap) { var descriptor = privateMap.get(receiver); if (!descriptor) { throw new TypeError("attempted to get private field on non-instance"); } if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; } + +// compile me with babel 7+: +// > babel --plugins @babel/plugin-proposal-class-properties test/fixtures/scripts/needs-compile.js --out-file test/fixtures/scripts/needs-compile.compiled.js --source-maps +// class uses private class fields - must be compiled +// hello is called +class Foo { + constructor() { + _defineProperty(this, "a", 1); + + _b.set(this, { + writable: true, + value: 2 + }); + + this.c = this.a + _classPrivateFieldGet(this, _b); + } + + hello(i) { + console.info('hello:', this.c + i); + } + +} // class uses private class fields - must be compiled +// hello is never called + + +var _b = new WeakMap(); + +class Bar { + constructor() { + _test.set(this, { + writable: true, + value: 99 + }); + } + + hello() { + console.info(`Hello ${_classPrivateFieldGet(this, _test)}`); + } + +} + +var _test = new WeakMap(); + +const foo = new Foo(); +foo.hello(1); +const bar = new Bar(); + +//# sourceMappingURL=needs-compile.compiled.js.map \ No newline at end of file diff --git a/test/fixtures/scripts/needs-compile.compiled.js.map b/test/fixtures/scripts/needs-compile.compiled.js.map new file mode 100644 index 00000000..fd116497 --- /dev/null +++ b/test/fixtures/scripts/needs-compile.compiled.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["needs-compile.js"],"names":[],"mappings":";;;;AAAA;AACA;AACA;AACA;AACA,MAAM,GAAN,CAAU;AAGR,EAAA,WAAW,GAAI;AAAA,+BAFX,CAEW;;AAAA;AAAA;AAAA,aADV;AACU;;AACb,SAAK,CAAL,GAAS,KAAK,CAAL,yBAAS,IAAT,KAAT;AACD;;AACD,EAAA,KAAK,CAAE,CAAF,EAAK;AACR,IAAA,OAAO,CAAC,IAAR,CAAa,QAAb,EAAuB,KAAK,CAAL,GAAS,CAAhC;AACD;;AARO,C,CAWV;AACA;;;;;AACA,MAAM,GAAN,CAAU;AAER,EAAA,WAAW,GAAI;AAAA;AAAA;AAAA,aADP;AACO;AACd;;AACD,EAAA,KAAK,GAAI;AACP,IAAA,OAAO,CAAC,IAAR,CAAc,SAAD,sBAAS,IAAT,QAAoB,EAAjC;AACD;;AANO;;;;AASV,MAAM,GAAG,GAAG,IAAI,GAAJ,EAAZ;AACA,GAAG,CAAC,KAAJ,CAAU,CAAV;AACA,MAAM,GAAG,GAAG,IAAI,GAAJ,EAAZ","file":"needs-compile.compiled.js","sourcesContent":["// compile me with babel 7+:\n// > babel --plugins @babel/plugin-proposal-class-properties test/fixtures/scripts/needs-compile.js --out-file test/fixtures/scripts/needs-compile.compiled.js --source-maps\n// class uses private class fields - must be compiled\n// hello is called\nclass Foo {\n a = 1\n #b = 2\n constructor () {\n this.c = this.a + this.#b;\n }\n hello (i) {\n console.info('hello:', this.c + i)\n }\n}\n\n// class uses private class fields - must be compiled\n// hello is never called\nclass Bar {\n #test = 99\n constructor () {\n }\n hello () {\n console.info(`Hello ${this.#test}`)\n }\n}\n\nconst foo = new Foo();\nfoo.hello(1)\nconst bar = new Bar()\n\n"]} \ No newline at end of file diff --git a/test/fixtures/scripts/needs-compile.js b/test/fixtures/scripts/needs-compile.js new file mode 100644 index 00000000..149129ff --- /dev/null +++ b/test/fixtures/scripts/needs-compile.js @@ -0,0 +1,32 @@ +// compile me with babel 7+: +// > babel --plugins @babel/plugin-proposal-class-properties test/fixtures/scripts/needs-compile.js --out-file test/fixtures/scripts/needs-compile.compiled.js --source-maps +// in addition, a .gitattribute clause forces this file and its compilation assets to crlf + +// class uses private class fields - must be compiled +// hello is called +class Foo { + a = 1 + #b = 2 + constructor () { + this.c = this.a + this.#b; + } + hello (i) { + console.info('hello:', this.c + i) + } +} + +// class uses private class fields - must be compiled +// hello is never called +class Bar { + #test = 99 + constructor () { + } + hello () { + console.info(`Hello ${this.#test}`) + } +} + +const foo = new Foo(); +foo.hello(1) +const bar = new Bar() + diff --git a/test/v8-to-istanbul.js b/test/v8-to-istanbul.js index 10004967..bf17f13a 100644 --- a/test/v8-to-istanbul.js +++ b/test/v8-to-istanbul.js @@ -6,6 +6,7 @@ const V8ToIstanbul = require('../lib/v8-to-istanbul') const crypto = require('crypto') const os = require('os') const sourcemap = require('source-map') +const assert = require('assert') require('tap').mochaGlobals() require('should') @@ -88,6 +89,36 @@ ${'//'}${'#'} sourceMappingURL=data:application/json;base64,${base64Sourcemap} // that means it was bale to access the content via the provided sources object v8ToIstanbul.sourceTranspiled.should.not.be.undefined() }) + + it('should clamp line source column >= 0', async () => { + const v8ToIstanbul = new V8ToIstanbul( + `file://${require.resolve('./fixtures/scripts/needs-compile.compiled.js')}`, + 0 + ) + + // read the file and find the first end of line char + const fileBody = readFileSync(require.resolve('./fixtures/scripts/needs-compile.compiled.js')).toString() + const matchedNewLineChar = fileBody.match(/(?<=\r?\n)/u).index + + // this isn't an assertion for the test so much as it is an assertion that the + // test fixture hasn't be reverted from \r\n to \n. + assert(fileBody.substring(matchedNewLineChar - 2, matchedNewLineChar) === '\r\n', 'The test fixture is misconfigured!') + + await v8ToIstanbul.load() + // apply a fake range that starts with the matched new line + // (these ranges can occur on v8 running on windows) and verify + // coverage is applied correctly. CovLine will have a + // gap between the endCol of the previous line ending on \r and startCol of the + // next line. This would cause -1 to be sent for the source map lookup. + // This would cause source map translation to throw + v8ToIstanbul.applyCoverage([{ + functionName: 'fake', + ranges: [{ + startOffset: matchedNewLineChar - 1, + endOffset: matchedNewLineChar + 10 + }] + }]) + }) }) describe('source map format edge cases', () => { let consoleWarn