<commit_before>/*
 (c) 2013, Vladimir Agafonkin
 RBush, a JavaScript library for high-performance 2D spatial indexing of points and rectangles.
 https://github.com/mourner/rbush
*/

(function () { 'use strict';

function rbush(maxEntries, format) {
    // jshint newcap: false, validthis: true, evil: true

    if (!(this instanceof rbush)) {
        // allow constructing RBush trees without "new"
        return new rbush(maxEntries, format);
    }

    this._maxEntries = Math.max(4, maxEntries || 9);
    this._minFill = Math.max(2, Math.floor(this._maxEntries * 0.4));

    // data format (minX, minY, maxX, maxY accessors),
    // uses eval-type function compilation instead of accepting functions to simplify customization;
    // performance is not affected since this happens only once

    format = format || ['[0]', '[1]', '[2]', '[3]'];

    this._sortMinX = this._createSort(format[0]);
    this._sortMinY = this._createSort(format[1]);

    this._sortNodeMinX = this._createSort('.bbox[0]');
    this._sortNodeMinY = this._createSort('.bbox[1]');

    this._toBBox = new Function('a', 'return [a' + format.join(', a') + '];');

    this.clear();
}

rbush.prototype = {

    search: function (bbox) {

        var node = this.data,
            result = [];

        if (!node || !this._intersects(bbox, node.bbox)) { return result; }

        var nodesToSearch = [],
            i, len, child, childBBox;

        while (node) {
            for (i = 0, len = node.children.length; i < len; i++) {
                child = node.children[i];
                childBBox = node.leaf ? this._toBBox(child) : child.bbox;

                if (this._intersects(bbox, childBBox)) {
                    (node.leaf ? result : nodesToSearch).push(child);
                }
            }

            node = nodesToSearch.pop();
        }

        return result;
    },

    load: function (data) {
        if (!(data && data.length)) { return this; }

        // recursively build the tree with the given data from stratch using OMT algorithm
        this.data = this._build(data.slice(), 0);

        // recursively calculate all bboxes
        this._calcBBoxes(this.data, true);

        return this;
    },

    insert: function (item) {
        if (item) {
            this._insert(item);
        }
        return this;
    },

    clear: function () {
        this.data = {
            children: [],
            leaf: true,
            bbox: this._infiniteBBox(),
            height: 1
        };
        return this;
    },

    remove: function (item) {
        if (!item) { return this; }

        var node = this.data,
            bbox = this._toBBox(item),
            path = [],
            indexes = [],
            i, parent, index, goingUp;

        // depth-first iterative tree traversal
        while (node || path.length) {

            if (!node) { // go up
                node = path.pop();
                parent = path[path.length - 1];
                i = indexes.pop();
                goingUp = true;
            }

            if (node.leaf) { // check current node
                index = node.children.indexOf(item);

                if (index !== -1) { // item found
                    node.children.splice(index, 1);
                    path.push(node);
                    this._condense(path);
                    return this;
                }
            }

            if (!goingUp && !node.leaf && this._intersects(bbox, node.bbox)) { // go down
                path.push(node);
                indexes.push(i);
                i = 0;
                parent = node;
                node = node.children[0];

            } else if (parent) { // go right
                i++;
                node = parent.children[i];
                goingUp = false;

            } else { // nothing found
                node = null;
            }
        }

        return this;
    },

    toJSON: function () {
        // TODO cleanup nodes from area, enlargement, sqDist properties
        return this.data;
    },

    fromJSON: function (data) {
        this.data = data;
        return this;
    },

    _build: function (items, level, height) {

        var N = items.length,
            M = this._maxEntries;

        if (N <= M) {
            return {
                children: items,
                leaf: true,
                height: 1
            };
        }

        if (!level) {
            // target height of the bulk-loaded tree
            height = Math.ceil(Math.log(N) / Math.log(M));

            // target number of root entries to maximize storage utilization
            M = Math.ceil(N / Math.pow(M, height - 1));

            items.sort(this._sortMinX);
        }

        var node = {
            children: [],
            height: height
        };

        var N1 = Math.ceil(N / M) * Math.ceil(Math.sqrt(M)),
            N2 = Math.ceil(N / M),
            sortFn = level % 2 === 1 ? this._sortMinX : this._sortMinY,
            i, j, slice, sliceLen, childNode;

        // split the items into M mostly square tiles
        for (i = 0; i < N; i += N1) {
            slice = items.slice(i, i + N1).sort(sortFn);

            for (j = 0, sliceLen = slice.length; j < sliceLen; j += N2) {
                // pack each entry recursively
                childNode = this._build(slice.slice(j, j + N2), level + 1, height - 1);
                node.children.push(childNode);
            }
        }

        return node;
    },

    _chooseSubtree: function (bbox, node, level, path) {

        var i, len, child, targetNode, area, enlargement, minArea, minEnlargement;

        while (true) {
            path.push(node);

            if (node.leaf || path.length - 1 === level) { break; }

            minArea = minEnlargement = Infinity;

            for (i = 0, len = node.children.length; i < len; i++) {
                child = node.children[i];
                area = this._area(child.bbox);
                enlargement = this._enlargedArea(bbox, child.bbox) - area;

                // choose entry with the least area enlargement
                if (enlargement < minEnlargement) {
                    minEnlargement = enlargement;
                    minArea = area < minArea ? area : minArea;
                    targetNode = child;

                } else if (enlargement === minEnlargement) {
                    // otherwise choose one with the smallest area
                    if (area < minArea) {
                        minArea = area;
                        targetNode = child;
                    }
                }
            }

            node = targetNode;
        }

        return node;
    },

    _insert: function (item, level, isNode, root) {

        var bbox = isNode ? item.bbox : this._toBBox(item),
            insertPath = [];

        // recursively find the best node for accommodating the item, saving all nodes along the path too
        var node = this._chooseSubtree(bbox, root || this.data, level, insertPath),
            splitOccured;

        if (typeof level === 'undefined') {
            level = insertPath.length - 1;
        }

        // put the item into the node
        node.children.push(item);
        this._extend(node.bbox, bbox);

        // deal with node overflow if it happened
        do {
            splitOccured = false;
            if (insertPath[level].children.length > this._maxEntries) {
                this._split(insertPath, level);
                splitOccured = true;
                level--;
            }
        } while (level >= 0 && splitOccured);

        // adjust bboxes along the insertion path
        this._adjustParentBBoxes(bbox, insertPath, level);
    },

    _split: function (insertPath, level) {

        var node = insertPath[level],
            M = node.children.length,
            m = this._minFill;

        this._chooseSplitAxis(node, m, M);

        var newNode = {
            children: node.children.splice(this._chooseSplitIndex(node, m, M)),
            height: node.height
        };

        if (node.leaf) {
            newNode.leaf = true;
        }

        this._calcBBoxes(node);
        this._calcBBoxes(newNode);

        if (level) {
            insertPath[level - 1].children.push(newNode);
        } else {
            // split root node
            this.data = {};
            this.data.children = [node, newNode];
            this.data.height = node.height + 1;
            this._calcBBoxes(this.data);
        }
    },

    _chooseSplitIndex: function (node, m, M) {

        var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index;

        minOverlap = minArea = Infinity;

        for (i = m; i <= M - m; i++) {
            bbox1 = this._distBBox(node, 0, i);
            bbox2 = this._distBBox(node, i, M);

            overlap = this._intersectionArea(bbox1, bbox2);
            area = this._area(bbox1) + this._area(bbox2);

            // choose distribution with minimum overlap
            if (overlap < minOverlap) {
                minOverlap = overlap;
                index = i;

                minArea = area < minArea ? area : minArea;

            } else if (overlap === minOverlap) {
                // otherwise choose distribution with minimum area
                if (area < minArea) {
                    minArea = area;
                    index = i;
                }
            }
        }

        return index;
    },

    _chooseSplitAxis: function (node, m, M) {

        var sortMinX = node.leaf ? this._sortMinX : this._sortNodeMinX,
            sortMinY = node.leaf ? this._sortMinY : this._sortNodeMinY,
            xMargin = this._allDistMargin(node, m, M, sortMinX),
            yMargin = this._allDistMargin(node, m, M, sortMinY);

        // if total distributions margin value is minimal for x, sort by minX,
        // otherwise it's already sorted by minY

        if (xMargin < yMargin) {
            node.children.sort(sortMinX);
        }
    },

    _allDistMargin: function (node, m, M, sort) {

        node.children.sort(sort);

        var leftBBox = this._distBBox(node, 0, m),
            rightBBox = this._distBBox(node, M - m, M),
            margin = this._margin(leftBBox) + this._margin(rightBBox),
            i, child;

        for (i = m; i < M - m; i++) {
            child = node.children[i];
            this._extend(leftBBox, node.leaf ? this._toBBox(child) : child.bbox);
            margin += this._margin(leftBBox);
        }

        for (i = M - m - 1; i >= 0; i--) {
            child = node.children[i];
            this._extend(rightBBox, node.leaf ? this._toBBox(child) : child.bbox);
            margin += this._margin(rightBBox);
        }

        return margin;
    },

    _distBBox: function (node, k, p) {
        var bbox = this._infiniteBBox();

        for (var i = k, child; i < p; i++) {
            child = node.children[i];
            this._extend(bbox, node.leaf ? this._toBBox(child) : child.bbox);
        }

        return bbox;
    },

    _calcBBoxes: function (node, recursive) {
        node.bbox = this._infiniteBBox();

        for (var i = 0, len = node.children.length, child; i < len; i++) {
            child = node.children[i];

            if (node.leaf) {
                this._extend(node.bbox, this._toBBox(child));
            } else {
                if (recursive) {
                    this._calcBBoxes(child, recursive);
                }
                this._extend(node.bbox, child.bbox);
            }
        }
    },

    _adjustParentBBoxes: function (bbox, path, level) {
        // adjust bboxes along the given tree path
        for (var i = level; i >= 0; i--) {
            this._extend(path[i].bbox, bbox);
        }
    },

    _condense: function (path) {
        // go through the path, removing empty nodes and updating bboxes
        for (var i = path.length - 1, parent; i >= 0; i--) {
            if (i > 0 && path[i].children.length === 0) {
                parent = path[i - 1].children;
                parent.splice(parent.indexOf(path[i]), 1);
            } else {
                this._calcBBoxes(path[i]);
            }
        }
    },

    _intersects: function (bbox, bbox2) {
        return bbox2[0] <= bbox[2] &&
               bbox2[1] <= bbox[3] &&
               bbox2[2] >= bbox[0] &&
               bbox2[3] >= bbox[1];
    },

    _extend: function (bbox, bbox2) {
        bbox[0] = Math.min(bbox[0], bbox2[0]);
        bbox[1] = Math.min(bbox[1], bbox2[1]);
        bbox[2] = Math.max(bbox[2], bbox2[2]);
        bbox[3] = Math.max(bbox[3], bbox2[3]);
        return bbox;
    },

    _area: function (bbox) {
        return (bbox[2] - bbox[0]) * (bbox[3] - bbox[1]);
    },

    _margin: function (bbox) {
        return (bbox[2] - bbox[0]) + (bbox[3] - bbox[1]);
    },

    _enlargedArea: function (bbox, bbox2) {
        return (Math.max(bbox2[2], bbox[2]) - Math.min(bbox2[0], bbox[0])) *
               (Math.max(bbox2[3], bbox[3]) - Math.min(bbox2[1], bbox[1]));
    },

    _intersectionArea: function (bbox, bbox2) {
        var minX = Math.max(bbox[0], bbox2[0]),
            maxX = Math.min(bbox[2], bbox2[2]),
            minY = Math.max(bbox[1], bbox2[1]),
            maxY = Math.min(bbox[3], bbox2[3]);

        return Math.max(0, maxX - minX) * Math.max(0, maxY - minY);
    },

    _infiniteBBox: function () {
        return [Infinity, Infinity, -Infinity, -Infinity];
    },

    _createSort: function (accessor) {
        // jshint evil: true
        return new Function('a', 'b', 'return a' + accessor + ' > b' + accessor + ' ? 1 : -1;');
    }
};

if (typeof module !== 'undefined') {
    module.exports = rbush;
} else {
    window.rbush = rbush;
}

})();
<commit_msg>implement STLT bulk insertion<commit_after>/*
 (c) 2013, Vladimir Agafonkin
 RBush, a JavaScript library for high-performance 2D spatial indexing of points and rectangles.
 https://github.com/mourner/rbush
*/

