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

rokucommunity / brighterscript / #15245

12 Mar 2026 12:50PM UTC coverage: 88.972% (-0.02%) from 88.989%
#15245

push

web-flow
Merge aa08edc30 into 3cb920f6e

7892 of 9354 branches covered (84.37%)

Branch coverage included in aggregate %.

0 of 2 new or added lines in 2 files covered. (0.0%)

1 existing line in 1 file now uncovered.

10115 of 10885 relevant lines covered (92.93%)

1921.44 hits per line

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

91.83
/src/parser/Statement.ts
1
/* eslint-disable no-bitwise */
2
import type { Token, Identifier } from '../lexer/Token';
3
import { CompoundAssignmentOperators, TokenKind } from '../lexer/TokenKind';
1✔
4
import type { BinaryExpression, NamespacedVariableNameExpression, FunctionParameterExpression, LiteralExpression } from './Expression';
5
import { FunctionExpression } from './Expression';
1✔
6
import { CallExpression, VariableExpression } from './Expression';
1✔
7
import { util } from '../util';
1✔
8
import type { Range } from 'vscode-languageserver';
9
import type { BrsTranspileState } from './BrsTranspileState';
10
import { ParseMode } from './Parser';
1✔
11
import type { WalkVisitor, WalkOptions } from '../astUtils/visitors';
12
import { InternalWalkMode, walk, createVisitor, WalkMode, walkArray } from '../astUtils/visitors';
1✔
13
import { isCallExpression, isCommentStatement, isEnumMemberStatement, isExpression, isExpressionStatement, isFieldStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isMethodStatement, isNamespaceStatement, isTypedefProvider, isUnaryExpression, isVoidType } from '../astUtils/reflection';
1✔
14
import type { TranspileResult, TypedefProvider } from '../interfaces';
15
import { createIdentifier, createInvalidLiteral, createMethodStatement, createToken } from '../astUtils/creators';
1✔
16
import { DynamicType } from '../types/DynamicType';
1✔
17
import type { BscType } from '../types/BscType';
18
import type { TranspileState } from './TranspileState';
19
import { SymbolTable } from '../SymbolTable';
1✔
20
import type { AstNode, Expression } from './AstNode';
21
import { Statement } from './AstNode';
1✔
22

23
export class EmptyStatement extends Statement {
1✔
24
    constructor(
25
        /**
26
         * Create a negative range to indicate this is an interpolated location
27
         */
28
        public range: Range = undefined
6✔
29
    ) {
30
        super();
6✔
31
    }
32

33
    transpile(state: BrsTranspileState) {
34
        return [];
2✔
35
    }
36
    walk(visitor: WalkVisitor, options: WalkOptions) {
37
        //nothing to walk
38
    }
39

40
    public clone() {
41
        return this.finalizeClone(
1✔
42
            new EmptyStatement(
43
                util.cloneRange(this.range)
44
            )
45
        );
46
    }
47
}
48

49
/**
50
 * This is a top-level statement. Consider this the root of the AST
51
 */
52
export class Body extends Statement implements TypedefProvider {
1✔
53
    constructor(
54
        public statements: Statement[] = []
4,913✔
55
    ) {
56
        super();
4,913✔
57
    }
58

59
    public symbolTable = new SymbolTable('Body', () => this.parent?.getSymbolTable());
4,913✔
60

61
    public get range() {
62
        //this needs to be a getter because the body has its statements pushed to it after being constructed
63
        return util.createBoundingRange(
17✔
64
            ...(this.statements ?? [])
51!
65
        );
66
    }
67

68
    transpile(state: BrsTranspileState) {
69
        let result = [] as TranspileResult;
336✔
70
        for (let i = 0; i < this.statements.length; i++) {
336✔
71
            let statement = this.statements[i];
497✔
72
            let previousStatement = this.statements[i - 1];
497✔
73
            let nextStatement = this.statements[i + 1];
497✔
74

75
            if (!previousStatement) {
497✔
76
                //this is the first statement. do nothing related to spacing and newlines
77

78
                //if comment is on same line as prior sibling
79
            } else if (isCommentStatement(statement) && previousStatement && statement.range?.start.line === previousStatement.range?.end.line) {
163✔
80
                result.push(
4✔
81
                    ' '
82
                );
83

84
                //add double newline if this is a comment, and next is a function
85
            } else if (isCommentStatement(statement) && nextStatement && isFunctionStatement(nextStatement)) {
159✔
86
                result.push(state.newline, state.newline);
1✔
87

88
                //add double newline if is function not preceeded by a comment
89
            } else if (isFunctionStatement(statement) && previousStatement && !(isCommentStatement(previousStatement))) {
158✔
90
                result.push(state.newline, state.newline);
82✔
91
            } else {
92
                //separate statements by a single newline
93
                result.push(state.newline);
76✔
94
            }
95

96
            result.push(...statement.transpile(state));
497✔
97
        }
98
        return result;
336✔
99
    }
100

101
    getTypedef(state: BrsTranspileState): TranspileResult {
102
        let result = [] as TranspileResult;
34✔
103
        for (const statement of this.statements) {
34✔
104
            //if the current statement supports generating typedef, call it
105
            if (isTypedefProvider(statement)) {
49!
106
                result.push(
49✔
107
                    state.indent(),
108
                    ...statement.getTypedef(state),
109
                    state.newline
110
                );
111
            }
112
        }
113
        return result;
34✔
114
    }
115

116
    walk(visitor: WalkVisitor, options: WalkOptions) {
117
        if (options.walkMode & InternalWalkMode.walkStatements) {
4,389!
118
            walkArray(this.statements, visitor, options, this);
4,389✔
119
        }
120
    }
121

122
    public clone() {
123
        return this.finalizeClone(
134✔
124
            new Body(
125
                this.statements?.map(s => s?.clone())
139✔
126
            ),
127
            ['statements']
128
        );
129
    }
130
}
131

132
export class AssignmentStatement extends Statement {
1✔
133
    constructor(
134
        readonly equals: Token,
995✔
135
        readonly name: Identifier,
995✔
136
        readonly value: Expression
995✔
137
    ) {
138
        super();
995✔
139
        this.range = util.createBoundingRange(name, equals, value);
995✔
140
    }
141

142
    public readonly range: Range | undefined;
143

144
    /**
145
     * Get the name of the wrapping namespace (if it exists)
146
     * @deprecated use `.findAncestor(isFunctionExpression)` instead.
147
     */
148
    public get containingFunction() {
149
        return this.findAncestor<FunctionExpression>(isFunctionExpression);
463✔
150
    }
151

152
    transpile(state: BrsTranspileState) {
153
        //if the value is a compound assignment, just transpile the expression itself
154
        if (CompoundAssignmentOperators.includes((this.value as BinaryExpression)?.operator?.kind)) {
272!
155
            return this.value.transpile(state);
21✔
156
        } else {
157
            return [
251✔
158
                state.transpileToken(this.name),
159
                ' ',
160
                state.transpileToken(this.equals),
161
                ' ',
162
                ...this.value.transpile(state)
163
            ];
164
        }
165
    }
166

167
    walk(visitor: WalkVisitor, options: WalkOptions) {
168
        if (options.walkMode & InternalWalkMode.walkExpressions) {
2,279✔
169
            walk(this, 'value', visitor, options);
1,814✔
170
        }
171
    }
172

173
    public clone() {
174
        return this.finalizeClone(
15✔
175
            new AssignmentStatement(
176
                util.cloneToken(this.equals),
177
                util.cloneToken(this.name),
178
                this.value?.clone()
45✔
179
            ),
180
            ['value']
181
        );
182
    }
183
}
184

185
export class Block extends Statement {
1✔
186
    constructor(
187
        readonly statements: Statement[],
2,481✔
188
        readonly startingRange?: Range
2,481✔
189
    ) {
190
        super();
2,481✔
191
        this.range = util.createBoundingRange(
2,481✔
192
            { range: this.startingRange },
193
            ...(statements ?? [])
7,443✔
194
        );
195
    }
196

197
    public readonly range: Range | undefined;
198

199
    transpile(state: BrsTranspileState) {
200
        state.blockDepth++;
514✔
201
        let results = [] as TranspileResult;
514✔
202
        for (let i = 0; i < this.statements.length; i++) {
514✔
203
            let previousStatement = this.statements[i - 1];
764✔
204
            let statement = this.statements[i];
764✔
205

206
            //if comment is on same line as parent
207
            if (isCommentStatement(statement) &&
764✔
208
                (util.linesTouch(state.lineage[0], statement) || util.linesTouch(previousStatement, statement))
209
            ) {
210
                results.push(' ');
65✔
211

212
                //is not a comment
213
            } else {
214
                //add a newline and indent
215
                results.push(
699✔
216
                    state.newline,
217
                    state.indent()
218
                );
219
            }
220

221
            //push block onto parent list
222
            state.lineage.unshift(this);
764✔
223
            results.push(
764✔
224
                ...statement.transpile(state)
225
            );
226
            state.lineage.shift();
764✔
227
        }
228
        state.blockDepth--;
514✔
229
        return results;
514✔
230
    }
231

232
    walk(visitor: WalkVisitor, options: WalkOptions) {
233
        if (options.walkMode & InternalWalkMode.walkStatements) {
5,890✔
234
            walkArray(this.statements, visitor, options, this);
5,884✔
235
        }
236
    }
237

238
    public clone() {
239
        return this.finalizeClone(
121✔
240
            new Block(
241
                this.statements?.map(s => s?.clone()),
110✔
242
                util.cloneRange(this.startingRange)
243
            ),
244
            ['statements']
245
        );
246
    }
247
}
248

249
export class ExpressionStatement extends Statement {
1✔
250
    constructor(
251
        readonly expression: Expression
339✔
252
    ) {
253
        super();
339✔
254
        this.range = this.expression?.range;
339✔
255
    }
256

257
    public readonly range: Range | undefined;
258

259
    transpile(state: BrsTranspileState) {
260
        return this.expression.transpile(state);
52✔
261
    }
262

263
    getTypedef(state: BrsTranspileState): TranspileResult {
264
        //ExpressionStatements should not be included in typedefs
265
        //as they represent code execution which is not part of the type definition
NEW
266
        return [];
×
267
    }
268

269
    walk(visitor: WalkVisitor, options: WalkOptions) {
270
        if (options.walkMode & InternalWalkMode.walkExpressions) {
987✔
271
            walk(this, 'expression', visitor, options);
745✔
272
        }
273
    }
274

275
    public clone() {
276
        return this.finalizeClone(
13✔
277
            new ExpressionStatement(
278
                this.expression?.clone()
39✔
279
            ),
280
            ['expression']
281
        );
282
    }
283
}
284

285
export class CommentStatement extends Statement implements Expression, TypedefProvider {
1✔
286
    constructor(
287
        public comments: Token[]
267✔
288
    ) {
289
        super();
267✔
290
        this.visitMode = InternalWalkMode.visitStatements | InternalWalkMode.visitExpressions;
267✔
291
        if (this.comments?.length > 0) {
267✔
292
            this.range = util.createBoundingRange(
265✔
293
                ...this.comments
294
            );
295
        }
296
    }
297

298
    public range: Range | undefined;
299

300
    get text() {
301
        return this.comments.map(x => x.text).join('\n');
39✔
302
    }
303

304
    transpile(state: BrsTranspileState) {
305
        let result = [] as TranspileResult;
105✔
306
        for (let i = 0; i < this.comments.length; i++) {
105✔
307
            let comment = this.comments[i];
106✔
308
            if (i > 0) {
106✔
309
                result.push(state.indent());
1✔
310
            }
311
            result.push(
106✔
312
                state.transpileToken(comment)
313
            );
314
            //add newline for all except final comment
315
            if (i < this.comments.length - 1) {
106✔
316
                result.push(state.newline);
1✔
317
            }
318
        }
319
        return result;
105✔
320
    }
321

322
    public getTypedef(state: TranspileState) {
323
        return this.transpile(state as BrsTranspileState);
×
324
    }
325

326
    walk(visitor: WalkVisitor, options: WalkOptions) {
327
        //nothing to walk
328
    }
329

330
    public clone() {
331
        return this.finalizeClone(
4✔
332
            new CommentStatement(
333
                this.comments?.map(x => util.cloneToken(x))
3✔
334
            ),
335
            ['comments' as any]
336
        );
337
    }
338
}
339

340
export class ExitForStatement extends Statement {
1✔
341
    constructor(
342
        readonly tokens: {
6✔
343
            exitFor: Token;
344
        }
345
    ) {
346
        super();
6✔
347
        this.range = this.tokens.exitFor.range;
6✔
348
    }
349

350
    public readonly range: Range;
351

352
    transpile(state: BrsTranspileState) {
353
        return [
2✔
354
            state.transpileToken(this.tokens.exitFor)
355
        ];
356
    }
357

358
    walk(visitor: WalkVisitor, options: WalkOptions) {
359
        //nothing to walk
360
    }
361

362
    public clone() {
363
        return this.finalizeClone(
1✔
364
            new ExitForStatement({
365
                exitFor: util.cloneToken(this.tokens.exitFor)
366
            })
367
        );
368
    }
369
}
370

371
export class ExitWhileStatement extends Statement {
1✔
372
    constructor(
373
        readonly tokens: {
9✔
374
            exitWhile: Token;
375
        }
376
    ) {
377
        super();
9✔
378
        this.range = this.tokens.exitWhile.range;
9✔
379
    }
380

381
    public readonly range: Range;
382

383
    transpile(state: BrsTranspileState) {
384
        return [
3✔
385
            state.transpileToken(this.tokens.exitWhile)
386
        ];
387
    }
388

389
    walk(visitor: WalkVisitor, options: WalkOptions) {
390
        //nothing to walk
391
    }
392

393
    public clone() {
394
        return this.finalizeClone(
1✔
395
            new ExitWhileStatement({
396
                exitWhile: util.cloneToken(this.tokens.exitWhile)
397
            })
398
        );
399
    }
400
}
401

