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

rokucommunity / brighterscript / #14407

14 May 2025 06:03PM UTC coverage: 87.001%. Remained the same
#14407

push

web-flow
Merge 21e318f51 into a194c3925

13885 of 16867 branches covered (82.32%)

Branch coverage included in aggregate %.

170 of 187 new or added lines in 11 files covered. (90.91%)

136 existing lines in 10 files now uncovered.

14746 of 16042 relevant lines covered (91.92%)

21905.94 hits per line

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

94.14
/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,602✔
56
        while (node) {
185,602✔
57
            if (node.symbolTable) {
476,848✔
58
                return node.symbolTable;
185,602✔
59
            }
60
            node = node.parent!;
291,246✔
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
    /**
234
     * The index of the statement containing this node (or in the case of a statement, itself) within the containing block.
235
     */
236
    public get statementIndex(): number {
237
        if (isBody(this)) {
25,769!
NEW
238
            return 0;
×
239
        }
240
        if (!this.parent) {
25,769!
NEW
241
            return -1;
×
242
        }
243
        let containingStatement: Statement;
244
        let currentNode: AstNode = this;
25,769✔
245
        while (currentNode && !isStatement(currentNode)) {
25,769✔
246
            currentNode = currentNode.parent;
58,722✔
247
        }
248
        if (isStatement(currentNode)) {
25,769!
249
            containingStatement = currentNode;
25,769✔
250
        } else {
NEW
251
            return -1;
×
252
        }
253
        if (!containingStatement.parent) {
25,769!
NEW
254
            return -1;
×
255
        }
256
        if (!(isBlock(containingStatement.parent) || isBody(containingStatement.parent))) {
25,769✔
257
            // this is a block inside an if statement, for example
258
            if (isIfStatement(containingStatement.parent) || isConditionalCompileStatement(containingStatement.parent)) {
2,176✔
259
                return containingStatement.parent.getBranchStatementIndex(containingStatement);
1,537✔
260
            }
261
            return 0;
639✔
262
        }
263
        return containingStatement.parent.statements.indexOf(containingStatement);
23,593✔
264
    }
265
}
266

267
export abstract class Statement extends AstNode {
1✔
268
    /**
269
     * When being considered by the walk visitor, this describes what type of element the current class is.
270
     */
271
    public visitMode = InternalWalkMode.visitStatements;
34,500✔
272
    /**
273
     * Annotations for this statement
274
     */
275
    public annotations?: AnnotationExpression[] | undefined;
276
}
277

278

279
/** A BrightScript expression */
280
export abstract class Expression extends AstNode {
1✔
281
    /**
282
     * When being considered by the walk visitor, this describes what type of element the current class is.
283
     */
284
    public visitMode = InternalWalkMode.visitExpressions;
40,375✔
285
}
286

