276 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			276 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 
								 | 
							
								/*
							 | 
						||
| 
								 | 
							
								 Copyright 2012-2015, Yahoo Inc.
							 | 
						||
| 
								 | 
							
								 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								'use strict';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const InsertionText = require('./insertion-text');
							 | 
						||
| 
								 | 
							
								const lt = '\u0001';
							 | 
						||
| 
								 | 
							
								const gt = '\u0002';
							 | 
						||
| 
								 | 
							
								const RE_LT = /</g;
							 | 
						||
| 
								 | 
							
								const RE_GT = />/g;
							 | 
						||
| 
								 | 
							
								const RE_AMP = /&/g;
							 | 
						||
| 
								 | 
							
								// eslint-disable-next-line
							 | 
						||
| 
								 | 
							
								var RE_lt = /\u0001/g;
							 | 
						||
| 
								 | 
							
								// eslint-disable-next-line
							 | 
						||
| 
								 | 
							
								var 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) {
							 | 
						||
| 
								 | 
							
								    const lineStats = fileCoverage.getLineCoverage();
							 | 
						||
| 
								 | 
							
								    if (!lineStats) {
							 | 
						||
| 
								 | 
							
								        return;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    Object.keys(lineStats).forEach(lineNumber => {
							 | 
						||
| 
								 | 
							
								        const count = lineStats[lineNumber];
							 | 
						||
| 
								 | 
							
								        if (structuredText[lineNumber]) {
							 | 
						||
| 
								 | 
							
								            structuredText[lineNumber].covered = count > 0 ? 'yes' : 'no';
							 | 
						||
| 
								 | 
							
								            structuredText[lineNumber].hits = count;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function annotateStatements(fileCoverage, structuredText) {
							 | 
						||
| 
								 | 
							
								    const statementStats = fileCoverage.s;
							 | 
						||
| 
								 | 
							
								    const statementMeta = fileCoverage.statementMap;
							 | 
						||
| 
								 | 
							
								    Object.keys(statementStats).forEach(stName => {
							 | 
						||
| 
								 | 
							
								        const count = statementStats[stName];
							 | 
						||
| 
								 | 
							
								        const meta = statementMeta[stName];
							 | 
						||
| 
								 | 
							
								        const type = count > 0 ? 'yes' : 'no';
							 | 
						||
| 
								 | 
							
								        const startCol = meta.start.column;
							 | 
						||
| 
								 | 
							
								        let endCol = meta.end.column + 1;
							 | 
						||
| 
								 | 
							
								        const startLine = meta.start.line;
							 | 
						||
| 
								 | 
							
								        const endLine = meta.end.line;
							 | 
						||
| 
								 | 
							
								        const openSpan =
							 | 
						||
| 
								 | 
							
								            lt +
							 | 
						||
| 
								 | 
							
								            'span class="' +
							 | 
						||
| 
								 | 
							
								            (meta.skip ? 'cstat-skip' : 'cstat-no') +
							 | 
						||
| 
								 | 
							
								            '"' +
							 | 
						||
| 
								 | 
							
								            title('statement not covered') +
							 | 
						||
| 
								 | 
							
								            gt;
							 | 
						||
| 
								 | 
							
								        const closeSpan = lt + '/span' + gt;
							 | 
						||
| 
								 | 
							
								        let text;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (type === 'no' && structuredText[startLine]) {
							 | 
						||
| 
								 | 
							
								            if (endLine !== startLine) {
							 | 
						||
| 
								 | 
							
								                endCol = structuredText[startLine].text.originalLength();
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            text = structuredText[startLine].text;
							 | 
						||
| 
								 | 
							
								            text.wrap(
							 | 
						||
| 
								 | 
							
								                startCol,
							 | 
						||
| 
								 | 
							
								                openSpan,
							 | 
						||
| 
								 | 
							
								                startCol < endCol ? endCol : text.originalLength(),
							 | 
						||
| 
								 | 
							
								                closeSpan
							 | 
						||
| 
								 | 
							
								            );
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function annotateFunctions(fileCoverage, structuredText) {
							 | 
						||
| 
								 | 
							
								    const fnStats = fileCoverage.f;
							 | 
						||
| 
								 | 
							
								    const fnMeta = fileCoverage.fnMap;
							 | 
						||
| 
								 | 
							
								    if (!fnStats) {
							 | 
						||
| 
								 | 
							
								        return;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    Object.keys(fnStats).forEach(fName => {
							 | 
						||
| 
								 | 
							
								        const count = fnStats[fName];
							 | 
						||
| 
								 | 
							
								        const meta = fnMeta[fName];
							 | 
						||
| 
								 | 
							
								        const type = count > 0 ? 'yes' : 'no';
							 | 
						||
| 
								 | 
							
								        const startCol = meta.decl.start.column;
							 | 
						||
| 
								 | 
							
								        let endCol = meta.decl.end.column + 1;
							 | 
						||
| 
								 | 
							
								        const startLine = meta.decl.start.line;
							 | 
						||
| 
								 | 
							
								        const endLine = meta.decl.end.line;
							 | 
						||
| 
								 | 
							
								        const openSpan =
							 | 
						||
| 
								 | 
							
								            lt +
							 | 
						||
| 
								 | 
							
								            'span class="' +
							 | 
						||
| 
								 | 
							
								            (meta.skip ? 'fstat-skip' : 'fstat-no') +
							 | 
						||
| 
								 | 
							
								            '"' +
							 | 
						||
| 
								 | 
							
								            title('function not covered') +
							 | 
						||
| 
								 | 
							
								            gt;
							 | 
						||
| 
								 | 
							
								        const closeSpan = lt + '/span' + gt;
							 | 
						||
| 
								 | 
							
								        let text;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (type === 'no' && structuredText[startLine]) {
							 | 
						||
| 
								 | 
							
								            if (endLine !== startLine) {
							 | 
						||
| 
								 | 
							
								                endCol = structuredText[startLine].text.originalLength();
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            text = structuredText[startLine].text;
							 | 
						||
| 
								 | 
							
								            text.wrap(
							 | 
						||
| 
								 | 
							
								                startCol,
							 | 
						||
| 
								 | 
							
								                openSpan,
							 | 
						||
| 
								 | 
							
								                startCol < endCol ? endCol : text.originalLength(),
							 | 
						||
| 
								 | 
							
								                closeSpan
							 | 
						||
| 
								 | 
							
								            );
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function annotateBranches(fileCoverage, structuredText) {
							 | 
						||
| 
								 | 
							
								    const branchStats = fileCoverage.b;
							 | 
						||
| 
								 | 
							
								    const branchMeta = fileCoverage.branchMap;
							 | 
						||
| 
								 | 
							
								    if (!branchStats) {
							 | 
						||
| 
								 | 
							
								        return;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    Object.keys(branchStats).forEach(branchName => {
							 | 
						||
| 
								 | 
							
								        const branchArray = branchStats[branchName];
							 | 
						||
| 
								 | 
							
								        const sumCount = branchArray.reduce((p, n) => p + n, 0);
							 | 
						||
| 
								 | 
							
								        const metaArray = branchMeta[branchName].locations;
							 | 
						||
| 
								 | 
							
								        let i;
							 | 
						||
| 
								 | 
							
								        let count;
							 | 
						||
| 
								 | 
							
								        let meta;
							 | 
						||
| 
								 | 
							
								        let startCol;
							 | 
						||
| 
								 | 
							
								        let endCol;
							 | 
						||
| 
								 | 
							
								        let startLine;
							 | 
						||
| 
								 | 
							
								        let endLine;
							 | 
						||
| 
								 | 
							
								        let openSpan;
							 | 
						||
| 
								 | 
							
								        let closeSpan;
							 | 
						||
| 
								 | 
							
								        let 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.length;
							 | 
						||
| 
								 | 
							
								                i += 1
							 | 
						||
| 
								 | 
							
								            ) {
							 | 
						||
| 
								 | 
							
								                count = branchArray[i];
							 | 
						||
| 
								 | 
							
								                meta = metaArray[i];
							 | 
						||
| 
								 | 
							
								                startCol = meta.start.column;
							 | 
						||
| 
								 | 
							
								                endCol = meta.end.column + 1;
							 | 
						||
| 
								 | 
							
								                startLine = meta.start.line;
							 | 
						||
| 
								 | 
							
								                endLine = meta.end.line;
							 | 
						||
| 
								 | 
							
								                openSpan =
							 | 
						||
| 
								 | 
							
								                    lt +
							 | 
						||
| 
								 | 
							
								                    'span class="branch-' +
							 | 
						||
| 
								 | 
							
								                    i +
							 | 
						||
| 
								 | 
							
								                    ' ' +
							 | 
						||
| 
								 | 
							
								                    (meta.skip ? 'cbranch-skip' : 'cbranch-no') +
							 | 
						||
| 
								 | 
							
								                    '"' +
							 | 
						||
| 
								 | 
							
								                    title('branch not covered') +
							 | 
						||
| 
								 | 
							
								                    gt;
							 | 
						||
| 
								 | 
							
								                closeSpan = lt + '/span' + gt;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                if (count === 0 && structuredText[startLine]) {
							 | 
						||
| 
								 | 
							
								                    //skip branches taken
							 | 
						||
| 
								 | 
							
								                    if (endLine !== startLine) {
							 | 
						||
| 
								 | 
							
								                        endCol = structuredText[
							 | 
						||
| 
								 | 
							
								                            startLine
							 | 
						||
| 
								 | 
							
								                        ].text.originalLength();
							 | 
						||
| 
								 | 
							
								                    }
							 | 
						||
| 
								 | 
							
								                    text = structuredText[startLine].text;
							 | 
						||
| 
								 | 
							
								                    if (branchMeta[branchName].type === 'if') {
							 | 
						||
| 
								 | 
							
								                        // 'if' is a special case
							 | 
						||
| 
								 | 
							
								                        // since the else branch might not be visible, being non-existent
							 | 
						||
| 
								 | 
							
								                        text.insertAt(
							 | 
						||
| 
								 | 
							
								                            startCol,
							 | 
						||
| 
								 | 
							
								                            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 {
							 | 
						||
| 
								 | 
							
								                        text.wrap(
							 | 
						||
| 
								 | 
							
								                            startCol,
							 | 
						||
| 
								 | 
							
								                            openSpan,
							 | 
						||
| 
								 | 
							
								                            startCol < endCol ? endCol : text.originalLength(),
							 | 
						||
| 
								 | 
							
								                            closeSpan
							 | 
						||
| 
								 | 
							
								                        );
							 | 
						||
| 
								 | 
							
								                    }
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function annotateSourceCode(fileCoverage, sourceStore) {
							 | 
						||
| 
								 | 
							
								    let codeArray;
							 | 
						||
| 
								 | 
							
								    let lineCoverageArray;
							 | 
						||
| 
								 | 
							
								    try {
							 | 
						||
| 
								 | 
							
								        const sourceText = sourceStore.getSource(fileCoverage.path);
							 | 
						||
| 
								 | 
							
								        const code = sourceText.split(/(?:\r?\n)|\r/);
							 | 
						||
| 
								 | 
							
								        let count = 0;
							 | 
						||
| 
								 | 
							
								        const structured = code.map(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(
							 | 
						||
| 
								 | 
							
								            item => customEscape(item.text.toString()) || ' '
							 | 
						||
| 
								 | 
							
								        );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        lineCoverageArray = structured.map(item => ({
							 | 
						||
| 
								 | 
							
								            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(line => {
							 | 
						||
| 
								 | 
							
								                codeArray.push(line);
							 | 
						||
| 
								 | 
							
								                lineCoverageArray.push({ covered: 'no', hits: 0 });
							 | 
						||
| 
								 | 
							
								            });
							 | 
						||
| 
								 | 
							
								        return {
							 | 
						||
| 
								 | 
							
								            annotatedCode: codeArray,
							 | 
						||
| 
								 | 
							
								            lineCoverage: lineCoverageArray,
							 | 
						||
| 
								 | 
							
								            maxLines: codeArray.length
							 | 
						||
| 
								 | 
							
								        };
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								module.exports = {
							 | 
						||
| 
								 | 
							
								    annotateSourceCode
							 | 
						||
| 
								 | 
							
								};
							 |