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

rokucommunity / brighterscript / #15673

29 Apr 2026 12:13AM UTC coverage: 88.992% (-0.02%) from 89.009%
#15673

push

web-flow
Merge 52394bb25 into f4cffdc58

8296 of 9822 branches covered (84.46%)

Branch coverage included in aggregate %.

64 of 69 new or added lines in 2 files covered. (92.75%)

10509 of 11309 relevant lines covered (92.93%)

2012.52 hits per line

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

91.56
/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[] = []
5,198✔
55
    ) {
56
        super();
5,198✔
57
    }
58

59
    public symbolTable = new SymbolTable('Body', () => this.parent?.getSymbolTable());
5,198✔
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(
33✔
64
            ...(this.statements ?? [])
99!
65
        );
66
    }
67

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

75
            if (!previousStatement) {
522✔
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) {
171✔
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)) {
167✔
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))) {
166✔
90
                result.push(state.newline, state.newline);
89✔
91
            } else {
92
                //separate statements by a single newline
93
                result.push(state.newline);
77✔
94
            }
95

96
            result.push(...statement.transpile(state));
522✔
97
        }
98
        return result;
353✔
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) {
7,500!
118
            walkArray(this.statements, visitor, options, this);
7,500✔
119
        }
120
    }
121

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

132
export class AssignmentStatement extends Statement {
1✔
133
    constructor(
134
        readonly equals: Token,
1,060✔
135
        readonly name: Identifier,
1,060✔
136
        readonly value: Expression
1,060✔
137
    ) {
138
        super();
1,060✔
139
        this.range = util.createBoundingRange(name, equals, value);
1,060✔
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);
502✔
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)) {
282!
155
            return this.value.transpile(state);
21✔
156
        } else {
157
            return [
261✔
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,846✔
169
            walk(this, 'value', visitor, options);
2,342✔
170
        }
171
    }
172

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

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

197
    public readonly range: Range | undefined;
198

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

206
            //if comment is on same line as parent
207
            if (isCommentStatement(statement) &&
780✔
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(
715✔
216
                    state.newline,
217
                    state.indent()
218
                );
219
            }
220

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

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

