IstanbulJS - レポート改善 - ソースコード修正

IstanbulJS - レポート改善 - ソースコード修正:


概要

IstanbulJS - レポート改善案で列挙した奇妙な点を解決するため、IstanbulJSのソースコードを修正する。TypeScript用に記載しているが、JavaScriptにも対応する。


前提

TypeScriptを使う場合は、予め [React + TypeScript] カバレッジレポートの取得方法 を実施しておくこと。


ルール

本修正で行うハイライトのルールを以下のように定義する。


ステートメント

  • 末尾の ; はステートメントではないため、ハイライトしない。
  • ステートメントの後に続く空白文字やコメントなど、プログラムとして意味の無い箇所はハイライトしない。
  • 複数行にまたがるステートメントは、全ての行をハイライトする。


ブランチ


if


  • else if 節の if キーワード以降を、複数行にまたがって全てハイライトする。


switch


  • case 節、 default 節の範囲は : までとし、後に続く空白文字やコメントなど、プログラムとして意味の無い箇所はハイライトしない。


関数

記法により、定義部の範囲を以下の通りとする。


Function Declaration

function foo(): void をハイライトする。

function foo(): void { 
 
} 


Function Expression

無名関数の場合
function(): void をハイライトする。

const foo = function(): void { 
 
} 
名前付き関数の場合
function foo(): void をハイライトする。

const foo = function foo(): void { 
 
} 


Arrow Function

括弧がある場合
(bar): void をハイライトする。

const foo = (bar): void => { 
 
} 
括弧がない場合
bar をハイライトする。

const foo = bar => { 
 
} 


Class Method

アクセス修飾子がある場合
foo(): void をハイライトする。

public foo(): void { 
 
} 
アクセス修飾子がない場合
foo(): void をハイライトする。

foo(): void { 
 
} 


修正ファイル


visitor.js


目的

関数の定義部を、上述のルールに沿うよう修正する。


パッケージ

istanbul-lib-instrument@1.10.2


コード

node_modules/istanbul-lib-instrument/dist/visitor.js
'use strict'; 
 
Object.defineProperty(exports, "__esModule", { 
    value: true 
}); 
 
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 
 
var _sourceCoverage = require('./source-coverage'); 
 
var _constants = require('./constants'); 
 
var _crypto = require('crypto'); 
 
var _babelTemplate = require('babel-template'); 
 
var _babelTemplate2 = _interopRequireDefault(_babelTemplate); 
 
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 
 
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 
 