402
export class FunctionStatement extends Statement implements TypedefProvider {
1✔
403
    constructor(
404
        public name: Identifier,
2,180✔
405
        public func: FunctionExpression
2,180✔
406
    ) {
407
        super();
2,180✔
408
        this.range = this.func?.range;
2,180✔
409
    }
410

411
    public readonly range: Range | undefined;
412

413
    /**
414
     * Get the name of this expression based on the parse mode
415
     */
416
    public getName(parseMode: ParseMode) {
417
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,214✔
418
        if (namespace) {
2,214✔
419
            let delimiter = parseMode === ParseMode.BrighterScript ? '.' : '_';
312✔
420
            let namespaceName = namespace.getName(parseMode);
312✔
421
            return namespaceName + delimiter + this.name?.text;
312✔
422
        } else {
423
            return this.name.text;
1,902✔
424
        }
425
    }
426

427
    /**
428
     * Get the name of the wrapping namespace (if it exists)
429
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
430
     */
431
    public get namespaceName() {
432
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
433
    }
434

435
    transpile(state: BrsTranspileState) {
436
        //create a fake token using the full transpiled name
437
        let nameToken = {
312✔
438
            ...this.name,
439
            text: this.getName(ParseMode.BrightScript)
440
        };
441

442
        return this.func.transpile(state, nameToken);
312✔
443
    }
444

445
    getTypedef(state: BrsTranspileState) {
446
        let result = [] as TranspileResult;
11✔
447
        for (let annotation of this.annotations ?? []) {
11✔
448
            result.push(
2✔
449
                ...annotation.getTypedef(state),
450
                state.newline,
451
                state.indent()
452
            );
453
        }
454

455
        result.push(
11✔
456
            ...this.func.getTypedef(state)
457
        );
458
        return result;
11✔
459
    }
460

461
    walk(visitor: WalkVisitor, options: WalkOptions) {
462
        if (options.walkMode & InternalWalkMode.walkExpressions) {
2,988✔
463
            walk(this, 'func', visitor, options);
2,961✔
464
        }
465
    }
466

467
    public clone() {
468
        return this.finalizeClone(
103✔
469
            new FunctionStatement(
470
                util.cloneToken(this.name),
471
                this.func?.clone()
309✔
472
            ),
473
            ['func']
474
        );
475
    }
476
}
477

478
export class IfStatement extends Statement {
1✔
479
    constructor(
480
        readonly tokens: {
244✔
481
            if: Token;
482
            then?: Token;
483
            else?: Token;
484
            endIf?: Token;
485
        },
486
        readonly condition: Expression,
244✔
487
        readonly thenBranch: Block,
244✔
488
        readonly elseBranch?: IfStatement | Block,
244✔
489
        readonly isInline?: boolean
244✔
490
    ) {
491
        super();
244✔
492
        this.range = util.createBoundingRange(
244✔
493
            tokens.if,
494
            condition,
495
            tokens.then,
496
            thenBranch,
497
            tokens.else,
498
            elseBranch,
499
            tokens.endIf
500
        );
501
    }
502
    public readonly range: Range | undefined;
503

504
    transpile(state: BrsTranspileState) {
505
        let results = [] as TranspileResult;
75✔
506
        //if   (already indented by block)
507
        results.push(state.transpileToken(this.tokens.if));
75✔
508
        results.push(' ');
75✔
509
        //conditions
510
        results.push(...this.condition.transpile(state));
75✔
511
        //then
512
        if (this.tokens.then) {
75✔
513
            results.push(' ');
64✔
514
            results.push(
64✔
515
                state.transpileToken(this.tokens.then)
516
            );
517
        }
518
        state.lineage.unshift(this);
75✔
519

520
        //if statement body
521
        let thenNodes = this.thenBranch.transpile(state);
75✔
522
        state.lineage.shift();
75✔
523
        if (thenNodes.length > 0) {
75✔
524
            results.push(thenNodes);
64✔
525
        }
526
        results.push('\n');
75✔
527

528
        //else branch
529
        if (this.tokens.else) {
75✔
530
            //else
531
            results.push(
56✔
532
                state.indent(),
533
                state.transpileToken(this.tokens.else)
534
            );
535
        }
536

537
        if (this.elseBranch) {
75✔
538
            if (isIfStatement(this.elseBranch)) {
56✔
539
                //chained elseif
540
                state.lineage.unshift(this.elseBranch);
19✔
541
                let body = this.elseBranch.transpile(state);
19✔
542
                state.lineage.shift();
19✔
543

544
                if (body.length > 0) {
19!
545
                    //zero or more spaces between the `else` and the `if`
546
                    results.push(this.elseBranch.tokens.if.leadingWhitespace!);
19✔
547
                    results.push(...body);
19✔
548

549
                    // stop here because chained if will transpile the rest
550
                    return results;
19✔
551
                } else {
552
                    results.push('\n');
×
553
                }
554

555
            } else {
556
                //else body
557
                state.lineage.unshift(this.tokens.else!);
37✔
558
                let body = this.elseBranch.transpile(state);
37✔
559
                state.lineage.shift();
37✔
560

561
                if (body.length > 0) {
37✔
562
                    results.push(...body);
35✔
563
                }
564
                results.push('\n');
37✔
565
            }
566
        }
567

568
        //end if
569
        results.push(state.indent());
56✔
570
        if (this.tokens.endIf) {
56!
571
            results.push(
56✔
572
                state.transpileToken(this.tokens.endIf)
573
            );
574
        } else {
575
            results.push('end if');
×
576
        }
577
        return results;
56✔
578
    }
579

580
    walk(visitor: WalkVisitor, options: WalkOptions) {
581
        if (options.walkMode & InternalWalkMode.walkExpressions) {
545✔
582
            walk(this, 'condition', visitor, options);
428✔
583
        }
584
        if (options.walkMode & InternalWalkMode.walkStatements) {
545✔
585
            walk(this, 'thenBranch', visitor, options);
543✔
586
        }
587
        if (this.elseBranch && options.walkMode & InternalWalkMode.walkStatements) {
545✔
588
            walk(this, 'elseBranch', visitor, options);
308✔
589
        }
590
    }
591

592
    public clone() {
593
        return this.finalizeClone(
5✔
594
            new IfStatement(
595
                {
596
                    if: util.cloneToken(this.tokens.if),
597
                    else: util.cloneToken(this.tokens.else),
598
                    endIf: util.cloneToken(this.tokens.endIf),
599
                    then: util.cloneToken(this.tokens.then)
600
                },
601
                this.condition?.clone(),
15✔
602
                this.thenBranch?.clone(),
15✔
603
                this.elseBranch?.clone(),
15✔
604
                this.isInline
605
            ),
606
            ['condition', 'thenBranch', 'elseBranch']
607
        );
608
    }
609
}
610

611
export class IncrementStatement extends Statement {
1✔
612
    constructor(
613
        readonly value: Expression,
21✔
614
        readonly operator: Token
21✔
615
    ) {
616
        super();
21✔
617
        this.range = util.createBoundingRange(
21✔
618
            value,
619
            operator
620
        );
621
    }
622

623
    public readonly range: Range | undefined;
624

625
    transpile(state: BrsTranspileState) {
626
        return [
8✔
627
            ...this.value.transpile(state),
628
            state.transpileToken(this.operator)
629
        ];
630
    }
631

632
    walk(visitor: WalkVisitor, options: WalkOptions) {
633
        if (options.walkMode & InternalWalkMode.walkExpressions) {
39✔
634
            walk(this, 'value', visitor, options);
32✔
635
        }
636
    }
637

638
    public clone() {
639
        return this.finalizeClone(
2✔
640
            new IncrementStatement(
641
                this.value?.clone(),
6✔
642
                util.cloneToken(this.operator)
643
            ),
644
            ['value']
645
        );
646
    }
647
}
648

649
/** Used to indent the current `print` position to the next 16-character-width output zone. */
650
export interface PrintSeparatorTab extends Token {
651
    kind: TokenKind.Comma;
652
}
653

654
/** Used to insert a single whitespace character at the current `print` position. */
655
export interface PrintSeparatorSpace extends Token {
656
    kind: TokenKind.Semicolon;
657
}
658

659
/**
660
 * Represents a `print` statement within BrightScript.
661
 */
662
export class PrintStatement extends Statement {
1✔
663
    /**
664
     * Creates a new internal representation of a BrightScript `print` statement.
665
     * @param tokens the tokens for this statement
666
     * @param tokens.print a print token
667
     * @param expressions an array of expressions or `PrintSeparator`s to be evaluated and printed.
668
     */
669
    constructor(
670
        readonly tokens: {
756✔
671
            print: Token;
672
        },
673
        readonly expressions: Array<Expression | PrintSeparatorTab | PrintSeparatorSpace>
756✔
674
    ) {
675
        super();
756✔
676
        this.range = util.createBoundingRange(
756✔
677
            tokens.print,
678
            ...(expressions ?? [])
2,268✔
679
        );
680
    }
681

682
    public readonly range: Range | undefined;
683

684
    transpile(state: BrsTranspileState) {
685
        let result = [
207✔
686
            state.transpileToken(this.tokens.print),
687
            ' '
688
        ] as TranspileResult;
689
        for (let i = 0; i < this.expressions.length; i++) {
207✔
690
            const expressionOrSeparator: any = this.expressions[i];
271✔
691
            if (expressionOrSeparator.transpile) {
271✔
692
                result.push(...(expressionOrSeparator as ExpressionStatement).transpile(state));
245✔
693
            } else {
694
                result.push(
26✔
695
                    state.tokenToSourceNode(expressionOrSeparator)
696
                );
697
            }
698
            //if there's an expression after us, add a space
699
            if ((this.expressions[i + 1] as any)?.transpile) {
271✔
700
                result.push(' ');
38✔
701
            }
702
        }
703
        return result;
207✔
704
    }
705

706
    walk(visitor: WalkVisitor, options: WalkOptions) {
707
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,801✔
708
            //sometimes we have semicolon Tokens in the expressions list (should probably fix that...), so only walk the actual expressions
709
            walkArray(this.expressions as AstNode[], visitor, options, this, (item) => isExpression(item as any));
1,613✔
710
        }
711
    }
712

713
    public clone() {
714
        return this.finalizeClone(
47✔
715
            new PrintStatement(
716
                {
717
                    print: util.cloneToken(this.tokens.print)
718
                },
719
                this.expressions?.map(e => {
141✔
720
                    if (isExpression(e as any)) {
47✔
721
                        return (e as Expression).clone();
46✔
722
                    } else {
723
                        return util.cloneToken(e as Token);
1✔
724
                    }
725
                })
726
            ),
727
            ['expressions' as any]
728
        );
729
    }
730
}
731

732
export class DimStatement extends Statement {
1✔
733
    constructor(
734
        public dimToken: Token,
46✔
735
        public identifier?: Identifier,
46✔
736
        public openingSquare?: Token,
46✔
737
        public dimensions?: Expression[],
46✔
738
        public closingSquare?: Token
46✔
739
    ) {
740
        super();
46✔
741
        this.range = util.createBoundingRange(
46✔
742
            dimToken,
743
            identifier,
744
            openingSquare,
745
            ...(dimensions ?? []),
138✔
746
            closingSquare
747
        );
748
    }
749
    public range: Range | undefined;
750

751
    public transpile(state: BrsTranspileState) {
752
        let result = [
15✔
753
            state.transpileToken(this.dimToken),
754
            ' ',
755
            state.transpileToken(this.identifier!),
756
            state.transpileToken(this.openingSquare!)
757
        ] as TranspileResult;
758
        for (let i = 0; i < this.dimensions!.length; i++) {
15✔
759
            if (i > 0) {
32✔
760
                result.push(', ');
17✔
761
            }
762
            result.push(
32✔
763
                ...this.dimensions![i].transpile(state)
764
            );
765
        }
766
        result.push(state.transpileToken(this.closingSquare!));
15✔
767
        return result;
15✔
768
    }
769

770
    public walk(visitor: WalkVisitor, options: WalkOptions) {
771
        if (this.dimensions?.length !== undefined && this.dimensions?.length > 0 && options.walkMode & InternalWalkMode.walkExpressions) {
98!
772
            walkArray(this.dimensions, visitor, options, this);
71✔
773

774
        }
775
    }
776

777
    public clone() {
778
        return this.finalizeClone(
3✔
779
            new DimStatement(
780
                util.cloneToken(this.dimToken),
781
                util.cloneToken(this.identifier),
782
                util.cloneToken(this.openingSquare),
783
                this.dimensions?.map(e => e?.clone()),
5✔
784
                util.cloneToken(this.closingSquare)
785
            ),
786
            ['dimensions']
787
        );
788
    }
789
}
790

791
export class GotoStatement extends Statement {
1✔
792
    constructor(
793
        readonly tokens: {
12✔
794
            goto: Token;
795
            label: Token;
796
        }
797
    ) {
798
        super();
12✔
799
        this.range = util.createBoundingRange(
12✔
800
            tokens.goto,
801
            tokens.label
802
        );
803
    }
804

805
    public readonly range: Range | undefined;
806

807
    transpile(state: BrsTranspileState) {
808
        return [
2✔
809
            state.transpileToken(this.tokens.goto),
810
            ' ',
811
            state.transpileToken(this.tokens.label)
812
        ];
813
    }
814

815
    walk(visitor: WalkVisitor, options: WalkOptions) {
816
        //nothing to walk
817
    }
818

819
    public clone() {
820
        return this.finalizeClone(
1✔
821
            new GotoStatement({
822
                goto: util.cloneToken(this.tokens.goto),
823
                label: util.cloneToken(this.tokens.label)
824
            })
825
        );
826
    }
827
}
828

829
export class LabelStatement extends Statement {
1✔
830
    constructor(
831
        readonly tokens: {
12✔
832
            identifier: Token;
833
            colon: Token;
834
        }
835
    ) {
836
        super();
12✔
837
        this.range = util.createBoundingRange(
12✔
838
            tokens.identifier,
839
            tokens.colon
840
        );
841
    }
842

843
    public readonly range: Range | undefined;
844

845
    transpile(state: BrsTranspileState) {
846
        return [
2✔
847
            state.transpileToken(this.tokens.identifier),
848
            state.transpileToken(this.tokens.colon)
849

850
        ];
851
    }
852

853
    walk(visitor: WalkVisitor, options: WalkOptions) {
854
        //nothing to walk
855
    }
856

857
    public clone() {
858
        return this.finalizeClone(
1✔
859
            new LabelStatement({
860
                identifier: util.cloneToken(this.tokens.identifier),
861
                colon: util.cloneToken(this.tokens.colon)
862
            })
863
        );
864
    }
865
}
866

867
export class ReturnStatement extends Statement {
1✔
868
    constructor(
869
        readonly tokens: {
234✔
870
            return: Token;
871
        },
872
        readonly value?: Expression
234✔
873
    ) {
874
        super();
234✔
875
        this.range = util.createBoundingRange(
234✔
876
            tokens.return,
877
            value
878
        );
879
    }
880

881
    public readonly range: Range | undefined;
882

883
    transpile(state: BrsTranspileState) {
884
        let result = [] as TranspileResult;
19✔
885
        result.push(
19✔
886
            state.transpileToken(this.tokens.return)
887
        );
888
        if (this.value) {
19✔
889
            result.push(' ');
18✔
890
            result.push(...this.value.transpile(state));
18✔
891
        }
892
        return result;
19✔
893
    }
894

895
    walk(visitor: WalkVisitor, options: WalkOptions) {
896
        if (options.walkMode & InternalWalkMode.walkExpressions) {
507✔
897
            walk(this, 'value', visitor, options);
419✔
898
        }
899
    }
900

901
    public clone() {
902
        return this.finalizeClone(
3✔
903
            new ReturnStatement(
904
                {
905
                    return: util.cloneToken(this.tokens.return)
906
                },
907
                this.value?.clone()
9✔
908
            ),
909
            ['value']
910
        );
911
    }
912
}
913