287
export enum AstNodeKind {
1✔
288
    Body = 'Body',
1✔
289
    BinaryExpression = 'BinaryExpression',
1✔
290
    CallExpression = 'CallExpression',
1✔
291
    FunctionExpression = 'FunctionExpression',
1✔
292
    FunctionParameterExpression = 'FunctionParameterExpression',
1✔
293
    NamespacedVariableNameExpression = 'NamespacedVariableNameExpression',
1✔
294
    DottedGetExpression = 'DottedGetExpression',
1✔
295
    XmlAttributeGetExpression = 'XmlAttributeGetExpression',
1✔
296
    IndexedGetExpression = 'IndexedGetExpression',
1✔
297
    GroupingExpression = 'GroupingExpression',
1✔
298
    LiteralExpression = 'LiteralExpression',
1✔
299
    EscapedCharCodeLiteralExpression = 'EscapedCharCodeLiteralExpression',
1✔
300
    ArrayLiteralExpression = 'ArrayLiteralExpression',
1✔
301
    AAMemberExpression = 'AAMemberExpression',
1✔
302
    AALiteralExpression = 'AALiteralExpression',
1✔
303
    UnaryExpression = 'UnaryExpression',
1✔
304
    VariableExpression = 'VariableExpression',
1✔
305
    SourceLiteralExpression = 'SourceLiteralExpression',
1✔
306
    NewExpression = 'NewExpression',
1✔
307
    CallfuncExpression = 'CallfuncExpression',
1✔
308
    TemplateStringQuasiExpression = 'TemplateStringQuasiExpression',
1✔
309
    TemplateStringExpression = 'TemplateStringExpression',
1✔
310
    TaggedTemplateStringExpression = 'TaggedTemplateStringExpression',
1✔
311
    AnnotationExpression = 'AnnotationExpression',
1✔
312
    TernaryExpression = 'TernaryExpression',
1✔
313
    NullCoalescingExpression = 'NullCoalescingExpression',
1✔
314
    RegexLiteralExpression = 'RegexLiteralExpression',
1✔
315
    EmptyStatement = 'EmptyStatement',
1✔
316
    AssignmentStatement = 'AssignmentStatement',
1✔
317
    ExpressionStatement = 'ExpressionStatement',
1✔
318
    ExitStatement = 'ExitStatement',
1✔
319
    FunctionStatement = 'FunctionStatement',
1✔
320
    IfStatement = 'IfStatement',
1✔
321
    IncrementStatement = 'IncrementStatement',
1✔
322
    PrintStatement = 'PrintStatement',
1✔
323
    DimStatement = 'DimStatement',
1✔
324
    GotoStatement = 'GotoStatement',
1✔
325
    LabelStatement = 'LabelStatement',
1✔
326
    ReturnStatement = 'ReturnStatement',
1✔
327
    EndStatement = 'EndStatement',
1✔
328
    StopStatement = 'StopStatement',
1✔
329
    ForStatement = 'ForStatement',
1✔
330
    ForEachStatement = 'ForEachStatement',
1✔
331
    WhileStatement = 'WhileStatement',
1✔
332
    DottedSetStatement = 'DottedSetStatement',
1✔
333
    IndexedSetStatement = 'IndexedSetStatement',
1✔
334
    LibraryStatement = 'LibraryStatement',
1✔
335
    NamespaceStatement = 'NamespaceStatement',
1✔
336
    ImportStatement = 'ImportStatement',
1✔
337
    InterfaceStatement = 'InterfaceStatement',
1✔
338
    InterfaceFieldStatement = 'InterfaceFieldStatement',
1✔
339
    InterfaceMethodStatement = 'InterfaceMethodStatement',
1✔
340
    ClassStatement = 'ClassStatement',
1✔
341
    MethodStatement = 'MethodStatement',
1✔
342
    ClassMethodStatement = 'ClassMethodStatement',
1✔
343
    FieldStatement = 'FieldStatement',
1✔
344
    ClassFieldStatement = 'ClassFieldStatement',
1✔
345
    TryCatchStatement = 'TryCatchStatement',
1✔
346
    CatchStatement = 'CatchStatement',
1✔
347
    ThrowStatement = 'ThrowStatement',
1✔
348
    EnumStatement = 'EnumStatement',
1✔
349
    EnumMemberStatement = 'EnumMemberStatement',
1✔
350
    ConstStatement = 'ConstStatement',
1✔
351
    ContinueStatement = 'ContinueStatement',
1✔
352
    Block = 'Block',
1✔
353
    TypeExpression = 'TypeExpression',
1✔
354
    TypecastExpression = 'TypecastExpression',
1✔
355
    TypedArrayExpression = 'TypedArrayExpression',
1✔
356
    TypecastStatement = 'TypecastStatement',
1✔
357
    AliasStatement = 'AliasStatement',
1✔
358
    ConditionalCompileStatement = 'ConditionalCompileStatement',
1✔
359
    ConditionalCompileConstStatement = 'ConditionalCompileConstStatement',
1✔
360
    ConditionalCompileErrorStatement = 'ConditionalCompileErrorStatement',
1✔
361
    AugmentedAssignmentStatement = 'AugmentedAssignmentStatement',
1✔
362
    PrintSeparatorExpression = 'PrintSeparatorExpression'
1✔
363
}
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