• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brighterscript / #14405

14 May 2025 05:45PM UTC coverage: 87.003% (+0.006%) from 86.997%
#14405

push

web-flow
Merge 343773173 into a194c3925

13887 of 16869 branches covered (82.32%)

Branch coverage included in aggregate %.

174 of 191 new or added lines in 11 files covered. (91.1%)

136 existing lines in 10 files now uncovered.

14750 of 16046 relevant lines covered (91.92%)

21898.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.27
/src/parser/AstNode.ts
1
import type { WalkVisitor, WalkOptions } from '../astUtils/visitors';
2
import { WalkMode } from '../astUtils/visitors';
1✔
3
import type { Location, Position } from 'vscode-languageserver';
4
import { CancellationTokenSource } from 'vscode-languageserver';
1✔
5
import { InternalWalkMode } from '../astUtils/visitors';
1✔
6
import type { SymbolTable } from '../SymbolTable';
7
import type { BrsTranspileState } from './BrsTranspileState';
8
import type { GetTypeOptions, TranspileResult } from '../interfaces';
9
import type { AnnotationExpression } from './Expression';
10
import util from '../util';
1✔
11
import { DynamicType } from '../types/DynamicType';
1✔
12
import type { BscType } from '../types/BscType';
13
import type { Token } from '../lexer/Token';
14
import { isBlock, isBody, isConditionalCompileStatement, isIfStatement, isStatement } from '../astUtils/reflection';
1✔
15

16
/**
17
 * A BrightScript AST node
18
 */
