<commit_before>goog.provide('ol.control.Rotate');

goog.require('ol.events');
goog.require('ol.events.EventType');
goog.require('ol');
goog.require('ol.animation');
goog.require('ol.control.Control');
goog.require('ol.css');
goog.require('ol.easing');

/**
 * @classdesc
 * A button control to reset rotation to 0.
 * To style this control use css selector `.ol-rotate`. A `.ol-hidden` css
 * selector is added to the button when the rotation is 0.
 *
 * @constructor
 * @extends {ol.control.Control}
 * @param {olx.control.RotateOptions=} opt_options Rotate options.
 * @api stable
 */
ol.control.Rotate = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var className = options.className !== undefined ? options.className : 'ol-rotate';

  var label = options.label !== undefined ? options.label : '\u21E7';

  /**
   * @type {Element}
   * @private
   */
  this.label_ = null;

  if (typeof label === 'string') {
    this.label_ = document.createElement('span');
    this.label_.className = 'ol-compass';
    this.label_.textContent = label;
  } else {
    this.label_ = label;
    this.label_.classList.add('ol-compass');
  }

  var tipLabel = options.tipLabel ? options.tipLabel : 'Reset rotation';

  var button = document.createElement('button');
  button.className = className + '-reset';
  button.setAttribute('type', 'button');
  button.title = tipLabel;
  button.appendChild(this.label_);

  ol.events.listen(button, ol.events.EventType.CLICK,
      ol.control.Rotate.prototype.handleClick_, this);

  var cssClasses = className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' +
      ol.css.CLASS_CONTROL;
  var element = document.createElement('div');
  element.className = cssClasses;
  element.appendChild(button);

  var render = options.render ? options.render : ol.control.Rotate.render;

  this.callResetNorth_ = options.resetNorth ? options.resetNorth : undefined;

  ol.control.Control.call(this, {
    element: element,
    render: render,
    target: options.target
  });

  /**
   * @type {number}
   * @private
   */
  this.duration_ = options.duration !== undefined ? options.duration : 250;

  /**
   * @type {boolean}
   * @private
   */
  this.autoHide_ = options.autoHide !== undefined ? options.autoHide : true;

  /**
   * @private
   * @type {number|undefined}
   */
  this.rotation_ = undefined;

  if (this.autoHide_) {
    this.element.classList.add(ol.css.CLASS_HIDDEN);
  }

};
ol.inherits(ol.control.Rotate, ol.control.Control);

/**
 * @param {Event} event The event to handle
 * @private
 */
ol.control.Rotate.prototype.handleClick_ = function(event) {
  event.preventDefault();
  if (this.callResetNorth_ !== undefined) {
    this.callResetNorth_();
  } else {
    this.resetNorth_();
  }
};

/**
 * @private
 */
ol.control.Rotate.prototype.resetNorth_ = function() {
  var map = this.getMap();
  var view = map.getView();
  if (!view) {
    // the map does not have a view, so we can't act
    // upon it
    return;
  }
  var currentRotation = view.getRotation();
  if (currentRotation !== undefined) {
    if (this.duration_ > 0) {
      currentRotation = currentRotation % (2 * Math.PI);
      if (currentRotation < -Math.PI) {
        currentRotation += 2 * Math.PI;
      }
      if (currentRotation > Math.PI) {
        currentRotation -= 2 * Math.PI;
      }
      map.beforeRender(ol.animation.rotate({
        rotation: currentRotation,
        duration: this.duration_,
        easing: ol.easing.easeOut
      }));
    }
    view.setRotation(0);
  }
};

/**
 * Update the rotate control element.
 * @param {ol.MapEvent} mapEvent Map event.
 * @this {ol.control.Rotate}
 * @api
 */
ol.control.Rotate.render = function(mapEvent) {
  var frameState = mapEvent.frameState;
  if (!frameState) {
    return;
  }
  var rotation = frameState.viewState.rotation;
  if (rotation != this.rotation_) {
    var transform = 'rotate(' + rotation + 'rad)';
    if (this.autoHide_) {
      var contains = this.element.classList.contains(ol.css.CLASS_HIDDEN);
      if (!contains && rotation === 0) {
        this.element.classList.add(ol.css.CLASS_HIDDEN);
      } else if (contains && rotation !== 0) {
        this.element.classList.remove(ol.css.CLASS_HIDDEN);
      }
    }
    this.label_.style.msTransform = transform;
    this.label_.style.webkitTransform = transform;
    this.label_.style.transform = transform;
  }
  this.rotation_ = rotation;
};
<commit_msg>Use view.animate() in the rotation control<commit_after>goog.provide('ol.control.Rotate');

goog.require('ol.events');
goog.require('ol.events.EventType');
goog.require('ol');
goog.require('ol.control.Control');
goog.require('ol.css');
goog.require('ol.easing');

/**
 * @classdesc
 * A button control to reset rotation to 0.
 * To style this control use css selector `.ol-rotate`. A `.ol-hidden` css
 * selector is added to the button when the rotation is 0.
 *
 * @constructor
 * @extends {ol.control.Control}
 * @param {olx.control.RotateOptions=} opt_options Rotate options.
 * @api stable
 */
ol.control.Rotate = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var className = options.className !== undefined ? options.className : 'ol-rotate';

  var label = options.label !== undefined ? options.label : '\u21E7';

  /**
   * @type {Element}
   * @private
   */
  this.label_ = null;

  if (typeof label === 'string') {
    this.label_ = document.createElement('span');
    this.label_.className = 'ol-compass';
    this.label_.textContent = label;
  } else {
    this.label_ = label;
    this.label_.classList.add('ol-compass');
  }

  var tipLabel = options.tipLabel ? options.tipLabel : 'Reset rotation';

  var button = document.createElement('button');
  button.className = className + '-reset';
  button.setAttribute('type', 'button');
  button.title = tipLabel;
  button.appendChild(this.label_);

  ol.events.listen(button, ol.events.EventType.CLICK,
      ol.control.Rotate.prototype.handleClick_, this);

  var cssClasses = className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' +
      ol.css.CLASS_CONTROL;
  var element = document.createElement('div');
  element.className = cssClasses;
  element.appendChild(button);

  var render = options.render ? options.render : ol.control.Rotate.render;

  this.callResetNorth_ = options.resetNorth ? options.resetNorth : undefined;

  ol.control.Control.call(this, {
    element: element,
    render: render,
    target: options.target
  });

  /**
   * @type {number}
   * @private
   */
  this.duration_ = options.duration !== undefined ? options.duration : 250;

  /**
   * @type {boolean}
   * @private
   */
  this.autoHide_ = options.autoHide !== undefined ? options.autoHide : true;

  /**
   * @private
   * @type {number|undefined}
   */
  this.rotation_ = undefined;

  if (this.autoHide_) {
    this.element.classList.add(ol.css.CLASS_HIDDEN);
  }

};
ol.inherits(ol.control.Rotate, ol.control.Control);

/**
 * @param {Event} event The event to handle
 * @private
 */
ol.control.Rotate.prototype.handleClick_ = function(event) {
  event.preventDefault();
  if (this.callResetNorth_ !== undefined) {
    this.callResetNorth_();
  } else {
    this.resetNorth_();
  }
};

/**
 * @private
 */
ol.control.Rotate.prototype.resetNorth_ = function() {
  var map = this.getMap();
  var view = map.getView();
  if (!view) {
    // the map does not have a view, so we can't act
    // upon it
    return;
  }
  var currentRotation = view.getRotation();
  if (currentRotation !== undefined) {
    if (this.duration_ > 0) {
      currentRotation = currentRotation % (2 * Math.PI);
      if (currentRotation < -Math.PI) {
        currentRotation += 2 * Math.PI;
      }
      if (currentRotation > Math.PI) {
        currentRotation -= 2 * Math.PI;
      }
      view.animate({
        rotation: 0,
        duration: this.duration_,
        easing: ol.easing.easeOut
      });
    } else {
      view.setRotation(0);
    }
  }
};

/**
 * Update the rotate control element.
 * @param {ol.MapEvent} mapEvent Map event.
 * @this {ol.control.Rotate}
 * @api
 */
ol.control.Rotate.render = function(mapEvent) {
  var frameState = mapEvent.frameState;
  if (!frameState) {
    return;
  }
  var rotation = frameState.viewState.rotation;
  if (rotation != this.rotation_) {
    var transform = 'rotate(' + rotation + 'rad)';
    if (this.autoHide_) {
      var contains = this.element.classList.contains(ol.css.CLASS_HIDDEN);
      if (!contains && rotation === 0) {
        this.element.classList.add(ol.css.CLASS_HIDDEN);
      } else if (contains && rotation !== 0) {
        this.element.classList.remove(ol.css.CLASS_HIDDEN);
      }
    }
    this.label_.style.msTransform = transform;
    this.label_.style.webkitTransform = transform;
    this.label_.style.transform = transform;
  }
  this.rotation_ = rotation;
};
<|endoftext|>


<commit_before>import { Component } from "react";
import PropTypes from "prop-types";

function scrollInViewport(treeItem) {
  const treeItemBounding = treeItem.getBoundingClientRect();

  if (
    treeItemBounding.top < 0 ||
    treeItemBounding.bottom >
      (window.innerHeight || document.documentElement.clientHeight)
  ) {
    // align to the top if the top of Option is out of the viewport
    if (treeItemBounding.top < 0) {
      treeItem.scrollIntoView(true);
    }
    // align to the bottom if the bottom of Option is out of the viewport
    if (
      treeItemBounding.bottom >
      (window.innerHeight || document.documentElement.clientHeight)
    ) {
      treeItem.scrollIntoView(true);
    }
  }
}

