LoginSignup
0
0

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-01-13

概要

IstanbulJS - レポート改善 - ソースコード修正 (以降、「前記事」と記載)を基に、別のルールを用いた改善案。

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

前提

予め 前記事の修正 を実施しておくこと。

ルール

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

関数

定義部の開始位置から関数の終了位置までをハイライトする。定義部の開始位置は 前記事の定義 と同様である。

修正ファイル

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, '&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のテンプレートを使用

👍 上述のルールに従ってハイライトされている。

JavaScript - react-scripts@2.xのテンプレートを使用

👍 上述のルールに従ってハイライトされている。

TypeScript - react-scripts@1.xのテンプレートを使用

👍 上述のルールに従ってハイライトされている。

TypeScript - react-scripts@2.xのテンプレートを使用

👍 上述のルールに従ってハイライトされている。

まとめ

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0