(function () { 'use strict';

function rbush(maxEntries, format) {
    // jshint newcap: false, validthis: true, evil: true

    if (!(this instanceof rbush)) {
        // allow constructing RBush trees without "new"
        return new rbush(maxEntries, format);
    }

    this._maxEntries = Math.max(4, maxEntries || 9);
    this._minFill = Math.max(2, Math.floor(this._maxEntries * 0.4));

    // data format (minX, minY, maxX, maxY accessors),
    // uses eval-type function compilation instead of accepting functions to simplify customization;
    // performance is not affected since this happens only once

    format = format || ['[0]', '[1]', '[2]', '[3]'];

    this._sortMinX = this._createSort(format[0]);
    this._sortMinY = this._createSort(format[1]);

    this._sortNodeMinX = this._createSort('.bbox[0]');
    this._sortNodeMinY = this._createSort('.bbox[1]');

    this._toBBox = new Function('a', 'return [a' + format.join(', a') + '];');

    this.clear();
}

rbush.prototype = {

    search: function (bbox) {

        var node = this.data,
            result = [];

        if (!node || !this._intersects(bbox, node.bbox)) { return result; }

        var nodesToSearch = [],
            i, len, child, childBBox;

        while (node) {
            for (i = 0, len = node.children.length; i < len; i++) {
                child = node.children[i];
                childBBox = node.leaf ? this._toBBox(child) : child.bbox;

                if (this._intersects(bbox, childBBox)) {
                    (node.leaf ? result : nodesToSearch).push(child);
                }
            }

            node = nodesToSearch.pop();
        }

        return result;
    },

    load: function (data) {
        if (!(data && data.length)) { return this; }

        if (data.length < this._minFill) {
            for (var i = 0, len = data.length; i < len; i++) {
                this.insert(data[i]);
            }
            return this;
        }

        // recursively build the tree with the given data from stratch using OMT algorithm
        var node = this._build(data.slice(), 0);
        this._calcBBoxes(node, true);

        if (!this.data.children.length) {
            // save as is if tree is empty
            this.data = node;

        } else if (this.data.height === node.height) {
            // split root if trees have the same height
            this._splitRoot(this.data, node);

        } else {
            if (this.data.height < node.height) {
                // swap trees if loaded one is bigger
                var tmpNode = this.data;
                this.data = node;
                node = tmpNode;
            }

            // insert the small tree into the large tree at appropriate level
            this._insert(node, this.data.height - node.height - 1, true);
        }

        return this;
    },

    insert: function (item) {
        if (item) {
            this._insert(item);
        }
        return this;
    },

    clear: function () {
        this.data = {
            children: [],
            leaf: true,
            bbox: this._infiniteBBox(),
            height: 1
        };
        return this;
    },

    remove: function (item) {
        if (!item) { return this; }

        var node = this.data,
            bbox = this._toBBox(item),
            path = [],
            indexes = [],
            i, parent, index, goingUp;

        // depth-first iterative tree traversal
        while (node || path.length) {

            if (!node) { // go up
                node = path.pop();
                parent = path[path.length - 1];
                i = indexes.pop();
                goingUp = true;
            }

            if (node.leaf) { // check current node
                index = node.children.indexOf(item);

                if (index !== -1) { // item found
                    node.children.splice(index, 1);
                    path.push(node);
                    this._condense(path);
                    return this;
                }
            }

            if (!goingUp && !node.leaf && this._intersects(bbox, node.bbox)) { // go down
                path.push(node);
                indexes.push(i);
                i = 0;
                parent = node;
                node = node.children[0];

            } else if (parent) { // go right
                i++;
                node = parent.children[i];
                goingUp = false;

            } else { // nothing found
                node = null;
            }
        }

        return this;
    },

    toJSON: function () {
        // TODO cleanup nodes from area, enlargement, sqDist properties
        return this.data;
    },

    fromJSON: function (data) {
        this.data = data;
        return this;
    },

    _build: function (items, level, height) {

        var N = items.length,
            M = this._maxEntries;

        if (N <= M) {
            return {
                children: items,
                leaf: true,
                height: 1
            };
        }

        if (!level) {
            // target height of the bulk-loaded tree
            height = Math.ceil(Math.log(N) / Math.log(M));

            // target number of root entries to maximize storage utilization
            M = Math.ceil(N / Math.pow(M, height - 1));

            items.sort(this._sortMinX);
        }

        var node = {
            children: [],
            height: height
        };

        var N1 = Math.ceil(N / M) * Math.ceil(Math.sqrt(M)),
            N2 = Math.ceil(N / M),
            sortFn = level % 2 === 1 ? this._sortMinX : this._sortMinY,
            i, j, slice, sliceLen, childNode;

        // split the items into M mostly square tiles
        for (i = 0; i < N; i += N1) {
            slice = items.slice(i, i + N1).sort(sortFn);

            for (j = 0, sliceLen = slice.length; j < sliceLen; j += N2) {
                // pack each entry recursively
                childNode = this._build(slice.slice(j, j + N2), level + 1, height - 1);
                node.children.push(childNode);
            }
        }

        return node;
    },

    _chooseSubtree: function (bbox, node, level, path) {

        var i, len, child, targetNode, area, enlargement, minArea, minEnlargement;

        while (true) {
            path.push(node);

            if (node.leaf || path.length - 1 === level) { break; }

            minArea = minEnlargement = Infinity;

            for (i = 0, len = node.children.length; i < len; i++) {
                child = node.children[i];
                area = this._area(child.bbox);
                enlargement = this._enlargedArea(bbox, child.bbox) - area;

                // choose entry with the least area enlargement
                if (enlargement < minEnlargement) {
                    minEnlargement = enlargement;
                    minArea = area < minArea ? area : minArea;
                    targetNode = child;

                } else if (enlargement === minEnlargement) {
                    // otherwise choose one with the smallest area
                    if (area < minArea) {
                        minArea = area;
                        targetNode = child;
                    }
                }
            }

            node = targetNode;
        }

        return node;
    },

    _insert: function (item, level, isNode, root) {

        var bbox = isNode ? item.bbox : this._toBBox(item),
            insertPath = [];

        // recursively find the best node for accommodating the item, saving all nodes along the path too
        var node = this._chooseSubtree(bbox, root || this.data, level, insertPath),
            splitOccured;

        if (typeof level === 'undefined') {
            level = insertPath.length - 1;
        }

        // put the item into the node
        node.children.push(item);
        this._extend(node.bbox, bbox);

        // deal with node overflow if it happened
        do {
            splitOccured = false;
            if (insertPath[level].children.length > this._maxEntries) {
                this._split(insertPath, level);
                splitOccured = true;
                level--;
            }
        } while (level >= 0 && splitOccured);

        // adjust bboxes along the insertion path
        this._adjustParentBBoxes(bbox, insertPath, level);
    },

    _split: function (insertPath, level) {

        var node = insertPath[level],
            M = node.children.length,
            m = this._minFill;

        this._chooseSplitAxis(node, m, M);

        var newNode = {
            children: node.children.splice(this._chooseSplitIndex(node, m, M)),
            height: node.height
        };

        if (node.leaf) {
            newNode.leaf = true;
        }

        this._calcBBoxes(node);
        this._calcBBoxes(newNode);

        if (level) {
            insertPath[level - 1].children.push(newNode);
        } else {
            this._splitRoot(node, newNode);
        }
    },

    _splitRoot: function (node, newNode) {
        // split root node
        this.data = {};
        this.data.children = [node, newNode];
        this.data.height = node.height + 1;
        this._calcBBoxes(this.data);
    },

    _chooseSplitIndex: function (node, m, M) {

        var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index;

        minOverlap = minArea = Infinity;

        for (i = m; i <= M - m; i++) {
            bbox1 = this._distBBox(node, 0, i);
            bbox2 = this._distBBox(node, i, M);

            overlap = this._intersectionArea(bbox1, bbox2);
            area = this._area(bbox1) + this._area(bbox2);

            // choose distribution with minimum overlap
            if (overlap < minOverlap) {
                minOverlap = overlap;
                index = i;

                minArea = area < minArea ? area : minArea;

            } else if (overlap === minOverlap) {
                // otherwise choose distribution with minimum area
                if (area < minArea) {
                    minArea = area;
                    index = i;
                }
            }
        }

        return index;
    },

    _chooseSplitAxis: function (node, m, M) {

        var sortMinX = node.leaf ? this._sortMinX : this._sortNodeMinX,
            sortMinY = node.leaf ? this._sortMinY : this._sortNodeMinY,
            xMargin = this._allDistMargin(node, m, M, sortMinX),
            yMargin = this._allDistMargin(node, m, M, sortMinY);

        // if total distributions margin value is minimal for x, sort by minX,
        // otherwise it's already sorted by minY

        if (xMargin < yMargin) {
            node.children.sort(sortMinX);
        }
    },

    _allDistMargin: function (node, m, M, sort) {

        node.children.sort(sort);

        var leftBBox = this._distBBox(node, 0, m),
            rightBBox = this._distBBox(node, M - m, M),
            margin = this._margin(leftBBox) + this._margin(rightBBox),
            i, child;

        for (i = m; i < M - m; i++) {
            child = node.children[i];
            this._extend(leftBBox, node.leaf ? this._toBBox(child) : child.bbox);
            margin += this._margin(leftBBox);
        }

        for (i = M - m - 1; i >= 0; i--) {
            child = node.children[i];
            this._extend(rightBBox, node.leaf ? this._toBBox(child) : child.bbox);
            margin += this._margin(rightBBox);
        }

        return margin;
    },

    _distBBox: function (node, k, p) {
        var bbox = this._infiniteBBox();

        for (var i = k, child; i < p; i++) {
            child = node.children[i];
            this._extend(bbox, node.leaf ? this._toBBox(child) : child.bbox);
        }

        return bbox;
    },

    _calcBBoxes: function (node, recursive) {
        node.bbox = this._infiniteBBox();

        for (var i = 0, len = node.children.length, child; i < len; i++) {
            child = node.children[i];

            if (node.leaf) {
                this._extend(node.bbox, this._toBBox(child));
            } else {
                if (recursive) {
                    this._calcBBoxes(child, recursive);
                }
                this._extend(node.bbox, child.bbox);
            }
        }
    },

    _adjustParentBBoxes: function (bbox, path, level) {
        // adjust bboxes along the given tree path
        for (var i = level; i >= 0; i--) {
            this._extend(path[i].bbox, bbox);
        }
    },

    _condense: function (path) {
        // go through the path, removing empty nodes and updating bboxes
        for (var i = path.length - 1, parent; i >= 0; i--) {
            if (i > 0 && path[i].children.length === 0) {
                parent = path[i - 1].children;
                parent.splice(parent.indexOf(path[i]), 1);
            } else {
                this._calcBBoxes(path[i]);
            }
        }
    },

    _intersects: function (bbox, bbox2) {
        return bbox2[0] <= bbox[2] &&
               bbox2[1] <= bbox[3] &&
               bbox2[2] >= bbox[0] &&
               bbox2[3] >= bbox[1];
    },

    _extend: function (bbox, bbox2) {
        bbox[0] = Math.min(bbox[0], bbox2[0]);
        bbox[1] = Math.min(bbox[1], bbox2[1]);
        bbox[2] = Math.max(bbox[2], bbox2[2]);
        bbox[3] = Math.max(bbox[3], bbox2[3]);
        return bbox;
    },

    _area: function (bbox) {
        return (bbox[2] - bbox[0]) * (bbox[3] - bbox[1]);
    },

    _margin: function (bbox) {
        return (bbox[2] - bbox[0]) + (bbox[3] - bbox[1]);
    },

    _enlargedArea: function (bbox, bbox2) {
        return (Math.max(bbox2[2], bbox[2]) - Math.min(bbox2[0], bbox[0])) *
               (Math.max(bbox2[3], bbox[3]) - Math.min(bbox2[1], bbox[1]));
    },

    _intersectionArea: function (bbox, bbox2) {
        var minX = Math.max(bbox[0], bbox2[0]),
            maxX = Math.min(bbox[2], bbox2[2]),
            minY = Math.max(bbox[1], bbox2[1]),
            maxY = Math.min(bbox[3], bbox2[3]);

        return Math.max(0, maxX - minX) * Math.max(0, maxY - minY);
    },

    _infiniteBBox: function () {
        return [Infinity, Infinity, -Infinity, -Infinity];
    },

    _createSort: function (accessor) {
        // jshint evil: true
        return new Function('a', 'b', 'return a' + accessor + ' > b' + accessor + ' ? 1 : -1;');
    }
};

if (typeof module !== 'undefined') {
    module.exports = rbush;
} else {
    window.rbush = rbush;
}

})();
<|endoftext|>