// istanbul ignore comment pattern 
var COMMENT_RE = /^\s*istanbul\s+ignore\s+(if|else|next)(?=\W|$)/; 
// istanbul ignore file pattern 
var COMMENT_FILE_RE = /^\s*istanbul\s+ignore\s+(file)(?=\W|$)/; 
// source map URL pattern 
var SOURCE_MAP_RE = /[#@]\s*sourceMappingURL=(.*)\s*$/m; 
 
// generate a variable name from hashing the supplied file path 
function genVar(filename) { 
    var hash = (0, _crypto.createHash)(_constants.SHA); 
    hash.update(filename); 
    return 'cov_' + parseInt(hash.digest('hex').substr(0, 12), 16).toString(36); 
} 
 
// VisitState holds the state of the visitor, provides helper functions 
// and is the `this` for the individual coverage visitors. 
 
var VisitState = function () { 
    function VisitState(types, sourceFilePath, inputSourceMap) { 
        _classCallCheck(this, VisitState); 
 
        this.varName = genVar(sourceFilePath); 
        this.attrs = {}; 
        this.nextIgnore = null; 
        this.cov = new _sourceCoverage.SourceCoverage(sourceFilePath); 
 
        if (typeof inputSourceMap !== "undefined") { 
            this.cov.inputSourceMap(inputSourceMap); 
        } 
        this.types = types; 
        this.sourceMappingURL = null; 
    } 
 
    // should we ignore the node? Yes, if specifically ignoring 
    // or if the node is generated. 
 
 
    _createClass(VisitState, [{ 
        key: 'shouldIgnore', 
        value: function shouldIgnore(path) { 
            return this.nextIgnore || !path.node.loc; 
        } 
 
        // extract the ignore comment hint (next|if|else) or null 
 
    }, { 
        key: 'hintFor', 
        value: function hintFor(node) { 
            var hint = null; 
            if (node.leadingComments) { 
                node.leadingComments.forEach(function (c) { 
                    var v = (c.value || /* istanbul ignore next: paranoid check */"").trim(); 
                    var groups = v.match(COMMENT_RE); 
                    if (groups) { 
                        hint = groups[1]; 
                    } 
                }); 
            } 
            return hint; 
        } 
 
        // extract a source map URL from comments and keep track of it 
 
    }, { 
        key: 'maybeAssignSourceMapURL', 
        value: function maybeAssignSourceMapURL(node) { 
            var that = this; 
            var extractURL = function extractURL(comments) { 
                if (!comments) { 
                    return; 
                } 
                comments.forEach(function (c) { 
                    var v = (c.value || /* istanbul ignore next: paranoid check */"").trim(); 
                    var groups = v.match(SOURCE_MAP_RE); 
                    if (groups) { 
                        that.sourceMappingURL = groups[1]; 
                    } 
                }); 
            }; 
            extractURL(node.leadingComments); 
            extractURL(node.trailingComments); 
        } 
 
        // for these expressions the statement counter needs to be hoisted, so 
        // function name inference can be preserved 
 
    }, { 
        key: 'counterNeedsHoisting', 
        value: function counterNeedsHoisting(path) { 
            return path.isFunctionExpression() || path.isArrowFunctionExpression() || path.isClassExpression(); 
        } 
 
        // all the generic stuff that needs to be done on enter for every node 
 
    }, { 
        key: 'onEnter', 
        value: function onEnter(path) { 
            var n = path.node; 
 
            this.maybeAssignSourceMapURL(n); 
 
            // if already ignoring, nothing more to do 
            if (this.nextIgnore !== null) { 
                return; 
            } 
            // check hint to see if ignore should be turned on 
            var hint = this.hintFor(n); 
            if (hint === 'next') { 
                this.nextIgnore = n; 
                return; 
            } 
            // else check custom node attribute set by a prior visitor 
            if (this.getAttr(path.node, 'skip-all') !== null) { 
                this.nextIgnore = n; 
            } 
        } 
 
        // all the generic stuff on exit of a node, 
        // including reseting ignores and custom node attrs 
 
    }, { 
        key: 'onExit', 
        value: function onExit(path) { 
            // restore ignore status, if needed 
            if (path.node === this.nextIgnore) { 
                this.nextIgnore = null; 
            } 
            // nuke all attributes for the node 
            delete path.node.__cov__; 
        } 
 
        // set a node attribute for the supplied node 
 
    }, { 
        key: 'setAttr', 
        value: function setAttr(node, name, value) { 
            node.__cov__ = node.__cov__ || {}; 
            node.__cov__[name] = value; 
        } 
 
        // retrieve a node attribute for the supplied node or null 
 
    }, { 
        key: 'getAttr', 
        value: function getAttr(node, name) { 
            var c = node.__cov__; 
            if (!c) { 
                return null; 
            } 
            return c[name]; 
        } 
 
        // 
 
    }, { 
        key: 'increase', 
        value: function increase(type, id, index) { 
            var T = this.types; 
            var wrap = index !== null 
            // If `index` present, turn `x` into `x[index]`. 
            ? function (x) { 
                return T.memberExpression(x, T.numericLiteral(index), true); 
            } : function (x) { 
                return x; 
            }; 
            return T.updateExpression('++', wrap(T.memberExpression(T.memberExpression(T.identifier(this.varName), T.identifier(type)), T.numericLiteral(id), true))); 
        } 
    }, { 
        key: 'insertCounter', 
        value: function insertCounter(path, increment) { 
            var T = this.types; 
            if (path.isBlockStatement()) { 
                path.node.body.unshift(T.expressionStatement(increment)); 
            } else if (path.isStatement()) { 
                path.insertBefore(T.expressionStatement(increment)); 
            } else if (this.counterNeedsHoisting(path) && T.isVariableDeclarator(path.parentPath)) { 
                // make an attempt to hoist the statement counter, so that 
                // function names are maintained. 
                var parent = path.parentPath.parentPath; 
                if (parent && T.isExportNamedDeclaration(parent.parentPath)) { 
                    parent.parentPath.insertBefore(T.expressionStatement(increment)); 
                } else if (parent && (T.isProgram(parent.parentPath) || T.isBlockStatement(parent.parentPath))) { 
                    parent.insertBefore(T.expressionStatement(increment)); 
                } else { 
                    path.replaceWith(T.sequenceExpression([increment, path.node])); 
                } 
            } else /* istanbul ignore else: not expected */if (path.isExpression()) { 
                    path.replaceWith(T.sequenceExpression([increment, path.node])); 
                } else { 
                    console.error('Unable to insert counter for node type:', path.node.type); 
                } 
        } 
    }, { 
        key: 'insertStatementCounter', 
        value: function insertStatementCounter(path) { 
            /* istanbul ignore if: paranoid check */ 
            if (!(path.node && path.node.loc)) { 
                return; 
            } 
            if (path.getSource().slice(-1) === ';') { 
                path.node.loc.end.column--; 
            } 
            var index = this.cov.newStatement(path.node.loc); 
            var increment = this.increase('s', index, null); 
            this.insertCounter(path, increment); 
        } 
    }, { 
        key: 'insertFunctionCounter', 
        value: function insertFunctionCounter(path) { 
            var T = this.types; 
            /* istanbul ignore if: paranoid check */ 
            if (!(path.node && path.node.loc)) { 
                return; 
            } 
            var n = path.node; 
 
            var dloc = null; 
            var name = undefined; 
            var source = path.getSource(); 
            var getDeclLoc = function getDeclLoc(isParameterParenthesized) { 
                var offsetCol = null, 
                    offsetLine = null, 
                    lastParam = null; 
                if (n.params.length > 0) { 
                    lastParam = n.params[n.params.length - 1]; 
                    offsetCol = lastParam.end - n.start + (isParameterParenthesized ? 1 : 0); 
                } else { 
                    offsetCol = source.indexOf(")") + 1; 
                } 
                offsetLine = source.substr(0, offsetCol).split(/(?:\r?\n)|\r/).length - 1; 
                return { 
                    start: n.loc.start, 
                    end: { 
                        line: n.loc.start.line + offsetLine, 
                        column: n.loc.start.column + offsetCol 
                    } 
                }; 
            }; 
            // get location for declaration 
            switch (n.type) { 
                case "FunctionDeclaration": 
                    /* istanbul ignore else: paranoid check */ 
                    dloc = getDeclLoc(true); 
                    name = n.id ? n.id.name : n.name; 
                    break; 
                case "FunctionExpression": 
                    dloc = getDeclLoc(true); 
                    name = n.id ? n.id.name : n.name; 
                    break; 
                case "ClassMethod": 
                    dloc = getDeclLoc(true); 
                    name = n.key ? n.key.name : n.name; 
                    break; 
                case "ArrowFunctionExpression": 
                    var isParameterParenthesized = source.substr(0, 1) === "("; 
                    dloc = getDeclLoc(isParameterParenthesized); 
 
                    var isReturnStatement = n.body.body.length === 1 && n.body.body[0].type === "ReturnStatement"; 
                    var returnStatement = isReturnStatement ? n.body.body[0] : undefined; 
                    var isReturnStatementParenthesized = returnStatement && returnStatement.argument.extra && returnStatement.argument.extra.parenthesized === true; 
                    if (isReturnStatementParenthesized) { 
                        var lines = source.split(/(?:\r?\n)|\r/); 
                        var offsetStartCol = n.body.body[0].argument.extra.parenStart - n.start; 
                        var offsetStartLine = source.substr(0, offsetStartCol).split(/(?:\r?\n)|\r/).length - 1; 
                        var offsetEndLine = lines.length - 1; 
                        var offsetEndCol = lines[offsetEndLine].lastIndexOf(")") + 1; 
                        n.body.loc = { 
                            start: { 
                                line: dloc.start.line + offsetStartLine, 
                                column: offsetStartLine > 0 ? offsetStartCol : dloc.start.column + offsetStartCol 
                            }, 
                            end: { 
                                line: dloc.start.line + offsetEndLine, 
                                column: offsetEndLine > 0 ? offsetEndCol : dloc.start.column + offsetEndCol 
                            } 
                        }; 
                    } 
                    break; 
            } 
            if (!dloc) { 
                dloc = { 
                    start: n.loc.start, 
                    end: { line: n.loc.start.line, column: n.loc.start.column + 1 } 
                }; 
                name = n.id ? n.id.name : n.name; 
            } 
 
            var index = this.cov.newFunction(name, dloc, n.body.loc); 
            var increment = this.increase('f', index, null); 
            var body = path.get('body'); 
            /* istanbul ignore else: not expected */ 
            if (body.isBlockStatement()) { 
                body.node.body.unshift(T.expressionStatement(increment)); 
            } else { 
                console.error('Unable to process function body node type:', n.type); 
            } 
        } 
    }, { 
        key: 'getBranchIncrement', 
        value: function getBranchIncrement(branchName, loc) { 
            var index = this.cov.addBranchPath(branchName, loc); 
            return this.increase('b', branchName, index); 
        } 
    }, { 
        key: 'insertBranchCounter', 
        value: function insertBranchCounter(path, branchName, loc) { 
            var increment = this.getBranchIncrement(branchName, loc || path.node.loc); 
            this.insertCounter(path, increment); 
        } 
    }, { 
        key: 'findLeaves', 
        value: function findLeaves(node, accumulator, parent, property) { 
            if (!node) { 
                return; 
            } 
            if (node.type === "LogicalExpression") { 
                var hint = this.hintFor(node); 
                if (hint !== 'next') { 
                    this.findLeaves(node.left, accumulator, node, 'left'); 
                    this.findLeaves(node.right, accumulator, node, 'right'); 
                } 
            } else { 
                accumulator.push({ 
                    node: node, 
                    parent: parent, 
                    property: property 
                }); 
            } 
        } 
    }]); 
 
    return VisitState; 
}(); 
 
// generic function that takes a set of visitor methods and 
// returns a visitor object with `enter` and `exit` properties, 
// such that: 
// 
// * standard entry processing is done 
// * the supplied visitors are called only when ignore is not in effect 
//   This relieves them from worrying about ignore states and generated nodes. 
// * standard exit processing is done 
// 
 
 
function entries() { 
    var enter = Array.prototype.slice.call(arguments); 
    // the enter function 
    var wrappedEntry = function wrappedEntry(path, node) { 
        this.onEnter(path); 
        if (this.shouldIgnore(path)) { 
            return; 
        } 
        var that = this; 
        enter.forEach(function (e) { 
            e.call(that, path, node); 
        }); 
    }; 
    var exit = function exit(path, node) { 
        this.onExit(path, node); 
    }; 
    return { 
        enter: wrappedEntry, 
        exit: exit 
    }; 
} 
 
function coverStatement(path) { 
    this.insertStatementCounter(path); 
} 
 
/* istanbul ignore next: no node.js support */ 
function coverAssignmentPattern(path) { 
    var n = path.node; 
    var b = this.cov.newBranch('default-arg', n.loc); 
    this.insertBranchCounter(path.get('right'), b); 
} 
 
function coverFunction(path) { 
    this.insertFunctionCounter(path); 
} 
 
function coverVariableDeclarator(path) { 
    this.insertStatementCounter(path.get('init')); 
} 
 
function skipInit(path) { 
    if (path.node.init) { 
        this.setAttr(path.node.init, 'skip-all', true); 
    } 
} 
 
function makeBlock(path) { 
    var T = this.types; 
    if (!path.node) { 
        path.replaceWith(T.blockStatement([])); 
    } 
    if (!path.isBlockStatement()) { 
        path.replaceWith(T.blockStatement([path.node])); 
        path.node.loc = path.node.body[0].loc; 
    } 
} 
 
function blockProp(prop) { 
    return function (path) { 
        makeBlock.call(this, path.get(prop)); 
    }; 
} 
 
function makeParenthesizedExpression(path) { 
    var T = this.types; 
    if (path.node) { 
        path.replaceWith(T.parenthesizedExpression(path.node)); 
    } 
} 
 
function parenthesizedExpressionProp(prop) { 
    return function (path) { 
        makeParenthesizedExpression.call(this, path.get(prop)); 
    }; 
} 
 
function convertArrowExpression(path) { 
    var n = path.node; 
    var T = this.types; 
    if (!T.isBlockStatement(n.body)) { 
        var bloc = n.body.loc; 
        if (n.expression === true) { 
            n.expression = false; 
        } 
        n.body = T.blockStatement([T.returnStatement(n.body)]); 
        // restore body location 
        n.body.loc = bloc; 
        // set up the location for the return statement so it gets 
        // instrumented 
        n.body.body[0].loc = bloc; 
    } 
} 
 
function coverIfBranches(path) { 
    var n = path.node, 
        hint = this.hintFor(n), 
        ignoreIf = hint === 'if', 
        ignoreElse = hint === 'else', 
        branch = this.cov.newBranch('if', n.loc); 
 
    if (ignoreIf) { 
        this.setAttr(n.consequent, 'skip-all', true); 
    } else { 
        this.insertBranchCounter(path.get('consequent'), branch, n.loc); 
    } 
    if (ignoreElse) { 
        this.setAttr(n.alternate, 'skip-all', true); 
    } else { 
        this.insertBranchCounter(path.get('alternate'), branch, n.loc); 
    } 
} 
 
function createSwitchBranch(path) { 
    var b = this.cov.newBranch('switch', path.node.loc); 
    this.setAttr(path.node, 'branchName', b); 
} 
 
function coverSwitchCase(path) { 
    var T = this.types; 
    var b = this.getAttr(path.parentPath.node, 'branchName'); 
    /* istanbul ignore if: paranoid check */ 
    if (b === null) { 
        throw new Error('Unable to get switch branch name'); 
    } 
    var increment = this.getBranchIncrement(b, path.node.loc); 
    path.node.consequent.unshift(T.expressionStatement(increment)); 
} 
 
function coverTernary(path) { 
    var n = path.node, 
        branch = this.cov.newBranch('cond-expr', path.node.loc), 
        cHint = this.hintFor(n.consequent), 
        aHint = this.hintFor(n.alternate); 
 
    if (cHint !== 'next') { 
        this.insertBranchCounter(path.get('consequent'), branch); 
    } 
    if (aHint !== 'next') { 
        this.insertBranchCounter(path.get('alternate'), branch); 
    } 
} 
 
function coverLogicalExpression(path) { 
    var T = this.types; 
    if (path.parentPath.node.type === "LogicalExpression") { 
        return; // already processed 
    } 
    var leaves = []; 
    this.findLeaves(path.node, leaves); 
    var b = this.cov.newBranch("binary-expr", path.node.loc); 
    for (var i = 0; i < leaves.length; i += 1) { 
        var leaf = leaves[i]; 
        var hint = this.hintFor(leaf.node); 
        if (hint === 'next') { 
            continue; 
        } 
        var increment = this.getBranchIncrement(b, leaf.node.loc); 
        if (!increment) { 
            continue; 
        } 
        leaf.parent[leaf.property] = T.sequenceExpression([increment, leaf.node]); 
    } 
} 
 
var codeVisitor = { 
    ArrowFunctionExpression: entries(convertArrowExpression, coverFunction), 
    AssignmentPattern: entries(coverAssignmentPattern), 
    BlockStatement: entries(), // ignore processing only 
    ClassMethod: entries(coverFunction), 
    ClassDeclaration: entries(parenthesizedExpressionProp('superClass')), 
    ExpressionStatement: entries(coverStatement), 
    BreakStatement: entries(coverStatement), 
    ContinueStatement: entries(coverStatement), 
    DebuggerStatement: entries(coverStatement), 
    ReturnStatement: entries(coverStatement), 
    ThrowStatement: entries(coverStatement), 
    TryStatement: entries(coverStatement), 
    VariableDeclaration: entries(), // ignore processing only 
    VariableDeclarator: entries(coverVariableDeclarator), 
    IfStatement: entries(blockProp('consequent'), blockProp('alternate'), coverStatement, coverIfBranches), 
    ForStatement: entries(blockProp('body'), skipInit, coverStatement), 
    ForInStatement: entries(blockProp('body'), skipInit, coverStatement), 
    ForOfStatement: entries(blockProp('body'), skipInit, coverStatement), 
    WhileStatement: entries(blockProp('body'), coverStatement), 
    DoWhileStatement: entries(blockProp('body'), coverStatement), 
    SwitchStatement: entries(createSwitchBranch, coverStatement), 
    SwitchCase: entries(coverSwitchCase), 
    WithStatement: entries(blockProp('body'), coverStatement), 
    FunctionDeclaration: entries(coverFunction), 
    FunctionExpression: entries(coverFunction), 
    LabeledStatement: entries(coverStatement), 
    ConditionalExpression: entries(coverTernary), 
    LogicalExpression: entries(coverLogicalExpression) 
}; 
// the template to insert at the top of the program. 
var coverageTemplate = (0, _babelTemplate2.default)('\n    var COVERAGE_VAR = (function () {\n        var path = PATH,\n            hash = HASH,\n            Function = (function(){}).constructor,\n            global = (new Function(\'return this\'))(),\n            gcv = GLOBAL_COVERAGE_VAR,\n            coverageData = INITIAL,\n            coverage = global[gcv] || (global[gcv] = {});\n        if (coverage[path] && coverage[path].hash === hash) {\n            return coverage[path];\n        }\n        coverageData.hash = hash;\n        return coverage[path] = coverageData;\n    })();\n'); 
// the rewire plugin (and potentially other babel middleware) 
// may cause files to be instrumented twice, see: 
// https://github.com/istanbuljs/babel-plugin-istanbul/issues/94 
// we should only instrument code for coverage the first time 
// it's run through istanbul-lib-instrument. 
function alreadyInstrumented(path, visitState) { 
    return path.scope.hasBinding(visitState.varName); 
} 
function shouldIgnoreFile(programNode) { 
    return programNode.parent && programNode.parent.comments.some(function (c) { 
        return COMMENT_FILE_RE.test(c.value); 
    }); 
} 
/** 
 * programVisitor is a `babel` adaptor for instrumentation. 
 * It returns an object with two methods `enter` and `exit`. 
 * These should be assigned to or called from `Program` entry and exit functions 
 * in a babel visitor. 
 * These functions do not make assumptions about the state set by Babel and thus 
 * can be used in a context other than a Babel plugin. 
 * 
 * The exit function returns an object that currently has the following keys: 
 * 
 * `fileCoverage` - the file coverage object created for the source file. 
 * `sourceMappingURL` - any source mapping URL found when processing the file. 
 * 
 * @param {Object} types - an instance of babel-types 
 * @param {string} sourceFilePath - the path to source file 
 * @param {Object} opts - additional options 
 * @param {string} [opts.coverageVariable=__coverage__] the global coverage variable name. 
 * @param {object} [opts.inputSourceMap=undefined] the input source map, that maps the uninstrumented code back to the 
 * original code. 
 */ 
function programVisitor(types) { 
    var sourceFilePath = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'unknown.js'; 
    var opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : { coverageVariable: '__coverage__', inputSourceMap: undefined }; 
 
    var T = types; 
    var visitState = new VisitState(types, sourceFilePath, opts.inputSourceMap); 
    return { 
        enter: function enter(path) { 
            if (shouldIgnoreFile(path.find(function (p) { 
                return p.isProgram(); 
            }))) { 
                return; 
            } 
            if (alreadyInstrumented(path, visitState)) { 
                return; 
            } 
            path.traverse(codeVisitor, visitState); 
        }, 
        exit: function exit(path) { 
            if (alreadyInstrumented(path, visitState)) { 
                return; 
            } 
            visitState.cov.freeze(); 
            var coverageData = visitState.cov.toJSON(); 
            if (shouldIgnoreFile(path.find(function (p) { 
                return p.isProgram(); 
            }))) { 
                return { fileCoverage: coverageData, sourceMappingURL: visitState.sourceMappingURL }; 
            } 
            coverageData[_constants.MAGIC_KEY] = _constants.MAGIC_VALUE; 
            var hash = (0, _crypto.createHash)(_constants.SHA).update(JSON.stringify(coverageData)).digest('hex'); 
            var coverageNode = T.valueToNode(coverageData); 
            delete coverageData[_constants.MAGIC_KEY]; 
            var cv = coverageTemplate({ 
                GLOBAL_COVERAGE_VAR: T.stringLiteral(opts.coverageVariable), 
                COVERAGE_VAR: T.identifier(visitState.varName), 
                PATH: T.stringLiteral(sourceFilePath), 
                INITIAL: coverageNode, 
                HASH: T.stringLiteral(hash) 
            }); 
            cv._blockHoist = 5; 
            path.node.body.unshift(cv); 
            return { 
                fileCoverage: coverageData, 
                sourceMappingURL: visitState.sourceMappingURL 
            }; 
        } 
    }; 
} 
 
exports.default = programVisitor; 


annotator.js


目的

ハイライトのアルゴリズムを、上述のルールに沿うよう修正する。


パッケージ

istanbul-reports@2.0.1


コード

※ TypeScriptを使わない場合のパスは node_modules/istanbul-reports/lib/html/annotator.js である。

node_modules/nyc/node_modules/istanbul-reports/lib/html/annotator.js
/* 
 Copyright 2012-2015, Yahoo Inc. 
 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. 
 */ 
"use strict"; 
 
var InsertionText = require('./insertion-text'), 
    lt = '\u0001', 
    gt = '\u0002', 
    RE_LT = /</g, 
    RE_GT = />/g, 
    RE_AMP = /&/g, 
    RE_lt = /\u0001/g, 
    RE_gt = /\u0002/g; 
 
function title(str) { 
    return ' title="' + str + '" '; 
} 
 
function customEscape(text) { 
    text = String(text); 
    return text.replace(RE_AMP, '&') 
        .replace(RE_LT, '<') 
        .replace(RE_GT, '>') 
        .replace(RE_lt, '<') 
        .replace(RE_gt, '>'); 
} 
 
function annotateLines(fileCoverage, structuredText) { 
    var lineStats = fileCoverage.getLineCoverage(); 
    if (!lineStats) { 
        return; 
    } 
    Object.keys(lineStats).forEach(function (lineNumber) { 
        var count = lineStats[lineNumber]; 
        if (structuredText[lineNumber]) { 
            structuredText[lineNumber].covered = count > 0 ? 'yes' : 'no'; 
            structuredText[lineNumber].hits = count; 
        } 
    }); 
} 
 
function annotateStatements(fileCoverage, structuredText) { 
    var statementStats = fileCoverage.s, 
        statementMeta = fileCoverage.statementMap; 
    var statements = []; 
    Object.keys(statementStats).forEach(function (stName) { 
        var count = statementStats[stName], 
            meta = statementMeta[stName], 
            type = count > 0 ? 'yes' : 'no', 
            startLine = meta.start.line, 
            startCol = meta.start.column, 
            endFirstCol = meta.end.column, 
            endLastCol = endFirstCol, 
            endLine = meta.end.line, 
            openSpan = lt + 'span class="'  
                + (meta.skip ? 'cstat-skip' : 'cstat-no')  
                + ' start-line-' + startLine  
                + ' start-column-' + startCol  
                + '"' + title('statement not covered') + gt, 
            closeSpan = lt + '/span' + gt, 
            i, 
            text, 
            lines = []; 
 
        if (type === 'no' && structuredText[startLine]) { 
            if (endLine !== startLine) { 
                endFirstCol = structuredText[startLine].text.originalLength(); 
            } 
            text = structuredText[startLine].text 
            lines.push({ 
                insertionText: text, 
                args: [ 
                    startCol, 
                    openSpan, 
                    startCol < endLastCol ? endFirstCol : text.originalLength(), 
                    closeSpan 
                ] 
            }); 
            for (i = startLine + 1; i < endLine; i++) { 
                text = structuredText[i].text 
                lines.push({ 
                    insertionText: text, 
                    args: [ 
                        text.startPos, 
                        openSpan, 
                        text.originalLength(), 
                        closeSpan 
                    ] 
                }); 
            } 
            if (endLine !== startLine) { 
                text = structuredText[endLine].text 
                lines.push({ 
                    insertionText: text, 
                    args: [ 
                        text.startPos, 
                        openSpan, 
                        endLastCol, 
                        closeSpan 
                    ] 
                }); 
            } 
            statements.push({ 
                startPos: meta.start, 
                lines: lines 
            }); 
        } 
    }); 
    // reverse sort 
    statements.sort(function (b, a) { 
        return a.startPos.line === b.startPos.line ? a.startPos.column - b.startPos.column : a.startPos.line - b.startPos.line; 
    }); 
    statements.map(function (statement) { 
        statement.lines.map(function (line) { 
            var insertionText = line.insertionText; 
            var args = line.args; 
            insertionText.wrap.apply(insertionText, args); 
        }) 
    }); 
} 
 
function annotateFunctions(fileCoverage, structuredText) { 
    var fnStats = fileCoverage.f, 
        fnMeta = fileCoverage.fnMap; 
 
    if (!fnStats) { 
        return; 
    } 
    Object.keys(fnStats).forEach(function (fName) { 
        var count = fnStats[fName], 
            meta = fnMeta[fName], 
            type = count > 0 ? 'yes' : 'no', 
            startCol = meta.decl.start.column, 
            endCol = meta.decl.end.column, 
            startLine = meta.decl.start.line, 
            endLine = meta.decl.end.line, 
            openSpan = lt + 'span class="'  
                + (meta.skip ? 'fstat-skip' : 'fstat-no')  
                + ' start-line-' + startLine  
                + ' start-column-' + startCol + '"'  
                + title('function not covered') + gt, 
            closeSpan = lt + '/span' + gt, 
            i, 
            text, 
            lines = []; 
 
        if (type === 'no' && structuredText[startLine]) { 
            text = structuredText[startLine].text; 
            lines.push({ 
                insertionText: text, 
                args: [ 
                    startCol, 
                    openSpan, 
                    endLine === startLine ? endCol : text.originalLength(), 
                    closeSpan 
                ] 
            }); 
            for (i = startLine + 1; i < endLine; i++) { 
                text = structuredText[i].text; 
                lines.push({ 
                    insertionText: text, 
                    args: [ 
                        text.startPos, 
                        openSpan, 
                        text.originalLength(), 
                        closeSpan 
                    ] 
                }); 
            } 
            if (endLine !== startLine) { 
                text = structuredText[endLine].text; 
                lines.push({ 
                    insertionText: text, 
                    args: [ 
                        text.startPos, 
                        openSpan, 
                        endCol, 
                        closeSpan 
                    ] 
                }); 
            } 
            lines.map(function (line) { 
                var insertionText = line.insertionText; 
                var args = line.args; 
                insertionText.wrap.apply(insertionText, args); 
            }); 
        } 
    }); 
} 
 
function annotateBranches(fileCoverage, structuredText) { 
    var branchStats = fileCoverage.b, 
        branchMeta = fileCoverage.branchMap; 
    if (!branchStats) { 
        return; 
    } 
 
    Object.keys(branchStats).forEach(function (branchName) { 
        var branchArray = branchStats[branchName], 
            sumCount = branchArray.reduce(function (p, n) { 
                return p + n; 
            }, 0), 
            metaArray = branchMeta[branchName], 
            i, 
            count, 
            meta, 
            type, 
            startCol, 
            endCol, 
            startLine, 
            endLine, 
            offset, 
            openSpan, 
            closeSpan, 
            text; 
 
        // only highlight if partial branches are missing or if there is a 
        // single uncovered branch. 
        if (sumCount > 0 || (sumCount === 0 && branchArray.length === 1)) { 
            for (i = 0; i < branchArray.length && i < metaArray.locations.length; i += 1) { 
                count = branchArray[i]; 
                meta = metaArray.locations[i]; 
                type = count > 0 ? 'yes' : 'no'; 
                startCol = meta.start.column; 
                endCol = meta.end.column; 
                startLine = meta.start.line; 
                endLine = meta.end.line; 
                openSpan = lt + 'span class="branch-' + i + ' '  
                    + (meta.skip ? 'cbranch-skip' : 'cbranch-no')  
                    + ' start-line-' + startLine  
                    + ' start-column-' + startCol + '"'  
                    + title('branch not covered') + gt; 
                closeSpan = lt + '/span' + gt; 
 
                if (count === 0 && structuredText[startLine]) { //skip branches taken 
                    if (branchMeta[branchName].type === 'if') { 
                    // 'if' is a special case 
                    // since the else branch might not be visible, being non-existent 
                        text = structuredText[metaArray.loc.start.line].text; 
                        text.insertAt(metaArray.loc.start.column, lt + 'span class="' + 
                            (meta.skip ? 'skip-if-branch' : 'missing-if-branch') + '"' + 
                            title((i === 0 ? 'if' : 'else') + ' path not taken') + gt + 
                            (i === 0 ? 'I' : 'E') + lt + '/span' + gt, true, false); 
                    } else if (branchMeta[branchName].type === 'switch') { 
                        text = structuredText[startLine].text; 
                        offset = text.offsets.reduce(function (accumulator, currentValue) { 
                            return accumulator + currentValue.len; 
                        }, 0); 
                        endCol = text.text.substr(startCol + offset).match(/^\s*(?:case\s+[^\[\('"`:]*[\[\(]?\s*['"`]?(?:\s*:|.*?[\)\]]\s*:|.*?[^\\]['"`]\s*:)|default\s*:)/)[0].length + startCol; 
                        text.wrap(startCol, 
                            openSpan, 
                            endCol, 
                            closeSpan); 
                        } else { 
                        if (endLine !== startLine) { 
                            endCol = structuredText[startLine].text.originalLength(); 
                        } 
                        text = structuredText[startLine].text; 
                        text.wrap(startCol, 
                            openSpan, 
                            startCol < endCol ? endCol : text.originalLength(), 
                            closeSpan); 
                    } 
                } 
            } 
        } 
    }); 
} 
 
 
function annotateSourceCode(fileCoverage, sourceStore) { 
    var codeArray, 
        lineCoverageArray; 
    try { 
        var sourceText = sourceStore.getSource(fileCoverage.path), 
            code = sourceText.split(/(?:\r?\n)|\r/), 
            count = 0, 
            structured = code.map(function (str) { 
                count += 1; 
                return { 
                    line: count, 
                    covered: 'neutral', 
                    hits: 0, 
                    text: new InsertionText(str, true) 
                }; 
            }); 
        structured.unshift({line: 0, covered: null, text: new InsertionText("")}); 
        annotateLines(fileCoverage, structured); 
        //note: order is important, since statements typically result in spanning the whole line and doing branches late 
        //causes mismatched tags 
        annotateBranches(fileCoverage, structured); 
        annotateFunctions(fileCoverage, structured); 
        annotateStatements(fileCoverage, structured); 
        structured.shift(); 
 
        codeArray = structured.map(function (item) { 
            return customEscape(item.text.toString()) || ' '; 
        }); 
 
        lineCoverageArray = structured.map(function (item) { 
            return { 
                covered: item.covered, 
                hits: item.hits > 0 ? item.hits + 'x' : ' ' 
            }; 
        }); 
 
        return { 
            annotatedCode: codeArray, 
            lineCoverage: lineCoverageArray, 
            maxLines: structured.length 
        }; 
    } catch (ex) { 
        codeArray = [ ex.message ]; 
        lineCoverageArray = [ { covered: 'no', hits: 0 } ]; 
        String(ex.stack || '').split(/\r?\n/).forEach(function (line) { 
            codeArray.push(line); 
            lineCoverageArray.push({ covered: 'no', hits: 0 }); 
        }); 
        return { 
            annotatedCode: codeArray, 
            lineCoverage: lineCoverageArray, 
            maxLines: codeArray.length 
        }; 
    } 
} 
 
module.exports = { 
    annotateSourceCode: annotateSourceCode 
}; 


block-navigation.js


目的

複数行にまたがったステートメントや節をひとまとめにしてフォーカスさせる。


パッケージ

istanbul-reports@2.0.1


コード

istanbul-reports@1.xには存在しない。

※ TypeScriptを使わない場合のパスは node_modules/istanbul-reports/lib/html/assets/block-navigation.js である。

node_modules/nyc/node_modules/istanbul-reports/lib/html/assets/block-navigation.js
var jumpToCode = (function init () { 
  // Classes of code we would like to highlight 
  var missingCoverageClasses = [ '.cbranch-no', '.cstat-no', '.fstat-no' ]; 
 
  // Selecter that finds elements on the page to which we can jump 
  var selector = missingCoverageClasses.join(', '); 
 
  // The NodeList of matching elements 
  var missingCoverageElements = document.querySelectorAll(selector); 
 
  // Group of NodeList which has same class 
  var groups = {}; 
  for (var i = 0, l = missingCoverageElements.length; i < l; i++) { 
    var node = missingCoverageElements[i]; 
    var className = node.className; 
    if (groups[className] === undefined) { 
      groups[className] = []; 
    } 
    groups[className].push(node); 
  } 
 
  // Convert NodeList to Array 
  var missingCoverageGroups = Object.keys(groups).map(function(className) { 
    return groups[className]; 
  }); 
 
  var currentIndex; 
 
  function toggleClass(index) { 
    if (currentIndex === undefined) { 
      currentIndex = 0; 
    } 
    missingCoverageGroups[currentIndex].map(function(node) { 
      return node.classList.remove('highlighted'); 
    }); 
    missingCoverageGroups[index].map(function(node) { 
      return node.classList.add('highlighted'); 
    }); 
  } 
 
  function makeCurrent(index) { 
    toggleClass(index); 
    currentIndex = index; 
    missingCoverageGroups[index][0] 
      .scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }); 
  } 
 
  function goToPrevious() { 
    var nextIndex = 0; 
    if (typeof currentIndex !== 'number' || currentIndex === 0) { 
      nextIndex = missingCoverageGroups.length - 1; 
    } else if (missingCoverageGroups.length > 1) { 
      nextIndex = currentIndex - 1; 
    } 
 
    makeCurrent(nextIndex); 
  } 
 
  function goToNext() { 
    var nextIndex = 0; 
 
    if (typeof currentIndex === 'number' && currentIndex < (missingCoverageGroups.length - 1)) { 
      nextIndex = currentIndex + 1; 
    } 
 
    makeCurrent(nextIndex); 
  } 
 
  return function jump(event) { 
    switch (event.which) { 
      case 78: // n 
      case 74: // j 
        goToNext(); 
        break; 
      case 66: // b 
      case 75: // k 
      case 80: // p 
        goToPrevious(); 
        break; 
    } 
  }; 
}()); 
window.addEventListener('keydown', jumpToCode); 


insertion-text.js


目的

ハイライト範囲の後に続く空白文字やコメントなど、プログラムとして意味の無い箇所のハイライトを抑制する。


パッケージ

istanbul-reports@2.0.1


コード

※ TypeScriptを使わない場合のパスは node_modules/istanbul-reports/lib/html/insertion-text.js である。

node_modules/nyc/node_modules/istanbul-reports/lib/html/insertion-text.js
wrap: function (startPos, startText, endPos, endText, consumeBlanks) { 
        this.insertAt(startPos, startText, true, consumeBlanks); 
-       this.insertAt(endPos, endText, false, consumeBlanks); 
+       this.insertAt(endPos, endText, false, false); 
        return this; 
    }, 


typescript.js


目的

ソースマップを正しく行うため。


パッケージ

typescript@3.2.2


コード

※ TypeScriptを使う場合のみ修正する。

node_modules/typescript/lib/typescript.js
function emitSignatureHead(node) { 
            emitTypeParameters(node, node.typeParameters); 
            emitParameters(node, node.parameters); 
            emitTypeAnnotation(node.type); 
+           if (node.original && node.original.type) { 
+               emitPos(node.original.type.end); 
+           } else { 
+               emitPos(node.body.pos); 
+           } 
        } 
 
        function emitArrowFunctionHead(node) { 
            emitTypeParameters(node, node.typeParameters); 
            emitParametersForArrow(node, node.parameters); 
            emitTypeAnnotation(node.type); 
+           emitPos(node.equalsGreaterThanToken.pos); 
            writeSpace(); 
            emit(node.equalsGreaterThanToken); 
        } 
 
        function emitBlockFunctionBody(body) { 
            writeSpace(); 
-           writePunctuation("{"); 
+           writeToken(18 /* OpenBraceToken */, body.pos, writePunctuation, body); 
            increaseIndent(); 
            var emitBlockFunctionBody = shouldEmitBlockFunctionBodyOnSingleLine(body) 
                ? emitBlockFunctionBodyOnSingleLine 
                : emitBlockFunctionBodyWorker; 
            if (emitBodyWithDetachedComments) { 
                emitBodyWithDetachedComments(body, body.statements, emitBlockFunctionBody); 
            } 
            else { 
                emitBlockFunctionBody(body); 
            } 
            decreaseIndent(); 
            writeToken(19 /* CloseBraceToken */, body.statements.end, writePunctuation, body); 
        } 


まとめ

本記事では、IstanbulJSが生成するHTMLレポートのハイライト範囲を修正するコードを示した。

コメント

このブログの人気の投稿

投稿時間:2021-04-30 23:37:32 RSSフィード2021-04-30 23:00 分まとめ(42件)

投稿時間:2021-07-31 23:31:38 RSSフィード2021-07-31 23:00 分まとめ(32件)

投稿時間:2023-02-05 02:09:04 RSSフィード2023-02-05 02:00 分まとめ(9件)