Skip to content

Commit 8608d10

Browse files
authored
feat: Walker traversal pruning (#113)
## Summary Unlike `walker.abort()` , this skips only the current node's children and then continues traversal with sibling nodes. This is useful when callers need to avoid nested scopes without stopping the whole walk.
1 parent 7fa8671 commit 8608d10

2 files changed

Lines changed: 191 additions & 1 deletion

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { EsqlQuery } from '../../../composer/query';
9+
import type { ESQLIntegerLiteral } from '../../../types';
10+
import { Walker } from '../walker';
11+
12+
describe('skipping children', () => {
13+
test('can skip command arguments', () => {
14+
const { ast } = EsqlQuery.fromSrc('FROM a, b, c');
15+
const sources: string[] = [];
16+
17+
Walker.walk(ast, {
18+
visitCommand: (_node, _parent, walker) => {
19+
walker.skipChildren();
20+
},
21+
visitSource: (node) => {
22+
sources.push(node.name);
23+
},
24+
});
25+
26+
expect(sources).toStrictEqual([]);
27+
});
28+
29+
test('can skip function arguments', () => {
30+
const { ast } = EsqlQuery.fromSrc('ROW fn(1, 2, 3, gg(4, 5))');
31+
const literals: number[] = [];
32+
33+
Walker.walk(ast, {
34+
visitFunction: (node, _parent, walker) => {
35+
if (node.name === 'fn') walker.skipChildren();
36+
},
37+
visitLiteral: (node) => {
38+
literals.push(node.value as number);
39+
},
40+
});
41+
42+
expect(literals).toStrictEqual([]);
43+
});
44+
45+
test('can skip fields of a command option', () => {
46+
const { ast } = EsqlQuery.fromSrc('FROM index METADATA a, b, c');
47+
const columns: string[] = [];
48+
49+
Walker.walk(ast, {
50+
visitCommandOption: (_node, _parent, walker) => {
51+
walker.skipChildren();
52+
},
53+
visitColumn: (node) => {
54+
columns.push(node.name);
55+
},
56+
});
57+
58+
expect(columns).toStrictEqual([]);
59+
});
60+
61+
test('can skip entries of a map', () => {
62+
const { ast } = EsqlQuery.fromSrc('ROW fn(TRUE, { "foo": 1, "bar": 2, "baz": 3 })');
63+
const keys: string[] = [];
64+
const values: number[] = [];
65+
66+
Walker.walk(ast, {
67+
visitMap: (_node, _parent, walker) => {
68+
walker.skipChildren();
69+
},
70+
visitMapEntry: (node) => {
71+
if (node.key.type === 'literal' && node.key.literalType === 'keyword') {
72+
keys.push(node.key.valueUnquoted);
73+
}
74+
values.push((node.value as ESQLIntegerLiteral).value);
75+
},
76+
});
77+
78+
expect(keys).toStrictEqual([]);
79+
expect(values).toStrictEqual([]);
80+
});
81+
82+
test('can skip children of a map entry', () => {
83+
const { ast } = EsqlQuery.fromSrc('ROW fn(TRUE, { "foo": 1, "bar": 2, "baz": 3 })');
84+
const keys: string[] = [];
85+
const values: number[] = [];
86+
87+
Walker.walk(ast, {
88+
visitMapEntry: (node, _parent, walker) => {
89+
if (node.key.type === 'literal' && node.key.literalType === 'keyword') {
90+
keys.push(node.key.valueUnquoted);
91+
}
92+
walker.skipChildren();
93+
},
94+
visitLiteral: (node) => {
95+
if (node.literalType === 'integer') {
96+
values.push((node as ESQLIntegerLiteral).value);
97+
}
98+
},
99+
});
100+
101+
expect(keys).toStrictEqual(['foo', 'bar', 'baz']);
102+
expect(values).toStrictEqual([]);
103+
});
104+
105+
test('sibling commands are still traversed when a command skips its children', () => {
106+
const { ast } = EsqlQuery.fromSrc('FROM index | LIMIT 123');
107+
const commands: string[] = [];
108+
const sources: string[] = [];
109+
const literals: number[] = [];
110+
111+
Walker.walk(ast, {
112+
visitCommand: (node, parent, walker) => {
113+
commands.push(node.name);
114+
if (node.name === 'from') walker.skipChildren();
115+
},
116+
visitSource: (node) => sources.push(node.name),
117+
visitLiteral: (node) => literals.push(node.value as number),
118+
});
119+
120+
expect(commands).toStrictEqual(['from', 'limit']);
121+
expect(sources).toStrictEqual([]);
122+
expect(literals).toStrictEqual([123]);
123+
});
124+
125+
test('can skip components of a source', () => {
126+
const { ast } = EsqlQuery.fromSrc('FROM a:b, c::d');
127+
const components: string[] = [];
128+
129+
Walker.walk(ast, {
130+
visitSource: (node, _parent, walker) => {
131+
if (node.name === 'a:b') walker.skipChildren();
132+
},
133+
visitLiteral: (node) => {
134+
components.push(node.value as string);
135+
},
136+
});
137+
138+
expect(components).toStrictEqual(['c', 'd']);
139+
});
140+
141+
test('can skip components of a source (backward)', () => {
142+
const { ast } = EsqlQuery.fromSrc('FROM a:b, c::d');
143+
const components: string[] = [];
144+
145+
Walker.walk(ast, {
146+
visitSource: (node, _parent, walker) => {
147+
if (node.name === 'a:b') walker.skipChildren();
148+
},
149+
visitLiteral: (node) => {
150+
components.push(node.value as string);
151+
},
152+
order: 'backward',
153+
});
154+
155+
expect(components).toStrictEqual(['d', 'c']);
156+
});
157+
});

src/ast/walker/walker.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export interface WalkerOptions {
146146

147147
export type WalkerAstNode = types.ESQLAstNode | types.ESQLAstNode[];
148148

149-
export type WalkerVisitorApi = Pick<Walker, 'abort'>;
149+
export type WalkerVisitorApi = Pick<Walker, 'abort' | 'skipChildren'>;
150150

151151
/**
152152
* Iterates over all nodes in the AST and calls the appropriate visitor
@@ -539,13 +539,24 @@ export class Walker {
539539
};
540540

541541
protected aborted: boolean = false;
542+
protected skippedChildren: boolean = false;
542543

543544
constructor(protected readonly options: WalkerOptions) {}
544545

545546
public abort(): void {
546547
this.aborted = true;
547548
}
548549

550+
public skipChildren(): void {
551+
this.skippedChildren = true;
552+
}
553+
554+
protected readAndResetSkippedChildren(): boolean {
555+
const skipped = this.skippedChildren;
556+
this.skippedChildren = false;
557+
return skipped;
558+
}
559+
549560
public walk(
550561
tree: WalkerAstNode | undefined,
551562
parent: types.ESQLProperNode | undefined = undefined
@@ -594,6 +605,8 @@ export class Walker {
594605

595606
const { options } = this;
596607
(options.visitCommand ?? options.visitAny)?.(node, parent, this);
608+
if (this.readAndResetSkippedChildren()) return;
609+
597610
this.walkList(node.args, node);
598611
}
599612

@@ -605,12 +618,16 @@ export class Walker {
605618

606619
const { options } = this;
607620
(options.visitHeaderCommand ?? options.visitAny)?.(node, parent, this);
621+
if (this.readAndResetSkippedChildren()) return;
622+
608623
this.walkList(node.args, node);
609624
}
610625

611626
public walkOption(node: types.ESQLCommandOption, parent: types.ESQLCommand | undefined): void {
612627
const { options } = this;
613628
(options.visitCommandOption ?? options.visitAny)?.(node, parent, this);
629+
if (this.readAndResetSkippedChildren()) return;
630+
614631
this.walkList(node.args, node);
615632
}
616633

@@ -630,13 +647,16 @@ export class Walker {
630647
public walkListLiteral(node: types.ESQLList, parent: types.ESQLProperNode | undefined): void {
631648
const { options } = this;
632649
(options.visitListLiteral ?? options.visitAny)?.(node, parent, this);
650+
if (this.readAndResetSkippedChildren()) return;
651+
633652
this.walkList(node.values, node);
634653
}
635654

636655
public walkSource(node: types.ESQLSource, parent: types.ESQLProperNode | undefined): void {
637656
const { options } = this;
638657

639658
(options.visitSource ?? options.visitAny)?.(node, parent, this);
659+
if (this.readAndResetSkippedChildren()) return;
640660

641661
const children: types.ESQLStringLiteral[] = [];
642662

@@ -658,6 +678,7 @@ export class Walker {
658678
const { args } = node;
659679

660680
(options.visitColumn ?? options.visitAny)?.(node, parent, this);
681+
if (this.readAndResetSkippedChildren()) return;
661682

662683
if (args) {
663684
this.walkList(args, node);
@@ -671,6 +692,7 @@ export class Walker {
671692
const { options } = this;
672693

673694
(options.visitOrder ?? options.visitAny)?.(node, parent, this);
695+
if (this.readAndResetSkippedChildren()) return;
674696

675697
this.walkList(node.args, node);
676698
}
@@ -681,6 +703,8 @@ export class Walker {
681703
): void {
682704
const { options } = this;
683705
(options.visitInlineCast ?? options.visitAny)?.(node, parent, this);
706+
if (this.readAndResetSkippedChildren()) return;
707+
684708
this.walkExpression(node.value, node);
685709
}
686710

@@ -690,6 +714,7 @@ export class Walker {
690714
): void {
691715
const { options } = this;
692716
(options.visitFunction ?? options.visitAny)?.(node, parent, this);
717+
if (this.readAndResetSkippedChildren()) return;
693718

694719
if (node.operator) this.walkSingleAstItem(node.operator, node);
695720

@@ -699,13 +724,16 @@ export class Walker {
699724
public walkMap(node: types.ESQLMap, parent: types.ESQLProperNode | undefined): void {
700725
const { options } = this;
701726
(options.visitMap ?? options.visitAny)?.(node, parent, this);
727+
if (this.readAndResetSkippedChildren()) return;
728+
702729
this.walkList(node.entries, node);
703730
}
704731

705732
public walkMapEntry(node: types.ESQLMapEntry, parent: types.ESQLProperNode | undefined): void {
706733
const { options } = this;
707734

708735
(options.visitMapEntry ?? options.visitAny)?.(node, parent, this);
736+
if (this.readAndResetSkippedChildren()) return;
709737

710738
if (options.order === 'backward') {
711739
this.walkSingleAstItem(resolveItem(node.value), node);
@@ -719,6 +747,7 @@ export class Walker {
719747
public walkParens(node: types.ESQLParens, parent: types.ESQLProperNode | undefined): void {
720748
const { options } = this;
721749
(options.visitParens ?? options.visitAny)?.(node, parent, this);
750+
if (this.readAndResetSkippedChildren()) return;
722751

723752
if (node.child) {
724753
this.walkSingleAstItem(node.child, node);
@@ -731,6 +760,7 @@ export class Walker {
731760
): void {
732761
const { options } = this;
733762
(options.visitQuery ?? options.visitAny)?.(node, parent, this);
763+
if (this.readAndResetSkippedChildren()) return;
734764

735765
if (node.header && !options.skipHeader) {
736766
this.walkList(node.header, node);
@@ -816,6 +846,7 @@ export class Walker {
816846
}
817847
case 'literal': {
818848
(options.visitLiteral ?? options.visitAny)?.(node, parent, this);
849+
this.readAndResetSkippedChildren();
819850
break;
820851
}
821852
case 'list': {
@@ -832,10 +863,12 @@ export class Walker {
832863
}
833864
case 'identifier': {
834865
(options.visitIdentifier ?? options.visitAny)?.(node, parent, this);
866+
this.readAndResetSkippedChildren();
835867
break;
836868
}
837869
case 'unknown': {
838870
(options.visitUnknown ?? options.visitAny)?.(node, parent, this);
871+
this.readAndResetSkippedChildren();
839872
break;
840873
}
841874
}

0 commit comments

Comments
 (0)