概要
IstanbulJS - レポート改善 - ソースコード修正 (以降、「前記事」と記載)を基に、別のルールを用いた改善案。
前提
予め 前記事の修正 を実施しておくこと。
ルール
本修正で行うハイライトのルールを以下のように定義する。
関数
定義部の開始位置から関数の終了位置までをハイライトする。定義部の開始位置は 前記事の定義 と同様である。
修正ファイル
annotator.js
目的
ハイライトのアルゴリズムを、上述のルールに沿うよう修正する。
パッケージ
コード
🚨 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レポートのハイライト範囲を 前記事 と異なるルールで修正するコードを示した。