function scrollInListbox(treeItem, treeView) {
  if (treeItem && treeView.scrollHeight > treeView.clientHeight) {
    const scrollBottom = treeView.clientHeight + treeView.scrollTop;
    const elementBottom = treeItem.offsetTop + treeItem.offsetHeight;
    if (elementBottom > scrollBottom) {
      // eslint-disable-next-line no-param-reassign
      treeView.scrollTop = elementBottom - treeView.clientHeight;
    }
    if (treeItem.offsetTop < treeView.scrollTop) {
      // eslint-disable-next-line no-param-reassign
      treeView.scrollTop = treeItem.offsetTop;
    }
  }
}

function checkScroll(treeItemId, treeView) {
  const treeItem = document.getElementById(treeItemId);
  // scroll if out of viewport
  scrollInViewport(treeItem);

  // scroll within the menu
  scrollInListbox(treeItem, treeView);
}

function buildTreeItemIdArray(list) {
  return list.map(item => item.id);
}

export default class TreeViewBehavior extends Component {
  static propTypes = {
    children: PropTypes.func,
    onKeyDown: PropTypes.func,
    treeNode: PropTypes.arrayOf(
      PropTypes.shape({
        id: PropTypes.number,
        parentId: PropTypes.number,
        meta: PropTypes.shape({
          label: PropTypes.string,
          collapsed: PropTypes.bool,
          active: PropTypes.bool,
          icon: PropTypes.node
        })
      })
    ),
    treeViewRef: PropTypes.func
  };

  static defaultProps = {};

  constructor(props) {
    super(props);

    this.state = {
      treeItemArray: null,
      activeTreeItemIndex: null,
      currentItemClicked: null,
      keyboardOpenId: ""
    };

    this.treeViewRef = null;
  }

  setTreeViewRef = element => {
    if (this.props.treeViewRef) {
      this.props.treeViewRef(element);
    }

    this.treeViewRef = element;
  };

  setTreeItemArray = objectArray => {
    this.setState({ treeItemArray: [...objectArray] });
  };

  getTreeItemArray = () => this.state.treeItemArray;

  getActiveTreeItemId = () =>
    this.state.treeItemArray &&
    this.state.treeItemArray[this.getActiveTreeItemIndex()];

  setActiveTreeItemId = currentItemClicked => {
    this.setState({ currentItemClicked });
  };

  getCurrentItemClicked = () => this.state.currentItemClicked;

  getActiveTreeItemIndex = () => this.state.activeTreeItemIndex;

  setActiveTreeItemIndex = index => {
    this.setState({ activeTreeItemIndex: index });
  };

  getKeyboardOpenId = () => this.state.keyboardOpenId;

  setKeyboardOpenId = id => {
    this.setState({ keyboardOpenId: id });
  };

  handleKeyDown = event => {
    if (this.props.onKeyDown) {
      this.props.onKeyDown(event);
    }

    const domNodeList = this.treeViewRef.querySelectorAll("li");
    const treeItemArrayControl =
      this.getTreeItemArray().length !== domNodeList.length ||
      this.props.treeNode
        ? buildTreeItemIdArray(Array.prototype.slice.call(domNodeList))
        : this.getTreeItemArray();

    const lowerLimit = 0;
    const upperLimit = treeItemArrayControl.length - 1;

    switch (event.keyCode) {
      // Arrow Down
      case 40: {
        event.preventDefault();
        if (this.getActiveTreeItemIndex() === upperLimit) {
          this.setActiveTreeItemIndex(lowerLimit);

          checkScroll(treeItemArrayControl[lowerLimit], this.treeViewRef);
        } else {
          this.setActiveTreeItemIndex(this.getActiveTreeItemIndex() + 1);

          checkScroll(
            treeItemArrayControl[this.getActiveTreeItemIndex() + 1],
            this.treeViewRef
          );
        }
        break;
      }

      // Arrow Up
      case 38: {
        event.preventDefault();
        if (this.getActiveTreeItemIndex() <= lowerLimit) {
          this.setActiveTreeItemIndex(upperLimit);

          checkScroll(treeItemArrayControl[upperLimit], this.treeViewRef);
        } else {
          this.setActiveTreeItemIndex(this.getActiveTreeItemIndex() - 1);

          checkScroll(
            treeItemArrayControl[this.getActiveTreeItemIndex() - 1],
            this.treeViewRef
          );
        }
        break;
      }

      // Enter
      case 13: {
        event.preventDefault();
        this.setKeyboardOpenId(this.getActiveTreeItemId());
        break;
      }
      // Space
      case 32: {
        event.preventDefault();
        this.setActiveTreeItemId(this.getActiveTreeItemId());
        break;
      }

      default:
    }
  };

  render() {
    const {
      getActiveTreeItemId,
      getActiveTreeItemIndex,
      getCurrentItemClicked,
      getKeyboardOpenId,
      getTreeItemArray,
      handleClick,
      handleKeyDown,
      setActiveTreeItemId,
      setActiveTreeItemIndex,
      setKeyboardOpenId,
      setTreeItemArray,
      setTreeViewRef,
      treeViewRef
    } = this;

    return this.props.children({
      getActiveTreeItemId,
      getActiveTreeItemIndex,
      getCurrentItemClicked,
      getKeyboardOpenId,
      getTreeItemArray,
      handleClick,
      handleKeyDown,
      setActiveTreeItemId,
      setActiveTreeItemIndex,
      setKeyboardOpenId,
      setTreeItemArray,
      setTreeViewRef,
      treeViewRef
    });
  }
}
<commit_msg>feat: pass internal method to table events<commit_after>import { Component } from "react";
import PropTypes from "prop-types";

function scrollInViewport(treeItem) {
  const treeItemBounding = treeItem.getBoundingClientRect();

  if (
    treeItemBounding.top < 0 ||
    treeItemBounding.bottom >
      (window.innerHeight || document.documentElement.clientHeight)
  ) {
    // align to the top if the top of Option is out of the viewport
    if (treeItemBounding.top < 0) {
      treeItem.scrollIntoView(true);
    }
    // align to the bottom if the bottom of Option is out of the viewport
    if (
      treeItemBounding.bottom >
      (window.innerHeight || document.documentElement.clientHeight)
    ) {
      treeItem.scrollIntoView(true);
    }
  }
}

function scrollInListbox(treeItem, treeView) {
  if (treeItem && treeView.scrollHeight > treeView.clientHeight) {
    const scrollBottom = treeView.clientHeight + treeView.scrollTop;
    const elementBottom = treeItem.offsetTop + treeItem.offsetHeight;
    if (elementBottom > scrollBottom) {
      // eslint-disable-next-line no-param-reassign
      treeView.scrollTop = elementBottom - treeView.clientHeight;
    }
    if (treeItem.offsetTop < treeView.scrollTop) {
      // eslint-disable-next-line no-param-reassign
      treeView.scrollTop = treeItem.offsetTop;
    }
  }
}

function checkScroll(treeItemId, treeView) {
  const treeItem = document.getElementById(treeItemId);
  // scroll if out of viewport
  scrollInViewport(treeItem);

  // scroll within the menu
  scrollInListbox(treeItem, treeView);
}

function buildTreeItemIdArray(list) {
  return list.map(item => item.id);
}

export default class TreeViewBehavior extends Component {
  static propTypes = {
    children: PropTypes.func,
    onKeyDown: PropTypes.func,
    treeNode: PropTypes.arrayOf(
      PropTypes.shape({
        id: PropTypes.number,
        parentId: PropTypes.number,
        meta: PropTypes.shape({
          label: PropTypes.string,
          collapsed: PropTypes.bool,
          active: PropTypes.bool,
          icon: PropTypes.node
        })
      })
    ),
    treeViewRef: PropTypes.func
  };

  static defaultProps = {};

  constructor(props) {
    super(props);

    this.state = {
      treeItemArray: null,
      activeTreeItemIndex: null,
      currentItemClicked: null,
      keyboardOpenId: ""
    };

    this.treeViewRef = null;
  }

  setTreeViewRef = element => {
    if (this.props.treeViewRef) {
      this.props.treeViewRef(element);
    }

    this.treeViewRef = element;
  };

  setTreeItemArray = objectArray => {
    this.setState({ treeItemArray: [...objectArray] });
  };

  getTreeItemArray = () => this.state.treeItemArray;

  getActiveTreeItemId = () =>
    this.state.treeItemArray &&
    this.state.treeItemArray[this.getActiveTreeItemIndex()];

  setActiveTreeItemId = currentItemClicked => {
    this.setState({ currentItemClicked });
  };

  getCurrentItemClicked = () => this.state.currentItemClicked;

  getActiveTreeItemIndex = () => this.state.activeTreeItemIndex;

  setActiveTreeItemIndex = index => {
    this.setState({ activeTreeItemIndex: index });
  };

  getKeyboardOpenId = () => this.state.keyboardOpenId;

  setKeyboardOpenId = id => {
    this.setState({ keyboardOpenId: id });
  };