914
export class EndStatement extends Statement {
1✔
915
    constructor(
916
        readonly tokens: {
10✔
917
            end: Token;
918
        }
919
    ) {
920
        super();
10✔
921
        this.range = tokens.end.range;
10✔
922
    }
923

924
    public readonly range: Range;
925

926
    transpile(state: BrsTranspileState) {
927
        return [
2✔
928
            state.transpileToken(this.tokens.end)
929
        ];
930
    }
931

932
    walk(visitor: WalkVisitor, options: WalkOptions) {
933
        //nothing to walk
934
    }
935

936
    public clone() {
937
        return this.finalizeClone(
1✔
938
            new EndStatement({
939
                end: util.cloneToken(this.tokens.end)
940
            })
941
        );
942
    }
943
}
944

945
export class StopStatement extends Statement {
1✔
946
    constructor(
947
        readonly tokens: {
18✔
948
            stop: Token;
949
        }
950
    ) {
951
        super();
18✔
952
        this.range = tokens?.stop?.range;
18!
953
    }
954

955
    public readonly range: Range;
956

957
    transpile(state: BrsTranspileState) {
958
        return [
2✔
959
            state.transpileToken(this.tokens.stop)
960
        ];
961
    }
962

963
    walk(visitor: WalkVisitor, options: WalkOptions) {
964
        //nothing to walk
965
    }
966

967
    public clone() {
968
        return this.finalizeClone(
1✔
969
            new StopStatement({
970
                stop: util.cloneToken(this.tokens.stop)
971
            })
972
        );
973
    }
974
}
975

976
export class ForStatement extends Statement {
1✔
977
    constructor(
978
        public forToken: Token,
39✔
979
        public counterDeclaration: AssignmentStatement,
39✔
980
        public toToken: Token,
39✔
981
        public finalValue: Expression,
39✔
982
        public body: Block,
39✔
983
        public endForToken: Token,
39✔
984
        public stepToken?: Token,
39✔
985
        public increment?: Expression
39✔
986
    ) {
987
        super();
39✔
988
        this.range = util.createBoundingRange(
39✔
989
            forToken,
990
            counterDeclaration,
991
            toToken,
992
            finalValue,
993
            stepToken,
994
            increment,
995
            body,
996
            endForToken
997
        );
998
    }
999

1000
    public readonly range: Range | undefined;
1001

1002
    transpile(state: BrsTranspileState) {
1003
        let result = [] as TranspileResult;
9✔
1004
        //for
1005
        result.push(
9✔
1006
            state.transpileToken(this.forToken),
1007
            ' '
1008
        );
1009
        //i=1
1010
        result.push(
9✔
1011
            ...this.counterDeclaration.transpile(state),
1012
            ' '
1013
        );
1014
        //to
1015
        result.push(
9✔
1016
            state.transpileToken(this.toToken),
1017
            ' '
1018
        );
1019
        //final value
1020
        result.push(this.finalValue.transpile(state));
9✔
1021
        //step
1022
        if (this.stepToken) {
9✔
1023
            result.push(
4✔
1024
                ' ',
1025
                state.transpileToken(this.stepToken),
1026
                ' ',
1027
                this.increment!.transpile(state)
1028
            );
1029
        }
1030
        //loop body
1031
        state.lineage.unshift(this);
9✔
1032
        result.push(...this.body.transpile(state));
9✔
1033
        state.lineage.shift();
9✔
1034

1035
        // add new line before "end for"
1036
        result.push('\n');
9✔
1037

1038
        //end for
1039
        result.push(
9✔
1040
            state.indent(),
1041
            state.transpileToken(this.endForToken)
1042
        );
1043

1044
        return result;
9✔
1045
    }
1046

1047
    walk(visitor: WalkVisitor, options: WalkOptions) {
1048
        if (options.walkMode & InternalWalkMode.walkStatements) {
85✔
1049
            walk(this, 'counterDeclaration', visitor, options);
84✔
1050
        }
1051
        if (options.walkMode & InternalWalkMode.walkExpressions) {
85✔
1052
            walk(this, 'finalValue', visitor, options);
64✔
1053
            walk(this, 'increment', visitor, options);
64✔
1054
        }
1055
        if (options.walkMode & InternalWalkMode.walkStatements) {
85✔
1056
            walk(this, 'body', visitor, options);
84✔
1057
        }
1058
    }
1059

1060
    public clone() {
1061
        return this.finalizeClone(
5✔
1062
            new ForStatement(
1063
                util.cloneToken(this.forToken),
1064
                this.counterDeclaration?.clone(),
15✔
1065
                util.cloneToken(this.toToken),
1066
                this.finalValue?.clone(),
15✔
1067
                this.body?.clone(),
15✔
1068
                util.cloneToken(this.endForToken),
1069
                util.cloneToken(this.stepToken),
1070
                this.increment?.clone()
15✔
1071
            ),
1072
            ['counterDeclaration', 'finalValue', 'body', 'increment']
1073
        );
1074
    }
1075
}
1076

1077
export class ForEachStatement extends Statement {
1✔
1078
    constructor(
1079
        readonly tokens: {
26✔
1080
            forEach: Token;
1081
            in: Token;
1082
            endFor: Token;
1083
        },
1084
        readonly item: Token,
26✔
1085
        readonly target: Expression,
26✔
1086
        readonly body: Block
26✔
1087
    ) {
1088
        super();
26✔
1089
        this.range = util.createBoundingRange(
26✔
1090
            tokens.forEach,
1091
            item,
1092
            tokens.in,
1093
            target,
1094
            body,
1095
            tokens.endFor
1096
        );
1097
    }
1098

1099
    public readonly range: Range | undefined;
1100

1101
    transpile(state: BrsTranspileState) {
1102
        let result = [] as TranspileResult;
6✔
1103
        //for each
1104
        result.push(
6✔
1105
            state.transpileToken(this.tokens.forEach),
1106
            ' '
1107
        );
1108
        //item
1109
        result.push(
6✔
1110
            state.transpileToken(this.item),
1111
            ' '
1112
        );
1113
        //in
1114
        result.push(
6✔
1115
            state.transpileToken(this.tokens.in),
1116
            ' '
1117
        );
1118
        //target
1119
        result.push(...this.target.transpile(state));
6✔
1120
        //body
1121
        state.lineage.unshift(this);
6✔
1122
        result.push(...this.body.transpile(state));
6✔
1123
        state.lineage.shift();
6✔
1124

1125
        // add new line before "end for"
1126
        result.push('\n');
6✔
1127

1128
        //end for
1129
        result.push(
6✔
1130
            state.indent(),
1131
            state.transpileToken(this.tokens.endFor)
1132
        );
1133
        return result;
6✔
1134
    }
1135

1136
    walk(visitor: WalkVisitor, options: WalkOptions) {
1137
        if (options.walkMode & InternalWalkMode.walkExpressions) {
54✔
1138
            walk(this, 'target', visitor, options);
42✔
1139
        }
1140
        if (options.walkMode & InternalWalkMode.walkStatements) {
54✔
1141
            walk(this, 'body', visitor, options);
53✔
1142
        }
1143
    }
1144

1145
    public clone() {
1146
        return this.finalizeClone(
2✔
1147
            new ForEachStatement(
1148
                {
1149
                    forEach: util.cloneToken(this.tokens.forEach),
1150
                    in: util.cloneToken(this.tokens.in),
1151
                    endFor: util.cloneToken(this.tokens.endFor)
1152
                },
1153
                util.cloneToken(this.item),
1154
                this.target?.clone(),
6✔
1155
                this.body?.clone()
6✔
1156
            ),
1157
            ['target', 'body']
1158
        );
1159
    }
1160
}
1161

1162
export class WhileStatement extends Statement {
1✔
1163
    constructor(
1164
        readonly tokens: {
27✔
1165
            while: Token;
1166
            endWhile: Token;
1167
        },
1168
        readonly condition: Expression,
27✔
1169
        readonly body: Block
27✔
1170
    ) {
1171
        super();
27✔
1172
        this.range = util.createBoundingRange(
27✔
1173
            tokens.while,
1174
            condition,
1175
            body,
1176
            tokens.endWhile
1177
        );
1178
    }
1179

1180
    public readonly range: Range | undefined;
1181

1182
    transpile(state: BrsTranspileState) {
1183
        let result = [] as TranspileResult;
4✔
1184
        //while
1185
        result.push(
4✔
1186
            state.transpileToken(this.tokens.while),
1187
            ' '
1188
        );
1189
        //condition
1190
        result.push(
4✔
1191
            ...this.condition.transpile(state)
1192
        );
1193
        state.lineage.unshift(this);
4✔
1194
        //body
1195
        result.push(...this.body.transpile(state));
4✔
1196
        state.lineage.shift();
4✔
1197

1198
        //trailing newline only if we have body statements
1199
        result.push('\n');
4✔
1200

1201
        //end while
1202
        result.push(
4✔
1203
            state.indent(),
1204
            state.transpileToken(this.tokens.endWhile)
1205
        );
1206

1207
        return result;
4✔
1208
    }
1209

1210
    walk(visitor: WalkVisitor, options: WalkOptions) {
1211
        if (options.walkMode & InternalWalkMode.walkExpressions) {
52✔
1212
            walk(this, 'condition', visitor, options);
38✔
1213
        }
1214
        if (options.walkMode & InternalWalkMode.walkStatements) {
52✔
1215
            walk(this, 'body', visitor, options);
51✔
1216
        }
1217
    }
1218

1219
    public clone() {
1220
        return this.finalizeClone(
4✔
1221
            new WhileStatement(
1222
                {
1223
                    while: util.cloneToken(this.tokens.while),
1224
                    endWhile: util.cloneToken(this.tokens.endWhile)
1225
                },
1226
                this.condition?.clone(),
12✔
1227
                this.body?.clone()
12✔
1228
            ),
1229
            ['condition', 'body']
1230
        );
1231
    }
1232
}
1233

1234
export class DottedSetStatement extends Statement {
1✔
1235
    constructor(
1236
        readonly obj: Expression,
250✔
1237
        readonly name: Identifier,
250✔
1238
        readonly value: Expression,
250✔
1239
        readonly dot?: Token,
250✔
1240
        readonly equals?: Token
250✔
1241
    ) {
1242
        super();
250✔
1243
        this.range = util.createBoundingRange(
250✔
1244
            obj,
1245
            dot,
1246
            name,
1247
            equals,
1248
            value
1249
        );
1250
    }
1251

1252
    public readonly range: Range | undefined;
1253

1254
    transpile(state: BrsTranspileState) {
1255
        //if the value is a compound assignment, don't add the obj, dot, name, or operator...the expression will handle that
1256
        if (CompoundAssignmentOperators.includes((this.value as BinaryExpression)?.operator?.kind)) {
16!
1257
            return this.value.transpile(state);
2✔
1258
        } else {
1259
            return [
14✔
1260
                //object
1261
                ...this.obj.transpile(state),
1262
                this.dot ? state.tokenToSourceNode(this.dot) : '.',
14✔
1263
                //name
1264
                state.transpileToken(this.name),
1265
                ' ',
1266
                state.transpileToken(this.equals, '='),
1267
                ' ',
1268
                //right-hand-side of assignment
1269
                ...this.value.transpile(state)
1270
            ];
1271
        }
1272
    }
1273

1274
    walk(visitor: WalkVisitor, options: WalkOptions) {
1275
        if (options.walkMode & InternalWalkMode.walkExpressions) {
332✔
1276
            walk(this, 'obj', visitor, options);
301✔
1277
            walk(this, 'value', visitor, options);
301✔
1278
        }
1279
    }
1280

1281
    public clone() {
1282
        return this.finalizeClone(
2✔
1283
            new DottedSetStatement(
1284
                this.obj?.clone(),
6✔
1285
                util.cloneToken(this.name),
1286
                this.value?.clone(),
6✔
1287
                util.cloneToken(this.dot),
1288
                util.cloneToken(this.equals)
1289
            ),
1290
            ['obj', 'value']
1291
        );
1292
    }
1293
}
1294

1295
export class IndexedSetStatement extends Statement {
1✔
1296
    constructor(
1297
        readonly obj: Expression,
55✔
1298
        readonly index: Expression,
55✔
1299
        readonly value: Expression,
55✔
1300
        readonly openingSquare: Token,
55✔
1301
        readonly closingSquare: Token,
55✔
1302
        readonly additionalIndexes?: Expression[],
55✔
1303
        readonly equals?: Token
55✔
1304
    ) {
1305
        super();
55✔
1306
        this.additionalIndexes ??= [];
55✔
1307
        this.range = util.createBoundingRange(
55✔
1308
            obj,
1309
            openingSquare,
1310
            index,
1311
            closingSquare,
1312
            equals,
1313
            value,
1314
            ...this.additionalIndexes
1315
        );
1316
    }
1317

1318
    public readonly range: Range | undefined;
1319

1320
    transpile(state: BrsTranspileState) {
1321
        //if the value is a component assignment, don't add the obj, index or operator...the expression will handle that
1322
        if (CompoundAssignmentOperators.includes((this.value as BinaryExpression)?.operator?.kind)) {
21!
1323
            return this.value.transpile(state);
2✔
1324
        } else {
1325
            const result = [];
19✔
1326
            result.push(
19✔
1327
                //obj
1328
                ...this.obj.transpile(state),
1329
                //   [
1330
                state.transpileToken(this.openingSquare)
1331
            );
1332
            const indexes = [this.index, ...this.additionalIndexes ?? []];
19!
1333
            for (let i = 0; i < indexes.length; i++) {
19✔
1334
                //add comma between indexes
1335
                if (i > 0) {
20✔
1336
                    result.push(', ');
1✔
1337
                }
1338
                let index = indexes[i];
20✔
1339
                result.push(
20✔
1340
                    ...(index?.transpile(state) ?? [])
120!
1341
                );
1342
            }
1343
            result.push(
19✔
1344
                state.transpileToken(this.closingSquare),
1345
                ' ',
1346
                state.transpileToken(this.equals, '='),
1347
                ' ',
1348
                ...this.value.transpile(state)
1349
            );
1350
            return result;
19✔
1351
        }
1352
    }
1353

1354
    walk(visitor: WalkVisitor, options: WalkOptions) {
1355
        if (options.walkMode & InternalWalkMode.walkExpressions) {
130✔
1356
            walk(this, 'obj', visitor, options);
106✔
1357
            walk(this, 'index', visitor, options);
106✔
1358
            walkArray(this.additionalIndexes, visitor, options, this);
106✔
1359
            walk(this, 'value', visitor, options);
106✔
1360
        }
1361
    }
1362

1363
    public clone() {
1364
        return this.finalizeClone(
6✔
1365
            new IndexedSetStatement(
1366
                this.obj?.clone(),
18✔
1367
                this.index?.clone(),
18✔
1368
                this.value?.clone(),
18✔
1369
                util.cloneToken(this.openingSquare),
1370
                util.cloneToken(this.closingSquare),
1371
                this.additionalIndexes?.map(e => e?.clone()),
2✔
1372
                util.cloneToken(this.equals)
1373
            ),
1374
            ['obj', 'index', 'value', 'additionalIndexes']
1375
        );
1376
    }
1377
}
1378