19
export abstract class AstNode {
1✔
20
    public abstract kind: AstNodeKind;
21
    /**
22
     *  The starting and ending location of the node.
23
     */
24
    public abstract location?: Location | undefined;
25

26
    public abstract transpile(state: BrsTranspileState): TranspileResult;
27

28
    /**
29
     * Optional property, set at the top level with a map of conditional compile consts and their values
30
     */
31
    public bsConsts?: Map<string, boolean>;
32

33
    /**
34
     * When being considered by the walk visitor, this describes what type of element the current class is.
35
     */
36
    public visitMode = InternalWalkMode.visitStatements;
74,875✔
37

38
    public abstract walk(visitor: WalkVisitor, options: WalkOptions);
39

40
    /**
41
     * The parent node for this statement. This is set dynamically during `onFileValidate`, and should not be set directly.
42
     */
43
    public parent?: AstNode;
44

45
    /**
46
     * Certain expressions or statements can have a symbol table (such as blocks, functions, namespace bodies, etc).
47
     * If you're interested in getting the closest SymbolTable, use `getSymbolTable` instead.
48
     */
49
    public symbolTable?: SymbolTable;
50

51
    /**
52
     * Get the closest symbol table for this node
53
     */
54
    public getSymbolTable(): SymbolTable {
55
        let node: AstNode = this;
185,598✔
56
        while (node) {
185,598✔
57
            if (node.symbolTable) {
476,842✔
58
                return node.symbolTable;
185,598✔
59
            }
60
            node = node.parent!;
291,244✔
61
        }
62

63
        //justification: we are following a chain of nodes until we get to one with a SymbolTable,
64
        //and the top-level node will always have a SymbolTable. So we'll never hit this undefined,
65
        //but it is not so easy to convince the typechecker of this.
UNCOV
66
        return undefined as any;
×
67
    }
68

69
    /**
70
     * Walk upward and return the first node that results in `true` from the matcher.
71
     * @param matcher a function called for each node. If you return true, this function returns the specified node. If you return a node, that node is returned. all other return values continue the loop
72
     *                The function's second parameter is a cancellation token. If you'd like to short-circuit the walk, call `cancellationToken.cancel()`, then this function will return `undefined`
73
     */
74
    public findAncestor<TNode extends AstNode = AstNode>(matcher: (node: AstNode, cancellationToken: CancellationTokenSource) => boolean | AstNode | undefined | void): TNode | undefined {
75
        let node = this.parent;
80,392✔
76

77
        const cancel = new CancellationTokenSource();
80,392✔
78
        while (node) {
80,392✔
79
            let matcherValue = matcher(node, cancel);
299,229✔
80
            if (cancel.token.isCancellationRequested) {
299,229✔
81
                return;
1✔
82
            }
83
            if (matcherValue) {
299,228✔
84
                cancel.cancel();
23,964✔
85
                return (matcherValue === true ? node : matcherValue) as TNode;
23,964✔
86

87
            }
88
            node = node.parent;
275,264✔
89
        }
90
    }
91

92
    /**
93
     * Find the first child where the matcher evaluates to true.
94
     * @param matcher a function called for each node. If you return true, this function returns the specified node. If you return a node, that node is returned. all other return values continue the loop
95
     */
96
    public findChild<TNode = AstNode>(matcher: (node: AstNode, cancellationSource) => boolean | AstNode | undefined | void, options?: WalkOptions): TNode | undefined {
97
        const cancel = new CancellationTokenSource();
1,881✔
98
        let result: AstNode | undefined;
99
        this.walk((node) => {
1,881✔
100
            const matcherValue = matcher(node, cancel);
4,465✔
101
            if (matcherValue) {
4,465✔
102
                cancel.cancel();
1,683✔
103
                result = matcherValue === true ? node : matcherValue;
1,683✔
104
            }
105
        }, {
106
            walkMode: WalkMode.visitAllRecursive,
107
            ...options ?? {},
5,643✔
108
            cancel: cancel.token
109
        });
110
        return result as unknown as TNode;
1,881✔
111
    }
112

113
    /**
114
     * Find a list of all children first child where the matcher evaluates to true.
115
     * @param matcher a function called for each node. If you return true, the specified node is included in the results. If you return a node,
116
     * that node is returned. all other return values exclude that value and continue the loop
117
     */
118
    public findChildren<TNode extends AstNode = AstNode>(matcher: (node: AstNode, cancellationSource: CancellationTokenSource) => boolean | AstNode | undefined | void, options?: WalkOptions): Array<TNode> {
119
        const cancel = new CancellationTokenSource();
33✔
120
        let result: Array<AstNode> = [];
33✔
121
        this.walk((node) => {
33✔
122
            const matcherValue = matcher(node, cancel);
446✔
123
            if (matcherValue) {
446✔
124
                result.push(matcherValue === true ? node : matcherValue);
71!
125
            }
126
        }, {
127
            walkMode: WalkMode.visitAllRecursive,
128
            ...options ?? {},
99✔
129
            cancel: cancel.token
130
        });
131
        return result as TNode[];
33✔
132
    }
133

134
    /**
135
     * FInd the deepest child that includes the given position
136
     */
137
    public findChildAtPosition<TNodeType extends AstNode = AstNode>(position: Position, options?: WalkOptions): TNodeType | undefined {
138
        return this.findChild<TNodeType>((node) => {
1,456✔
139
            //if the current node includes this range, keep that node
140
            if (util.rangeContains(node?.location.range, position)) {
3,034!
141
                return node.findChildAtPosition(position, options) ?? node;
1,272✔
142
            }
143
        }, options);
144
    }
145

146
    /**
147
     * Get the BscType of this node.
148
     */
149
    public getType(options: GetTypeOptions): BscType {
150
        return DynamicType.instance;
508✔
151
    }
152

153
    /**
154
     * Links all child nodes to their parent AstNode, and the same with symbol tables. This performs a full AST walk, so you should use this sparingly
155
     */
156
    public link() {
157
        //the act of walking causes the nodes to be linked
158
        this.walk(() => { }, {
3,935✔
159
            // eslint-disable-next-line no-bitwise
160
            walkMode: WalkMode.visitAllRecursive | InternalWalkMode.visitFalseConditionalCompilationBlocks
161
        });
162
    }
163

164
    /**
165
     * Walk upward and return the root node
166
     */
167
    public getRoot() {
168
        let node = this as AstNode;
13,051✔
169

170
        while (node) {
13,051✔
171
            if (!node.parent) {
71,287✔
172
                return node;
13,051✔
173
            }
174
            node = node.parent;
58,236✔
175
        }
176
    }
177

178

179
    /**
180
     * Gets all the trivia (comments, whitespace) that is directly before the start of this node
181
     * Note: this includes all trivia that might start on the line of the previous node
182
     */
183
    public get leadingTrivia(): Token[] {
184
        return [];
6,985✔
185
    }
186

187
    /**
188
     * Gets any trivia that is directly before the end of the node
189
     * For example, this would return all trivia before a `end function` token of a FunctionExpression
190
     */
191
    public get endTrivia(): Token[] {
192
        return [];
35✔
193
    }
194

195
    public getBsConsts() {
196
        return this.bsConsts ?? this.parent?.getBsConsts?.();
93,520✔
197
    }
198

199
    /**
200
     * Clone this node and all of its children. This creates a completely detached and identical copy of the AST.
201
     * All tokens, statements, expressions, range, and location are cloned.
202
     */
203
    public abstract clone();
204

205
    /**
206
     * Helper function for creating a clone. This will clone any attached annotations, as well as reparent the cloned node's children to the clone
207
     */
208
    protected finalizeClone<T extends AstNode>(
209
        clone: T,
210
        propsToReparent?: Array<{ [K in keyof T]: T[K] extends AstNode | AstNode[] ? K : never }[keyof T]>
211
    ) {
212
        //clone the annotations if they exist
213
        if (Array.isArray((this as unknown as Statement).annotations)) {
976✔
214
            (clone as unknown as Statement).annotations = (this as unknown as Statement).annotations?.map(x => x.clone());
7!
215
        }
216
        //reparent all of the supplied props
217
        for (let key of propsToReparent ?? []) {
976✔
218
            const children = (Array.isArray(clone?.[key]) ? clone[key] : [clone?.[key]]) as any[];
1,027!
219
            for (let child of children ?? []) {
1,027!
220
                if (child) {
1,048✔
221
                    (clone[key as any] as AstNode).parent = clone;
797✔
222
                }
223
            }
224
        }
225

226
        //reapply the location if we have one but the clone doesn't
227
        if (!clone.location && this.location) {
976✔
228
            clone.location = util.cloneLocation(this.location);
25✔
229
        }
230
        return clone;
976✔
231
    }
232

233
    private _statementIndex: number;
234

235
    /**
236
     * The index of the statement containing this node (or in the case of a statement, itself) within the containing block.
237
     */
238
    public get statementIndex(): number {
239
        if (this._statementIndex) {
25,769✔
240
            return this._statementIndex;
5,925✔
241
        }
242
        if (isBody(this)) {
19,844!
NEW
243
            return 0;
×
244
        }
245
        if (!this.parent) {
19,844!
NEW
246
            return -1;
×
247
        }
248
        let containingStatement: Statement;
249
        let currentNode: AstNode = this;
19,844✔
250
        while (currentNode && !isStatement(currentNode)) {
19,844✔
251
            currentNode = currentNode.parent;
47,752✔
252
        }
253
        if (isStatement(currentNode)) {
19,844!
254
            containingStatement = currentNode;
19,844✔
255
        } else {
NEW
256
            return -1;
×
257
        }
258
        if (!containingStatement.parent) {
19,844!
NEW
259
            return -1;
×
260
        }
261
        if (!(isBlock(containingStatement.parent) || isBody(containingStatement.parent))) {
19,844✔
262
            // this is a block inside an if statement, for example
263
            if (isIfStatement(containingStatement.parent) || isConditionalCompileStatement(containingStatement.parent)) {
1,979✔
264
                this._statementIndex = containingStatement.parent.getBranchStatementIndex(containingStatement);
1,340✔
265
                return this._statementIndex;
1,340✔
266
            }
267
            return 0;
639✔
268
        }
269
        this._statementIndex = containingStatement.parent.statements.indexOf(containingStatement);
17,865✔
270
        return this._statementIndex;
17,865✔
271
    }
272
}
273