  handleKeyDown = event => {
    const {
      getActiveTreeItemId,
      getActiveTreeItemIndex,
      getTreeItemArray,
      setActiveTreeItemId,
      setActiveTreeItemIndex,
      setKeyboardOpenId,
      props,
      treeViewRef
    } = this;
    if (this.props.onKeyDown) {
      this.props.onKeyDown(
        event,
        {
          getActiveTreeItemId,
          getActiveTreeItemIndex,
          getTreeItemArray,
          setActiveTreeItemId,
          setActiveTreeItemIndex,
          setKeyboardOpenId
        }
      );
    }

    const domNodeList = treeViewRef.querySelectorAll("li");
    const treeItemArrayControl =
      getTreeItemArray().length !== domNodeList.length ||
      props.treeNode
        ? buildTreeItemIdArray(Array.prototype.slice.call(domNodeList))
        : getTreeItemArray();

    const lowerLimit = 0;
    const upperLimit = treeItemArrayControl.length - 1;

    switch (event.keyCode) {
      // Arrow Down
      case 40: {
        event.preventDefault();
        if (getActiveTreeItemIndex() === upperLimit) {
          setActiveTreeItemIndex(lowerLimit);

          checkScroll(treeItemArrayControl[lowerLimit], treeViewRef);
        } else {
          setActiveTreeItemIndex(getActiveTreeItemIndex() + 1);

          checkScroll(
            treeItemArrayControl[getActiveTreeItemIndex() + 1],
            treeViewRef
          );
        }
        break;
      }

      // Arrow Up
      case 38: {
        event.preventDefault();
        if (getActiveTreeItemIndex() <= lowerLimit) {
          setActiveTreeItemIndex(upperLimit);

          checkScroll(treeItemArrayControl[upperLimit], treeViewRef);
        } else {
          setActiveTreeItemIndex(getActiveTreeItemIndex() - 1);

          checkScroll(
            treeItemArrayControl[getActiveTreeItemIndex() - 1],
            treeViewRef
          );
        }
        break;
      }

      // Enter
      case 13: {
        event.preventDefault();
        setKeyboardOpenId(getActiveTreeItemId());
        break;
      }
      // Space
      case 32: {
        event.preventDefault();
        setActiveTreeItemId(getActiveTreeItemId());
        break;
      }

      default:
    }
  };

  render() {
    const {
      getActiveTreeItemId,
      getActiveTreeItemIndex,
      getCurrentItemClicked,
      getKeyboardOpenId,
      getTreeItemArray,
      handleClick,
      handleKeyDown,
      setActiveTreeItemId,
      setActiveTreeItemIndex,
      setKeyboardOpenId,
      setTreeItemArray,
      setTreeViewRef,
      treeViewRef
    } = this;

    return this.props.children({
      getActiveTreeItemId,
      getActiveTreeItemIndex,
      getCurrentItemClicked,
      getKeyboardOpenId,
      getTreeItemArray,
      handleClick,
      handleKeyDown,
      setActiveTreeItemId,
      setActiveTreeItemIndex,
      setKeyboardOpenId,
      setTreeItemArray,
      setTreeViewRef,
      treeViewRef
    });
  }
}
<|endoftext|>


<commit_before>import React from "react";
import Template from "../../core/Template";
import Wizard from "../base/Wizard";
import DataError from "../base/DataError";

/**
 * @author Niklas Keller
 */
class ExperimentWizard extends Wizard {
    constructor() {
        super();

        this.state = Object.assign(this.state || {}, {
            algorithms: {
                taskChooserAlgorithms: [],
                ratingQualityAlgorithms: [],
                answerQualityAlgorithms: []
            }
        });
    }

    componentDidMount() {
        super.componentDidMount();
        this.fetchAlgorithms();
    }

    getForm() {
        if (!this.state) {
            return {};
        }

        let base = {
            title: {
                type: "text",
                label: "Title",
                help: "Task name will be published on crowdworking platforms."
            }
        };

        let nonTemplate = {
            description: {
                type: "longtext",
                label: "Description",
                help: "Description of the creative task."
            },
            answerType: {
                type: "enum",
                label: "Answer Type",
                help: "Collect text or images from workers?",
                default: "TEXT",
                values: [
                    {
                        value: "TEXT",
                        text: "Text"
                    },
                    {
                        value: "IMAGE",
                        text: "Images"
                    }
                ]
            },
            placeholders: {
                type: "hidden",
                value: "",
                encoder: () => {
                    return {};
                }
            }
        };

        if (this.state.loaded && !this.state.failed) {
            let isTemplate = this.state.new && this.props.location.state && "template" in this.props.location.state || this.state.data && "templateId" in this.state.data;

            if (isTemplate) {
                let text;

                if (this.state.new && this.props.location.state && "template" in this.props.location.state) {
                    let template = this.props.location.state.template;
                    text = template.content;

                    Object.assign(base, {
                        description: {
                            type: "hidden",
                            value: text
                        },
                        answerType: {
                            type: "hidden",
                            value: template.answerType
                        },
                        templateId: {
                            type: "hidden",
                            value: template.id,
                            encoder: parseInt
                        }
                    });
                } else {
                    text = this.state.data.description;
                }

                let placeholders = Template.parse(text);

                Object.keys(placeholders).forEach((name) => {
                    base["placeholders[" + name + "]"] = {
                        type: placeholders[name].type,
                        label: name,
                        help: placeholders[name].description
                    };
                });

                if (Object.keys(placeholders).length === 0) {
                    base["placeholders"] = {
                        type: "hidden",
                        value: "{}",
                        encoder: () => {
                            return {};
                        },
                        decoder: (i) => "{}"
                    };
                }
            } else {
                Object.assign(base, nonTemplate);
            }
        } else {
            Object.assign(base, nonTemplate);
        }

        let algorithmMapper = (chooser) => {
            return {
                value: chooser.name,
                text: chooser.description
            };
        };

        let algorithmOptions = (formName, specName) => {
            let current = this.state.form[formName] ? this.state.form[formName].name : null;
            let availableAlgorithms = this.state.algorithms[specName];

            for (let key in availableAlgorithms) {
                if (availableAlgorithms[key].name !== current) {
                    continue;
                }

                let parameters = availableAlgorithms[key].parameters || [];

                parameters.map((item, i) => {
                    base[formName + "[parameters][" + i + "][id]"] = {
                        type: "hidden",
                        value: availableAlgorithms[key].id,
                        encoder: parseInt
                    };

                    base[formName + "[parameters][" + i + "][value]"] = {
                        type: "text",
                        label: item.description,
                        help: Parameter for {availableAlgorithms[key].name}.
                    };
                });
            }
        };

        base["algorithmTaskChooser[name]"] = {
            type: "enum",
            label: "Task Chooser Algorithm",
            help: "How should tasks be chosen?",
            values: this.state.algorithms.taskChooserAlgorithms.map(algorithmMapper)
        };

        Object.assign(base, algorithmOptions("algorithmTaskChooser", "taskChooserAlgorithms"));

        base["algorithmQualityAnswer[name]"] = {
            type: "enum",
            label: "Answer Quality Algorithm",
            help: "How should the quality of an answer be determined?",
            values: this.state.algorithms.answerQualityAlgorithms.map(algorithmMapper)
        };

        Object.assign(base, algorithmOptions("algorithmQualityAnswer", "answerQualityAlgorithms"));

        base["algorithmQualityRating[name]"] = {
            type: "enum",
            label: "Rating Quality Algorithm",
            help: "How should the quality of a rating be determined?",
            values: this.state.algorithms.ratingQualityAlgorithms.map(algorithmMapper)
        };

        Object.assign(base, algorithmOptions("algorithmQualityRating", "ratingQualityAlgorithms"));

        Object.assign(base, {
            state: {
                type: "hidden",
                value: "DRAFT"
            },
            neededAnswers: {
                type: "number",
                label: "Needed Answers",
                help: "How many creative answers should be collected?",
                default: 10
            },
            answersPerWorker: {
                type: "number",
                label: "Answers / Worker",
                help: "How many answers should each worker be able to give?",
                default: 3
            },
            ratingsPerWorker: {
                type: "number",
                label: "Ratings / Worker",
                help: "How many ratings should each worker be able to give?",
                default: 3
            },
            ratingsPerAnswer: {
                type: "number",
                label: "Ratings / Answer",
                help: "How many ratings should each answer receive?",
                default: 3
            },
            paymentBase: {
                type: "number",
                label: "Base Payment",
                help: "Basic payment a worker gets for completing a task.",
                unit: "cents"
            },
            paymentAnswer: {
                type: "number",
                label: "Answer Payment",
                help: "Payment for each creative answer.",
                unit: "cents",
                default: 10
            },
            paymentRating: {
                type: "number",
                label: "Rating Payment",
                help: "Payment for each rating.",
                unit: "cents",
                default: 10
            },
            ratingOptions: { // TODO: Add editor for rating options, just preserve them for now…
                type: "hidden",
                value: "",
                encoder: (json) => {
                    console.log(json);
                    try {
                        return JSON.parse(json);
                    } catch (e) {
                        console.error(e);
                    }
                },
                decoder: (items) => {
                    if (items.length) {
                        return JSON.stringify(items);
                    } else {
                        return JSON.stringify([
                            {name: "good", value: 10},
                            {name: "bad", value: 0}
                        ]);
                    }
                }
            },
            constraints: {
                type: "longtext",
                label: "Constraints",
                help: "Constraints that should be matched by all creative answers.",
                encoder: (text) => text.split("\n").map((item) => item.trim()).filter((item) => item !== "").map((item) => {
                    return {name: item}
                }),
                decoder: (items) => items.map((item) => item.name).join("\n")
            },
            tags: {
                type: "text",
                label: "Tags",
                help: "Tags! TODO…", // TODO
                encoder: (text) => {
                    text.split(",").map((item) => item.trim()).filter((item) => item !== "").map((item) => {
                        return {
                            name: item
                        };
                    });
                },
                decoder: (items) => items.map((item) => item.name).join(", ")
            },
            workerQualityThreshold: {
                type: "number",
                label: "Worker Quality Threshold",
                help: "Minimum worker quality to participate in this experiment. Values from 0 to 9."
            }
        });

        return base;
    }

    render() {
        if (this.state.algorithmsFailed) {
            return (
                <DataError />
            );
        }

        return super.render();
    }

    getCollectionUri(query) {
        if (typeof query !== "undefined") {
            return "experiments?" + query;
        }

        return "experiments";
    }

    fetchAlgorithms() {
        this.props.backend.request("GET", "algorithms").then((response) => {
            this.setState({
                algorithms: {
                    taskChooserAlgorithms: response.data.taskChooserAlgorithms || [],
                    ratingQualityAlgorithms: response.data.ratingQualityAlgorithms || [],
                    answerQualityAlgorithms: response.data.answerQualityAlgorithms || []
                }
            });
        }).catch(() => {
            this.setState({
                algorithmsFailed: true
            });
        });
    }
}