238
    public clone() {
239
        return this.finalizeClone(
123✔
240
            new Block(
241
                this.statements?.map(s => s?.clone()),
112✔
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
356✔
252
    ) {
253
        super();
356✔
254
        this.range = this.expression?.range;
356✔
255
    }
256

257
    public readonly range: Range | undefined;
258

259
    transpile(state: BrsTranspileState) {
260
        return this.expression.transpile(state);
53✔
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
266
        return [];
×
267
    }
268

269
    walk(visitor: WalkVisitor, options: WalkOptions) {
270
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,198✔
271
            walk(this, 'expression', visitor, options);
940✔
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[]
271✔
288
    ) {
289
        super();
271✔
290
        this.visitMode = InternalWalkMode.visitStatements | InternalWalkMode.visitExpressions;
271✔
291
        if (this.comments?.length > 0) {
271✔
292
            this.range = util.createBoundingRange(
269✔
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,313✔
405
        public func: FunctionExpression
2,313✔
406
    ) {
407
        super();
2,313✔
408
        this.range = this.func?.range;
2,313✔
409
    }
410

411
    public readonly range: Range | undefined;
412

413
    private _cachedNameBrighterScript: string | undefined;
414
    private _cachedNameBrightScript: string | undefined;
415

416
    /**
417
     * Get the name of this expression based on the parse mode.
418
     *
419
     * Memoized per `ParseMode` because validators call this per-reference per-scope and
420
     * the underlying work (`findAncestor` + recursive `namespace.getName`) is O(depth) per
421
     * call. The AST is treated as readonly post-parse, so the cached string remains valid
422
     * for the lifetime of the statement.
423
     */
424
    public getName(parseMode: ParseMode) {
425
        if (parseMode === ParseMode.BrighterScript) {
2,428✔
426
            if (this._cachedNameBrighterScript !== undefined) {
1,086✔
427
                return this._cachedNameBrighterScript;
205✔
428
            }
429
        } else if (this._cachedNameBrightScript !== undefined) {
1,342✔
430
            return this._cachedNameBrightScript;
458✔
431
        }
432
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
1,765✔
433
        let result: string | undefined;
434
        if (namespace) {
1,765✔
435
            const delimiter = parseMode === ParseMode.BrighterScript ? '.' : '_';
165✔
436
            const namespaceName = namespace.getName(parseMode);
165✔
437
            result = namespaceName + delimiter + this.name?.text;
165✔
438
        } else {
439
            result = this.name.text;
1,600✔
440
        }
441
        //only cache once parent linking is in place (set during AST walk). Caching
442
        //before walk could lock in a parentless result.
443
        if (this.parent) {
1,765!
444
            if (parseMode === ParseMode.BrighterScript) {
1,765✔
445
                this._cachedNameBrighterScript = result;
881✔
446
            } else {
447
                this._cachedNameBrightScript = result;
884✔
448
            }
449
        }
450
        return result;
1,765✔
451
    }
452

453
    /**
454
     * Get the name of the wrapping namespace (if it exists)
455
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
456
     */
457
    public get namespaceName() {
458
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
459
    }
460

461
    transpile(state: BrsTranspileState) {
462
        //create a fake token using the full transpiled name
463
        let nameToken = {
326✔
464
            ...this.name,
465
            text: this.getName(ParseMode.BrightScript)
466
        };
467

468
        return this.func.transpile(state, nameToken);
326✔
469
    }
470

471
    getTypedef(state: BrsTranspileState) {
472
        let result = [] as TranspileResult;
11✔
473
        for (let annotation of this.annotations ?? []) {
11✔
474
            result.push(
2✔
475
                ...annotation.getTypedef(state),
476
                state.newline,
477
                state.indent()
478
            );
479
        }
480

481
        result.push(
11✔
482
            ...this.func.getTypedef(state)
483
        );
484
        return result;
11✔
485
    }
486

487
    walk(visitor: WalkVisitor, options: WalkOptions) {
488
        if (options.walkMode & InternalWalkMode.walkExpressions) {
4,162✔
489
            walk(this, 'func', visitor, options);
4,135✔
490
        }
491
    }
492

493
    public clone() {
494
        return this.finalizeClone(
105✔
495
            new FunctionStatement(
496
                util.cloneToken(this.name),
497
                this.func?.clone()
315✔
498
            ),
499
            ['func']
500
        );
501
    }
502
}
503

504
export class IfStatement extends Statement {
1✔
505
    constructor(
506
        readonly tokens: {
245✔
507
            if: Token;
508
            then?: Token;
509
            else?: Token;
510
            endIf?: Token;
511
        },
512
        readonly condition: Expression,
245✔
513
        readonly thenBranch: Block,
245✔
514
        readonly elseBranch?: IfStatement | Block,
245✔
515
        readonly isInline?: boolean
245✔
516
    ) {
517
        super();
245✔
518
        this.range = util.createBoundingRange(
245✔
519
            tokens.if,
520
            condition,
521
            tokens.then,
522
            thenBranch,
523
            tokens.else,
524
            elseBranch,
525
            tokens.endIf
526
        );
527
    }
528
    public readonly range: Range | undefined;
529

530
    transpile(state: BrsTranspileState) {
531
        let results = [] as TranspileResult;
75✔
532
        //if   (already indented by block)
533
        results.push(state.transpileToken(this.tokens.if));
75✔
534
        results.push(' ');
75✔
535
        //conditions
536
        results.push(...this.condition.transpile(state));
75✔
537
        //then
538
        if (this.tokens.then) {
75✔
539
            results.push(' ');
64✔
540
            results.push(
64✔
541
                state.transpileToken(this.tokens.then)
542
            );
543
        }
544
        state.lineage.unshift(this);
75✔
545

546
        //if statement body
547
        let thenNodes = this.thenBranch.transpile(state);
75✔
548
        state.lineage.shift();
75✔
549
        if (thenNodes.length > 0) {
75✔
550
            results.push(thenNodes);
64✔
551
        }
552
        results.push('\n');
75✔
553

554
        //else branch
555
        if (this.tokens.else) {
75✔
556
            //else
557
            results.push(
56✔
558
                state.indent(),
559
                state.transpileToken(this.tokens.else)
560
            );
561
        }
562

563
        if (this.elseBranch) {
75✔
564
            if (isIfStatement(this.elseBranch)) {
56✔
565
                //chained elseif
566
                state.lineage.unshift(this.elseBranch);
19✔
567
                let body = this.elseBranch.transpile(state);
19✔
568
                state.lineage.shift();
19✔
569

570
                if (body.length > 0) {
19!
571
                    //zero or more spaces between the `else` and the `if`
572
                    results.push(this.elseBranch.tokens.if.leadingWhitespace!);
19✔
573
                    results.push(...body);
19✔
574

575
                    // stop here because chained if will transpile the rest
576
                    return results;
19✔
577
                } else {
578
                    results.push('\n');
×
579
                }
580

581
            } else {
582
                //else body
583
                state.lineage.unshift(this.tokens.else!);
37✔
584
                let body = this.elseBranch.transpile(state);
37✔
585
                state.lineage.shift();
37✔
586

587
                if (body.length > 0) {
37✔
588
                    results.push(...body);
35✔
589
                }
590
                results.push('\n');
37✔
591
            }
592
        }
593

594
        //end if
595
        results.push(state.indent());
56✔
596
        if (this.tokens.endIf) {
56!
597
            results.push(
56✔
598
                state.transpileToken(this.tokens.endIf)
599
            );
600
        } else {
601
            results.push('end if');
×
602
        }
603
        return results;
56✔
604
    }
605

606
    walk(visitor: WalkVisitor, options: WalkOptions) {
607
        if (options.walkMode & InternalWalkMode.walkExpressions) {
612✔
608
            walk(this, 'condition', visitor, options);
494✔
609
        }
610
        if (options.walkMode & InternalWalkMode.walkStatements) {
612✔
611
            walk(this, 'thenBranch', visitor, options);
610✔
612
        }
613
        if (this.elseBranch && options.walkMode & InternalWalkMode.walkStatements) {
612✔
614
            walk(this, 'elseBranch', visitor, options);
340✔
615
        }
616
    }
617

618
    public clone() {
619
        return this.finalizeClone(
5✔
620
            new IfStatement(
621
                {
622
                    if: util.cloneToken(this.tokens.if),
623
                    else: util.cloneToken(this.tokens.else),
624
                    endIf: util.cloneToken(this.tokens.endIf),
625
                    then: util.cloneToken(this.tokens.then)
626
                },
627
                this.condition?.clone(),
15✔
628
                this.thenBranch?.clone(),
15✔
629
                this.elseBranch?.clone(),
15✔
630
                this.isInline
631
            ),
632
            ['condition', 'thenBranch', 'elseBranch']
633
        );
634
    }
635
}
636

637
export class IncrementStatement extends Statement {
1✔
638
    constructor(
639
        readonly value: Expression,
21✔
640
        readonly operator: Token
21✔
641
    ) {
642
        super();
21✔
643
        this.range = util.createBoundingRange(
21✔
644
            value,
645
            operator
646
        );
647
    }
648

649
    public readonly range: Range | undefined;
650

651
    transpile(state: BrsTranspileState) {
652
        return [
8✔
653
            ...this.value.transpile(state),
654
            state.transpileToken(this.operator)
655
        ];
656
    }
657

658
    walk(visitor: WalkVisitor, options: WalkOptions) {
659
        if (options.walkMode & InternalWalkMode.walkExpressions) {
43✔
660
            walk(this, 'value', visitor, options);
36✔
661
        }
662
    }
663

664
    public clone() {
665
        return this.finalizeClone(
2✔
666
            new IncrementStatement(
667
                this.value?.clone(),
6✔
668
                util.cloneToken(this.operator)
669
            ),
670
            ['value']
671
        );
672
    }
673
}
674

675
/** Used to indent the current `print` position to the next 16-character-width output zone. */
676
export interface PrintSeparatorTab extends Token {
677
    kind: TokenKind.Comma;
678
}
679

680
/** Used to insert a single whitespace character at the current `print` position. */
681
export interface PrintSeparatorSpace extends Token {
682
    kind: TokenKind.Semicolon;
683
}
684

685
/**
686
 * Represents a `print` statement within BrightScript.
687
 */
688
export class PrintStatement extends Statement {
1✔
689
    /**
690
     * Creates a new internal representation of a BrightScript `print` statement.
691
     * @param tokens the tokens for this statement
692
     * @param tokens.print a print token
693
     * @param expressions an array of expressions or `PrintSeparator`s to be evaluated and printed.
694
     */
695
    constructor(
696
        readonly tokens: {
768✔
697
            print: Token;
698
        },
699
        readonly expressions: Array<Expression | PrintSeparatorTab | PrintSeparatorSpace>
768✔
700
    ) {
701
        super();
768✔
702
        this.range = util.createBoundingRange(
768✔
703
            tokens.print,
704
            ...(expressions ?? [])
2,304✔
705
        );
706
    }
707

708
    public readonly range: Range | undefined;
709

710
    transpile(state: BrsTranspileState) {
711
        let result = [
210✔
712
            state.transpileToken(this.tokens.print),
713
            ' '
714
        ] as TranspileResult;
715
        for (let i = 0; i < this.expressions.length; i++) {
210✔
716
            const expressionOrSeparator: any = this.expressions[i];
274✔
717
            if (expressionOrSeparator.transpile) {
274✔
718
                result.push(...(expressionOrSeparator as ExpressionStatement).transpile(state));
248✔
719
            } else {
720
                result.push(
26✔
721
                    state.tokenToSourceNode(expressionOrSeparator)
722
                );
723
            }
724
            //if there's an expression after us, add a space
725
            if ((this.expressions[i + 1] as any)?.transpile) {
274✔
726
                result.push(' ');
38✔
727
            }
728
        }
729
        return result;
210✔
730
    }
731

732
    walk(visitor: WalkVisitor, options: WalkOptions) {
733
        if (options.walkMode & InternalWalkMode.walkExpressions) {
2,205✔
734
            //sometimes we have semicolon Tokens in the expressions list (should probably fix that...), so only walk the actual expressions
735
            walkArray(this.expressions as AstNode[], visitor, options, this, (item) => isExpression(item as any));
2,069✔
736
        }
737
    }
738

739
    public clone() {
740
        return this.finalizeClone(
47✔
741
            new PrintStatement(
742
                {
743
                    print: util.cloneToken(this.tokens.print)
744
                },
745
                this.expressions?.map(e => {
141✔
746
                    if (isExpression(e as any)) {
47✔
747
                        return (e as Expression).clone();
46✔
748
                    } else {
749
                        return util.cloneToken(e as Token);
1✔
750
                    }
751
                })
752
            ),
753
            ['expressions' as any]
754
        );
755
    }
756
}
757

758
export class DimStatement extends Statement {
1✔
759
    constructor(
760
        public dimToken: Token,
46✔
761
        public identifier?: Identifier,
46✔
762
        public openingSquare?: Token,
46✔
763
        public dimensions?: Expression[],
46✔
764
        public closingSquare?: Token
46✔
765
    ) {
766
        super();
46✔
767
        this.range = util.createBoundingRange(
46✔
768
            dimToken,
769
            identifier,
770
            openingSquare,
771
            ...(dimensions ?? []),
138✔
772
            closingSquare
773
        );
774
    }
775
    public range: Range | undefined;
776

777
    public transpile(state: BrsTranspileState) {
778
        let result = [
15✔
779
            state.transpileToken(this.dimToken),
780
            ' ',
781
            state.transpileToken(this.identifier!),
782
            state.transpileToken(this.openingSquare!)
783
        ] as TranspileResult;
784
        for (let i = 0; i < this.dimensions!.length; i++) {
15✔
785
            if (i > 0) {
32✔
786
                result.push(', ');
17✔
787
            }
788
            result.push(
32✔
789
                ...this.dimensions![i].transpile(state)
790
            );
791
        }
792
        result.push(state.transpileToken(this.closingSquare!));
15✔
793
        return result;
15✔
794
    }
795

796
    public walk(visitor: WalkVisitor, options: WalkOptions) {
797
        if (this.dimensions?.length !== undefined && this.dimensions?.length > 0 && options.walkMode & InternalWalkMode.walkExpressions) {
116!
798
            walkArray(this.dimensions, visitor, options, this);
87✔
799

800
        }
801
    }
802

803
    public clone() {
804
        return this.finalizeClone(
3✔
805
            new DimStatement(
806
                util.cloneToken(this.dimToken),
807
                util.cloneToken(this.identifier),
808
                util.cloneToken(this.openingSquare),
809
                this.dimensions?.map(e => e?.clone()),
5✔
810
                util.cloneToken(this.closingSquare)
811
            ),
812
            ['dimensions']
813
        );
814
    }
815
}
816

817
export class GotoStatement extends Statement {
1✔
818
    constructor(
819
        readonly tokens: {
12✔
820
            goto: Token;
821
            label: Token;
822
        }
823
    ) {
824
        super();
12✔
825
        this.range = util.createBoundingRange(
12✔
826
            tokens.goto,
827
            tokens.label
828
        );
829
    }
830

831
    public readonly range: Range | undefined;
832

833
    transpile(state: BrsTranspileState) {
834
        return [
2✔
835
            state.transpileToken(this.tokens.goto),
836
            ' ',
837
            state.transpileToken(this.tokens.label)
838
        ];
839
    }
840

841
    walk(visitor: WalkVisitor, options: WalkOptions) {
842
        //nothing to walk
843
    }
844

845
    public clone() {
846
        return this.finalizeClone(
1✔
847
            new GotoStatement({
848
                goto: util.cloneToken(this.tokens.goto),
849
                label: util.cloneToken(this.tokens.label)
850
            })
851
        );
852
    }
853
}
854

855
export class LabelStatement extends Statement {
1✔
856
    constructor(
857
        readonly tokens: {
12✔
858
            identifier: Token;
859
            colon: Token;
860
        }
861
    ) {
862
        super();
12✔
863
        this.range = util.createBoundingRange(
12✔
864
            tokens.identifier,
865
            tokens.colon
866
        );
867
    }
868

869
    public readonly range: Range | undefined;
870

871
    transpile(state: BrsTranspileState) {
872
        return [
2✔
873
            state.transpileToken(this.tokens.identifier),
874
            state.transpileToken(this.tokens.colon)
875

876
        ];
877
    }
878

879
    walk(visitor: WalkVisitor, options: WalkOptions) {
880
        //nothing to walk
881
    }
882

883
    public clone() {
884
        return this.finalizeClone(
1✔
885
            new LabelStatement({
886
                identifier: util.cloneToken(this.tokens.identifier),
887
                colon: util.cloneToken(this.tokens.colon)
888
            })
889
        );
890
    }
891
}
892

893
export class ReturnStatement extends Statement {
1✔
894
    constructor(
895
        readonly tokens: {
255✔
896
            return: Token;
897
        },
898
        readonly value?: Expression
255✔
899
    ) {
900
        super();
255✔
901
        this.range = util.createBoundingRange(
255✔
902
            tokens.return,
903
            value
904
        );
905
    }
906

907
    public readonly range: Range | undefined;
908

909
    transpile(state: BrsTranspileState) {
910
        let result = [] as TranspileResult;
21✔
911
        result.push(
21✔
912
            state.transpileToken(this.tokens.return)
913
        );
914
        if (this.value) {
21✔
915
            result.push(' ');
20✔
916
            result.push(...this.value.transpile(state));
20✔
917
        }
918
        return result;
21✔
919
    }
920

921
    walk(visitor: WalkVisitor, options: WalkOptions) {
922
        if (options.walkMode & InternalWalkMode.walkExpressions) {
729✔
923
            walk(this, 'value', visitor, options);
620✔
924
        }
925
    }
926

927
    public clone() {
928
        return this.finalizeClone(
3✔
929
            new ReturnStatement(
930
                {
931
                    return: util.cloneToken(this.tokens.return)
932
                },
933
                this.value?.clone()
9✔
934
            ),
935
            ['value']
936
        );
937
    }
938
}
939

940
export class EndStatement extends Statement {
1✔
941
    constructor(
942
        readonly tokens: {
10✔
943
            end: Token;
944
        }
945
    ) {
946
        super();
10✔
947
        this.range = tokens.end.range;
10✔
948
    }
949

950
    public readonly range: Range;
951

952
    transpile(state: BrsTranspileState) {
953
        return [
2✔
954
            state.transpileToken(this.tokens.end)
955
        ];
956
    }
957

958
    walk(visitor: WalkVisitor, options: WalkOptions) {
959
        //nothing to walk
960
    }
961

962
    public clone() {
963
        return this.finalizeClone(
1✔
964
            new EndStatement({
965
                end: util.cloneToken(this.tokens.end)
966
            })
967
        );
968
    }
969
}
970

971
export class StopStatement extends Statement {
1✔
972
    constructor(
973
        readonly tokens: {
18✔
974
            stop: Token;
975
        }
976
    ) {
977
        super();
18✔
978
        this.range = tokens?.stop?.range;
18!
979
    }
980

981
    public readonly range: Range;
982

983
    transpile(state: BrsTranspileState) {
984
        return [
2✔
985
            state.transpileToken(this.tokens.stop)
986
        ];
987
    }
988

989
    walk(visitor: WalkVisitor, options: WalkOptions) {
990
        //nothing to walk
991
    }
992

993
    public clone() {
994
        return this.finalizeClone(
1✔
995
            new StopStatement({
996
                stop: util.cloneToken(this.tokens.stop)
997
            })
998
        );
999
    }
1000
}
1001

1002
export class ForStatement extends Statement {
1✔
1003
    constructor(
1004
        public forToken: Token,
40✔
1005
        public counterDeclaration: AssignmentStatement,
40✔
1006
        public toToken: Token,
40✔
1007
        public finalValue: Expression,
40✔
1008
        public body: Block,
40✔
1009
        public endForToken: Token,
40✔
1010
        public stepToken?: Token,
40✔
1011
        public increment?: Expression
40✔
1012
    ) {
1013
        super();
40✔
1014
        this.range = util.createBoundingRange(
40✔
1015
            forToken,
1016
            counterDeclaration,
1017
            toToken,
1018
            finalValue,
1019
            stepToken,
1020
            increment,
1021
            body,
1022
            endForToken
1023
        );
1024
    }
1025

1026
    public readonly range: Range | undefined;
1027

1028
    transpile(state: BrsTranspileState) {
1029
        let result = [] as TranspileResult;
9✔
1030
        //for
1031
        result.push(
9✔
1032
            state.transpileToken(this.forToken),
1033
            ' '
1034
        );
1035
        //i=1
1036
        result.push(
9✔
1037
            ...this.counterDeclaration.transpile(state),
1038
            ' '
1039
        );
1040
        //to
1041
        result.push(
9✔
1042
            state.transpileToken(this.toToken),
1043
            ' '
1044
        );
1045
        //final value
1046
        result.push(this.finalValue.transpile(state));
9✔
1047
        //step
1048
        if (this.stepToken) {
9✔
1049
            result.push(
4✔
1050
                ' ',
1051
                state.transpileToken(this.stepToken),
1052
                ' ',
1053
                this.increment!.transpile(state)
1054
            );
1055
        }
1056
        //loop body
1057
        state.lineage.unshift(this);
9✔
1058
        result.push(...this.body.transpile(state));
9✔
1059
        state.lineage.shift();
9✔
1060

1061
        // add new line before "end for"
1062
        result.push('\n');
9✔
1063

1064
        //end for
1065
        result.push(
9✔
1066
            state.indent(),
1067
            state.transpileToken(this.endForToken)
1068
        );
1069

1070
        return result;
9✔
1071
    }
1072

1073
    walk(visitor: WalkVisitor, options: WalkOptions) {
1074
        if (options.walkMode & InternalWalkMode.walkStatements) {
103✔
1075
            walk(this, 'counterDeclaration', visitor, options);
102✔
1076
        }
1077
        if (options.walkMode & InternalWalkMode.walkExpressions) {
103✔
1078
            walk(this, 'finalValue', visitor, options);
81✔
1079
            walk(this, 'increment', visitor, options);
81✔
1080
        }
1081
        if (options.walkMode & InternalWalkMode.walkStatements) {
103✔
1082
            walk(this, 'body', visitor, options);
102✔
1083
        }
1084
    }
1085

1086
    public clone() {
1087
        return this.finalizeClone(
5✔
1088
            new ForStatement(
1089
                util.cloneToken(this.forToken),
1090
                this.counterDeclaration?.clone(),
15✔
1091
                util.cloneToken(this.toToken),
1092
                this.finalValue?.clone(),
15✔
1093
                this.body?.clone(),
15✔
1094
                util.cloneToken(this.endForToken),
1095
                util.cloneToken(this.stepToken),
1096
                this.increment?.clone()
15✔
1097
            ),
1098
            ['counterDeclaration', 'finalValue', 'body', 'increment']
1099
        );
1100
    }
1101
}
1102

1103
export class ForEachStatement extends Statement {
1✔
1104
    constructor(
1105
        readonly tokens: {
26✔
1106
            forEach: Token;
1107
            in: Token;
1108
            endFor: Token;
1109
        },
1110
        readonly item: Token,
26✔
1111
        readonly target: Expression,
26✔
1112
        readonly body: Block
26✔
1113
    ) {
1114
        super();
26✔
1115
        this.range = util.createBoundingRange(
26✔
1116
            tokens.forEach,
1117
            item,
1118
            tokens.in,
1119
            target,
1120
            body,
1121
            tokens.endFor
1122
        );
1123
    }
1124

1125
    public readonly range: Range | undefined;
1126

1127
    transpile(state: BrsTranspileState) {
1128
        let result = [] as TranspileResult;
6✔
1129
        //for each
1130
        result.push(
6✔
1131
            state.transpileToken(this.tokens.forEach),
1132
            ' '
1133
        );
1134
        //item
1135
        result.push(
6✔
1136
            state.transpileToken(this.item),
1137
            ' '
1138
        );
1139
        //in
1140
        result.push(
6✔
1141
            state.transpileToken(this.tokens.in),
1142
            ' '
1143
        );
1144
        //target
1145
        result.push(...this.target.transpile(state));
6✔
1146
        //body
1147
        state.lineage.unshift(this);
6✔
1148
        result.push(...this.body.transpile(state));
6✔
1149
        state.lineage.shift();
6✔
1150

1151
        // add new line before "end for"
1152
        result.push('\n');
6✔
1153

1154
        //end for
1155
        result.push(
6✔
1156
            state.indent(),
1157
            state.transpileToken(this.tokens.endFor)
1158
        );
1159
        return result;
6✔
1160
    }
1161

1162
    walk(visitor: WalkVisitor, options: WalkOptions) {
1163
        if (options.walkMode & InternalWalkMode.walkExpressions) {
63✔
1164
            walk(this, 'target', visitor, options);
51✔
1165
        }
1166
        if (options.walkMode & InternalWalkMode.walkStatements) {
63✔
1167
            walk(this, 'body', visitor, options);
62✔
1168
        }
1169
    }
1170

1171
    public clone() {
1172
        return this.finalizeClone(
2✔
1173
            new ForEachStatement(
1174
                {
1175
                    forEach: util.cloneToken(this.tokens.forEach),
1176
                    in: util.cloneToken(this.tokens.in),
1177
                    endFor: util.cloneToken(this.tokens.endFor)
1178
                },
1179
                util.cloneToken(this.item),
1180
                this.target?.clone(),
6✔
1181
                this.body?.clone()
6✔
1182
            ),
1183
            ['target', 'body']
1184
        );
1185
    }
1186
}
1187

1188
export class WhileStatement extends Statement {
1✔
1189
    constructor(
1190
        readonly tokens: {
27✔
1191
            while: Token;
1192
            endWhile: Token;
1193
        },
1194
        readonly condition: Expression,
27✔
1195
        readonly body: Block
27✔
1196
    ) {
1197
        super();
27✔
1198
        this.range = util.createBoundingRange(
27✔
1199
            tokens.while,
1200
            condition,
1201
            body,
1202
            tokens.endWhile
1203
        );
1204
    }
1205

1206
    public readonly range: Range | undefined;
1207

1208
    transpile(state: BrsTranspileState) {
1209
        let result = [] as TranspileResult;
4✔
1210
        //while
1211
        result.push(
4✔
1212
            state.transpileToken(this.tokens.while),
1213
            ' '
1214
        );
1215
        //condition
1216
        result.push(
4✔
1217
            ...this.condition.transpile(state)
1218
        );
1219
        state.lineage.unshift(this);
4✔
1220
        //body
1221
        result.push(...this.body.transpile(state));
4✔
1222
        state.lineage.shift();
4✔
1223

1224
        //trailing newline only if we have body statements
1225
        result.push('\n');
4✔
1226

1227
        //end while
1228
        result.push(
4✔
1229
            state.indent(),
1230
            state.transpileToken(this.tokens.endWhile)
1231
        );
1232

1233
        return result;
4✔
1234
    }
1235

1236
    walk(visitor: WalkVisitor, options: WalkOptions) {
1237
        if (options.walkMode & InternalWalkMode.walkExpressions) {
58✔
1238
            walk(this, 'condition', visitor, options);
44✔
1239
        }
1240
        if (options.walkMode & InternalWalkMode.walkStatements) {
58✔
1241
            walk(this, 'body', visitor, options);
57✔
1242
        }
1243
    }
1244

1245
    public clone() {
1246
        return this.finalizeClone(
4✔
1247
            new WhileStatement(
1248
                {
1249
                    while: util.cloneToken(this.tokens.while),
1250
                    endWhile: util.cloneToken(this.tokens.endWhile)
1251
                },
1252
                this.condition?.clone(),
12✔
1253
                this.body?.clone()
12✔
1254
            ),
1255
            ['condition', 'body']
1256
        );
1257
    }
1258
}
1259

1260
export class DottedSetStatement extends Statement {
1✔
1261
    constructor(
1262
        readonly obj: Expression,
250✔
1263
        readonly name: Identifier,
250✔
1264
        readonly value: Expression,
250✔
1265
        readonly dot?: Token,
250✔
1266
        readonly equals?: Token
250✔
1267
    ) {
1268
        super();
250✔
1269
        this.range = util.createBoundingRange(
250✔
1270
            obj,
1271
            dot,
1272
            name,
1273
            equals,
1274
            value
1275
        );
1276
    }
1277

1278
    public readonly range: Range | undefined;
1279

1280
    transpile(state: BrsTranspileState) {
1281
        //if the value is a compound assignment, don't add the obj, dot, name, or operator...the expression will handle that
1282
        if (CompoundAssignmentOperators.includes((this.value as BinaryExpression)?.operator?.kind)) {
16!
1283
            return this.value.transpile(state);
2✔
1284
        } else {
1285
            return [
14✔
1286
                //object
1287
                ...this.obj.transpile(state),
1288
                this.dot ? state.tokenToSourceNode(this.dot) : '.',
14✔
1289
                //name
1290
                state.transpileToken(this.name),
1291
                ' ',
1292
                state.transpileToken(this.equals, '='),
1293
                ' ',
1294
                //right-hand-side of assignment
1295
                ...this.value.transpile(state)
1296
            ];
1297
        }
1298
    }
1299

1300
    walk(visitor: WalkVisitor, options: WalkOptions) {
1301
        if (options.walkMode & InternalWalkMode.walkExpressions) {
354✔
1302
            walk(this, 'obj', visitor, options);
323✔
1303
            walk(this, 'value', visitor, options);
323✔
1304
        }
1305
    }
1306

1307
    public clone() {
1308
        return this.finalizeClone(
2✔
1309
            new DottedSetStatement(
1310
                this.obj?.clone(),
6✔
1311
                util.cloneToken(this.name),
1312
                this.value?.clone(),
6✔
1313
                util.cloneToken(this.dot),
1314
                util.cloneToken(this.equals)
1315
            ),
1316
            ['obj', 'value']
1317
        );
1318
    }
1319
}
1320

1321
export class IndexedSetStatement extends Statement {
1✔
1322
    constructor(
1323
        readonly obj: Expression,
55✔
1324
        readonly index: Expression,
55✔
1325
        readonly value: Expression,
55✔
1326
        readonly openingSquare: Token,
55✔
1327
        readonly closingSquare: Token,
55✔
1328
        readonly additionalIndexes?: Expression[],
55✔
1329
        readonly equals?: Token
55✔
1330
    ) {
1331
        super();
55✔
1332
        this.additionalIndexes ??= [];
55✔
1333
        this.range = util.createBoundingRange(
55✔
1334
            obj,
1335
            openingSquare,
1336
            index,
1337
            closingSquare,
1338
            equals,
1339
            value,
1340
            ...this.additionalIndexes
1341
        );
1342
    }
1343

1344
    public readonly range: Range | undefined;
1345

1346
    transpile(state: BrsTranspileState) {
1347
        //if the value is a component assignment, don't add the obj, index or operator...the expression will handle that
1348
        if (CompoundAssignmentOperators.includes((this.value as BinaryExpression)?.operator?.kind)) {
21!
1349
            return this.value.transpile(state);
2✔
1350
        } else {
1351
            const result = [];
19✔
1352
            result.push(
19✔
1353
                //obj
1354
                ...this.obj.transpile(state),
1355
                //   [
1356
                state.transpileToken(this.openingSquare)
1357
            );
1358
            const indexes = [this.index, ...this.additionalIndexes ?? []];
19!
1359
            for (let i = 0; i < indexes.length; i++) {
19✔
1360
                //add comma between indexes
1361
                if (i > 0) {
20✔
1362
                    result.push(', ');
1✔
1363
                }
1364
                let index = indexes[i];
20✔
1365
                result.push(
20✔
1366
                    ...(index?.transpile(state) ?? [])
120!
1367
                );
1368
            }
1369
            result.push(
19✔
1370
                state.transpileToken(this.closingSquare),
1371
                ' ',
1372
                state.transpileToken(this.equals, '='),
1373
                ' ',
1374
                ...this.value.transpile(state)
1375
            );
1376
            return result;
19✔
1377
        }
1378
    }
1379

1380
    walk(visitor: WalkVisitor, options: WalkOptions) {
1381
        if (options.walkMode & InternalWalkMode.walkExpressions) {
150✔
1382
            walk(this, 'obj', visitor, options);
126✔
1383
            walk(this, 'index', visitor, options);
126✔
1384
            walkArray(this.additionalIndexes, visitor, options, this);
126✔
1385
            walk(this, 'value', visitor, options);
126✔
1386
        }
1387
    }
1388

1389
    public clone() {
1390
        return this.finalizeClone(
6✔
1391
            new IndexedSetStatement(
1392
                this.obj?.clone(),
18✔
1393
                this.index?.clone(),
18✔
1394
                this.value?.clone(),
18✔
1395
                util.cloneToken(this.openingSquare),
1396
                util.cloneToken(this.closingSquare),
1397
                this.additionalIndexes?.map(e => e?.clone()),
2✔
1398
                util.cloneToken(this.equals)
1399
            ),
1400
            ['obj', 'index', 'value', 'additionalIndexes']
1401
        );
1402
    }
1403
}
1404

1405
export class LibraryStatement extends Statement implements TypedefProvider {
1✔
1406
    constructor(
1407
        readonly tokens: {
17✔
1408
            library: Token;
1409
            filePath: Token | undefined;
1410
        }
1411
    ) {
1412
        super();
17✔
1413
        this.range = util.createBoundingRange(
17✔
1414
            this.tokens?.library,
51✔
1415
            this.tokens?.filePath
51✔
1416
        );
1417
    }
1418

1419
    public readonly range: Range | undefined;
1420

1421
    transpile(state: BrsTranspileState) {
1422
        let result = [] as TranspileResult;
2✔
1423
        result.push(
2✔
1424
            state.transpileToken(this.tokens.library)
1425
        );
1426
        //there will be a parse error if file path is missing, but let's prevent a runtime error just in case
1427
        if (this.tokens.filePath) {
2!
1428
            result.push(
2✔
1429
                ' ',
1430
                state.transpileToken(this.tokens.filePath)
1431
            );
1432
        }
1433
        return result;
2✔
1434
    }
1435

1436
    getTypedef(state: BrsTranspileState) {
1437
        return this.transpile(state);
×
1438
    }
1439

1440
    walk(visitor: WalkVisitor, options: WalkOptions) {
1441
        //nothing to walk
1442
    }
1443

1444
    public clone() {
1445
        return this.finalizeClone(
2✔
1446
            new LibraryStatement(
1447
                this.tokens === undefined ? undefined : {
2✔
1448
                    library: util.cloneToken(this.tokens?.library),
3!
1449
                    filePath: util.cloneToken(this.tokens?.filePath)
3!
1450
                }
1451
            )
1452
        );
1453
    }
1454
}
1455

1456
export class NamespaceStatement extends Statement implements TypedefProvider {
1✔
1457
    constructor(
1458
        public keyword: Token,
283✔
1459
        // this should technically only be a VariableExpression or DottedGetExpression, but that can be enforced elsewhere
1460
        public nameExpression: NamespacedVariableNameExpression,
283✔
1461
        public body: Body,
283✔
1462
        public endKeyword: Token
283✔
1463
    ) {
1464
        super();
283✔
1465
        this.symbolTable = new SymbolTable(`NamespaceStatement: '${this.name}'`, () => this.parent?.getSymbolTable());
283!
1466
    }
1467

1468
    /**
1469
     * The string name for this namespace
1470
     */
1471
    public get name(): string {
1472
        return this.getName(ParseMode.BrighterScript);
991✔
1473
    }
1474

1475
    public get range() {
1476
        return this.cacheRange();
169✔
1477
    }
1478
    private _range: Range | undefined;
1479

1480
    public cacheRange() {
1481
        if (!this._range) {
451✔
1482
            this._range = util.createBoundingRange(
284✔
1483
                this.keyword,
1484
                this.nameExpression,
1485
                this.body,
1486
                this.endKeyword
1487
            );
1488
        }
1489
        return this._range;
451✔
1490
    }
1491

1492
    private _cachedNameBrighterScript: string | undefined;
1493
    private _cachedNameBrightScript: string | undefined;
1494

1495
    public getName(parseMode: ParseMode) {
1496
        if (parseMode === ParseMode.BrighterScript) {
3,153✔
1497
            if (this._cachedNameBrighterScript !== undefined) {
3,039✔
1498
                return this._cachedNameBrighterScript;
2,235✔
1499
            }
1500
        } else if (this._cachedNameBrightScript !== undefined) {
114✔
1501
            return this._cachedNameBrightScript;
23✔
1502
        }
1503
        const parentNamespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
895✔
1504
        let name = this.nameExpression?.getName?.(parseMode);
895✔
1505
        if (!name) {
895✔
1506
            return name;
1✔
1507
        }
1508
        if (parentNamespace) {
894✔
1509
            const sep = parseMode === ParseMode.BrighterScript ? '.' : '_';
21✔
1510
            name = parentNamespace.getName(parseMode) + sep + name;
21✔
1511
        }
1512
        //only cache once parent linking is in place (set during AST walk). The
1513
        //NamespaceStatement constructor calls `this.name` to build a SymbolTable
1514
        //label before walk runs; caching that pre-walk value would lock in the
1515
        //wrong (parentless) name forever.
1516
        if (this.parent) {
894✔
1517
            if (parseMode === ParseMode.BrighterScript) {
333✔
1518
                this._cachedNameBrighterScript = name;
242✔
1519
            } else {
1520
                this._cachedNameBrightScript = name;
91✔
1521
            }
1522
        }
1523
        return name;
894✔
1524
    }
1525

1526
    transpile(state: BrsTranspileState) {
1527
        //namespaces don't actually have any real content, so just transpile their bodies
1528
        return this.body.transpile(state);
42✔
1529
    }
1530

1531
    getTypedef(state: BrsTranspileState): TranspileResult {
1532
        let result = [
10✔
1533
            'namespace ',
1534
            ...this.getName(ParseMode.BrighterScript),
1535
            state.newline
1536
        ] as TranspileResult;
1537
        state.blockDepth++;
10✔
1538
        result.push(
10✔
1539
            ...this.body.getTypedef(state)
1540
        );
1541
        state.blockDepth--;
10✔
1542

1543
        result.push(
10✔
1544
            state.indent(),
1545
            'end namespace'
1546
        );
1547
        return result;
10✔
1548
    }
1549

1550
    walk(visitor: WalkVisitor, options: WalkOptions) {
1551
        if (options.walkMode & InternalWalkMode.walkExpressions) {
775✔
1552
            walk(this, 'nameExpression', visitor, options);
771✔
1553
        }
1554

1555
        if (this.body.statements.length > 0 && options.walkMode & InternalWalkMode.walkStatements) {
775✔
1556
            walk(this, 'body', visitor, options);
686✔
1557
        }
1558
    }
1559

1560
    public clone() {
1561
        const clone = this.finalizeClone(
3✔
1562
            new NamespaceStatement(
1563
                util.cloneToken(this.keyword),
1564
                this.nameExpression?.clone(),
9✔
1565
                this.body?.clone(),
9✔
1566
                util.cloneToken(this.endKeyword)
1567
            ),
1568
            ['nameExpression', 'body']
1569
        );
1570
        clone.cacheRange();
3✔
1571
        return clone;
3✔
1572
    }
1573
}
1574

1575
export class ImportStatement extends Statement implements TypedefProvider {
1✔
1576
    constructor(
1577
        readonly importToken: Token,
47✔
1578
        readonly filePathToken: Token | undefined
47✔
1579
    ) {
1580
        super();
47✔
1581
        this.range = util.createBoundingRange(
47✔
1582
            importToken,
1583
            filePathToken
1584
        );
1585
        if (this.filePathToken) {
47✔
1586
            //remove quotes
1587
            this.filePath = this.filePathToken.text.replace(/"/g, '');
45✔
1588
            if (this.filePathToken.range) {
45✔
1589
                //adjust the range to exclude the quotes
1590
                this.filePathToken.range = util.createRange(
42✔
1591
                    this.filePathToken.range.start.line,
1592
                    this.filePathToken.range.start.character + 1,
1593
                    this.filePathToken.range.end.line,
1594
                    this.filePathToken.range.end.character - 1
1595
                );
1596
            }
1597
        }
1598
    }
1599
    public filePath: string | undefined;
1600
    public range: Range | undefined;
1601

1602
    transpile(state: BrsTranspileState) {
1603
        //The xml files are responsible for adding the additional script imports, but
1604
        //add the import statement as a comment just for debugging purposes
1605
        return [
3✔
1606
            `'`,
1607
            state.transpileToken(this.importToken),
1608
            ' ',
1609
            state.transpileToken(this.filePathToken!)
1610
        ];
1611
    }
1612

1613
    /**
1614
     * Get the typedef for this statement
1615
     */
1616
    public getTypedef(state: BrsTranspileState) {
1617
        return [
3✔
1618
            this.importToken.text,
1619
            ' ',
1620
            //replace any `.bs` extension with `.brs`
1621
            this.filePathToken!.text.replace(/\.bs"?$/i, '.brs"')
1622
        ];
1623
    }
1624

1625
    walk(visitor: WalkVisitor, options: WalkOptions) {
1626
        //nothing to walk
1627
    }
1628

1629
    public clone() {
1630
        return this.finalizeClone(
1✔
1631
            new ImportStatement(
1632
                util.cloneToken(this.importToken),
1633
                util.cloneToken(this.filePathToken)
1634
            )
1635
        );
1636
    }
1637
}
1638

1639
export class InterfaceStatement extends Statement implements TypedefProvider {
1✔
1640
    constructor(
1641
        interfaceToken: Token,
1642
        name: Identifier,
1643
        extendsToken: Token,
1644
        public parentInterfaceName: NamespacedVariableNameExpression,
74✔
1645
        public body: Statement[],
74✔
1646
        endInterfaceToken: Token
1647
    ) {
1648
        super();
74✔
1649
        this.tokens.interface = interfaceToken;
74✔
1650
        this.tokens.name = name;
74✔
1651
        this.tokens.extends = extendsToken;
74✔
1652
        this.tokens.endInterface = endInterfaceToken;
74✔
1653
        this.range = util.createBoundingRange(
74✔
1654
            this.tokens.interface,
1655
            this.tokens.name,
1656
            this.tokens.extends,
1657
            this.parentInterfaceName,
1658
            ...this.body ?? [],
222✔
1659
            this.tokens.endInterface
1660
        );
1661
    }
1662

1663
    public tokens = {} as {
74✔
1664
        interface: Token;
1665
        name: Identifier;
1666
        extends: Token;
1667
        endInterface: Token;
1668
    };
1669

1670
    public range: Range | undefined;
1671

1672
    /**
1673
     * Get the name of the wrapping namespace (if it exists)
1674
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
1675
     */
1676
    public get namespaceName() {
1677
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
1678
    }
1679

1680
    public get fields() {
1681
        return this.body.filter(x => isInterfaceFieldStatement(x));
×
1682
    }
1683

1684
    public get methods() {
1685
        return this.body.filter(x => isInterfaceMethodStatement(x));
×
1686
    }
1687

1688
    /**
1689
     * The name of the interface WITH its leading namespace (if applicable)
1690
     */
1691
    public get fullName() {
1692
        const name = this.tokens.name?.text;
×
1693
        if (name) {
×
1694
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
×
1695
            if (namespace) {
×
1696
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
×
1697
                return `${namespaceName}.${name}`;
×
1698
            } else {
1699
                return name;
×
1700
            }
1701
        } else {
1702
            //return undefined which will allow outside callers to know that this interface doesn't have a name
1703
            return undefined;
×
1704
        }
1705
    }
1706

1707
    /**
1708
     * The name of the interface (without the namespace prefix)
1709
     */
1710
    public get name() {
1711
        return this.tokens.name?.text;
5!
1712
    }
1713

1714
    /**
1715
     * Get the name of this expression based on the parse mode
1716
     */
1717
    private _cachedNameBrighterScript: string | undefined;
1718
    private _cachedNameBrightScript: string | undefined;
1719

1720
    public getName(parseMode: ParseMode) {
1721
        if (parseMode === ParseMode.BrighterScript) {
5!
1722
            if (this._cachedNameBrighterScript !== undefined) {
5!
NEW
1723
                return this._cachedNameBrighterScript;
×
1724
            }
NEW
1725
        } else if (this._cachedNameBrightScript !== undefined) {
×
NEW
1726
            return this._cachedNameBrightScript;
×
1727
        }
1728
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
5✔
1729
        let result: string;
1730
        if (namespace) {
5✔
1731
            const delimiter = parseMode === ParseMode.BrighterScript ? '.' : '_';
1!
1732
            const namespaceName = namespace.getName(parseMode);
1✔
1733
            result = namespaceName + delimiter + this.name;
1✔
1734
        } else {
1735
            result = this.name;
4✔
1736
        }
1737
        //only cache once parent linking is in place (set during AST walk).
1738
        if (this.parent) {
5!
1739
            if (parseMode === ParseMode.BrighterScript) {
5!
1740
                this._cachedNameBrighterScript = result;
5✔
1741
            } else {
NEW
1742
                this._cachedNameBrightScript = result;
×
1743
            }
1744
        }
1745
        return result;
5✔
1746
    }
1747

1748
    public transpile(state: BrsTranspileState): TranspileResult {
1749
        //interfaces should completely disappear at runtime
1750
        return [];
19✔
1751
    }
1752

1753
    getTypedef(state: BrsTranspileState) {
1754
        const result = [] as TranspileResult;
6✔
1755
        for (let annotation of this.annotations ?? []) {
6✔
1756
            result.push(
1✔
1757
                ...annotation.getTypedef(state),
1758
                state.newline,
1759
                state.indent()
1760
            );
1761
        }
1762
        result.push(
6✔
1763
            this.tokens.interface.text,
1764
            ' ',
1765
            this.tokens.name.text
1766
        );
1767
        const parentInterfaceName = this.parentInterfaceName?.getName(ParseMode.BrighterScript);
6!
1768
        if (parentInterfaceName) {
6!
1769
            result.push(
×
1770
                ' extends ',
1771
                parentInterfaceName
1772
            );
1773
        }
1774
        const body = this.body ?? [];
6!
1775
        if (body.length > 0) {
6!
1776
            state.blockDepth++;
6✔
1777
        }
1778
        for (const statement of body) {
6✔
1779
            if (isInterfaceMethodStatement(statement) || isInterfaceFieldStatement(statement)) {
21✔
1780
                result.push(
20✔
1781
                    state.newline,
1782
                    state.indent(),
1783
                    ...statement.getTypedef(state)
1784
                );
1785
            } else {
1786
                result.push(
1✔
1787
                    state.newline,
1788
                    state.indent(),
1789
                    ...statement.transpile(state)
1790
                );
1791
            }
1792
        }
1793
        if (body.length > 0) {
6!
1794
            state.blockDepth--;
6✔
1795
        }
1796
        result.push(
6✔
1797
            state.newline,
1798
            state.indent(),
1799
            'end interface',
1800
            state.newline
1801
        );
1802
        return result;
6✔
1803
    }
1804

1805
    walk(visitor: WalkVisitor, options: WalkOptions) {
1806
        //visitor-less walk function to do parent linking
1807
        walk(this, 'parentInterfaceName', null, options);
177✔
1808

1809
        if (options.walkMode & InternalWalkMode.walkStatements) {
177!
1810
            walkArray(this.body, visitor, options, this);
177✔
1811
        }
1812
    }
1813

1814
    public clone() {
1815
        return this.finalizeClone(
8✔
1816
            new InterfaceStatement(
1817
                util.cloneToken(this.tokens.interface),
1818
                util.cloneToken(this.tokens.name),
1819
                util.cloneToken(this.tokens.extends),
1820
                this.parentInterfaceName?.clone(),
24✔
1821
                this.body?.map(x => x?.clone()),
8✔
1822
                util.cloneToken(this.tokens.endInterface)
1823
            ),
1824
            ['parentInterfaceName', 'body']
1825
        );
1826
    }
1827
}
1828

1829
export class InterfaceFieldStatement extends Statement implements TypedefProvider {
1✔
1830
    public transpile(state: BrsTranspileState): TranspileResult {
1831
        throw new Error('Method not implemented.');
×
1832
    }
1833
    constructor(
1834
        nameToken: Identifier,
1835
        asToken: Token,
1836
        typeToken: Token,
1837
        public type: BscType,
79✔
1838
        optionalToken?: Token
1839
    ) {
1840
        super();
79✔
1841
        this.tokens.optional = optionalToken;
79✔
1842
        this.tokens.name = nameToken;
79✔
1843
        this.tokens.as = asToken;
79✔
1844
        this.tokens.type = typeToken;
79✔
1845
        this.range = util.createBoundingRange(
79✔
1846
            optionalToken,
1847
            nameToken,
1848
            asToken,
1849
            typeToken
1850
        );
1851
    }
1852

1853
    public range: Range | undefined;
1854

1855
    public tokens = {} as {
79✔
1856
        optional?: Token;
1857
        name: Identifier;
1858
        as: Token;
1859
        type: Token;
1860
    };
1861

1862
    public get name() {
1863
        return this.tokens.name.text;
×
1864
    }
1865

1866
    public get isOptional() {
1867
        return !!this.tokens.optional;
10✔
1868
    }
1869

1870
    walk(visitor: WalkVisitor, options: WalkOptions) {
1871
        //nothing to walk
1872
    }
1873

1874
    getTypedef(state: BrsTranspileState): TranspileResult {
1875
        const result = [] as TranspileResult;
10✔
1876
        for (let annotation of this.annotations ?? []) {
10✔
1877
            result.push(
1✔
1878
                ...annotation.getTypedef(state),
1879
                state.newline,
1880
                state.indent()
1881
            );
1882
        }
1883
        if (this.isOptional) {
10!
1884
            result.push(
×
1885
                this.tokens.optional!.text,
1886
                ' '
1887
            );
1888
        }
1889
        result.push(
10✔
1890
            this.tokens.name.text
1891
        );
1892
        if (this.tokens.type?.text?.length > 0) {
10!
1893
            result.push(
10✔
1894
                ' as ',
1895
                this.tokens.type.text
1896
            );
1897
        }
1898
        return result;
10✔
1899
    }
1900

1901
    public clone() {
1902
        return this.finalizeClone(
4✔
1903
            new InterfaceFieldStatement(
1904
                util.cloneToken(this.tokens.name),
1905
                util.cloneToken(this.tokens.as),
1906
                util.cloneToken(this.tokens.type),
1907
                this.type?.clone(),
12✔
1908
                util.cloneToken(this.tokens.optional)
1909
            )
1910
        );
1911
    }
1912

1913
}
1914

1915
export class InterfaceMethodStatement extends Statement implements TypedefProvider {
1✔
1916
    public transpile(state: BrsTranspileState): TranspileResult {
1917
        throw new Error('Method not implemented.');
×
1918
    }
1919
    constructor(
1920
        functionTypeToken: Token,
1921
        nameToken: Identifier,
1922
        leftParen: Token,
1923
        public params: FunctionParameterExpression[],
23✔
1924
        rightParen: Token,
1925
        asToken?: Token,
1926
        returnTypeToken?: Token,
1927
        public returnType?: BscType,
23✔
1928
        optionalToken?: Token
1929
    ) {
1930
        super();
23✔
1931
        this.tokens.optional = optionalToken;
23✔
1932
        this.tokens.functionType = functionTypeToken;
23✔
1933
        this.tokens.name = nameToken;
23✔
1934
        this.tokens.leftParen = leftParen;
23✔
1935
        this.tokens.rightParen = rightParen;
23✔
1936
        this.tokens.as = asToken;
23✔
1937
        this.tokens.returnType = returnTypeToken;
23✔
1938
    }
1939

1940
    public get range() {
1941
        return util.createBoundingRange(
2✔
1942
            this.tokens.optional,
1943
            this.tokens.functionType,
1944
            this.tokens.name,
1945
            this.tokens.leftParen,
1946
            ...(this.params ?? []),
6!
1947
            this.tokens.rightParen,
1948
            this.tokens.as,
1949
            this.tokens.returnType
1950
        );
1951
    }
1952

1953
    public tokens = {} as {
23✔
1954
        optional?: Token;
1955
        functionType: Token;
1956
        name: Identifier;
1957
        leftParen: Token;
1958
        rightParen: Token;
1959
        as: Token | undefined;
1960
        returnType: Token | undefined;
1961
    };
1962

1963
    public get isOptional() {
1964
        return !!this.tokens.optional;
10✔
1965
    }
1966

1967
    walk(visitor: WalkVisitor, options: WalkOptions) {
1968
        //nothing to walk
1969
    }
1970

1971
    getTypedef(state: BrsTranspileState) {
1972
        const result = [] as TranspileResult;
10✔
1973
        for (let annotation of this.annotations ?? []) {
10✔
1974
            result.push(
1✔
1975
                ...annotation.getTypedef(state),
1976
                state.newline,
1977
                state.indent()
1978
            );
1979
        }
1980

1981
        if (this.isOptional) {
10!
1982
            result.push(
×
1983
                this.tokens.optional!.text,
1984
                ' '
1985
            );
1986
        }
1987

1988
        result.push(
10✔
1989
            this.tokens.functionType.text,
1990
            ' ',
1991
            this.tokens.name.text,
1992
            '('
1993
        );
1994
        const params = this.params ?? [];
10!
1995
        for (let i = 0; i < params.length; i++) {
10✔
1996
            if (i > 0) {
2✔
1997
                result.push(', ');
1✔
1998
            }
1999
            const param = params[i];
2✔
2000
            result.push(param.name.text);
2✔
2001
            const typeToken = param.typeToken;
2✔
2002
            if (typeToken && typeToken.text.length > 0) {
2!
2003
                result.push(
2✔
2004
                    ' as ',
2005
                    typeToken.text
2006
                );
2007
            }
2008
        }
2009
        result.push(
10✔
2010
            ')'
2011
        );
2012
        const returnTypeToken = this.tokens.returnType;
10✔
2013
        if (returnTypeToken && returnTypeToken.text.length > 0) {
10!
2014
            result.push(
10✔
2015
                ' as ',
2016
                returnTypeToken.text
2017
            );
2018
        }
2019
        return result;
10✔
2020
    }
2021

2022
    public clone() {
2023
        return this.finalizeClone(
3✔
2024
            new InterfaceMethodStatement(
2025
                util.cloneToken(this.tokens.functionType),
2026
                util.cloneToken(this.tokens.name),
2027
                util.cloneToken(this.tokens.leftParen),
2028
                this.params?.map(p => p?.clone()),
3✔
2029
                util.cloneToken(this.tokens.rightParen),
2030
                util.cloneToken(this.tokens.as),
2031
                util.cloneToken(this.tokens.returnType),
2032
                this.returnType?.clone(),
9✔
2033
                util.cloneToken(this.tokens.optional)
2034
            ),
2035
            ['params']
2036
        );
2037
    }
2038
}
2039

2040
export class ClassStatement extends Statement implements TypedefProvider {
1✔
2041

2042
    constructor(
2043
        readonly classKeyword: Token,
509✔
2044
        /**
2045
         * The name of the class (without namespace prefix)
2046
         */
2047
        readonly name: Identifier,
509✔
2048
        public body: Statement[],
509✔
2049
        readonly end: Token,
509✔
2050
        readonly extendsKeyword?: Token,
509✔
2051
        readonly parentClassName?: NamespacedVariableNameExpression
509✔
2052
    ) {
2053
        super();
509✔
2054
        this.body = this.body ?? [];
509✔
2055

2056
        for (let statement of this.body) {
509✔
2057
            if (isMethodStatement(statement)) {
479✔
2058
                this.methods.push(statement);
283✔
2059
                this.memberMap[statement?.name?.text.toLowerCase()] = statement;
283!
2060
            } else if (isFieldStatement(statement)) {
196✔
2061
                this.fields.push(statement);
187✔
2062
                this.memberMap[statement.name?.text.toLowerCase()] = statement;
187!
2063
            }
2064
        }
2065

2066
        this.range = util.createBoundingRange(
509✔
2067
            classKeyword,
2068
            name,
2069
            extendsKeyword,
2070
            parentClassName,
2071
            ...(body ?? []),
1,527✔
2072
            end
2073
        );
2074
    }
2075

2076
    /**
2077
     * Get the name of the wrapping namespace (if it exists)
2078
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
2079
     */
2080
    public get namespaceName() {
2081
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
2082
    }
2083

2084

2085
    private _cachedNameBrighterScript: string | undefined;
2086
    private _cachedNameBrightScript: string | undefined;
2087

2088
    public getName(parseMode: ParseMode) {
2089
        if (parseMode === ParseMode.BrighterScript) {
922✔
2090
            if (this._cachedNameBrighterScript !== undefined) {
813✔
2091
                return this._cachedNameBrighterScript;
593✔
2092
            }
2093
        } else if (this._cachedNameBrightScript !== undefined) {
109✔
2094
            return this._cachedNameBrightScript;
56✔
2095
        }
2096
        const name = this.name?.text;
273✔
2097
        if (!name) {
273✔
2098
            //return undefined which will allow outside callers to know that this class doesn't have a name
2099
            return undefined;
2✔
2100
        }
2101
        let result: string;
2102
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
271✔
2103
        if (namespace) {
271✔
2104
            const namespaceName = namespace.getName(parseMode);
72✔
2105
            const separator = parseMode === ParseMode.BrighterScript ? '.' : '_';
72✔
2106
            result = namespaceName + separator + name;
72✔
2107
        } else {
2108
            result = name;
199✔
2109
        }
2110
        //only cache once parent linking is in place (set during AST walk).
2111
        if (this.parent) {
271!
2112
            if (parseMode === ParseMode.BrighterScript) {
271✔
2113
                this._cachedNameBrighterScript = result;
218✔
2114
            } else {
2115
                this._cachedNameBrightScript = result;
53✔
2116
            }
2117
        }
2118
        return result;
271✔
2119
    }
2120

2121
    public memberMap = {} as Record<string, MemberStatement>;
509✔
2122
    public methods = [] as MethodStatement[];
509✔
2123
    public fields = [] as FieldStatement[];
509✔
2124

2125
    public readonly range: Range | undefined;
2126

2127
    transpile(state: BrsTranspileState) {
2128
        let result = [] as TranspileResult;
49✔
2129

2130
        const className = this.getName(ParseMode.BrightScript).replace(/\./g, '_');
49✔
2131
        const ancestors = this.getAncestors(state);
49✔
2132
        const body = this.getTranspiledClassBody(ancestors);
49✔
2133

2134
        //make the methods
2135
        result.push(...this.getTranspiledMethods(state, className, body));
49✔
2136
        //make the builder
2137
        result.push(...this.getTranspiledBuilder(state, className, ancestors, body));
49✔
2138
        result.push('\n', state.indent());
49✔
2139
        //make the class assembler (i.e. the public-facing class creator method)
2140
        result.push(...this.getTranspiledClassFunction(state, className));
49✔
2141

2142
        return result;
49✔
2143
    }
2144

2145
    getTypedef(state: BrsTranspileState) {
2146
        const result = [] as TranspileResult;
15✔
2147
        for (let annotation of this.annotations ?? []) {
15!
2148
            result.push(
×
2149
                ...annotation.getTypedef(state),
2150
                state.newline,
2151
                state.indent()
2152
            );
2153
        }
2154
        result.push(
15✔
2155
            'class ',
2156
            this.name.text
2157
        );
2158
        if (this.extendsKeyword && this.parentClassName) {
15✔
2159
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
4✔
2160
            const fqName = util.getFullyQualifiedClassName(
4✔
2161
                this.parentClassName.getName(ParseMode.BrighterScript),
2162
                namespace?.getName(ParseMode.BrighterScript)
12✔
2163
            );
2164
            result.push(
4✔
2165
                ` extends ${fqName}`
2166
            );
2167
        }
2168
        result.push(state.newline);
15✔
2169
        state.blockDepth++;
15✔
2170

2171
        let body = this.body;
15✔
2172
        //inject an empty "new" method if missing
2173
        if (!this.getConstructorFunction()) {
15✔
2174
            const constructor = createMethodStatement('new', TokenKind.Sub);
11✔
2175
            constructor.parent = this;
11✔
2176
            //walk the constructor to set up parent links
2177
            constructor.link();
11✔
2178
            body = [
11✔
2179
                constructor,
2180
                ...this.body
2181
            ];
2182
        }
2183

2184
        for (const member of body) {
15✔
2185
            if (isTypedefProvider(member)) {
33!
2186
                result.push(
33✔
2187
                    state.indent(),
2188
                    ...member.getTypedef(state),
2189
                    state.newline
2190
                );
2191
            }
2192
        }
2193
        state.blockDepth--;
15✔
2194
        result.push(
15✔
2195
            state.indent(),
2196
            'end class'
2197
        );
2198
        return result;
15✔
2199
    }
2200

2201
    /**
2202
     * Find the parent index for this class's parent.
2203
     * For class inheritance, every class is given an index.
2204
     * The base class is index 0, its child is index 1, and so on.
2205
     */
2206
    public getParentClassIndex(state: BrsTranspileState) {
2207
        let myIndex = 0;
112✔
2208
        let stmt = this as ClassStatement;
112✔
2209
        while (stmt) {
112✔
2210
            if (stmt.parentClassName) {
185✔
2211
                const namespace = stmt.findAncestor<NamespaceStatement>(isNamespaceStatement);
74✔
2212
                //find the parent class
2213
                stmt = state.file.getClassFileLink(
74✔
2214
                    stmt.parentClassName.getName(ParseMode.BrighterScript),
2215
                    namespace?.getName(ParseMode.BrighterScript)
222✔
2216
                )?.item;
74✔
2217
                myIndex++;
74✔
2218
            } else {
2219
                break;
111✔
2220
            }
2221
        }
2222
        const result = myIndex - 1;
112✔
2223
        if (result >= 0) {
112✔
2224
            return result;
55✔
2225
        } else {
2226
            return null;
57✔
2227
        }
2228
    }
2229

2230
    public hasParentClass() {
2231
        return !!this.parentClassName;
49✔
2232
    }
2233

2234
    /**
2235
     * Get all ancestor classes, in closest-to-furthest order (i.e. 0 is parent, 1 is grandparent, etc...).
2236
     * This will return an empty array if no ancestors were found
2237
     */
2238
    public getAncestors(state: BrsTranspileState) {
2239
        let ancestors = [] as ClassStatement[];
125✔
2240
        let stmt = this as ClassStatement;
125✔
2241
        while (stmt) {
125✔
2242
            if (stmt.parentClassName) {
213✔
2243
                const namespace = stmt.findAncestor<NamespaceStatement>(isNamespaceStatement);
88✔
2244
                stmt = state.file.getClassFileLink(
88✔
2245
                    stmt.parentClassName.getName(ParseMode.BrighterScript),
2246
                    namespace?.getName(ParseMode.BrighterScript)
264✔
2247
                )?.item;
88!
2248
                ancestors.push(stmt);
88✔
2249
            } else {
2250
                break;
125✔
2251
            }
2252
        }
2253
        return ancestors;
125✔
2254
    }
2255

2256
    private getBuilderName(transpiledClassName: string) {
2257
        return `__${transpiledClassName}_builder`;
122✔
2258
    }
2259

2260
    private getMethodIdentifier(transpiledClassName: string, statement: MethodStatement) {
2261
        return { ...statement.name, text: `__${transpiledClassName}_method_${statement.name.text}` };
124✔
2262
    }
2263

2264
    /**
2265
     * Get the constructor function for this class (if exists), or undefined if not exist
2266
     */
2267
    private getConstructorFunction() {
2268
        return this.body.find((stmt) => {
153✔
2269
            return (stmt as MethodStatement)?.name?.text?.toLowerCase() === 'new';
134!
2270
        }) as MethodStatement;
2271
    }
2272

2273
    /**
2274
     * Return the parameters for the first constructor function for this class
2275
     * @param ancestors The list of ancestors for this class
2276
     * @returns The parameters for the first constructor function for this class
2277
     */
2278
    private getConstructorParams(ancestors: ClassStatement[]) {
2279
        for (let ancestor of ancestors) {
42✔
2280
            const ctor = ancestor?.getConstructorFunction();
40!
2281
            if (ctor) {
40✔
2282
                return ctor.func.parameters;
16✔
2283
            }
2284
        }
2285
        return [];
26✔
2286
    }
2287

2288
    /**
2289
     * Determine if the specified field was declared in one of the ancestor classes
2290
     */
2291
    public isFieldDeclaredByAncestor(fieldName: string, ancestors: ClassStatement[]) {
2292
        let lowerFieldName = fieldName.toLowerCase();
×
2293
        for (let ancestor of ancestors) {
×
2294
            if (ancestor.memberMap[lowerFieldName]) {
×
2295
                return true;
×
2296
            }
2297
        }
2298
        return false;
×
2299
    }
2300

2301
    /**
2302
     * The builder is a function that assigns all of the methods and property names to a class instance.
2303
     * This needs to be a separate function so that child classes can call the builder from their parent
2304
     * without instantiating the parent constructor at that point in time.
2305
     */
2306
    private getTranspiledBuilder(state: BrsTranspileState, transpiledClassName: string, ancestors: ClassStatement[], body: Statement[]) {
2307
        let result = [] as TranspileResult;
49✔
2308
        result.push(`function ${this.getBuilderName(transpiledClassName)}()\n`);
49✔
2309
        state.blockDepth++;
49✔
2310
        //indent
2311
        result.push(state.indent());
49✔
2312

2313
        //construct parent class or empty object
2314
        if (ancestors[0]) {
49✔
2315
            const ancestorNamespace = ancestors[0].findAncestor<NamespaceStatement>(isNamespaceStatement);
24✔
2316
            let fullyQualifiedClassName = util.getFullyQualifiedClassName(
24✔
2317
                ancestors[0].getName(ParseMode.BrighterScript)!,
2318
                ancestorNamespace?.getName(ParseMode.BrighterScript)
72✔
2319
            );
2320
            result.push(`instance = ${this.getBuilderName(fullyQualifiedClassName.replace(/\./g, '_'))}()`);
24✔
2321
        } else {
2322
            //use an empty object.
2323
            result.push('instance = {}');
25✔
2324
        }
2325
        result.push(
49✔
2326
            state.newline,
2327
            state.indent()
2328
        );
2329
        let parentClassIndex = this.getParentClassIndex(state);
49✔
2330

2331
        for (let statement of body) {
49✔
2332
            //is field statement
2333
            if (isFieldStatement(statement)) {
73✔
2334
                //do nothing with class fields in this situation, they are handled elsewhere
2335
                continue;
11✔
2336

2337
                //methods
2338
            } else if (isMethodStatement(statement)) {
62!
2339

2340
                //store overridden parent methods as super{parentIndex}_{methodName}
2341
                if (
62✔
2342
                    //is override method
2343
                    statement.override ||
169✔
2344
                    //is constructor function in child class
2345
                    (statement.name.text.toLowerCase() === 'new' && ancestors[0])
2346
                ) {
2347
                    result.push(
28✔
2348
                        `instance.super${parentClassIndex}_${statement.name.text} = instance.${statement.name.text}`,
2349
                        state.newline,
2350
                        state.indent()
2351
                    );
2352
                }
2353

2354
                state.classStatement = this;
62✔
2355
                result.push(
62✔
2356
                    'instance.',
2357
                    state.transpileToken(statement.name),
2358
                    ' = ',
2359
                    state.transpileToken(this.getMethodIdentifier(transpiledClassName, statement)),
2360
                    state.newline,
2361
                    state.indent()
2362
                );
2363
                delete state.classStatement;
62✔
2364
            } else {
2365
                //other random statements (probably just comments)
2366
                result.push(
×
2367
                    ...statement.transpile(state),
2368
                    state.newline,
2369
                    state.indent()
2370
                );
2371
            }
2372
        }
2373
        //return the instance
2374
        result.push('return instance\n');
49✔
2375
        state.blockDepth--;
49✔
2376
        result.push(state.indent());
49✔
2377
        result.push(`end function`);
49✔
2378
        return result;
49✔
2379
    }
2380

2381
    /**
2382
     * Returns a copy of the class' body, with the constructor function added if it doesn't exist.
2383
     */
2384
    private getTranspiledClassBody(ancestors: ClassStatement[]): Statement[] {
2385
        const body = [];
49✔
2386
        body.push(...this.body);
49✔
2387

2388
        //inject an empty "new" method if missing
2389
        if (!this.getConstructorFunction()) {
49✔
2390
            if (ancestors.length === 0) {
27✔
2391
                body.unshift(createMethodStatement('new', TokenKind.Sub));
12✔
2392
            } else {
2393
                const params = this.getConstructorParams(ancestors);
15✔
2394
                const call = new ExpressionStatement(
15✔
2395
                    new CallExpression(
2396
                        new VariableExpression(createToken(TokenKind.Identifier, 'super')),
2397
                        createToken(TokenKind.LeftParen),
2398
                        createToken(TokenKind.RightParen),
2399
                        params.map(x => new VariableExpression(x.name))
6✔
2400
                    )
2401
                );
2402
                body.unshift(
15✔
2403
                    new MethodStatement(
2404
                        [],
2405
                        createIdentifier('new'),
2406
                        new FunctionExpression(
2407
                            params.map(x => x.clone()),
6✔
2408
                            new Block([call]),
2409
                            createToken(TokenKind.Sub),
2410
                            createToken(TokenKind.EndSub),
2411
                            createToken(TokenKind.LeftParen),
2412
                            createToken(TokenKind.RightParen)
2413
                        ),
2414
                        null
2415
                    )
2416
                );
2417
            }
2418
        }
2419

2420
        return body;
49✔
2421
    }
2422

2423
    /**
2424
     * These are the methods that are defined in this class. They are transpiled outside of the class body
2425
     * to ensure they don't appear as "$anon_#" in stack traces and crash logs.
2426
     */
2427
    private getTranspiledMethods(state: BrsTranspileState, transpiledClassName: string, body: Statement[]) {
2428
        let result = [] as TranspileResult;
49✔
2429
        for (let statement of body) {
49✔
2430
            if (isMethodStatement(statement)) {
73✔
2431
                state.classStatement = this;
62✔
2432
                result.push(
62✔
2433
                    ...statement.transpile(state, this.getMethodIdentifier(transpiledClassName, statement)),
2434
                    state.newline,
2435
                    state.indent()
2436
                );
2437
                delete state.classStatement;
62✔
2438
            }
2439
        }
2440
        return result;
49✔
2441
    }
2442

2443
    /**
2444
     * The class function is the function with the same name as the class. This is the function that
2445
     * consumers should call to create a new instance of that class.
2446
     * This invokes the builder, gets an instance of the class, then invokes the "new" function on that class.
2447
     */
2448
    private getTranspiledClassFunction(state: BrsTranspileState, transpiledClassName: string) {
2449
        let result = [] as TranspileResult;
49✔
2450

2451
        const constructorFunction = this.getConstructorFunction();
49✔
2452
        let constructorParams = [];
49✔
2453
        if (constructorFunction) {
49✔
2454
            constructorParams = constructorFunction.func.parameters;
22✔
2455
        } else {
2456
            constructorParams = this.getConstructorParams(this.getAncestors(state));
27✔
2457
        }
2458

2459
        result.push(
49✔
2460
            state.sourceNode(this.classKeyword, 'function'),
2461
            state.sourceNode(this.classKeyword, ' '),
2462
            state.sourceNode(this.name, this.getName(ParseMode.BrightScript)!),
2463
            `(`
2464
        );
2465
        let i = 0;
49✔
2466
        for (let param of constructorParams) {
49✔
2467
            if (i > 0) {
17✔
2468
                result.push(', ');
4✔
2469
            }
2470
            result.push(
17✔
2471
                param.transpile(state)
2472
            );
2473
            i++;
17✔
2474
        }
2475
        result.push(
49✔
2476
            ')',
2477
            '\n'
2478
        );
2479

2480
        state.blockDepth++;
49✔
2481
        result.push(state.indent());
49✔
2482
        result.push(`instance = ${this.getBuilderName(transpiledClassName)}()\n`);
49✔
2483

2484
        result.push(state.indent());
49✔
2485
        result.push(`instance.new(`);
49✔
2486

2487
        //append constructor arguments
2488
        i = 0;
49✔
2489
        for (let param of constructorParams) {
49✔
2490
            if (i > 0) {
17✔
2491
                result.push(', ');
4✔
2492
            }
2493
            result.push(
17✔
2494
                state.transpileToken(param.name)
2495
            );
2496
            i++;
17✔
2497
        }
2498
        result.push(
49✔
2499
            ')',
2500
            '\n'
2501
        );
2502

2503
        result.push(state.indent());
49✔
2504
        result.push(`return instance\n`);
49✔
2505

2506
        state.blockDepth--;
49✔
2507
        result.push(state.indent());
49✔
2508
        result.push(`end function`);
49✔
2509
        return result;
49✔
2510
    }
2511

2512
    walk(visitor: WalkVisitor, options: WalkOptions) {
2513
        //visitor-less walk function to do parent linking
2514
        walk(this, 'parentClassName', null, options);
1,214✔
2515

2516
        if (options.walkMode & InternalWalkMode.walkStatements) {
1,214!
2517
            walkArray(this.body, visitor, options, this);
1,214✔
2518
        }
2519
    }
2520

2521
    public clone() {
2522
        return this.finalizeClone(
11✔
2523
            new ClassStatement(
2524
                util.cloneToken(this.classKeyword),
2525
                util.cloneToken(this.name),
2526
                this.body?.map(x => x?.clone()),
10✔
2527
                util.cloneToken(this.end),
2528
                util.cloneToken(this.extendsKeyword),
2529
                this.parentClassName?.clone()
33✔
2530
            ),
2531
            ['body', 'parentClassName']
2532
        );
2533
    }
2534
}
2535

2536
const accessModifiers = [
1✔
2537
    TokenKind.Public,
2538
    TokenKind.Protected,
2539
    TokenKind.Private
2540
];
2541
export class MethodStatement extends FunctionStatement {
1✔
2542
    constructor(
2543
        modifiers: Token | Token[],
2544
        name: Identifier,
2545
        func: FunctionExpression,
2546
        public override: Token
321✔
2547
    ) {
2548
        super(name, func);
321✔
2549
        if (modifiers) {
321✔
2550
            if (Array.isArray(modifiers)) {
51✔
2551
                this.modifiers.push(...modifiers);
19✔
2552
            } else {
2553
                this.modifiers.push(modifiers);
32✔
2554
            }
2555
        }
2556
        this.range = util.createBoundingRange(
321✔
2557
            ...(this.modifiers),
2558
            override,
2559
            func
2560
        );
2561
    }
2562

2563
    public modifiers: Token[] = [];
321✔
2564

2565
    public get accessModifier() {
2566
        return this.modifiers.find(x => accessModifiers.includes(x.kind));
110✔
2567
    }
2568

2569
    public readonly range: Range | undefined;
2570

2571
    /**
2572
     * Get the name of this method.
2573
     */
2574
    public getName(parseMode: ParseMode) {
2575
        return this.name.text;
1✔
2576
    }
2577

2578
    transpile(state: BrsTranspileState, name?: Identifier) {
2579
        if (this.name.text.toLowerCase() === 'new') {
62✔
2580
            this.ensureSuperConstructorCall(state);
49✔
2581
            //TODO we need to undo this at the bottom of this method
2582
            this.injectFieldInitializersForConstructor(state);
49✔
2583
        }
2584
        //TODO - remove type information from these methods because that doesn't work
2585
        //convert the `super` calls into the proper methods
2586
        const parentClassIndex = state.classStatement.getParentClassIndex(state);
62✔
2587
        const visitor = createVisitor({
62✔
2588
            VariableExpression: e => {
2589
                if (e.name.text.toLocaleLowerCase() === 'super') {
69✔
2590
                    state.editor.setProperty(e.name, 'text', `m.super${parentClassIndex}_new`);
24✔
2591
                }
2592
            },
2593
            DottedGetExpression: e => {
2594
                const beginningVariable = util.findBeginningVariableExpression(e);
27✔
2595
                const lowerName = beginningVariable?.getName(ParseMode.BrighterScript).toLowerCase();
27!
2596
                if (lowerName === 'super') {
27✔
2597
                    state.editor.setProperty(beginningVariable.name, 'text', 'm');
7✔
2598
                    state.editor.setProperty(e.name, 'text', `super${parentClassIndex}_${e.name.text}`);
7✔
2599
                }
2600
            }
2601
        });
2602
        const walkOptions: WalkOptions = { walkMode: WalkMode.visitExpressions };
62✔
2603
        for (const statement of this.func.body.statements) {
62✔
2604
            visitor(statement, undefined);
65✔
2605
            statement.walk(visitor, walkOptions);
65✔
2606
        }
2607
        return this.func.transpile(state, name);
62✔
2608
    }
2609

2610
    getTypedef(state: BrsTranspileState) {
2611
        const result = [] as TranspileResult;
23✔
2612
        for (let annotation of this.annotations ?? []) {
23✔
2613
            result.push(
2✔
2614
                ...annotation.getTypedef(state),
2615
                state.newline,
2616
                state.indent()
2617
            );
2618
        }
2619
        if (this.accessModifier) {
23✔
2620
            result.push(
8✔
2621
                this.accessModifier.text,
2622
                ' '
2623
            );
2624
        }
2625
        if (this.override) {
23✔
2626
            result.push('override ');
1✔
2627
        }
2628
        result.push(
23✔
2629
            ...this.func.getTypedef(state)
2630
        );
2631
        return result;
23✔
2632
    }
2633

2634
    /**
2635
     * All child classes must call the parent constructor. The type checker will warn users when they don't call it in their own class,
2636
     * but we still need to call it even if they have omitted it. This injects the super call if it's missing
2637
     */
2638
    private ensureSuperConstructorCall(state: BrsTranspileState) {
2639
        //if this class doesn't extend another class, quit here
2640
        if (state.classStatement!.getAncestors(state).length === 0) {
49✔
2641
            return;
25✔
2642
        }
2643

2644
        //check whether any calls to super exist
2645
        let containsSuperCall =
2646
            this.func.body.statements.findIndex((x) => {
24✔
2647
                //is a call statement
2648
                return isExpressionStatement(x) && isCallExpression(x.expression) &&
25✔
2649
                    //is a call to super
2650
                    util.findBeginningVariableExpression(x.expression.callee as any)?.name.text.toLowerCase() === 'super';
69!
2651
            }) !== -1;
2652

2653
        //if a call to super exists, quit here
2654
        if (containsSuperCall) {
24✔
2655
            return;
23✔
2656
        }
2657

2658
        //this is a child class, and the constructor doesn't contain a call to super. Inject one
2659
        const superCall = new ExpressionStatement(
1✔
2660
            new CallExpression(
2661
                new VariableExpression(
2662
                    {
2663
                        kind: TokenKind.Identifier,
2664
                        text: 'super',
2665
                        isReserved: false,
2666
                        range: state.classStatement!.name.range,
2667
                        leadingWhitespace: ''
2668
                    }
2669
                ),
2670
                {
2671
                    kind: TokenKind.LeftParen,
2672
                    text: '(',
2673
                    isReserved: false,
2674
                    range: state.classStatement!.name.range,
2675
                    leadingWhitespace: ''
2676
                },
2677
                {
2678
                    kind: TokenKind.RightParen,
2679
                    text: ')',
2680
                    isReserved: false,
2681
                    range: state.classStatement!.name.range,
2682
                    leadingWhitespace: ''
2683
                },
2684
                []
2685
            )
2686
        );
2687
        state.editor.arrayUnshift(this.func.body.statements, superCall);
1✔
2688
    }
2689

2690
    /**
2691
     * Inject field initializers at the top of the `new` function (after any present `super()` call)
2692
     */
2693
    private injectFieldInitializersForConstructor(state: BrsTranspileState) {
2694
        let startingIndex = state.classStatement!.hasParentClass() ? 1 : 0;
49✔
2695

2696
        let newStatements = [] as Statement[];
49✔
2697
        //insert the field initializers in order
2698
        for (let field of state.classStatement!.fields) {
49✔
2699
            let thisQualifiedName = { ...field.name };
11✔
2700
            thisQualifiedName.text = 'm.' + field.name?.text;
11!
2701
            if (field.initialValue) {
11✔
2702
                newStatements.push(
9✔
2703
                    new AssignmentStatement(field.equal, thisQualifiedName, field.initialValue)
2704
                );
2705
            } else {
2706
                //if there is no initial value, set the initial value to `invalid`
2707
                newStatements.push(
2✔
2708
                    new AssignmentStatement(
2709
                        createToken(TokenKind.Equal, '=', field.name?.range),
6!
2710
                        thisQualifiedName,
2711
                        createInvalidLiteral('invalid', field.name?.range)
6!
2712
                    )
2713
                );
2714
            }
2715
        }
2716
        state.editor.arraySplice(this.func.body.statements, startingIndex, 0, ...newStatements);
49✔
2717
    }
2718

2719
    walk(visitor: WalkVisitor, options: WalkOptions) {
2720
        if (options.walkMode & InternalWalkMode.walkExpressions) {
966✔
2721
            walk(this, 'func', visitor, options);
761✔
2722
        }
2723
    }
2724

2725
    public clone() {
2726
        return this.finalizeClone(
5✔
2727
            new MethodStatement(
2728
                this.modifiers?.map(m => util.cloneToken(m)),
1✔
2729
                util.cloneToken(this.name),
2730
                this.func?.clone(),
15✔
2731
                util.cloneToken(this.override)
2732
            ),
2733
            ['func']
2734
        );
2735
    }
2736
}
2737
/**
2738
 * @deprecated use `MethodStatement`
2739
 */
2740
export class ClassMethodStatement extends MethodStatement { }
1✔
2741

2742
export class FieldStatement extends Statement implements TypedefProvider {
1✔
2743

2744
    constructor(
2745
        readonly accessModifier?: Token,
187✔
2746
        readonly name?: Identifier,
187✔
2747
        readonly as?: Token,
187✔
2748
        readonly type?: Token,
187✔
2749
        readonly equal?: Token,
187✔
2750
        readonly initialValue?: Expression,
187✔
2751
        readonly optional?: Token
187✔
2752
    ) {
2753
        super();
187✔
2754
        this.range = util.createBoundingRange(
187✔
2755
            accessModifier,
2756
            name,
2757
            as,
2758
            type,
2759
            equal,
2760
            initialValue
2761
        );
2762
    }
2763

2764
    /**
2765
     * Derive a ValueKind from the type token, or the initial value.
2766
     * Defaults to `DynamicType`
2767
     */
2768
    getType() {
2769
        if (this.type) {
77✔
2770
            return util.tokenToBscType(this.type);
38✔
2771
        } else if (isLiteralExpression(this.initialValue)) {
39✔
2772
            return this.initialValue.type;
25✔
2773
        } else {
2774
            return new DynamicType();
14✔
2775
        }
2776
    }
2777

2778
    public readonly range: Range | undefined;
2779

2780
    public get isOptional() {
2781
        return !!this.optional;
17✔
2782
    }
2783

2784
    transpile(state: BrsTranspileState): TranspileResult {
2785
        throw new Error('transpile not implemented for ' + Object.getPrototypeOf(this).constructor.name);
×
2786
    }
2787

2788
    getTypedef(state: BrsTranspileState) {
2789
        const result = [] as TranspileResult;
10✔
2790
        if (this.name) {
10!
2791
            for (let annotation of this.annotations ?? []) {
10✔
2792
                result.push(
2✔
2793
                    ...annotation.getTypedef(state),
2794
                    state.newline,
2795
                    state.indent()
2796
                );
2797
            }
2798

2799
            let type = this.getType();
10✔
2800
            if (isInvalidType(type) || isVoidType(type)) {
10✔
2801
                type = new DynamicType();
1✔
2802
            }
2803

2804
            result.push(
10✔
2805
                this.accessModifier?.text ?? 'public',
60!
2806
                ' '
2807
            );
2808
            if (this.isOptional) {
10!
2809
                result.push(this.optional!.text, ' ');
×
2810
            }
2811
            result.push(this.name?.text,
10!
2812
                ' as ',
2813
                type.toTypeString()
2814
            );
2815
        }
2816
        return result;
10✔
2817
    }
2818

2819
    walk(visitor: WalkVisitor, options: WalkOptions) {
2820
        if (this.initialValue && options.walkMode & InternalWalkMode.walkExpressions) {
314✔
2821
            walk(this, 'initialValue', visitor, options);
91✔
2822
        }
2823
    }
2824

2825
    public clone() {
2826
        return this.finalizeClone(
4✔
2827
            new FieldStatement(
2828
                util.cloneToken(this.accessModifier),
2829
                util.cloneToken(this.name),
2830
                util.cloneToken(this.as),
2831
                util.cloneToken(this.type),
2832
                util.cloneToken(this.equal),
2833
                this.initialValue?.clone(),
12✔
2834
                util.cloneToken(this.optional)
2835
            ),
2836
            ['initialValue']
2837
        );
2838
    }
2839
}
2840

2841
/**
2842
 * @deprecated use `FieldStatement`
2843
 */
2844
export class ClassFieldStatement extends FieldStatement { }
1✔
2845

2846
export type MemberStatement = FieldStatement | MethodStatement;
2847

2848
/**
2849
 * @deprecated use `MemeberStatement`
2850
 */
2851
export type ClassMemberStatement = MemberStatement;
2852

2853
export class TryCatchStatement extends Statement {
1✔
2854
    constructor(
2855
        public tokens: {
31✔
2856
            try: Token;
2857
            endTry?: Token;
2858
        },
2859
        public tryBranch?: Block,
31✔
2860
        public catchStatement?: CatchStatement
31✔
2861
    ) {
2862
        super();
31✔
2863
        this.range = util.createBoundingRange(
31✔
2864
            tokens.try,
2865
            tryBranch,
2866
            catchStatement,
2867
            tokens.endTry
2868
        );
2869
    }
2870

2871
    public readonly range: Range | undefined;
2872

2873
    public transpile(state: BrsTranspileState): TranspileResult {
2874
        return [
3✔
2875
            state.transpileToken(this.tokens.try),
2876
            ...this.tryBranch!.transpile(state),
2877
            state.newline,
2878
            state.indent(),
2879
            ...(this.catchStatement?.transpile(state) ?? ['catch']),
18!
2880
            state.newline,
2881
            state.indent(),
2882
            state.transpileToken(this.tokens.endTry!)
2883
        ] as TranspileResult;
2884
    }
2885

2886
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2887
        if (this.tryBranch && options.walkMode & InternalWalkMode.walkStatements) {
47✔
2888
            walk(this, 'tryBranch', visitor, options);
46✔
2889
            walk(this, 'catchStatement', visitor, options);
46✔
2890
        }
2891
    }
2892

2893
    public clone() {
2894
        return this.finalizeClone(
3✔
2895
            new TryCatchStatement(
2896
                {
2897
                    try: util.cloneToken(this.tokens.try),
2898
                    endTry: util.cloneToken(this.tokens.endTry)
2899
                },
2900
                this.tryBranch?.clone(),
9✔
2901
                this.catchStatement?.clone()
9✔
2902
            ),
2903
            ['tryBranch', 'catchStatement']
2904
        );
2905
    }
2906
}
2907

2908
export class CatchStatement extends Statement {
1✔
2909
    constructor(
2910
        public tokens: {
28✔
2911
            catch: Token;
2912
        },
2913
        public exceptionVariable?: Identifier,
28✔
2914
        public catchBranch?: Block
28✔
2915
    ) {
2916
        super();
28✔
2917
        this.range = util.createBoundingRange(
28✔
2918
            tokens.catch,
2919
            exceptionVariable,
2920
            catchBranch
2921
        );
2922
    }
2923

2924
    public range: Range | undefined;
2925

2926
    public transpile(state: BrsTranspileState): TranspileResult {
2927
        return [
3✔
2928
            state.transpileToken(this.tokens.catch),
2929
            ' ',
2930
            this.exceptionVariable?.text ?? 'e',
18!
2931
            ...(this.catchBranch?.transpile(state) ?? [])
18!
2932
        ];
2933
    }
2934

2935
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2936
        if (this.catchBranch && options.walkMode & InternalWalkMode.walkStatements) {
43✔
2937
            walk(this, 'catchBranch', visitor, options);
42✔
2938
        }
2939
    }
2940

2941
    public clone() {
2942
        return this.finalizeClone(
2✔
2943
            new CatchStatement(
2944
                {
2945
                    catch: util.cloneToken(this.tokens.catch)
2946
                },
2947
                util.cloneToken(this.exceptionVariable),
2948
                this.catchBranch?.clone()
6✔
2949
            ),
2950
            ['catchBranch']
2951
        );
2952
    }
2953
}
2954

2955
export class ThrowStatement extends Statement {
1✔
2956
    constructor(
2957
        public throwToken: Token,
12✔
2958
        public expression?: Expression
12✔
2959
    ) {
2960
        super();
12✔
2961
        this.range = util.createBoundingRange(
12✔
2962
            throwToken,
2963
            expression
2964
        );
2965
    }
2966
    public range: Range | undefined;
2967

2968
    public transpile(state: BrsTranspileState) {
2969
        const result = [
4✔
2970
            state.transpileToken(this.throwToken),
2971
            ' '
2972
        ] as TranspileResult;
2973

2974
        //if we have an expression, transpile it
2975
        if (this.expression) {
4!
2976
            result.push(
4✔
2977
                ...this.expression.transpile(state)
2978
            );
2979

2980
            //no expression found. Rather than emit syntax errors, provide a generic error message
2981
        } else {
2982
            result.push('"An error has occurred"');
×
2983
        }
2984
        return result;
4✔
2985
    }
2986

2987
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2988
        if (this.expression && options.walkMode & InternalWalkMode.walkExpressions) {
27✔
2989
            walk(this, 'expression', visitor, options);
20✔
2990
        }
2991
    }
2992

2993
    public clone() {
2994
        return this.finalizeClone(
2✔
2995
            new ThrowStatement(
2996
                util.cloneToken(this.throwToken),
2997
                this.expression?.clone()
6✔
2998
            ),
2999
            ['expression']
3000
        );
3001
    }
3002
}
3003

3004

3005
export class EnumStatement extends Statement implements TypedefProvider {
1✔
3006

3007
    constructor(
3008
        public tokens: {
138✔
3009
            enum: Token;
3010
            name: Identifier;
3011
            endEnum: Token;
3012
        },
3013
        public body: Array<EnumMemberStatement | CommentStatement>
138✔
3014
    ) {
3015
        super();
138✔
3016
        this.body = this.body ?? [];
138✔
3017
    }
3018

3019
    public get range(): Range | undefined {
3020
        return util.createBoundingRange(
19✔
3021
            this.tokens.enum,
3022
            this.tokens.name,
3023
            ...this.body,
3024
            this.tokens.endEnum
3025
        );
3026
    }
3027

3028
    /**
3029
     * Get the name of the wrapping namespace (if it exists)
3030
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
3031
     */
3032
    public get namespaceName() {
3033
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
3034
    }
3035

3036
    public getMembers() {
3037
        const result = [] as EnumMemberStatement[];
190✔
3038
        for (const statement of this.body) {
190✔
3039
            if (isEnumMemberStatement(statement)) {
384✔
3040
                result.push(statement);
371✔
3041
            }
3042
        }
3043
        return result;
190✔
3044
    }
3045

3046
    /**
3047
     * Get a map of member names and their values.
3048
     * All values are stored as their AST LiteralExpression representation (i.e. string enum values include the wrapping quotes)
3049
     */
3050
    public getMemberValueMap() {
3051
        const result = new Map<string, string>();
87✔
3052
        const members = this.getMembers();
87✔
3053
        let currentIntValue = 0;
87✔
3054
        for (const member of members) {
87✔
3055
            //if there is no value, assume an integer and increment the int counter
3056
            if (!member.value) {
194✔
3057
                result.set(member.name?.toLowerCase(), currentIntValue.toString());
33!
3058
                currentIntValue++;
33✔
3059

3060
                //if explicit integer value, use it and increment the int counter
3061
            } else if (isLiteralExpression(member.value) && member.value.token.kind === TokenKind.IntegerLiteral) {
161✔
3062
                //try parsing as integer literal, then as hex integer literal.
3063
                let tokenIntValue = util.parseInt(member.value.token.text) ?? util.parseInt(member.value.token.text.replace(/&h/i, '0x'));
32✔
3064
                if (tokenIntValue !== undefined) {
32!
3065
                    currentIntValue = tokenIntValue;
32✔
3066
                    currentIntValue++;
32✔
3067
                }
3068
                result.set(member.name?.toLowerCase(), member.value.token.text);
32!
3069

3070
                //simple unary expressions (like `-1`)
3071
            } else if (isUnaryExpression(member.value) && isLiteralExpression(member.value.right)) {
129✔
3072
                result.set(member.name?.toLowerCase(), member.value.operator.text + member.value.right.token.text);
1!
3073

3074
                //all other values
3075
            } else {
3076
                result.set(member.name?.toLowerCase(), (member.value as LiteralExpression)?.token?.text ?? 'invalid');
128!
3077
            }
3078
        }
3079
        return result;
87✔
3080
    }
3081

3082
    public getMemberValue(name: string) {
3083
        return this.getMemberValueMap().get(name.toLowerCase());
84✔
3084
    }
3085

3086
    /**
3087
     * The name of the enum (without the namespace prefix)
3088
     */
3089
    public get name() {
3090
        return this.tokens.name?.text;
16!
3091
    }
3092

3093
    /**
3094
     * The name of the enum WITH its leading namespace (if applicable)
3095
     */
3096
    public get fullName() {
3097
        const name = this.tokens.name?.text;
399!
3098
        if (name) {
399!
3099
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
399✔
3100

3101
            if (namespace) {
399✔
3102
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
193✔
3103
                return `${namespaceName}.${name}`;
193✔
3104
            } else {
3105
                return name;
206✔
3106
            }
3107
        } else {
3108
            //return undefined which will allow outside callers to know that this doesn't have a name
3109
            return undefined;
×
3110
        }
3111
    }
3112

3113
    transpile(state: BrsTranspileState) {
3114
        //enum declarations do not exist at runtime, so don't transpile anything...
3115
        return [];
29✔
3116
    }
3117

3118
    getTypedef(state: BrsTranspileState) {
3119
        const result = [] as TranspileResult;
1✔
3120
        for (let annotation of this.annotations ?? []) {
1!
3121
            result.push(
×
3122
                ...annotation.getTypedef(state),
3123
                state.newline,
3124
                state.indent()
3125
            );
3126
        }
3127
        result.push(
1✔
3128
            this.tokens.enum.text ?? 'enum',
3!
3129
            ' ',
3130
            this.tokens.name.text
3131
        );
3132
        result.push(state.newline);
1✔
3133
        state.blockDepth++;
1✔
3134
        for (const member of this.body) {
1✔
3135
            if (isTypedefProvider(member)) {
1!
3136
                result.push(
1✔
3137
                    state.indent(),
3138
                    ...member.getTypedef(state),
3139
                    state.newline
3140
                );
3141
            }
3142
        }
3143
        state.blockDepth--;
1✔
3144
        result.push(
1✔
3145
            state.indent(),
3146
            this.tokens.endEnum.text ?? 'end enum'
3!
3147
        );
3148
        return result;
1✔
3149
    }
3150

3151
    walk(visitor: WalkVisitor, options: WalkOptions) {
3152
        if (options.walkMode & InternalWalkMode.walkStatements) {
556!
3153
            walkArray(this.body, visitor, options, this);
556✔
3154

3155
        }
3156
    }
3157

3158
    public clone() {
3159
        return this.finalizeClone(
6✔
3160
            new EnumStatement(
3161
                {
3162
                    enum: util.cloneToken(this.tokens.enum),
3163
                    name: util.cloneToken(this.tokens.name),
3164
                    endEnum: util.cloneToken(this.tokens.endEnum)
3165
                },
3166
                this.body?.map(x => x?.clone())
6✔
3167
            ),
3168
            ['body']
3169
        );
3170
    }
3171
}
3172

3173
export class EnumMemberStatement extends Statement implements TypedefProvider {
1✔
3174

3175
    public constructor(
3176
        public tokens: {
228✔
3177
            name: Identifier;
3178
            equal?: Token;
3179
        },
3180
        public value?: Expression
228✔
3181
    ) {
3182
        super();
228✔
3183
    }
3184

3185
    /**
3186
     * The name of the member
3187
     */
3188
    public get name() {
3189
        return this.tokens.name.text;
746✔
3190
    }
3191

3192
    public get range() {
3193
        return util.createBoundingRange(
69✔
3194
            this.tokens.name,
3195
            this.tokens.equal,
3196
            this.value
3197
        );
3198
    }
3199

3200
    /**
3201
     * Get the value of this enum. Requires that `.parent` is set
3202
     */
3203
    public getValue() {
3204
        return (this.parent as EnumStatement).getMemberValue(this.name);
84✔
3205
    }
3206

3207
    public transpile(state: BrsTranspileState): TranspileResult {
3208
        return [];
×
3209
    }
3210

3211
    getTypedef(state: BrsTranspileState): TranspileResult {
3212
        const result = [
1✔
3213
            this.tokens.name.text
3214
        ] as TranspileResult;
3215
        if (this.tokens.equal) {
1!
3216
            result.push(' ', this.tokens.equal.text, ' ');
×
3217
            if (this.value) {
×
3218
                result.push(
×
3219
                    ...this.value.transpile(state)
3220
                );
3221
            }
3222
        }
3223
        return result;
1✔
3224
    }
3225

3226
    walk(visitor: WalkVisitor, options: WalkOptions) {
3227
        if (this.value && options.walkMode & InternalWalkMode.walkExpressions) {
723✔
3228
            walk(this, 'value', visitor, options);
483✔
3229
        }
3230
    }
3231

3232
    public clone() {
3233
        return this.finalizeClone(
3✔
3234
            new EnumMemberStatement(
3235
                {
3236
                    name: util.cloneToken(this.tokens.name),
3237
                    equal: util.cloneToken(this.tokens.equal)
3238
                },
3239
                this.value?.clone()
9✔
3240
            ),
3241
            ['value']
3242
        );
3243
    }
3244
}
3245

3246
export class ConstStatement extends Statement implements TypedefProvider {
1✔
3247

3248
    public constructor(
3249
        public tokens: {
115✔
3250
            const: Token;
3251
            name: Identifier;
3252
            equals: Token;
3253
        },
3254
        public value: Expression
115✔
3255
    ) {
3256
        super();
115✔
3257
        this.range = util.createBoundingRange(this.tokens.const, this.tokens.name, this.tokens.equals, this.value);
115✔
3258
    }
3259

3260
    public range: Range | undefined;
3261

3262
    public get name() {
3263
        return this.tokens.name.text;
5✔
3264
    }
3265

3266
    /**
3267
     * The name of the statement WITH its leading namespace (if applicable)
3268
     */
3269
    public get fullName() {
3270
        const name = this.tokens.name?.text;
258!
3271
        if (name) {
258!
3272
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
258✔
3273
            if (namespace) {
258✔
3274
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
184✔
3275
                return `${namespaceName}.${name}`;
184✔
3276
            } else {
3277
                return name;
74✔
3278
            }
3279
        } else {
3280
            //return undefined which will allow outside callers to know that this doesn't have a name
3281
            return undefined;
×
3282
        }
3283
    }
3284

3285
    public transpile(state: BrsTranspileState): TranspileResult {
3286
        //const declarations don't exist at runtime, so just transpile empty
3287
        return [];
37✔
3288
    }
3289

3290
    getTypedef(state: BrsTranspileState): TranspileResult {
3291
        return [
3✔
3292
            state.tokenToSourceNode(this.tokens.const),
3293
            ' ',
3294
            state.tokenToSourceNode(this.tokens.name),
3295
            ' ',
3296
            state.tokenToSourceNode(this.tokens.equals),
3297
            ' ',
3298
            ...this.value.transpile(state)
3299
        ];
3300
    }
3301

3302
    walk(visitor: WalkVisitor, options: WalkOptions) {
3303
        if (this.value && options.walkMode & InternalWalkMode.walkExpressions) {
358✔
3304
            walk(this, 'value', visitor, options);
356✔
3305
        }
3306
    }
3307

3308
    public clone() {
3309
        return this.finalizeClone(
3✔
3310
            new ConstStatement(
3311
                {
3312
                    const: util.cloneToken(this.tokens.const),
3313
                    name: util.cloneToken(this.tokens.name),
3314
                    equals: util.cloneToken(this.tokens.equals)
3315
                },
3316
                this.value?.clone()
9✔
3317
            ),
3318
            ['value']
3319
        );
3320
    }
3321
}
3322

3323
export class ContinueStatement extends Statement {
1✔
3324
    constructor(
3325
        public tokens: {
13✔
3326
            continue: Token;
3327
            loopType: Token;
3328
        }
3329
    ) {
3330
        super();
13✔
3331
        this.range = util.createBoundingRange(
13✔
3332
            tokens.continue,
3333
            tokens.loopType
3334
        );
3335
    }
3336

3337
    public range: Range | undefined;
3338

3339
    transpile(state: BrsTranspileState) {
3340
        return [
3✔
3341
            state.sourceNode(this.tokens.continue, this.tokens.continue?.text ?? 'continue'),
18!
3342
            this.tokens.loopType?.leadingWhitespace ?? ' ',
18✔
3343
            state.sourceNode(this.tokens.continue, this.tokens.loopType?.text)
9✔
3344
        ];
3345
    }
3346

3347
    walk(visitor: WalkVisitor, options: WalkOptions) {
3348
        //nothing to walk
3349
    }
3350

3351
    public clone() {
3352
        return this.finalizeClone(
1✔
3353
            new ContinueStatement({
3354
                continue: util.cloneToken(this.tokens.continue),
3355
                loopType: util.cloneToken(this.tokens.loopType)
3356
            })
3357
        );
3358
    }
3359
}
3360

3361
export class TypecastStatement extends Statement {
1✔
3362
    constructor(options: {
3363
        typecast?: Token;
3364
        obj: Token;
3365
        as?: Token;
3366
        type: Token;
3367
    }
3368
    ) {
3369
        super();
5✔
3370
        this.tokens = {
5✔
3371
            typecast: options.typecast,
3372
            obj: options.obj,
3373
            as: options.as,
3374
            type: options.type
3375
        };
3376
        this.range = util.createBoundingRange(
5✔
3377
            this.tokens.typecast,
3378
            this.tokens.obj,
3379
            this.tokens.as,
3380
            this.tokens.type
3381
        );
3382
    }
3383

3384
    public readonly tokens: {
3385
        readonly typecast?: Token;
3386
        readonly obj: Token;
3387
        readonly as?: Token;
3388
        readonly type: Token;
3389
    };
3390

3391
    public readonly typecastExpression: Expression;
3392

3393
    public readonly range: Range;
3394

3395
    transpile(state: BrsTranspileState) {
3396
        return [];
1✔
3397
    }
3398

3399
    walk(visitor: WalkVisitor, options: WalkOptions) {
3400
        //nothing to walk
3401
    }
3402

3403
    public clone() {
3404
        return this.finalizeClone(
×
3405
            new TypecastStatement({
3406
                typecast: util.cloneToken(this.tokens.typecast),
3407
                obj: util.cloneToken(this.tokens.obj),
3408
                as: util.cloneToken(this.tokens.as),
3409
                type: util.cloneToken(this.tokens.type)
3410
            })
3411
        );
3412
    }
3413
}
3414

3415
export class AliasStatement extends Statement {
1✔
3416
    constructor(options: {
3417
        alias?: Token;
3418
        name: Token;
3419
        equals?: Token;
3420
        value: Token;
3421
    }
3422
    ) {
3423
        super();
2✔
3424
        this.tokens = {
2✔
3425
            alias: options.alias,
3426
            name: options.name,
3427
            equals: options.equals,
3428
            value: options.value
3429
        };
3430
        this.range = util.createBoundingRange(
2✔
3431
            this.tokens.alias,
3432
            this.tokens.name,
3433
            this.tokens.equals,
3434
            this.tokens.value
3435
        );
3436
    }
3437

3438
    public readonly tokens: {
3439
        readonly alias?: Token;
3440
        readonly name: Token;
3441
        readonly equals?: Token;
3442
        readonly value: Token;
3443
    };
3444

3445
    public readonly range: Range;
3446

3447
    transpile(state: BrsTranspileState) {
3448
        return [];
×
3449
    }
3450

3451
    walk(visitor: WalkVisitor, options: WalkOptions) {
3452
        //nothing to walk
3453
    }
3454

3455

3456
    public clone() {
3457
        return this.finalizeClone(
×
3458
            new AliasStatement({
3459
                alias: util.cloneToken(this.tokens.alias),
3460
                name: util.cloneToken(this.tokens.name),
3461
                equals: util.cloneToken(this.tokens.equals),
3462
                value: util.cloneToken(this.tokens.value)
3463
            })
3464
        );
3465
    }
3466
}
3467

3468
export class TypeStatement extends Statement {
1✔
3469
    constructor(options: {
3470
        type?: Token;
3471
        name: Token;
3472
        equals?: Token;
3473
        value: Token;
3474
    }
3475
    ) {
3476
        super();
11✔
3477
        this.tokens = {
11✔
3478
            type: options.type,
3479
            name: options.name,
3480
            equals: options.equals,
3481
            value: options.value
3482
        };
3483
        this.range = util.createBoundingRange(
11✔
3484
            this.tokens.type,
3485
            this.tokens.name,
3486
            this.tokens.equals,
3487
            this.tokens.value
3488
        );
3489
    }
3490

3491
    public readonly tokens: {
3492
        readonly type?: Token;
3493
        readonly name: Token;
3494
        readonly equals?: Token;
3495
        readonly value: Token;
3496
    };
3497

3498
    public readonly range: Range;
3499

3500
    transpile(state: BrsTranspileState) {
3501
        return [];
2✔
3502
    }
3503

3504
    walk(visitor: WalkVisitor, options: WalkOptions) {
3505
        //nothing to walk
3506
    }
3507

3508
    public get fullName() {
3509
        const name = this.tokens.name?.text;
5!
3510
        if (name) {
5!
3511
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
5✔
3512
            if (namespace) {
5✔
3513
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
3✔
3514
                return `${namespaceName}.${name}`;
3✔
3515
            } else {
3516
                return name;
2✔
3517
            }
3518
        } else {
3519
            //return undefined which will allow outside callers to know that this doesn't have a name
3520
            return undefined;
×
3521
        }
3522
    }
3523

3524
    public clone() {
3525
        return this.finalizeClone(
×
3526
            new TypeStatement({
3527
                type: util.cloneToken(this.tokens.type),
3528
                name: util.cloneToken(this.tokens.name),
3529
                equals: util.cloneToken(this.tokens.equals),
3530
                value: util.cloneToken(this.tokens.value)
3531
            })
3532
        );
3533
    }
3534
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc