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;
}
|