diff --git a/packages/istanbul-api/lib/config.js b/packages/istanbul-api/lib/config.js index d0cc833b..91a0d039 100644 --- a/packages/istanbul-api/lib/config.js +++ b/packages/istanbul-api/lib/config.js @@ -28,7 +28,8 @@ function defaultConfig() { 'include-all-sources': false, 'include-pid': false, 'es-modules': false, - 'auto-wrap': false + 'auto-wrap': false, + 'ignore-class-methods': [] }, reporting: { print: 'summary', @@ -191,7 +192,7 @@ addMethods(InstrumentOptions, 'extensions', 'defaultExcludes', 'completeCopy', 'variable', 'compact', 'preserveComments', 'saveBaseline', 'baselineFile', 'esModules', - 'includeAllSources', 'includePid', 'autoWrap'); + 'includeAllSources', 'includePid', 'autoWrap', 'ignoreClassMethods'); /** * returns the root directory used by istanbul which is typically the root of the @@ -225,7 +226,8 @@ InstrumentOptions.prototype.getInstrumenterOpts = function () { compact: this.compact(), preserveComments: this.preserveComments(), esModules: this.esModules(), - autoWrap: this.autoWrap() + autoWrap: this.autoWrap(), + ignoreClassMethods: this.ignoreClassMethods() }; }; diff --git a/packages/istanbul-lib-instrument/api.md b/packages/istanbul-lib-instrument/api.md index 313adc97..51dd65dc 100644 --- a/packages/istanbul-lib-instrument/api.md +++ b/packages/istanbul-lib-instrument/api.md @@ -36,6 +36,7 @@ instead. - `opts.esModules` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** set to true to instrument ES6 modules. (optional, default `false`) - `opts.autoWrap` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** set to true to allow `return` statements outside of functions. (optional, default `false`) - `opts.produceSourceMap` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** set to true to produce a source map for the instrumented code. (optional, default `false`) + - `opts.ignoreClassMethods` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)** set to array of class method names to ignore for coverage. (optional, default `[]`) - `opts.sourceMapUrlCallback` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** a callback function that is called when a source map URL is found in the original code. This function is called with the source file name and the source map URL. (optional, default `null`) - `opts.debug` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** turn debugging on (optional, default `false`) @@ -104,5 +105,6 @@ The exit function returns an object that currently has the following keys: - `sourceFilePath` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** the path to source file (optional, default `'unknown.js'`) - `opts` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** additional options (optional, default `{coverageVariable:'__coverage__',inputSourceMap:undefined}`) - `opts.coverageVariable` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** the global coverage variable name. (optional, default `__coverage__`) + - `opts.ignoreClassMethods` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)** names of methods to ignore by default on classes. (optional, default `[]`) - `opts.inputSourceMap` **[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** the input source map, that maps the uninstrumented code back to the original code. (optional, default `undefined`) diff --git a/packages/istanbul-lib-instrument/src/instrumenter.js b/packages/istanbul-lib-instrument/src/instrumenter.js index 96fbed35..09465128 100644 --- a/packages/istanbul-lib-instrument/src/instrumenter.js +++ b/packages/istanbul-lib-instrument/src/instrumenter.js @@ -16,6 +16,7 @@ function defaultOpts() { esModules: false, autoWrap: false, produceSourceMap: false, + ignoreClassMethods: [], sourceMapUrlCallback: null, debug: false }; @@ -32,6 +33,7 @@ function defaultOpts() { * @param {boolean} [opts.esModules=false] set to true to instrument ES6 modules. * @param {boolean} [opts.autoWrap=false] set to true to allow `return` statements outside of functions. * @param {boolean} [opts.produceSourceMap=false] set to true to produce a source map for the instrumented code. + * @param {Array} [opts.ignoreClassMethods=[]] set to array of class method names to ignore for coverage. * @param {Function} [opts.sourceMapUrlCallback=null] a callback function that is called when a source map URL * is found in the original code. This function is called with the source file name and the source map URL. * @param {boolean} [opts.debug=false] - turn debugging on @@ -91,6 +93,7 @@ class Instrumenter { }); const ee = programVisitor(t, filename, { coverageVariable: opts.coverageVariable, + ignoreClassMethods: opts.ignoreClassMethods, inputSourceMap: inputSourceMap }); let output = {}; diff --git a/packages/istanbul-lib-instrument/src/visitor.js b/packages/istanbul-lib-instrument/src/visitor.js index b7975f46..711e794b 100644 --- a/packages/istanbul-lib-instrument/src/visitor.js +++ b/packages/istanbul-lib-instrument/src/visitor.js @@ -20,7 +20,7 @@ function genVar(filename) { // VisitState holds the state of the visitor, provides helper functions // and is the `this` for the individual coverage visitors. class VisitState { - constructor(types, sourceFilePath, inputSourceMap) { + constructor(types, sourceFilePath, inputSourceMap, ignoreClassMethods) { this.varName = genVar(sourceFilePath); this.attrs = {}; this.nextIgnore = null; @@ -29,6 +29,7 @@ class VisitState { if (typeof (inputSourceMap) !== "undefined") { this.cov.inputSourceMap(inputSourceMap); } + this.ignoreClassMethods = ignoreClassMethods; this.types = types; this.sourceMappingURL = null; } @@ -72,6 +73,7 @@ class VisitState { extractURL(node.leadingComments); extractURL(node.trailingComments); } + // for these expressions the statement counter needs to be hoisted, so // function name inference can be preserved @@ -99,6 +101,16 @@ class VisitState { if (this.getAttr(path.node, 'skip-all') !== null) { this.nextIgnore = n; } + + // else check for ignored class methods + if (path.isFunctionExpression() && this.ignoreClassMethods.some(name => path.node.id && name === path.node.id.name)) { + this.nextIgnore = n; + return; + } + if (path.isClassMethod() && this.ignoreClassMethods.some(name => name === path.node.key.name)) { + this.nextIgnore = n; + return; + } } // all the generic stuff on exit of a node, @@ -491,6 +503,12 @@ function alreadyInstrumented(path, visitState) { function shouldIgnoreFile(programNode) { return programNode.parent && programNode.parent.comments.some(c => COMMENT_FILE_RE.test(c.value)); } + +const defaultProgramVisitorOpts = { + coverageVariable: '__coverage__', + ignoreClassMethods: [], + inputSourceMap: undefined +}; /** * programVisitor is a `babel` adaptor for instrumentation. * It returns an object with two methods `enter` and `exit`. @@ -508,12 +526,13 @@ function shouldIgnoreFile(programNode) { * @param {string} sourceFilePath - the path to source file * @param {Object} opts - additional options * @param {string} [opts.coverageVariable=__coverage__] the global coverage variable name. + * @param {Array} [opts.ignoreClassMethods=[]] names of methods to ignore by default on classes. * @param {object} [opts.inputSourceMap=undefined] the input source map, that maps the uninstrumented code back to the * original code. */ -function programVisitor(types, sourceFilePath = 'unknown.js', opts = {coverageVariable: '__coverage__', inputSourceMap: undefined }) { +function programVisitor(types, sourceFilePath = 'unknown.js', opts = defaultProgramVisitorOpts) { const T = types; - const visitState = new VisitState(types, sourceFilePath, opts.inputSourceMap); + const visitState = new VisitState(types, sourceFilePath, opts.inputSourceMap, opts.ignoreClassMethods); return { enter(path) { if (shouldIgnoreFile(path.find(p => p.isProgram()))) { diff --git a/packages/istanbul-lib-instrument/test/specs/statement-hints.yaml b/packages/istanbul-lib-instrument/test/specs/statement-hints.yaml index 804eb537..7f79b4d8 100644 --- a/packages/istanbul-lib-instrument/test/specs/statement-hints.yaml +++ b/packages/istanbul-lib-instrument/test/specs/statement-hints.yaml @@ -149,3 +149,46 @@ tests: lines: {'1': 1, '2': 1, '4': 0} branches: {'0': [1, 0] } statements: {'0': 1, '1': 1, '2': 0} +--- +name: ignore class methods +guard: isClassAvailable +code: | + class TestClass { + dummy(i) {return i;} + nonIgnored(i) {return i;} + } + var testClass = new TestClass(); + testClass.nonIgnored(); + output = testClass.dummy(args[0]); +instrumentOpts: + ignoreClassMethods: ['dummy'] +tests: + - name: ignores only specified es6 methods + args: [10] + out: 10 + lines: {'3': 1, '5': 1, '6': 1, '7': 1} + functions: {'0': 1} + branches: {} + statements: {'0': 1, '1': 1, '2': 1, '3': 1} +--- +name: ignore class methods +code: | + function TestClass() {} + TestClass.prototype.testMethod = function testMethod(i) { + return i; + }; + TestClass.prototype.goodMethod = function goodMethod(i) {return i;}; + var testClass = new TestClass(); + testClass.goodMethod(); + output = testClass.testMethod(args[0]); +instrumentOpts: + ignoreClassMethods: ['testMethod'] +tests: + - name: ignores only specified es5 methods + args: [10] + out: 10 + lines: {'2': 1, '5': 1, '6': 1, '7': 1, '8': 1} + functions: {'0': 1, '1': 1} + branches: {} + statements: {'0': 1, '1': 1, '2': 1, '3': 1, '4': 1, '5': 1} +