1379
export class LibraryStatement extends Statement implements TypedefProvider {
1✔
1380
    constructor(
1381
        readonly tokens: {
17✔
1382
            library: Token;
1383
            filePath: Token | undefined;
1384
        }
1385
    ) {
1386
        super();
17✔
1387
        this.range = util.createBoundingRange(
17✔
1388
            this.tokens?.library,
51✔
1389
            this.tokens?.filePath
51✔
1390
        );
1391
    }
1392

1393
    public readonly range: Range | undefined;
1394

1395
    transpile(state: BrsTranspileState) {
1396
        let result = [] as TranspileResult;
2✔
1397
        result.push(
2✔
1398
            state.transpileToken(this.tokens.library)
1399
        );
1400
        //there will be a parse error if file path is missing, but let's prevent a runtime error just in case
1401
        if (this.tokens.filePath) {
2!
1402
            result.push(
2✔
1403
                ' ',
1404
                state.transpileToken(this.tokens.filePath)
1405
            );
1406
        }
1407
        return result;
2✔
1408
    }
1409

1410
    getTypedef(state: BrsTranspileState) {
1411
        return this.transpile(state);
×
1412
    }
1413

1414
    walk(visitor: WalkVisitor, options: WalkOptions) {
1415
        //nothing to walk
1416
    }
1417

1418
    public clone() {
1419
        return this.finalizeClone(
2✔
1420
            new LibraryStatement(
1421
                this.tokens === undefined ? undefined : {
2✔
1422
                    library: util.cloneToken(this.tokens?.library),
3!
1423
                    filePath: util.cloneToken(this.tokens?.filePath)
3!
1424
                }
1425
            )
1426
        );
1427
    }
1428
}
1429

1430
export class NamespaceStatement extends Statement implements TypedefProvider {
1✔
1431
    constructor(
1432
        public keyword: Token,
269✔
1433
        // this should technically only be a VariableExpression or DottedGetExpression, but that can be enforced elsewhere
1434
        public nameExpression: NamespacedVariableNameExpression,
269✔
1435
        public body: Body,
269✔
1436
        public endKeyword: Token
269✔
1437
    ) {
1438
        super();
269✔
1439
        this.symbolTable = new SymbolTable(`NamespaceStatement: '${this.name}'`, () => this.parent?.getSymbolTable());
269!
1440
    }
1441

1442
    /**
1443
     * The string name for this namespace
1444
     */
1445
    public get name(): string {
1446
        return this.getName(ParseMode.BrighterScript);
932✔
1447
    }
1448

1449
    public get range() {
1450
        return this.cacheRange();
157✔
1451
    }
1452
    private _range: Range | undefined;
1453

1454
    public cacheRange() {
1455
        if (!this._range) {
425✔
1456
            this._range = util.createBoundingRange(
270✔
1457
                this.keyword,
1458
                this.nameExpression,
1459
                this.body,
1460
                this.endKeyword
1461
            );
1462
        }
1463
        return this._range;
425✔
1464
    }
1465

1466
    public getName(parseMode: ParseMode) {
1467
        const parentNamespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
3,236✔
1468
        let name = this.nameExpression?.getName?.(parseMode);
3,236✔
1469
        if (!name) {
3,236✔
1470
            return name;
1✔
1471
        }
1472

1473
        if (parentNamespace) {
3,235✔
1474
            const sep = parseMode === ParseMode.BrighterScript ? '.' : '_';
148✔
1475
            name = parentNamespace.getName(parseMode) + sep + name;
148✔
1476
        }
1477

1478
        return name;
3,235✔
1479
    }
1480

1481
    transpile(state: BrsTranspileState) {
1482
        //namespaces don't actually have any real content, so just transpile their bodies
1483
        return this.body.transpile(state);
39✔
1484
    }
1485

1486
    getTypedef(state: BrsTranspileState): TranspileResult {
1487
        let result = [
10✔
1488
            'namespace ',
1489
            ...this.getName(ParseMode.BrighterScript),
1490
            state.newline
1491
        ] as TranspileResult;
1492
        state.blockDepth++;
10✔
1493
        result.push(
10✔
1494
            ...this.body.getTypedef(state)
1495
        );
1496
        state.blockDepth--;
10✔
1497

1498
        result.push(
10✔
1499
            state.indent(),
1500
            'end namespace'
1501
        );
1502
        return result;
10✔
1503
    }
1504

1505
    walk(visitor: WalkVisitor, options: WalkOptions) {
1506
        if (options.walkMode & InternalWalkMode.walkExpressions) {
540✔
1507
            walk(this, 'nameExpression', visitor, options);
536✔
1508
        }
1509

1510
        if (this.body.statements.length > 0 && options.walkMode & InternalWalkMode.walkStatements) {
540✔
1511
            walk(this, 'body', visitor, options);
470✔
1512
        }
1513
    }
1514

1515
    public clone() {
1516
        const clone = this.finalizeClone(
3✔
1517
            new NamespaceStatement(
1518
                util.cloneToken(this.keyword),
1519
                this.nameExpression?.clone(),
9✔
1520
                this.body?.clone(),
9✔
1521
                util.cloneToken(this.endKeyword)
1522
            ),
1523
            ['nameExpression', 'body']
1524
        );
1525
        clone.cacheRange();
3✔
1526
        return clone;
3✔
1527
    }
1528
}
1529

