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

# 概要
[IstanbulJS - レポート改善 - ソースコード修正](https://qiita.com/TakuyaHara/items/defc3284929161ac4809) (以降、「前記事」と記載)を基に、別のルールを用いた改善案。

![screencapture-takuyahara-github-io-share-20190104_IstanbulJS_Coverage_Issue-11_SolutionB-typescript_v1-coverage-2_Function-0_Normal-ClassMethod-tsx-html-2019-01-18-00_27_46.png](https://qiita-image-store.s3.amazonaws.com/0/286440/f2cedaab-15da-ef88-358e-72b7bcde7d0c.png)

# 前提
予め [前記事の修正](https://qiita.com/TakuyaHara/items/defc3284929161ac4809) を実施しておくこと。

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

## 関数
定義部の開始位置から関数の終了位置までをハイライトする。定義部の開始位置は [前記事の定義](https://qiita.com/TakuyaHara/items/defc3284929161ac4809#関数) と同様である。

# 修正ファイル
## annotator.js
### 目的
ハイライトのアルゴリズムを、上述のルールに沿うよう修正する。

### パッケージ
📦 [istanbul-reports@2.0.1](https://github.com/istanbuljs/istanbuljs/tree/istanbul-reports%402.0.1/packages/istanbul-reports)

### コード
🚨 TypeScriptを使わない場合のパスは `node_modules/istanbul-reports/lib/html/annotator.js` である。

```javascript: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, '&amp;')
        .replace(RE_LT, '&lt;')
        .replace(RE_GT, '&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;
    var functions = [];

    if (!fnStats) {
        return;
    }
    Object.keys(fnStats).forEach(function (fName) {
        var count = fnStats[fName],
            meta = fnMeta[fName],
            type = count > 0 ? 'yes' : 'no',
            declStartCol = meta.decl.start.column,
            declEndCol = meta.decl.end.column,
            declStartLine = meta.decl.start.line,
            declEndLine = meta.decl.end.line,
            locStartCol = meta.loc.start.column,
            locEndCol = meta.loc.end.column,
            locStartLine = meta.loc.start.line,
            locEndLine = meta.loc.end.line,
            openSpan = lt + 'span class="' 
                + (meta.skip ? 'fstat-skip' : 'fstat-no') 
                + ' start-line-' + declStartLine 
                + ' start-column-' + declStartCol + '"' 
                + title('function not covered') + gt,
            closeSpan = lt + '/span' + gt,
            i,
            text,
            lines = [];

        if (type === 'no' && structuredText[declStartLine]) {
            text = structuredText[declStartLine].text;
            lines.push({
                insertionText: text,
                args: [
                    declStartCol,
                    openSpan,
                    locEndLine === declStartLine ? locEndCol : text.originalLength(),
                    closeSpan
                ]
            });
            for (i = declStartLine + 1; i < locEndLine; i++) {
                text = structuredText[i].text;
                lines.push({
                    insertionText: text,
                    args: [
                        text.startPos,
                        openSpan,
                        text.originalLength(),
                        closeSpan
                    ]
                });
            }
            if (locEndLine !== declStartLine) {
                text = structuredText[locEndLine].text;
                lines.push({
                    insertionText: text,
                    args: [
                        text.startPos,
                        openSpan,
                        locEndCol,
                        closeSpan
                    ]
                });
            }
            functions.push({
                startPos: meta.decl.start,
                lines: lines
            });
        }
    });
    // reverse sort
    functions.sort(function (b, a) {
        return a.startPos.line === b.startPos.line ? a.startPos.column - b.startPos.column : a.startPos.line - b.startPos.line;
    });
    functions.map(function (fn) {
        fn.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);
        annotateBranches(fileCoverage, structured);
        annotateStatements(fileCoverage, structured);
        annotateFunctions(fileCoverage, structured);
        structured.shift();

        codeArray = structured.map(function (item) {
            return customEscape(item.text.toString()) || '&nbsp;';
        });

        lineCoverageArray = structured.map(function (item) {
            return {
                covered: item.covered,
                hits: item.hits > 0 ? item.hits + 'x' : '&nbsp;'
            };
        });

        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
};

```

# 検証
修正を実施後に再度生成した各レポートが、上述のルールに従っているか検証する。

## [JavaScript - react-scripts@1.xのテンプレートを使用](https://takuyahara.github.io/share/20190104_IstanbulJS_Coverage_Issue/11_SolutionB/javascript_v1/coverage/lcov-report/index.html)
👍 上述のルールに従ってハイライトされている。

## [JavaScript - react-scripts@2.xのテンプレートを使用](https://takuyahara.github.io/share/20190104_IstanbulJS_Coverage_Issue/11_SolutionB/javascript_v2/coverage/lcov-report/index.html)
👍 上述のルールに従ってハイライトされている。

## [TypeScript - react-scripts@1.xのテンプレートを使用](https://takuyahara.github.io/share/20190104_IstanbulJS_Coverage_Issue/11_SolutionB/typescript_v1/coverage/index.html)
👍 上述のルールに従ってハイライトされている。

## [TypeScript - react-scripts@2.xのテンプレートを使用](https://takuyahara.github.io/share/20190104_IstanbulJS_Coverage_Issue/11_SolutionB/typescript_v2/coverage/index.html)
👍 上述のルールに従ってハイライトされている。


# まとめ
本記事では、IstanbulJSが生成するHTMLレポートのハイライト範囲を [前記事](https://qiita.com/TakuyaHara/items/defc3284929161ac4809#%E3%83%AB%E3%83%BC%E3%83%AB) と異なるルールで修正するコードを示した。