export default ExperimentWizard;<commit_msg>Add special case for no tags / no constraints<commit_after>import React from "react";
import Template from "../../core/Template";
import Wizard from "../base/Wizard";
import DataError from "../base/DataError";

/**
 * @author Niklas Keller
 */
class ExperimentWizard extends Wizard {
    constructor() {
        super();

        this.state = Object.assign(this.state || {}, {
            algorithms: {
                taskChooserAlgorithms: [],
                ratingQualityAlgorithms: [],
                answerQualityAlgorithms: []
            }
        });
    }

    componentDidMount() {
        super.componentDidMount();
        this.fetchAlgorithms();
    }

    getForm() {
        if (!this.state) {
            return {};
        }

        let base = {
            title: {
                type: "text",
                label: "Title",
                help: "Task name will be published on crowdworking platforms."
            }
        };

        let nonTemplate = {
            description: {
                type: "longtext",
                label: "Description",
                help: "Description of the creative task."
            },
            answerType: {
                type: "enum",
                label: "Answer Type",
                help: "Collect text or images from workers?",
                default: "TEXT",
                values: [
                    {
                        value: "TEXT",
                        text: "Text"
                    },
                    {
                        value: "IMAGE",
                        text: "Images"
                    }
                ]
            },
            placeholders: {
                type: "hidden",
                value: "",
                encoder: () => {
                    return {};
                }
            }
        };

        if (this.state.loaded && !this.state.failed) {
            let isTemplate = this.state.new && this.props.location.state && "template" in this.props.location.state || this.state.data && "templateId" in this.state.data;

            if (isTemplate) {
                let text;

                if (this.state.new && this.props.location.state && "template" in this.props.location.state) {
                    let template = this.props.location.state.template;
                    text = template.content;

                    Object.assign(base, {
                        description: {
                            type: "hidden",
                            value: text
                        },
                        answerType: {
                            type: "hidden",
                            value: template.answerType
                        },
                        templateId: {
                            type: "hidden",
                            value: template.id,
                            encoder: parseInt
                        }
                    });
                } else {
                    text = this.state.data.description;
                }

                let placeholders = Template.parse(text);

                Object.keys(placeholders).forEach((name) => {
                    base["placeholders[" + name + "]"] = {
                        type: placeholders[name].type,
                        label: name,
                        help: placeholders[name].description
                    };
                });

                if (Object.keys(placeholders).length === 0) {
                    base["placeholders"] = {
                        type: "hidden",
                        value: "{}",
                        encoder: () => {
                            return {};
                        },
                        decoder: (i) => "{}"
                    };
                }
            } else {
                Object.assign(base, nonTemplate);
            }
        } else {
            Object.assign(base, nonTemplate);
        }

        let algorithmMapper = (chooser) => {
            return {
                value: chooser.name,
                text: chooser.description
            };
        };

        let algorithmOptions = (formName, specName) => {
            let current = this.state.form[formName] ? this.state.form[formName].name : null;
            let availableAlgorithms = this.state.algorithms[specName];

            for (let key in availableAlgorithms) {
                if (availableAlgorithms[key].name !== current) {
                    continue;
                }

                let parameters = availableAlgorithms[key].parameters || [];

                parameters.map((item, i) => {
                    base[formName + "[parameters][" + i + "][id]"] = {
                        type: "hidden",
                        value: availableAlgorithms[key].id,
                        encoder: parseInt
                    };

                    base[formName + "[parameters][" + i + "][value]"] = {
                        type: "text",
                        label: item.description,
                        help: Parameter for {availableAlgorithms[key].name}.
                    };
                });
            }
        };

        base["algorithmTaskChooser[name]"] = {
            type: "enum",
            label: "Task Chooser Algorithm",
            help: "How should tasks be chosen?",
            values: this.state.algorithms.taskChooserAlgorithms.map(algorithmMapper)
        };

        Object.assign(base, algorithmOptions("algorithmTaskChooser", "taskChooserAlgorithms"));

        base["algorithmQualityAnswer[name]"] = {
            type: "enum",
            label: "Answer Quality Algorithm",
            help: "How should the quality of an answer be determined?",
            values: this.state.algorithms.answerQualityAlgorithms.map(algorithmMapper)
        };

        Object.assign(base, algorithmOptions("algorithmQualityAnswer", "answerQualityAlgorithms"));

        base["algorithmQualityRating[name]"] = {
            type: "enum",
            label: "Rating Quality Algorithm",
            help: "How should the quality of a rating be determined?",
            values: this.state.algorithms.ratingQualityAlgorithms.map(algorithmMapper)
        };

        Object.assign(base, algorithmOptions("algorithmQualityRating", "ratingQualityAlgorithms"));

        Object.assign(base, {
            state: {
                type: "hidden",
                value: "DRAFT"
            },
            neededAnswers: {
                type: "number",
                label: "Needed Answers",
                help: "How many creative answers should be collected?",
                default: 10
            },
            answersPerWorker: {
                type: "number",
                label: "Answers / Worker",
                help: "How many answers should each worker be able to give?",
                default: 3
            },
            ratingsPerWorker: {
                type: "number",
                label: "Ratings / Worker",
                help: "How many ratings should each worker be able to give?",
                default: 3
            },
            ratingsPerAnswer: {
                type: "number",
                label: "Ratings / Answer",
                help: "How many ratings should each answer receive?",
                default: 3
            },
            paymentBase: {
                type: "number",
                label: "Base Payment",
                help: "Basic payment a worker gets for completing a task.",
                unit: "cents"
            },
            paymentAnswer: {
                type: "number",
                label: "Answer Payment",
                help: "Payment for each creative answer.",
                unit: "cents",
                default: 10
            },
            paymentRating: {
                type: "number",
                label: "Rating Payment",
                help: "Payment for each rating.",
                unit: "cents",
                default: 10
            },
            ratingOptions: { // TODO: Add editor for rating options, just preserve them for now…
                type: "hidden",
                value: "",
                encoder: (json) => {
                    console.log(json);
                    try {
                        return JSON.parse(json);
                    } catch (e) {
                        console.error(e);
                    }
                },
                decoder: (items) => {
                    if (items.length) {
                        return JSON.stringify(items);
                    } else {
                        return JSON.stringify([
                            {name: "good", value: 10},
                            {name: "bad", value: 0}
                        ]);
                    }
                }
            },
            constraints: {
                type: "longtext",
                label: "Constraints",
                help: "Constraints that should be matched by all creative answers.",
                encoder: (text) => {
                    let items = text.split("\n");

                    // Remove whitespace and empty elements
                    items = items.map((item) => item.trim()).filter((item) => item !== "");

                    if (items.length === 0) {
                        // Special value to signal no elements, because proto can't differentiate
                        // between default value and empty collection…
                        return [
                            {
                                name: ""
                            }
                        ];
                    }

                    return items.map((item) => {
                        return {
                            name: item
                        };
                    })
                },
                decoder: (items) => items.map((item) => item.name).join("\n")
            },
            tags: {
                type: "text",
                label: "Tags",
                help: "Tags! TODO…", // TODO
                encoder: (text) => {
                    let items = text.split(",");

                    // Remove whitespace and empty elements
                    items = items.map((item) => item.trim()).filter((item) => item !== "");

                    if (items.length === 0) {
                        // Special value to signal no elements, because proto can't differentiate
                        // between default value and empty collection…
                        return [
                            {
                                name: ""
                            }
                        ];
                    }

                    return items.map((item) => {
                        return {
                            name: item
                        };
                    });
                },
                decoder: (items) => items.map((item) => item.name).join(", ")
            },
            workerQualityThreshold: {
                type: "number",
                label: "Worker Quality Threshold",
                help: "Minimum worker quality to participate in this experiment. Values from 0 to 9."
            }
        });

        return base;
    }

    render() {
        if (this.state.algorithmsFailed) {
            return (
                <DataError />
            );
        }

        return super.render();
    }

    getCollectionUri(query) {
        if (typeof query !== "undefined") {
            return "experiments?" + query;
        }

        return "experiments";
    }

    fetchAlgorithms() {
        this.props.backend.request("GET", "algorithms").then((response) => {
            this.setState({
                algorithms: {
                    taskChooserAlgorithms: response.data.taskChooserAlgorithms || [],
                    ratingQualityAlgorithms: response.data.ratingQualityAlgorithms || [],
                    answerQualityAlgorithms: response.data.answerQualityAlgorithms || []
                }
            });
        }).catch(() => {
            this.setState({
                algorithmsFailed: true
            });
        });
    }
}

export default ExperimentWizard;<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var EXPORTED_SYMBOLS = ['SearchFolderManager'];

var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;

Cu.import('resource://gre/modules/XPCOMUtils.jsm');

XPCOMUtils.defineLazyServiceGetter(this,
                                   'AccountManager',
                                   '@mozilla.org/messenger/account-manager;1',
                                   'nsIMsgAccountManager');
XPCOMUtils.defineLazyServiceGetter(this,
                                   'MailSession',
                                   '@mozilla.org/messenger/services/session;1',
                                   'nsIMsgMailSession');

XPCOMUtils.defineLazyModuleGetter(this,
                                  'Services',
                                  'resource://gre/modules/Services.jsm');
XPCOMUtils.defineLazyModuleGetter(this,
                                  'MailServices',
                                  'resource:///modules/mailServices.js');
XPCOMUtils.defineLazyModuleGetter(this,
                                  'VirtualFolderHelper',
                                  'resource:///modules/virtualFolderWrapper.js');