1530
export class ImportStatement extends Statement implements TypedefProvider {
1✔
1531
    constructor(
1532
        readonly importToken: Token,
47✔
1533
        readonly filePathToken: Token | undefined
47✔
1534
    ) {
1535
        super();
47✔
1536
        this.range = util.createBoundingRange(
47✔
1537
            importToken,
1538
            filePathToken
1539
        );
1540
        if (this.filePathToken) {
47✔
1541
            //remove quotes
1542
            this.filePath = this.filePathToken.text.replace(/"/g, '');
45✔
1543
            if (this.filePathToken.range) {
45✔
1544
                //adjust the range to exclude the quotes
1545
                this.filePathToken.range = util.createRange(
42✔
1546
                    this.filePathToken.range.start.line,
1547
                    this.filePathToken.range.start.character + 1,
1548
                    this.filePathToken.range.end.line,
1549
                    this.filePathToken.range.end.character - 1
1550
                );
1551
            }
1552
        }
1553
    }
1554
    public filePath: string | undefined;
1555
    public range: Range | undefined;
1556

1557
    transpile(state: BrsTranspileState) {
1558
        //The xml files are responsible for adding the additional script imports, but
1559
        //add the import statement as a comment just for debugging purposes
1560
        return [
3✔
1561
            `'`,
1562
            state.transpileToken(this.importToken),
1563
            ' ',
1564
            state.transpileToken(this.filePathToken!)
1565
        ];
1566
    }
1567

1568
    /**
1569
     * Get the typedef for this statement
1570
     */
1571
    public getTypedef(state: BrsTranspileState) {
1572
        return [
3✔
1573
            this.importToken.text,
1574
            ' ',
1575
            //replace any `.bs` extension with `.brs`
1576
            this.filePathToken!.text.replace(/\.bs"?$/i, '.brs"')
1577
        ];
1578
    }
1579

1580
    walk(visitor: WalkVisitor, options: WalkOptions) {
1581
        //nothing to walk
1582
    }
1583

1584
    public clone() {
1585
        return this.finalizeClone(
1✔
1586
            new ImportStatement(
1587
                util.cloneToken(this.importToken),
1588
                util.cloneToken(this.filePathToken)
1589
            )
1590
        );
1591
    }
1592
}
1593

1594
export class InterfaceStatement extends Statement implements TypedefProvider {
1✔
1595
    constructor(
1596
        interfaceToken: Token,
1597
        name: Identifier,
1598
        extendsToken: Token,
1599
        public parentInterfaceName: NamespacedVariableNameExpression,
74✔
1600
        public body: Statement[],
74✔
1601
        endInterfaceToken: Token
1602
    ) {
1603
        super();
74✔
1604
        this.tokens.interface = interfaceToken;
74✔
1605
        this.tokens.name = name;
74✔
1606
        this.tokens.extends = extendsToken;
74✔
1607
        this.tokens.endInterface = endInterfaceToken;
74✔
1608
        this.range = util.createBoundingRange(
74✔
1609
            this.tokens.interface,
1610
            this.tokens.name,
1611
            this.tokens.extends,
1612
            this.parentInterfaceName,
1613
            ...this.body ?? [],
222✔
1614
            this.tokens.endInterface
1615
        );
1616
    }
1617

1618
    public tokens = {} as {
74✔
1619
        interface: Token;
1620
        name: Identifier;
1621
        extends: Token;
1622
        endInterface: Token;
1623
    };
1624

1625
    public range: Range | undefined;
1626

1627
    /**
1628
     * Get the name of the wrapping namespace (if it exists)
1629
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
1630
     */
1631
    public get namespaceName() {
1632
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
1633
    }
1634

1635
    public get fields() {
1636
        return this.body.filter(x => isInterfaceFieldStatement(x));
×
1637
    }
1638

1639
    public get methods() {
1640
        return this.body.filter(x => isInterfaceMethodStatement(x));
×
1641
    }
1642

1643
    /**
1644
     * The name of the interface WITH its leading namespace (if applicable)
1645
     */
1646
    public get fullName() {
1647
        const name = this.tokens.name?.text;
×
1648
        if (name) {
×
1649
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
×
1650
            if (namespace) {
×
1651
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
×
1652
                return `${namespaceName}.${name}`;
×
1653
            } else {
1654
                return name;
×
1655
            }
1656
        } else {
1657
            //return undefined which will allow outside callers to know that this interface doesn't have a name
1658
            return undefined;
×
1659
        }
1660
    }
1661

1662
    /**
1663
     * The name of the interface (without the namespace prefix)
1664
     */
1665
    public get name() {
1666
        return this.tokens.name?.text;
5!
1667
    }
1668

1669
    /**
1670
     * Get the name of this expression based on the parse mode
1671
     */
1672
    public getName(parseMode: ParseMode) {
1673
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
5✔
1674
        if (namespace) {
5✔
1675
            let delimiter = parseMode === ParseMode.BrighterScript ? '.' : '_';
1!
1676
            let namespaceName = namespace.getName(parseMode);
1✔
1677
            return namespaceName + delimiter + this.name;
1✔
1678
        } else {
1679
            return this.name;
4✔
1680
        }
1681
    }
1682

1683
    public transpile(state: BrsTranspileState): TranspileResult {
1684
        //interfaces should completely disappear at runtime
1685
        return [];
19✔
1686
    }
1687

1688
    getTypedef(state: BrsTranspileState) {
1689
        const result = [] as TranspileResult;
6✔
1690
        for (let annotation of this.annotations ?? []) {
6✔
1691
            result.push(
1✔
1692
                ...annotation.getTypedef(state),
1693
                state.newline,
1694
                state.indent()
1695
            );
1696
        }
1697
        result.push(
6✔
1698
            this.tokens.interface.text,
1699
            ' ',
1700
            this.tokens.name.text
1701
        );
1702
        const parentInterfaceName = this.parentInterfaceName?.getName(ParseMode.BrighterScript);
6!
1703
        if (parentInterfaceName) {
6!
1704
            result.push(
×
1705
                ' extends ',
1706
                parentInterfaceName
1707
            );
1708
        }
1709
        const body = this.body ?? [];
6!
1710
        if (body.length > 0) {
6!
1711
            state.blockDepth++;
6✔
1712
        }
1713
        for (const statement of body) {
6✔
1714
            if (isInterfaceMethodStatement(statement) || isInterfaceFieldStatement(statement)) {
21✔
1715
                result.push(
20✔
1716
                    state.newline,
1717
                    state.indent(),
1718
                    ...statement.getTypedef(state)
1719
                );
1720
            } else {
1721
                result.push(
1✔
1722
                    state.newline,
1723
                    state.indent(),
1724
                    ...statement.transpile(state)
1725
                );
1726
            }
1727
        }
1728
        if (body.length > 0) {
6!
1729
            state.blockDepth--;
6✔
1730
        }
1731
        result.push(
6✔
1732
            state.newline,
1733
            state.indent(),
1734
            'end interface',
1735
            state.newline
1736
        );
1737
        return result;
6✔
1738
    }
1739

1740
    walk(visitor: WalkVisitor, options: WalkOptions) {
1741
        //visitor-less walk function to do parent linking
1742
        walk(this, 'parentInterfaceName', null, options);
139✔
1743

1744
        if (options.walkMode & InternalWalkMode.walkStatements) {
139!
1745
            walkArray(this.body, visitor, options, this);
139✔
1746
        }
1747
    }
1748

1749
    public clone() {
1750
        return this.finalizeClone(
8✔
1751
            new InterfaceStatement(
1752
                util.cloneToken(this.tokens.interface),
1753
                util.cloneToken(this.tokens.name),
1754
                util.cloneToken(this.tokens.extends),
1755
                this.parentInterfaceName?.clone(),
24✔
1756
                this.body?.map(x => x?.clone()),
8✔
1757
                util.cloneToken(this.tokens.endInterface)
1758
            ),
1759
            ['parentInterfaceName', 'body']
1760
        );
1761
    }
1762
}
1763

1764
export class InterfaceFieldStatement extends Statement implements TypedefProvider {
1✔
1765
    public transpile(state: BrsTranspileState): TranspileResult {
1766
        throw new Error('Method not implemented.');
×
1767
    }
1768
    constructor(
1769
        nameToken: Identifier,
1770
        asToken: Token,
1771
        typeToken: Token,
1772
        public type: BscType,
79✔
1773
        optionalToken?: Token
1774
    ) {
1775
        super();
79✔
1776
        this.tokens.optional = optionalToken;
79✔
1777
        this.tokens.name = nameToken;
79✔
1778
        this.tokens.as = asToken;
79✔
1779
        this.tokens.type = typeToken;
79✔
1780
        this.range = util.createBoundingRange(
79✔
1781
            optionalToken,
1782
            nameToken,
1783
            asToken,
1784
            typeToken
1785
        );
1786
    }
1787

1788
    public range: Range | undefined;
1789

1790
    public tokens = {} as {
79✔
1791
        optional?: Token;
1792
        name: Identifier;
1793
        as: Token;
1794
        type: Token;
1795
    };
1796

1797
    public get name() {
1798
        return this.tokens.name.text;
×
1799
    }
1800

1801
    public get isOptional() {
1802
        return !!this.tokens.optional;
10✔
1803
    }
1804

1805
    walk(visitor: WalkVisitor, options: WalkOptions) {
1806
        //nothing to walk
1807
    }
1808

1809
    getTypedef(state: BrsTranspileState): TranspileResult {
1810
        const result = [] as TranspileResult;
10✔
1811
        for (let annotation of this.annotations ?? []) {
10✔
1812
            result.push(
1✔
1813
                ...annotation.getTypedef(state),
1814
                state.newline,
1815
                state.indent()
1816
            );
1817
        }
1818
        if (this.isOptional) {
10!
1819
            result.push(
×
1820
                this.tokens.optional!.text,
1821
                ' '
1822
            );
1823
        }
1824
        result.push(
10✔
1825
            this.tokens.name.text
1826
        );
1827
        if (this.tokens.type?.text?.length > 0) {
10!
1828
            result.push(
10✔
1829
                ' as ',
1830
                this.tokens.type.text
1831
            );
1832
        }
1833
        return result;
10✔
1834
    }
1835

1836
    public clone() {
1837
        return this.finalizeClone(
4✔
1838
            new InterfaceFieldStatement(
1839
                util.cloneToken(this.tokens.name),
1840
                util.cloneToken(this.tokens.as),
1841
                util.cloneToken(this.tokens.type),
1842
                this.type?.clone(),
12✔
1843
                util.cloneToken(this.tokens.optional)
1844
            )
1845
        );
1846
    }
1847

1848
}
1849

1850
export class InterfaceMethodStatement extends Statement implements TypedefProvider {
1✔
1851
    public transpile(state: BrsTranspileState): TranspileResult {
1852
        throw new Error('Method not implemented.');
×
1853
    }
1854
    constructor(
1855
        functionTypeToken: Token,
1856
        nameToken: Identifier,
1857
        leftParen: Token,
1858
        public params: FunctionParameterExpression[],
23✔
1859
        rightParen: Token,
1860
        asToken?: Token,
1861
        returnTypeToken?: Token,
1862
        public returnType?: BscType,
23✔
1863
        optionalToken?: Token
1864
    ) {
1865
        super();
23✔
1866
        this.tokens.optional = optionalToken;
23✔
1867
        this.tokens.functionType = functionTypeToken;
23✔
1868
        this.tokens.name = nameToken;
23✔
1869
        this.tokens.leftParen = leftParen;
23✔
1870
        this.tokens.rightParen = rightParen;
23✔
1871
        this.tokens.as = asToken;
23✔
1872
        this.tokens.returnType = returnTypeToken;
23✔
1873
    }
1874

1875
    public get range() {
1876
        return util.createBoundingRange(
2✔
1877
            this.tokens.optional,
1878
            this.tokens.functionType,
1879
            this.tokens.name,
1880
            this.tokens.leftParen,
1881
            ...(this.params ?? []),
6!
1882
            this.tokens.rightParen,
1883
            this.tokens.as,
1884
            this.tokens.returnType
1885
        );
1886
    }
1887

1888
    public tokens = {} as {
23✔
1889
        optional?: Token;
1890
        functionType: Token;
1891
        name: Identifier;
1892
        leftParen: Token;
1893
        rightParen: Token;
1894
        as: Token | undefined;
1895
        returnType: Token | undefined;
1896
    };
1897

1898
    public get isOptional() {
1899
        return !!this.tokens.optional;
10✔
1900
    }
1901

1902
    walk(visitor: WalkVisitor, options: WalkOptions) {
1903
        //nothing to walk
1904
    }
1905

1906
    getTypedef(state: BrsTranspileState) {
1907
        const result = [] as TranspileResult;
10✔
1908
        for (let annotation of this.annotations ?? []) {
10✔
1909
            result.push(
1✔
1910
                ...annotation.getTypedef(state),
1911
                state.newline,
1912
                state.indent()
1913
            );
1914
        }
1915

1916
        if (this.isOptional) {
10!
1917
            result.push(
×
1918
                this.tokens.optional!.text,
1919
                ' '
1920
            );
1921
        }
1922

1923
        result.push(
10✔
1924
            this.tokens.functionType.text,
1925
            ' ',
1926
            this.tokens.name.text,
1927
            '('
1928
        );
1929
        const params = this.params ?? [];
10!
1930
        for (let i = 0; i < params.length; i++) {
10✔
1931
            if (i > 0) {
2✔
1932
                result.push(', ');
1✔
1933
            }
1934
            const param = params[i];
2✔
1935
            result.push(param.name.text);
2✔
1936
            const typeToken = param.typeToken;
2✔
1937
            if (typeToken && typeToken.text.length > 0) {
2!
1938
                result.push(
2✔
1939
                    ' as ',
1940
                    typeToken.text
1941
                );
1942
            }
1943
        }
1944
        result.push(
10✔
1945
            ')'
1946
        );
1947
        const returnTypeToken = this.tokens.returnType;
10✔
1948
        if (returnTypeToken && returnTypeToken.text.length > 0) {
10!
1949
            result.push(
10✔
1950
                ' as ',
1951
                returnTypeToken.text
1952
            );
1953
        }
1954
        return result;
10✔
1955
    }
1956

1957
    public clone() {
1958
        return this.finalizeClone(
3✔
1959
            new InterfaceMethodStatement(
1960
                util.cloneToken(this.tokens.functionType),
1961
                util.cloneToken(this.tokens.name),
1962
                util.cloneToken(this.tokens.leftParen),
1963
                this.params?.map(p => p?.clone()),
3✔
1964
                util.cloneToken(this.tokens.rightParen),
1965
                util.cloneToken(this.tokens.as),
1966
                util.cloneToken(this.tokens.returnType),
1967
                this.returnType?.clone(),
9✔
1968
                util.cloneToken(this.tokens.optional)
1969
            ),
1970
            ['params']
1971
        );
1972
    }
1973
}
1974

1975
export class ClassStatement extends Statement implements TypedefProvider {
1✔
1976

1977
    constructor(
1978
        readonly classKeyword: Token,
498✔
1979
        /**
1980
         * The name of the class (without namespace prefix)
1981
         */
1982
        readonly name: Identifier,
498✔
1983
        public body: Statement[],
498✔
1984
        readonly end: Token,
498✔
1985
        readonly extendsKeyword?: Token,
498✔
1986
        readonly parentClassName?: NamespacedVariableNameExpression
498✔
1987
    ) {
1988
        super();
498✔
1989
        this.body = this.body ?? [];
498✔
1990

1991
        for (let statement of this.body) {
498✔
1992
            if (isMethodStatement(statement)) {
467✔
1993
                this.methods.push(statement);
271✔
1994
                this.memberMap[statement?.name?.text.toLowerCase()] = statement;
271!
1995
            } else if (isFieldStatement(statement)) {
196✔
1996
                this.fields.push(statement);
187✔
1997
                this.memberMap[statement.name?.text.toLowerCase()] = statement;
187!
1998
            }
1999
        }
2000

2001
        this.range = util.createBoundingRange(
498✔
2002
            classKeyword,
2003
            name,
2004
            extendsKeyword,
2005
            parentClassName,
2006
            ...(body ?? []),
1,494✔
2007
            end
2008
        );
2009
    }
2010

2011
    /**
2012
     * Get the name of the wrapping namespace (if it exists)
2013
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
2014
     */
2015
    public get namespaceName() {
2016
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
2017
    }
2018

2019

2020
    public getName(parseMode: ParseMode) {
2021
        const name = this.name?.text;
879✔
2022
        if (name) {
879✔
2023
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
877✔
2024
            if (namespace) {
877✔
2025
                let namespaceName = namespace.getName(parseMode);
236✔
2026
                let separator = parseMode === ParseMode.BrighterScript ? '.' : '_';
236✔
2027
                return namespaceName + separator + name;
236✔
2028
            } else {
2029
                return name;
641✔
2030
            }
2031
        } else {
2032
            //return undefined which will allow outside callers to know that this class doesn't have a name
2033
            return undefined;
2✔
2034
        }
2035
    }
2036

2037
    public memberMap = {} as Record<string, MemberStatement>;
498✔
2038
    public methods = [] as MethodStatement[];
498✔
2039
    public fields = [] as FieldStatement[];
498✔
2040

2041
    public readonly range: Range | undefined;
2042

2043
    transpile(state: BrsTranspileState) {
2044
        let result = [] as TranspileResult;
48✔
2045

2046
        const className = this.getName(ParseMode.BrightScript).replace(/\./g, '_');
48✔
2047
        const ancestors = this.getAncestors(state);
48✔
2048
        const body = this.getTranspiledClassBody(ancestors);
48✔
2049

2050
        //make the methods
2051
        result.push(...this.getTranspiledMethods(state, className, body));
48✔
2052
        //make the builder
2053
        result.push(...this.getTranspiledBuilder(state, className, ancestors, body));
48✔
2054
        result.push('\n', state.indent());
48✔
2055
        //make the class assembler (i.e. the public-facing class creator method)
2056
        result.push(...this.getTranspiledClassFunction(state, className));
48✔
2057

2058
        return result;
48✔
2059
    }
2060

2061
    getTypedef(state: BrsTranspileState) {
2062
        const result = [] as TranspileResult;
15✔
2063
        for (let annotation of this.annotations ?? []) {
15!
2064
            result.push(
×
2065
                ...annotation.getTypedef(state),
2066
                state.newline,
2067
                state.indent()
2068
            );
2069
        }
2070
        result.push(
15✔
2071
            'class ',
2072
            this.name.text
2073
        );
2074
        if (this.extendsKeyword && this.parentClassName) {
15✔
2075
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
4✔
2076
            const fqName = util.getFullyQualifiedClassName(
4✔
2077
                this.parentClassName.getName(ParseMode.BrighterScript),
2078
                namespace?.getName(ParseMode.BrighterScript)
12✔
2079
            );
2080
            result.push(
4✔
2081
                ` extends ${fqName}`
2082
            );
2083
        }
2084
        result.push(state.newline);
15✔
2085
        state.blockDepth++;
15✔
2086

2087
        let body = this.body;
15✔
2088
        //inject an empty "new" method if missing
2089
        if (!this.getConstructorFunction()) {
15✔
2090
            const constructor = createMethodStatement('new', TokenKind.Sub);
11✔
2091
            constructor.parent = this;
11✔
2092
            //walk the constructor to set up parent links
2093
            constructor.link();
11✔
2094
            body = [
11✔
2095
                constructor,
2096
                ...this.body
2097
            ];
2098
        }
2099

2100
        for (const member of body) {
15✔
2101
            if (isTypedefProvider(member)) {
33!
2102
                result.push(
33✔
2103
                    state.indent(),
2104
                    ...member.getTypedef(state),
2105
                    state.newline
2106
                );
2107
            }
2108
        }
2109
        state.blockDepth--;
15✔
2110
        result.push(
15✔
2111
            state.indent(),
2112
            'end class'
2113
        );
2114
        return result;
15✔
2115
    }
2116

2117
    /**
2118
     * Find the parent index for this class's parent.
2119
     * For class inheritance, every class is given an index.
2120
     * The base class is index 0, its child is index 1, and so on.
2121
     */
2122
    public getParentClassIndex(state: BrsTranspileState) {
2123
        let myIndex = 0;
109✔
2124
        let stmt = this as ClassStatement;
109✔
2125
        while (stmt) {
109✔
2126
            if (stmt.parentClassName) {
182✔
2127
                const namespace = stmt.findAncestor<NamespaceStatement>(isNamespaceStatement);
74✔
2128
                //find the parent class
2129
                stmt = state.file.getClassFileLink(
74✔
2130
                    stmt.parentClassName.getName(ParseMode.BrighterScript),
2131
                    namespace?.getName(ParseMode.BrighterScript)
222✔
2132
                )?.item;
74✔
2133
                myIndex++;
74✔
2134
            } else {
2135
                break;
108✔
2136
            }
2137
        }
2138
        const result = myIndex - 1;
109✔
2139
        if (result >= 0) {
109✔
2140
            return result;
55✔
2141
        } else {
2142
            return null;
54✔
2143
        }
2144
    }
2145

2146
    public hasParentClass() {
2147
        return !!this.parentClassName;
48✔
2148
    }
2149

2150
    /**
2151
     * Get all ancestor classes, in closest-to-furthest order (i.e. 0 is parent, 1 is grandparent, etc...).
2152
     * This will return an empty array if no ancestors were found
2153
     */
2154
    public getAncestors(state: BrsTranspileState) {
2155
        let ancestors = [] as ClassStatement[];
122✔
2156
        let stmt = this as ClassStatement;
122✔
2157
        while (stmt) {
122✔
2158
            if (stmt.parentClassName) {
210✔
2159
                const namespace = stmt.findAncestor<NamespaceStatement>(isNamespaceStatement);
88✔
2160
                stmt = state.file.getClassFileLink(
88✔
2161
                    stmt.parentClassName.getName(ParseMode.BrighterScript),
2162
                    namespace?.getName(ParseMode.BrighterScript)
264✔
2163
                )?.item;
88!
2164
                ancestors.push(stmt);
88✔
2165
            } else {
2166
                break;
122✔
2167
            }
2168
        }
2169
        return ancestors;
122✔
2170
    }
2171

2172
    private getBuilderName(transpiledClassName: string) {
2173
        return `__${transpiledClassName}_builder`;
120✔
2174
    }
2175

2176
    private getMethodIdentifier(transpiledClassName: string, statement: MethodStatement) {
2177
        return { ...statement.name, text: `__${transpiledClassName}_method_${statement.name.text}` };
120✔
2178
    }
2179

2180
    /**
2181
     * Get the constructor function for this class (if exists), or undefined if not exist
2182
     */
2183
    private getConstructorFunction() {
2184
        return this.body.find((stmt) => {
151✔
2185
            return (stmt as MethodStatement)?.name?.text?.toLowerCase() === 'new';
132!
2186
        }) as MethodStatement;
2187
    }
2188

2189
    /**
2190
     * Return the parameters for the first constructor function for this class
2191
     * @param ancestors The list of ancestors for this class
2192
     * @returns The parameters for the first constructor function for this class
2193
     */
2194
    private getConstructorParams(ancestors: ClassStatement[]) {
2195
        for (let ancestor of ancestors) {
41✔
2196
            const ctor = ancestor?.getConstructorFunction();
40!
2197
            if (ctor) {
40✔
2198
                return ctor.func.parameters;
16✔
2199
            }
2200
        }
2201
        return [];
25✔
2202
    }
2203

2204
    /**
2205
     * Determine if the specified field was declared in one of the ancestor classes
2206
     */
2207
    public isFieldDeclaredByAncestor(fieldName: string, ancestors: ClassStatement[]) {
2208
        let lowerFieldName = fieldName.toLowerCase();
×
2209
        for (let ancestor of ancestors) {
×
2210
            if (ancestor.memberMap[lowerFieldName]) {
×
2211
                return true;
×
2212
            }
2213
        }
2214
        return false;
×
2215
    }
2216

2217
    /**
2218
     * The builder is a function that assigns all of the methods and property names to a class instance.
2219
     * This needs to be a separate function so that child classes can call the builder from their parent
2220
     * without instantiating the parent constructor at that point in time.
2221
     */
2222
    private getTranspiledBuilder(state: BrsTranspileState, transpiledClassName: string, ancestors: ClassStatement[], body: Statement[]) {
2223
        let result = [] as TranspileResult;
48✔
2224
        result.push(`function ${this.getBuilderName(transpiledClassName)}()\n`);
48✔
2225
        state.blockDepth++;
48✔
2226
        //indent
2227
        result.push(state.indent());
48✔
2228

2229
        //construct parent class or empty object
2230
        if (ancestors[0]) {
48✔
2231
            const ancestorNamespace = ancestors[0].findAncestor<NamespaceStatement>(isNamespaceStatement);
24✔
2232
            let fullyQualifiedClassName = util.getFullyQualifiedClassName(
24✔
2233
                ancestors[0].getName(ParseMode.BrighterScript)!,
2234
                ancestorNamespace?.getName(ParseMode.BrighterScript)
72✔
2235
            );
2236
            result.push(`instance = ${this.getBuilderName(fullyQualifiedClassName.replace(/\./g, '_'))}()`);
24✔
2237
        } else {
2238
            //use an empty object.
2239
            result.push('instance = {}');
24✔
2240
        }
2241
        result.push(
48✔
2242
            state.newline,
2243
            state.indent()
2244
        );
2245
        let parentClassIndex = this.getParentClassIndex(state);
48✔
2246

2247
        for (let statement of body) {
48✔
2248
            //is field statement
2249
            if (isFieldStatement(statement)) {
71✔
2250
                //do nothing with class fields in this situation, they are handled elsewhere
2251
                continue;
11✔
2252

2253
                //methods
2254
            } else if (isMethodStatement(statement)) {
60!
2255

2256
                //store overridden parent methods as super{parentIndex}_{methodName}
2257
                if (
60✔
2258
                    //is override method
2259
                    statement.override ||
164✔
2260
                    //is constructor function in child class
2261
                    (statement.name.text.toLowerCase() === 'new' && ancestors[0])
2262
                ) {
2263
                    result.push(
28✔
2264
                        `instance.super${parentClassIndex}_${statement.name.text} = instance.${statement.name.text}`,
2265
                        state.newline,
2266
                        state.indent()
2267
                    );
2268
                }
2269

2270
                state.classStatement = this;
60✔
2271
                result.push(
60✔
2272
                    'instance.',
2273
                    state.transpileToken(statement.name),
2274
                    ' = ',
2275
                    state.transpileToken(this.getMethodIdentifier(transpiledClassName, statement)),
2276
                    state.newline,
2277
                    state.indent()
2278
                );
2279
                delete state.classStatement;
60✔
2280
            } else {
2281
                //other random statements (probably just comments)
2282
                result.push(
×
2283
                    ...statement.transpile(state),
2284
                    state.newline,
2285
                    state.indent()
2286
                );
2287
            }
2288
        }
2289
        //return the instance
2290
        result.push('return instance\n');
48✔
2291
        state.blockDepth--;
48✔
2292
        result.push(state.indent());
48✔
2293
        result.push(`end function`);
48✔
2294
        return result;
48✔
2295
    }
2296

2297
    /**
2298
     * Returns a copy of the class' body, with the constructor function added if it doesn't exist.
2299
     */
2300
    private getTranspiledClassBody(ancestors: ClassStatement[]): Statement[] {
2301
        const body = [];
48✔
2302
        body.push(...this.body);
48✔
2303

2304
        //inject an empty "new" method if missing
2305
        if (!this.getConstructorFunction()) {
48✔
2306
            if (ancestors.length === 0) {
26✔
2307
                body.unshift(createMethodStatement('new', TokenKind.Sub));
11✔
2308
            } else {
2309
                const params = this.getConstructorParams(ancestors);
15✔
2310
                const call = new ExpressionStatement(
15✔
2311
                    new CallExpression(
2312
                        new VariableExpression(createToken(TokenKind.Identifier, 'super')),
2313
                        createToken(TokenKind.LeftParen),
2314
                        createToken(TokenKind.RightParen),
2315
                        params.map(x => new VariableExpression(x.name))
6✔
2316
                    )
2317
                );
2318
                body.unshift(
15✔
2319
                    new MethodStatement(
2320
                        [],
2321
                        createIdentifier('new'),
2322
                        new FunctionExpression(
2323
                            params.map(x => x.clone()),
6✔
2324
                            new Block([call]),
2325
                            createToken(TokenKind.Sub),
2326
                            createToken(TokenKind.EndSub),
2327
                            createToken(TokenKind.LeftParen),
2328
                            createToken(TokenKind.RightParen)
2329
                        ),
2330
                        null
2331
                    )
2332
                );
2333
            }
2334
        }
2335

2336
        return body;
48✔
2337
    }
2338

2339
    /**
2340
     * These are the methods that are defined in this class. They are transpiled outside of the class body
2341
     * to ensure they don't appear as "$anon_#" in stack traces and crash logs.
2342
     */
2343
    private getTranspiledMethods(state: BrsTranspileState, transpiledClassName: string, body: Statement[]) {
2344
        let result = [] as TranspileResult;
48✔
2345
        for (let statement of body) {
48✔
2346
            if (isMethodStatement(statement)) {
71✔
2347
                state.classStatement = this;
60✔
2348
                result.push(
60✔
2349
                    ...statement.transpile(state, this.getMethodIdentifier(transpiledClassName, statement)),
2350
                    state.newline,
2351
                    state.indent()
2352
                );
2353
                delete state.classStatement;
60✔
2354
            }
2355
        }
2356
        return result;
48✔
2357
    }
2358

2359
    /**
2360
     * The class function is the function with the same name as the class. This is the function that
2361
     * consumers should call to create a new instance of that class.
2362
     * This invokes the builder, gets an instance of the class, then invokes the "new" function on that class.
2363
     */
2364
    private getTranspiledClassFunction(state: BrsTranspileState, transpiledClassName: string) {
2365
        let result = [] as TranspileResult;
48✔
2366

2367
        const constructorFunction = this.getConstructorFunction();
48✔
2368
        let constructorParams = [];
48✔
2369
        if (constructorFunction) {
48✔
2370
            constructorParams = constructorFunction.func.parameters;
22✔
2371
        } else {
2372
            constructorParams = this.getConstructorParams(this.getAncestors(state));
26✔
2373
        }
2374

2375
        result.push(
48✔
2376
            state.sourceNode(this.classKeyword, 'function'),
2377
            state.sourceNode(this.classKeyword, ' '),
2378
            state.sourceNode(this.name, this.getName(ParseMode.BrightScript)!),
2379
            `(`
2380
        );
2381
        let i = 0;
48✔
2382
        for (let param of constructorParams) {
48✔
2383
            if (i > 0) {
17✔
2384
                result.push(', ');
4✔
2385
            }
2386
            result.push(
17✔
2387
                param.transpile(state)
2388
            );
2389
            i++;
17✔
2390
        }
2391
        result.push(
48✔
2392
            ')',
2393
            '\n'
2394
        );
2395

2396
        state.blockDepth++;
48✔
2397
        result.push(state.indent());
48✔
2398
        result.push(`instance = ${this.getBuilderName(transpiledClassName)}()\n`);
48✔
2399

2400
        result.push(state.indent());
48✔
2401
        result.push(`instance.new(`);
48✔
2402

2403
        //append constructor arguments
2404
        i = 0;
48✔
2405
        for (let param of constructorParams) {
48✔
2406
            if (i > 0) {
17✔
2407
                result.push(', ');
4✔
2408
            }
2409
            result.push(
17✔
2410
                state.transpileToken(param.name)
2411
            );
2412
            i++;
17✔
2413
        }
2414
        result.push(
48✔
2415
            ')',
2416
            '\n'
2417
        );
2418

2419
        result.push(state.indent());
48✔
2420
        result.push(`return instance\n`);
48✔
2421

2422
        state.blockDepth--;
48✔
2423
        result.push(state.indent());
48✔
2424
        result.push(`end function`);
48✔
2425
        return result;
48✔
2426
    }
2427

2428
    walk(visitor: WalkVisitor, options: WalkOptions) {
2429
        //visitor-less walk function to do parent linking
2430
        walk(this, 'parentClassName', null, options);
979✔
2431

2432
        if (options.walkMode & InternalWalkMode.walkStatements) {
979!
2433
            walkArray(this.body, visitor, options, this);
979✔
2434
        }
2435
    }
2436

2437
    public clone() {
2438
        return this.finalizeClone(
11✔
2439
            new ClassStatement(
2440
                util.cloneToken(this.classKeyword),
2441
                util.cloneToken(this.name),
2442
                this.body?.map(x => x?.clone()),
10✔
2443
                util.cloneToken(this.end),
2444
                util.cloneToken(this.extendsKeyword),
2445
                this.parentClassName?.clone()
33✔
2446
            ),
2447
            ['body', 'parentClassName']
2448
        );
2449
    }
2450
}
2451

2452
const accessModifiers = [
1✔
2453
    TokenKind.Public,
2454
    TokenKind.Protected,
2455
    TokenKind.Private
2456
];
2457
export class MethodStatement extends FunctionStatement {
1✔
2458
    constructor(
2459
        modifiers: Token | Token[],
2460
        name: Identifier,
2461
        func: FunctionExpression,
2462
        public override: Token
308✔
2463
    ) {
2464
        super(name, func);
308✔
2465
        if (modifiers) {
308✔
2466
            if (Array.isArray(modifiers)) {
50✔
2467
                this.modifiers.push(...modifiers);
19✔
2468
            } else {
2469
                this.modifiers.push(modifiers);
31✔
2470
            }
2471
        }
2472
        this.range = util.createBoundingRange(
308✔
2473
            ...(this.modifiers),
2474
            override,
2475
            func
2476
        );
2477
    }
2478

2479
    public modifiers: Token[] = [];
308✔
2480

2481
    public get accessModifier() {
2482
        return this.modifiers.find(x => accessModifiers.includes(x.kind));
102✔
2483
    }
2484

2485
    public readonly range: Range | undefined;
2486

2487
    /**
2488
     * Get the name of this method.
2489
     */
2490
    public getName(parseMode: ParseMode) {
2491
        return this.name.text;
1✔
2492
    }
2493

2494
    transpile(state: BrsTranspileState, name?: Identifier) {
2495
        if (this.name.text.toLowerCase() === 'new') {
60✔
2496
            this.ensureSuperConstructorCall(state);
48✔
2497
            //TODO we need to undo this at the bottom of this method
2498
            this.injectFieldInitializersForConstructor(state);
48✔
2499
        }
2500
        //TODO - remove type information from these methods because that doesn't work
2501
        //convert the `super` calls into the proper methods
2502
        const parentClassIndex = state.classStatement.getParentClassIndex(state);
60✔
2503
        const visitor = createVisitor({
60✔
2504
            VariableExpression: e => {
2505
                if (e.name.text.toLocaleLowerCase() === 'super') {
67✔
2506
                    state.editor.setProperty(e.name, 'text', `m.super${parentClassIndex}_new`);
24✔
2507
                }
2508
            },
2509
            DottedGetExpression: e => {
2510
                const beginningVariable = util.findBeginningVariableExpression(e);
25✔
2511
                const lowerName = beginningVariable?.getName(ParseMode.BrighterScript).toLowerCase();
25!
2512
                if (lowerName === 'super') {
25✔
2513
                    state.editor.setProperty(beginningVariable.name, 'text', 'm');
7✔
2514
                    state.editor.setProperty(e.name, 'text', `super${parentClassIndex}_${e.name.text}`);
7✔
2515
                }
2516
            }
2517
        });
2518
        const walkOptions: WalkOptions = { walkMode: WalkMode.visitExpressions };
60✔
2519
        for (const statement of this.func.body.statements) {
60✔
2520
            visitor(statement, undefined);
64✔
2521
            statement.walk(visitor, walkOptions);
64✔
2522
        }
2523
        return this.func.transpile(state, name);
60✔
2524
    }
2525

2526
    getTypedef(state: BrsTranspileState) {
2527
        const result = [] as TranspileResult;
23✔
2528
        for (let annotation of this.annotations ?? []) {
23✔
2529
            result.push(
2✔
2530
                ...annotation.getTypedef(state),
2531
                state.newline,
2532
                state.indent()
2533
            );
2534
        }
2535
        if (this.accessModifier) {
23✔
2536
            result.push(
8✔
2537
                this.accessModifier.text,
2538
                ' '
2539
            );
2540
        }
2541
        if (this.override) {
23✔
2542
            result.push('override ');
1✔
2543
        }
2544
        result.push(
23✔
2545
            ...this.func.getTypedef(state)
2546
        );
2547
        return result;
23✔
2548
    }
2549

2550
    /**
2551
     * All child classes must call the parent constructor. The type checker will warn users when they don't call it in their own class,
2552
     * but we still need to call it even if they have omitted it. This injects the super call if it's missing
2553
     */
2554
    private ensureSuperConstructorCall(state: BrsTranspileState) {
2555
        //if this class doesn't extend another class, quit here
2556
        if (state.classStatement!.getAncestors(state).length === 0) {
48✔
2557
            return;
24✔
2558
        }
2559

2560
        //check whether any calls to super exist
2561
        let containsSuperCall =
2562
            this.func.body.statements.findIndex((x) => {
24✔
2563
                //is a call statement
2564
                return isExpressionStatement(x) && isCallExpression(x.expression) &&
25✔
2565
                    //is a call to super
2566
                    util.findBeginningVariableExpression(x.expression.callee as any)?.name.text.toLowerCase() === 'super';
69!
2567
            }) !== -1;
2568

2569
        //if a call to super exists, quit here
2570
        if (containsSuperCall) {
24✔
2571
            return;
23✔
2572
        }
2573

2574
        //this is a child class, and the constructor doesn't contain a call to super. Inject one
2575
        const superCall = new ExpressionStatement(
1✔
2576
            new CallExpression(
2577
                new VariableExpression(
2578
                    {
2579
                        kind: TokenKind.Identifier,
2580
                        text: 'super',
2581
                        isReserved: false,
2582
                        range: state.classStatement!.name.range,
2583
                        leadingWhitespace: ''
2584
                    }
2585
                ),
2586
                {
2587
                    kind: TokenKind.LeftParen,
2588
                    text: '(',
2589
                    isReserved: false,
2590
                    range: state.classStatement!.name.range,
2591
                    leadingWhitespace: ''
2592
                },
2593
                {
2594
                    kind: TokenKind.RightParen,
2595
                    text: ')',
2596
                    isReserved: false,
2597
                    range: state.classStatement!.name.range,
2598
                    leadingWhitespace: ''
2599
                },
2600
                []
2601
            )
2602
        );
2603
        state.editor.arrayUnshift(this.func.body.statements, superCall);
1✔
2604
    }
2605

2606
    /**
2607
     * Inject field initializers at the top of the `new` function (after any present `super()` call)
2608
     */
2609
    private injectFieldInitializersForConstructor(state: BrsTranspileState) {
2610
        let startingIndex = state.classStatement!.hasParentClass() ? 1 : 0;
48✔
2611

2612
        let newStatements = [] as Statement[];
48✔
2613
        //insert the field initializers in order
2614
        for (let field of state.classStatement!.fields) {
48✔
2615
            let thisQualifiedName = { ...field.name };
11✔
2616
            thisQualifiedName.text = 'm.' + field.name?.text;
11!
2617
            if (field.initialValue) {
11✔
2618
                newStatements.push(
9✔
2619
                    new AssignmentStatement(field.equal, thisQualifiedName, field.initialValue)
2620
                );
2621
            } else {
2622
                //if there is no initial value, set the initial value to `invalid`
2623
                newStatements.push(
2✔
2624
                    new AssignmentStatement(
2625
                        createToken(TokenKind.Equal, '=', field.name?.range),
6!
2626
                        thisQualifiedName,
2627
                        createInvalidLiteral('invalid', field.name?.range)
6!
2628
                    )
2629
                );
2630
            }
2631
        }
2632
        state.editor.arraySplice(this.func.body.statements, startingIndex, 0, ...newStatements);
48✔
2633
    }
2634

2635
    walk(visitor: WalkVisitor, options: WalkOptions) {
2636
        if (options.walkMode & InternalWalkMode.walkExpressions) {
795✔
2637
            walk(this, 'func', visitor, options);
590✔
2638
        }
2639
    }
2640

2641
    public clone() {
2642
        return this.finalizeClone(
5✔
2643
            new MethodStatement(
2644
                this.modifiers?.map(m => util.cloneToken(m)),
1✔
2645
                util.cloneToken(this.name),
2646
                this.func?.clone(),
15✔
2647
                util.cloneToken(this.override)
2648
            ),
2649
            ['func']
2650
        );
2651
    }
2652
}
2653
/**
2654
 * @deprecated use `MethodStatement`
2655
 */
2656
export class ClassMethodStatement extends MethodStatement { }
1✔
2657

2658
export class FieldStatement extends Statement implements TypedefProvider {
1✔
2659

2660
    constructor(
2661
        readonly accessModifier?: Token,
187✔
2662
        readonly name?: Identifier,
187✔
2663
        readonly as?: Token,
187✔
2664
        readonly type?: Token,
187✔
2665
        readonly equal?: Token,
187✔
2666
        readonly initialValue?: Expression,
187✔
2667
        readonly optional?: Token
187✔
2668
    ) {
2669
        super();
187✔
2670
        this.range = util.createBoundingRange(
187✔
2671
            accessModifier,
2672
            name,
2673
            as,
2674
            type,
2675
            equal,
2676
            initialValue
2677
        );
2678
    }
2679

2680
    /**
2681
     * Derive a ValueKind from the type token, or the initial value.
2682
     * Defaults to `DynamicType`
2683
     */
2684
    getType() {
2685
        if (this.type) {
77✔
2686
            return util.tokenToBscType(this.type);
38✔
2687
        } else if (isLiteralExpression(this.initialValue)) {
39✔
2688
            return this.initialValue.type;
25✔
2689
        } else {
2690
            return new DynamicType();
14✔
2691
        }
2692
    }
2693

2694
    public readonly range: Range | undefined;
2695

2696
    public get isOptional() {
2697
        return !!this.optional;
17✔
2698
    }
2699

2700
    transpile(state: BrsTranspileState): TranspileResult {
2701
        throw new Error('transpile not implemented for ' + Object.getPrototypeOf(this).constructor.name);
×
2702
    }
2703

2704
    getTypedef(state: BrsTranspileState) {
2705
        const result = [] as TranspileResult;
10✔
2706
        if (this.name) {
10!
2707
            for (let annotation of this.annotations ?? []) {
10✔
2708
                result.push(
2✔
2709
                    ...annotation.getTypedef(state),
2710
                    state.newline,
2711
                    state.indent()
2712
                );
2713
            }
2714

2715
            let type = this.getType();
10✔
2716
            if (isInvalidType(type) || isVoidType(type)) {
10✔
2717
                type = new DynamicType();
1✔
2718
            }
2719

2720
            result.push(
10✔
2721
                this.accessModifier?.text ?? 'public',
60!
2722
                ' '
2723
            );
2724
            if (this.isOptional) {
10!
2725
                result.push(this.optional!.text, ' ');
×
2726
            }
2727
            result.push(this.name?.text,
10!
2728
                ' as ',
2729
                type.toTypeString()
2730
            );
2731
        }
2732
        return result;
10✔
2733
    }
2734

2735
    walk(visitor: WalkVisitor, options: WalkOptions) {
2736
        if (this.initialValue && options.walkMode & InternalWalkMode.walkExpressions) {
258✔
2737
            walk(this, 'initialValue', visitor, options);
71✔
2738
        }
2739
    }
2740

2741
    public clone() {
2742
        return this.finalizeClone(
4✔
2743
            new FieldStatement(
2744
                util.cloneToken(this.accessModifier),
2745
                util.cloneToken(this.name),
2746
                util.cloneToken(this.as),
2747
                util.cloneToken(this.type),
2748
                util.cloneToken(this.equal),
2749
                this.initialValue?.clone(),
12✔
2750
                util.cloneToken(this.optional)
2751
            ),
2752
            ['initialValue']
2753
        );
2754
    }
2755
}
2756

2757
/**
2758
 * @deprecated use `FieldStatement`
2759
 */
2760
export class ClassFieldStatement extends FieldStatement { }
1✔
2761

2762
export type MemberStatement = FieldStatement | MethodStatement;
2763

2764
/**
2765
 * @deprecated use `MemeberStatement`
2766
 */
2767
export type ClassMemberStatement = MemberStatement;
2768

2769
export class TryCatchStatement extends Statement {
1✔
2770
    constructor(
2771
        public tokens: {
31✔
2772
            try: Token;
2773
            endTry?: Token;
2774
        },
2775
        public tryBranch?: Block,
31✔
2776
        public catchStatement?: CatchStatement
31✔
2777
    ) {
2778
        super();
31✔
2779
        this.range = util.createBoundingRange(
31✔
2780
            tokens.try,
2781
            tryBranch,
2782
            catchStatement,
2783
            tokens.endTry
2784
        );
2785
    }
2786

2787
    public readonly range: Range | undefined;
2788

2789
    public transpile(state: BrsTranspileState): TranspileResult {
2790
        return [
3✔
2791
            state.transpileToken(this.tokens.try),
2792
            ...this.tryBranch!.transpile(state),
2793
            state.newline,
2794
            state.indent(),
2795
            ...(this.catchStatement?.transpile(state) ?? ['catch']),
18!
2796
            state.newline,
2797
            state.indent(),
2798
            state.transpileToken(this.tokens.endTry!)
2799
        ] as TranspileResult;
2800
    }
2801

2802
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2803
        if (this.tryBranch && options.walkMode & InternalWalkMode.walkStatements) {
43✔
2804
            walk(this, 'tryBranch', visitor, options);
42✔
2805
            walk(this, 'catchStatement', visitor, options);
42✔
2806
        }
2807
    }
2808

2809
    public clone() {
2810
        return this.finalizeClone(
3✔
2811
            new TryCatchStatement(
2812
                {
2813
                    try: util.cloneToken(this.tokens.try),
2814
                    endTry: util.cloneToken(this.tokens.endTry)
2815
                },
2816
                this.tryBranch?.clone(),
9✔
2817
                this.catchStatement?.clone()
9✔
2818
            ),
2819
            ['tryBranch', 'catchStatement']
2820
        );
2821
    }
2822
}
2823

2824
export class CatchStatement extends Statement {
1✔
2825
    constructor(
2826
        public tokens: {
28✔
2827
            catch: Token;
2828
        },
2829
        public exceptionVariable?: Identifier,
28✔
2830
        public catchBranch?: Block
28✔
2831
    ) {
2832
        super();
28✔
2833
        this.range = util.createBoundingRange(
28✔
2834
            tokens.catch,
2835
            exceptionVariable,
2836
            catchBranch
2837
        );
2838
    }
2839

2840
    public range: Range | undefined;
2841

2842
    public transpile(state: BrsTranspileState): TranspileResult {
2843
        return [
3✔
2844
            state.transpileToken(this.tokens.catch),
2845
            ' ',
2846
            this.exceptionVariable?.text ?? 'e',
18!
2847
            ...(this.catchBranch?.transpile(state) ?? [])
18!
2848
        ];
2849
    }
2850

2851
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2852
        if (this.catchBranch && options.walkMode & InternalWalkMode.walkStatements) {
39✔
2853
            walk(this, 'catchBranch', visitor, options);
38✔
2854
        }
2855
    }
2856

2857
    public clone() {
2858
        return this.finalizeClone(
2✔
2859
            new CatchStatement(
2860
                {
2861
                    catch: util.cloneToken(this.tokens.catch)
2862
                },
2863
                util.cloneToken(this.exceptionVariable),
2864
                this.catchBranch?.clone()
6✔
2865
            ),
2866
            ['catchBranch']
2867
        );
2868
    }
2869
}
2870

