/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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/. */
"use strict";

const Ci = Components.interfaces;
const Cu = Components.utils;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const DevToolsUtils = require("devtools/shared/DevToolsUtils");

XPCOMUtils.defineLazyModuleGetter(this,
  "Reflect", "resource://gre/modules/reflect.jsm");

this.EXPORTED_SYMBOLS = ["Parser", "ParserHelpers", "SyntaxTreeVisitor"];

/**
 * A JS parser using the reflection API.
 */
this.Parser = function Parser() {
  this._cache = new Map();
  this.errors = [];
  this.logExceptions = true;
};

Parser.prototype = {
  /**
   * Gets a collection of parser methods for a specified source.
   *
   * @param string aSource
   *        The source text content.
   * @param string aUrl [optional]
   *        The source url. The AST nodes will be cached, so you can use this
   *        identifier to avoid parsing the whole source again.
   */
  get: function(aSource, aUrl = "") {
    // Try to use the cached AST nodes, to avoid useless parsing operations.
    if (this._cache.has(aUrl)) {
      return this._cache.get(aUrl);
    }

    // The source may not necessarily be JS, in which case we need to extract
    // all the scripts. Fastest/easiest way is with a regular expression.
    // Don't worry, the rules of using a <script> tag are really strict,
    // this will work.
    let regexp = /<script[^>]*?(?:>([^]*?)<\/script\s*>|\/>)/gim;
    let syntaxTrees = [];
    let scriptMatches = [];
    let scriptMatch;

    if (aSource.match(/^\s*</)) {
      // First non whitespace character is &lt, so most definitely HTML.
      while (scriptMatch = regexp.exec(aSource)) {
        // Contents are captured at index 1 or nothing: Self-closing scripts
        // won't capture code content
        scriptMatches.push(scriptMatch[1] || "");
      }
    }

    // If there are no script matches, send the whole source directly to the
    // reflection API to generate the AST nodes.
    if (!scriptMatches.length) {
      // Reflect.parse throws when encounters a syntax error.
      try {
        let nodes = Reflect.parse(aSource);
        let length = aSource.length;
        syntaxTrees.push(new SyntaxTree(nodes, aUrl, length));
      } catch (e) {
        this.errors.push(e);
        if (this.logExceptions) {
          DevToolsUtils.reportException(aUrl, e);
        }
      }
    }
    // Generate the AST nodes for each script.
    else {
      for (let script of scriptMatches) {
        // Reflect.parse throws when encounters a syntax error.
        try {
          let nodes = Reflect.parse(script);
          let offset = aSource.indexOf(script);
          let length = script.length;
          syntaxTrees.push(new SyntaxTree(nodes, aUrl, length, offset));
        } catch (e) {
          this.errors.push(e);
          if (this.logExceptions) {
            DevToolsUtils.reportException(aUrl, e);
          }
        }
      }
    }

    let pool = new SyntaxTreesPool(syntaxTrees, aUrl);

    // Cache the syntax trees pool by the specified url. This is entirely
    // optional, but it's strongly encouraged to cache ASTs because
    // generating them can be costly with big/complex sources.
    if (aUrl) {
      this._cache.set(aUrl, pool);
    }

    return pool;
  },

  /**
   * Clears all the parsed sources from cache.
   */
  clearCache: function() {
    this._cache.clear();
  },

  /**
   * Clears the AST for a particular source.
   *
   * @param String aUrl
   *        The URL of the source that is being cleared.
   */
  clearSource: function(aUrl) {
    this._cache.delete(aUrl);
  },

  _cache: null,
  errors: null
};

/**
 * A pool handling a collection of AST nodes generated by the reflection API.
 *
 * @param object aSyntaxTrees
 *        A collection of AST nodes generated for a source.
 * @param string aUrl [optional]
 *        The source url.
 */
function SyntaxTreesPool(aSyntaxTrees, aUrl = "<unknown>") {
  this._trees = aSyntaxTrees;
  this._url = aUrl;
  this._cache = new Map();
}

SyntaxTreesPool.prototype = {
  /**
   * @see SyntaxTree.prototype.getIdentifierAt
   */
  getIdentifierAt: function({ line, column, scriptIndex, ignoreLiterals }) {
    return this._call("getIdentifierAt", scriptIndex, line, column, ignoreLiterals)[0];
  },

  /**
   * @see SyntaxTree.prototype.getNamedFunctionDefinitions
   */
  getNamedFunctionDefinitions: function(aSubstring) {
    return this._call("getNamedFunctionDefinitions", -1, aSubstring);
  },

  /**
   * @return SyntaxTree
   *         The last tree in this._trees
   */
  getLastSyntaxTree: function() {
    return this._trees[this._trees.length - 1];
  },

  /**
   * Gets the total number of scripts in the parent source.
   * @return number
   */
  get scriptCount() {
    return this._trees.length;
  },

  /**
   * Finds the start and length of the script containing the specified offset
   * relative to its parent source.
   *
   * @param number aOffset
   *        The offset relative to the parent source.
   * @return object
   *         The offset and length relative to the enclosing script.
   */
  getScriptInfo: function(aOffset) {
    let info = { start: -1, length: -1, index: -1 };

    for (let { offset, length } of this._trees) {
      info.index++;
      if (offset <= aOffset && offset + length >= aOffset) {
        info.start = offset;
        info.length = length;
        return info;
      }
    }

    info.index = -1;
    return info;
  },

  /**
   * Handles a request for a specific or all known syntax trees.
   *
   * @param string aFunction
   *        The function name to call on the SyntaxTree instances.
   * @param number aSyntaxTreeIndex
   *        The syntax tree for which to handle the request. If the tree at
   *        the specified index isn't found, the accumulated results for all
   *        syntax trees are returned.
   * @param any aParams
   *        Any kind params to pass to the request function.
   * @return array
   *         The results given by all known syntax trees.
   */
  _call: function(aFunction, aSyntaxTreeIndex, ...aParams) {
    let results = [];
    let requestId = [aFunction, aSyntaxTreeIndex, aParams].toSource();

    if (this._cache.has(requestId)) {
      return this._cache.get(requestId);
    }

    let requestedTree = this._trees[aSyntaxTreeIndex];
    let targettedTrees = requestedTree ? [requestedTree] : this._trees;

    for (let syntaxTree of targettedTrees) {
      try {
        let parseResults = syntaxTree[aFunction].apply(syntaxTree, aParams);
        if (parseResults) {
          parseResults.sourceUrl = syntaxTree.url;
          parseResults.scriptLength = syntaxTree.length;
          parseResults.scriptOffset = syntaxTree.offset;
          results.push(parseResults);
        }
      } catch (e) {
        // Can't guarantee that the tree traversal logic is forever perfect :)
        // Language features may be added, in which case the recursive methods
        // need to be updated. If an exception is thrown here, file a bug.
        DevToolsUtils.reportException("Syntax tree visitor for " + this._url, e);
      }
    }
    this._cache.set(requestId, results);
    return results;
  },

  _trees: null,
  _cache: null
};

