Home Reference Source

src/core/scopeTree.js

/**
 * A scope tree is a tree that captures the scope of a piece of code. A node
 * contains the start and end of the scope. If start or end is null then the
 * start or end is not in the code. A parent node contains all scopes inside
 * it as children nodes.
 */
export default class ScopeTree {

  /**
   * Creates a new node.
   *
   * @constructor
   * @param parent The parent node.
   * @param start The start of the scope of this node.
   */
  constructor(parent, start) {
    /**
     * @private
     */
    this.parent = parent

    /**
     * @private
     */
    this.children = []

    /**
     * @private
     */
    this.start = start

    /**
     * @private
     */
    this.end = null
  }

  /**
   * Get the start of the scope.
   *
   * @returns {Number} The start of the scope, or null if not defined.
   */
  getStart() {
    return this.start
  }

  /**
   * Get the end of the scope.
   *
   * @returns {Number} The end of the scope, or null if not defined.
   */
  getEnd() {
    return this.end
  }

  /**
   * Get the parent of this node. Returns null if this is the root node.
   *
   * @returns {ScopeTree} The parent of this node, or null if this is the root node.
   */
  getParent() {
    return this.parent
  }

  /**
   * Get an array of all children of this node.
   *
   * @returns {Array} An array of all children of this node.
   */
  getChildren() {
    return this.children
  }

  /**
   * Add a child to this node.
   *
   * @param child The child to add to this node, should also be of type ScopeTree.
   */
  addChild(child) {
    this.children.push(child)
  }

  /**
   * Close this scope. This means the end of the scope has been reached.
   *
   * @param line The line that the scope ends at.
   */
  close(line) {
    this.end = line
  }

  /**
   * Returns true if the scope of this node is closed (meaning the scope has
   * a start and an end point)
   *
   * @returns {Boolean} True if the scope for this node is defined, else false.
   */
  isClosed() {
    return this.start !== null && this.end !== null
  }

  /**
   * Balance the tree. This enforces an ordering of the nodes in the tree such
   * that parent nodes are always 1 scope above their child nodes.
   *
   * By nature, the tree will be built in a balanced fashion. However because
   * we might be dealing with snippets of code scope entries or exits (i.e. brackets)
   * might be missing, which puts the tree out of balance. This method fixes that.
   *
   * Particularly, this moves all left siblings of a node that only has an end
   * but not a start under it, making them his children.
   *
   * @example
   *  <-> : scope is open and closed
   *  <-  : scope is only opened but not closed
   *   -> : scope is only closed but never opened
   *
   * IMBALANCED TREE                BALANCED TREE
   *  -root-                         -root-
   *    |                              |
   *    |---------------               |-----
   *    |    |    |    |               |    |
   *    |    |    |    |               |    |
   *   <->  <->   ->  <-    =======>   ->  <-
   *                                   |
   *    ^    ^    ^    ^               |-----
   *    |    |    |    |               |    |
   *   ok   ok   not  ok               |    |
   *             ok                   <->  <->
   */
  balance() {
    for (let i = 0; i < this.getChildren().length; i++) {
      let child = this.getChildren()[i]

      // Check if there is a lone closing scope
      if (child.getStart() === null && child.getEnd() !== null) {

        /* Pretend this lone closing scope had a matching opening scope at the
         * beginning and turn all previous siblings into children of the closing
         * scope.
         */
        for (let j = 0; j < i; j++) {
          let sibling = this.getChildren()[0]
          this.getChildren().splice(0, 1)
          child.addChild(sibling)
        }
      }

      child.balance()
    }
  }

  /**
   * Builds the scope tree. This function is designed in such a way that it can
   * build scope trees over a variety of different programming languages. That
   * is why as a parameter, the function takes two functions, each which
   * determine where the scope starts or ends in a current line, if it does at
   * all. These functions NEED to have the following signatures:
   *
   * Index:Integer scopeEnterFunc(arrayOfAllLines:Array, lineToCheckIndex:Integer)
   * Index:Integer scopeExitFunc(arrayOfAllLines:Array, lineToCheckIndex:Integer)
   *
   * Both functions take the full array of lines and the index of the line to
   * examine, and return the column within the line at which the scope is entered
   * or exited. If there is no scope change in the line, null must be returned.
   *
   * @param lines Array of lines to build the scope tree over.
   * @param index Current line number.
   * @param scopeEnterFunc Function with the above signature. It takes the full
   *        array of lines and the index of the line to examine, and returns
   *        the column within the line at which the scope is entered. If no scope
   *        is entered in the line, null must be returned.
   * @param scopeExitFunc Function with the above signature. It takes the full
   *        array of lines and the index of the line to examine, and returns
   *        the column within the line at which the scope is exited. If no scope
   *        is exited in the line, null must be returned.
   * @returns {ScopeTree} The root node of the scope tree.
   */
  build(lines, index, scopeEnterFunc, scopeExitFunc) {
    let node = this
    if (lines.length === 0) {
      if (node.getParent() !== null) {
        return node.getParent().build([], index, scopeEnterFunc, scopeExitFunc)
      } else {
        return node
      }
    }

    let enterIndex = scopeEnterFunc(lines, 0)
    let exitIndex = scopeExitFunc(lines, 0)
    let line = lines.shift()

    if (enterIndex !== null && (enterIndex < exitIndex || exitIndex === null)) {
      let child = new ScopeTree(node, index)
      node.addChild(child)

      let remaining = line.substr(enterIndex + 1)
      if (scopeEnterFunc([remaining], 0) !== null) {
        lines.unshift(remaining)
        return child.build(lines, index, scopeEnterFunc, scopeExitFunc)
      } else {
        node = child // This allows us to check if the scope is closed in the
                     // same line and if not we pass on the child as the next node
      }
    } else if (enterIndex !== null && exitIndex !== null) {
      // Check for this case: } foo {
      if (node.getParent() === null) {
        let child = new ScopeTree(node, null)
        child.close(index)
        node.addChild(child)
        node = child
      } else {
        node.close(index)
      }

      let remaining = line.substr(exitIndex + 1)
      lines.unshift(remaining)
      return node.getParent().build(lines, index, scopeEnterFunc, scopeExitFunc)
    }

    if (exitIndex !== null) {
      if (node.getParent() === null) {
        let child = new ScopeTree(node, null)
        child.close(index)
        node.addChild(child)
        node = child // This avoids a null pointer exception when we call
                     // node.getParent() below
      } else {
        node.close(index)
      }

      let remaining = line.substr(exitIndex + 1)

      if (scopeExitFunc([remaining], 0) !== null) {
        lines.unshift(remaining)
        return node.getParent().build(lines, index, scopeEnterFunc, scopeExitFunc)
      } else {
        node = node.getParent()
      }
    }

    return node.build(lines, index + 1, scopeEnterFunc, scopeExitFunc)
  }
}