File size: 5,047 Bytes
f0634fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// src/node.ts
//
// Syntax tree node model. Named node types correspond one-to-one with
// tree-sitter-bash's named node types, so consumers can write tree queries
// against either implementation. Two deliberate deviations from tree-sitter:
//
//   - startIndex / endIndex are UTF-16 code unit offsets (native JS string
//     indexing), not tree-sitter's UTF-8 byte offsets. `node.text` therefore
//     always equals `source.slice(node.startIndex, node.endIndex)`.
//   - `text` is pre-computed when the node is created instead of being
//     re-sliced on every access.
//
// Child semantics: `children` contains every child in source order, including
// anonymous (punctuation / keyword token) nodes; `namedChildren` contains only
// the named ones, in the same relative order. Anonymous nodes never appear in
// `namedChildren`. `descendantsOfType` walks named descendants only, matching
// how tree-sitter queries see the tree.

export interface SyntaxNode {
  /** Node type, e.g. 'program', 'command', 'word'. Matches tree-sitter-bash. */
  readonly type: string;
  /** Source text covered by this node (UTF-16 slice of the original source). */
  readonly text: string;
  /** Start offset in UTF-16 code units, inclusive. */
  readonly startIndex: number;
  /** End offset in UTF-16 code units, exclusive. */
  readonly endIndex: number;
  /** Whether this is a named node (false for punctuation/keyword tokens). */
  readonly isNamed: boolean;
  readonly parent: SyntaxNode | null;
  /** All children in source order, named and anonymous. */
  readonly children: readonly SyntaxNode[];
  /** Named children only, in source order. */
  readonly namedChildren: readonly SyntaxNode[];
}

export interface NodeInit {
  type: string;
  source: string;
  startIndex: number;
  endIndex: number;
  isNamed?: boolean;
}

/**
 * Mutable node under construction. The parser builds trees with this class and
 * exposes them through the readonly `SyntaxNode` interface; once a node is
 * handed out it must be treated as immutable.
 */
export class SyntaxNodeBuilder {
  readonly type: string;
  readonly text: string;
  readonly startIndex: number;
  readonly endIndex: number;
  readonly isNamed: boolean;
  parent: SyntaxNodeBuilder | null = null;
  readonly children: SyntaxNodeBuilder[] = [];
  readonly namedChildren: SyntaxNodeBuilder[] = [];

  constructor(init: NodeInit) {
    if (init.startIndex < 0 || init.endIndex < init.startIndex || init.endIndex > init.source.length) {
      throw new RangeError(
        `invalid node range [${init.startIndex}, ${init.endIndex}) for source of length ${init.source.length}`,
      );
    }
    this.type = init.type;
    this.startIndex = init.startIndex;
    this.endIndex = init.endIndex;
    this.isNamed = init.isNamed ?? true;
    this.text = init.source.slice(init.startIndex, init.endIndex);
  }

  /** Attach a child, wiring its parent pointer. Named children are also added
   *  to `namedChildren`. Returns the child for chaining.
   *
   *  Children must lie inside the parent's range and be appended in source
   *  order without overlapping the previous sibling (a zero-width child may
   *  start exactly where the previous sibling ends). */
  addChild<T extends SyntaxNodeBuilder>(child: T): T {
    if (child.parent !== null) throw new Error(`node '${child.type}' already has a parent`);
    if (child.startIndex < this.startIndex || child.endIndex > this.endIndex) {
      throw new RangeError(
        `child '${child.type}' [${child.startIndex}, ${child.endIndex}) escapes parent '${this.type}' [${this.startIndex}, ${this.endIndex})`,
      );
    }
    const last = this.children.at(-1);
    if (last !== undefined && child.startIndex < last.endIndex) {
      throw new RangeError(
        `child '${child.type}' [${child.startIndex}, ${child.endIndex}) overlaps sibling '${last.type}' [${last.startIndex}, ${last.endIndex})`,
      );
    }
    child.parent = this;
    this.children.push(child);
    if (child.isNamed) this.namedChildren.push(child);
    return child;
  }
}

/** Convenience factory for a detached node. */
export function createNode(init: NodeInit): SyntaxNodeBuilder {
  return new SyntaxNodeBuilder(init);
}

/**
 * Pre-order traversal of the named descendants of `root` (not including
 * `root` itself), filtered to the given types. With no types, returns every
 * named descendant in pre-order. Iterative with an explicit stack so that
 * pathologically deep trees cannot overflow the call stack.
 */
export function descendantsOfType(root: SyntaxNode, ...types: string[]): SyntaxNode[] {
  const wanted = types.length > 0 ? new Set(types) : null;
  const out: SyntaxNode[] = [];
  const stack: SyntaxNode[] = [];
  for (let i = root.namedChildren.length - 1; i >= 0; i--) stack.push(root.namedChildren[i]!);
  while (stack.length > 0) {
    const node = stack.pop()!;
    if (wanted === null || wanted.has(node.type)) out.push(node);
    for (let i = node.namedChildren.length - 1; i >= 0; i--) stack.push(node.namedChildren[i]!);
  }
  return out;
}