/**
 * A collection of AST nodes generated by the reflection API.
 *
 * @param object aNodes
 *        The AST nodes.
 * @param string aUrl
 *        The source url.
 * @param number aLength
 *        The total number of chars of the parsed script in the parent source.
 * @param number aOffset [optional]
 *        The char offset of the parsed script in the parent source.
 */
function SyntaxTree(aNodes, aUrl, aLength, aOffset = 0) {
  this.AST = aNodes;
  this.url = aUrl;
  this.length = aLength;
  this.offset = aOffset;
};

SyntaxTree.prototype = {
  /**
   * Gets the identifier at the specified location.
   *
   * @param number aLine
   *        The line in the source.
   * @param number aColumn
   *        The column in the source.
   * @param boolean aIgnoreLiterals
   *        Specifies if alone literals should be ignored.
   * @return object
   *         An object containing identifier information as { name, location,
   *         evalString } properties, or null if nothing is found.
   */
  getIdentifierAt: function(aLine, aColumn, aIgnoreLiterals) {
    let info = null;

    SyntaxTreeVisitor.walk(this.AST, {
      /**
       * Callback invoked for each identifier node.
       * @param Node aNode
       */
      onIdentifier: function(aNode) {
        if (ParserHelpers.nodeContainsPoint(aNode, aLine, aColumn)) {
          info = {
            name: aNode.name,
            location: ParserHelpers.getNodeLocation(aNode),
            evalString: ParserHelpers.getIdentifierEvalString(aNode)
          };

          // Abruptly halt walking the syntax tree.
          SyntaxTreeVisitor.break = true;
        }
      },

      /**
       * Callback invoked for each literal node.
       * @param Node aNode
       */
      onLiteral: function(aNode) {
        if (!aIgnoreLiterals) {
          this.onIdentifier(aNode);
        }
      },

      /**
       * Callback invoked for each 'this' node.
       * @param Node aNode
       */
      onThisExpression: function(aNode) {
        this.onIdentifier(aNode);
      }
    });

    return info;
  },

  /**
   * Searches for all function definitions (declarations and expressions)
   * whose names (or inferred names) contain a string.
   *
   * @param string aSubstring
   *        The string to be contained in the function name (or inferred name).
   *        Can be an empty string to match all functions.
   * @return array
   *         All the matching function declarations and expressions, as
   *         { functionName, functionLocation ... } object hashes.
   */
  getNamedFunctionDefinitions: function(aSubstring) {
    let lowerCaseToken = aSubstring.toLowerCase();
    let store = [];

    SyntaxTreeVisitor.walk(this.AST, {
      /**
       * Callback invoked for each function declaration node.
       * @param Node aNode
       */
      onFunctionDeclaration: function(aNode) {
        let functionName = aNode.id.name;
        if (functionName.toLowerCase().includes(lowerCaseToken)) {
          store.push({
            functionName: functionName,
            functionLocation: ParserHelpers.getNodeLocation(aNode)
          });
        }
      },

      /**
       * Callback invoked for each function expression node.
       * @param Node aNode
       */
      onFunctionExpression: function(aNode) {
        // Function expressions don't necessarily have a name.
        let functionName = aNode.id ? aNode.id.name : "";
        let functionLocation = ParserHelpers.getNodeLocation(aNode);

        // Infer the function's name from an enclosing syntax tree node.
        let inferredInfo = ParserHelpers.inferFunctionExpressionInfo(aNode);
        let inferredName = inferredInfo.name;
        let inferredChain = inferredInfo.chain;
        let inferredLocation = inferredInfo.loc;

        // Current node may be part of a larger assignment expression stack.
        if (aNode._parent.type == "AssignmentExpression") {
          this.onFunctionExpression(aNode._parent);
        }

        if ((functionName && functionName.toLowerCase().includes(lowerCaseToken)) ||
            (inferredName && inferredName.toLowerCase().includes(lowerCaseToken))) {
          store.push({
            functionName: functionName,
            functionLocation: functionLocation,
            inferredName: inferredName,
            inferredChain: inferredChain,
            inferredLocation: inferredLocation
          });
        }
      },

      /**
       * Callback invoked for each arrow expression node.
       * @param Node aNode
       */
      onArrowFunctionExpression: function(aNode) {
        // Infer the function's name from an enclosing syntax tree node.
        let inferredInfo = ParserHelpers.inferFunctionExpressionInfo(aNode);
        let inferredName = inferredInfo.name;
        let inferredChain = inferredInfo.chain;
        let inferredLocation = inferredInfo.loc;

        // Current node may be part of a larger assignment expression stack.
        if (aNode._parent.type == "AssignmentExpression") {
          this.onFunctionExpression(aNode._parent);
        }

        if (inferredName && inferredName.toLowerCase().includes(lowerCaseToken)) {
          store.push({
            inferredName: inferredName,
            inferredChain: inferredChain,
            inferredLocation: inferredLocation
          });
        }
      }
    });

    return store;
  },

  AST: null,
  url: "",
  length: 0,
  offset: 0
};

/**
 * Parser utility methods.
 */