2871
export class ThrowStatement extends Statement {
1✔
2872
    constructor(
2873
        public throwToken: Token,
12✔
2874
        public expression?: Expression
12✔
2875
    ) {
2876
        super();
12✔
2877
        this.range = util.createBoundingRange(
12✔
2878
            throwToken,
2879
            expression
2880
        );
2881
    }
2882
    public range: Range | undefined;
2883

2884
    public transpile(state: BrsTranspileState) {
2885
        const result = [
4✔
2886
            state.transpileToken(this.throwToken),
2887
            ' '
2888
        ] as TranspileResult;
2889

2890
        //if we have an expression, transpile it
2891
        if (this.expression) {
4!
2892
            result.push(
4✔
2893
                ...this.expression.transpile(state)
2894
            );
2895

2896
            //no expression found. Rather than emit syntax errors, provide a generic error message
2897
        } else {
2898
            result.push('"An error has occurred"');
×
2899
        }
2900
        return result;
4✔
2901
    }
2902

2903
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2904
        if (this.expression && options.walkMode & InternalWalkMode.walkExpressions) {
23✔
2905
            walk(this, 'expression', visitor, options);
16✔
2906
        }
2907
    }
2908

2909
    public clone() {
2910
        return this.finalizeClone(
2✔
2911
            new ThrowStatement(
2912
                util.cloneToken(this.throwToken),
2913
                this.expression?.clone()
6✔
2914
            ),
2915
            ['expression']
2916
        );
2917
    }