<commit_before>function require(path) {
  switch (path.substr(path.lastIndexOf('/') + 1)) {
    case 'assert':
      return assert;
    case '..':
      return R;
    case 'lazylist':
      return R.lazylist;
    default:
      throw new Error('Unexpected require path "' + path + '"');
  }
}
<commit_msg>stoopid IE<commit_after>function require(path) {
  switch (path.substr(path.lastIndexOf('/') + 1)) {
    case 'assert':
      return assert;
    case '..':
      return this.R ? this.R : this.ramda;
    case 'lazylist':
      return this.R ? this.R.lazylist : this.ramda.lazylist;
    default:
      throw new Error('Unexpected require path "' + path + '"');
  }
}
<|endoftext|>


<commit_before>module.exports = {
    env: {
        browser: true,
        es6: true,
    },
    extends: [
        'plugin:react/recommended',
        'kokarn',
    ],
    parserOptions: {
        ecmaFeatures: {
            jsx: true,
        },
        sourceType: 'module',
    },
    plugins: [
        'react',
    ],
    rules: {
        'no-extra-parens': [
            'off',
        ],
        'react/default-props-match-prop-types': [
            'error',
        ],
        'react/display-name': [
            'error',
        ],
        'react/forbid-component-props': [
            'error',
        ],
        'react/forbid-foreign-prop-types': [
            'error',
        ],
        'react/forbid-prop-types': [
            'error',
        ],
        'react/jsx-boolean-value': [
            'error',
        ],
        'react/jsx-closing-bracket-location': [
            'error',
        ],
        'react/jsx-closing-tag-location': [
            'error',
        ],
        'react/jsx-curly-spacing': [
            'error',
            'always',
        ],
        'react/jsx-equals-spacing': [
            'error',
            'always',
        ],
        'react/jsx-filename-extension': [
            'error',
        ],
        'react/jsx-first-prop-new-line': [
            'error',
            'always',
        ],
        'react/jsx-handler-names': [
            'error',
        ],
        'react/jsx-indent': [
            'error',
        ],
        'react/jsx-indent-props': [
            'error',
        ],
        'react/jsx-key': [
            'error',
        ],
        'react/jsx-max-props-per-line': [
            'error',
        ],
        'react/jsx-no-bind': [
            'error',
        ],
        'react/jsx-no-comment-textnodes': [
            'error',
        ],
        'react/jsx-no-duplicate-props': [
            'error',
        ],
        'react/jsx-no-literals': [
            'error',
        ],
        'react/jsx-no-target-blank': [
            'error',
        ],
        'react/jsx-no-undef': [
            'error',
        ],
        'react/jsx-pascal-case': [
            'error',
        ],
        'react/jsx-sort-props': [
            'error',
        ],
        'react/jsx-tag-spacing': [
            'error',
        ],
        'react/jsx-uses-react': [
            'error',
        ],
        'react/jsx-uses-vars': [
            'error',
        ],
        'react/jsx-wrap-multilines': [
            'error',
        ],
        'react/no-array-index-key': [
            'error',
        ],
        'react/no-children-prop': [
            'error',
        ],
        'react/no-danger': [
            'error',
        ],
        'react/no-danger-with-children': [
            'error',
        ],
        'react/no-deprecated': [
            'error',
        ],
        'react/no-did-mount-set-state': [
            'error',
        ],
        'react/no-did-update-set-state': [
            'error',
        ],
        'react/no-direct-mutation-state': [
            'error',
        ],
        'react/no-find-dom-node': [
            'error',
        ],
        'react/no-is-mounted': [
            'error',
        ],
        'react/no-multi-comp': [
            'error',
        ],
        'react/no-redundant-should-component-update': [
            'error',
        ],
        'react/no-render-return-value': [
            'error',
        ],
        'react/no-string-refs': [
            'error',
        ],
        'react/no-unescaped-entities': [
            'error',
        ],
        'react/no-unknown-property': [
            'error',
        ],
        'react/no-unused-prop-types': [
            'error',
        ],
        'react/prefer-es6-class': [
            'error',
        ],
        'react/require-default-props': [
            'error',
        ],
        'react/require-render-return': [
            'error',
        ],
        'react/self-closing-comp': [
            'error',
        ],
        'react/sort-comp': [
            'error',
        ],
        'react/sort-prop-types': [
            'error',
        ],
        'react/style-prop-object': [
            'error',
        ],
        'react/void-dom-elements-no-children': [
            'error',
        ],
    },
};
<commit_msg>allow long functions in react<commit_after>module.exports = {
    env: {
        browser: true,
        es6: true,
    },
    extends: [
        'plugin:react/recommended',
        'kokarn',
    ],
    parserOptions: {
        ecmaFeatures: {
            jsx: true,
        },
        sourceType: 'module',
    },
    plugins: [
        'react',
    ],
    rules: {
        'max-lines-per-function': [
            'off',
        ],
        'no-extra-parens': [
            'off',
        ],
        'react/default-props-match-prop-types': [
            'error',
        ],
        'react/display-name': [
            'error',
        ],
        'react/forbid-component-props': [
            'error',
        ],
        'react/forbid-foreign-prop-types': [
            'error',
        ],
        'react/forbid-prop-types': [
            'error',
        ],
        'react/jsx-boolean-value': [
            'error',
        ],
        'react/jsx-closing-bracket-location': [
            'error',
        ],
        'react/jsx-closing-tag-location': [
            'error',
        ],
        'react/jsx-curly-spacing': [
            'error',
            'always',
        ],
        'react/jsx-equals-spacing': [
            'error',
            'always',
        ],
        'react/jsx-filename-extension': [
            'error',
        ],
        'react/jsx-first-prop-new-line': [
            'error',
            'always',
        ],
        'react/jsx-handler-names': [
            'error',
        ],
        'react/jsx-indent': [
            'error',
        ],
        'react/jsx-indent-props': [
            'error',
        ],
        'react/jsx-key': [
            'error',
        ],
        'react/jsx-max-props-per-line': [
            'error',
        ],
        'react/jsx-no-bind': [
            'error',
        ],
        'react/jsx-no-comment-textnodes': [
            'error',
        ],
        'react/jsx-no-duplicate-props': [
            'error',
        ],
        'react/jsx-no-literals': [
            'error',
        ],
        'react/jsx-no-target-blank': [
            'error',
        ],
        'react/jsx-no-undef': [
            'error',
        ],
        'react/jsx-pascal-case': [
            'error',
        ],
        'react/jsx-sort-props': [
            'error',
        ],
        'react/jsx-tag-spacing': [
            'error',
        ],
        'react/jsx-uses-react': [
            'error',
        ],
        'react/jsx-uses-vars': [
            'error',
        ],
        'react/jsx-wrap-multilines': [
            'error',
        ],
        'react/no-array-index-key': [
            'error',
        ],
        'react/no-children-prop': [
            'error',
        ],
        'react/no-danger': [
            'error',
        ],
        'react/no-danger-with-children': [
            'error',
        ],
        'react/no-deprecated': [
            'error',
        ],
        'react/no-did-mount-set-state': [
            'error',
        ],
        'react/no-did-update-set-state': [
            'error',
        ],
        'react/no-direct-mutation-state': [
            'error',
        ],
        'react/no-find-dom-node': [
            'error',
        ],
        'react/no-is-mounted': [
            'error',
        ],
        'react/no-multi-comp': [
            'error',
        ],
        'react/no-redundant-should-component-update': [
            'error',
        ],
        'react/no-render-return-value': [
            'error',
        ],
        'react/no-string-refs': [
            'error',
        ],
        'react/no-unescaped-entities': [
            'error',
        ],
        'react/no-unknown-property': [
            'error',
        ],
        'react/no-unused-prop-types': [
            'error',
        ],
        'react/prefer-es6-class': [
            'error',
        ],
        'react/require-default-props': [
            'error',
        ],
        'react/require-render-return': [
            'error',
        ],
        'react/self-closing-comp': [
            'error',
        ],
        'react/sort-comp': [
            'error',
        ],
        'react/sort-prop-types': [
            'error',
        ],
        'react/style-prop-object': [
            'error',
        ],
        'react/void-dom-elements-no-children': [
            'error',
        ],
    },
};
<|endoftext|>