function SearchFolderManager(aDefinitions) {
  this.definitions = aDefinitions;

  Services.obs.addObserver(this, 'mail-tabs-session-restored', false);
}
SearchFolderManager.prototype = {
  get allAccounts() {
    return this.toArray(AccountManager.accounts, Ci.nsIMsgAccount);
  },
  get allAccountKeys() {
    return this.allAccounts.map(function(aAccount) {
      return this.getAccountKey(aAccount);
    }, this).filter(function(aKey) {
      return aKey != '';
    });
  },
  toArray: function (aEnumerator, aInterface) {
    aInterface = aInterface || Ci.nsISupports;
    var array = [];
    if (aEnumerator instanceof Ci.nsISupportsArray) {
      let count = aEnumerator.Count();
      for (let i = 0; i < count; i++) {
        array.push(aEnumerator.QueryElementAt(i, aInterface));
      }
    } else if (aEnumerator instanceof Ci.nsIArray) {
      let count = aEnumerator.length;
      for (let i = 0; i < count; i++) {
        array.push(aEnumerator.queryElementAt(i, aInterface));
      }
    } else if (aEnumerator instanceof Ci.nsISimpleEnumerator) {
      while (aEnumerator.hasMoreElements()) {
        array.push(aEnumerator.getNext().QueryInterface(aInterface));
      }
    }
    return array;
  },
  getDescendants: function(aRoot) {
    var folders = [];
    if ('descendants' in aRoot) { // Thunderbird 24
      let descendants = aRoot.descendants;
      for (let i = 0, maxi = descendants.length; i < maxi; i++) {
        let folder = descendants.queryElementAt(i, Ci.nsIMsgFolder);
        folders.push(folder);
      }
    } else { // Thunderbird 17 or olders
      let descendants = Cc['@mozilla.org/supports-array;1']
                          .createInstance(Ci.nsISupportsArray);
      aRoot.ListDescendents(descendants);
      for (let i = 0, maxi = descendants.Count(); i < maxi; i++) {
        let folder = descendants.GetElementAt(i).QueryInterface(Ci.nsIMsgFolder);
        folders.push(folder);
      }
    }
    return folders;
  },

  observe: function(aSubject, aTopic, aData) {
    Services.obs.removeObserver(this, 'mail-tabs-session-restored');
    // This must be done after mail-tabs-session-restored.
    // Otherwise, non-ASCII search conditions are not saved correctly.
    this.buildSearchFolders();

    this.startListenFolderChanges();
  },
  startListenFolderChanges: function() {
    var notifyFlags = Ci.nsIFolderListener.added |
                        Ci.nsIFolderListener.removed;
    MailSession.AddFolderListener(this, notifyFlags);
  },
  // nsIFolderListener
  OnItemAdded: function(aParent, aItem) {
    try {
      aItem = aItem.QueryInterface(Ci.nsIMsgFolder);
      if (aItem.flags & Ci.nsMsgFolderFlags.Virtual)
        return;
    } catch(e) {
      return;
    }
    this.buildSearchFoldersIn(aItem.rootFolder, {
      addedFolder: aItem.URI
    });
  },
  OnItemRemoved: function(aParent, aItem) {
    try {
      aItem = aItem.QueryInterface(Ci.nsIMsgFolder);
      if (aItem.flags & Ci.nsMsgFolderFlags.Virtual)
        return;
    } catch(e) {
      return;
    }
    this.buildSearchFoldersIn(aItem.rootFolder, {
      removedFolder: aItem.URI
    });
  },
  OnItemPropertyChanged: function() {},
  OnItemIntPropertyChanged: function() {},
  OnItemBoolPropertyChanged: function() {},
  OnItemUnicharPropertyChanged: function() {},
  OnItemEvent: function() {},

  buildSearchFolders: function() {
    this.allAccounts.forEach(function(aAccount) {
      if (!aAccount.incomingServer)
        return;
      this.buildSearchFoldersIn(aAccount.incomingServer.rootMsgFolder);
    }, this);
  },
  buildSearchFoldersIn: function(aRoot, aModification) {
    if (!aRoot)
      return;
    this.definitions.forEach(function(aDefinition) {
      try {
        this.buildSearchFolderIn(aDefinition, aRoot, aModification);
      }
      catch(e) {
        Cu.reportError(e);
      }
    }, this);
  },
  buildSearchFolderIn: function(aDefinition, aRoot, aModification) {
    if (!aDefinition.subjectPrefix &&
        !aDefinition.subject ||
        !aRoot)
      return;

    var searchTargets = aDefinition.searchTargets;
    if (!searchTargets)
      return;

    var searchFolders;
    if (!aModification) {
      searchFolders = this.getSearchFolders(aRoot, searchTargets.split(/[,\s]+/));
      if (!searchFolders)
        return;
    }

    var name = 'autogen-' + aDefinition.label;

    var isCreation = false;
    var isModified = false;
    var virtualFolder;
    try {
      virtualFolder = aRoot.getChildNamed(name);
    } catch(e) {
      try {
        virtualFolder = aRoot.getChildNamed(aDefinition.label);
        if (!(virtualFolder.flags & Ci.nsMsgFolderFlags.Virtual))
          virtualFolder = null;
      } catch(e) {
        // folder not found!
      }
    }
    if (!virtualFolder) {
      isCreation = true;
      isModified = true;
      virtualFolder = aRoot.addSubfolder(name);
      virtualFolder.setFlag(Ci.nsMsgFolderFlags.Virtual);
    }

    // We always have to set prettyName because it is not saved.
    virtualFolder.prettyName = aDefinition.label;

    var wrapper = VirtualFolderHelper.wrapVirtualFolder(virtualFolder);

    var conditions = this.buildSearchCondition(aDefinition);
    if (wrapper.searchString != conditions) {
      wrapper.searchString = conditions;
      isModified = true;
    }
    var currentSearchFolders = wrapper.searchFolders.map(function(aFolder) {
      return aFolder.URI
    }).join('|');
    if (aModification) {
      searchFolders = currentSearchFolders.split('|');
      if (aModification.addedFolder) {
        let index = searchFolders.indexOf(aModification.addedFolder);
        if (index < 0) {
          searchFolders.push(aModification.addedFolder);
        }
      }
      if (aModification.removedFolder) {
        let index = searchFolders.indexOf(aModification.removedFolder);
        if (index > -1) {
          searchFolders.splice(index, 1);
        }
      }
      searchFolders = searchFolders.join('|');
    }
    if (currentSearchFolders != searchFolders) {
      wrapper.searchFolders = searchFolders;
      isModified = true;
    }
    if (isCreation) {
      wrapper.onlineSearch = false;
    }

    if (!isModified)
      return;

    wrapper.cleanUpMessageDatabase();
    if (isCreation) {
      virtualFolder.msgDatabase.Close(true);
      aRoot.NotifyItemAdded(virtualFolder);
    }
    MailServices.accounts.saveVirtualFolders();
  },
  getSearchFolders: function(aRoot, aKeys) {
    var flags = {};
    Object.keys(Ci.nsMsgFolderFlags).forEach(function(aKey) {
      flags[aKey.toLowerCase()] = Ci.nsMsgFolderFlags[aKey];
    });
    var folders = [];
    aKeys.some(function(aKey) {
      aKey = aKey.toLowerCase();
      if (aKey == 'all') {
        folders = this.getDescendants(aRoot).map(function(aFolder) {
          return aFolder.URI;
        });
        return true;
      }
      if (typeof flags[aKey] == 'number') {
        let folder = aRoot.getFolderWithFlags(flags[aKey]);
        if (folder)
          folders.push(folder.URI);
      }
      return false;
    }, this);
    try {
      if (folders.length > 0)
        folders.unshift(aRoot.URI);
    } catch(e) {
    }
    return folders.join('|');
  },
  buildSearchCondition: function(aDefinition) {
    if (aDefinition.subjectPrefix)
      return 'AND (subject,begins with,' + this.UnicodeToUTF8(aDefinition.subjectPrefix) + ')';

    return 'AND (subject,is,' + this.UnicodeToUTF8(aDefinition.subject) + ')';
  },
  UnicodeToUTF8: function(aString) {
    return unescape(encodeURIComponent(aString));
  }
};
<commit_msg>Add API descriptions for SearchFolderManager<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var EXPORTED_SYMBOLS = ['SearchFolderManager'];

var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;

Cu.import('resource://gre/modules/XPCOMUtils.jsm');

XPCOMUtils.defineLazyServiceGetter(this,
                                   'AccountManager',
                                   '@mozilla.org/messenger/account-manager;1',
                                   'nsIMsgAccountManager');
XPCOMUtils.defineLazyServiceGetter(this,
                                   'MailSession',
                                   '@mozilla.org/messenger/services/session;1',
                                   'nsIMsgMailSession');

XPCOMUtils.defineLazyModuleGetter(this,
                                  'Services',
                                  'resource://gre/modules/Services.jsm');
XPCOMUtils.defineLazyModuleGetter(this,
                                  'MailServices',
                                  'resource:///modules/mailServices.js');
XPCOMUtils.defineLazyModuleGetter(this,
                                  'VirtualFolderHelper',
                                  'resource:///modules/virtualFolderWrapper.js');

/**
 * Initializes a search folder manager for given folder
 * definitions (search conditions).
 *
 * @param Array of Definition
 *        An array of search folder definitions (search conditions).
 *
 * Definition:
 *   @param label         String: the name of the folder
 *   @param subject       String: the subject to be matched (optional)
 *   @param subjectPrefix String: the prefix of the subject to be matched (optional)
 *   @param searchTargets String: "all" or comma-separated URIs of mail folders
 */