274
export abstract class Statement extends AstNode {
1✔
275
    /**
276
     * When being considered by the walk visitor, this describes what type of element the current class is.
277
     */
278
    public visitMode = InternalWalkMode.visitStatements;
34,500✔
279
    /**
280
     * Annotations for this statement
281
     */
282
    public annotations?: AnnotationExpression[] | undefined;
283
}
284

285

286
/** A BrightScript expression */
287
export abstract class Expression extends AstNode {
1✔
288
    /**
289
     * When being considered by the walk visitor, this describes what type of element the current class is.
290
     */
291
    public visitMode = InternalWalkMode.visitExpressions;
40,375✔
292
}
293

294
export enum AstNodeKind {
1✔
295
    Body = 'Body',
1✔
296
    BinaryExpression = 'BinaryExpression',
1✔
297
    CallExpression = 'CallExpression',
1✔
298
    FunctionExpression = 'FunctionExpression',
1✔
299
    FunctionParameterExpression = 'FunctionParameterExpression',
1✔
300
    NamespacedVariableNameExpression = 'NamespacedVariableNameExpression',
1✔
301
    DottedGetExpression = 'DottedGetExpression',
1✔
302
    XmlAttributeGetExpression = 'XmlAttributeGetExpression',
1✔
303
    IndexedGetExpression = 'IndexedGetExpression',
1✔
304
    GroupingExpression = 'GroupingExpression',
1✔
305
    LiteralExpression = 'LiteralExpression',
1✔
306
    EscapedCharCodeLiteralExpression = 'EscapedCharCodeLiteralExpression',
1✔
307
    ArrayLiteralExpression = 'ArrayLiteralExpression',
1✔
308
    AAMemberExpression = 'AAMemberExpression',
1✔
309
    AALiteralExpression = 'AALiteralExpression',
1✔
310
    UnaryExpression = 'UnaryExpression',
1✔
311
    VariableExpression = 'VariableExpression',
1✔
312
    SourceLiteralExpression = 'SourceLiteralExpression',
1✔
313
    NewExpression = 'NewExpression',
1✔
314
    CallfuncExpression = 'CallfuncExpression',
1✔
315
    TemplateStringQuasiExpression = 'TemplateStringQuasiExpression',
1✔
316
    TemplateStringExpression = 'TemplateStringExpression',
1✔
317
    TaggedTemplateStringExpression = 'TaggedTemplateStringExpression',
1✔
318
    AnnotationExpression = 'AnnotationExpression',
1✔
319
    TernaryExpression = 'TernaryExpression',
1✔
320
    NullCoalescingExpression = 'NullCoalescingExpression',
1✔
321
    RegexLiteralExpression = 'RegexLiteralExpression',
1✔
322
    EmptyStatement = 'EmptyStatement',
1✔
323
    AssignmentStatement = 'AssignmentStatement',
1✔
324
    ExpressionStatement = 'ExpressionStatement',
1✔
325
    ExitStatement = 'ExitStatement',
1✔
326
    FunctionStatement = 'FunctionStatement',
1✔
327
    IfStatement = 'IfStatement',
1✔
328
    IncrementStatement = 'IncrementStatement',
1✔
329
    PrintStatement = 'PrintStatement',
1✔
330
    DimStatement = 'DimStatement',
1✔
331
    GotoStatement = 'GotoStatement',
1✔
332
    LabelStatement = 'LabelStatement',
1✔
333
    ReturnStatement = 'ReturnStatement',
1✔
334
    EndStatement = 'EndStatement',
1✔
335
    StopStatement = 'StopStatement',
1✔
336
    ForStatement = 'ForStatement',
1✔
337
    ForEachStatement = 'ForEachStatement',
1✔
338
    WhileStatement = 'WhileStatement',
1✔
339
    DottedSetStatement = 'DottedSetStatement',
1✔
340
    IndexedSetStatement = 'IndexedSetStatement',
1✔
341
    LibraryStatement = 'LibraryStatement',
1✔
342
    NamespaceStatement = 'NamespaceStatement',
1✔
343
    ImportStatement = 'ImportStatement',
1✔
344
    InterfaceStatement = 'InterfaceStatement',
1✔
345
    InterfaceFieldStatement = 'InterfaceFieldStatement',
1✔
346
    InterfaceMethodStatement = 'InterfaceMethodStatement',
1✔
347
    ClassStatement = 'ClassStatement',
1✔
348
    MethodStatement = 'MethodStatement',
1✔
349
    ClassMethodStatement = 'ClassMethodStatement',
1✔
350
    FieldStatement = 'FieldStatement',
1✔
351
    ClassFieldStatement = 'ClassFieldStatement',
1✔
352
    TryCatchStatement = 'TryCatchStatement',
1✔
353
    CatchStatement = 'CatchStatement',
1✔
354
    ThrowStatement = 'ThrowStatement',
1✔
355
    EnumStatement = 'EnumStatement',
1✔
356
    EnumMemberStatement = 'EnumMemberStatement',
1✔
357
    ConstStatement = 'ConstStatement',
1✔
358
    ContinueStatement = 'ContinueStatement',
1✔
359
    Block = 'Block',
1✔
360
    TypeExpression = 'TypeExpression',
1✔
361
    TypecastExpression = 'TypecastExpression',
1✔
362
    TypedArrayExpression = 'TypedArrayExpression',
1✔
363
    TypecastStatement = 'TypecastStatement',
1✔
364
    AliasStatement = 'AliasStatement',
1✔
365
    ConditionalCompileStatement = 'ConditionalCompileStatement',
1✔
366
    ConditionalCompileConstStatement = 'ConditionalCompileConstStatement',
1✔
367
    ConditionalCompileErrorStatement = 'ConditionalCompileErrorStatement',
1✔
368
    AugmentedAssignmentStatement = 'AugmentedAssignmentStatement',
1✔
369
    PrintSeparatorExpression = 'PrintSeparatorExpression'
1✔
370
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc