├── .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 | 
12 | =============================
13 |
14 | [](https://badge.fury.io/js/babel-plugin-__coverage__)
15 | [](https://www.npmjs.com/package/babel-plugin-__coverage__)
16 | [](https://travis-ci.org/dtinth/babel-plugin-__coverage__)
17 | [](https://codecov.io/github/dtinth/babel-plugin-__coverage__?branch=master)
18 | 
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 | 
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 |
--------------------------------------------------------------------------------