function SearchFolderManager(aDefinitions) {
  this.definitions = aDefinitions;

  Services.obs.addObserver(this, 'mail-tabs-session-restored', false);
}
SearchFolderManager.prototype = {
  get allAccounts() {
    return this.toArray(AccountManager.accounts, Ci.nsIMsgAccount);
  },
  get allAccountKeys() {
    return this.allAccounts.map(function(aAccount) {
      return this.getAccountKey(aAccount);
    }, this).filter(function(aKey) {
      return aKey != '';
    });
  },
  toArray: function (aEnumerator, aInterface) {
    aInterface = aInterface || Ci.nsISupports;
    var array = [];
    if (aEnumerator instanceof Ci.nsISupportsArray) {
      let count = aEnumerator.Count();
      for (let i = 0; i < count; i++) {
        array.push(aEnumerator.QueryElementAt(i, aInterface));
      }
    } else if (aEnumerator instanceof Ci.nsIArray) {
      let count = aEnumerator.length;
      for (let i = 0; i < count; i++) {
        array.push(aEnumerator.queryElementAt(i, aInterface));
      }
    } else if (aEnumerator instanceof Ci.nsISimpleEnumerator) {
      while (aEnumerator.hasMoreElements()) {
        array.push(aEnumerator.getNext().QueryInterface(aInterface));
      }
    }
    return array;
  },
  getDescendants: function(aRoot) {
    var folders = [];
    if ('descendants' in aRoot) { // Thunderbird 24
      let descendants = aRoot.descendants;
      for (let i = 0, maxi = descendants.length; i < maxi; i++) {
        let folder = descendants.queryElementAt(i, Ci.nsIMsgFolder);
        folders.push(folder);
      }
    } else { // Thunderbird 17 or olders
      let descendants = Cc['@mozilla.org/supports-array;1']
                          .createInstance(Ci.nsISupportsArray);
      aRoot.ListDescendents(descendants);
      for (let i = 0, maxi = descendants.Count(); i < maxi; i++) {
        let folder = descendants.GetElementAt(i).QueryInterface(Ci.nsIMsgFolder);
        folders.push(folder);
      }
    }
    return folders;
  },

  observe: function(aSubject, aTopic, aData) {
    Services.obs.removeObserver(this, 'mail-tabs-session-restored');
    // This must be done after mail-tabs-session-restored.
    // Otherwise, non-ASCII search conditions are not saved correctly.
    this.buildSearchFolders();

    this.startListenFolderChanges();
  },
  startListenFolderChanges: function() {
    var notifyFlags = Ci.nsIFolderListener.added |
                        Ci.nsIFolderListener.removed;
    MailSession.AddFolderListener(this, notifyFlags);
  },
  // nsIFolderListener
  OnItemAdded: function(aParent, aItem) {
    try {
      aItem = aItem.QueryInterface(Ci.nsIMsgFolder);
      if (aItem.flags & Ci.nsMsgFolderFlags.Virtual)
        return;
    } catch(e) {
      return;
    }
    this.buildSearchFoldersIn(aItem.rootFolder, {
      addedFolder: aItem.URI
    });
  },
  OnItemRemoved: function(aParent, aItem) {
    try {
      aItem = aItem.QueryInterface(Ci.nsIMsgFolder);
      if (aItem.flags & Ci.nsMsgFolderFlags.Virtual)
        return;
    } catch(e) {
      return;
    }
    this.buildSearchFoldersIn(aItem.rootFolder, {
      removedFolder: aItem.URI
    });
  },
  OnItemPropertyChanged: function() {},
  OnItemIntPropertyChanged: function() {},
  OnItemBoolPropertyChanged: function() {},
  OnItemUnicharPropertyChanged: function() {},
  OnItemEvent: function() {},

  buildSearchFolders: function() {
    this.allAccounts.forEach(function(aAccount) {
      if (!aAccount.incomingServer)
        return;
      this.buildSearchFoldersIn(aAccount.incomingServer.rootMsgFolder);
    }, this);
  },
  buildSearchFoldersIn: function(aRoot, aModification) {
    if (!aRoot)
      return;
    this.definitions.forEach(function(aDefinition) {
      try {
        this.buildSearchFolderIn(aDefinition, aRoot, aModification);
      }
      catch(e) {
        Cu.reportError(e);
      }
    }, this);
  },
  buildSearchFolderIn: function(aDefinition, aRoot, aModification) {
    if (!aDefinition.subjectPrefix &&
        !aDefinition.subject ||
        !aRoot)
      return;

    var searchTargets = aDefinition.searchTargets;
    if (!searchTargets)
      return;

    var searchFolders;
    if (!aModification) {
      searchFolders = this.getSearchFolders(aRoot, searchTargets.split(/[,\s]+/));
      if (!searchFolders)
        return;
    }

    var name = 'autogen-' + aDefinition.label;

    var isCreation = false;
    var isModified = false;
    var virtualFolder;
    try {
      virtualFolder = aRoot.getChildNamed(name);
    } catch(e) {
      try {
        virtualFolder = aRoot.getChildNamed(aDefinition.label);
        if (!(virtualFolder.flags & Ci.nsMsgFolderFlags.Virtual))
          virtualFolder = null;
      } catch(e) {
        // folder not found!
      }
    }
    if (!virtualFolder) {
      isCreation = true;
      isModified = true;
      virtualFolder = aRoot.addSubfolder(name);
      virtualFolder.setFlag(Ci.nsMsgFolderFlags.Virtual);
    }

    // We always have to set prettyName because it is not saved.
    virtualFolder.prettyName = aDefinition.label;

    var wrapper = VirtualFolderHelper.wrapVirtualFolder(virtualFolder);

    var conditions = this.buildSearchCondition(aDefinition);
    if (wrapper.searchString != conditions) {
      wrapper.searchString = conditions;
      isModified = true;
    }
    var currentSearchFolders = wrapper.searchFolders.map(function(aFolder) {
      return aFolder.URI
    }).join('|');
    if (aModification) {
      searchFolders = currentSearchFolders.split('|');
      if (aModification.addedFolder) {
        let index = searchFolders.indexOf(aModification.addedFolder);
        if (index < 0) {
          searchFolders.push(aModification.addedFolder);
        }
      }
      if (aModification.removedFolder) {
        let index = searchFolders.indexOf(aModification.removedFolder);
        if (index > -1) {
          searchFolders.splice(index, 1);
        }
      }
      searchFolders = searchFolders.join('|');
    }
    if (currentSearchFolders != searchFolders) {
      wrapper.searchFolders = searchFolders;
      isModified = true;
    }
    if (isCreation) {
      wrapper.onlineSearch = false;
    }

    if (!isModified)
      return;

    wrapper.cleanUpMessageDatabase();
    if (isCreation) {
      virtualFolder.msgDatabase.Close(true);
      aRoot.NotifyItemAdded(virtualFolder);
    }
    MailServices.accounts.saveVirtualFolders();
  },
  getSearchFolders: function(aRoot, aKeys) {
    var flags = {};
    Object.keys(Ci.nsMsgFolderFlags).forEach(function(aKey) {
      flags[aKey.toLowerCase()] = Ci.nsMsgFolderFlags[aKey];
    });
    var folders = [];
    aKeys.some(function(aKey) {
      aKey = aKey.toLowerCase();
      if (aKey == 'all') {
        folders = this.getDescendants(aRoot).map(function(aFolder) {
          return aFolder.URI;
        });
        return true;
      }
      if (typeof flags[aKey] == 'number') {
        let folder = aRoot.getFolderWithFlags(flags[aKey]);
        if (folder)
          folders.push(folder.URI);
      }
      return false;
    }, this);
    try {
      if (folders.length > 0)
        folders.unshift(aRoot.URI);
    } catch(e) {
    }
    return folders.join('|');
  },
  buildSearchCondition: function(aDefinition) {
    if (aDefinition.subjectPrefix)
      return 'AND (subject,begins with,' + this.UnicodeToUTF8(aDefinition.subjectPrefix) + ')';

    return 'AND (subject,is,' + this.UnicodeToUTF8(aDefinition.subject) + ')';
  },
  UnicodeToUTF8: function(aString) {
    return unescape(encodeURIComponent(aString));
  }
};
<|endoftext|>


<commit_before>'use strict';

var assert = require('assert');

var JsigAST = require('../ast/');
var isSameType = require('./lib/is-same-type.js');
var deepCloneJSIG = require('./lib/deep-clone-ast.js');

module.exports = TypeInference;

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

TypeInference.prototype.inferType = function inferType(node) {
    if (node.type === 'CallExpression') {
        return this.inferCallExpression(node);
    } else if (node.type === 'Literal') {
        return this.inferLiteral(node);
    } else if (node.type === 'ArrayExpression') {
        return this.inferArrayExpression(node);
    } else if (node.type === 'ObjectExpression') {
        return this.inferObjectExpression(node);
    } else {
        throw new Error('!! skipping inferType: ' + node.type);
    }
};

TypeInference.prototype.inferCallExpression =
function inferCallExpression(node) {
    var untypedFunc = this.meta.currentScope
        .getUntypedFunction(node.callee.name);
    if (!untypedFunc) {
        return null;
    }

    var args = node.arguments;

    var argTypes = [];
    for (var i = 0; i < args.length; i++) {
        var funcArg = this.meta.verifyNode(args[i], null);
        if (!funcArg) {
            return null;
        }

        argTypes.push(JsigAST.param(null, funcArg));
    }

    var returnType = JsigAST.literal('%Void%%UnknownReturn', true);
    if (this.meta.currentExpressionType) {
        returnType = this.meta.currentExpressionType;
    }

    // if (this.meta.currentScope.getAssignmentType()) {
        // returnType = this.meta.currentScope.getAssignmentType();
    // } else if (this.meta.currentScope.getReturnExpressionType()) {
        // returnType = this.meta.currentScope.getReturnExpressionType();
    // }

    // TODO: infer this arg based on method calls
    var funcType = JsigAST.functionType({
        args: argTypes,
        result: returnType,
        thisArg: null
    });

    if (!this.meta.tryUpdateFunction(node.callee.name, funcType)) {
        return null;
    }

    if (returnType.builtin && returnType.name === '%Void%%UnknownReturn') {
        // Grab the scope for the known function
        var funcScopes = this.meta.currentScope.getKnownFunctionScope(
            node.callee.name
        ).funcScopes;

        assert(funcScopes.length === 1,
            'cannot infer call return for overloaded function'
        );
        var funcScope = funcScopes[0];

        if (funcScope.knownReturnTypes.length > 0) {
            // TODO: grab the smallest union?
            assert(funcScope.knownReturnTypes.length === 1,
                'only support trivial single return funcs');
            funcType.result = funcScope.knownReturnTypes[0];
        }
    }

    return funcType;
};