<commit_before>module.exports = {
    extends: [
        'plugin:react/recommended',
        'kokarn'
    ],
    env: {
        'browser': true,
        'es6': true
    },
    parserOptions: {
        'ecmaFeatures': {
            'experimentalObjectRestSpread': true,
            'jsx': true
        },
        'sourceType': 'module'
    },
    plugins: [
        'react'
    ],
    rules: {
        'react/forbid-prop-types': [
            'error'
        ],
        'react/no-danger': [
            'error'
        ],
        'react/no-danger-with-children': [
            'error'
        ],
        'react/no-did-mount-set-state': [
            'error'
        ],
        'react/no-did-update-set-state': [
            'error'
        ],
        'react/no-multi-comp': [
            'error'
        ],
        'react/no-render-return-value': [
            'error'
        ],
        'react/no-string-refs': [
            'error'
        ],
        'react/no-unused-prop-types': [
            'error'
        ],
        'react/prefer-es6-class': [
            'error'
        ],
        'react/self-closing-comp': [
            'error'
        ],
        'react/sort-comp': [
            'error'
        ],
        'react/sort-prop-types': [
            'error'
        ],
        'react/style-prop-object': [
            'error'
        ],
        'react/jsx-boolean-value': [
            'error'
        ],
        'react/jsx-closing-bracket-location': [
            'error'
        ],
        'react/jsx-curly-spacing': [
            'error',
            'always'
        ],
        'react/jsx-equals-spacing': [
            'error',
            'always'
        ],
        'react/jsx-filename-extension': [
            'error'
        ],
        'react/jsx-first-prop-new-line': [
            'error',
            'always'
        ],
        'react/jsx-handler-names': [
            'error'
        ],
        'react/jsx-indent': [
            'error'
        ],
        'react/jsx-indent-props': [
            'error'
        ],
        'react/jsx-key': [
            'error'
        ],
        'react/jsx-max-props-per-line': [
            'error'
        ],
        'react/jsx-no-bind': [
            'error'
        ],
        'react/jsx-no-comment-textnodes': [
            'error'
        ],
        'react/jsx-no-literals': [
            'error'
        ],
        'react/jsx-no-target-blank': [
            'error'
        ],
        'react/jsx-pascal-case': [
            'error'
        ],
        'react/jsx-sort-props': [
            'error'
        ],
        'react/jsx-space-before-closing': [
            'error'
        ],
        'react/jsx-wrap-multilines': [
            'error'
        ]
    }
};
<commit_msg>Update react styling to make a bit more sense<commit_after>module.exports = {
    extends: [
        'plugin:react/recommended',
        'kokarn'
    ],
    env: {
        'browser': true,
        'es6': true
    },
    parserOptions: {
        'ecmaFeatures': {
            'experimentalObjectRestSpread': true,
            'jsx': true
        },
        'sourceType': 'module'
    },
    plugins: [
        'react'
    ],
    rules: {
        'no-extra-parens': [
            'off'
        ],
        'react/forbid-prop-types': [
            'error'
        ],
        'react/no-danger': [
            'error'
        ],
        'react/no-danger-with-children': [
            'error'
        ],
        'react/no-did-mount-set-state': [
            'error'
        ],
        'react/no-did-update-set-state': [
            'error'
        ],
        'react/no-multi-comp': [
            'error'
        ],
        'react/no-render-return-value': [
            'error'
        ],
        'react/no-string-refs': [
            'error'
        ],
        'react/no-unused-prop-types': [
            'error'
        ],
        'react/prefer-es6-class': [
            'error'
        ],
        'react/self-closing-comp': [
            'error'
        ],
        'react/sort-comp': [
            'error'
        ],
        'react/sort-prop-types': [
            'error'
        ],
        'react/style-prop-object': [
            'error'
        ],
        'react/jsx-boolean-value': [
            'error'
        ],
        'react/jsx-closing-bracket-location': [
            'error'
        ],
        'react/jsx-curly-spacing': [
            'error',
            'always'
        ],
        'react/jsx-equals-spacing': [
            'error',
            'always'
        ],
        'react/jsx-filename-extension': [
            'error'
        ],
        'react/jsx-first-prop-new-line': [
            'error',
            'always'
        ],
        'react/jsx-handler-names': [
            'error'
        ],
        'react/jsx-indent': [
            'error'
        ],
        'react/jsx-indent-props': [
            'error'
        ],
        'react/jsx-key': [
            'error'
        ],
        'react/jsx-max-props-per-line': [
            'error'
        ],
        'react/jsx-no-bind': [
            'error'
        ],
        'react/jsx-no-comment-textnodes': [
            'error'
        ],
        'react/jsx-no-literals': [
            'error'
        ],
        'react/jsx-no-target-blank': [
            'error'
        ],
        'react/jsx-pascal-case': [
            'error'
        ],
        'react/jsx-sort-props': [
            'error'
        ],
        'react/jsx-space-before-closing': [
            'error'
        ],
        'react/jsx-wrap-multilines': [
            'error'
        ]
    }
};
<|endoftext|>


<commit_before>const http = require('http');
const stream = require('stream');
const isStream = require('is-stream');
const _ = require('underscore');
const FormData = require('form-data');
const qs = require('qs');
const messy = require('messy');

const resolveExpectedResponseProperties = require('./resolveExpectedResponseProperties');

