All files / src/language ast-walker.ts

100% Statements 54/54
95.83% Branches 23/24
100% Functions 8/8
100% Lines 54/54

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166                                                                                            1x                     16x 64x 64x 64x                 16x 3x 3x 3x 3x 3x 3x 11x 7x 7x 11x 11x 3x 3x 3x                       16x 9x 9x 9x 9x                 16x 3x 3x 3x                 16x 2x 2x       16x   136x 136x     135x     136x 72x 72x     135x     136x 136x   16x   12x 8x 8x   4x     12x 3x 1x 1x 3x     3x 12x    
import type { SpelNodeImpl } from '../ast/spel-node.js';
import type { NodeType } from './node-type.js';
 
/**
 * Visitor interface for SpEL AST traversal.
 *
 * Usage pattern:
 * ```typescript
 * AstWalker.walk(ast, {
 *   enterNode(node, ancestors) {
 *     // Called before traversing children.
 *     // Return false to skip this node's subtree.
 *     return true;
 *   },
 *   leaveNode(node, ancestors) {
 *     // Called after all children have been traversed.
 *   },
 * });
 * ```
 *
 * The `ancestors` parameter provides the full path from the root node
 * to the current node's parent (current node NOT included).
 */
export interface AstVisitor {
  /**
   * Called when entering a node (pre-order).
   * @param node The current AST node
   * @param ancestors Path from root to parent (current node NOT included)
   * @returns `false` to skip traversing this node's children, otherwise continue
   */
  enterNode?: (node: SpelNodeImpl, ancestors: SpelNodeImpl[]) => boolean | undefined;
 
  /**
   * Called when leaving a node (post-order).
   * @param node The current AST node
   * @param ancestors Path from root to parent (current node NOT included)
   */
  leaveNode?: (node: SpelNodeImpl, ancestors: SpelNodeImpl[]) => void;
}
 
/**
 * AST Walker — utilities for traversing and querying SpEL ASTs.
 *
 * Designed to be the single traversal utility used by editors,
 * validators, formatters, and code generators.
 */
export namespace AstWalker {
  /**
   * Depth-first traversal of the AST.
   *
   * Calls `enterNode` before traversing children,
   * and `leaveNode` after all children have been visited.
   * If `enterNode` returns `false`, the subtree is skipped.
   *
   * @param root The AST root node
   * @param visitor Callback object
   */
  export function walk(root: SpelNodeImpl, visitor: AstVisitor): void {
    const ancestors: SpelNodeImpl[] = [];
    walkNode(root, ancestors, visitor);
  }
 
  /**
   * Collect all nodes matching a predicate.
   *
   * @param root The AST root node
   * @param predicate Filter function
   * @returns All matching nodes in depth-first order
   */
  export function collect(
    root: SpelNodeImpl,
    predicate: (node: SpelNodeImpl) => boolean,
  ): SpelNodeImpl[] {
    const result: SpelNodeImpl[] = [];
    walk(root, {
      enterNode(node) {
        if (predicate(node)) {
          result.push(node);
        }
        return true;
      },
    });
    return result;
  }
 
  /**
   * Find the node path from root to the node at a given character position.
   *
   * Returns the sequence of nodes from root to the deepest node
   * whose position range contains the given offset.
   *
   * @param root The AST root node
   * @param position Character offset in the source expression
   * @returns Array of nodes from root to target (inclusive), or empty if not found
   */
  export function findNodePath(root: SpelNodeImpl, position: number): SpelNodeImpl[] {
    const path: SpelNodeImpl[] = [];
    findPath(root, position, path);
    return path;
  }
 
  /**
   * Find the deepest node at a given character position.
   *
   * @param root The AST root node
   * @param position Character offset in the source expression
   * @returns The deepest node containing the position, or null
   */
  export function findNodeAt(root: SpelNodeImpl, position: number): SpelNodeImpl | null {
    const path = findNodePath(root, position);
    return path.length > 0 ? (path[path.length - 1] ?? null) : null;
  }
 
  /**
   * Collect all nodes of a specific NodeType.
   *
   * @param root The AST root node
   * @param nodeType The node type to filter by
   * @returns All matching nodes
   */
  export function collectOfType(root: SpelNodeImpl, nodeType: NodeType): SpelNodeImpl[] {
    return collect(root, (node) => node.nodeType === nodeType);
  }
 
  // ===== Private helpers =====
 
  function walkNode(node: SpelNodeImpl, ancestors: SpelNodeImpl[], visitor: AstVisitor): void {
    // Enter
    const shouldContinue = visitor.enterNode?.(node, ancestors);
    if (shouldContinue === false) return;
 
    // Push current node to ancestors for children
    ancestors.push(node);
 
    // Traverse children
    for (let i = 0; i < node.getChildCount(); i++) {
      walkNode(node.getChild(i), ancestors, visitor);
    }
 
    // Pop current node from ancestors
    ancestors.pop();
 
    // Leave
    visitor.leaveNode?.(node, ancestors);
  }
 
  function findPath(node: SpelNodeImpl, position: number, path: SpelNodeImpl[]): boolean {
    // Check if position falls within this node's span
    if (position < node.startPos || position > node.endPos) {
      return false;
    }
 
    path.push(node);
 
    // Try children — find the deepest match
    for (let i = 0; i < node.getChildCount(); i++) {
      if (findPath(node.getChild(i), position, path)) {
        return true; // Found in a child, path already contains child chain
      }
    }
 
    // No deeper match — current node is the deepest
    return true;
  }
}