TypeInference.prototype.inferLiteral =
function inferLiteral(node) {
    var value = node.value;

    if (typeof value === 'string') {
        return JsigAST.literal('String', true, {
            concreteValue: value
        });
    } else if (typeof value === 'number') {
        return JsigAST.literal('Number', true, {
            concreteValue: parseFloat(value, 10)
        });
    } else if (value === null) {
        return JsigAST.value('null');
    } else if (Object.prototype.toString.call(value) === '[object RegExp]') {
        return JsigAST.literal('RegExp');
    } else if (typeof value === 'boolean') {
        return JsigAST.literal('Boolean');
    } else {
        throw new Error('not recognised literal');
    }
};

TypeInference.prototype.inferArrayExpression =
function inferArrayExpression(node) {
    var elems = node.elements;

    if (elems.length === 0) {
        var currExprType = this.meta.currentExpressionType;
        if (currExprType && currExprType.type === 'genericLiteral' &&
            currExprType.value.type === 'typeLiteral' &&
            currExprType.value.builtin && currExprType.value.name === 'Array'
        ) {
            return currExprType;
        }

        return JsigAST.generic(
            JsigAST.literal('Array'),
            [JsigAST.freeLiteral('T')]
        );
    }

    if (this.meta.currentExpressionType &&
        this.meta.currentExpressionType.type === 'tuple'
    ) {
        return this._inferTupleExpression(node);
    }

    var values = [];

    for (var i = 0; i < elems.length; i++) {
        var newType = this.meta.verifyNode(elems[i], null);
        if (!newType) {
            return null;
        }

        values[i] = newType;
    }

    return JsigAST.tuple(values);

    // var type = null;
    // for (var i = 0; i < elems.length; i++) {
    //     var newType = this.meta.verifyNode(elems[i], null);
    //     if (type) {
    //         assert(isSameType(newType, type), 'arrays must be homogenous');
    //     }
    //     type = newType;
    // }

    // if (!type) {
    //     return null;
    // }

    // return JsigAST.generic(JsigAST.literal('Array'), [type]);
};

TypeInference.prototype._inferTupleExpression =
function _inferTupleExpression(node) {
    var values = [];

    assert(this.meta.currentExpressionType &&
        this.meta.currentExpressionType.type === 'tuple',
        'must be a tuple...'
    );

    var tupleTypes = this.meta.currentExpressionType.values;

    for (var i = 0; i < node.elements.length; i++) {
        var expected = tupleTypes[i] || null;

        values[i] = this.meta.verifyNode(node.elements[i], expected);
        if (!values[i]) {
            return null;
        }
    }

    return JsigAST.tuple(values);
};

TypeInference.prototype.inferObjectExpression =
function inferObjectExpression(node) {
    var properties = node.properties;

    if (properties.length === 0) {
        var openObj = JsigAST.object([]);
        openObj.open = true;
        return openObj;
    }

    var currentExpressionType = this.meta.currentExpressionType;
    var index = null;
    if (currentExpressionType && currentExpressionType.type === 'object') {
        index = currentExpressionType.buildObjectIndex();
    }

    var keyValues = [];
    for (var i = 0; i < properties.length; i++) {
        var prop = properties[i];
        assert(prop.kind === 'init', 'only support init kind');

        var keyName = null;
        if (prop.key.type === 'Identifier') {
            keyName = prop.key.name;
        } else if (prop.key.type === 'Literal') {
            keyName = prop.key.value;
        }

        var expectedType = null;
        if (keyName && index && index[keyName]) {
            expectedType = index[keyName];
        }

        if (prop.value.type === 'Identifier') {
            this.meta.currentScope.markVarAsAlias(
                prop.value.name, null
            );
        }

        var value = this.meta.verifyNode(prop.value, expectedType);
        if (!value) {
            return null;
        }

        keyValues.push(JsigAST.keyValue(keyName, value));
    }

    return JsigAST.object(keyValues);
};

TypeInference.prototype.resolveGeneric =
function resolveGeneric(funcType, node, currentExpressionType) {
    /*
        CallExpression : {
            callee: {
                type: 'MemberExpression',
                object: { type: 'Identifier' }
            },
            arguments: Array<X>
        }

        NewExpression : {
            callee: { type: 'Identifier' },
            arguments: Array<X>
        }
    */

    var copyFunc = deepCloneJSIG(funcType);
    copyFunc._raw = null;

    var knownGenericTypes = this._findGenericTypes(
        copyFunc, node, currentExpressionType
    );
    if (!knownGenericTypes) {
        return null;
    }

    for (var i = 0; i < copyFunc.generics.length; i++) {
        var g = copyFunc.generics[i];
        var newType = knownGenericTypes[g.name];
        assert(newType, 'newType must exist');

        var stack = g.location.slice();

        var obj = copyFunc;
        for (var j = 0; j < stack.length - 1; j++) {
            obj = obj[stack[j]];
            obj._raw = null;
        }

        var lastProp = stack[stack.length - 1];
        obj[lastProp] = newType;
    }

    return copyFunc;
};

/*eslint complexity: [2, 25], max-statements: [2, 60] */
TypeInference.prototype._findGenericTypes =
function _findGenericTypes(copyFunc, node, currentExpressionType) {
    var knownGenericTypes = Object.create(null);

    for (var i = 0; i < copyFunc.generics.length; i++) {
        var g = copyFunc.generics[i];

        var newType;
        var referenceNode;
        var stack = g.location;
        var ast = walkProps(copyFunc, stack, 0);

        if (stack[0] === 'args') {
            referenceNode = node.arguments[stack[1]];
            if (!referenceNode) {
                return null;
            }

            newType = this.meta.verifyNode(referenceNode, null);
            if (!newType) {
                return null;
            }

            newType = walkProps(newType, stack, 3);
        } else if (stack[0] === 'thisArg') {

            // Method call()
            if (node.callee.type === 'MemberExpression') {
                referenceNode = node.callee.object;
                // TODO: this might be wrong
                newType = this.meta.verifyNode(referenceNode, null);
                if (!newType) {
                    return null;
                }

                newType = walkProps(newType, stack, 2);
            // new expression
            } else if (node.callee.type === 'Identifier') {
                if (currentExpressionType) {
                    newType = currentExpressionType;
                    newType = walkProps(newType, stack, 2);
                } else {
                    // If we have no ctx as for type then free literal
                    newType = JsigAST.freeLiteral('T');
                }
            } else {
                assert(false, 'unknown caller type: ' + node.callee.type);
            }
        } else {
            referenceNode = node;
            newType = knownGenericTypes[ast.name];
            assert(newType, 'newType must exist in fallback');
        }

        if (!newType) {
            return null;
        }

        if (knownGenericTypes[ast.name]) {
            var oldType = knownGenericTypes[ast.name];

            var subTypeError;
            if (newType.type === 'freeLiteral') {
                subTypeError = null;
            } else {
                subTypeError = this.meta.checkSubTypeRaw(
                    referenceNode, oldType, newType
                );
            }

            if (subTypeError) {
                // A free variable fits in any type.
                var isSub = oldType.type === 'freeLiteral';
                if (!isSub) {
                    isSub = this.meta.isSubType(
                        referenceNode, newType, oldType
                    );
                }
                if (isSub) {
                    knownGenericTypes[ast.name] = newType;
                    subTypeError = null;
                }
            }

            if (subTypeError) {
                this.meta.addError(subTypeError);
                return null;
                // TODO: bug and shit
                // assert(false, 'could not resolve generics');
            }
        } else {
            knownGenericTypes[ast.name] = newType;
        }
    }

    return knownGenericTypes;
};

function walkProps(object, stack, start) {
    for (var i = start; i < stack.length; i++) {
        if (!object) {
            return null;
        }

        object = object[stack[i]];
    }
    return object;
}
<commit_msg>infer(): improve array literal inference<commit_after>'use strict';

var assert = require('assert');

var JsigAST = require('../ast/');
var isSameType = require('./lib/is-same-type.js');
var deepCloneJSIG = require('./lib/deep-clone-ast.js');

module.exports = TypeInference;

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

TypeInference.prototype.inferType = function inferType(node) {
    if (node.type === 'CallExpression') {
        return this.inferCallExpression(node);
    } else if (node.type === 'Literal') {
        return this.inferLiteral(node);
    } else if (node.type === 'ArrayExpression') {
        return this.inferArrayExpression(node);
    } else if (node.type === 'ObjectExpression') {
        return this.inferObjectExpression(node);
    } else {
        throw new Error('!! skipping inferType: ' + node.type);
    }
};

