/
ast-utils.ts
303 lines (285 loc) · 11.4 KB
/
ast-utils.ts
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/******************************************************************************
* Copyright 2021 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/
import type { Range } from 'vscode-languageserver-types';
import type { AstNode, AstReflection, CstNode, GenericAstNode, Mutable, PropertyType, Reference, ReferenceInfo } from '../syntax-tree.js';
import type { Stream, TreeStream } from './stream.js';
import type { LangiumDocument } from '../workspace/documents.js';
import { isAstNode, isReference } from '../syntax-tree.js';
import { DONE_RESULT, stream, StreamImpl, TreeStreamImpl } from './stream.js';
import { inRange } from './cst-utils.js';
/**
* Link the `$container` and other related properties of every AST node that is directly contained
* in the given `node`.
*/
export function linkContentToContainer(node: AstNode): void {
for (const [name, value] of Object.entries(node)) {
if (!name.startsWith('$')) {
if (Array.isArray(value)) {
value.forEach((item, index) => {
if (isAstNode(item)) {
(item as Mutable<AstNode>).$container = node;
(item as Mutable<AstNode>).$containerProperty = name;
(item as Mutable<AstNode>).$containerIndex = index;
}
});
} else if (isAstNode(value)) {
(value as Mutable<AstNode>).$container = node;
(value as Mutable<AstNode>).$containerProperty = name;
}
}
}
}
/**
* Walk along the hierarchy of containers from the given AST node to the root and return the first
* node that matches the type predicate. If the start node itself matches, it is returned.
* If no container matches, `undefined` is returned.
*/
export function getContainerOfType<T extends AstNode>(node: AstNode | undefined, typePredicate: (n: AstNode) => n is T): T | undefined {
let item = node;
while (item) {
if (typePredicate(item)) {
return item;
}
item = item.$container;
}
return undefined;
}
/**
* Walk along the hierarchy of containers from the given AST node to the root and check for existence
* of a container that matches the given predicate. The start node is included in the checks.
*/
export function hasContainerOfType(node: AstNode | undefined, predicate: (n: AstNode) => boolean): boolean {
let item = node;
while (item) {
if (predicate(item)) {
return true;
}
item = item.$container;
}
return false;
}
/**
* Retrieve the document in which the given AST node is contained. A reference to the document is
* usually held by the root node of the AST.
*
* @throws an error if the node is not contained in a document.
*/
export function getDocument<T extends AstNode = AstNode>(node: AstNode): LangiumDocument<T> {
const rootNode = findRootNode(node);
const result = rootNode.$document;
if (!result) {
throw new Error('AST node has no document.');
}
return result as LangiumDocument<T>;
}
/**
* Returns the root node of the given AST node by following the `$container` references.
*/
export function findRootNode(node: AstNode): AstNode {
while (node.$container) {
node = node.$container;
}
return node;
}
export interface AstStreamOptions {
/**
* Optional target range that the nodes in the stream need to intersect
*/
range?: Range
}
/**
* Create a stream of all AST nodes that are directly contained in the given node. This includes
* single-valued as well as multi-valued (array) properties.
*/
export function streamContents(node: AstNode, options?: AstStreamOptions): Stream<AstNode> {
if (!node) {
throw new Error('Node must be an AstNode.');
}
const range = options?.range;
type State = { keys: string[], keyIndex: number, arrayIndex: number };
return new StreamImpl<State, AstNode>(() => ({
keys: Object.keys(node),
keyIndex: 0,
arrayIndex: 0
}), state => {
while (state.keyIndex < state.keys.length) {
const property = state.keys[state.keyIndex];
if (!property.startsWith('$')) {
const value = (node as GenericAstNode)[property];
if (isAstNode(value)) {
state.keyIndex++;
if (isAstNodeInRange(value, range)) {
return { done: false, value };
}
} else if (Array.isArray(value)) {
while (state.arrayIndex < value.length) {
const index = state.arrayIndex++;
const element = value[index];
if (isAstNode(element) && isAstNodeInRange(element, range)) {
return { done: false, value: element };
}
}
state.arrayIndex = 0;
}
}
state.keyIndex++;
}
return DONE_RESULT;
});
}
/**
* Create a stream of all AST nodes that are directly and indirectly contained in the given root node.
* This does not include the root node itself.
*/
export function streamAllContents(root: AstNode, options?: AstStreamOptions): TreeStream<AstNode> {
if (!root) {
throw new Error('Root node must be an AstNode.');
}
return new TreeStreamImpl(root, node => streamContents(node, options));
}
/**
* Create a stream of all AST nodes that are directly and indirectly contained in the given root node,
* including the root node itself.
*/
export function streamAst(root: AstNode, options?: AstStreamOptions): TreeStream<AstNode> {
if (!root) {
throw new Error('Root node must be an AstNode.');
} else if (options?.range && !isAstNodeInRange(root, options.range)) {
// Return an empty stream if the root node isn't in range
return new TreeStreamImpl(root, () => []);
}
return new TreeStreamImpl(root, node => streamContents(node, options), { includeRoot: true });
}
function isAstNodeInRange(astNode: AstNode, range?: Range): boolean {
if (!range) {
return true;
}
const nodeRange = astNode.$cstNode?.range;
if (!nodeRange) {
return false;
}
return inRange(nodeRange, range);
}
/**
* Create a stream of all cross-references that are held by the given AST node. This includes
* single-valued as well as multi-valued (array) properties.
*/
export function streamReferences(node: AstNode): Stream<ReferenceInfo> {
type State = { keys: string[], keyIndex: number, arrayIndex: number };
return new StreamImpl<State, ReferenceInfo>(() => ({
keys: Object.keys(node),
keyIndex: 0,
arrayIndex: 0
}), state => {
while (state.keyIndex < state.keys.length) {
const property = state.keys[state.keyIndex];
if (!property.startsWith('$')) {
const value = (node as GenericAstNode)[property];
if (isReference(value)) {
state.keyIndex++;
return { done: false, value: { reference: value, container: node, property } };
} else if (Array.isArray(value)) {
while (state.arrayIndex < value.length) {
const index = state.arrayIndex++;
const element = value[index];
if (isReference(element)) {
return { done: false, value: { reference: element, container: node, property, index } };
}
}
state.arrayIndex = 0;
}
}
state.keyIndex++;
}
return DONE_RESULT;
});
}
/**
* Returns a Stream of references to the target node from the AstNode tree
*
* @param targetNode AstNode we are looking for
* @param lookup AstNode where we search for references. If not provided, the root node of the document is used as the default value
*/
export function findLocalReferences(targetNode: AstNode, lookup = getDocument(targetNode).parseResult.value): Stream<Reference> {
const refs: Reference[] = [];
streamAst(lookup).forEach(node => {
streamReferences(node).forEach(refInfo => {
if (refInfo.reference.ref === targetNode) {
refs.push(refInfo.reference);
}
});
});
return stream(refs);
}
/**
* Assigns all mandatory AST properties to the specified node.
*
* @param reflection Reflection object used to gather mandatory properties for the node.
* @param node Specified node is modified in place and properties are directly assigned.
*/
export function assignMandatoryProperties(reflection: AstReflection, node: AstNode): void {
const typeMetaData = reflection.getTypeMetaData(node.$type);
const genericNode = node as GenericAstNode;
for (const property of typeMetaData.properties) {
// Only set the value if the property is not already set and if it has a default value
if (property.defaultValue !== undefined && genericNode[property.name] === undefined) {
genericNode[property.name] = copyDefaultValue(property.defaultValue);
}
}
}
function copyDefaultValue(propertyType: PropertyType): PropertyType {
if (Array.isArray(propertyType)) {
return [...propertyType.map(copyDefaultValue)];
} else {
return propertyType;
}
}
/**
* Creates a deep copy of the specified AST node.
* The resulting copy will only contain semantically relevant information, such as the `$type` property and AST properties.
*
* References are copied without resolved cross reference. The specified function is used to rebuild them.
*/
export function copyAstNode<T extends AstNode = AstNode>(node: T, buildReference: (node: AstNode, property: string, refNode: CstNode | undefined, refText: string) => Reference<AstNode>): T {
const copy: GenericAstNode = { $type: node.$type };
for (const [name, value] of Object.entries(node)) {
if (!name.startsWith('$')) {
if (isAstNode(value)) {
copy[name] = copyAstNode(value, buildReference);
} else if (isReference(value)) {
copy[name] = buildReference(
copy,
name,
value.$refNode,
value.$refText
);
} else if (Array.isArray(value)) {
const copiedArray: unknown[] = [];
for (const element of value) {
if (isAstNode(element)) {
copiedArray.push(copyAstNode(element, buildReference));
} else if (isReference(element)) {
copiedArray.push(
buildReference(
copy,
name,
element.$refNode,
element.$refText
)
);
} else {
copiedArray.push(element);
}
}
copy[name] = copiedArray;
} else {
copy[name] = value;
}
}
}
linkContentToContainer(copy);
return copy as unknown as T;
}