module.exports = {
  name: 'unexpected-express',
  version: require('../package.json').version,
  installInto(expect) {
    expect = expect.child();

    const topLevelExpect = expect;

    expect.use(require('unexpected-messy'));

    expect.addType({
      name: 'IncomingMessage',
      base: 'object',
      identify(obj) {
        return (
          obj && obj.constructor && obj.constructor.name === 'IncomingMessage'
        );
      },
      inspect(obj, depth, output) {
        output.text(obj.constructor.name, 'jsFunctionName');
      }
    });

    expect.exportAssertion(
      [
        '<function> to yield exchange satisfying <any>', // Please prefer this one because it does use 'to satisfy' semantics
        '<function> to yield exchange <any>'
      ],
      (expect, subject, value) => {
        if (!subject.handle || !subject.set) {
          // This check is from the lib/application file in express @ 4.10.2.
          // If we get inside here, we have something that is not an express app
          // https://github.com/strongloop/express/blob/661435256384165bb656cb7b6046b4138ca24c9e/lib/application.js#L186
          subject = require('express')().use(subject);
          expect.subjectOutput = function() {
            this.text('express middleware');
          };
        } else {
          expect.subjectOutput = function() {
            this.text('express app');
          };
        }

        const missingProperties = Object.keys(value).filter(
          key => key !== 'request' && key !== 'response'
        );
        if (missingProperties.length > 0) {
          throw new Error(`Property "${missingProperties[0]}" does not exist`);
        }

        const context = {};
        const nextCalls = [];

        const requestProperties =
          typeof value.request === 'string'
            ? { url: value.request }
            : _.extend({}, value.request);

        let requestBody = requestProperties.body;
        let requestForm = requestProperties.form;

        const httpRequest = new messy.HttpRequest({
          method: requestProperties.method,
          url: requestProperties.url || '/',
          protocolName: 'HTTP',
          protocolVersion: requestProperties.httpVersion || '1.1',
          headers: requestProperties.headers,
          unchunkedBody: requestProperties.unchunkedBody,
          rawBody: requestProperties.rawBody
        });

        function updateHttpRequestBody(requestBody) {
          if (Buffer.isBuffer(requestBody)) {
            httpRequest.unchunkedBody = requestBody;
          } else {
            // string or non-Buffer object (implies JSON)
            httpRequest.body = requestBody;
          }
        }

        if (requestForm) {
          if (typeof requestForm !== 'string') {
            requestForm = qs.stringify(requestForm);
          }
          requestBody = requestForm;
          if (!httpRequest.headers.has('Content-Type')) {
            httpRequest.headers.set(
              'Content-Type',
              'application/x-www-form-urlencoded'
            );
          }
        }

        if (typeof requestBody !== 'undefined') {
          updateHttpRequestBody(requestBody);
        } else if (
          'unchunkedBody' in requestProperties ||
          'rawBody' in requestProperties
        ) {
          requestBody = httpRequest.body;
        }

        value = { ...value };
        delete value.request;
        delete requestProperties.method;
        delete requestProperties.url;
        delete requestProperties.httpVersion;
        delete requestProperties.headers;
        delete requestProperties.body;
        delete requestProperties.unchunkedBody;
        delete requestProperties.rawBody;
        httpRequest.method = httpRequest.method || 'GET';
        if (
          httpRequest.encrypted &&
          typeof requestProperties.https === 'undefined'
        ) {
          // Warn if conflicting?
          requestProperties.https = true;
        }
        if (requestProperties.formData) {
          if (requestBody) {
            throw new Error(
              'unexpected-express: The "body" and "formData" options are not supported together'
            );
          }
          requestBody = new FormData();
          Object.keys(requestProperties.formData).forEach(name => {
            let value = requestProperties.formData[name];

            let options = {};

            if (isStream.readable(value) && value.path) {
              options.filename = value.path;
            } else if (typeof value === 'object' && !Buffer.isBuffer(value)) {
              options = _.extend({}, value);
              value = options.value;
              delete options.value;
              if (options.fileName) {
                options.filename = options.fileName;
                delete options.fileName;
              }
            }
            requestBody.append(name, value, options);
          });

          delete requestProperties.formData;
        }

        if (typeof requestProperties.query !== 'undefined') {
          if (
            typeof requestProperties.query === 'object' &&
            requestProperties.query
          ) {
            const stringifiedQueryString = qs.stringify(
              requestProperties.query
            );
            if (stringifiedQueryString) {
              httpRequest.url +=
                (httpRequest.url.indexOf('?') === -1 ? '?' : '&') +
                stringifiedQueryString;
            }
          } else {
            httpRequest.url +=
              (httpRequest.url.indexOf('?') === -1 ? '?' : '&') +
              String(requestProperties.query);
          }
          delete requestProperties.query;
        }

        const {
          expectedResponseProperties,
          expectedMetadata
        } = resolveExpectedResponseProperties(value);

        const req = new http.IncomingMessage({
          destroy() {
            requestDestroyed = true;
          }
        });

        if (typeof requestBody !== 'undefined') {
          httpRequest.headers.set('Transfer-Encoding', 'chunked');
          if (requestBody.pipe) {
            if (
              requestBody.constructor &&
              requestBody.constructor.name === 'FormData'
            ) {
              if (!httpRequest.headers.has('Content-Type')) {
                httpRequest.headers.set(
                  'Content-Type',
                  `multipart/form-data; boundary=${requestBody.getBoundary()}`
                );
                // form-data pauses its streams by default for some reason:
                setImmediate(() => {
                  requestBody.resume();
                });
              }
            }
            const requestBodyChunks = [];
            requestBody
              .on('data', chunk => {
                if (!Buffer.isBuffer(chunk)) {
                  chunk = Buffer.from(chunk, 'utf-8');
                }
                requestBodyChunks.push(chunk);
                req.push(chunk);
              })
              .on('end', () => {
                updateHttpRequestBody(Buffer.concat(requestBodyChunks));
                req.push(null);
              })
              .on('error', err => {
                req.emit('error', err);
              });
          } else {
            if (
              typeof requestBody === 'object' &&
              !Buffer.isBuffer(requestBody)
            ) {
              if (!httpRequest.headers.has('Content-Type')) {
                httpRequest.headers.set('Content-Type', 'application/json');
              }
            }

            if (
              !httpRequest.headers.has('Content-Length') &&
              !httpRequest.headers.has('Transfer-Encoding')
            ) {
              httpRequest.headers.set(
                'Content-Length',
                String(requestBody.length)
              );
            }
            setImmediate(() => {
              // To work around nodejs v0.10.x issue with old-style streams, see also https://github.com/stream-utils/raw-body/pull/34
              req.push(httpRequest.unchunkedBody);
              req.push(null);
            });
          }
        } else {
          req.push(null);
        }

        // Make req.connection.setTimeout a no-op so that req.setTimeout doesn't break
        // in this mocked state:
        req.connection.setTimeout = () => {};

        req.httpVersion = httpRequest.protocolVersion;
        const matchProtocolVersion = String(httpRequest.protocolVersion).match(
          /^(\d+)(?:\.(\d+))$/
        );
        if (matchProtocolVersion) {
          req.httpVersionMajor = parseInt(matchProtocolVersion[1], 10);
          req.httpVersionMinor = matchProtocolVersion[2]
            ? parseInt(matchProtocolVersion[2], 10)
            : 0;
        }
        req.connection.encrypted = !!requestProperties.https;
        delete requestProperties.https;
        req.connection.remoteAddress =
          requestProperties.remoteAddress ||
          requestProperties.ip ||
          '127.0.0.1';
        delete requestProperties.ip;
        delete requestProperties.remoteAddress;
        req.headers = {};
        httpRequest.headers.getNames().forEach(headerName => {
          const headerNameLowerCase = headerName.toLowerCase();
          if (headerNameLowerCase === 'set-cookie') {
            req.headers[headerNameLowerCase] = [].concat(
              httpRequest.headers.getAll(headerName)
            );
          } else {
            req.headers[headerNameLowerCase] = httpRequest.headers
              .getAll(headerName)
              .join(', ');
          }
        });
        req.method = httpRequest.method;
        req.url = httpRequest.requestLine.url;
        _.extend(req, requestProperties);

        const res = new http.ServerResponse(req);
        _.extend(res, requestProperties.res); // Allows for specifying eg. res.locals
        delete requestProperties.res;
        res.locals = res.locals || {};

        const rawResponseChunks = [];
        res.assignSocket(new stream.Writable());
        res.connection._write = (chunk, encoding, cb) => {
          rawResponseChunks.push(chunk);
          cb();
        };
        let isAsync = false;

        let isDestroyed = false;

        var requestDestroyed = false;

        let done = false;

        let errorPassedToNext;

        res.connection.destroy = () => {
          isDestroyed = true;
        };

        setImmediate(() => {
          isAsync = true;
        });

        return expect
          .promise((resolve, reject) => {
            ['write', 'end', 'destroy'].forEach(methodName => {
              const orig = res[methodName];
              res[methodName] = function(chunk, encoding) {
                const returnValue = orig.apply(this, arguments);
                isDestroyed = isDestroyed || methodName === 'destroy';
                if (methodName === 'end' || methodName === 'destroy') {
                  resolve();
                }
                // Don't attempt to implement backpressure, since we're buffering the entire response anyway.
                if (methodName !== 'write') {
                  return returnValue;
                }
              };
            });
            subject(req, res, function(err, _req, _res, _next) {
              nextCalls.push(arguments);
              if (nextCalls.length > 1) {
                if (done) {
                  if (err) {
                    throw err;
                  } else {
                    throw new Error('next called more than once');
                  }
                } else {
                  // Will be reported as a failure later
                  return;
                }
              }
              errorPassedToNext = err;
              if (typeof err === 'number') {
                const statusCode = err;
                err = new Error(`${statusCode}`);
                err.statusCode = statusCode;
              }
              res.statusCode = (err && (err.statusCode || err.status)) || 404;
              resolve();
            });
          })
          .then(() => {
            if (res.connection._writableState.corked > 0) {
              // Wait for the connection to become uncorked before proceeding
              const originalUncork = res.connection.uncork;
              return expect.promise((resolve, reject) => {
                res.connection.uncork = function() {
                  const returnValue = originalUncork.apply(this, arguments);
                  if (res.connection._writableState.corked === 0) {
                    resolve();
                  }
                  return returnValue;
                };
              });
            }
          })
          .then(() => {
            _.extend(context, {
              req,
              res,
              httpRequest,
              metadata: {
                strictAsync: isAsync,
                errorPassedToNext: false,
                isDestroyed,
                requestDestroyed,
                nextCalled: nextCalls.length > 0,
                locals: res.locals,
                url: req.url
              }
            });

            if (
              errorPassedToNext &&
              errorPassedToNext.statusCode &&
              !res.headersSent
            ) {
              res.writeHead(errorPassedToNext.statusCode);
            }

            if (!res.headersSent) {
              // Make sure that the already set headers get flushed:
              res.writeHead(404);
            }

            const httpResponse = (context.httpResponse = new messy.HttpResponse(
              rawResponseChunks.length > 0
                ? Buffer.concat(rawResponseChunks)
                : res._header
            ));
            if (typeof httpResponse.rawBody === 'undefined') {
              httpResponse.rawBody = Buffer.from([]);
            }
            httpResponse.statusCode = httpResponse.statusCode || res.statusCode;

            if (errorPassedToNext) {
              context.metadata.errorPassedToNext = errorPassedToNext;
              if (typeof expectedMetadata.errorPassedToNext !== 'undefined') {
                if (expectedMetadata.errorPassedToNext === true) {
                  context.metadata.errorPassedToNext = true;
                } else if (
                  typeof expectedMetadata.errorPassedToNext === 'string'
                ) {
                  context.metadata.errorPassedToNext =
                    errorPassedToNext.message;
                } else {
                  context.metadata.errorPassedToNext = errorPassedToNext;
                }
              } else if (typeof errorPassedToNext.statusCode === 'number') {
                // FIXME
                if (!httpResponse.headers.get('Content-Type')) {
                  httpResponse.headers.set('Content-Type', 'text/plain');
                  httpResponse.body = errorPassedToNext.stack;
                }
              } else {
                throw errorPassedToNext;
              }
            }

            context.httpExchange = new messy.HttpExchange({
              request: context.httpRequest,
              response: context.httpResponse
            });

            const promiseByKey = {
              httpExchange: expect.promise(() =>
                expect(context.httpExchange, 'to satisfy', {
                  response: expectedResponseProperties
                })
              ),
              metadata: {}
            };
            Object.keys(expectedMetadata).forEach(key => {
              promiseByKey.metadata[key] = expect.promise(() =>
                topLevelExpect(
                  context.metadata[key],
                  'to satisfy',
                  expectedMetadata[key]
                )
              );
            });

            return expect.promise.settle(promiseByKey).then(promises => {
              if (promises.some(promise => promise.isRejected())) {
                expect.fail({
                  diff(output) {
                    if (promiseByKey.httpExchange.isRejected()) {
                      output.append(
                        promiseByKey.httpExchange.reason().getDiff(output)
                      );
                    } else {
                      output.appendInspected(context.httpExchange);
                    }
                    Object.keys(promiseByKey.metadata).forEach(key => {
                      if (promiseByKey.metadata[key].isRejected()) {
                        output.nl().annotationBlock(function() {
                          this.text(key)
                            .text(':')
                            .sp()
                            .append(
                              promiseByKey.metadata[key]
                                .reason()
                                .getErrorMessage(output)
                            );
                        });
                      }
                    });
                    return output;
                  }
                });
              }
            });
          })
          .then(() => {
            if (nextCalls.length > 1) {
              throw new Error('next called more than once');
            }
            done = true; // Tell the next function that subsequent calls should cause an exception to be thrown
            return context;
          });
      }
    );
  }
};

module.exports.messy = messy;
<commit_msg>Split out a resolveRequestBody() function.<commit_after>const http = require('http');
const stream = require('stream');
const isStream = require('is-stream');
const _ = require('underscore');
const FormData = require('form-data');
const qs = require('qs');
const messy = require('messy');

const resolveExpectedResponseProperties = require('./resolveExpectedResponseProperties');