var ParserHelpers = {
  /**
   * Gets the location information for a node. Not all nodes have a
   * location property directly attached, or the location information
   * is incorrect, in which cases it's accessible via the parent.
   *
   * @param Node aNode
   *        The node who's location needs to be retrieved.
   * @return object
   *         An object containing { line, column } information.
   */
  getNodeLocation: function(aNode) {
    if (aNode.type != "Identifier") {
      return aNode.loc;
    }
    // Work around the fact that some identifier nodes don't have the
    // correct location attached.
    let { loc: parentLocation, type: parentType } = aNode._parent;
    let { loc: nodeLocation } = aNode;
    if (!nodeLocation) {
      if (parentType == "FunctionDeclaration" ||
          parentType == "FunctionExpression") {
        // e.g. "function foo() {}" or "{ bar: function foo() {} }"
        // The location is unavailable for the identifier node "foo".
        let loc = Cu.cloneInto(parentLocation, {});
        loc.end.line = loc.start.line;
        loc.end.column = loc.start.column + aNode.name.length;
        return loc;
      }
      if (parentType == "MemberExpression") {
        // e.g. "foo.bar"
        // The location is unavailable for the identifier node "bar".
        let loc = Cu.cloneInto(parentLocation, {});
        loc.start.line = loc.end.line;
        loc.start.column = loc.end.column - aNode.name.length;
        return loc;
      }
      if (parentType == "LabeledStatement") {
        // e.g. label: ...
        // The location is unavailable for the identifier node "label".
        let loc = Cu.cloneInto(parentLocation, {});
        loc.end.line = loc.start.line;
        loc.end.column = loc.start.column + aNode.name.length;
        return loc;
      }
      if (parentType == "ContinueStatement" || parentType == "BreakStatement") {
        // e.g. continue label; or break label;
        // The location is unavailable for the identifier node "label".
        let loc = Cu.cloneInto(parentLocation, {});
        loc.start.line = loc.end.line;
        loc.start.column = loc.end.column - aNode.name.length;
        return loc;
      }
    } else {
      if (parentType == "VariableDeclarator") {
        // e.g. "let foo = 42"
        // The location incorrectly spans across the whole variable declaration,
        // not just the identifier node "foo".
        let loc = Cu.cloneInto(nodeLocation, {});
        loc.end.line = loc.start.line;
        loc.end.column = loc.start.column + aNode.name.length;
        return loc;
      }
    }
    return aNode.loc;
  },

  /**
   * Checks if a node's bounds contains a specified line.
   *
   * @param Node aNode
   *        The node's bounds used as reference.
   * @param number aLine
   *        The line number to check.
   * @return boolean
   *         True if the line and column is contained in the node's bounds.
   */
  nodeContainsLine: function(aNode, aLine) {
    let { start: s, end: e } = this.getNodeLocation(aNode);
    return s.line <= aLine && e.line >= aLine;
  },

  /**
   * Checks if a node's bounds contains a specified line and column.
   *
   * @param Node aNode
   *        The node's bounds used as reference.
   * @param number aLine
   *        The line number to check.
   * @param number aColumn
   *        The column number to check.
   * @return boolean
   *         True if the line and column is contained in the node's bounds.
   */
  nodeContainsPoint: function(aNode, aLine, aColumn) {
    let { start: s, end: e } = this.getNodeLocation(aNode);
    return s.line == aLine && e.line == aLine &&
           s.column <= aColumn && e.column >= aColumn;
  },

  /**
   * Try to infer a function expression's name & other details based on the
   * enclosing VariableDeclarator, AssignmentExpression or ObjectExpression.
   *
   * @param Node aNode
   *        The function expression node to get the name for.
   * @return object
   *         The inferred function name, or empty string can't infer the name,
   *         along with the chain (a generic "context", like a prototype chain)
   *         and location if available.
   */
  inferFunctionExpressionInfo: function(aNode) {
    let parent = aNode._parent;

    // A function expression may be defined in a variable declarator,
    // e.g. var foo = function(){}, in which case it is possible to infer
    // the variable name.
    if (parent.type == "VariableDeclarator") {
      return {
        name: parent.id.name,
        chain: null,
        loc: this.getNodeLocation(parent.id)
      };
    }

    // Function expressions can also be defined in assignment expressions,
    // e.g. foo = function(){} or foo.bar = function(){}, in which case it is
    // possible to infer the assignee name ("foo" and "bar" respectively).
    if (parent.type == "AssignmentExpression") {
      let propertyChain = this._getMemberExpressionPropertyChain(parent.left);
      let propertyLeaf = propertyChain.pop();
      return {
        name: propertyLeaf,
        chain: propertyChain,
        loc: this.getNodeLocation(parent.left)
      };
    }

    // If a function expression is defined in an object expression,
    // e.g. { foo: function(){} }, then it is possible to infer the name
    // from the corresponding property.
    if (parent.type == "ObjectExpression") {
      let propertyKey = this._getObjectExpressionPropertyKeyForValue(aNode);
      let propertyChain = this._getObjectExpressionPropertyChain(parent);
      let propertyLeaf = propertyKey.name;
      return {
        name: propertyLeaf,
        chain: propertyChain,
        loc: this.getNodeLocation(propertyKey)
      };
    }

    // Can't infer the function expression's name.
    return {
      name: "",
      chain: null,
      loc: null
    };
  },

  /**
   * Gets the name of an object expression's property to which a specified
   * value is assigned.
   *
   * Used for inferring function expression information and retrieving
   * an identifier evaluation string.
   *
   * For example, if aNode represents the "bar" identifier in a hypothetical
   * "{ foo: bar }" object expression, the returned node is the "foo" identifier.
   *
   * @param Node aNode
   *        The value node in an object expression.
   * @return object
   *         The key identifier node in the object expression.
   */
  _getObjectExpressionPropertyKeyForValue: function(aNode) {
    let parent = aNode._parent;
    if (parent.type != "ObjectExpression") {
      return null;
    }
    for (let property of parent.properties) {
      if (property.value == aNode) {
        return property.key;
      }
    }
  },

  /**
   * Gets an object expression's property chain to its parent
   * variable declarator or assignment expression, if available.
   *
   * Used for inferring function expression information and retrieving
   * an identifier evaluation string.
   *
   * For example, if aNode represents the "baz: {}" object expression in a
   * hypothetical "foo = { bar: { baz: {} } }" assignment expression, the
   * returned chain is ["foo", "bar", "baz"].
   *
   * @param Node aNode
   *        The object expression node to begin the scan from.
   * @param array aStore [optional]
   *        The chain to store the nodes into.
   * @return array
   *         The chain to the parent variable declarator, as strings.
   */
  _getObjectExpressionPropertyChain: function(aNode, aStore = []) {
    switch (aNode.type) {
      case "ObjectExpression":
        this._getObjectExpressionPropertyChain(aNode._parent, aStore);
        let propertyKey = this._getObjectExpressionPropertyKeyForValue(aNode);
        if (propertyKey) {
          aStore.push(propertyKey.name);
        }
        break;
      // Handle "var foo = { ... }" variable declarators.
      case "VariableDeclarator":
        aStore.push(aNode.id.name);
        break;
      // Handle "foo.bar = { ... }" assignment expressions, since they're
      // commonly used when defining an object's prototype methods; e.g:
      // "Foo.prototype = { ... }".
      case "AssignmentExpression":
        this._getMemberExpressionPropertyChain(aNode.left, aStore);
        break;
      // Additionally handle stuff like "foo = bar.baz({ ... })", because it's
      // commonly used in prototype-based inheritance in many libraries; e.g:
      // "Foo = Bar.extend({ ... })".
      case "NewExpression":
      case "CallExpression":
        this._getObjectExpressionPropertyChain(aNode._parent, aStore);
        break;
    }
    return aStore;
  },

  /**
   * Gets a member expression's property chain.
   *
   * Used for inferring function expression information and retrieving
   * an identifier evaluation string.
   *
   * For example, if aNode represents a hypothetical "foo.bar.baz"
   * member expression, the returned chain ["foo", "bar", "baz"].
   *
   * More complex expressions like foo.bar().baz are intentionally not handled.
   *
   * @param Node aNode
   *        The member expression node to begin the scan from.
   * @param array aStore [optional]
   *        The chain to store the nodes into.
   * @return array
   *         The full member chain, as strings.
   */
  _getMemberExpressionPropertyChain: function(aNode, aStore = []) {
    switch (aNode.type) {
      case "MemberExpression":
        this._getMemberExpressionPropertyChain(aNode.object, aStore);
        this._getMemberExpressionPropertyChain(aNode.property, aStore);
        break;
      case "ThisExpression":
        aStore.push("this");
        break;
      case "Identifier":
        aStore.push(aNode.name);
        break;
    }
    return aStore;
  },

  /**
   * Returns an evaluation string which can be used to obtain the
   * current value for the respective identifier.
   *
   * @param Node aNode
   *        The leaf node (e.g. Identifier, Literal) to begin the scan from.
   * @return string
   *         The corresponding evaluation string, or empty string if
   *         the specified leaf node can't be used.
   */
  getIdentifierEvalString: function(aNode) {
    switch (aNode._parent.type) {
      case "ObjectExpression":
        // If the identifier is the actual property value, it can be used
        // directly as an evaluation string. Otherwise, construct the property
        // access chain, since the value might have changed.
        if (!this._getObjectExpressionPropertyKeyForValue(aNode)) {
          let propertyChain = this._getObjectExpressionPropertyChain(aNode._parent);
          let propertyLeaf = aNode.name;
          return [...propertyChain, propertyLeaf].join(".");
        }
        break;
      case "MemberExpression":
        // Make sure this is a property identifier, not the parent object.
        if (aNode._parent.property == aNode) {
          return this._getMemberExpressionPropertyChain(aNode._parent).join(".");
        }
        break;
    }
    switch (aNode.type) {
      case "ThisExpression":
        return "this";
      case "Identifier":
        return aNode.name;
      case "Literal":
        return uneval(aNode.value);
      default:
        return "";
    }
  }
};