TypeInference.prototype.inferCallExpression =
function inferCallExpression(node) {
    var untypedFunc = this.meta.currentScope
        .getUntypedFunction(node.callee.name);
    if (!untypedFunc) {
        return null;
    }

    var args = node.arguments;

    var argTypes = [];
    for (var i = 0; i < args.length; i++) {
        var funcArg = this.meta.verifyNode(args[i], null);
        if (!funcArg) {
            return null;
        }

        argTypes.push(JsigAST.param(null, funcArg));
    }

    var returnType = JsigAST.literal('%Void%%UnknownReturn', true);
    if (this.meta.currentExpressionType) {
        returnType = this.meta.currentExpressionType;
    }

    // if (this.meta.currentScope.getAssignmentType()) {
        // returnType = this.meta.currentScope.getAssignmentType();
    // } else if (this.meta.currentScope.getReturnExpressionType()) {
        // returnType = this.meta.currentScope.getReturnExpressionType();
    // }

    // TODO: infer this arg based on method calls
    var funcType = JsigAST.functionType({
        args: argTypes,
        result: returnType,
        thisArg: null
    });

    if (!this.meta.tryUpdateFunction(node.callee.name, funcType)) {
        return null;
    }

    if (returnType.builtin && returnType.name === '%Void%%UnknownReturn') {
        // Grab the scope for the known function
        var funcScopes = this.meta.currentScope.getKnownFunctionScope(
            node.callee.name
        ).funcScopes;

        assert(funcScopes.length === 1,
            'cannot infer call return for overloaded function'
        );
        var funcScope = funcScopes[0];

        if (funcScope.knownReturnTypes.length > 0) {
            // TODO: grab the smallest union?
            assert(funcScope.knownReturnTypes.length === 1,
                'only support trivial single return funcs');
            funcType.result = funcScope.knownReturnTypes[0];
        }
    }

    return funcType;
};

TypeInference.prototype.inferLiteral =
function inferLiteral(node) {
    var value = node.value;

    if (typeof value === 'string') {
        return JsigAST.literal('String', true, {
            concreteValue: value
        });
    } else if (typeof value === 'number') {
        return JsigAST.literal('Number', true, {
            concreteValue: parseFloat(value, 10)
        });
    } else if (value === null) {
        return JsigAST.value('null');
    } else if (Object.prototype.toString.call(value) === '[object RegExp]') {
        return JsigAST.literal('RegExp');
    } else if (typeof value === 'boolean') {
        return JsigAST.literal('Boolean');
    } else {
        throw new Error('not recognised literal');
    }
};

TypeInference.prototype.inferArrayExpression =
function inferArrayExpression(node) {
    var elems = node.elements;

    if (elems.length === 0) {
        var currExprType = this.meta.currentExpressionType;
        if (currExprType && currExprType.type === 'genericLiteral' &&
            currExprType.value.type === 'typeLiteral' &&
            currExprType.value.builtin && currExprType.value.name === 'Array'
        ) {
            return currExprType;
        }

        return JsigAST.generic(
            JsigAST.literal('Array'),
            [JsigAST.freeLiteral('T')]
        );
    }

    var currExpr = this.meta.currentExpressionType;
    if (currExpr && currExpr.type === 'tuple') {
        return this._inferTupleExpression(node);
    }

    if (currExpr && currExpr.type === 'genericLiteral' &&
        currExpr.value.type === 'typeLiteral' &&
        currExpr.value.name === 'Array' && currExpr.value.builtin
    ) {
        return this._inferArrayExpression(node);
    }

    var values = [];

    for (var i = 0; i < elems.length; i++) {
        var newType = this.meta.verifyNode(elems[i], null);
        if (!newType) {
            return null;
        }

        values[i] = newType;
    }

    return JsigAST.tuple(values);
};

TypeInference.prototype._inferArrayExpression =
function _inferArrayExpression(node) {
    var currExpr = this.meta.currentExpressionType;

    assert(currExpr && currExpr.type === 'genericLiteral' &&
        currExpr.value.type === 'typeLiteral' &&
        currExpr.value.name === 'Array' && currExpr.value.builtin,
        'must be an array...'
    );

    var arrayType = currExpr.generics[0];
    var elems = node.elements;

    var type = null;
    for (var i = 0; i < elems.length; i++) {
        var newType = this.meta.verifyNode(elems[i], arrayType);
        if (type) {
            assert(isSameType(newType, type), 'arrays must be homogenous');
        }
        type = newType;
    }

    if (!type) {
        return null;
    }

    return JsigAST.generic(JsigAST.literal('Array'), [type]);
};

TypeInference.prototype._inferTupleExpression =
function _inferTupleExpression(node) {
    var values = [];

    assert(this.meta.currentExpressionType &&
        this.meta.currentExpressionType.type === 'tuple',
        'must be a tuple...'
    );

    var tupleTypes = this.meta.currentExpressionType.values;

    for (var i = 0; i < node.elements.length; i++) {
        var expected = tupleTypes[i] || null;

        values[i] = this.meta.verifyNode(node.elements[i], expected);
        if (!values[i]) {
            return null;
        }
    }

    return JsigAST.tuple(values);
};

TypeInference.prototype.inferObjectExpression =
function inferObjectExpression(node) {
    var properties = node.properties;

    if (properties.length === 0) {
        var openObj = JsigAST.object([]);
        openObj.open = true;
        return openObj;
    }

    var currentExpressionType = this.meta.currentExpressionType;
    var index = null;
    if (currentExpressionType && currentExpressionType.type === 'object') {
        index = currentExpressionType.buildObjectIndex();
    }

    var keyValues = [];
    for (var i = 0; i < properties.length; i++) {
        var prop = properties[i];
        assert(prop.kind === 'init', 'only support init kind');

        var keyName = null;
        if (prop.key.type === 'Identifier') {
            keyName = prop.key.name;
        } else if (prop.key.type === 'Literal') {
            keyName = prop.key.value;
        }

        var expectedType = null;
        if (keyName && index && index[keyName]) {
            expectedType = index[keyName];
        }

        if (prop.value.type === 'Identifier') {
            this.meta.currentScope.markVarAsAlias(
                prop.value.name, null
            );
        }

        var value = this.meta.verifyNode(prop.value, expectedType);
        if (!value) {
            return null;
        }

        keyValues.push(JsigAST.keyValue(keyName, value));
    }

    return JsigAST.object(keyValues);
};

TypeInference.prototype.resolveGeneric =
function resolveGeneric(funcType, node, currentExpressionType) {
    /*
        CallExpression : {
            callee: {
                type: 'MemberExpression',
                object: { type: 'Identifier' }
            },
            arguments: Array<X>
        }

        NewExpression : {
            callee: { type: 'Identifier' },
            arguments: Array<X>
        }
    */

    var copyFunc = deepCloneJSIG(funcType);
    copyFunc._raw = null;

    var knownGenericTypes = this._findGenericTypes(
        copyFunc, node, currentExpressionType
    );
    if (!knownGenericTypes) {
        return null;
    }

    for (var i = 0; i < copyFunc.generics.length; i++) {
        var g = copyFunc.generics[i];
        var newType = knownGenericTypes[g.name];
        assert(newType, 'newType must exist');

        var stack = g.location.slice();

        var obj = copyFunc;
        for (var j = 0; j < stack.length - 1; j++) {
            obj = obj[stack[j]];
            obj._raw = null;
        }

        var lastProp = stack[stack.length - 1];
        obj[lastProp] = newType;
    }

    return copyFunc;
};

/*eslint complexity: [2, 25], max-statements: [2, 60] */
TypeInference.prototype._findGenericTypes =
function _findGenericTypes(copyFunc, node, currentExpressionType) {
    var knownGenericTypes = Object.create(null);

    for (var i = 0; i < copyFunc.generics.length; i++) {
        var g = copyFunc.generics[i];

        var newType;
        var referenceNode;
        var stack = g.location;
        var ast = walkProps(copyFunc, stack, 0);

        if (stack[0] === 'args') {
            referenceNode = node.arguments[stack[1]];
            if (!referenceNode) {
                return null;
            }

            newType = this.meta.verifyNode(referenceNode, null);
            if (!newType) {
                return null;
            }

            newType = walkProps(newType, stack, 3);
        } else if (stack[0] === 'thisArg') {

            // Method call()
            if (node.callee.type === 'MemberExpression') {
                referenceNode = node.callee.object;
                // TODO: this might be wrong
                newType = this.meta.verifyNode(referenceNode, null);
                if (!newType) {
                    return null;
                }

                newType = walkProps(newType, stack, 2);
            // new expression
            } else if (node.callee.type === 'Identifier') {
                if (currentExpressionType) {
                    newType = currentExpressionType;
                    newType = walkProps(newType, stack, 2);
                } else {
                    // If we have no ctx as for type then free literal
                    newType = JsigAST.freeLiteral('T');
                }
            } else {
                assert(false, 'unknown caller type: ' + node.callee.type);
            }
        } else {
            referenceNode = node;
            newType = knownGenericTypes[ast.name];
            assert(newType, 'newType must exist in fallback');
        }

        if (!newType) {
            return null;
        }

        if (knownGenericTypes[ast.name]) {
            var oldType = knownGenericTypes[ast.name];

            var subTypeError;
            if (newType.type === 'freeLiteral') {
                subTypeError = null;
            } else {
                subTypeError = this.meta.checkSubTypeRaw(
                    referenceNode, oldType, newType
                );
            }

            if (subTypeError) {
                // A free variable fits in any type.
                var isSub = oldType.type === 'freeLiteral';
                if (!isSub) {
                    isSub = this.meta.isSubType(
                        referenceNode, newType, oldType
                    );
                }
                if (isSub) {
                    knownGenericTypes[ast.name] = newType;
                    subTypeError = null;
                }
            }

            if (subTypeError) {
                this.meta.addError(subTypeError);
                return null;
                // TODO: bug and shit
                // assert(false, 'could not resolve generics');
            }
        } else {
            knownGenericTypes[ast.name] = newType;
        }
    }

    return knownGenericTypes;
};

function walkProps(object, stack, start) {
    for (var i = start; i < stack.length; i++) {
        if (!object) {
            return null;
        }

        object = object[stack[i]];
    }
    return object;
}
<|endoftext|>