2918
}
2919

2920

2921
export class EnumStatement extends Statement implements TypedefProvider {
1✔
2922

2923
    constructor(
2924
        public tokens: {
125✔
2925
            enum: Token;
2926
            name: Identifier;
2927
            endEnum: Token;
2928
        },
2929
        public body: Array<EnumMemberStatement | CommentStatement>
125✔
2930
    ) {
2931
        super();
125✔
2932
        this.body = this.body ?? [];
125✔
2933
    }
2934

2935
    public get range(): Range | undefined {
2936
        return util.createBoundingRange(
15✔
2937
            this.tokens.enum,
2938
            this.tokens.name,
2939
            ...this.body,
2940
            this.tokens.endEnum
2941
        );
2942
    }
2943

2944
    /**
2945
     * Get the name of the wrapping namespace (if it exists)
2946
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
2947
     */
2948
    public get namespaceName() {
2949
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
2950
    }
2951

2952
    public getMembers() {
2953
        const result = [] as EnumMemberStatement[];
152✔
2954
        for (const statement of this.body) {
152✔
2955
            if (isEnumMemberStatement(statement)) {
327✔
2956
                result.push(statement);
314✔
2957
            }
2958
        }
2959
        return result;
152✔
2960
    }
2961

2962
    /**
2963
     * Get a map of member names and their values.
2964
     * All values are stored as their AST LiteralExpression representation (i.e. string enum values include the wrapping quotes)
2965
     */
2966
    public getMemberValueMap() {
2967
        const result = new Map<string, string>();
62✔
2968
        const members = this.getMembers();
62✔
2969
        let currentIntValue = 0;
62✔
2970
        for (const member of members) {
62✔
2971
            //if there is no value, assume an integer and increment the int counter
2972
            if (!member.value) {
155✔
2973
                result.set(member.name?.toLowerCase(), currentIntValue.toString());
32!
2974
                currentIntValue++;
32✔
2975

2976
                //if explicit integer value, use it and increment the int counter
2977
            } else if (isLiteralExpression(member.value) && member.value.token.kind === TokenKind.IntegerLiteral) {
123✔
2978
                //try parsing as integer literal, then as hex integer literal.
2979
                let tokenIntValue = util.parseInt(member.value.token.text) ?? util.parseInt(member.value.token.text.replace(/&h/i, '0x'));
31✔
2980
                if (tokenIntValue !== undefined) {
31!
2981
                    currentIntValue = tokenIntValue;
31✔
2982
                    currentIntValue++;
31✔
2983
                }
2984
                result.set(member.name?.toLowerCase(), member.value.token.text);
31!
2985

2986
                //simple unary expressions (like `-1`)
2987
            } else if (isUnaryExpression(member.value) && isLiteralExpression(member.value.right)) {
92✔
2988
                result.set(member.name?.toLowerCase(), member.value.operator.text + member.value.right.token.text);
1!
2989

2990
                //all other values
2991
            } else {
2992
                result.set(member.name?.toLowerCase(), (member.value as LiteralExpression)?.token?.text ?? 'invalid');
91!
2993
            }
2994
        }
2995
        return result;
62✔
2996
    }
2997

2998
    public getMemberValue(name: string) {
2999
        return this.getMemberValueMap().get(name.toLowerCase());
59✔
3000
    }
3001

3002
    /**
3003
     * The name of the enum (without the namespace prefix)
3004
     */
3005
    public get name() {
3006
        return this.tokens.name?.text;
16!
3007
    }
3008

3009
    /**
3010
     * The name of the enum WITH its leading namespace (if applicable)
3011
     */
3012
    public get fullName() {
3013
        const name = this.tokens.name?.text;
349!
3014
        if (name) {
349!
3015
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
349✔
3016

3017
            if (namespace) {
349✔
3018
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
163✔
3019
                return `${namespaceName}.${name}`;
163✔
3020
            } else {
3021
                return name;
186✔
3022
            }
3023
        } else {
3024
            //return undefined which will allow outside callers to know that this doesn't have a name
3025
            return undefined;
×
3026
        }
3027
    }
3028

3029
    transpile(state: BrsTranspileState) {
3030
        //enum declarations do not exist at runtime, so don't transpile anything...
3031
        return [];
24✔
3032
    }
3033

3034
    getTypedef(state: BrsTranspileState) {
3035
        const result = [] as TranspileResult;
1✔
3036
        for (let annotation of this.annotations ?? []) {
1!
3037
            result.push(
×
3038
                ...annotation.getTypedef(state),
3039
                state.newline,
3040
                state.indent()
3041
            );
3042
        }
3043
        result.push(
1✔
3044
            this.tokens.enum.text ?? 'enum',
3!
3045
            ' ',
3046
            this.tokens.name.text
3047
        );
3048
        result.push(state.newline);
1✔
3049
        state.blockDepth++;
1✔
3050
        for (const member of this.body) {
1✔
3051
            if (isTypedefProvider(member)) {
1!
3052
                result.push(
1✔
3053
                    state.indent(),
3054
                    ...member.getTypedef(state),
3055
                    state.newline
3056
                );
3057
            }
3058
        }
3059
        state.blockDepth--;
1✔
3060
        result.push(
1✔
3061
            state.indent(),
3062
            this.tokens.endEnum.text ?? 'end enum'
3!
3063
        );
3064
        return result;
1✔
3065
    }
3066

3067
    walk(visitor: WalkVisitor, options: WalkOptions) {
3068
        if (options.walkMode & InternalWalkMode.walkStatements) {
382!
3069
            walkArray(this.body, visitor, options, this);
382✔
3070

3071
        }
3072
    }
3073

3074
    public clone() {
3075
        return this.finalizeClone(
6✔
3076
            new EnumStatement(
3077
                {
3078
                    enum: util.cloneToken(this.tokens.enum),
3079
                    name: util.cloneToken(this.tokens.name),
3080
                    endEnum: util.cloneToken(this.tokens.endEnum)
3081
                },
3082
                this.body?.map(x => x?.clone())
6✔
3083
            ),
3084
            ['body']
3085
        );
3086
    }
3087
}
3088

