Home Manual Reference Source Repository

src/ASTWalker.js

/**
 * ASTWalker - Provides a simple AST traversal utility that traverses all nodes / children regardless of type.
 *
 * A callback object is provided in `traverse` which may contain two methods `enterNode` and `exitNode` which are
 * invoked with the current node and the parent node respectively when entering and exiting a given node.
 *
 * `enterNode` may return a array of strings which provide a set of children keys to ignore.
 *
 * `enterNode` may also return null to skip traversing children keys entirely.
 */
export default class ASTWalker
{
   /**
    * Traverses the ast tree provided and invokes `callbacks.enterNode` / `callbacks.exitNode`
    *
    * @param {object|Array}   ast - An AST Tree object hash or an array of nodes.
    * @param {object}         callbacks - An object hash containing a function for `enterNode` and / or `exitNode` keys.
    */
   traverse(ast, callbacks)
   {
      if (typeof callbacks !== 'object') { throw new TypeError('Invalid callbacks'); }
      if (typeof callbacks.enterNode !== 'function' && typeof callbacks.exitNode !== 'function')
      {
         throw new TypeError('Invalid callbacks - missing `enterNode` and / or `exitNode`.');
      }

      if (Array.isArray(ast))
      {
         this._visitNodes(ast, undefined, callbacks);
      }
      else if (typeof ast === 'object')
      {
         this._visitNode(ast, undefined, callbacks);
      }
      else
      {
         throw new TypeError('Invalid syntax tree');
      }
   }

   /**
    * Performs the main node visit invoking the callbacks for entering / exiting the node.
    *
    * `enterNode` may return an array of strings indicating children keys to ignore / skip. If `enterNode` returns
    * `null` children nodes are skipped entirely.
    *
    * @param {object}   node - AST node being visited.
    * @param {object}   parent - The parent node.
    * @param {object}   callbacks - An object hash containing a function for `enterNode` and / or `exitNode` keys.
    *
    * @private
    */
   _visitNode(node, parent, callbacks)
   {
      if (node !== null && typeof node === 'object' && typeof node.type === 'string')
      {
         const ignoreNodeKeys = typeof callbacks.enterNode === 'function' ? callbacks.enterNode(node, parent) : [];

         // If `enterNode` returns null then traversal of children is aborted.
         if (ignoreNodeKeys !== null) { this._visitChildren(node, ignoreNodeKeys, callbacks); }

         if (typeof callbacks.exitNode === 'function') { callbacks.exitNode(node, parent); }
      }
   }

   /**
    * Visits all nodes in the given array.
    *
    * @param {Array}    nodes - An array of nodes to visit.
    * @param {object}   parent - The parent node.
    * @param {object}   callbacks - An object hash containing a function for `enterNode` and / or `exitNode` keys.
    *
    * @private
    */
   _visitNodes(nodes, parent, callbacks)
   {
      nodes.forEach((node) => { this._visitNode(node, parent, callbacks); }, this);
   }

   /**
    * Visits all children nodes from a given node.
    *
    * @param {object}   node - Current AST node being visited.
    * @param {Array}    ignoreNodeKeys - A set of keys to ignore.
    * @param {object}   callbacks - An object hash containing a function for `enterNode` and / or `exitNode` keys.
    *
    * @private
    */
   _visitChildren(node, ignoreNodeKeys, callbacks)
   {
      ignoreNodeKeys = Array.isArray(ignoreNodeKeys) ? ignoreNodeKeys : [];

      // Visit all node keys which are an array or an object.
      Object.keys(node).forEach((key) =>
      {
         // Potentially ignore the given key if it is in the `ignoreNodeKeys` array.
         if (ignoreNodeKeys.indexOf(key) >= 0) { return; }

         if (Array.isArray(node[key]) || typeof node[key] === 'object')
         {
            this._visitChild(node[key], node, callbacks);
         }
      });
   }

   /**
    * Visits all children nodes from the given child object or array.
    *
    * @param {Array|object}   child - Child key to visit.
    * @param {object}         parent - The parent node.
    * @param {object}         callbacks - An object hash containing a function for `enterNode` and / or `exitNode` keys.
    *
    * @private
    */
   _visitChild(child, parent, callbacks)
   {
      const visitor = Array.isArray(child) ? this._visitNodes : this._visitNode;
      visitor.call(this, child, parent, callbacks);
   }
}