├── .babelrc ├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── fixtures ├── arrows.js ├── basic.js ├── default.js ├── destruct.js ├── expression.js ├── ignored.js ├── imports.js ├── label.js ├── only.js ├── react.js └── strictFunction.js ├── package.json ├── src └── index.js └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015" ] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # We recommend you to keep these unchanged 10 | indent_style = space 11 | indent_size = 2 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # created by git-ignore 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | coverage.json 15 | .nyc_output 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # Deployed apps should consider commenting this line out: 28 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 29 | node_modules 30 | 31 | 32 | # created by git-ignore 33 | .DS_Store 34 | .AppleDouble 35 | .LSOverride 36 | 37 | # Icon must ends with two \r. 38 | Icon 39 | 40 | # Thumbnails 41 | ._* 42 | 43 | # Files that might appear on external disk 44 | .Spotlight-V100 45 | .Trashes 46 | 47 | 48 | /lib/ 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '5' 4 | after_success: 5 | - codecov 6 | deploy: 7 | provider: npm 8 | email: org.yi.dttvb@gmail.com 9 | api_key: 10 | secure: rTQk3SwZuAjeR4TzSkUuBqMii0dIboIfauDg2nq5FHhPw6OZyUdj6I17YxEl1K0e34STc8Y2CqA9RzmSlYh4tBtyZdNV4Usgyjm+P2EkbtXJm1nP2v/WYuXaJ2XmPjWLXLJgWC/cUdY9NODnp5VvDnJK+X4CeZ5UlvgrfIYs1xnn+kSEgHFkOgGsjft6Cqumh8BkPKUIWYGAQlxMtaT5cSsf62Fq4hwl7b7GVJ9db+imCjRr09H0h2EQjLJiWtOGVW6tZXTyC/gOiOqQZVxLMpMpu0DpimeRKlBey05BUifU1aBWDvLlRZbHpSjYrdDTctwImmfzKM4v/UfS1/+b+bVyI5IWl5VUmf0vcfV6Pc2Az4KhgRbe4dGmhAyjqoHaB/ovbIfyLGP37UMHd1atulYYA+XSyo01lvKw2J+EeIsFL2obuTBHTptL0RYawdFPFx8EhVwPcjg9Aj0srCXDJ7zAoB8PmH/L5UzjbbpidNVyQqiKTzQWuBxnkbtSRf6zCRzG874VszThpgEhykuqeY/qMMccteBAS5mbOjwtWifQhyGBDMeE40vfVRUXLKw9AgOjCm+ZAe6KXAW/3H0h4EQN3dWWmjTqyxVfNdLuaxEYqQ96BsVmJIGPKOdf5QJdeF/949t7ZH8nY5tMFaI7zcHLgoonADw2bzJUZzX3LY0= 11 | on: 12 | tags: true 13 | repo: dtinth/babel-plugin-__coverage__ 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UNMAINTAINED 2 | 3 | This Babel plugin is [not maintained anymore](http://unmaintained.tech/), sorry. 4 | 5 | Please consider checking out [babel-plugin-istanbul](https://github.com/istanbuljs/babel-plugin-istanbul). 6 | 7 | Feel free to keep using this plugin if you have difficulty migrating to other solutions, but please keep in mind that no new versions will be published. 8 | 9 | -- 10 | 11 | ![babel-plugin-\_\_coverage\_\_](http://i.imgur.com/WNq6pvg.png) 12 | ============================= 13 | 14 | [![npm version](https://badge.fury.io/js/babel-plugin-__coverage__.svg)](https://badge.fury.io/js/babel-plugin-__coverage__) 15 | [![npm downloads](https://img.shields.io/npm/dm/babel-plugin-__coverage__.svg)](https://www.npmjs.com/package/babel-plugin-__coverage__) 16 | [![Build Status](https://travis-ci.org/dtinth/babel-plugin-__coverage__.svg?branch=master)](https://travis-ci.org/dtinth/babel-plugin-__coverage__) 17 | [![codecov.io](https://codecov.io/github/dtinth/babel-plugin-__coverage__/coverage.svg?branch=master)](https://codecov.io/github/dtinth/babel-plugin-__coverage__?branch=master) 18 | ![MIT Licensed](https://img.shields.io/badge/license-MIT%20License-blue.svg) 19 | 20 | A Babel plugin that instruments your code with `__coverage__` variable. 21 | The resulting `__coverage__` object is compatible with Istanbul, which means it can instantly be used with [karma-coverage](https://github.com/karma-runner/karma-coverage) and mocha on Node.js (through [nyc](https://github.com/bcoe/nyc)). 22 | 23 | __Note:__ This plugin does not generate any report or save any data to any file; 24 | it only adds instrumenting code to your JavaScript source code. 25 | To integrate with testing tools, please see the [Integrations](#integrations) section. 26 | 27 | > __News:__ For `nyc` users, v11.0.0 is a breaking change from v1.11.111. See [release notes](https://github.com/dtinth/babel-plugin-__coverage__/releases/tag/v11.0.0) for more details. 28 | 29 | 30 | ## Usage 31 | 32 | Install it: 33 | 34 | ``` 35 | npm install --save-dev babel-plugin-__coverage__ 36 | ``` 37 | 38 | Add it to `.babelrc` in test mode: 39 | 40 | ```js 41 | { 42 | "env": { 43 | "test": { 44 | "plugins": [ "__coverage__" ] 45 | } 46 | } 47 | } 48 | ``` 49 | 50 | 51 | ## Integrations 52 | 53 | ### karma 54 | 55 | It _just works_ with Karma. First, make sure that the code is already transpiled by Babel (either using `karma-babel-preprocessor`, `karma-webpack`, or `karma-browserify`). Then, simply set up [karma-coverage](https://github.com/karma-runner/karma-coverage) according to the docs, but __don’t add the `coverage` preprocessor.__ This plugin has already instrumented your code, and Karma should pick it up automatically. 56 | 57 | It has been tested with [bemusic/bemuse](https://codecov.io/github/bemusic/bemuse) project, which contains ~2400 statements. 58 | 59 | 60 | ### mocha on node.js (through nyc) 61 | 62 | Configure Mocha to transpile JavaScript code using Babel, then you can run your tests with [`nyc`](https://github.com/bcoe/nyc), which will collect all the coverage report. 63 | 64 | babel-plugin-\_\_coverage\_\_ respects the `include`/`exclude` configuration options from nyc, 65 | but you also need to __configure NYC not to instrument your code__ by adding these settings in your `package.json`: 66 | 67 | ```js 68 | "nyc": { 69 | "sourceMap": false, 70 | "instrument": false 71 | }, 72 | ``` 73 | 74 | 75 | ## Ignoring files 76 | 77 | You don't want to cover your test files as this will skew your coverage results. You can configure this by configuring the plugin like so: 78 | 79 | ```js 80 | "plugins": [ [ "__coverage__", { "ignore": "test/" } ] ] 81 | ``` 82 | 83 | Where `ignore` is a [glob pattern](https://www.npmjs.com/package/minimatch). 84 | 85 | Alternatively, you can specify `only` which will take precedence over `ignore`: 86 | 87 | ```js 88 | "plugins": [ [ "__coverage__", { "only": "src/" } ] ] 89 | ``` 90 | 91 | ## Canned Answers 92 | 93 | ### There’s already Isparta. Why another coverage tool? 94 | 95 | > __Note:__ Some text in this section is outdated and I say this instead of keeping this section up-to-date lol. So [Isparta is now unmaintained](https://github.com/douglasduteil/isparta/commit/7e89d4e467f54558396533462122e73ea4e9dc31) and [a new version of Istanbul](https://github.com/istanbuljs/istanbul-lib-source-maps) that supports arbitrary compile-to-JS language is coming… 96 | 97 | Isparta is currently the de-facto tool for measuring coverage against ES6 code, which extends Istanbul with ES6 support through Babel. It works very well so far, but then I hit some walls with it. 98 | 99 | So I’ve been trying to get webpack 2 to work with Istanbul/Isparta. 100 | To benefit from [webpack 2’s with tree shaking](http://www.2ality.com/2015/12/webpack-tree-shaking.html), I need to keep `import` and `export` statements in my ES6 modules intact. However, with this setup I can’t get code coverage to work. Inspecting Isparta’s [source code](https://github.com/douglasduteil/isparta/blob/749862a7d1810dd25b8c62c9e613720b57d36da1/src/instrumenter.js) reveals how it works: 101 | 102 | 1. It uses Babel to transpile ES6 back into ES5, saving the source map. 103 | 2. It uses Istanbul to instrument the transpiled source code. This produces some initial metadata (in a global variable called `__coverage__`) which contains the location of each statement, branch, and function. Unfortunately, these mapings are mapped to the transpiled code. Therefore, 104 | 3. The metadata is processed using source map obtained from step 1 to map the location in transpiled code back to the location in the original source code. 105 | 4. The final instrumented code in ES5 is generated. This code shouldn’t be processed through Babel again, or it will be redundant and leads to slower builds. 106 | 107 | Since transforming `import`/`export` statements has now been disabled, instrumentation now dies at step 2, because the Esprima that Istanbul is using cannot process `import`/`export` statements. 108 | 109 | So I looked for something else, and I found [babel-plugin-transform-adana](https://github.com/adana-coverage/babel-plugin-transform-adana). I tried it out immediately. 110 | It turns out that although adana also generates the `__coverage__` variable, it uses its own format which is not compatible with most existing tools (e.g. `istanbul`’s reporter, `karma-coverage` and `nyc`). Tools need to be reinvented for each test harness. 111 | 112 | So I went back to use webpack 1 for the time being. 113 | 114 | Now, with lots of tools to help developers author Babel 6 plugins, 115 | such as [the Babel Handbook](https://github.com/thejameskyle/babel-handbook) and [the AST Explorer](https://astexplorer.net/), it’s not that hard to create Babel plugins today. So I gave it a shot. This is my first Babel plugin. 116 | 117 | It turns out that I can create a rudimentary instrumenter with Babel 6 in roughly 300 LOC (compare to Istanbul’s instrumenter which has ~1,000 LOC). Babel has A LOT of cool stuff to make transpilation easy, from [babel-template](https://github.com/babel/babel/tree/master/packages/babel-template) to [babel-traverse](https://github.com/babel/babel/tree/master/packages/babel-traverse) to [babel-helper-function-name](https://github.com/babel/babel/tree/master/packages/babel-helper-function-name). Babel’s convenient API also handles a lot of edge cases automatically. For example, if a function begins with `'use strict'` statement, prepending a statement into its body will insert it _after_ the `'use strict'` statement. It also transparently converts `if (a) b` into `if (a) { b }` if I want to insert another statement before or after `b`. 118 | 119 | I haven’t tested this with webpack 2 yet. 120 | 121 | 122 | ### Is it stable? 123 | 124 | Well, I wrote most of it in two nights and have only tested some basic stuffs. 125 | So speaking in terms of maturity, this one is very new. 126 | 127 | However, I tried using this in some bigger projects, such as [bemusic/bemuse](https://github.com/bemusic/bemuse) (which contains around 2400 statements). It works, with only few problems (which have now been fixed), and now it works fine. 128 | 129 | __Note:__ If you’re using babel-plugin-\_\_coverage\_\_ inside a larger-scale application, feel free to send pull request and update this section kthx! 130 | 131 | 132 | ### Is the resulting coverage differ from Istanbul/Isparta? 133 | 134 | Sometimes there’s so much logic in a single statement (usually due to functional programming techniques), 135 | it is very likely that not every part of a statement will be covered by tests (see picture below). 136 | Since most coverage service only cares about statement coverage, and I want coverage numbers to be very frank, 137 | `babel-plugin-__coverage__` will treat certain expressions as statements. 138 | 139 | Most likely, this means __if you switch to this plugin your coverage percentage will most likely go down__ compared to when you were using Istanbul or Isparta. But if you use a lot of arrow functions, this means your coverage will be more accurate! Here’s an example from the [bemusic/bemuse](https://github.com/bemusic/bemuse) project: 140 | 141 | ![Imgur](http://i.imgur.com/PX0s8Hy.png) 142 | 143 | Here are some notable differences: 144 | 145 | 1. Arrow function expression body is treated as a statement, because its body may never be evaluated. 146 | 147 | ```js 148 | const x = a => b => c => a + b + c 149 | // <--------------------------------> S1 150 | // <-----------------> S2 151 | // <------------> S3 152 | // <-------> S4 153 | ``` 154 | 155 | 2. Logical operator’s right hand side expression is treated as a statement, because it may be short-circuited. 156 | 157 | ```js 158 | const value = a + b || a - b 159 | // <--------------------------> S1 160 | // <---> S2 161 | ``` 162 | 163 | 3. Each of conditional operator’s branch is treated as a statement because it may not be evaluated. 164 | 165 | ```js 166 | const value = op === 'add' ? a + b : a - b 167 | // <----------------------------------------> S1 168 | // <---> S2 169 | // <---> S3 170 | ``` 171 | 172 | 4. Default parameters and values for destructuring are treated as a statement, because the default may not be evaluated. 173 | 174 | ```js 175 | function setValue (newValue = defaultValue) { } 176 | // <-> S1 177 | // <----------> S2 178 | ``` 179 | 180 | 181 | ### How do I ignore branches/statements? 182 | 183 | I haven’t implemented it. I once [posted an issue on Isparta](https://github.com/douglasduteil/isparta/issues/24) asking about ignoring statements in Isparta (which is now fixed). But nowadays I just think that “coverage is just a number,” so I don’t need them anymore. If you want it, pull requests are welcome! 184 | 185 | -------------------------------------------------------------------------------- /fixtures/arrows.js: -------------------------------------------------------------------------------- 1 | 2 | const arrow = a => ( 3 | b => ( 4 | c => ( 5 | d => a + b + c + d 6 | ) 7 | ) 8 | ) 9 | 10 | arrow(1)(2) 11 | -------------------------------------------------------------------------------- /fixtures/basic.js: -------------------------------------------------------------------------------- 1 | 2 | function run () { 3 | for (var i = 0; i < 10; i++) { 4 | if (i % 2 === 0) console.log('Hello world!') 5 | else console.log('Yeah!') 6 | } 7 | if (i === 10) console.log('wow') 8 | if (i === 0) console.log('wow') 9 | } 10 | 11 | function counter (state = 0, action) { 12 | switch (action.type) { 13 | case 'INCREMENT': 14 | return state + (42 ? 2 : 3) - (1 || 0) + (0 && 1) + a(2 && 0, 0 || 0) 15 | case 'DECREMENT': 16 | return state - 1 17 | default: 18 | return state 19 | } 20 | } 21 | 22 | const a = (x, y) => x + y 23 | const b = (x, y) => x - y 24 | 25 | run() 26 | console.log((c => c(0, { type: 'INCREMENT' }))(counter)) 27 | 28 | void b 29 | -------------------------------------------------------------------------------- /fixtures/default.js: -------------------------------------------------------------------------------- 1 | 2 | export function add (a = 1, b = 2, c = 3, d = 4) { 3 | return a + b + c + d 4 | } 5 | 6 | export const multiply = (a = 1, b = 2, c = 3, d = 4) => a * b * c * d 7 | -------------------------------------------------------------------------------- /fixtures/destruct.js: -------------------------------------------------------------------------------- 1 | 2 | const [ a = 1, b = 2, c = 3 ] = [ 4, 5 ] 3 | 4 | const data = { 5 | screen_name: 'dtinth', 6 | status: 'Cool!' 7 | } 8 | 9 | const { 10 | screen_name = 'no_name', 11 | user_name = 'No Name', 12 | status 13 | } = data 14 | -------------------------------------------------------------------------------- /fixtures/expression.js: -------------------------------------------------------------------------------- 1 | 2 | // Example from Bemuse project: `bemusic/bemuse` 3 | import debug from 'debug/browser' 4 | let log = debug('scintillator:expression') 5 | 6 | import parser from './parser.pegjs' 7 | 8 | function createFunction (code) { 9 | let fn = eval('(function(get) { return ' + code + ' })') // eslint-disable-line no-eval 10 | fn.displayName = '(' + code + ')' 11 | fn.constant = !!/^[\-0-9\.]+$/.test(code) 12 | return fn 13 | } 14 | 15 | export function Expression (text) { 16 | log('parsing %s', text) 17 | let code = parser.parse(text) 18 | log('parsed %s => %s', text, code) 19 | let f = createFunction(code) 20 | let evaluate 21 | if (f.constant) { 22 | evaluate = f 23 | } else { 24 | evaluate = function (data) { 25 | return f(function (key) { 26 | return data[key] 27 | }) 28 | } 29 | } 30 | evaluate.constant = f.constant 31 | return evaluate 32 | } 33 | 34 | export default Expression 35 | -------------------------------------------------------------------------------- /fixtures/ignored.js: -------------------------------------------------------------------------------- 1 | // this file does nothing 2 | // it is just here to be ignored 3 | 4 | export function sadPanda () { 5 | return 'ヽ( ̄(エ) ̄)ノ' 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/imports.js: -------------------------------------------------------------------------------- 1 | 2 | import 'fs' 3 | 4 | import path from 'path' 5 | export default path.join('a', 'b') 6 | -------------------------------------------------------------------------------- /fixtures/label.js: -------------------------------------------------------------------------------- 1 | var count = 0 2 | x: for (var i = 0; i < 10; i++) { 3 | for (var b of [ '4', '5', '6' ]) { 4 | count++ 5 | void b 6 | break x 7 | } 8 | } 9 | module.exports = count 10 | -------------------------------------------------------------------------------- /fixtures/only.js: -------------------------------------------------------------------------------- 1 | // this file does nothing 2 | // it is just here to be special 3 | 4 | export function special () { 5 | return 'ᕙ༼ຈل͜ຈ༽ᕗ' 6 | } 7 | 8 | -------------------------------------------------------------------------------- /fixtures/react.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ReactDOMServer from 'react-dom/server' 4 | 5 | const App = React.createClass({ 6 | propTypes: { 7 | side: React.PropTypes.string 8 | }, 9 | render () { 10 | return
11 | {this.props.side === 'dark' ? 'dark side' : 'light side'} 12 |
13 | } 14 | }) 15 | 16 | console.log(ReactDOMServer.renderToStaticMarkup()) 17 | -------------------------------------------------------------------------------- /fixtures/strictFunction.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function () { 3 | 'use strict' 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-__coverage__", 3 | "version": "11.0.0", 4 | "description": "Babel 6.x plugin to add instrument code with Istanbul-compatible `__coverage__` variable.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prebuild": "npm run clean", 8 | "build": "cross-env NODE_ENV=development babel src -d lib", 9 | "clean": "rimraf lib/ lib-cov/", 10 | "lint": "standard src/** test.js", 11 | "premocha": "cross-env BABEL_DISABLE_CACHE=1 babel src --plugins $(pwd)/lib/index -d lib-cov", 12 | "mocha": "cross-env NODE_ENV=test BABEL_PLUGIN__COVERAGE__TEST=1 nyc -r lcov -r text mocha", 13 | "prepublish": "npm test", 14 | "pretest": "npm run lint && npm run build", 15 | "test": "npm run mocha" 16 | }, 17 | "author": "Thai Pangsakulyanont (http://dt.in.th/)", 18 | "license": "MIT", 19 | "files": [ 20 | "lib" 21 | ], 22 | "nyc": { 23 | "include": [ 24 | "*" 25 | ], 26 | "sourceMap": false, 27 | "instrument": false 28 | }, 29 | "devDependencies": { 30 | "babel": "^6.5.2", 31 | "babel-cli": "^6.5.1", 32 | "babel-core": "^6.5.2", 33 | "babel-preset-es2015": "^6.5.0", 34 | "babel-preset-react": "^6.5.0", 35 | "babel-register": "^6.5.2", 36 | "codecov": "^1.0.1", 37 | "cross-env": "^1.0.7", 38 | "istanbul": "^0.4.2", 39 | "mocha": "^2.4.5", 40 | "nyc": "^6.6.1", 41 | "react": "^15.0.0", 42 | "react-dom": "^15.0.0", 43 | "rimraf": "^2.5.2", 44 | "standard": "^7.0.0" 45 | }, 46 | "dependencies": { 47 | "babel-helper-function-name": "^6.5.0", 48 | "babel-template": "^6.8.0", 49 | "test-exclude": "^1.1.0" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "git+https://github.com/dtinth/babel-plugin-__coverage__.git" 54 | }, 55 | "keywords": [ 56 | "babel", 57 | "coverage", 58 | "istanbul", 59 | "tdd", 60 | "test", 61 | "unit" 62 | ], 63 | "bugs": { 64 | "url": "https://github.com/dtinth/babel-plugin-__coverage__/issues" 65 | }, 66 | "homepage": "https://github.com/dtinth/babel-plugin-__coverage__#readme" 67 | } 68 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // babel-plugin-__coverage__ 3 | // 4 | // This is my first Babel plugin, and I wrote it during the night. 5 | // Therefore, be prepared to see a lot of copypasta and wtf code. 6 | 7 | import { util } from 'babel-core' 8 | import template from 'babel-template' 9 | import nameFunction from 'babel-helper-function-name' 10 | import { realpathSync } from 'fs' 11 | import { createHash } from 'crypto' 12 | import testExclude from 'test-exclude' 13 | 14 | const coverageTemplate = template(` 15 | var FILE_COVERAGE 16 | function COVER () { 17 | if (!FILE_COVERAGE) FILE_COVERAGE = GET_INITIAL_FILE_COVERAGE() 18 | return FILE_COVERAGE 19 | } 20 | function GET_INITIAL_FILE_COVERAGE () { 21 | var path = PATH, hash = HASH 22 | var global = (new Function('return this'))() 23 | var coverage = global['__coverage__'] || (global['__coverage__'] = { }) 24 | if (coverage[path] && coverage[path].hash === hash) return coverage[path] 25 | var coverageData = global['JSON'].parse(INITIAL) 26 | coverageData.hash = hash 27 | return coverage[path] = coverageData 28 | } 29 | COVER () 30 | `) 31 | 32 | // 33 | // support nyc's include/exclude logic when 34 | // provided as config in package.json. 35 | // 36 | let exclude 37 | function nycShouldInstrument (filename) { 38 | if (!exclude) { 39 | exclude = testExclude({ 40 | configKey: 'nyc', 41 | configPath: process.cwd() 42 | }) 43 | } 44 | 45 | if (!exclude.configFound) return true 46 | else return exclude.shouldInstrument(filename) 47 | } 48 | 49 | // 50 | // Takes a relative path and returns a real path. 51 | // Assumes the path name is relative to working directory. 52 | // 53 | function getRealpath (n) { 54 | try { 55 | return realpathSync(n) || n 56 | } catch (e) { 57 | return n 58 | } 59 | } 60 | 61 | /** 62 | * This determines whether the given state and options combination 63 | * should result in the file being ignored and not covered 64 | * Big thanks to babel-plugin-transform-adana and their CC0-1.0 license 65 | * from which this code was mostly copy/pasted 66 | */ 67 | function skip ({ opts, file } = { }) { 68 | let shouldSkip = false 69 | 70 | if (file && opts) { 71 | const { ignore = [], only } = opts 72 | shouldSkip = util.shouldIgnore( 73 | file.opts.filename, 74 | util.arrayify(ignore, util.regexify), 75 | only ? util.arrayify(only, util.regexify) : null 76 | ) 77 | } 78 | 79 | return shouldSkip || !nycShouldInstrument(file.opts.filename) 80 | } 81 | 82 | module.exports = function ({ types: t }) { 83 | // 84 | // Return the immediate data structure local to a file. 85 | // 86 | function getData (context) { 87 | const path = getRealpath(context.file.opts.filename) 88 | // 89 | // XXX: Is it OK to mutate `context.file`? I don’t know but it works! 90 | // 91 | return context.file.__coverage__data || (context.file.__coverage__data = { 92 | // 93 | // Initial data that will be added in front of generated source code 94 | base: { 95 | path: path, 96 | s: { }, 97 | b: { }, 98 | f: { }, 99 | statementMap: { }, 100 | fnMap: { }, 101 | branchMap: { } 102 | }, 103 | // 104 | // The counter that generates the next ID for each statement type. 105 | nextId: { 106 | s: 1, 107 | b: 1, 108 | f: 1 109 | }, 110 | // 111 | // True if coverage info is already emitted. 112 | sealed: false 113 | }) 114 | } 115 | 116 | // 117 | // Turns a `SourceLocation` into a plain object. 118 | // 119 | function locToObject (loc) { 120 | return { 121 | start: { 122 | line: loc.start.line, 123 | column: loc.start.column 124 | }, 125 | end: { 126 | line: loc.end.line, 127 | column: loc.end.column 128 | } 129 | } 130 | } 131 | 132 | // 133 | // Generates an AST representing an expression that will increment the 134 | // code coverage counter. 135 | // 136 | function increase (context, type, id, index) { 137 | const wrap = (index != null 138 | // If `index` present, turn `x` into `x[index]`. 139 | ? (x) => t.memberExpression(x, t.numericLiteral(index), true) 140 | : (x) => x 141 | ) 142 | return t.unaryExpression('++', 143 | wrap( 144 | t.memberExpression( 145 | t.memberExpression(t.callExpression(getData(context).id, [ ]), t.identifier(type)), 146 | t.stringLiteral(id), 147 | true 148 | ) 149 | ) 150 | ) 151 | } 152 | 153 | // 154 | // Adds coverage traking expression to a path. 155 | // 156 | // - If it’s a statement (`a`), turns into `++coverage; a`. 157 | // - If it’s an expression (`x`), turns into `(++coverage, x)`. 158 | // 159 | function instrument (path, increment) { 160 | if (path.isBlockStatement()) { 161 | path.node.body.unshift(t.expressionStatement(increment)) 162 | } else if (path.isStatement()) { 163 | path.insertBefore(t.expressionStatement(increment)) 164 | } else if (path.isExpression()) { 165 | path.replaceWith(t.sequenceExpression([ increment, path.node ])) 166 | } else { 167 | throw new Error(`wtf? I can’t cover a ${path.node.type}!!!!??`) 168 | } 169 | } 170 | 171 | // 172 | // Adds coverage to any statement. 173 | // 174 | function instrumentStatement (context, path) { 175 | const node = path.node 176 | if (!node) return 177 | 178 | // Don’t cover code generated by Babel. 179 | if (!node.loc) return 180 | 181 | // Make sure we don’t cover already instrumented code (only applies to statements). 182 | // XXX: Hacky node mutation again. PRs welcome! 183 | if (node.__coverage__instrumented) return 184 | node.__coverage__instrumented = true 185 | 186 | const id = nextStatementId(context, node.loc) 187 | instrument(path, increase(context, 's', id)) 188 | } 189 | 190 | // 191 | // Returns the next statement ID. 192 | // 193 | function nextStatementId (context, loc) { 194 | const data = getData(context) 195 | const id = String(data.nextId.s++) 196 | data.base.s[id] = 0 197 | data.base.statementMap[id] = locToObject(loc) 198 | return id 199 | } 200 | 201 | // 202 | // Returns the next branch ID and adds the information to `branchMap` object. 203 | // 204 | function nextBranchId (context, line, type, locations) { 205 | const data = getData(context) 206 | const id = String(data.nextId.b++) 207 | data.base.b[id] = locations.map(() => 0) 208 | data.base.branchMap[id] = { line, type, locations: locations.map(locToObject) } 209 | return id 210 | } 211 | 212 | // 213 | // `a` => `++coverage; a` For most common type of statements. 214 | // 215 | function coverStatement (path) { 216 | instrumentStatement(this, path) 217 | } 218 | 219 | // 220 | // `var x = 1` => `var x = (++coverage, 1)` 221 | // 222 | function coverVariableDeclarator (path) { 223 | instrumentStatement(this, path.get('init')) 224 | } 225 | 226 | // 227 | // Adds branch coverage to `if` statements. 228 | // 229 | function coverIfStatement (path) { 230 | if (!path.node.loc) return 231 | const loc0 = path.node.loc 232 | const node = path.node 233 | makeBlock(path.get('consequent')) 234 | makeBlock(path.get('alternate')) 235 | const loc1 = node.consequent && node.consequent.loc || loc0 236 | const loc2 = node.alternate && node.alternate.loc || loc1 237 | const id = nextBranchId(this, loc0.start.line, 'if', [ loc1, loc2 ]) 238 | instrument(path.get('consequent'), increase(this, 'b', id, 0)) 239 | instrument(path.get('alternate'), increase(this, 'b', id, 1)) 240 | instrumentStatement(this, path) 241 | } 242 | 243 | // 244 | // Turns path into block. 245 | // 246 | function makeBlock (path) { 247 | if (!path.node) { 248 | return path.replaceWith(t.blockStatement([ ])) 249 | } 250 | if (!path.isBlockStatement()) { 251 | return path.replaceWith(t.blockStatement([ path.node ])) 252 | } 253 | } 254 | 255 | // 256 | // Adds branch coverage to `switch` statements. 257 | // 258 | function coverSwitchStatement (path) { 259 | if (!path.node.loc) return 260 | instrumentStatement(this, path) 261 | const validCases = path.get('cases').filter((p) => p.node.loc) 262 | const id = nextBranchId(this, path.node.loc.start.line, 'switch', validCases.map((p) => p.node.loc)) 263 | let index = 0 264 | validCases.forEach(p => { 265 | if (p.node.test) { 266 | instrumentStatement(this, p.get('test')) 267 | } 268 | p.node.consequent.unshift(increase(this, 'b', id, index++)) 269 | }) 270 | } 271 | 272 | // 273 | // `for (;; x)` => `for (;; ++coverage, x)`. 274 | // Because the increment may be stopped in the first iteration due to `break`. 275 | // 276 | function coverForStatement (path) { 277 | makeBlock(path.get('body')) 278 | instrumentStatement(this, path) 279 | instrumentStatement(this, path.get('update')) 280 | } 281 | 282 | // 283 | // Turn the body into block. This fixes some really weird edge cases where 284 | // `while (x) if (y) z` is missing coverage on `z`. 285 | // 286 | function coverLoopStatement (path) { 287 | makeBlock(path.get('body')) 288 | instrumentStatement(this, path) 289 | } 290 | 291 | // 292 | // Covers a function. 293 | // 294 | function coverFunction (path) { 295 | if (!path.node.loc) return 296 | const node = path.node 297 | const data = getData(this) 298 | const id = String(data.nextId.f++) 299 | const nameOf = (namedNode) => namedNode && namedNode.id && namedNode.id.name || null 300 | data.base.f[id] = 0 301 | data.base.fnMap[id] = { 302 | name: nameOf(nameFunction(path)), // I love Babel! 303 | line: node.loc.start.line, 304 | loc: locToObject(node.loc) 305 | } 306 | const increment = increase(this, 'f', id) 307 | const body = path.get('body') 308 | if (body.isBlockStatement()) { 309 | body.node.body.unshift(t.expressionStatement(increment)) 310 | } else if (body.isExpression()) { 311 | const sid = nextStatementId(this, body.node.loc || path.node.loc) 312 | body.replaceWith(t.sequenceExpression([ 313 | increment, 314 | increase(this, 's', sid), 315 | body.node 316 | ])) 317 | } else { 318 | throw new Error(`wtf?? Can’t cover function with ${body.node.type}`) 319 | } 320 | } 321 | 322 | // 323 | // `a ? b : c` => `a ? (++coverage, b) : (++coverage, c)`. 324 | // Also adds branch coverage. 325 | // 326 | function coverConditionalExpression (path) { 327 | instrumentStatement(this, path.get('consequent')) 328 | instrumentStatement(this, path.get('alternate')) 329 | if (path.node.loc) { 330 | const node = path.node 331 | const loc1 = node.consequent.loc || node.loc 332 | const loc2 = node.alternate.loc || loc1 333 | const id = nextBranchId(this, node.loc.start.line, 'cond-expr', [ loc1, loc2 ]) 334 | instrument(path.get('consequent'), increase(this, 'b', id, 0)) 335 | instrument(path.get('alternate'), increase(this, 'b', id, 1)) 336 | } 337 | } 338 | 339 | // 340 | // `a || b` => `a || (++coverage, b)`. Required due to short circuiting. 341 | // Also adds branch coverage. 342 | // 343 | function coverLogicalExpression (path) { 344 | instrumentStatement(this, path.get('right')) 345 | if (!path.node.loc) return 346 | const node = path.node 347 | const loc1 = node.left.loc || node.loc 348 | const loc2 = node.right.loc || loc1 349 | const id = nextBranchId(this, node.loc.start.line, 'binary-expr', [ loc1, loc2 ]) 350 | instrument(path.get('left'), increase(this, 'b', id, 0)) 351 | instrument(path.get('right'), increase(this, 'b', id, 1)) 352 | } 353 | 354 | // 355 | // `(function (a = x) { })` => `(function (a = (++coverage, x)) { })`. 356 | // Because default may not be evaluated. 357 | // 358 | function coverAssignmentPattern (path) { 359 | instrumentStatement(this, path.get('right')) 360 | } 361 | 362 | // If the coverage for this file is sealed, make the guarded function noop. 363 | // It is here to fix some very weird edge case in `fixtures/imports.js` 364 | const guard = (f) => function (path, state) { 365 | if (skip(state)) return 366 | if (getData(this).sealed) return 367 | return f.call(this, path) 368 | } 369 | 370 | const coverWith = (process.env.BABEL_PLUGIN__COVERAGE__TEST 371 | // Defer execution so we can measure coverage easily. 372 | ? (f) => guard(function () { return f().apply(this, arguments) }) 373 | // Execute immediately so it runs faster at runtime. 374 | // NOTE: This case should have already been covered due to 375 | // self-instrumentation to generate `lib-cov`. 376 | : (f) => guard(f()) 377 | ) 378 | 379 | return { 380 | visitor: { 381 | // 382 | // Shamelessly copied from istanbul. 383 | // 384 | ExpressionStatement: coverWith(() => coverStatement), 385 | BreakStatement: coverWith(() => coverStatement), 386 | ContinueStatement: coverWith(() => coverStatement), 387 | DebuggerStatement: coverWith(() => coverStatement), 388 | ReturnStatement: coverWith(() => coverStatement), 389 | ThrowStatement: coverWith(() => coverStatement), 390 | TryStatement: coverWith(() => coverStatement), 391 | VariableDeclarator: coverWith(() => coverVariableDeclarator), 392 | IfStatement: coverWith(() => coverIfStatement), 393 | ForStatement: coverWith(() => coverForStatement), 394 | ForInStatement: coverWith(() => coverLoopStatement), 395 | ForOfStatement: coverWith(() => coverLoopStatement), 396 | WhileStatement: coverWith(() => coverLoopStatement), 397 | DoWhileStatement: coverWith(() => coverStatement), 398 | SwitchStatement: coverWith(() => coverSwitchStatement), 399 | ArrowFunctionExpression: coverWith(() => coverFunction), 400 | FunctionExpression: coverWith(() => coverFunction), 401 | FunctionDeclaration: coverWith(() => coverFunction), 402 | LabeledStatement: coverWith(() => coverStatement), 403 | ConditionalExpression: coverWith(() => coverConditionalExpression), 404 | LogicalExpression: coverWith(() => coverLogicalExpression), 405 | AssignmentPattern: coverWith(() => coverAssignmentPattern), 406 | ExportDefaultDeclaration: coverWith(() => coverStatement), 407 | 408 | Program: { 409 | enter (path, state) { 410 | if (skip(state)) return 411 | // Save the variable name used for tracking coverage. 412 | getData(this).id = path.scope.generateUidIdentifier('__cover__') 413 | }, 414 | exit (path, state) { 415 | if (skip(state)) return 416 | // Prepends the coverage runtime. 417 | const realPath = getRealpath(this.file.opts.filename) 418 | const initialJson = JSON.stringify(getData(this).base) 419 | const hash = createHash('md5').update(initialJson).digest('hex') 420 | getData(this).sealed = true 421 | path.node.body.unshift(...coverageTemplate({ 422 | GET_INITIAL_FILE_COVERAGE: path.scope.generateUidIdentifier('__coverage__getInitialState'), 423 | FILE_COVERAGE: path.scope.generateUidIdentifier('__coverage__file'), 424 | COVER: getData(this).id, 425 | PATH: t.stringLiteral(realPath), 426 | INITIAL: t.stringLiteral(initialJson), 427 | HASH: t.stringLiteral(hash) 428 | })) 429 | } 430 | } 431 | } 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global describe, it, __coverage__ */ 4 | /* eslint no-eval: 0 */ 5 | const assert = require('assert') 6 | 7 | describe('test', function () { 8 | const coveragePlugin = require('./lib-cov') 9 | const BABEL_OPTIONS = { 10 | presets: [ 'es2015', 'react' ], 11 | plugins: [ [ coveragePlugin, { ignore: 'ignored' } ] ], 12 | babelrc: false 13 | } 14 | const Babel = require('babel-core') 15 | 16 | process.env.BABEL_DISABLE_CACHE = true 17 | require('babel-register')(BABEL_OPTIONS) 18 | 19 | describe('statement coverage', function () { 20 | describe('variable declartions', function () { 21 | testStatementCoverage('const x = 0', 1) 22 | testStatementCoverage('let x = 0', 1) 23 | testStatementCoverage('var x = 1, y = 2', 2) 24 | testStatementCoverage('var x', 0) 25 | }) 26 | 27 | describe('import declarations', function () { 28 | testStatementCoverage('import lodash from \'lodash\'', 0) 29 | }) 30 | 31 | describe('export declarations', function () { 32 | testStatementCoverage('export { keys } from \'lodash\'', 0) 33 | testStatementCoverage('export const x = 10 * 10', 1) 34 | testStatementCoverage('export default 10 * 10', 1) 35 | }) 36 | 37 | describe('arrow functions', function () { 38 | testStatementCoverage('const x = a => b => c => a + b + c', 4) 39 | testStatementCoverage('const x = a => a + a', 2) 40 | testStatementCoverage('const x = (a = 99) => a + a', 3) 41 | }) 42 | 43 | describe('destructuring', function () { 44 | testStatementCoverage('const [ a, b, c ] = [ 1, 2, 3 ]', 1) 45 | testStatementCoverage('const [ a = 1, b = 2, c = 3 ] = [ ]', 4) 46 | }) 47 | 48 | describe('loop', function () { 49 | testStatementCoverage('for (const x in y) break', 2) 50 | testStatementCoverage('for (const x of y) break', 2) 51 | testStatementCoverage('while (true !== false) break', 2) 52 | testStatementCoverage('do { break } while (true)', 2) 53 | testStatementCoverage('for (x in y) if (!y[x]) continue', 3) 54 | testStatementCoverage('for (x in y) if (unexpected(x)) debugger', 3) 55 | testStatementCoverage('if (unexpected(x)) for (var x in y) try { throw new Error("wtf") } finally { }', 4) 56 | }) 57 | 58 | describe('nested', function () { 59 | testStatementCoverage('if (x) if (y) z', 3) 60 | testStatementCoverage('while (x) if (y) break', 3) 61 | testStatementCoverage('for (a; b; c) if (y) break', 4) 62 | }) 63 | }) 64 | 65 | describe('integrated tests', function () { 66 | it('can run basic fixture', function () { 67 | require('./fixtures/basic') 68 | }) 69 | 70 | it('can run jsx fixture', function () { 71 | require('./fixtures/react') 72 | }) 73 | 74 | it('can haz arrow funktions', function () { 75 | require('./fixtures/arrows') 76 | }) 77 | 78 | it('can run es6 modules with bare import', function () { 79 | require('./fixtures/imports') 80 | }) 81 | 82 | it('can haz defualt paramitrz', function () { 83 | const lib = require('./fixtures/default') 84 | lib.add(10, 20) 85 | lib.multiply(10, 20) 86 | }) 87 | 88 | it('can DESTRUCTUR', function () { 89 | require('./fixtures/destruct') 90 | }) 91 | 92 | it('does not choke on bemuse codebase', function () { 93 | Babel.transformFileSync(require.resolve('./fixtures/expression'), BABEL_OPTIONS) 94 | }) 95 | 96 | it('does not affect strictness of function', function () { 97 | assert.ok(require('./fixtures/strictFunction').toString().match(/\{\s*['"]use strict/)) 98 | }) 99 | 100 | it('works with label statement', function () { 101 | assert.equal(require('./fixtures/label'), 1) 102 | }) 103 | 104 | it('ignores files excluded by `ignore` option', function () { 105 | const code = transpileFile('./fixtures/ignored', { ignore: 'ignored' }) 106 | assert.ok(!codeIsCovered(code)) 107 | }) 108 | 109 | it('includes files not matched by `ignore` option', function () { 110 | const code = transpileFile('./fixtures/only', { ignore: 'ignored' }) 111 | assert.ok(codeIsCovered(code)) 112 | }) 113 | 114 | it('instruments files included by `only`', function () { 115 | const code = transpileFile('./fixtures/only', { only: 'only' }) 116 | assert.ok(codeIsCovered(code)) 117 | }) 118 | 119 | it('ignores files not included by `only`', function () { 120 | const code = transpileFile('./fixtures/ignored', { only: 'only' }) 121 | assert.ok(!codeIsCovered(code)) 122 | }) 123 | 124 | it('favors only over ignore', function () { 125 | const code = transpileFile('./fixtures/only', { only: 'only', ignore: 'only' }) 126 | assert.ok(codeIsCovered(code)) 127 | }) 128 | 129 | // Need this test because some source files may be differented bundles 130 | // but executed in the same execution context (same page). 131 | // 132 | // For a real-world case see: 133 | // 134 | // https://github.com/dtinth/babel-plugin-__coverage__/issues/8 135 | // 136 | it('shalt not disregard previous coverage data if the code is the same', function () { 137 | try { 138 | { 139 | const instrumentedCode = instrument( 140 | 'var a = 1', 141 | 'tests/no_override.js' 142 | ) 143 | eval(instrumentedCode) 144 | assert.equal(__coverage__['tests/no_override.js'].s['1'], 1) 145 | assert.equal(__coverage__['tests/no_override.js'].s['2'], undefined) 146 | } 147 | { 148 | const instrumentedCode = instrument( 149 | 'var a = 1', 150 | 'tests/no_override.js' 151 | ) 152 | eval(instrumentedCode) 153 | assert.equal(__coverage__['tests/no_override.js'].s['1'], 2) 154 | } 155 | } finally { 156 | delete __coverage__['tests/no_override.js'] 157 | } 158 | }) 159 | 160 | // Need this test because some source files may be hot-reloaded. 161 | // This makes the coverage data out of sync and leads to run-time errors :( 162 | // 163 | // For a real-world case see: 164 | // 165 | // https://github.com/dtinth/babel-plugin-__coverage__/issues/8#issuecomment-209548685 166 | // 167 | it('shall supersede old coverage object when code is changed', function () { 168 | try { 169 | { 170 | const instrumentedCode = instrument( 171 | 'var a = 1', 172 | 'tests/yes_override.js' 173 | ) 174 | eval(instrumentedCode) 175 | assert.equal(__coverage__['tests/yes_override.js'].s['1'], 1) 176 | assert.equal(__coverage__['tests/yes_override.js'].s['2'], undefined) 177 | } 178 | { 179 | const instrumentedCode = instrument( 180 | 'var a = 1; var b = 1', 181 | 'tests/yes_override.js' 182 | ) 183 | eval(instrumentedCode) 184 | assert.equal(__coverage__['tests/yes_override.js'].s['1'], 1) 185 | assert.equal(__coverage__['tests/yes_override.js'].s['2'], 1) 186 | } 187 | } finally { 188 | delete __coverage__['tests/yes_override.js'] 189 | } 190 | }) 191 | }) 192 | 193 | // --------------------------------------------------------------------------- 194 | // Helper functions. 195 | // --------------------------------------------------------------------------- 196 | 197 | function testStatementCoverage (code, expected) { 198 | it('`' + code + '` -> ' + expected, function () { 199 | const coverageData = extractCoverageData(code).s 200 | assert.equal(Object.keys(coverageData).length, expected) 201 | }) 202 | } 203 | 204 | // Very crude hack to parse coverage data from the source map!! 205 | function extractCoverageData (code) { 206 | const instrumentedCode = instrument(code) 207 | { 208 | const match = instrumentedCode.match(/'(\{"path[^']+)/) 209 | if (match) return JSON.parse(match[1]) 210 | } 211 | { 212 | const match = instrumentedCode.match(/"(\{\\"path(?:\\.|[^"])+)"/) 213 | if (match) return JSON.parse(JSON.parse(match[0])) 214 | } 215 | } 216 | 217 | function instrument (code, filename) { 218 | return Babel.transform(code, { 219 | babelrc: false, 220 | plugins: [ coveragePlugin ], 221 | filename: filename 222 | }).code 223 | } 224 | 225 | function transpileFile (filename, pluginOptions) { 226 | return Babel.transformFileSync(require.resolve(filename), { 227 | presets: [ 'es2015' ], 228 | plugins: [ [ coveragePlugin, pluginOptions ] ], 229 | babelrc: false 230 | }).code 231 | } 232 | 233 | function codeIsCovered (code) { 234 | return code.indexOf('_cover__') !== -1 235 | } 236 | }) 237 | --------------------------------------------------------------------------------