function resolveRequestBody(requestProperties, httpRequest) {
  let requestBody = requestProperties.body;

  if (requestProperties.form) {
    const requestForm = requestProperties.form;
    if (typeof requestForm !== 'string') {
      requestBody = qs.stringify(requestForm);
    } else {
      requestBody = requestForm;
    }

    if (!httpRequest.headers.has('Content-Type')) {
      httpRequest.headers.set(
        'Content-Type',
        'application/x-www-form-urlencoded'
      );
    }
  } else if (requestProperties.formData) {
    if (requestBody) {
      throw new Error(
        'unexpected-express: The "body" and "formData" options are not supported together'
      );
    }

    requestBody = new FormData();

    Object.keys(requestProperties.formData).forEach(name => {
      let value = requestProperties.formData[name];

      let options = {};

      if (isStream.readable(value) && value.path) {
        options.filename = value.path;
      } else if (typeof value === 'object' && !Buffer.isBuffer(value)) {
        options = _.extend({}, value);
        value = options.value;
        delete options.value;
        if (options.fileName) {
          options.filename = options.fileName;
          delete options.fileName;
        }
      }
      requestBody.append(name, value, options);
    });

    delete requestProperties.formData;
  } else if (
    'unchunkedBody' in requestProperties ||
    'rawBody' in requestProperties
  ) {
    requestBody = httpRequest.body;
  }

  return requestBody;
}

module.exports = {
  name: 'unexpected-express',
  version: require('../package.json').version,
  installInto(expect) {
    expect = expect.child();

    const topLevelExpect = expect;

    expect.use(require('unexpected-messy'));

    expect.addType({
      name: 'IncomingMessage',
      base: 'object',
      identify(obj) {
        return (
          obj && obj.constructor && obj.constructor.name === 'IncomingMessage'
        );
      },
      inspect(obj, depth, output) {
        output.text(obj.constructor.name, 'jsFunctionName');
      }
    });

    expect.exportAssertion(
      [
        '<function> to yield exchange satisfying <any>', // Please prefer this one because it does use 'to satisfy' semantics
        '<function> to yield exchange <any>'
      ],
      (expect, subject, value) => {
        if (!subject.handle || !subject.set) {
          // This check is from the lib/application file in express @ 4.10.2.
          // If we get inside here, we have something that is not an express app
          // https://github.com/strongloop/express/blob/661435256384165bb656cb7b6046b4138ca24c9e/lib/application.js#L186
          subject = require('express')().use(subject);
          expect.subjectOutput = function() {
            this.text('express middleware');
          };
        } else {
          expect.subjectOutput = function() {
            this.text('express app');
          };
        }

        const missingProperties = Object.keys(value).filter(
          key => key !== 'request' && key !== 'response'
        );
        if (missingProperties.length > 0) {
          throw new Error(`Property "${missingProperties[0]}" does not exist`);
        }

        const context = {};
        const nextCalls = [];

        const requestProperties =
          typeof value.request === 'string'
            ? { url: value.request }
            : _.extend({}, value.request);

        const httpRequest = new messy.HttpRequest({
          method: requestProperties.method,
          url: requestProperties.url || '/',
          protocolName: 'HTTP',
          protocolVersion: requestProperties.httpVersion || '1.1',
          headers: requestProperties.headers,
          unchunkedBody: requestProperties.unchunkedBody,
          rawBody: requestProperties.rawBody
        });

        function updateHttpRequestBody(requestBody) {
          if (Buffer.isBuffer(requestBody)) {
            httpRequest.unchunkedBody = requestBody;
          } else {
            // string or non-Buffer object (implies JSON)
            httpRequest.body = requestBody;
          }
        }

        const requestBody = resolveRequestBody(requestProperties, httpRequest);
        updateHttpRequestBody(requestBody);

        value = { ...value };
        delete value.request;
        delete requestProperties.method;
        delete requestProperties.url;
        delete requestProperties.httpVersion;
        delete requestProperties.headers;
        delete requestProperties.body;
        delete requestProperties.unchunkedBody;
        delete requestProperties.rawBody;
        httpRequest.method = httpRequest.method || 'GET';
        if (
          httpRequest.encrypted &&
          typeof requestProperties.https === 'undefined'
        ) {
          // Warn if conflicting?
          requestProperties.https = true;
        }

        if (typeof requestProperties.query !== 'undefined') {
          if (
            typeof requestProperties.query === 'object' &&
            requestProperties.query
          ) {
            const stringifiedQueryString = qs.stringify(
              requestProperties.query
            );
            if (stringifiedQueryString) {
              httpRequest.url +=
                (httpRequest.url.indexOf('?') === -1 ? '?' : '&') +
                stringifiedQueryString;
            }
          } else {
            httpRequest.url +=
              (httpRequest.url.indexOf('?') === -1 ? '?' : '&') +
              String(requestProperties.query);
          }
          delete requestProperties.query;
        }

        const {
          expectedResponseProperties,
          expectedMetadata
        } = resolveExpectedResponseProperties(value);

        const req = new http.IncomingMessage({
          destroy() {
            requestDestroyed = true;
          }
        });

        if (typeof requestBody !== 'undefined') {
          httpRequest.headers.set('Transfer-Encoding', 'chunked');
          if (requestBody.pipe) {
            if (
              requestBody.constructor &&
              requestBody.constructor.name === 'FormData'
            ) {
              if (!httpRequest.headers.has('Content-Type')) {
                httpRequest.headers.set(
                  'Content-Type',
                  `multipart/form-data; boundary=${requestBody.getBoundary()}`
                );
                // form-data pauses its streams by default for some reason:
                setImmediate(() => {
                  requestBody.resume();
                });
              }
            }
            const requestBodyChunks = [];
            requestBody
              .on('data', chunk => {
                if (!Buffer.isBuffer(chunk)) {
                  chunk = Buffer.from(chunk, 'utf-8');
                }
                requestBodyChunks.push(chunk);
                req.push(chunk);
              })
              .on('end', () => {
                updateHttpRequestBody(Buffer.concat(requestBodyChunks));
                req.push(null);
              })
              .on('error', err => {
                req.emit('error', err);
              });
          } else {
            if (
              typeof requestBody === 'object' &&
              !Buffer.isBuffer(requestBody)
            ) {
              if (!httpRequest.headers.has('Content-Type')) {
                httpRequest.headers.set('Content-Type', 'application/json');
              }
            }

            if (
              !httpRequest.headers.has('Content-Length') &&
              !httpRequest.headers.has('Transfer-Encoding')
            ) {
              httpRequest.headers.set(
                'Content-Length',
                String(requestBody.length)
              );
            }
            setImmediate(() => {
              // To work around nodejs v0.10.x issue with old-style streams, see also https://github.com/stream-utils/raw-body/pull/34
              req.push(httpRequest.unchunkedBody);
              req.push(null);
            });
          }
        } else {
          req.push(null);
        }

        // Make req.connection.setTimeout a no-op so that req.setTimeout doesn't break
        // in this mocked state:
        req.connection.setTimeout = () => {};

        req.httpVersion = httpRequest.protocolVersion;
        const matchProtocolVersion = String(httpRequest.protocolVersion).match(
          /^(\d+)(?:\.(\d+))$/
        );
        if (matchProtocolVersion) {
          req.httpVersionMajor = parseInt(matchProtocolVersion[1], 10);
          req.httpVersionMinor = matchProtocolVersion[2]
            ? parseInt(matchProtocolVersion[2], 10)
            : 0;
        }
        req.connection.encrypted = !!requestProperties.https;
        delete requestProperties.https;
        req.connection.remoteAddress =
          requestProperties.remoteAddress ||
          requestProperties.ip ||
          '127.0.0.1';
        delete requestProperties.ip;
        delete requestProperties.remoteAddress;
        req.headers = {};
        httpRequest.headers.getNames().forEach(headerName => {
          const headerNameLowerCase = headerName.toLowerCase();
          if (headerNameLowerCase === 'set-cookie') {
            req.headers[headerNameLowerCase] = [].concat(
              httpRequest.headers.getAll(headerName)
            );
          } else {
            req.headers[headerNameLowerCase] = httpRequest.headers
              .getAll(headerName)
              .join(', ');
          }
        });
        req.method = httpRequest.method;
        req.url = httpRequest.requestLine.url;
        _.extend(req, requestProperties);

        const res = new http.ServerResponse(req);
        _.extend(res, requestProperties.res); // Allows for specifying eg. res.locals
        delete requestProperties.res;
        res.locals = res.locals || {};

        const rawResponseChunks = [];
        res.assignSocket(new stream.Writable());
        res.connection._write = (chunk, encoding, cb) => {
          rawResponseChunks.push(chunk);
          cb();
        };
        let isAsync = false;

        let isDestroyed = false;

        var requestDestroyed = false;

        let done = false;

        let errorPassedToNext;

        res.connection.destroy = () => {
          isDestroyed = true;
        };

        setImmediate(() => {
          isAsync = true;
        });

        return expect
          .promise((resolve, reject) => {
            ['write', 'end', 'destroy'].forEach(methodName => {
              const orig = res[methodName];
              res[methodName] = function(chunk, encoding) {
                const returnValue = orig.apply(this, arguments);
                isDestroyed = isDestroyed || methodName === 'destroy';
                if (methodName === 'end' || methodName === 'destroy') {
                  resolve();
                }
                // Don't attempt to implement backpressure, since we're buffering the entire response anyway.
                if (methodName !== 'write') {
                  return returnValue;
                }
              };
            });
            subject(req, res, function(err, _req, _res, _next) {
              nextCalls.push(arguments);
              if (nextCalls.length > 1) {
                if (done) {
                  if (err) {
                    throw err;
                  } else {
                    throw new Error('next called more than once');
                  }
                } else {
                  // Will be reported as a failure later
                  return;
                }
              }
              errorPassedToNext = err;
              if (typeof err === 'number') {
                const statusCode = err;
                err = new Error(`${statusCode}`);
                err.statusCode = statusCode;
              }
              res.statusCode = (err && (err.statusCode || err.status)) || 404;
              resolve();
            });
          })
          .then(() => {
            if (res.connection._writableState.corked > 0) {
              // Wait for the connection to become uncorked before proceeding
              const originalUncork = res.connection.uncork;
              return expect.promise((resolve, reject) => {
                res.connection.uncork = function() {
                  const returnValue = originalUncork.apply(this, arguments);
                  if (res.connection._writableState.corked === 0) {
                    resolve();
                  }
                  return returnValue;
                };
              });
            }
          })
          .then(() => {
            _.extend(context, {
              req,
              res,
              httpRequest,
              metadata: {
                strictAsync: isAsync,
                errorPassedToNext: false,
                isDestroyed,
                requestDestroyed,
                nextCalled: nextCalls.length > 0,
                locals: res.locals,
                url: req.url
              }
            });

            if (
              errorPassedToNext &&
              errorPassedToNext.statusCode &&
              !res.headersSent
            ) {
              res.writeHead(errorPassedToNext.statusCode);
            }

            if (!res.headersSent) {
              // Make sure that the already set headers get flushed:
              res.writeHead(404);
            }

            const httpResponse = (context.httpResponse = new messy.HttpResponse(
              rawResponseChunks.length > 0
                ? Buffer.concat(rawResponseChunks)
                : res._header
            ));
            if (typeof httpResponse.rawBody === 'undefined') {
              httpResponse.rawBody = Buffer.from([]);
            }
            httpResponse.statusCode = httpResponse.statusCode || res.statusCode;

            if (errorPassedToNext) {
              context.metadata.errorPassedToNext = errorPassedToNext;
              if (typeof expectedMetadata.errorPassedToNext !== 'undefined') {
                if (expectedMetadata.errorPassedToNext === true) {
                  context.metadata.errorPassedToNext = true;
                } else if (
                  typeof expectedMetadata.errorPassedToNext === 'string'
                ) {
                  context.metadata.errorPassedToNext =
                    errorPassedToNext.message;
                } else {
                  context.metadata.errorPassedToNext = errorPassedToNext;
                }
              } else if (typeof errorPassedToNext.statusCode === 'number') {
                // FIXME
                if (!httpResponse.headers.get('Content-Type')) {
                  httpResponse.headers.set('Content-Type', 'text/plain');
                  httpResponse.body = errorPassedToNext.stack;
                }
              } else {
                throw errorPassedToNext;
              }
            }

            context.httpExchange = new messy.HttpExchange({
              request: context.httpRequest,
              response: context.httpResponse
            });

            const promiseByKey = {
              httpExchange: expect.promise(() =>
                expect(context.httpExchange, 'to satisfy', {
                  response: expectedResponseProperties
                })
              ),
              metadata: {}
            };
            Object.keys(expectedMetadata).forEach(key => {
              promiseByKey.metadata[key] = expect.promise(() =>
                topLevelExpect(
                  context.metadata[key],
                  'to satisfy',
                  expectedMetadata[key]
                )
              );
            });

            return expect.promise.settle(promiseByKey).then(promises => {
              if (promises.some(promise => promise.isRejected())) {
                expect.fail({
                  diff(output) {
                    if (promiseByKey.httpExchange.isRejected()) {
                      output.append(
                        promiseByKey.httpExchange.reason().getDiff(output)
                      );
                    } else {
                      output.appendInspected(context.httpExchange);
                    }
                    Object.keys(promiseByKey.metadata).forEach(key => {
                      if (promiseByKey.metadata[key].isRejected()) {
                        output.nl().annotationBlock(function() {
                          this.text(key)
                            .text(':')
                            .sp()
                            .append(
                              promiseByKey.metadata[key]
                                .reason()
                                .getErrorMessage(output)
                            );
                        });
                      }
                    });
                    return output;
                  }
                });
              }
            });
          })
          .then(() => {
            if (nextCalls.length > 1) {
              throw new Error('next called more than once');
            }
            done = true; // Tell the next function that subsequent calls should cause an exception to be thrown
            return context;
          });
      }
    );
  }
};

