| 1 | const { createHash } = require('crypto');
|
|---|
| 2 | const { template } = require('@babel/core');
|
|---|
| 3 | const { defaults } = require('@istanbuljs/schema');
|
|---|
| 4 | const { SourceCoverage } = require('./source-coverage');
|
|---|
| 5 | const { SHA, MAGIC_KEY, MAGIC_VALUE } = require('./constants');
|
|---|
| 6 |
|
|---|
| 7 | // pattern for istanbul to ignore a section
|
|---|
| 8 | const COMMENT_RE = /^\s*istanbul\s+ignore\s+(if|else|next)(?=\W|$)/;
|
|---|
| 9 | // pattern for istanbul to ignore the whole file
|
|---|
| 10 | const COMMENT_FILE_RE = /^\s*istanbul\s+ignore\s+(file)(?=\W|$)/;
|
|---|
| 11 | // source map URL pattern
|
|---|
| 12 | const SOURCE_MAP_RE = /[#@]\s*sourceMappingURL=(.*)\s*$/m;
|
|---|
| 13 |
|
|---|
| 14 | // generate a variable name from hashing the supplied file path
|
|---|
| 15 | function genVar(filename) {
|
|---|
| 16 | const hash = createHash(SHA);
|
|---|
| 17 | hash.update(filename);
|
|---|
| 18 | return 'cov_' + parseInt(hash.digest('hex').substr(0, 12), 16).toString(36);
|
|---|
| 19 | }
|
|---|
| 20 |
|
|---|
| 21 | // VisitState holds the state of the visitor, provides helper functions
|
|---|
| 22 | // and is the `this` for the individual coverage visitors.
|
|---|
| 23 | class VisitState {
|
|---|
| 24 | constructor(
|
|---|
| 25 | types,
|
|---|
| 26 | sourceFilePath,
|
|---|
| 27 | inputSourceMap,
|
|---|
| 28 | ignoreClassMethods = [],
|
|---|
| 29 | reportLogic = false
|
|---|
| 30 | ) {
|
|---|
| 31 | this.varName = genVar(sourceFilePath);
|
|---|
| 32 | this.attrs = {};
|
|---|
| 33 | this.nextIgnore = null;
|
|---|
| 34 | this.cov = new SourceCoverage(sourceFilePath);
|
|---|
| 35 |
|
|---|
| 36 | if (typeof inputSourceMap !== 'undefined') {
|
|---|
| 37 | this.cov.inputSourceMap(inputSourceMap);
|
|---|
| 38 | }
|
|---|
| 39 | this.ignoreClassMethods = ignoreClassMethods;
|
|---|
| 40 | this.types = types;
|
|---|
| 41 | this.sourceMappingURL = null;
|
|---|
| 42 | this.reportLogic = reportLogic;
|
|---|
| 43 | }
|
|---|
| 44 |
|
|---|
| 45 | // should we ignore the node? Yes, if specifically ignoring
|
|---|
| 46 | // or if the node is generated.
|
|---|
| 47 | shouldIgnore(path) {
|
|---|
| 48 | return this.nextIgnore || !path.node.loc;
|
|---|
| 49 | }
|
|---|
| 50 |
|
|---|
| 51 | // extract the ignore comment hint (next|if|else) or null
|
|---|
| 52 | hintFor(node) {
|
|---|
| 53 | let hint = null;
|
|---|
| 54 | if (node.leadingComments) {
|
|---|
| 55 | node.leadingComments.forEach(c => {
|
|---|
| 56 | const v = (
|
|---|
| 57 | c.value || /* istanbul ignore next: paranoid check */ ''
|
|---|
| 58 | ).trim();
|
|---|
| 59 | const groups = v.match(COMMENT_RE);
|
|---|
| 60 | if (groups) {
|
|---|
| 61 | hint = groups[1];
|
|---|
| 62 | }
|
|---|
| 63 | });
|
|---|
| 64 | }
|
|---|
| 65 | return hint;
|
|---|
| 66 | }
|
|---|
| 67 |
|
|---|
| 68 | // extract a source map URL from comments and keep track of it
|
|---|
| 69 | maybeAssignSourceMapURL(node) {
|
|---|
| 70 | const extractURL = comments => {
|
|---|
| 71 | if (!comments) {
|
|---|
| 72 | return;
|
|---|
| 73 | }
|
|---|
| 74 | comments.forEach(c => {
|
|---|
| 75 | const v = (
|
|---|
| 76 | c.value || /* istanbul ignore next: paranoid check */ ''
|
|---|
| 77 | ).trim();
|
|---|
| 78 | const groups = v.match(SOURCE_MAP_RE);
|
|---|
| 79 | if (groups) {
|
|---|
| 80 | this.sourceMappingURL = groups[1];
|
|---|
| 81 | }
|
|---|
| 82 | });
|
|---|
| 83 | };
|
|---|
| 84 | extractURL(node.leadingComments);
|
|---|
| 85 | extractURL(node.trailingComments);
|
|---|
| 86 | }
|
|---|
| 87 |
|
|---|
| 88 | // for these expressions the statement counter needs to be hoisted, so
|
|---|
| 89 | // function name inference can be preserved
|
|---|
| 90 | counterNeedsHoisting(path) {
|
|---|
| 91 | return (
|
|---|
| 92 | path.isFunctionExpression() ||
|
|---|
| 93 | path.isArrowFunctionExpression() ||
|
|---|
| 94 | path.isClassExpression()
|
|---|
| 95 | );
|
|---|
| 96 | }
|
|---|
| 97 |
|
|---|
| 98 | // all the generic stuff that needs to be done on enter for every node
|
|---|
| 99 | onEnter(path) {
|
|---|
| 100 | const n = path.node;
|
|---|
| 101 |
|
|---|
| 102 | this.maybeAssignSourceMapURL(n);
|
|---|
| 103 |
|
|---|
| 104 | // if already ignoring, nothing more to do
|
|---|
| 105 | if (this.nextIgnore !== null) {
|
|---|
| 106 | return;
|
|---|
| 107 | }
|
|---|
| 108 | // check hint to see if ignore should be turned on
|
|---|
| 109 | const hint = this.hintFor(n);
|
|---|
| 110 | if (hint === 'next') {
|
|---|
| 111 | this.nextIgnore = n;
|
|---|
| 112 | return;
|
|---|
| 113 | }
|
|---|
| 114 | // else check custom node attribute set by a prior visitor
|
|---|
| 115 | if (this.getAttr(path.node, 'skip-all') !== null) {
|
|---|
| 116 | this.nextIgnore = n;
|
|---|
| 117 | }
|
|---|
| 118 |
|
|---|
| 119 | // else check for ignored class methods
|
|---|
| 120 | if (
|
|---|
| 121 | path.isFunctionExpression() &&
|
|---|
| 122 | this.ignoreClassMethods.some(
|
|---|
| 123 | name => path.node.id && name === path.node.id.name
|
|---|
| 124 | )
|
|---|
| 125 | ) {
|
|---|
| 126 | this.nextIgnore = n;
|
|---|
| 127 | return;
|
|---|
| 128 | }
|
|---|
| 129 | if (
|
|---|
| 130 | path.isClassMethod() &&
|
|---|
| 131 | this.ignoreClassMethods.some(name => name === path.node.key.name)
|
|---|
| 132 | ) {
|
|---|
| 133 | this.nextIgnore = n;
|
|---|
| 134 | return;
|
|---|
| 135 | }
|
|---|
| 136 | }
|
|---|
| 137 |
|
|---|
| 138 | // all the generic stuff on exit of a node,
|
|---|
| 139 | // including reseting ignores and custom node attrs
|
|---|
| 140 | onExit(path) {
|
|---|
| 141 | // restore ignore status, if needed
|
|---|
| 142 | if (path.node === this.nextIgnore) {
|
|---|
| 143 | this.nextIgnore = null;
|
|---|
| 144 | }
|
|---|
| 145 | // nuke all attributes for the node
|
|---|
| 146 | delete path.node.__cov__;
|
|---|
| 147 | }
|
|---|
| 148 |
|
|---|
| 149 | // set a node attribute for the supplied node
|
|---|
| 150 | setAttr(node, name, value) {
|
|---|
| 151 | node.__cov__ = node.__cov__ || {};
|
|---|
| 152 | node.__cov__[name] = value;
|
|---|
| 153 | }
|
|---|
| 154 |
|
|---|
| 155 | // retrieve a node attribute for the supplied node or null
|
|---|
| 156 | getAttr(node, name) {
|
|---|
| 157 | const c = node.__cov__;
|
|---|
| 158 | if (!c) {
|
|---|
| 159 | return null;
|
|---|
| 160 | }
|
|---|
| 161 | return c[name];
|
|---|
| 162 | }
|
|---|
| 163 |
|
|---|
| 164 | //
|
|---|
| 165 | increase(type, id, index) {
|
|---|
| 166 | const T = this.types;
|
|---|
| 167 | const wrap =
|
|---|
| 168 | index !== null
|
|---|
| 169 | ? // If `index` present, turn `x` into `x[index]`.
|
|---|
| 170 | x => T.memberExpression(x, T.numericLiteral(index), true)
|
|---|
| 171 | : x => x;
|
|---|
| 172 | return T.updateExpression(
|
|---|
| 173 | '++',
|
|---|
| 174 | wrap(
|
|---|
| 175 | T.memberExpression(
|
|---|
| 176 | T.memberExpression(
|
|---|
| 177 | T.callExpression(T.identifier(this.varName), []),
|
|---|
| 178 | T.identifier(type)
|
|---|
| 179 | ),
|
|---|
| 180 | T.numericLiteral(id),
|
|---|
| 181 | true
|
|---|
| 182 | )
|
|---|
| 183 | )
|
|---|
| 184 | );
|
|---|
| 185 | }
|
|---|
| 186 |
|
|---|
| 187 | // Reads the logic expression conditions and conditionally increments truthy counter.
|
|---|
| 188 | increaseTrue(type, id, index, node) {
|
|---|
| 189 | const T = this.types;
|
|---|
| 190 | const tempName = `${this.varName}_temp`;
|
|---|
| 191 |
|
|---|
| 192 | return T.sequenceExpression([
|
|---|
| 193 | T.assignmentExpression(
|
|---|
| 194 | '=',
|
|---|
| 195 | T.memberExpression(
|
|---|
| 196 | T.callExpression(T.identifier(this.varName), []),
|
|---|
| 197 | T.identifier(tempName)
|
|---|
| 198 | ),
|
|---|
| 199 | node // Only evaluates once.
|
|---|
| 200 | ),
|
|---|
| 201 | T.parenthesizedExpression(
|
|---|
| 202 | T.conditionalExpression(
|
|---|
| 203 | this.validateTrueNonTrivial(T, tempName),
|
|---|
| 204 | this.increase(type, id, index),
|
|---|
| 205 | T.nullLiteral()
|
|---|
| 206 | )
|
|---|
| 207 | ),
|
|---|
| 208 | T.memberExpression(
|
|---|
| 209 | T.callExpression(T.identifier(this.varName), []),
|
|---|
| 210 | T.identifier(tempName)
|
|---|
| 211 | )
|
|---|
| 212 | ]);
|
|---|
| 213 | }
|
|---|
| 214 |
|
|---|
| 215 | validateTrueNonTrivial(T, tempName) {
|
|---|
| 216 | return T.logicalExpression(
|
|---|
| 217 | '&&',
|
|---|
| 218 | T.memberExpression(
|
|---|
| 219 | T.callExpression(T.identifier(this.varName), []),
|
|---|
| 220 | T.identifier(tempName)
|
|---|
| 221 | ),
|
|---|
| 222 | T.logicalExpression(
|
|---|
| 223 | '&&',
|
|---|
| 224 | T.parenthesizedExpression(
|
|---|
| 225 | T.logicalExpression(
|
|---|
| 226 | '||',
|
|---|
| 227 | T.unaryExpression(
|
|---|
| 228 | '!',
|
|---|
| 229 | T.callExpression(
|
|---|
| 230 | T.memberExpression(
|
|---|
| 231 | T.identifier('Array'),
|
|---|
| 232 | T.identifier('isArray')
|
|---|
| 233 | ),
|
|---|
| 234 | [
|
|---|
| 235 | T.memberExpression(
|
|---|
| 236 | T.callExpression(
|
|---|
| 237 | T.identifier(this.varName),
|
|---|
| 238 | []
|
|---|
| 239 | ),
|
|---|
| 240 | T.identifier(tempName)
|
|---|
| 241 | )
|
|---|
| 242 | ]
|
|---|
| 243 | )
|
|---|
| 244 | ),
|
|---|
| 245 | T.memberExpression(
|
|---|
| 246 | T.memberExpression(
|
|---|
| 247 | T.callExpression(
|
|---|
| 248 | T.identifier(this.varName),
|
|---|
| 249 | []
|
|---|
| 250 | ),
|
|---|
| 251 | T.identifier(tempName)
|
|---|
| 252 | ),
|
|---|
| 253 | T.identifier('length')
|
|---|
| 254 | )
|
|---|
| 255 | )
|
|---|
| 256 | ),
|
|---|
| 257 | T.parenthesizedExpression(
|
|---|
| 258 | T.logicalExpression(
|
|---|
| 259 | '||',
|
|---|
| 260 | T.binaryExpression(
|
|---|
| 261 | '!==',
|
|---|
| 262 | T.callExpression(
|
|---|
| 263 | T.memberExpression(
|
|---|
| 264 | T.identifier('Object'),
|
|---|
| 265 | T.identifier('getPrototypeOf')
|
|---|
| 266 | ),
|
|---|
| 267 | [
|
|---|
| 268 | T.memberExpression(
|
|---|
| 269 | T.callExpression(
|
|---|
| 270 | T.identifier(this.varName),
|
|---|
| 271 | []
|
|---|
| 272 | ),
|
|---|
| 273 | T.identifier(tempName)
|
|---|
| 274 | )
|
|---|
| 275 | ]
|
|---|
| 276 | ),
|
|---|
| 277 | T.memberExpression(
|
|---|
| 278 | T.identifier('Object'),
|
|---|
| 279 | T.identifier('prototype')
|
|---|
| 280 | )
|
|---|
| 281 | ),
|
|---|
| 282 | T.memberExpression(
|
|---|
| 283 | T.callExpression(
|
|---|
| 284 | T.memberExpression(
|
|---|
| 285 | T.identifier('Object'),
|
|---|
| 286 | T.identifier('values')
|
|---|
| 287 | ),
|
|---|
| 288 | [
|
|---|
| 289 | T.memberExpression(
|
|---|
| 290 | T.callExpression(
|
|---|
| 291 | T.identifier(this.varName),
|
|---|
| 292 | []
|
|---|
| 293 | ),
|
|---|
| 294 | T.identifier(tempName)
|
|---|
| 295 | )
|
|---|
| 296 | ]
|
|---|
| 297 | ),
|
|---|
| 298 | T.identifier('length')
|
|---|
| 299 | )
|
|---|
| 300 | )
|
|---|
| 301 | )
|
|---|
| 302 | )
|
|---|
| 303 | );
|
|---|
| 304 | }
|
|---|
| 305 |
|
|---|
| 306 | insertCounter(path, increment) {
|
|---|
| 307 | const T = this.types;
|
|---|
| 308 | if (path.isBlockStatement()) {
|
|---|
| 309 | path.node.body.unshift(T.expressionStatement(increment));
|
|---|
| 310 | } else if (path.isStatement()) {
|
|---|
| 311 | path.insertBefore(T.expressionStatement(increment));
|
|---|
| 312 | } else if (
|
|---|
| 313 | this.counterNeedsHoisting(path) &&
|
|---|
| 314 | T.isVariableDeclarator(path.parentPath)
|
|---|
| 315 | ) {
|
|---|
| 316 | // make an attempt to hoist the statement counter, so that
|
|---|
| 317 | // function names are maintained.
|
|---|
| 318 | const parent = path.parentPath.parentPath;
|
|---|
| 319 | if (parent && T.isExportNamedDeclaration(parent.parentPath)) {
|
|---|
| 320 | parent.parentPath.insertBefore(
|
|---|
| 321 | T.expressionStatement(increment)
|
|---|
| 322 | );
|
|---|
| 323 | } else if (
|
|---|
| 324 | parent &&
|
|---|
| 325 | (T.isProgram(parent.parentPath) ||
|
|---|
| 326 | T.isBlockStatement(parent.parentPath))
|
|---|
| 327 | ) {
|
|---|
| 328 | parent.insertBefore(T.expressionStatement(increment));
|
|---|
| 329 | } else {
|
|---|
| 330 | path.replaceWith(T.sequenceExpression([increment, path.node]));
|
|---|
| 331 | }
|
|---|
| 332 | } /* istanbul ignore else: not expected */ else if (
|
|---|
| 333 | path.isExpression()
|
|---|
| 334 | ) {
|
|---|
| 335 | path.replaceWith(T.sequenceExpression([increment, path.node]));
|
|---|
| 336 | } else {
|
|---|
| 337 | console.error(
|
|---|
| 338 | 'Unable to insert counter for node type:',
|
|---|
| 339 | path.node.type
|
|---|
| 340 | );
|
|---|
| 341 | }
|
|---|
| 342 | }
|
|---|
| 343 |
|
|---|
| 344 | insertStatementCounter(path) {
|
|---|
| 345 | /* istanbul ignore if: paranoid check */
|
|---|
| 346 | if (!(path.node && path.node.loc)) {
|
|---|
| 347 | return;
|
|---|
| 348 | }
|
|---|
| 349 | const index = this.cov.newStatement(path.node.loc);
|
|---|
| 350 | const increment = this.increase('s', index, null);
|
|---|
| 351 | this.insertCounter(path, increment);
|
|---|
| 352 | }
|
|---|
| 353 |
|
|---|
| 354 | insertFunctionCounter(path) {
|
|---|
| 355 | const T = this.types;
|
|---|
| 356 | /* istanbul ignore if: paranoid check */
|
|---|
| 357 | if (!(path.node && path.node.loc)) {
|
|---|
| 358 | return;
|
|---|
| 359 | }
|
|---|
| 360 | const n = path.node;
|
|---|
| 361 |
|
|---|
| 362 | let dloc = null;
|
|---|
| 363 | // get location for declaration
|
|---|
| 364 | switch (n.type) {
|
|---|
| 365 | case 'FunctionDeclaration':
|
|---|
| 366 | case 'FunctionExpression':
|
|---|
| 367 | /* istanbul ignore else: paranoid check */
|
|---|
| 368 | if (n.id) {
|
|---|
| 369 | dloc = n.id.loc;
|
|---|
| 370 | }
|
|---|
| 371 | break;
|
|---|
| 372 | }
|
|---|
| 373 | if (!dloc) {
|
|---|
| 374 | dloc = {
|
|---|
| 375 | start: n.loc.start,
|
|---|
| 376 | end: { line: n.loc.start.line, column: n.loc.start.column + 1 }
|
|---|
| 377 | };
|
|---|
| 378 | }
|
|---|
| 379 |
|
|---|
| 380 | const name = path.node.id ? path.node.id.name : path.node.name;
|
|---|
| 381 | const index = this.cov.newFunction(name, dloc, path.node.body.loc);
|
|---|
| 382 | const increment = this.increase('f', index, null);
|
|---|
| 383 | const body = path.get('body');
|
|---|
| 384 | /* istanbul ignore else: not expected */
|
|---|
| 385 | if (body.isBlockStatement()) {
|
|---|
| 386 | body.node.body.unshift(T.expressionStatement(increment));
|
|---|
| 387 | } else {
|
|---|
| 388 | console.error(
|
|---|
| 389 | 'Unable to process function body node type:',
|
|---|
| 390 | path.node.type
|
|---|
| 391 | );
|
|---|
| 392 | }
|
|---|
| 393 | }
|
|---|
| 394 |
|
|---|
| 395 | getBranchIncrement(branchName, loc) {
|
|---|
| 396 | const index = this.cov.addBranchPath(branchName, loc);
|
|---|
| 397 | return this.increase('b', branchName, index);
|
|---|
| 398 | }
|
|---|
| 399 |
|
|---|
| 400 | getBranchLogicIncrement(path, branchName, loc) {
|
|---|
| 401 | const index = this.cov.addBranchPath(branchName, loc);
|
|---|
| 402 | return [
|
|---|
| 403 | this.increase('b', branchName, index),
|
|---|
| 404 | this.increaseTrue('bT', branchName, index, path.node)
|
|---|
| 405 | ];
|
|---|
| 406 | }
|
|---|
| 407 |
|
|---|
| 408 | insertBranchCounter(path, branchName, loc) {
|
|---|
| 409 | const increment = this.getBranchIncrement(
|
|---|
| 410 | branchName,
|
|---|
| 411 | loc || path.node.loc
|
|---|
| 412 | );
|
|---|
| 413 | this.insertCounter(path, increment);
|
|---|
| 414 | }
|
|---|
| 415 |
|
|---|
| 416 | findLeaves(node, accumulator, parent, property) {
|
|---|
| 417 | if (!node) {
|
|---|
| 418 | return;
|
|---|
| 419 | }
|
|---|
| 420 | if (node.type === 'LogicalExpression') {
|
|---|
| 421 | const hint = this.hintFor(node);
|
|---|
| 422 | if (hint !== 'next') {
|
|---|
| 423 | this.findLeaves(node.left, accumulator, node, 'left');
|
|---|
| 424 | this.findLeaves(node.right, accumulator, node, 'right');
|
|---|
| 425 | }
|
|---|
| 426 | } else {
|
|---|
| 427 | accumulator.push({
|
|---|
| 428 | node,
|
|---|
| 429 | parent,
|
|---|
| 430 | property
|
|---|
| 431 | });
|
|---|
| 432 | }
|
|---|
| 433 | }
|
|---|
| 434 | }
|
|---|
| 435 |
|
|---|
| 436 | // generic function that takes a set of visitor methods and
|
|---|
| 437 | // returns a visitor object with `enter` and `exit` properties,
|
|---|
| 438 | // such that:
|
|---|
| 439 | //
|
|---|
| 440 | // * standard entry processing is done
|
|---|
| 441 | // * the supplied visitors are called only when ignore is not in effect
|
|---|
| 442 | // This relieves them from worrying about ignore states and generated nodes.
|
|---|
| 443 | // * standard exit processing is done
|
|---|
| 444 | //
|
|---|
| 445 | function entries(...enter) {
|
|---|
| 446 | // the enter function
|
|---|
| 447 | const wrappedEntry = function(path, node) {
|
|---|
| 448 | this.onEnter(path);
|
|---|
| 449 | if (this.shouldIgnore(path)) {
|
|---|
| 450 | return;
|
|---|
| 451 | }
|
|---|
| 452 | enter.forEach(e => {
|
|---|
| 453 | e.call(this, path, node);
|
|---|
| 454 | });
|
|---|
| 455 | };
|
|---|
| 456 | const exit = function(path, node) {
|
|---|
| 457 | this.onExit(path, node);
|
|---|
| 458 | };
|
|---|
| 459 | return {
|
|---|
| 460 | enter: wrappedEntry,
|
|---|
| 461 | exit
|
|---|
| 462 | };
|
|---|
| 463 | }
|
|---|
| 464 |
|
|---|
| 465 | function coverStatement(path) {
|
|---|
| 466 | this.insertStatementCounter(path);
|
|---|
| 467 | }
|
|---|
| 468 |
|
|---|
| 469 | /* istanbul ignore next: no node.js support */
|
|---|
| 470 | function coverAssignmentPattern(path) {
|
|---|
| 471 | const n = path.node;
|
|---|
| 472 | const b = this.cov.newBranch('default-arg', n.loc);
|
|---|
| 473 | this.insertBranchCounter(path.get('right'), b);
|
|---|
| 474 | }
|
|---|
| 475 |
|
|---|
| 476 | function coverFunction(path) {
|
|---|
| 477 | this.insertFunctionCounter(path);
|
|---|
| 478 | }
|
|---|
| 479 |
|
|---|
| 480 | function coverVariableDeclarator(path) {
|
|---|
| 481 | this.insertStatementCounter(path.get('init'));
|
|---|
| 482 | }
|
|---|
| 483 |
|
|---|
| 484 | function coverClassPropDeclarator(path) {
|
|---|
| 485 | this.insertStatementCounter(path.get('value'));
|
|---|
| 486 | }
|
|---|
| 487 |
|
|---|
| 488 | function makeBlock(path) {
|
|---|
| 489 | const T = this.types;
|
|---|
| 490 | if (!path.node) {
|
|---|
| 491 | path.replaceWith(T.blockStatement([]));
|
|---|
| 492 | }
|
|---|
| 493 | if (!path.isBlockStatement()) {
|
|---|
| 494 | path.replaceWith(T.blockStatement([path.node]));
|
|---|
| 495 | path.node.loc = path.node.body[0].loc;
|
|---|
| 496 | path.node.body[0].leadingComments = path.node.leadingComments;
|
|---|
| 497 | path.node.leadingComments = undefined;
|
|---|
| 498 | }
|
|---|
| 499 | }
|
|---|
| 500 |
|
|---|
| 501 | function blockProp(prop) {
|
|---|
| 502 | return function(path) {
|
|---|
| 503 | makeBlock.call(this, path.get(prop));
|
|---|
| 504 | };
|
|---|
| 505 | }
|
|---|
| 506 |
|
|---|
| 507 | function makeParenthesizedExpressionForNonIdentifier(path) {
|
|---|
| 508 | const T = this.types;
|
|---|
| 509 | if (path.node && !path.isIdentifier()) {
|
|---|
| 510 | path.replaceWith(T.parenthesizedExpression(path.node));
|
|---|
| 511 | }
|
|---|
| 512 | }
|
|---|
| 513 |
|
|---|
| 514 | function parenthesizedExpressionProp(prop) {
|
|---|
| 515 | return function(path) {
|
|---|
| 516 | makeParenthesizedExpressionForNonIdentifier.call(this, path.get(prop));
|
|---|
| 517 | };
|
|---|
| 518 | }
|
|---|
| 519 |
|
|---|
| 520 | function convertArrowExpression(path) {
|
|---|
| 521 | const n = path.node;
|
|---|
| 522 | const T = this.types;
|
|---|
| 523 | if (!T.isBlockStatement(n.body)) {
|
|---|
| 524 | const bloc = n.body.loc;
|
|---|
| 525 | if (n.expression === true) {
|
|---|
| 526 | n.expression = false;
|
|---|
| 527 | }
|
|---|
| 528 | n.body = T.blockStatement([T.returnStatement(n.body)]);
|
|---|
| 529 | // restore body location
|
|---|
| 530 | n.body.loc = bloc;
|
|---|
| 531 | // set up the location for the return statement so it gets
|
|---|
| 532 | // instrumented
|
|---|
| 533 | n.body.body[0].loc = bloc;
|
|---|
| 534 | }
|
|---|
| 535 | }
|
|---|
| 536 |
|
|---|
| 537 | function coverIfBranches(path) {
|
|---|
| 538 | const n = path.node;
|
|---|
| 539 | const hint = this.hintFor(n);
|
|---|
| 540 | const ignoreIf = hint === 'if';
|
|---|
| 541 | const ignoreElse = hint === 'else';
|
|---|
| 542 | const branch = this.cov.newBranch('if', n.loc);
|
|---|
| 543 |
|
|---|
| 544 | if (ignoreIf) {
|
|---|
| 545 | this.setAttr(n.consequent, 'skip-all', true);
|
|---|
| 546 | } else {
|
|---|
| 547 | this.insertBranchCounter(path.get('consequent'), branch, n.loc);
|
|---|
| 548 | }
|
|---|
| 549 | if (ignoreElse) {
|
|---|
| 550 | this.setAttr(n.alternate, 'skip-all', true);
|
|---|
| 551 | } else {
|
|---|
| 552 | this.insertBranchCounter(path.get('alternate'), branch);
|
|---|
| 553 | }
|
|---|
| 554 | }
|
|---|
| 555 |
|
|---|
| 556 | function createSwitchBranch(path) {
|
|---|
| 557 | const b = this.cov.newBranch('switch', path.node.loc);
|
|---|
| 558 | this.setAttr(path.node, 'branchName', b);
|
|---|
| 559 | }
|
|---|
| 560 |
|
|---|
| 561 | function coverSwitchCase(path) {
|
|---|
| 562 | const T = this.types;
|
|---|
| 563 | const b = this.getAttr(path.parentPath.node, 'branchName');
|
|---|
| 564 | /* istanbul ignore if: paranoid check */
|
|---|
| 565 | if (b === null) {
|
|---|
| 566 | throw new Error('Unable to get switch branch name');
|
|---|
| 567 | }
|
|---|
| 568 | const increment = this.getBranchIncrement(b, path.node.loc);
|
|---|
| 569 | path.node.consequent.unshift(T.expressionStatement(increment));
|
|---|
| 570 | }
|
|---|
| 571 |
|
|---|
| 572 | function coverTernary(path) {
|
|---|
| 573 | const n = path.node;
|
|---|
| 574 | const branch = this.cov.newBranch('cond-expr', path.node.loc);
|
|---|
| 575 | const cHint = this.hintFor(n.consequent);
|
|---|
| 576 | const aHint = this.hintFor(n.alternate);
|
|---|
| 577 |
|
|---|
| 578 | if (cHint !== 'next') {
|
|---|
| 579 | this.insertBranchCounter(path.get('consequent'), branch);
|
|---|
| 580 | }
|
|---|
| 581 | if (aHint !== 'next') {
|
|---|
| 582 | this.insertBranchCounter(path.get('alternate'), branch);
|
|---|
| 583 | }
|
|---|
| 584 | }
|
|---|
| 585 |
|
|---|
| 586 | function coverLogicalExpression(path) {
|
|---|
| 587 | const T = this.types;
|
|---|
| 588 | if (path.parentPath.node.type === 'LogicalExpression') {
|
|---|
| 589 | return; // already processed
|
|---|
| 590 | }
|
|---|
| 591 | const leaves = [];
|
|---|
| 592 | this.findLeaves(path.node, leaves);
|
|---|
| 593 | const b = this.cov.newBranch(
|
|---|
| 594 | 'binary-expr',
|
|---|
| 595 | path.node.loc,
|
|---|
| 596 | this.reportLogic
|
|---|
| 597 | );
|
|---|
| 598 | for (let i = 0; i < leaves.length; i += 1) {
|
|---|
| 599 | const leaf = leaves[i];
|
|---|
| 600 | const hint = this.hintFor(leaf.node);
|
|---|
| 601 | if (hint === 'next') {
|
|---|
| 602 | continue;
|
|---|
| 603 | }
|
|---|
| 604 |
|
|---|
| 605 | if (this.reportLogic) {
|
|---|
| 606 | const increment = this.getBranchLogicIncrement(
|
|---|
| 607 | leaf,
|
|---|
| 608 | b,
|
|---|
| 609 | leaf.node.loc
|
|---|
| 610 | );
|
|---|
| 611 | if (!increment[0]) {
|
|---|
| 612 | continue;
|
|---|
| 613 | }
|
|---|
| 614 | leaf.parent[leaf.property] = T.sequenceExpression([
|
|---|
| 615 | increment[0],
|
|---|
| 616 | increment[1]
|
|---|
| 617 | ]);
|
|---|
| 618 | continue;
|
|---|
| 619 | }
|
|---|
| 620 |
|
|---|
| 621 | const increment = this.getBranchIncrement(b, leaf.node.loc);
|
|---|
| 622 | if (!increment) {
|
|---|
| 623 | continue;
|
|---|
| 624 | }
|
|---|
| 625 | leaf.parent[leaf.property] = T.sequenceExpression([
|
|---|
| 626 | increment,
|
|---|
| 627 | leaf.node
|
|---|
| 628 | ]);
|
|---|
| 629 | }
|
|---|
| 630 | }
|
|---|
| 631 |
|
|---|
| 632 | const codeVisitor = {
|
|---|
| 633 | ArrowFunctionExpression: entries(convertArrowExpression, coverFunction),
|
|---|
| 634 | AssignmentPattern: entries(coverAssignmentPattern),
|
|---|
| 635 | BlockStatement: entries(), // ignore processing only
|
|---|
| 636 | ExportDefaultDeclaration: entries(), // ignore processing only
|
|---|
| 637 | ExportNamedDeclaration: entries(), // ignore processing only
|
|---|
| 638 | ClassMethod: entries(coverFunction),
|
|---|
| 639 | ClassDeclaration: entries(parenthesizedExpressionProp('superClass')),
|
|---|
| 640 | ClassProperty: entries(coverClassPropDeclarator),
|
|---|
| 641 | ClassPrivateProperty: entries(coverClassPropDeclarator),
|
|---|
| 642 | ObjectMethod: entries(coverFunction),
|
|---|
| 643 | ExpressionStatement: entries(coverStatement),
|
|---|
| 644 | BreakStatement: entries(coverStatement),
|
|---|
| 645 | ContinueStatement: entries(coverStatement),
|
|---|
| 646 | DebuggerStatement: entries(coverStatement),
|
|---|
| 647 | ReturnStatement: entries(coverStatement),
|
|---|
| 648 | ThrowStatement: entries(coverStatement),
|
|---|
| 649 | TryStatement: entries(coverStatement),
|
|---|
| 650 | VariableDeclaration: entries(), // ignore processing only
|
|---|
| 651 | VariableDeclarator: entries(coverVariableDeclarator),
|
|---|
| 652 | IfStatement: entries(
|
|---|
| 653 | blockProp('consequent'),
|
|---|
| 654 | blockProp('alternate'),
|
|---|
| 655 | coverStatement,
|
|---|
| 656 | coverIfBranches
|
|---|
| 657 | ),
|
|---|
| 658 | ForStatement: entries(blockProp('body'), coverStatement),
|
|---|
| 659 | ForInStatement: entries(blockProp('body'), coverStatement),
|
|---|
| 660 | ForOfStatement: entries(blockProp('body'), coverStatement),
|
|---|
| 661 | WhileStatement: entries(blockProp('body'), coverStatement),
|
|---|
| 662 | DoWhileStatement: entries(blockProp('body'), coverStatement),
|
|---|
| 663 | SwitchStatement: entries(createSwitchBranch, coverStatement),
|
|---|
| 664 | SwitchCase: entries(coverSwitchCase),
|
|---|
| 665 | WithStatement: entries(blockProp('body'), coverStatement),
|
|---|
| 666 | FunctionDeclaration: entries(coverFunction),
|
|---|
| 667 | FunctionExpression: entries(coverFunction),
|
|---|
| 668 | LabeledStatement: entries(coverStatement),
|
|---|
| 669 | ConditionalExpression: entries(coverTernary),
|
|---|
| 670 | LogicalExpression: entries(coverLogicalExpression)
|
|---|
| 671 | };
|
|---|
| 672 | const globalTemplateAlteredFunction = template(`
|
|---|
| 673 | var Function = (function(){}).constructor;
|
|---|
| 674 | var global = (new Function(GLOBAL_COVERAGE_SCOPE))();
|
|---|
| 675 | `);
|
|---|
| 676 | const globalTemplateFunction = template(`
|
|---|
| 677 | var global = (new Function(GLOBAL_COVERAGE_SCOPE))();
|
|---|
| 678 | `);
|
|---|
| 679 | const globalTemplateVariable = template(`
|
|---|
| 680 | var global = GLOBAL_COVERAGE_SCOPE;
|
|---|
| 681 | `);
|
|---|
| 682 | // the template to insert at the top of the program.
|
|---|
| 683 | const coverageTemplate = template(
|
|---|
| 684 | `
|
|---|
| 685 | function COVERAGE_FUNCTION () {
|
|---|
| 686 | var path = PATH;
|
|---|
| 687 | var hash = HASH;
|
|---|
| 688 | GLOBAL_COVERAGE_TEMPLATE
|
|---|
| 689 | var gcv = GLOBAL_COVERAGE_VAR;
|
|---|
| 690 | var coverageData = INITIAL;
|
|---|
| 691 | var coverage = global[gcv] || (global[gcv] = {});
|
|---|
| 692 | if (!coverage[path] || coverage[path].hash !== hash) {
|
|---|
| 693 | coverage[path] = coverageData;
|
|---|
| 694 | }
|
|---|
| 695 |
|
|---|
| 696 | var actualCoverage = coverage[path];
|
|---|
| 697 | {
|
|---|
| 698 | // @ts-ignore
|
|---|
| 699 | COVERAGE_FUNCTION = function () {
|
|---|
| 700 | return actualCoverage;
|
|---|
| 701 | }
|
|---|
| 702 | }
|
|---|
| 703 |
|
|---|
| 704 | return actualCoverage;
|
|---|
| 705 | }
|
|---|
| 706 | `,
|
|---|
| 707 | { preserveComments: true }
|
|---|
| 708 | );
|
|---|
| 709 | // the rewire plugin (and potentially other babel middleware)
|
|---|
| 710 | // may cause files to be instrumented twice, see:
|
|---|
| 711 | // https://github.com/istanbuljs/babel-plugin-istanbul/issues/94
|
|---|
| 712 | // we should only instrument code for coverage the first time
|
|---|
| 713 | // it's run through istanbul-lib-instrument.
|
|---|
| 714 | function alreadyInstrumented(path, visitState) {
|
|---|
| 715 | return path.scope.hasBinding(visitState.varName);
|
|---|
| 716 | }
|
|---|
| 717 | function shouldIgnoreFile(programNode) {
|
|---|
| 718 | return (
|
|---|
| 719 | programNode.parent &&
|
|---|
| 720 | programNode.parent.comments.some(c => COMMENT_FILE_RE.test(c.value))
|
|---|
| 721 | );
|
|---|
| 722 | }
|
|---|
| 723 |
|
|---|
| 724 | /**
|
|---|
| 725 | * programVisitor is a `babel` adaptor for instrumentation.
|
|---|
| 726 | * It returns an object with two methods `enter` and `exit`.
|
|---|
| 727 | * These should be assigned to or called from `Program` entry and exit functions
|
|---|
| 728 | * in a babel visitor.
|
|---|
| 729 | * These functions do not make assumptions about the state set by Babel and thus
|
|---|
| 730 | * can be used in a context other than a Babel plugin.
|
|---|
| 731 | *
|
|---|
| 732 | * The exit function returns an object that currently has the following keys:
|
|---|
| 733 | *
|
|---|
| 734 | * `fileCoverage` - the file coverage object created for the source file.
|
|---|
| 735 | * `sourceMappingURL` - any source mapping URL found when processing the file.
|
|---|
| 736 | *
|
|---|
| 737 | * @param {Object} types - an instance of babel-types.
|
|---|
| 738 | * @param {string} sourceFilePath - the path to source file.
|
|---|
| 739 | * @param {Object} opts - additional options.
|
|---|
| 740 | * @param {string} [opts.coverageVariable=__coverage__] the global coverage variable name.
|
|---|
| 741 | * @param {boolean} [opts.reportLogic=false] report boolean value of logical expressions.
|
|---|
| 742 | * @param {string} [opts.coverageGlobalScope=this] the global coverage variable scope.
|
|---|
| 743 | * @param {boolean} [opts.coverageGlobalScopeFunc=true] use an evaluated function to find coverageGlobalScope.
|
|---|
| 744 | * @param {Array} [opts.ignoreClassMethods=[]] names of methods to ignore by default on classes.
|
|---|
| 745 | * @param {object} [opts.inputSourceMap=undefined] the input source map, that maps the uninstrumented code back to the
|
|---|
| 746 | * original code.
|
|---|
| 747 | */
|
|---|
| 748 | function programVisitor(types, sourceFilePath = 'unknown.js', opts = {}) {
|
|---|
| 749 | const T = types;
|
|---|
| 750 | opts = {
|
|---|
| 751 | ...defaults.instrumentVisitor,
|
|---|
| 752 | ...opts
|
|---|
| 753 | };
|
|---|
| 754 | const visitState = new VisitState(
|
|---|
| 755 | types,
|
|---|
| 756 | sourceFilePath,
|
|---|
| 757 | opts.inputSourceMap,
|
|---|
| 758 | opts.ignoreClassMethods,
|
|---|
| 759 | opts.reportLogic
|
|---|
| 760 | );
|
|---|
| 761 | return {
|
|---|
| 762 | enter(path) {
|
|---|
| 763 | if (shouldIgnoreFile(path.find(p => p.isProgram()))) {
|
|---|
| 764 | return;
|
|---|
| 765 | }
|
|---|
| 766 | if (alreadyInstrumented(path, visitState)) {
|
|---|
| 767 | return;
|
|---|
| 768 | }
|
|---|
| 769 | path.traverse(codeVisitor, visitState);
|
|---|
| 770 | },
|
|---|
| 771 | exit(path) {
|
|---|
| 772 | if (alreadyInstrumented(path, visitState)) {
|
|---|
| 773 | return;
|
|---|
| 774 | }
|
|---|
| 775 | visitState.cov.freeze();
|
|---|
| 776 | const coverageData = visitState.cov.toJSON();
|
|---|
| 777 | if (shouldIgnoreFile(path.find(p => p.isProgram()))) {
|
|---|
| 778 | return {
|
|---|
| 779 | fileCoverage: coverageData,
|
|---|
| 780 | sourceMappingURL: visitState.sourceMappingURL
|
|---|
| 781 | };
|
|---|
| 782 | }
|
|---|
| 783 | coverageData[MAGIC_KEY] = MAGIC_VALUE;
|
|---|
| 784 | const hash = createHash(SHA)
|
|---|
| 785 | .update(JSON.stringify(coverageData))
|
|---|
| 786 | .digest('hex');
|
|---|
| 787 | coverageData.hash = hash;
|
|---|
| 788 | if (
|
|---|
| 789 | coverageData.inputSourceMap &&
|
|---|
| 790 | Object.getPrototypeOf(coverageData.inputSourceMap) !==
|
|---|
| 791 | Object.prototype
|
|---|
| 792 | ) {
|
|---|
| 793 | coverageData.inputSourceMap = {
|
|---|
| 794 | ...coverageData.inputSourceMap
|
|---|
| 795 | };
|
|---|
| 796 | }
|
|---|
| 797 | const coverageNode = T.valueToNode(coverageData);
|
|---|
| 798 | delete coverageData[MAGIC_KEY];
|
|---|
| 799 | delete coverageData.hash;
|
|---|
| 800 | let gvTemplate;
|
|---|
| 801 | if (opts.coverageGlobalScopeFunc) {
|
|---|
| 802 | if (path.scope.getBinding('Function')) {
|
|---|
| 803 | gvTemplate = globalTemplateAlteredFunction({
|
|---|
| 804 | GLOBAL_COVERAGE_SCOPE: T.stringLiteral(
|
|---|
| 805 | 'return ' + opts.coverageGlobalScope
|
|---|
| 806 | )
|
|---|
| 807 | });
|
|---|
| 808 | } else {
|
|---|
| 809 | gvTemplate = globalTemplateFunction({
|
|---|
| 810 | GLOBAL_COVERAGE_SCOPE: T.stringLiteral(
|
|---|
| 811 | 'return ' + opts.coverageGlobalScope
|
|---|
| 812 | )
|
|---|
| 813 | });
|
|---|
| 814 | }
|
|---|
| 815 | } else {
|
|---|
| 816 | gvTemplate = globalTemplateVariable({
|
|---|
| 817 | GLOBAL_COVERAGE_SCOPE: opts.coverageGlobalScope
|
|---|
| 818 | });
|
|---|
| 819 | }
|
|---|
| 820 | const cv = coverageTemplate({
|
|---|
| 821 | GLOBAL_COVERAGE_VAR: T.stringLiteral(opts.coverageVariable),
|
|---|
| 822 | GLOBAL_COVERAGE_TEMPLATE: gvTemplate,
|
|---|
| 823 | COVERAGE_FUNCTION: T.identifier(visitState.varName),
|
|---|
| 824 | PATH: T.stringLiteral(sourceFilePath),
|
|---|
| 825 | INITIAL: coverageNode,
|
|---|
| 826 | HASH: T.stringLiteral(hash)
|
|---|
| 827 | });
|
|---|
| 828 | // explicitly call this.varName to ensure coverage is always initialized
|
|---|
| 829 | path.node.body.unshift(
|
|---|
| 830 | T.expressionStatement(
|
|---|
| 831 | T.callExpression(T.identifier(visitState.varName), [])
|
|---|
| 832 | )
|
|---|
| 833 | );
|
|---|
| 834 | path.node.body.unshift(cv);
|
|---|
| 835 | return {
|
|---|
| 836 | fileCoverage: coverageData,
|
|---|
| 837 | sourceMappingURL: visitState.sourceMappingURL
|
|---|
| 838 | };
|
|---|
| 839 | }
|
|---|
| 840 | };
|
|---|
| 841 | }
|
|---|
| 842 |
|
|---|
| 843 | module.exports = programVisitor;
|
|---|