3089
export class EnumMemberStatement extends Statement implements TypedefProvider {
1✔
3090

3091
    public constructor(
3092
        public tokens: {
210✔
3093
            name: Identifier;
3094
            equal?: Token;
3095
        },
3096
        public value?: Expression
210✔
3097
    ) {
3098
        super();
210✔
3099
    }
3100

3101
    /**
3102
     * The name of the member
3103
     */
3104
    public get name() {
3105
        return this.tokens.name.text;
610✔
3106
    }
3107

3108
    public get range() {
3109
        return util.createBoundingRange(
66✔
3110
            this.tokens.name,
3111
            this.tokens.equal,
3112
            this.value
3113
        );
3114
    }
3115

3116
    /**
3117
     * Get the value of this enum. Requires that `.parent` is set
3118
     */
3119
    public getValue() {
3120
        return (this.parent as EnumStatement).getMemberValue(this.name);
59✔
3121
    }
3122

3123
    public transpile(state: BrsTranspileState): TranspileResult {
3124
        return [];
×
3125
    }
3126

3127
    getTypedef(state: BrsTranspileState): TranspileResult {
3128
        const result = [
1✔
3129
            this.tokens.name.text
3130
        ] as TranspileResult;
3131
        if (this.tokens.equal) {
1!
3132
            result.push(' ', this.tokens.equal.text, ' ');
×
3133
            if (this.value) {
×
3134
                result.push(
×
3135
                    ...this.value.transpile(state)
3136
                );
3137
            }
3138
        }
3139
        return result;
1✔
3140
    }
3141

3142
    walk(visitor: WalkVisitor, options: WalkOptions) {
3143
        if (this.value && options.walkMode & InternalWalkMode.walkExpressions) {
512✔
3144
            walk(this, 'value', visitor, options);
327✔
3145
        }
3146
    }
3147

3148
    public clone() {
3149
        return this.finalizeClone(
3✔
3150
            new EnumMemberStatement(
3151
                {
3152
                    name: util.cloneToken(this.tokens.name),
3153
                    equal: util.cloneToken(this.tokens.equal)
3154
                },
3155
                this.value?.clone()
9✔
3156
            ),
3157
            ['value']
3158
        );
3159
    }
3160
}
3161

3162
export class ConstStatement extends Statement implements TypedefProvider {
1✔
3163

3164
    public constructor(
3165
        public tokens: {
85✔
3166
            const: Token;
3167
            name: Identifier;
3168
            equals: Token;
3169
        },
3170
        public value: Expression
85✔
3171
    ) {
3172
        super();
85✔
3173
        this.range = util.createBoundingRange(this.tokens.const, this.tokens.name, this.tokens.equals, this.value);
85✔
3174
    }
3175

3176
    public range: Range | undefined;
3177

3178
    public get name() {
3179
        return this.tokens.name.text;
5✔
3180
    }
3181

3182
    /**
3183
     * The name of the statement WITH its leading namespace (if applicable)
3184
     */
3185
    public get fullName() {
3186
        const name = this.tokens.name?.text;
131!
3187
        if (name) {
131!
3188
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
131✔
3189
            if (namespace) {
131✔
3190
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
98✔
3191
                return `${namespaceName}.${name}`;
98✔
3192
            } else {
3193
                return name;
33✔
3194
            }
3195
        } else {
3196
            //return undefined which will allow outside callers to know that this doesn't have a name
3197
            return undefined;
×
3198
        }
3199
    }
3200

3201
    public transpile(state: BrsTranspileState): TranspileResult {
3202
        //const declarations don't exist at runtime, so just transpile empty
3203
        return [];
35✔
3204
    }
3205

3206
    getTypedef(state: BrsTranspileState): TranspileResult {
3207
        return [
3✔
3208
            state.tokenToSourceNode(this.tokens.const),
3209
            ' ',
3210
            state.tokenToSourceNode(this.tokens.name),
3211
            ' ',
3212
            state.tokenToSourceNode(this.tokens.equals),
3213
            ' ',
3214
            ...this.value.transpile(state)
3215
        ];
3216
    }
3217

3218
    walk(visitor: WalkVisitor, options: WalkOptions) {
3219
        if (this.value && options.walkMode & InternalWalkMode.walkExpressions) {
200✔
3220
            walk(this, 'value', visitor, options);
198✔
3221
        }
3222
    }
3223

3224
    public clone() {
3225
        return this.finalizeClone(
3✔
3226
            new ConstStatement(
3227
                {
3228
                    const: util.cloneToken(this.tokens.const),
3229
                    name: util.cloneToken(this.tokens.name),
3230
                    equals: util.cloneToken(this.tokens.equals)
3231
                },
3232
                this.value?.clone()
9✔
3233
            ),
3234
            ['value']
3235
        );
3236
    }
3237
}
3238

3239
export class ContinueStatement extends Statement {
1✔
3240
    constructor(
3241
        public tokens: {
13✔
3242
            continue: Token;
3243
            loopType: Token;
3244
        }
3245
    ) {
3246
        super();
13✔
3247
        this.range = util.createBoundingRange(
13✔
3248
            tokens.continue,
3249
            tokens.loopType
3250
        );
3251
    }
3252

3253
    public range: Range | undefined;
3254

3255
    transpile(state: BrsTranspileState) {
3256
        return [
3✔
3257
            state.sourceNode(this.tokens.continue, this.tokens.continue?.text ?? 'continue'),
18!
3258
            this.tokens.loopType?.leadingWhitespace ?? ' ',
18✔
3259
            state.sourceNode(this.tokens.continue, this.tokens.loopType?.text)
9✔
3260
        ];
3261
    }
3262

3263
    walk(visitor: WalkVisitor, options: WalkOptions) {
3264
        //nothing to walk
3265
    }
3266

3267
    public clone() {
3268
        return this.finalizeClone(
1✔
3269
            new ContinueStatement({
3270
                continue: util.cloneToken(this.tokens.continue),
3271
                loopType: util.cloneToken(this.tokens.loopType)
3272
            })
3273
        );
3274
    }
3275
}
3276

3277
export class TypecastStatement extends Statement {
1✔
3278
    constructor(options: {
3279
        typecast?: Token;
3280
        obj: Token;
3281
        as?: Token;
3282
        type: Token;
3283
    }
3284
    ) {
3285
        super();
5✔
3286
        this.tokens = {
5✔
3287
            typecast: options.typecast,
3288
            obj: options.obj,
3289
            as: options.as,
3290
            type: options.type
3291
        };
3292
        this.range = util.createBoundingRange(
5✔
3293
            this.tokens.typecast,
3294
            this.tokens.obj,
3295
            this.tokens.as,
3296
            this.tokens.type
3297
        );
3298
    }
3299

3300
    public readonly tokens: {
3301
        readonly typecast?: Token;
3302
        readonly obj: Token;
3303
        readonly as?: Token;
3304
        readonly type: Token;
3305
    };
3306

3307
    public readonly typecastExpression: Expression;
3308

3309
    public readonly range: Range;
3310

3311
    transpile(state: BrsTranspileState) {
3312
        return [];
1✔
3313
    }
3314

3315
    walk(visitor: WalkVisitor, options: WalkOptions) {
3316
        //nothing to walk
3317
    }
3318

3319
    public clone() {
3320
        return this.finalizeClone(
×
3321
            new TypecastStatement({
3322
                typecast: util.cloneToken(this.tokens.typecast),
3323
                obj: util.cloneToken(this.tokens.obj),
3324
                as: util.cloneToken(this.tokens.as),
3325
                type: util.cloneToken(this.tokens.type)
3326
            })
3327
        );
3328
    }
3329
}
3330

3331
export class AliasStatement extends Statement {
1✔
3332
    constructor(options: {
3333
        alias?: Token;
3334
        name: Token;
3335
        equals?: Token;
3336
        value: Token;
3337
    }
3338
    ) {
3339
        super();
2✔
3340
        this.tokens = {
2✔
3341
            alias: options.alias,
3342
            name: options.name,
3343
            equals: options.equals,
3344
            value: options.value
3345
        };
3346
        this.range = util.createBoundingRange(
2✔
3347
            this.tokens.alias,
3348
            this.tokens.name,
3349
            this.tokens.equals,
3350
            this.tokens.value
3351
        );
3352
    }
3353

3354
    public readonly tokens: {
3355
        readonly alias?: Token;
3356
        readonly name: Token;
3357
        readonly equals?: Token;
3358
        readonly value: Token;
3359
    };
3360

3361
    public readonly range: Range;
3362

3363
    transpile(state: BrsTranspileState) {
3364
        return [];
×
3365
    }
3366

3367
    walk(visitor: WalkVisitor, options: WalkOptions) {
3368
        //nothing to walk
3369
    }
3370

3371

3372
    public clone() {
3373
        return this.finalizeClone(
×
3374
            new AliasStatement({
3375
                alias: util.cloneToken(this.tokens.alias),
3376
                name: util.cloneToken(this.tokens.name),
3377
                equals: util.cloneToken(this.tokens.equals),
3378
                value: util.cloneToken(this.tokens.value)
3379
            })
3380
        );
3381
    }
3382
}
3383

3384
export class TypeStatement extends Statement {
1✔
3385
    constructor(options: {
3386
        type?: Token;
3387
        name: Token;
3388
        equals?: Token;
3389
        value: Token;
3390
    }
3391
    ) {
3392
        super();
11✔
3393
        this.tokens = {
11✔
3394
            type: options.type,
3395
            name: options.name,
3396
            equals: options.equals,
3397
            value: options.value
3398
        };
3399
        this.range = util.createBoundingRange(
11✔
3400
            this.tokens.type,
3401
            this.tokens.name,
3402
            this.tokens.equals,
3403
            this.tokens.value
3404
        );
3405
    }
3406

3407
    public readonly tokens: {
3408
        readonly type?: Token;
3409
        readonly name: Token;
3410
        readonly equals?: Token;
3411
        readonly value: Token;
3412
    };
3413

3414
    public readonly range: Range;
3415

3416
    transpile(state: BrsTranspileState) {
3417
        return [];
2✔
3418
    }
3419

3420
    walk(visitor: WalkVisitor, options: WalkOptions) {
3421
        //nothing to walk
3422
    }
3423

3424
    public get fullName() {
3425
        const name = this.tokens.name?.text;
5!
3426
        if (name) {
5!
3427
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
5✔
3428
            if (namespace) {
5✔
3429
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
3✔
3430
                return `${namespaceName}.${name}`;
3✔
3431
            } else {
3432
                return name;
2✔
3433
            }
3434
        } else {
3435
            //return undefined which will allow outside callers to know that this doesn't have a name
3436
            return undefined;
×
3437
        }
3438
    }
3439

3440
    public clone() {
3441
        return this.finalizeClone(
×
3442
            new TypeStatement({
3443
                type: util.cloneToken(this.tokens.type),
3444
                name: util.cloneToken(this.tokens.name),
3445
                equals: util.cloneToken(this.tokens.equals),
3446
                value: util.cloneToken(this.tokens.value)
3447
            })
3448
        );
3449
    }
3450
}
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