module.exports.messy = messy;
<|endoftext|>


<commit_before>'use strict';

var assert = require('assert');

var Errors = require('./errors.js');
var serialize = require('../serialize.js');
var isSameType = require('./lib/is-same-type.js');

module.exports = SubTypeChecker;

function SubTypeChecker(meta) {
    this.meta = meta;
}

SubTypeChecker.prototype.checkSubType =
function checkSubType(node, parent, child) {
    assert(node && node.type, 'ast node must have type');
    assert(parent && parent.type, 'parent must have a type');
    assert(child && child.type, 'child must have a type');

    var result;

    if (parent === child) {
        result = null;
    } else if (parent.type === 'typeLiteral') {
        result = this.checkTypeLiteralSubType(node, parent, child);
    } else if (parent.type === 'genericLiteral') {
        result = this.checkGenericLiteralSubType(node, parent, child);
    } else if (parent.type === 'function') {
        result = this.checkFunctionSubType(node, parent, child);
    } else if (parent.type === 'object') {
        result = this.checkObjectSubType(node, parent, child);
    } else if (parent.type === 'valueLiteral') {
        result = this.checkValueLiteralSubType(node, parent, child);
    } else if (parent.type === 'unionType') {
        result = this.checkUnionSubType(node, parent, child);
    } else if (parent.type === 'freeLiteral') {
        return reportTypeMisMatch(node, parent, child);
    } else {
        throw new Error('not implemented sub type: ' + parent.type);
    }

    assert(typeof result !== 'boolean', 'must return error or null');
    return result;
};

/*eslint complexity: [2, 30]*/
SubTypeChecker.prototype.checkTypeLiteralSubType =
function checkTypeLiteralSubType(node, parent, child) {
    if (!parent.builtin && !parent.isGeneric) {
        return reportTypeMisMatch(node, parent, child);
        // throw new Error('not implemented, sub type for non-builtin');
    }
    if (parent.isGeneric) {
        if (!child.isGeneric) {
            return reportTypeMisMatch(node, parent, child);
        }

        if (parent.name !== child.name) {
            return reportTypeMisMatch(node, parent, child);
        }

        return null;
    }

    if (parent.name === '%Any%%ModuleExports') {
        return null;
    } else if (parent.name === '%Boolean%%Mixed') {
        return null;
    } else if (parent.name === '%Void%%UnknownReturn') {
        return null;
    } else if (parent.name === 'Function' && child.type === 'function') {
        return null;
    } else if (parent.name === 'Object' && child.type === 'object') {
        return null;
    }

    if (child.type !== 'typeLiteral') {
        return reportTypeMisMatch(node, parent, child);
        // return new Error('[Internal] expected type literal');
    }

    var name = parent.name;
    if (name === 'Object') {
        // TODO: model that Error inherits from Object
        if (child.name !== name && child.name !== 'Error') {
            return reportTypeMisMatch(node, parent, child);
        }
    } else if (name === 'Array') {
        if (child.name !== name) {
            return new Error('[Internal] Not an array');
        }
    } else if (name === 'String') {
        if (child.name !== name) {
            return reportTypeMisMatch(node, parent, child);
        }
    } else if (name === 'void') {
        if (child.name !== name) {
            return new Error('[Internal] Not a void');
        }
    } else if (name === 'RegExp') {
        if (child.name !== name) {
            return reportTypeMisMatch(node, parent, child);
        }
    } else if (name === 'Number') {
        if (child.name !== name) {
            return reportTypeMisMatch(node, parent, child);
        }
    } else if (name === 'Boolean') {
        if (child.name !== name) {
            return reportTypeMisMatch(node, parent, child);
        }
    } else if (name === 'Error') {
        if (child.name !== name) {
            return reportTypeMisMatch(node, parent, child);
        }
    } else {
        throw new Error('NotImplemented: ' + parent.name);
    }
};

SubTypeChecker.prototype.checkValueLiteralSubType =
function checkValueLiteralSubType(node, parent, child) {
    if (parent.name === 'null' && child.type === 'typeLiteral' &&
        child.builtin && child.name === '%Null%%Default'
    ) {
        return null;
    }

    if (child.type !== 'valueLiteral') {
        return reportTypeMisMatch(node, parent, child);
    }

    var name = parent.name;
    if (name === 'null') {
        if (child.name !== name) {
            return new Error('[Internal] Not null');
        }
    } else if (name === 'undefined') {
        if (child.name !== name) {
            return new Error('[Internal] Not undefined');
        }
    } else {
        // console.log('valueLiteral?', {
        //     p: parent,
        //     c: child
        // });
        throw new Error('NotImplemented: ' + parent.type);
    }
};

SubTypeChecker.prototype.checkGenericLiteralSubType =
function checkGenericLiteralSubType(node, parent, child) {
    if (parent.value.name === 'Array' &&
        parent.value.builtin &&
        child.type === 'typeLiteral' &&
        child.name === '%Array%%Empty'
    ) {
        return null;
    }

    if (parent.value.name === 'Object' &&
        parent.value.builtin &&
        child.type === 'typeLiteral' &&
        child.name === '%Object%%Empty'
    ) {
        return null;
    }

    if (child.type !== 'genericLiteral') {
        return reportTypeMisMatch(node, parent, child);
    }

    var isSame = isSameType(parent.value, child.value);
    if (!isSame) {
        return reportTypeMisMatch(node, parent, child);
    }

    if (parent.generics.length !== child.generics.length) {
        throw new Error('generics mismatch');
    }

    for (var i = 0; i < parent.generics.length; i++) {
        isSame = isSameType(parent.generics[i], child.generics[i]);

        if (!isSame) {
            return reportTypeMisMatch(node, parent, child);
        }
    }

    return null;
};

SubTypeChecker.prototype.checkFunctionSubType =
function checkFunctionSubType(node, parent, child) {
    if (child.type !== 'function') {
        return reportTypeMisMatch(node, parent, child);
    }

    var err = this.checkSubType(node, parent.result, child.result);
    if (err) {
        return err;
    }

    if (parent.thisArg) {
        err = this.checkSubType(node, parent.thisArg, child.thisArg);
        if (err) {
            return err;
        }
    }

    if (parent.args.length !== child.args.length) {
        throw new Error('[Internal] function args mismatch');
    }

    for (var i = 0; i < parent.args.length; i++) {
        err = this.checkSubType(node, parent.args[i], child.args[i]);
        if (err) {
            return err;
        }
    }

    return null;
};

SubTypeChecker.prototype.checkObjectSubType =
function checkObjectSubType(node, parent, child) {
    if (child.type !== 'object') {
        return reportTypeMisMatch(node, parent, child);
    }

    if (parent.keyValues.length !== child.keyValues.length) {
        return Errors.IncorrectFieldCount({
            expected: serialize(parent._raw || parent),
            expectedFields: parent.keyValues.length,
            actual: serialize(child._raw || child),
            actualFields: child.keyValues.length,
            loc: node.loc,
            line: node.loc.start.line
        });
        // throw new Error('[Internal] object key pairs mismatch');
    }

    var err;
    for (var i = 0; i < parent.keyValues.length; i++) {
        // TODO: check key names...

        err = this.checkSubType(
            node, parent.keyValues[i].value, child.keyValues[i].value
        );
        if (err) {
            return err;
        }
    }

    return null;
};