/**
 * A visitor for a syntax tree generated by the reflection API.
 * See https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API.
 *
 * All node types implement the following interface:
 * interface Node {
 *   type: string;
 *   loc: SourceLocation | null;
 * }
 */
var SyntaxTreeVisitor = {
  /**
   * Walks a syntax tree.
   *
   * @param object aTree
   *        The AST nodes generated by the reflection API
   * @param object aCallbacks
   *        A map of all the callbacks to invoke when passing through certain
   *        types of noes (e.g: onFunctionDeclaration, onBlockStatement etc.).
   */
  walk: function(aTree, aCallbacks) {
    this.break = false;
    this[aTree.type](aTree, aCallbacks);
  },

  /**
   * Filters all the nodes in this syntax tree based on a predicate.
   *
   * @param object aTree
   *        The AST nodes generated by the reflection API
   * @param function aPredicate
   *        The predicate ran on each node.
   * @return array
   *         An array of nodes validating the predicate.
   */
  filter: function(aTree, aPredicate) {
    let store = [];
    this.walk(aTree, { onNode: e => { if (aPredicate(e)) store.push(e); } });
    return store;
  },

  /**
   * A flag checked on each node in the syntax tree. If true, walking is
   * abruptly halted.
   */
  break: false,

  /**
   * A complete program source tree.
   *
   * interface Program <: Node {
   *   type: "Program";
   *   body: [ Statement ];
   * }
   */
  Program: function(aNode, aCallbacks) {
    if (aCallbacks.onProgram) {
      aCallbacks.onProgram(aNode);
    }
    for (let statement of aNode.body) {
      this[statement.type](statement, aNode, aCallbacks);
    }
  },

  /**
   * Any statement.
   *
   * interface Statement <: Node { }
   */
  Statement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onStatement) {
      aCallbacks.onStatement(aNode);
    }
  },

  /**
   * An empty statement, i.e., a solitary semicolon.
   *
   * interface EmptyStatement <: Statement {
   *   type: "EmptyStatement";
   * }
   */
  EmptyStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onEmptyStatement) {
      aCallbacks.onEmptyStatement(aNode);
    }
  },

  /**
   * A block statement, i.e., a sequence of statements surrounded by braces.
   *
   * interface BlockStatement <: Statement {
   *   type: "BlockStatement";
   *   body: [ Statement ];
   * }
   */
  BlockStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onBlockStatement) {
      aCallbacks.onBlockStatement(aNode);
    }
    for (let statement of aNode.body) {
      this[statement.type](statement, aNode, aCallbacks);
    }
  },

  /**
   * An expression statement, i.e., a statement consisting of a single expression.
   *
   * interface ExpressionStatement <: Statement {
   *   type: "ExpressionStatement";
   *   expression: Expression;
   * }
   */
  ExpressionStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onExpressionStatement) {
      aCallbacks.onExpressionStatement(aNode);
    }
    this[aNode.expression.type](aNode.expression, aNode, aCallbacks);
  },

  /**
   * An if statement.
   *
   * interface IfStatement <: Statement {
   *   type: "IfStatement";
   *   test: Expression;
   *   consequent: Statement;
   *   alternate: Statement | null;
   * }
   */
  IfStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onIfStatement) {
      aCallbacks.onIfStatement(aNode);
    }
    this[aNode.test.type](aNode.test, aNode, aCallbacks);
    this[aNode.consequent.type](aNode.consequent, aNode, aCallbacks);
    if (aNode.alternate) {
      this[aNode.alternate.type](aNode.alternate, aNode, aCallbacks);
    }
  },

  /**
   * A labeled statement, i.e., a statement prefixed by a break/continue label.
   *
   * interface LabeledStatement <: Statement {
   *   type: "LabeledStatement";
   *   label: Identifier;
   *   body: Statement;
   * }
   */
  LabeledStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onLabeledStatement) {
      aCallbacks.onLabeledStatement(aNode);
    }
    this[aNode.label.type](aNode.label, aNode, aCallbacks);
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
  },

  /**
   * A break statement.
   *
   * interface BreakStatement <: Statement {
   *   type: "BreakStatement";
   *   label: Identifier | null;
   * }
   */
  BreakStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onBreakStatement) {
      aCallbacks.onBreakStatement(aNode);
    }
    if (aNode.label) {
      this[aNode.label.type](aNode.label, aNode, aCallbacks);
    }
  },

  /**
   * A continue statement.
   *
   * interface ContinueStatement <: Statement {
   *   type: "ContinueStatement";
   *   label: Identifier | null;
   * }
   */
  ContinueStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onContinueStatement) {
      aCallbacks.onContinueStatement(aNode);
    }
    if (aNode.label) {
      this[aNode.label.type](aNode.label, aNode, aCallbacks);
    }
  },

  /**
   * A with statement.
   *
   * interface WithStatement <: Statement {
   *   type: "WithStatement";
   *   object: Expression;
   *   body: Statement;
   * }
   */
  WithStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onWithStatement) {
      aCallbacks.onWithStatement(aNode);
    }
    this[aNode.object.type](aNode.object, aNode, aCallbacks);
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
  },

  /**
   * A switch statement. The lexical flag is metadata indicating whether the
   * switch statement contains any unnested let declarations (and therefore
   * introduces a new lexical scope).
   *
   * interface SwitchStatement <: Statement {
   *   type: "SwitchStatement";
   *   discriminant: Expression;
   *   cases: [ SwitchCase ];
   *   lexical: boolean;
   * }
   */
  SwitchStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onSwitchStatement) {
      aCallbacks.onSwitchStatement(aNode);
    }
    this[aNode.discriminant.type](aNode.discriminant, aNode, aCallbacks);
    for (let _case of aNode.cases) {
      this[_case.type](_case, aNode, aCallbacks);
    }
  },

  /**
   * A return statement.
   *
   * interface ReturnStatement <: Statement {
   *   type: "ReturnStatement";
   *   argument: Expression | null;
   * }
   */
  ReturnStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onReturnStatement) {
      aCallbacks.onReturnStatement(aNode);
    }
    if (aNode.argument) {
      this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
    }
  },

  /**
   * A throw statement.
   *
   * interface ThrowStatement <: Statement {
   *   type: "ThrowStatement";
   *   argument: Expression;
   * }
   */
  ThrowStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onThrowStatement) {
      aCallbacks.onThrowStatement(aNode);
    }
    this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
  },

  /**
   * A try statement.
   *
   * interface TryStatement <: Statement {
   *   type: "TryStatement";
   *   block: BlockStatement;
   *   handler: CatchClause | null;
   *   guardedHandlers: [ CatchClause ];
   *   finalizer: BlockStatement | null;
   * }
   */
  TryStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onTryStatement) {
      aCallbacks.onTryStatement(aNode);
    }
    this[aNode.block.type](aNode.block, aNode, aCallbacks);
    if (aNode.handler) {
      this[aNode.handler.type](aNode.handler, aNode, aCallbacks);
    }
    for (let guardedHandler of aNode.guardedHandlers) {
      this[guardedHandler.type](guardedHandler, aNode, aCallbacks);
    }
    if (aNode.finalizer) {
      this[aNode.finalizer.type](aNode.finalizer, aNode, aCallbacks);
    }
  },

  /**
   * A while statement.
   *
   * interface WhileStatement <: Statement {
   *   type: "WhileStatement";
   *   test: Expression;
   *   body: Statement;
   * }
   */
  WhileStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onWhileStatement) {
      aCallbacks.onWhileStatement(aNode);
    }
    this[aNode.test.type](aNode.test, aNode, aCallbacks);
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
  },

  /**
   * A do/while statement.
   *
   * interface DoWhileStatement <: Statement {
   *   type: "DoWhileStatement";
   *   body: Statement;
   *   test: Expression;
   * }
   */
  DoWhileStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onDoWhileStatement) {
      aCallbacks.onDoWhileStatement(aNode);
    }
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
    this[aNode.test.type](aNode.test, aNode, aCallbacks);
  },

  /**
   * A for statement.
   *
   * interface ForStatement <: Statement {
   *   type: "ForStatement";
   *   init: VariableDeclaration | Expression | null;
   *   test: Expression | null;
   *   update: Expression | null;
   *   body: Statement;
   * }
   */
  ForStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onForStatement) {
      aCallbacks.onForStatement(aNode);
    }
    if (aNode.init) {
      this[aNode.init.type](aNode.init, aNode, aCallbacks);
    }
    if (aNode.test) {
      this[aNode.test.type](aNode.test, aNode, aCallbacks);
    }
    if (aNode.update) {
      this[aNode.update.type](aNode.update, aNode, aCallbacks);
    }
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
  },

  /**
   * A for/in statement, or, if each is true, a for each/in statement.
   *
   * interface ForInStatement <: Statement {
   *   type: "ForInStatement";
   *   left: VariableDeclaration | Expression;
   *   right: Expression;
   *   body: Statement;
   *   each: boolean;
   * }
   */
  ForInStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onForInStatement) {
      aCallbacks.onForInStatement(aNode);
    }
    this[aNode.left.type](aNode.left, aNode, aCallbacks);
    this[aNode.right.type](aNode.right, aNode, aCallbacks);
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
  },

  /**
   * A for/of statement.
   *
   * interface ForOfStatement <: Statement {
   *   type: "ForOfStatement";
   *   left: VariableDeclaration | Expression;
   *   right: Expression;
   *   body: Statement;
   * }
   */
  ForOfStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onForOfStatement) {
      aCallbacks.onForOfStatement(aNode);
    }
    this[aNode.left.type](aNode.left, aNode, aCallbacks);
    this[aNode.right.type](aNode.right, aNode, aCallbacks);
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
  },

  /**
   * A let statement.
   *
   * interface LetStatement <: Statement {
   *   type: "LetStatement";
   *   head: [ { id: Pattern, init: Expression | null } ];
   *   body: Statement;
   * }
   */
  LetStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onLetStatement) {
      aCallbacks.onLetStatement(aNode);
    }
    for (let { id, init } of aNode.head) {
      this[id.type](id, aNode, aCallbacks);
      if (init) {
        this[init.type](init, aNode, aCallbacks);
      }
    }
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
  },

  /**
   * A debugger statement.
   *
   * interface DebuggerStatement <: Statement {
   *   type: "DebuggerStatement";
   * }
   */
  DebuggerStatement: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onDebuggerStatement) {
      aCallbacks.onDebuggerStatement(aNode);
    }
  },

  /**
   * Any declaration node. Note that declarations are considered statements;
   * this is because declarations can appear in any statement context in the
   * language recognized by the SpiderMonkey parser.
   *
   * interface Declaration <: Statement { }
   */
  Declaration: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onDeclaration) {
      aCallbacks.onDeclaration(aNode);
    }
  },

  /**
   * A function declaration.
   *
   * interface FunctionDeclaration <: Function, Declaration {
   *   type: "FunctionDeclaration";
   *   id: Identifier;
   *   params: [ Pattern ];
   *   defaults: [ Expression ];
   *   rest: Identifier | null;
   *   body: BlockStatement | Expression;
   *   generator: boolean;
   *   expression: boolean;
   * }
   */
  FunctionDeclaration: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onFunctionDeclaration) {
      aCallbacks.onFunctionDeclaration(aNode);
    }
    this[aNode.id.type](aNode.id, aNode, aCallbacks);
    for (let param of aNode.params) {
      this[param.type](param, aNode, aCallbacks);
    }
    for (let _default of aNode.defaults) {
      this[_default.type](_default, aNode, aCallbacks);
    }
    if (aNode.rest) {
      this[aNode.rest.type](aNode.rest, aNode, aCallbacks);
    }
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
  },

  /**
   * A variable declaration, via one of var, let, or const.
   *
   * interface VariableDeclaration <: Declaration {
   *   type: "VariableDeclaration";
   *   declarations: [ VariableDeclarator ];
   *   kind: "var" | "let" | "const";
   * }
   */
  VariableDeclaration: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onVariableDeclaration) {
      aCallbacks.onVariableDeclaration(aNode);
    }
    for (let declaration of aNode.declarations) {
      this[declaration.type](declaration, aNode, aCallbacks);
    }
  },

  /**
   * A variable declarator.
   *
   * interface VariableDeclarator <: Node {
   *   type: "VariableDeclarator";
   *   id: Pattern;
   *   init: Expression | null;
   * }
   */
  VariableDeclarator: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onVariableDeclarator) {
      aCallbacks.onVariableDeclarator(aNode);
    }
    this[aNode.id.type](aNode.id, aNode, aCallbacks);
    if (aNode.init) {
      this[aNode.init.type](aNode.init, aNode, aCallbacks);
    }
  },

  /**
   * Any expression node. Since the left-hand side of an assignment may be any
   * expression in general, an expression can also be a pattern.
   *
   * interface Expression <: Node, Pattern { }
   */
  Expression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onExpression) {
      aCallbacks.onExpression(aNode);
    }
  },

  /**
   * A this expression.
   *
   * interface ThisExpression <: Expression {
   *   type: "ThisExpression";
   * }
   */
  ThisExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onThisExpression) {
      aCallbacks.onThisExpression(aNode);
    }
  },

  /**
   * An array expression.
   *
   * interface ArrayExpression <: Expression {
   *   type: "ArrayExpression";
   *   elements: [ Expression | null ];
   * }
   */
  ArrayExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onArrayExpression) {
      aCallbacks.onArrayExpression(aNode);
    }
    for (let element of aNode.elements) {
      // TODO: remove the typeof check when support for SpreadExpression is
      // added (bug 890913).
      if (element && typeof this[element.type] == "function") {
        this[element.type](element, aNode, aCallbacks);
      }
    }
  },

  /**
   * An object expression. A literal property in an object expression can have
   * either a string or number as its value. Ordinary property initializers
   * have a kind value "init"; getters and setters have the kind values "get"
   * and "set", respectively.
   *
   * interface ObjectExpression <: Expression {
   *   type: "ObjectExpression";
   *   properties: [ { key: Literal | Identifier,
   *                   value: Expression,
   *                   kind: "init" | "get" | "set" } ];
   * }
   */
  ObjectExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onObjectExpression) {
      aCallbacks.onObjectExpression(aNode);
    }
    for (let { key, value } of aNode.properties) {
      this[key.type](key, aNode, aCallbacks);
      this[value.type](value, aNode, aCallbacks);
    }
  },

  /**
   * A function expression.
   *
   * interface FunctionExpression <: Function, Expression {
   *   type: "FunctionExpression";
   *   id: Identifier | null;
   *   params: [ Pattern ];
   *   defaults: [ Expression ];
   *   rest: Identifier | null;
   *   body: BlockStatement | Expression;
   *   generator: boolean;
   *   expression: boolean;
   * }
   */
  FunctionExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onFunctionExpression) {
      aCallbacks.onFunctionExpression(aNode);
    }
    if (aNode.id) {
      this[aNode.id.type](aNode.id, aNode, aCallbacks);
    }
    for (let param of aNode.params) {
      this[param.type](param, aNode, aCallbacks);
    }
    for (let _default of aNode.defaults) {
      this[_default.type](_default, aNode, aCallbacks);
    }
    if (aNode.rest) {
      this[aNode.rest.type](aNode.rest, aNode, aCallbacks);
    }
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
  },

  /**
   * An arrow expression.
   *
   * interface ArrowFunctionExpression <: Function, Expression {
   *   type: "ArrowFunctionExpression";
   *   params: [ Pattern ];
   *   defaults: [ Expression ];
   *   rest: Identifier | null;
   *   body: BlockStatement | Expression;
   *   generator: boolean;
   *   expression: boolean;
   * }
   */
  ArrowFunctionExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onArrowFunctionExpression) {
      aCallbacks.onArrowFunctionExpression(aNode);
    }
    for (let param of aNode.params) {
      this[param.type](param, aNode, aCallbacks);
    }
    for (let _default of aNode.defaults) {
      this[_default.type](_default, aNode, aCallbacks);
    }
    if (aNode.rest) {
      this[aNode.rest.type](aNode.rest, aNode, aCallbacks);
    }
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
  },

  /**
   * A sequence expression, i.e., a comma-separated sequence of expressions.
   *
   * interface SequenceExpression <: Expression {
   *   type: "SequenceExpression";
   *   expressions: [ Expression ];
   * }
   */
  SequenceExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onSequenceExpression) {
      aCallbacks.onSequenceExpression(aNode);
    }
    for (let expression of aNode.expressions) {
      this[expression.type](expression, aNode, aCallbacks);
    }
  },

  /**
   * A unary operator expression.
   *
   * interface UnaryExpression <: Expression {
   *   type: "UnaryExpression";
   *   operator: UnaryOperator;
   *   prefix: boolean;
   *   argument: Expression;
   * }
   */
  UnaryExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onUnaryExpression) {
      aCallbacks.onUnaryExpression(aNode);
    }
    this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
  },

  /**
   * A binary operator expression.
   *
   * interface BinaryExpression <: Expression {
   *   type: "BinaryExpression";
   *   operator: BinaryOperator;
   *   left: Expression;
   *   right: Expression;
   * }
   */
  BinaryExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onBinaryExpression) {
      aCallbacks.onBinaryExpression(aNode);
    }
    this[aNode.left.type](aNode.left, aNode, aCallbacks);
    this[aNode.right.type](aNode.right, aNode, aCallbacks);
  },

  /**
   * An assignment operator expression.
   *
   * interface AssignmentExpression <: Expression {
   *   type: "AssignmentExpression";
   *   operator: AssignmentOperator;
   *   left: Expression;
   *   right: Expression;
   * }
   */
  AssignmentExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onAssignmentExpression) {
      aCallbacks.onAssignmentExpression(aNode);
    }
    this[aNode.left.type](aNode.left, aNode, aCallbacks);
    this[aNode.right.type](aNode.right, aNode, aCallbacks);
  },

  /**
   * An update (increment or decrement) operator expression.
   *
   * interface UpdateExpression <: Expression {
   *   type: "UpdateExpression";
   *   operator: UpdateOperator;
   *   argument: Expression;
   *   prefix: boolean;
   * }
   */
  UpdateExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onUpdateExpression) {
      aCallbacks.onUpdateExpression(aNode);
    }
    this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
  },

  /**
   * A logical operator expression.
   *
   * interface LogicalExpression <: Expression {
   *   type: "LogicalExpression";
   *   operator: LogicalOperator;
   *   left: Expression;
   *   right: Expression;
   * }
   */
  LogicalExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onLogicalExpression) {
      aCallbacks.onLogicalExpression(aNode);
    }
    this[aNode.left.type](aNode.left, aNode, aCallbacks);
    this[aNode.right.type](aNode.right, aNode, aCallbacks);
  },

  /**
   * A conditional expression, i.e., a ternary ?/: expression.
   *
   * interface ConditionalExpression <: Expression {
   *   type: "ConditionalExpression";
   *   test: Expression;
   *   alternate: Expression;
   *   consequent: Expression;
   * }
   */
  ConditionalExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onConditionalExpression) {
      aCallbacks.onConditionalExpression(aNode);
    }
    this[aNode.test.type](aNode.test, aNode, aCallbacks);
    this[aNode.alternate.type](aNode.alternate, aNode, aCallbacks);
    this[aNode.consequent.type](aNode.consequent, aNode, aCallbacks);
  },

  /**
   * A new expression.
   *
   * interface NewExpression <: Expression {
   *   type: "NewExpression";
   *   callee: Expression;
   *   arguments: [ Expression | null ];
   * }
   */
  NewExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onNewExpression) {
      aCallbacks.onNewExpression(aNode);
    }
    this[aNode.callee.type](aNode.callee, aNode, aCallbacks);
    for (let argument of aNode.arguments) {
      if (argument) {
        this[argument.type](argument, aNode, aCallbacks);
      }
    }
  },

  /**
   * A function or method call expression.
   *
   * interface CallExpression <: Expression {
   *   type: "CallExpression";
   *   callee: Expression;
   *   arguments: [ Expression | null ];
   * }
   */
  CallExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onCallExpression) {
      aCallbacks.onCallExpression(aNode);
    }
    this[aNode.callee.type](aNode.callee, aNode, aCallbacks);
    for (let argument of aNode.arguments) {
      if (argument) {
        this[argument.type](argument, aNode, aCallbacks);
      }
    }
  },

  /**
   * A member expression. If computed is true, the node corresponds to a
   * computed e1[e2] expression and property is an Expression. If computed is
   * false, the node corresponds to a static e1.x expression and property is an
   * Identifier.
   *
   * interface MemberExpression <: Expression {
   *   type: "MemberExpression";
   *   object: Expression;
   *   property: Identifier | Expression;
   *   computed: boolean;
   * }
   */
  MemberExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onMemberExpression) {
      aCallbacks.onMemberExpression(aNode);
    }
    this[aNode.object.type](aNode.object, aNode, aCallbacks);
    this[aNode.property.type](aNode.property, aNode, aCallbacks);
  },

  /**
   * A yield expression.
   *
   * interface YieldExpression <: Expression {
   *   argument: Expression | null;
   * }
   */
  YieldExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onYieldExpression) {
      aCallbacks.onYieldExpression(aNode);
    }
    if (aNode.argument) {
      this[aNode.argument.type](aNode.argument, aNode, aCallbacks);
    }
  },

  /**
   * An array comprehension. The blocks array corresponds to the sequence of
   * for and for each blocks. The optional filter expression corresponds to the
   * final if clause, if present.
   *
   * interface ComprehensionExpression <: Expression {
   *   body: Expression;
   *   blocks: [ ComprehensionBlock ];
   *   filter: Expression | null;
   * }
   */
  ComprehensionExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onComprehensionExpression) {
      aCallbacks.onComprehensionExpression(aNode);
    }
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
    for (let block of aNode.blocks) {
      this[block.type](block, aNode, aCallbacks);
    }
    if (aNode.filter) {
      this[aNode.filter.type](aNode.filter, aNode, aCallbacks);
    }
  },

  /**
   * A generator expression. As with array comprehensions, the blocks array
   * corresponds to the sequence of for and for each blocks, and the optional
   * filter expression corresponds to the final if clause, if present.
   *
   * interface GeneratorExpression <: Expression {
   *   body: Expression;
   *   blocks: [ ComprehensionBlock ];
   *   filter: Expression | null;
   * }
   */
  GeneratorExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onGeneratorExpression) {
      aCallbacks.onGeneratorExpression(aNode);
    }
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
    for (let block of aNode.blocks) {
      this[block.type](block, aNode, aCallbacks);
    }
    if (aNode.filter) {
      this[aNode.filter.type](aNode.filter, aNode, aCallbacks);
    }
  },

  /**
   * A graph expression, aka "sharp literal," such as #1={ self: #1# }.
   *
   * interface GraphExpression <: Expression {
   *   index: uint32;
   *   expression: Literal;
   * }
   */
  GraphExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onGraphExpression) {
      aCallbacks.onGraphExpression(aNode);
    }
    this[aNode.expression.type](aNode.expression, aNode, aCallbacks);
  },

  /**
   * A graph index expression, aka "sharp variable," such as #1#.
   *
   * interface GraphIndexExpression <: Expression {
   *   index: uint32;
   * }
   */
  GraphIndexExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onGraphIndexExpression) {
      aCallbacks.onGraphIndexExpression(aNode);
    }
  },

  /**
   * A let expression.
   *
   * interface LetExpression <: Expression {
   *   type: "LetExpression";
   *   head: [ { id: Pattern, init: Expression | null } ];
   *   body: Expression;
   * }
   */
  LetExpression: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onLetExpression) {
      aCallbacks.onLetExpression(aNode);
    }
    for (let { id, init } of aNode.head) {
      this[id.type](id, aNode, aCallbacks);
      if (init) {
        this[init.type](init, aNode, aCallbacks);
      }
    }
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
  },

  /**
   * Any pattern.
   *
   * interface Pattern <: Node { }
   */
  Pattern: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onPattern) {
      aCallbacks.onPattern(aNode);
    }
  },

  /**
   * An object-destructuring pattern. A literal property in an object pattern
   * can have either a string or number as its value.
   *
   * interface ObjectPattern <: Pattern {
   *   type: "ObjectPattern";
   *   properties: [ { key: Literal | Identifier, value: Pattern } ];
   * }
   */
  ObjectPattern: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onObjectPattern) {
      aCallbacks.onObjectPattern(aNode);
    }
    for (let { key, value } of aNode.properties) {
      this[key.type](key, aNode, aCallbacks);
      this[value.type](value, aNode, aCallbacks);
    }
  },

  /**
   * An array-destructuring pattern.
   *
   * interface ArrayPattern <: Pattern {
   *   type: "ArrayPattern";
   *   elements: [ Pattern | null ];
   * }
   */
  ArrayPattern: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onArrayPattern) {
      aCallbacks.onArrayPattern(aNode);
    }
    for (let element of aNode.elements) {
      if (element) {
        this[element.type](element, aNode, aCallbacks);
      }
    }
  },

  /**
   * A case (if test is an Expression) or default (if test is null) clause in
   * the body of a switch statement.
   *
   * interface SwitchCase <: Node {
   *   type: "SwitchCase";
   *   test: Expression | null;
   *   consequent: [ Statement ];
   * }
   */
  SwitchCase: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onSwitchCase) {
      aCallbacks.onSwitchCase(aNode);
    }
    if (aNode.test) {
      this[aNode.test.type](aNode.test, aNode, aCallbacks);
    }
    for (let consequent of aNode.consequent) {
      this[consequent.type](consequent, aNode, aCallbacks);
    }
  },

  /**
   * A catch clause following a try block. The optional guard property
   * corresponds to the optional expression guard on the bound variable.
   *
   * interface CatchClause <: Node {
   *   type: "CatchClause";
   *   param: Pattern;
   *   guard: Expression | null;
   *   body: BlockStatement;
   * }
   */
  CatchClause: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onCatchClause) {
      aCallbacks.onCatchClause(aNode);
    }
    this[aNode.param.type](aNode.param, aNode, aCallbacks);
    if (aNode.guard) {
      this[aNode.guard.type](aNode.guard, aNode, aCallbacks);
    }
    this[aNode.body.type](aNode.body, aNode, aCallbacks);
  },

  /**
   * A for or for each block in an array comprehension or generator expression.
   *
   * interface ComprehensionBlock <: Node {
   *   left: Pattern;
   *   right: Expression;
   *   each: boolean;
   * }
   */
  ComprehensionBlock: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onComprehensionBlock) {
      aCallbacks.onComprehensionBlock(aNode);
    }
    this[aNode.left.type](aNode.left, aNode, aCallbacks);
    this[aNode.right.type](aNode.right, aNode, aCallbacks);
  },

  /**
   * An identifier. Note that an identifier may be an expression or a
   * destructuring pattern.
   *
   * interface Identifier <: Node, Expression, Pattern {
   *   type: "Identifier";
   *   name: string;
   * }
   */
  Identifier: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onIdentifier) {
      aCallbacks.onIdentifier(aNode);
    }
  },

  /**
   * A literal token. Note that a literal can be an expression.
   *
   * interface Literal <: Node, Expression {
   *   type: "Literal";
   *   value: string | boolean | null | number | RegExp;
   * }
   */
  Literal: function(aNode, aParent, aCallbacks) {
    aNode._parent = aParent;

    if (this.break) {
      return;
    }
    if (aCallbacks.onNode) {
      if (aCallbacks.onNode(aNode, aParent) === false) {
        return;
      }
    }
    if (aCallbacks.onLiteral) {
      aCallbacks.onLiteral(aNode);
    }
  }
};

XPCOMUtils.defineLazyGetter(Parser, "reflectionAPI", () => Reflect);
