transformer.js 5.21 KB
/*
 Copyright 2015, Yahoo Inc.
 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
 */
"use strict";

var debug = require('debug')('istanbuljs'),
    pathutils = require('./pathutils'),
    libCoverage = require('istanbul-lib-coverage'),
    MappedCoverage = require('./mapped').MappedCoverage;

function isInvalidPosition (pos) {
    return !pos || typeof pos.line !== "number" || typeof pos.column !== "number" || pos.line < 0 || pos.column < 0;
}

/**
 * determines the original position for a given location
 * @param  {SourceMapConsumer} sourceMap the source map
 * @param  {Object} location the original location Object
 * @returns {Object} the remapped location Object
 */
function getMapping(sourceMap, location, origFile) {

    if (!location) {
        return null;
    }

    if (isInvalidPosition(location.start) || isInvalidPosition(location.end)) {
        return null;
    }

    var start = sourceMap.originalPositionFor(location.start),
        end = sourceMap.originalPositionFor(location.end);

    /* istanbul ignore if: edge case too hard to test for */
    if (!(start && end)) {
        return null;
    }
    if (!(start.source && end.source)) {
        return null;
    }
    if (start.source !== end.source) {
        return null;
    }

    /* istanbul ignore if: edge case too hard to test for */
    if (start.line === null || start.column === null) {
        return null;
    }
    /* istanbul ignore if: edge case too hard to test for */
    if (end.line === null || end.column === null) {
        return null;
    }

    if (start.line === end.line && start.column === end.column) {
        end = sourceMap.originalPositionFor({
            line: location.end.line,
            column: location.end.column,
            bias: 2
        });
        end.column = end.column - 1;
    }

    return {
        source: pathutils.relativeTo(start.source, origFile),
        loc: {
            start: {
                line: start.line,
                column: start.column
            },
            end: {
                line: end.line,
                column: end.column
            }
        }
    };
}

function SourceMapTransformer(finder, opts) {
    opts = opts || {};
    this.finder = finder;
    this.baseDir = opts.baseDir || process.cwd();
}

SourceMapTransformer.prototype.processFile = function (fc, sourceMap, coverageMapper) {
    var changes = 0;

    Object.keys(fc.statementMap).forEach(function (s) {
        var loc = fc.statementMap[s],
            hits = fc.s[s],
            mapping = getMapping(sourceMap, loc, fc.path),
            mappedCoverage;

        if (mapping) {
            changes += 1;
            mappedCoverage = coverageMapper(mapping.source);
            mappedCoverage.addStatement(mapping.loc, hits);
        }
    });

    Object.keys(fc.fnMap).forEach(function (f) {
        var fnMeta = fc.fnMap[f],
            hits = fc.f[f],
            mapping = getMapping(sourceMap, fnMeta.decl, fc.path),
            spanMapping = getMapping(sourceMap, fnMeta.loc, fc.path),
            mappedCoverage;

        if (mapping && spanMapping && mapping.source === spanMapping.source) {
            changes += 1;
            mappedCoverage = coverageMapper(mapping.source);
            mappedCoverage.addFunction(fnMeta.name, mapping.loc, spanMapping.loc, hits);
        }
    });

    Object.keys(fc.branchMap).forEach(function (b) {
        var branchMeta = fc.branchMap[b],
            source,
            hits = fc.b[b],
            mapping,
            locs = [],
            mappedHits = [],
            mappedCoverage,
            skip,
            i;
        for (i = 0; i < branchMeta.locations.length; i += 1) {
            mapping = getMapping(sourceMap, branchMeta.locations[i], fc.path);
            if (mapping) {
                if (!source) {
                    source = mapping.source;
                }
                if (mapping.source !== source) {
                    skip = true;
                }
                locs.push(mapping.loc);
                mappedHits.push(hits[i]);
            }
        }
        if (!skip && locs.length > 0) {
            changes += 1;
            mappedCoverage = coverageMapper(source);
            mappedCoverage.addBranch(branchMeta.type, locs[0] /* XXX */, locs, mappedHits);
        }
    });

    return changes > 0;
};

SourceMapTransformer.prototype.transform = function (coverageMap) {
    var that = this,
        finder = this.finder,
        output = {},
        getMappedCoverage = function (file) {
            if (!output[file]) {
                output[file] = new MappedCoverage(file);
            }
            return output[file];
        };

    coverageMap.files().forEach(function (file) {
        var fc = coverageMap.fileCoverageFor(file),
            sourceMap = finder(file),
            changed;

        if (!sourceMap) {
            output[file] = fc;
            return;
        }

        changed = that.processFile(fc, sourceMap, getMappedCoverage);
        if (!changed) {
            debug('File [' + file + '] ignored, nothing could be mapped');
        }
    });
    return libCoverage.createCoverageMap(output);
};

module.exports = {
    create: function (finder, opts) {
        return new SourceMapTransformer(finder, opts);
    }
};