SubTypeChecker.prototype.checkUnionSubType =
function checkUnionSubType(node, parent, child) {
    // Check to see that child matches ONE of parent.

    // console.log('check?', {
    //     p: this.meta.serializeType(parent),
    //     c: this.meta.serializeType(child)
    // });

    var childUnions = child.type === 'unionType' ? child.unions : [child];

    var err;
    var possibleErrors = [];
    var errors = [];
    for (var j = 0; j < childUnions.length; j++) {
        var isBad = true;

        for (var i = 0; i < parent.unions.length; i++) {
            err = this.checkSubType(
                node, parent.unions[i], childUnions[j]
            );
            if (!err) {
                isBad = false;
            } else {
                possibleErrors.push(err);
            }
        }

        if (isBad) {
            for (var k = 0; k < possibleErrors.length; k++) {
                errors.push(possibleErrors[k]);
            }
        }
    }

    if (!isBad) {
        return null;
    }

    var finalErr = Errors.UnionTypeClassMismatch({
        expected: serialize(parent._raw || parent),
        actual: serialize(child._raw || child),
        loc: node.loc,
        line: node.loc.start.line
    });

    finalErr.originalErrors = errors;
    return finalErr;
};

function reportTypeMisMatch(node, parent, child) {
    return Errors.TypeClassMismatch({
        expected: serialize(parent._raw || parent),
        actual: serialize(child._raw || child),
        loc: node.loc,
        line: node.loc.start.line
    });
}
<commit_msg>sub-type: support kinder object subtype checks<commit_after>'use strict';

var assert = require('assert');

var Errors = require('./errors.js');
var serialize = require('../serialize.js');
var isSameType = require('./lib/is-same-type.js');

/* TODO:

    Need to add more context for non trivial assignments

    objA = objB

    gives a Number != String type error without fieldname context

*/

module.exports = SubTypeChecker;

function SubTypeChecker(meta) {
    this.meta = meta;
}

SubTypeChecker.prototype.checkSubType =
function checkSubType(node, parent, child) {
    assert(node && node.type, 'ast node must have type');
    assert(parent && parent.type, 'parent must have a type');
    assert(child && child.type, 'child must have a type');

    var result;

    if (parent === child) {
        result = null;
    } else if (parent.type === 'typeLiteral') {
        result = this.checkTypeLiteralSubType(node, parent, child);
    } else if (parent.type === 'genericLiteral') {
        result = this.checkGenericLiteralSubType(node, parent, child);
    } else if (parent.type === 'function') {
        result = this.checkFunctionSubType(node, parent, child);
    } else if (parent.type === 'object') {
        result = this.checkObjectSubType(node, parent, child);
    } else if (parent.type === 'valueLiteral') {
        result = this.checkValueLiteralSubType(node, parent, child);
    } else if (parent.type === 'unionType') {
        result = this.checkUnionSubType(node, parent, child);
    } else if (parent.type === 'freeLiteral') {
        return reportTypeMisMatch(node, parent, child);
    } else {
        throw new Error('not implemented sub type: ' + parent.type);
    }

    assert(typeof result !== 'boolean', 'must return error or null');
    return result;
};

/*eslint complexity: [2, 30]*/
SubTypeChecker.prototype.checkTypeLiteralSubType =
function checkTypeLiteralSubType(node, parent, child) {
    if (!parent.builtin && !parent.isGeneric) {
        return reportTypeMisMatch(node, parent, child);
        // throw new Error('not implemented, sub type for non-builtin');
    }
    if (parent.isGeneric) {
        if (!child.isGeneric) {
            return reportTypeMisMatch(node, parent, child);
        }

        if (parent.name !== child.name) {
            return reportTypeMisMatch(node, parent, child);
        }

        return null;
    }

    if (parent.name === '%Any%%ModuleExports') {
        return null;
    } else if (parent.name === '%Boolean%%Mixed') {
        return null;
    } else if (parent.name === '%Void%%UnknownReturn') {
        return null;
    } else if (parent.name === 'Function' && child.type === 'function') {
        return null;
    } else if (parent.name === 'Object' && child.type === 'object') {
        return null;
    }

    if (child.type !== 'typeLiteral') {
        return reportTypeMisMatch(node, parent, child);
        // return new Error('[Internal] expected type literal');
    }

    var name = parent.name;
    if (name === 'Object') {
        // TODO: model that Error inherits from Object
        if (child.name !== name && child.name !== 'Error') {
            return reportTypeMisMatch(node, parent, child);
        }
    } else if (name === 'Array') {
        if (child.name !== name) {
            return new Error('[Internal] Not an array');
        }
    } else if (name === 'String') {
        if (child.name !== name) {
            return reportTypeMisMatch(node, parent, child);
        }
    } else if (name === 'void') {
        if (child.name !== name) {
            return new Error('[Internal] Not a void');
        }
    } else if (name === 'RegExp') {
        if (child.name !== name) {
            return reportTypeMisMatch(node, parent, child);
        }
    } else if (name === 'Number') {
        if (child.name !== name) {
            return reportTypeMisMatch(node, parent, child);
        }
    } else if (name === 'Boolean') {
        if (child.name !== name) {
            return reportTypeMisMatch(node, parent, child);
        }
    } else if (name === 'Error') {
        if (child.name !== name) {
            return reportTypeMisMatch(node, parent, child);
        }
    } else {
        throw new Error('NotImplemented: ' + parent.name);
    }
};

SubTypeChecker.prototype.checkValueLiteralSubType =
function checkValueLiteralSubType(node, parent, child) {
    if (parent.name === 'null' && child.type === 'typeLiteral' &&
        child.builtin && child.name === '%Null%%Default'
    ) {
        return null;
    }

    if (child.type !== 'valueLiteral') {
        return reportTypeMisMatch(node, parent, child);
    }

    var name = parent.name;
    if (name === 'null') {
        if (child.name !== name) {
            return new Error('[Internal] Not null');
        }
    } else if (name === 'undefined') {
        if (child.name !== name) {
            return new Error('[Internal] Not undefined');
        }
    } else {
        // console.log('valueLiteral?', {
        //     p: parent,
        //     c: child
        // });
        throw new Error('NotImplemented: ' + parent.type);
    }
};

SubTypeChecker.prototype.checkGenericLiteralSubType =
function checkGenericLiteralSubType(node, parent, child) {
    if (parent.value.name === 'Array' &&
        parent.value.builtin &&
        child.type === 'typeLiteral' &&
        child.name === '%Array%%Empty'
    ) {
        return null;
    }

    if (parent.value.name === 'Object' &&
        parent.value.builtin &&
        child.type === 'typeLiteral' &&
        child.name === '%Object%%Empty'
    ) {
        return null;
    }

    if (child.type !== 'genericLiteral') {
        return reportTypeMisMatch(node, parent, child);
    }

    var isSame = isSameType(parent.value, child.value);
    if (!isSame) {
        return reportTypeMisMatch(node, parent, child);
    }

    if (parent.generics.length !== child.generics.length) {
        throw new Error('generics mismatch');
    }

    for (var i = 0; i < parent.generics.length; i++) {
        isSame = isSameType(parent.generics[i], child.generics[i]);

        if (!isSame) {
            return reportTypeMisMatch(node, parent, child);
        }
    }

    return null;
};

SubTypeChecker.prototype.checkFunctionSubType =
function checkFunctionSubType(node, parent, child) {
    if (child.type !== 'function') {
        return reportTypeMisMatch(node, parent, child);
    }

    var err = this.checkSubType(node, parent.result, child.result);
    if (err) {
        return err;
    }

    if (parent.thisArg) {
        err = this.checkSubType(node, parent.thisArg, child.thisArg);
        if (err) {
            return err;
        }
    }

    if (parent.args.length !== child.args.length) {
        throw new Error('[Internal] function args mismatch');
    }

    for (var i = 0; i < parent.args.length; i++) {
        err = this.checkSubType(node, parent.args[i], child.args[i]);
        if (err) {
            return err;
        }
    }

    return null;
};

SubTypeChecker.prototype.checkObjectSubType =
function checkObjectSubType(node, parent, child) {
    if (child.type !== 'object') {
        return reportTypeMisMatch(node, parent, child);
    }

    // If child has more keys then that's ok.
    if (parent.keyValues.length > child.keyValues.length) {
        return Errors.IncorrectFieldCount({
            expected: serialize(parent._raw || parent),
            expectedFields: parent.keyValues.length,
            actual: serialize(child._raw || child),
            actualFields: child.keyValues.length,
            loc: node.loc,
            line: node.loc.start.line
        });
        // throw new Error('[Internal] object key pairs mismatch');
    }

    var err;
    var childIndex = buildIndex(child.keyValues);
    for (var i = 0; i < parent.keyValues.length; i++) {
        // TODO: check key names...
        var pair = parent.keyValues[i];

        var childType = childIndex[pair.key];

        err = this.checkSubType(node, pair.value, childType);
        if (err) {
            return err;
        }
    }

    return null;
};

function buildIndex(list) {
    var index = {};
    for (var i = 0; i < list.length; i++) {
        index[list[i].key] = list[i].value;
    }

    return index;
}

SubTypeChecker.prototype.checkUnionSubType =
function checkUnionSubType(node, parent, child) {
    // Check to see that child matches ONE of parent.

    // console.log('check?', {
    //     p: this.meta.serializeType(parent),
    //     c: this.meta.serializeType(child)
    // });

    var childUnions = child.type === 'unionType' ? child.unions : [child];

    var err;
    var possibleErrors = [];
    var errors = [];
    for (var j = 0; j < childUnions.length; j++) {
        var isBad = true;

        for (var i = 0; i < parent.unions.length; i++) {
            err = this.checkSubType(
                node, parent.unions[i], childUnions[j]
            );
            if (!err) {
                isBad = false;
            } else {
                possibleErrors.push(err);
            }
        }

        if (isBad) {
            for (var k = 0; k < possibleErrors.length; k++) {
                errors.push(possibleErrors[k]);
            }
        }
    }

    if (!isBad) {
        return null;
    }

    var finalErr = Errors.UnionTypeClassMismatch({
        expected: serialize(parent._raw || parent),
        actual: serialize(child._raw || child),
        loc: node.loc,
        line: node.loc.start.line
    });

    finalErr.originalErrors = errors;
    return finalErr;
};

function reportTypeMisMatch(node, parent, child) {
    return Errors.TypeClassMismatch({
        expected: serialize(parent._raw || parent),
        actual: serialize(child._raw || child),
        loc: node.loc,
        line: node.loc.start.line
    });
}
<|endoftext|>