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

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


概要

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


前提

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


ルール

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


関数

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


修正ファイル


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; 
    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()) || ' '; 
        }); 
 
        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 
}; 
 


検証

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


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

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


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

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


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

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


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

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


まとめ

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

コメント

このブログの人気の投稿

投稿時間:2021-06-17 22:08:45 RSSフィード2021-06-17 22:00 分まとめ(2089件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)