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

rokucommunity / brighterscript / #12837

24 Jul 2024 05:52PM UTC coverage: 87.936% (+2.3%) from 85.65%
#12837

push

TwitchBronBron
0.67.4

6069 of 7376 branches covered (82.28%)

Branch coverage included in aggregate %.

8793 of 9525 relevant lines covered (92.31%)

1741.63 hits per line

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

89.71
/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, FunctionExpression, FunctionParameterExpression, LiteralExpression } from './Expression';
5
import { CallExpression, VariableExpression } from './Expression';
1✔
6
import { util } from '../util';
1✔
7
import type { Range } from 'vscode-languageserver';
8
import type { BrsTranspileState } from './BrsTranspileState';
9
import { ParseMode } from './Parser';
1✔
10
import type { WalkVisitor, WalkOptions } from '../astUtils/visitors';
11
import { InternalWalkMode, walk, createVisitor, WalkMode, walkArray } from '../astUtils/visitors';
1✔
12
import { isCallExpression, isCommentStatement, isEnumMemberStatement, isExpression, isExpressionStatement, isFieldStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isMethodStatement, isNamespaceStatement, isTypedefProvider, isUnaryExpression, isVoidType } from '../astUtils/reflection';
1✔
13
import type { TranspileResult, TypedefProvider } from '../interfaces';
14
import { createInvalidLiteral, createMethodStatement, createToken } from '../astUtils/creators';
1✔
15
import { DynamicType } from '../types/DynamicType';
1✔
16
import type { BscType } from '../types/BscType';
17
import type { TranspileState } from './TranspileState';
18
import { SymbolTable } from '../SymbolTable';
1✔
19
import type { Expression } from './AstNode';
20
import { Statement } from './AstNode';
1✔
21

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

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

40
/**
41
 * This is a top-level statement. Consider this the root of the AST
42
 */
43
export class Body extends Statement implements TypedefProvider {
1✔
44
    constructor(
45
        public statements: Statement[] = []
3,879✔
46
    ) {
47
        super();
3,879✔
48
    }
49

50
    public symbolTable = new SymbolTable('Body', () => this.parent?.getSymbolTable());
3,879✔
51

52
    public get range() {
53
        //this needs to be a getter because the body has its statements pushed to it after being constructed
54
        return util.createBoundingRange(
17✔
55
            ...(this.statements ?? [])
51!
56
        );
57
    }
58

59
    transpile(state: BrsTranspileState) {
60
        let result = [] as TranspileResult;
281✔
61
        for (let i = 0; i < this.statements.length; i++) {
281✔
62
            let statement = this.statements[i];
400✔
63
            let previousStatement = this.statements[i - 1];
400✔
64
            let nextStatement = this.statements[i + 1];
400✔
65

66
            if (!previousStatement) {
400✔
67
                //this is the first statement. do nothing related to spacing and newlines
68

69
                //if comment is on same line as prior sibling
70
            } else if (isCommentStatement(statement) && previousStatement && statement.range?.start.line === previousStatement.range?.end.line) {
121✔
71
                result.push(
4✔
72
                    ' '
73
                );
74

75
                //add double newline if this is a comment, and next is a function
76
            } else if (isCommentStatement(statement) && nextStatement && isFunctionStatement(nextStatement)) {
117✔
77
                result.push(state.newline, state.newline);
1✔
78

79
                //add double newline if is function not preceeded by a comment
80
            } else if (isFunctionStatement(statement) && previousStatement && !(isCommentStatement(previousStatement))) {
116✔
81
                result.push(state.newline, state.newline);
61✔
82
            } else {
83
                //separate statements by a single newline
84
                result.push(state.newline);
55✔
85
            }
86

87
            result.push(...statement.transpile(state));
400✔
88
        }
89
        return result;
281✔
90
    }
91

92
    getTypedef(state: BrsTranspileState): TranspileResult {
93
        let result = [] as TranspileResult;
29✔
94
        for (const statement of this.statements) {
29✔
95
            //if the current statement supports generating typedef, call it
96
            if (isTypedefProvider(statement)) {
41!
97
                result.push(
41✔
98
                    state.indent(),
99
                    ...statement.getTypedef(state),
100
                    state.newline
101
                );
102
            }
103
        }
104
        return result;
29✔
105
    }
106

107
    walk(visitor: WalkVisitor, options: WalkOptions) {
108
        if (options.walkMode & InternalWalkMode.walkStatements) {
3,258!
109
            walkArray(this.statements, visitor, options, this);
3,258✔
110
        }
111
    }
112
}
113

114
export class AssignmentStatement extends Statement {
1✔
115
    constructor(
116
        readonly equals: Token,
874✔
117
        readonly name: Identifier,
874✔
118
        readonly value: Expression
874✔
119
    ) {
120
        super();
874✔
121
        this.range = util.createBoundingRange(name, equals, value);
874✔
122
    }
123

124
    public readonly range: Range | undefined;
125

126
    /**
127
     * Get the name of the wrapping namespace (if it exists)
128
     * @deprecated use `.findAncestor(isFunctionExpression)` instead.
129
     */
130
    public get containingFunction() {
131
        return this.findAncestor<FunctionExpression>(isFunctionExpression);
417✔
132
    }
133

134
    transpile(state: BrsTranspileState) {
135
        //if the value is a compound assignment, just transpile the expression itself
136
        if (CompoundAssignmentOperators.includes((this.value as BinaryExpression)?.operator?.kind)) {
239!
137
            return this.value.transpile(state);
21✔
138
        } else {
139
            return [
218✔
140
                state.transpileToken(this.name),
141
                ' ',
142
                state.transpileToken(this.equals),
143
                ' ',
144
                ...this.value.transpile(state)
145
            ];
146
        }
147
    }
148

149
    walk(visitor: WalkVisitor, options: WalkOptions) {
150
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,786✔
151
            walk(this, 'value', visitor, options);
1,365✔
152
        }
153
    }
154
}
155

156
export class Block extends Statement {
1✔
157
    constructor(
158
        readonly statements: Statement[],
1,883✔
159
        readonly startingRange?: Range
1,883✔
160
    ) {
161
        super();
1,883✔
162
        this.range = util.createBoundingRange(
1,883✔
163
            { range: this.startingRange },
164
            ...(statements ?? [])
5,649!
165
        );
166
    }
167

168
    public readonly range: Range | undefined;
169

170
    transpile(state: BrsTranspileState) {
171
        state.blockDepth++;
407✔
172
        let results = [] as TranspileResult;
407✔
173
        for (let i = 0; i < this.statements.length; i++) {
407✔
174
            let previousStatement = this.statements[i - 1];
660✔
175
            let statement = this.statements[i];
660✔
176

177
            //if comment is on same line as parent
178
            if (isCommentStatement(statement) &&
660✔
179
                (util.linesTouch(state.lineage[0], statement) || util.linesTouch(previousStatement, statement))
180
            ) {
181
                results.push(' ');
65✔
182

183
                //is not a comment
184
            } else {
185
                //add a newline and indent
186
                results.push(
595✔
187
                    state.newline,
188
                    state.indent()
189
                );
190
            }
191

192
            //push block onto parent list
193
            state.lineage.unshift(this);
660✔
194
            results.push(
660✔
195
                ...statement.transpile(state)
196
            );
197
            state.lineage.shift();
660✔
198
        }
199
        state.blockDepth--;
407✔
200
        return results;
407✔
201
    }
202

203
    walk(visitor: WalkVisitor, options: WalkOptions) {
204
        if (options.walkMode & InternalWalkMode.walkStatements) {
4,338✔
205
            walkArray(this.statements, visitor, options, this);
4,332✔
206
        }
207
    }
208
}
209

210
export class ExpressionStatement extends Statement {
1✔
211
    constructor(
212
        readonly expression: Expression
247✔
213
    ) {
214
        super();
247✔
215
        this.range = this.expression.range;
247✔
216
    }
217

218
    public readonly range: Range | undefined;
219

220
    transpile(state: BrsTranspileState) {
221
        return this.expression.transpile(state);
47✔
222
    }
223

224
    walk(visitor: WalkVisitor, options: WalkOptions) {
225
        if (options.walkMode & InternalWalkMode.walkExpressions) {
750✔
226
            walk(this, 'expression', visitor, options);
561✔
227
        }
228
    }
229
}
230

231
export class CommentStatement extends Statement implements Expression, TypedefProvider {
1✔
232
    constructor(
233
        public comments: Token[]
247✔
234
    ) {
235
        super();
247✔
236
        this.visitMode = InternalWalkMode.visitStatements | InternalWalkMode.visitExpressions;
247✔
237
        if (this.comments?.length > 0) {
247!
238
            this.range = util.createBoundingRange(
246✔
239
                ...this.comments
240
            );
241
        }
242
    }
243

244
    public range: Range | undefined;
245

246
    get text() {
247
        return this.comments.map(x => x.text).join('\n');
39✔
248
    }
249

250
    transpile(state: BrsTranspileState) {
251
        let result = [] as TranspileResult;
105✔
252
        for (let i = 0; i < this.comments.length; i++) {
105✔
253
            let comment = this.comments[i];
106✔
254
            if (i > 0) {
106✔
255
                result.push(state.indent());
1✔
256
            }
257
            result.push(
106✔
258
                state.transpileToken(comment)
259
            );
260
            //add newline for all except final comment
261
            if (i < this.comments.length - 1) {
106✔
262
                result.push(state.newline);
1✔
263
            }
264
        }
265
        return result;
105✔
266
    }
267

268
    public getTypedef(state: TranspileState) {
269
        return this.transpile(state as BrsTranspileState);
×
270
    }
271

272
    walk(visitor: WalkVisitor, options: WalkOptions) {
273
        //nothing to walk
274
    }
275
}
276

277
export class ExitForStatement extends Statement {
1✔
278
    constructor(
279
        readonly tokens: {
4✔
280
            exitFor: Token;
281
        }
282
    ) {
283
        super();
4✔
284
        this.range = this.tokens.exitFor.range;
4✔
285
    }
286

287
    public readonly range: Range;
288

289
    transpile(state: BrsTranspileState) {
290
        return [
2✔
291
            state.transpileToken(this.tokens.exitFor)
292
        ];
293
    }
294

295
    walk(visitor: WalkVisitor, options: WalkOptions) {
296
        //nothing to walk
297
    }
298

299
}
300

301
export class ExitWhileStatement extends Statement {
1✔
302
    constructor(
303
        readonly tokens: {
7✔
304
            exitWhile: Token;
305
        }
306
    ) {
307
        super();
7✔
308
        this.range = this.tokens.exitWhile.range;
7✔
309
    }
310

311
    public readonly range: Range;
312

313
    transpile(state: BrsTranspileState) {
314
        return [
3✔
315
            state.transpileToken(this.tokens.exitWhile)
316
        ];
317
    }
318

319
    walk(visitor: WalkVisitor, options: WalkOptions) {
320
        //nothing to walk
321
    }
322
}
323

324
export class FunctionStatement extends Statement implements TypedefProvider {
1✔
325
    constructor(
326
        public name: Identifier,
1,693✔
327
        public func: FunctionExpression
1,693✔
328
    ) {
329
        super();
1,693✔
330
        this.range = this.func.range;
1,693✔
331
    }
332

333
    public readonly range: Range | undefined;
334

335
    /**
336
     * Get the name of this expression based on the parse mode
337
     */
338
    public getName(parseMode: ParseMode) {
339
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
1,863✔
340
        if (namespace) {
1,863✔
341
            let delimiter = parseMode === ParseMode.BrighterScript ? '.' : '_';
272✔
342
            let namespaceName = namespace.getName(parseMode);
272✔
343
            return namespaceName + delimiter + this.name?.text;
272✔
344
        } else {
345
            return this.name.text;
1,591✔
346
        }
347
    }
348

349
    /**
350
     * Get the name of the wrapping namespace (if it exists)
351
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
352
     */
353
    public get namespaceName() {
354
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
355
    }
356

357
    transpile(state: BrsTranspileState) {
358
        //create a fake token using the full transpiled name
359
        let nameToken = {
264✔
360
            ...this.name,
361
            text: this.getName(ParseMode.BrightScript)
362
        };
363

364
        return this.func.transpile(state, nameToken);
264✔
365
    }
366

367
    getTypedef(state: BrsTranspileState) {
368
        let result = [] as TranspileResult;
6✔
369
        for (let annotation of this.annotations ?? []) {
6✔
370
            result.push(
2✔
371
                ...annotation.getTypedef(state),
372
                state.newline,
373
                state.indent()
374
            );
375
        }
376

377
        result.push(
6✔
378
            ...this.func.getTypedef(state)
379
        );
380
        return result;
6✔
381
    }
382

383
    walk(visitor: WalkVisitor, options: WalkOptions) {
384
        if (options.walkMode & InternalWalkMode.walkExpressions) {
2,084✔
385
            walk(this, 'func', visitor, options);
2,057✔
386
        }
387
    }
388
}
389

390
export class IfStatement extends Statement {
1✔
391
    constructor(
392
        readonly tokens: {
186✔
393
            if: Token;
394
            then?: Token;
395
            else?: Token;
396
            endIf?: Token;
397
        },
398
        readonly condition: Expression,
186✔
399
        readonly thenBranch: Block,
186✔
400
        readonly elseBranch?: IfStatement | Block,
186✔
401
        readonly isInline?: boolean
186✔
402
    ) {
403
        super();
186✔
404
        this.range = util.createBoundingRange(
186✔
405
            tokens.if,
406
            condition,
407
            tokens.then,
408
            thenBranch,
409
            tokens.else,
410
            elseBranch,
411
            tokens.endIf
412
        );
413
    }
414
    public readonly range: Range | undefined;
415

416
    transpile(state: BrsTranspileState) {
417
        let results = [] as TranspileResult;
48✔
418
        //if   (already indented by block)
419
        results.push(state.transpileToken(this.tokens.if));
48✔
420
        results.push(' ');
48✔
421
        //conditions
422
        results.push(...this.condition.transpile(state));
48✔
423
        //then
424
        if (this.tokens.then) {
48✔
425
            results.push(' ');
37✔
426
            results.push(
37✔
427
                state.transpileToken(this.tokens.then)
428
            );
429
        }
430
        state.lineage.unshift(this);
48✔
431

432
        //if statement body
433
        let thenNodes = this.thenBranch.transpile(state);
48✔
434
        state.lineage.shift();
48✔
435
        if (thenNodes.length > 0) {
48✔
436
            results.push(thenNodes);
37✔
437
        }
438
        results.push('\n');
48✔
439

440
        //else branch
441
        if (this.tokens.else) {
48✔
442
            //else
443
            results.push(
29✔
444
                state.indent(),
445
                state.transpileToken(this.tokens.else)
446
            );
447
        }
448

449
        if (this.elseBranch) {
48✔
450
            if (isIfStatement(this.elseBranch)) {
29✔
451
                //chained elseif
452
                state.lineage.unshift(this.elseBranch);
19✔
453
                let body = this.elseBranch.transpile(state);
19✔
454
                state.lineage.shift();
19✔
455

456
                if (body.length > 0) {
19!
457
                    //zero or more spaces between the `else` and the `if`
458
                    results.push(this.elseBranch.tokens.if.leadingWhitespace!);
19✔
459
                    results.push(...body);
19✔
460

461
                    // stop here because chained if will transpile the rest
462
                    return results;
19✔
463
                } else {
464
                    results.push('\n');
×
465
                }
466

467
            } else {
468
                //else body
469
                state.lineage.unshift(this.tokens.else!);
10✔
470
                let body = this.elseBranch.transpile(state);
10✔
471
                state.lineage.shift();
10✔
472

473
                if (body.length > 0) {
10✔
474
                    results.push(...body);
8✔
475
                }
476
                results.push('\n');
10✔
477
            }
478
        }
479

480
        //end if
481
        results.push(state.indent());
29✔
482
        if (this.tokens.endIf) {
29!
483
            results.push(
29✔
484
                state.transpileToken(this.tokens.endIf)
485
            );
486
        } else {
487
            results.push('end if');
×
488
        }
489
        return results;
29✔
490
    }
491

492
    walk(visitor: WalkVisitor, options: WalkOptions) {
493
        if (options.walkMode & InternalWalkMode.walkExpressions) {
372✔
494
            walk(this, 'condition', visitor, options);
274✔
495
        }
496
        if (options.walkMode & InternalWalkMode.walkStatements) {
372✔
497
            walk(this, 'thenBranch', visitor, options);
370✔
498
        }
499
        if (this.elseBranch && options.walkMode & InternalWalkMode.walkStatements) {
372✔
500
            walk(this, 'elseBranch', visitor, options);
196✔
501
        }
502
    }
503
}
504

505
export class IncrementStatement extends Statement {
1✔
506
    constructor(
507
        readonly value: Expression,
17✔
508
        readonly operator: Token
17✔
509
    ) {
510
        super();
17✔
511
        this.range = util.createBoundingRange(
17✔
512
            value,
513
            operator
514
        );
515
    }
516

517
    public readonly range: Range | undefined;
518

519
    transpile(state: BrsTranspileState) {
520
        return [
8✔
521
            ...this.value.transpile(state),
522
            state.transpileToken(this.operator)
523
        ];
524
    }
525

526
    walk(visitor: WalkVisitor, options: WalkOptions) {
527
        if (options.walkMode & InternalWalkMode.walkExpressions) {
32✔
528
            walk(this, 'value', visitor, options);
25✔
529
        }
530
    }
531
}
532

533
/** Used to indent the current `print` position to the next 16-character-width output zone. */
534
export interface PrintSeparatorTab extends Token {
535
    kind: TokenKind.Comma;
536
}
537

538
/** Used to insert a single whitespace character at the current `print` position. */
539
export interface PrintSeparatorSpace extends Token {
540
    kind: TokenKind.Semicolon;
541
}
542

543
/**
544
 * Represents a `print` statement within BrightScript.
545
 */
546
export class PrintStatement extends Statement {
1✔
547
    /**
548
     * Creates a new internal representation of a BrightScript `print` statement.
549
     * @param tokens the tokens for this statement
550
     * @param tokens.print a print token
551
     * @param expressions an array of expressions or `PrintSeparator`s to be evaluated and printed.
552
     */
553
    constructor(
554
        readonly tokens: {
577✔
555
            print: Token;
556
        },
557
        readonly expressions: Array<Expression | PrintSeparatorTab | PrintSeparatorSpace>
577✔
558
    ) {
559
        super();
577✔
560
        this.range = util.createBoundingRange(
577✔
561
            tokens.print,
562
            ...(expressions ?? [])
1,731!
563
        );
564
    }
565

566
    public readonly range: Range | undefined;
567

568
    transpile(state: BrsTranspileState) {
569
        let result = [
192✔
570
            state.transpileToken(this.tokens.print),
571
            ' '
572
        ] as TranspileResult;
573
        for (let i = 0; i < this.expressions.length; i++) {
192✔
574
            const expressionOrSeparator: any = this.expressions[i];
256✔
575
            if (expressionOrSeparator.transpile) {
256✔
576
                result.push(...(expressionOrSeparator as ExpressionStatement).transpile(state));
230✔
577
            } else {
578
                result.push(
26✔
579
                    state.tokenToSourceNode(expressionOrSeparator)
580
                );
581
            }
582
            //if there's an expression after us, add a space
583
            if ((this.expressions[i + 1] as any)?.transpile) {
256✔
584
                result.push(' ');
38✔
585
            }
586
        }
587
        return result;
192✔
588
    }
589

590
    walk(visitor: WalkVisitor, options: WalkOptions) {
591
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,299✔
592
            //sometimes we have semicolon Tokens in the expressions list (should probably fix that...), so only walk the actual expressions
593
            walkArray(this.expressions, visitor, options, this, (item) => isExpression(item as any));
1,090✔
594
        }
595
    }
596
}
597

598
export class DimStatement extends Statement {
1✔
599
    constructor(
600
        public dimToken: Token,
40✔
601
        public identifier?: Identifier,
40✔
602
        public openingSquare?: Token,
40✔
603
        public dimensions?: Expression[],
40✔
604
        public closingSquare?: Token
40✔
605
    ) {
606
        super();
40✔
607
        this.range = util.createBoundingRange(
40✔
608
            dimToken,
609
            identifier,
610
            openingSquare,
611
            ...(dimensions ?? []),
120!
612
            closingSquare
613
        );
614
    }
615
    public range: Range | undefined;
616

617
    public transpile(state: BrsTranspileState) {
618
        let result = [
15✔
619
            state.transpileToken(this.dimToken),
620
            ' ',
621
            state.transpileToken(this.identifier!),
622
            state.transpileToken(this.openingSquare!)
623
        ] as TranspileResult;
624
        for (let i = 0; i < this.dimensions!.length; i++) {
15✔
625
            if (i > 0) {
32✔
626
                result.push(', ');
17✔
627
            }
628
            result.push(
32✔
629
                ...this.dimensions![i].transpile(state)
630
            );
631
        }
632
        result.push(state.transpileToken(this.closingSquare!));
15✔
633
        return result;
15✔
634
    }
635

636
    public walk(visitor: WalkVisitor, options: WalkOptions) {
637
        if (this.dimensions?.length !== undefined && this.dimensions?.length > 0 && options.walkMode & InternalWalkMode.walkExpressions) {
78!
638
            walkArray(this.dimensions, visitor, options, this);
52✔
639

640
        }
641
    }
642
}
643

644
export class GotoStatement extends Statement {
1✔
645
    constructor(
646
        readonly tokens: {
10✔
647
            goto: Token;
648
            label: Token;
649
        }
650
    ) {
651
        super();
10✔
652
        this.range = util.createBoundingRange(
10✔
653
            tokens.goto,
654
            tokens.label
655
        );
656
    }
657

658
    public readonly range: Range | undefined;
659

660
    transpile(state: BrsTranspileState) {
661
        return [
2✔
662
            state.transpileToken(this.tokens.goto),
663
            ' ',
664
            state.transpileToken(this.tokens.label)
665
        ];
666
    }
667

668
    walk(visitor: WalkVisitor, options: WalkOptions) {
669
        //nothing to walk
670
    }
671
}
672

673
export class LabelStatement extends Statement {
1✔
674
    constructor(
675
        readonly tokens: {
10✔
676
            identifier: Token;
677
            colon: Token;
678
        }
679
    ) {
680
        super();
10✔
681
        this.range = util.createBoundingRange(
10✔
682
            tokens.identifier,
683
            tokens.colon
684
        );
685
    }
686

687
    public readonly range: Range | undefined;
688

689
    transpile(state: BrsTranspileState) {
690
        return [
2✔
691
            state.transpileToken(this.tokens.identifier),
692
            state.transpileToken(this.tokens.colon)
693

694
        ];
695
    }
696

697
    walk(visitor: WalkVisitor, options: WalkOptions) {
698
        //nothing to walk
699
    }
700
}
701

702
export class ReturnStatement extends Statement {
1✔
703
    constructor(
704
        readonly tokens: {
150✔
705
            return: Token;
706
        },
707
        readonly value?: Expression
150✔
708
    ) {
709
        super();
150✔
710
        this.range = util.createBoundingRange(
150✔
711
            tokens.return,
712
            value
713
        );
714
    }
715

716
    public readonly range: Range | undefined;
717

718
    transpile(state: BrsTranspileState) {
719
        let result = [] as TranspileResult;
12✔
720
        result.push(
12✔
721
            state.transpileToken(this.tokens.return)
722
        );
723
        if (this.value) {
12!
724
            result.push(' ');
12✔
725
            result.push(...this.value.transpile(state));
12✔
726
        }
727
        return result;
12✔
728
    }
729

730
    walk(visitor: WalkVisitor, options: WalkOptions) {
731
        if (options.walkMode & InternalWalkMode.walkExpressions) {
318✔
732
            walk(this, 'value', visitor, options);
258✔
733
        }
734
    }
735
}
736

737
export class EndStatement extends Statement {
1✔
738
    constructor(
739
        readonly tokens: {
8✔
740
            end: Token;
741
        }
742
    ) {
743
        super();
8✔
744
        this.range = tokens.end.range;
8✔
745
    }
746

747
    public readonly range: Range;
748

749
    transpile(state: BrsTranspileState) {
750
        return [
2✔
751
            state.transpileToken(this.tokens.end)
752
        ];
753
    }
754

755
    walk(visitor: WalkVisitor, options: WalkOptions) {
756
        //nothing to walk
757
    }
758
}
759

760
export class StopStatement extends Statement {
1✔
761
    constructor(
762
        readonly tokens: {
16✔
763
            stop: Token;
764
        }
765
    ) {
766
        super();
16✔
767
        this.range = tokens?.stop?.range;
16!
768
    }
769

770
    public readonly range: Range;
771

772
    transpile(state: BrsTranspileState) {
773
        return [
2✔
774
            state.transpileToken(this.tokens.stop)
775
        ];
776
    }
777

778
    walk(visitor: WalkVisitor, options: WalkOptions) {
779
        //nothing to walk
780
    }
781
}
782

783
export class ForStatement extends Statement {
1✔
784
    constructor(
785
        public forToken: Token,
29✔
786
        public counterDeclaration: AssignmentStatement,
29✔
787
        public toToken: Token,
29✔
788
        public finalValue: Expression,
29✔
789
        public body: Block,
29✔
790
        public endForToken: Token,
29✔
791
        public stepToken?: Token,
29✔
792
        public increment?: Expression
29✔
793
    ) {
794
        super();
29✔
795
        this.range = util.createBoundingRange(
29✔
796
            forToken,
797
            counterDeclaration,
798
            toToken,
799
            finalValue,
800
            stepToken,
801
            increment,
802
            body,
803
            endForToken
804
        );
805
    }
806

807
    public readonly range: Range | undefined;
808

809
    transpile(state: BrsTranspileState) {
810
        let result = [] as TranspileResult;
9✔
811
        //for
812
        result.push(
9✔
813
            state.transpileToken(this.forToken),
814
            ' '
815
        );
816
        //i=1
817
        result.push(
9✔
818
            ...this.counterDeclaration.transpile(state),
819
            ' '
820
        );
821
        //to
822
        result.push(
9✔
823
            state.transpileToken(this.toToken),
824
            ' '
825
        );
826
        //final value
827
        result.push(this.finalValue.transpile(state));
9✔
828
        //step
829
        if (this.stepToken) {
9✔
830
            result.push(
4✔
831
                ' ',
832
                state.transpileToken(this.stepToken),
833
                ' ',
834
                this.increment!.transpile(state)
835
            );
836
        }
837
        //loop body
838
        state.lineage.unshift(this);
9✔
839
        result.push(...this.body.transpile(state));
9✔
840
        state.lineage.shift();
9✔
841

842
        // add new line before "end for"
843
        result.push('\n');
9✔
844

845
        //end for
846
        result.push(
9✔
847
            state.indent(),
848
            state.transpileToken(this.endForToken)
849
        );
850

851
        return result;
9✔
852
    }
853

854
    walk(visitor: WalkVisitor, options: WalkOptions) {
855
        if (options.walkMode & InternalWalkMode.walkStatements) {
68✔
856
            walk(this, 'counterDeclaration', visitor, options);
67✔
857
        }
858
        if (options.walkMode & InternalWalkMode.walkExpressions) {
68✔
859
            walk(this, 'finalValue', visitor, options);
47✔
860
            walk(this, 'increment', visitor, options);
47✔
861
        }
862
        if (options.walkMode & InternalWalkMode.walkStatements) {
68✔
863
            walk(this, 'body', visitor, options);
67✔
864
        }
865
    }
866
}
867

868
export class ForEachStatement extends Statement {
1✔
869
    constructor(
870
        readonly tokens: {
19✔
871
            forEach: Token;
872
            in: Token;
873
            endFor: Token;
874
        },
875
        readonly item: Token,
19✔
876
        readonly target: Expression,
19✔
877
        readonly body: Block
19✔
878
    ) {
879
        super();
19✔
880
        this.range = util.createBoundingRange(
19✔
881
            tokens.forEach,
882
            item,
883
            tokens.in,
884
            target,
885
            body,
886
            tokens.endFor
887
        );
888
    }
889

890
    public readonly range: Range | undefined;
891

892
    transpile(state: BrsTranspileState) {
893
        let result = [] as TranspileResult;
5✔
894
        //for each
895
        result.push(
5✔
896
            state.transpileToken(this.tokens.forEach),
897
            ' '
898
        );
899
        //item
900
        result.push(
5✔
901
            state.transpileToken(this.item),
902
            ' '
903
        );
904
        //in
905
        result.push(
5✔
906
            state.transpileToken(this.tokens.in),
907
            ' '
908
        );
909
        //target
910
        result.push(...this.target.transpile(state));
5✔
911
        //body
912
        state.lineage.unshift(this);
5✔
913
        result.push(...this.body.transpile(state));
5✔
914
        state.lineage.shift();
5✔
915

916
        // add new line before "end for"
917
        result.push('\n');
5✔
918

919
        //end for
920
        result.push(
5✔
921
            state.indent(),
922
            state.transpileToken(this.tokens.endFor)
923
        );
924
        return result;
5✔
925
    }
926

927
    walk(visitor: WalkVisitor, options: WalkOptions) {
928
        if (options.walkMode & InternalWalkMode.walkExpressions) {
40✔
929
            walk(this, 'target', visitor, options);
29✔
930
        }
931
        if (options.walkMode & InternalWalkMode.walkStatements) {
40✔
932
            walk(this, 'body', visitor, options);
39✔
933
        }
934
    }
935
}
936

937
export class WhileStatement extends Statement {
1✔
938
    constructor(
939
        readonly tokens: {
19✔
940
            while: Token;
941
            endWhile: Token;
942
        },
943
        readonly condition: Expression,
19✔
944
        readonly body: Block
19✔
945
    ) {
946
        super();
19✔
947
        this.range = util.createBoundingRange(
19✔
948
            tokens.while,
949
            condition,
950
            body,
951
            tokens.endWhile
952
        );
953
    }
954

955
    public readonly range: Range | undefined;
956

957
    transpile(state: BrsTranspileState) {
958
        let result = [] as TranspileResult;
4✔
959
        //while
960
        result.push(
4✔
961
            state.transpileToken(this.tokens.while),
962
            ' '
963
        );
964
        //condition
965
        result.push(
4✔
966
            ...this.condition.transpile(state)
967
        );
968
        state.lineage.unshift(this);
4✔
969
        //body
970
        result.push(...this.body.transpile(state));
4✔
971
        state.lineage.shift();
4✔
972

973
        //trailing newline only if we have body statements
974
        result.push('\n');
4✔
975

976
        //end while
977
        result.push(
4✔
978
            state.indent(),
979
            state.transpileToken(this.tokens.endWhile)
980
        );
981

982
        return result;
4✔
983
    }
984

985
    walk(visitor: WalkVisitor, options: WalkOptions) {
986
        if (options.walkMode & InternalWalkMode.walkExpressions) {
41✔
987
            walk(this, 'condition', visitor, options);
27✔
988
        }
989
        if (options.walkMode & InternalWalkMode.walkStatements) {
41✔
990
            walk(this, 'body', visitor, options);
40✔
991
        }
992
    }
993
}
994

995
export class DottedSetStatement extends Statement {
1✔
996
    constructor(
997
        readonly obj: Expression,
234✔
998
        readonly name: Identifier,
234✔
999
        readonly value: Expression,
234✔
1000
        readonly dot?: Token
234✔
1001
    ) {
1002
        super();
234✔
1003
        this.range = util.createBoundingRange(
234✔
1004
            obj,
1005
            dot,
1006
            name,
1007
            value
1008
        );
1009
    }
1010

1011
    public readonly range: Range | undefined;
1012

1013
    transpile(state: BrsTranspileState) {
1014
        //if the value is a compound assignment, don't add the obj, dot, name, or operator...the expression will handle that
1015
        if (CompoundAssignmentOperators.includes((this.value as BinaryExpression)?.operator?.kind)) {
8!
1016
            return this.value.transpile(state);
2✔
1017
        } else {
1018
            return [
6✔
1019
                //object
1020
                ...this.obj.transpile(state),
1021
                this.dot ? state.tokenToSourceNode(this.dot) : '.',
6!
1022
                //name
1023
                state.transpileToken(this.name),
1024
                ' = ',
1025
                //right-hand-side of assignment
1026
                ...this.value.transpile(state)
1027
            ];
1028
        }
1029
    }
1030

1031
    walk(visitor: WalkVisitor, options: WalkOptions) {
1032
        if (options.walkMode & InternalWalkMode.walkExpressions) {
288✔
1033
            walk(this, 'obj', visitor, options);
261✔
1034
            walk(this, 'value', visitor, options);
261✔
1035
        }
1036
    }
1037
}
1038

1039
export class IndexedSetStatement extends Statement {
1✔
1040
    constructor(
1041
        readonly obj: Expression,
30✔
1042
        readonly index: Expression,
30✔
1043
        readonly value: Expression,
30✔
1044
        readonly openingSquare: Token,
30✔
1045
        readonly closingSquare: Token,
30✔
1046
        readonly additionalIndexes?: Expression[]
30✔
1047
    ) {
1048
        super();
30✔
1049
        this.range = util.createBoundingRange(
30✔
1050
            obj,
1051
            openingSquare,
1052
            index,
1053
            closingSquare,
1054
            value
1055
        );
1056
        this.additionalIndexes ??= [];
30✔
1057
    }
1058

1059
    public readonly range: Range | undefined;
1060

1061
    transpile(state: BrsTranspileState) {
1062
        //if the value is a component assignment, don't add the obj, index or operator...the expression will handle that
1063
        if (CompoundAssignmentOperators.includes((this.value as BinaryExpression)?.operator?.kind)) {
12!
1064
            return this.value.transpile(state);
2✔
1065
        } else {
1066
            const result = [];
10✔
1067
            result.push(
10✔
1068
                //obj
1069
                ...this.obj.transpile(state),
1070
                //   [
1071
                state.transpileToken(this.openingSquare)
1072
            );
1073
            const indexes = [this.index, ...this.additionalIndexes ?? []];
10!
1074
            for (let i = 0; i < indexes.length; i++) {
10✔
1075
                //add comma between indexes
1076
                if (i > 0) {
11✔
1077
                    result.push(', ');
1✔
1078
                }
1079
                let index = indexes[i];
11✔
1080
                result.push(
11✔
1081
                    ...(index?.transpile(state) ?? [])
66!
1082
                );
1083
            }
1084
            result.push(
10✔
1085
                state.transpileToken(this.closingSquare),
1086
                ' = ',
1087
                ...this.value.transpile(state)
1088
            );
1089
            return result;
10✔
1090
        }
1091
    }
1092

1093
    walk(visitor: WalkVisitor, options: WalkOptions) {
1094
        if (options.walkMode & InternalWalkMode.walkExpressions) {
72✔
1095
            walk(this, 'obj', visitor, options);
53✔
1096
            walk(this, 'index', visitor, options);
53✔
1097
            walkArray(this.additionalIndexes, visitor, options, this);
53✔
1098
            walk(this, 'value', visitor, options);
53✔
1099
        }
1100
    }
1101
}
1102

1103
export class LibraryStatement extends Statement implements TypedefProvider {
1✔
1104
    constructor(
1105
        readonly tokens: {
13✔
1106
            library: Token;
1107
            filePath: Token | undefined;
1108
        }
1109
    ) {
1110
        super();
13✔
1111
        this.range = util.createBoundingRange(
13✔
1112
            this.tokens.library,
1113
            this.tokens.filePath
1114
        );
1115
    }
1116

1117
    public readonly range: Range | undefined;
1118

1119
    transpile(state: BrsTranspileState) {
1120
        let result = [] as TranspileResult;
2✔
1121
        result.push(
2✔
1122
            state.transpileToken(this.tokens.library)
1123
        );
1124
        //there will be a parse error if file path is missing, but let's prevent a runtime error just in case
1125
        if (this.tokens.filePath) {
2!
1126
            result.push(
2✔
1127
                ' ',
1128
                state.transpileToken(this.tokens.filePath)
1129
            );
1130
        }
1131
        return result;
2✔
1132
    }
1133

1134
    getTypedef(state: BrsTranspileState) {
1135
        return this.transpile(state);
×
1136
    }
1137

1138
    walk(visitor: WalkVisitor, options: WalkOptions) {
1139
        //nothing to walk
1140
    }
1141
}
1142

1143
export class NamespaceStatement extends Statement implements TypedefProvider {
1✔
1144
    constructor(
1145
        public keyword: Token,
231✔
1146
        // this should technically only be a VariableExpression or DottedGetExpression, but that can be enforced elsewhere
1147
        public nameExpression: NamespacedVariableNameExpression,
231✔
1148
        public body: Body,
231✔
1149
        public endKeyword: Token
231✔
1150
    ) {
1151
        super();
231✔
1152
        this.name = this.getName(ParseMode.BrighterScript);
231✔
1153
        this.symbolTable = new SymbolTable(`NamespaceStatement: '${this.name}'`, () => this.parent?.getSymbolTable());
231!
1154
    }
1155

1156
    /**
1157
     * The string name for this namespace
1158
     */
1159
    public name: string;
1160

1161
    public get range() {
1162
        return this.cacheRange();
124✔
1163
    }
1164
    private _range: Range | undefined;
1165

1166
    public cacheRange() {
1167
        if (!this._range) {
354✔
1168
            this._range = util.createBoundingRange(
232✔
1169
                this.keyword,
1170
                this.nameExpression,
1171
                this.body,
1172
                this.endKeyword
1173
            );
1174
        }
1175
        return this._range;
354✔
1176
    }
1177

1178
    public getName(parseMode: ParseMode) {
1179
        const parentNamespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,173✔
1180
        let name = this.nameExpression.getName(parseMode);
2,173✔
1181

1182
        if (parentNamespace) {
2,173✔
1183
            const sep = parseMode === ParseMode.BrighterScript ? '.' : '_';
114✔
1184
            name = parentNamespace.getName(parseMode) + sep + name;
114✔
1185
        }
1186

1187
        return name;
2,173✔
1188
    }
1189

1190
    transpile(state: BrsTranspileState) {
1191
        //namespaces don't actually have any real content, so just transpile their bodies
1192
        return this.body.transpile(state);
29✔
1193
    }
1194

1195
    getTypedef(state: BrsTranspileState): TranspileResult {
1196
        let result = [
7✔
1197
            'namespace ',
1198
            ...this.getName(ParseMode.BrighterScript),
1199
            state.newline
1200
        ] as TranspileResult;
1201
        state.blockDepth++;
7✔
1202
        result.push(
7✔
1203
            ...this.body.getTypedef(state)
1204
        );
1205
        state.blockDepth--;
7✔
1206

1207
        result.push(
7✔
1208
            state.indent(),
1209
            'end namespace'
1210
        );
1211
        return result;
7✔
1212
    }
1213

1214
    walk(visitor: WalkVisitor, options: WalkOptions) {
1215
        if (options.walkMode & InternalWalkMode.walkExpressions) {
435✔
1216
            walk(this, 'nameExpression', visitor, options);
431✔
1217
        }
1218

1219
        if (this.body.statements.length > 0 && options.walkMode & InternalWalkMode.walkStatements) {
435✔
1220
            walk(this, 'body', visitor, options);
368✔
1221
        }
1222
    }
1223
}
1224

1225
export class ImportStatement extends Statement implements TypedefProvider {
1✔
1226
    constructor(
1227
        readonly importToken: Token,
36✔
1228
        readonly filePathToken: Token | undefined
36✔
1229
    ) {
1230
        super();
36✔
1231
        this.range = util.createBoundingRange(
36✔
1232
            importToken,
1233
            filePathToken
1234
        );
1235
        if (this.filePathToken) {
36✔
1236
            //remove quotes
1237
            this.filePath = this.filePathToken.text.replace(/"/g, '');
34✔
1238
            if (this.filePathToken.range) {
34✔
1239
                //adjust the range to exclude the quotes
1240
                this.filePathToken.range = util.createRange(
31✔
1241
                    this.filePathToken.range.start.line,
1242
                    this.filePathToken.range.start.character + 1,
1243
                    this.filePathToken.range.end.line,
1244
                    this.filePathToken.range.end.character - 1
1245
                );
1246
            }
1247
        }
1248
    }
1249
    public filePath: string | undefined;
1250
    public range: Range | undefined;
1251

1252
    transpile(state: BrsTranspileState) {
1253
        //The xml files are responsible for adding the additional script imports, but
1254
        //add the import statement as a comment just for debugging purposes
1255
        return [
3✔
1256
            `'`,
1257
            state.transpileToken(this.importToken),
1258
            ' ',
1259
            state.transpileToken(this.filePathToken!)
1260
        ];
1261
    }
1262

1263
    /**
1264
     * Get the typedef for this statement
1265
     */
1266
    public getTypedef(state: BrsTranspileState) {
1267
        return [
3✔
1268
            this.importToken.text,
1269
            ' ',
1270
            //replace any `.bs` extension with `.brs`
1271
            this.filePathToken!.text.replace(/\.bs"?$/i, '.brs"')
1272
        ];
1273
    }
1274

1275
    walk(visitor: WalkVisitor, options: WalkOptions) {
1276
        //nothing to walk
1277
    }
1278
}
1279

1280
export class InterfaceStatement extends Statement implements TypedefProvider {
1✔
1281
    constructor(
1282
        interfaceToken: Token,
1283
        name: Identifier,
1284
        extendsToken: Token,
1285
        public parentInterfaceName: NamespacedVariableNameExpression,
43✔
1286
        public body: Statement[],
43✔
1287
        endInterfaceToken: Token
1288
    ) {
1289
        super();
43✔
1290
        this.tokens.interface = interfaceToken;
43✔
1291
        this.tokens.name = name;
43✔
1292
        this.tokens.extends = extendsToken;
43✔
1293
        this.tokens.endInterface = endInterfaceToken;
43✔
1294
        this.range = util.createBoundingRange(
43✔
1295
            this.tokens.interface,
1296
            this.tokens.name,
1297
            this.tokens.extends,
1298
            this.parentInterfaceName,
1299
            ...this.body,
1300
            this.tokens.endInterface
1301
        );
1302
    }
1303

1304
    public tokens = {} as {
43✔
1305
        interface: Token;
1306
        name: Identifier;
1307
        extends: Token;
1308
        endInterface: Token;
1309
    };
1310

1311
    public range: Range | undefined;
1312

1313
    /**
1314
     * Get the name of the wrapping namespace (if it exists)
1315
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
1316
     */
1317
    public get namespaceName() {
1318
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
1319
    }
1320

1321
    public get fields() {
1322
        return this.body.filter(x => isInterfaceFieldStatement(x));
×
1323
    }
1324

1325
    public get methods() {
1326
        return this.body.filter(x => isInterfaceMethodStatement(x));
×
1327
    }
1328

1329
    /**
1330
     * The name of the interface WITH its leading namespace (if applicable)
1331
     */
1332
    public get fullName() {
1333
        const name = this.tokens.name?.text;
×
1334
        if (name) {
×
1335
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
×
1336
            if (namespace) {
×
1337
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
×
1338
                return `${namespaceName}.${name}`;
×
1339
            } else {
1340
                return name;
×
1341
            }
1342
        } else {
1343
            //return undefined which will allow outside callers to know that this interface doesn't have a name
1344
            return undefined;
×
1345
        }
1346
    }
1347

1348
    /**
1349
     * The name of the interface (without the namespace prefix)
1350
     */
1351
    public get name() {
1352
        return this.tokens.name?.text;
5!
1353
    }
1354

1355
    /**
1356
     * Get the name of this expression based on the parse mode
1357
     */
1358
    public getName(parseMode: ParseMode) {
1359
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
5✔
1360
        if (namespace) {
5✔
1361
            let delimiter = parseMode === ParseMode.BrighterScript ? '.' : '_';
1!
1362
            let namespaceName = namespace.getName(parseMode);
1✔
1363
            return namespaceName + delimiter + this.name;
1✔
1364
        } else {
1365
            return this.name;
4✔
1366
        }
1367
    }
1368

1369
    public transpile(state: BrsTranspileState): TranspileResult {
1370
        //interfaces should completely disappear at runtime
1371
        return [];
11✔
1372
    }
1373

1374
    getTypedef(state: BrsTranspileState) {
1375
        const result = [] as TranspileResult;
6✔
1376
        for (let annotation of this.annotations ?? []) {
6✔
1377
            result.push(
1✔
1378
                ...annotation.getTypedef(state),
1379
                state.newline,
1380
                state.indent()
1381
            );
1382
        }
1383
        result.push(
6✔
1384
            this.tokens.interface.text,
1385
            ' ',
1386
            this.tokens.name.text
1387
        );
1388
        const parentInterfaceName = this.parentInterfaceName?.getName(ParseMode.BrighterScript);
6!
1389
        if (parentInterfaceName) {
6!
1390
            result.push(
×
1391
                ' extends ',
1392
                parentInterfaceName
1393
            );
1394
        }
1395
        const body = this.body ?? [];
6!
1396
        if (body.length > 0) {
6!
1397
            state.blockDepth++;
6✔
1398
        }
1399
        for (const statement of body) {
6✔
1400
            if (isInterfaceMethodStatement(statement) || isInterfaceFieldStatement(statement)) {
21✔
1401
                result.push(
20✔
1402
                    state.newline,
1403
                    state.indent(),
1404
                    ...statement.getTypedef(state)
1405
                );
1406
            } else {
1407
                result.push(
1✔
1408
                    state.newline,
1409
                    state.indent(),
1410
                    ...statement.transpile(state)
1411
                );
1412
            }
1413
        }
1414
        if (body.length > 0) {
6!
1415
            state.blockDepth--;
6✔
1416
        }
1417
        result.push(
6✔
1418
            state.newline,
1419
            state.indent(),
1420
            'end interface',
1421
            state.newline
1422
        );
1423
        return result;
6✔
1424
    }
1425

1426
    walk(visitor: WalkVisitor, options: WalkOptions) {
1427
        //visitor-less walk function to do parent linking
1428
        walk(this, 'parentInterfaceName', null, options);
86✔
1429

1430
        if (options.walkMode & InternalWalkMode.walkStatements) {
86!
1431
            walkArray(this.body, visitor, options, this);
86✔
1432
        }
1433
    }
1434
}
1435

1436
export class InterfaceFieldStatement extends Statement implements TypedefProvider {
1✔
1437
    public transpile(state: BrsTranspileState): TranspileResult {
1438
        throw new Error('Method not implemented.');
×
1439
    }
1440
    constructor(
1441
        nameToken: Identifier,
1442
        asToken: Token,
1443
        typeToken: Token,
1444
        public type: BscType,
49✔
1445
        optionalToken?: Token
1446
    ) {
1447
        super();
49✔
1448
        this.tokens.optional = optionalToken;
49✔
1449
        this.tokens.name = nameToken;
49✔
1450
        this.tokens.as = asToken;
49✔
1451
        this.tokens.type = typeToken;
49✔
1452
        this.range = util.createBoundingRange(
49✔
1453
            optionalToken,
1454
            nameToken,
1455
            asToken,
1456
            typeToken
1457
        );
1458
    }
1459

1460
    public range: Range | undefined;
1461

1462
    public tokens = {} as {
49✔
1463
        optional?: Token;
1464
        name: Identifier;
1465
        as: Token;
1466
        type: Token;
1467
    };
1468

1469
    public get name() {
1470
        return this.tokens.name.text;
×
1471
    }
1472

1473
    public get isOptional() {
1474
        return !!this.tokens.optional;
10✔
1475
    }
1476

1477
    walk(visitor: WalkVisitor, options: WalkOptions) {
1478
        //nothing to walk
1479
    }
1480

1481
    getTypedef(state: BrsTranspileState): TranspileResult {
1482
        const result = [] as TranspileResult;
10✔
1483
        for (let annotation of this.annotations ?? []) {
10✔
1484
            result.push(
1✔
1485
                ...annotation.getTypedef(state),
1486
                state.newline,
1487
                state.indent()
1488
            );
1489
        }
1490
        if (this.isOptional) {
10!
1491
            result.push(
×
1492
                this.tokens.optional!.text,
1493
                ' '
1494
            );
1495
        }
1496
        result.push(
10✔
1497
            this.tokens.name.text
1498
        );
1499
        if (this.tokens.type?.text?.length > 0) {
10!
1500
            result.push(
10✔
1501
                ' as ',
1502
                this.tokens.type.text
1503
            );
1504
        }
1505
        return result;
10✔
1506
    }
1507

1508
}
1509

1510
export class InterfaceMethodStatement extends Statement implements TypedefProvider {
1✔
1511
    public transpile(state: BrsTranspileState): TranspileResult {
1512
        throw new Error('Method not implemented.');
×
1513
    }
1514
    constructor(
1515
        functionTypeToken: Token,
1516
        nameToken: Identifier,
1517
        leftParen: Token,
1518
        public params: FunctionParameterExpression[],
16✔
1519
        rightParen: Token,
1520
        asToken?: Token,
1521
        returnTypeToken?: Token,
1522
        public returnType?: BscType,
16✔
1523
        optionalToken?: Token
1524
    ) {
1525
        super();
16✔
1526
        this.tokens.optional = optionalToken;
16✔
1527
        this.tokens.functionType = functionTypeToken;
16✔
1528
        this.tokens.name = nameToken;
16✔
1529
        this.tokens.leftParen = leftParen;
16✔
1530
        this.tokens.rightParen = rightParen;
16✔
1531
        this.tokens.as = asToken;
16✔
1532
        this.tokens.returnType = returnTypeToken;
16✔
1533
    }
1534

1535
    public get range() {
1536
        return util.createBoundingRange(
2✔
1537
            this.tokens.optional,
1538
            this.tokens.functionType,
1539
            this.tokens.name,
1540
            this.tokens.leftParen,
1541
            ...(this.params ?? []),
6!
1542
            this.tokens.rightParen,
1543
            this.tokens.as,
1544
            this.tokens.returnType
1545
        );
1546
    }
1547

1548
    public tokens = {} as {
16✔
1549
        optional?: Token;
1550
        functionType: Token;
1551
        name: Identifier;
1552
        leftParen: Token;
1553
        rightParen: Token;
1554
        as: Token | undefined;
1555
        returnType: Token | undefined;
1556
    };
1557

1558
    public get isOptional() {
1559
        return !!this.tokens.optional;
10✔
1560
    }
1561

1562
    walk(visitor: WalkVisitor, options: WalkOptions) {
1563
        //nothing to walk
1564
    }
1565

1566
    getTypedef(state: BrsTranspileState) {
1567
        const result = [] as TranspileResult;
10✔
1568
        for (let annotation of this.annotations ?? []) {
10✔
1569
            result.push(
1✔
1570
                ...annotation.getTypedef(state),
1571
                state.newline,
1572
                state.indent()
1573
            );
1574
        }
1575

1576
        if (this.isOptional) {
10!
1577
            result.push(
×
1578
                this.tokens.optional!.text,
1579
                ' '
1580
            );
1581
        }
1582

1583
        result.push(
10✔
1584
            this.tokens.functionType.text,
1585
            ' ',
1586
            this.tokens.name.text,
1587
            '('
1588
        );
1589
        const params = this.params ?? [];
10!
1590
        for (let i = 0; i < params.length; i++) {
10✔
1591
            if (i > 0) {
2✔
1592
                result.push(', ');
1✔
1593
            }
1594
            const param = params[i];
2✔
1595
            result.push(param.name.text);
2✔
1596
            const typeToken = param.typeToken;
2✔
1597
            if (typeToken && typeToken.text.length > 0) {
2!
1598
                result.push(
2✔
1599
                    ' as ',
1600
                    typeToken.text
1601
                );
1602
            }
1603
        }
1604
        result.push(
10✔
1605
            ')'
1606
        );
1607
        const returnTypeToken = this.tokens.returnType;
10✔
1608
        if (returnTypeToken && returnTypeToken.text.length > 0) {
10!
1609
            result.push(
10✔
1610
                ' as ',
1611
                returnTypeToken.text
1612
            );
1613
        }
1614
        return result;
10✔
1615
    }
1616
}
1617

1618
export class ClassStatement extends Statement implements TypedefProvider {
1✔
1619

1620
    constructor(
1621
        readonly classKeyword: Token,
453✔
1622
        /**
1623
         * The name of the class (without namespace prefix)
1624
         */
1625
        readonly name: Identifier,
453✔
1626
        public body: Statement[],
453✔
1627
        readonly end: Token,
453✔
1628
        readonly extendsKeyword?: Token,
453✔
1629
        readonly parentClassName?: NamespacedVariableNameExpression
453✔
1630
    ) {
1631
        super();
453✔
1632
        this.body = this.body ?? [];
453!
1633

1634
        for (let statement of this.body) {
453✔
1635
            if (isMethodStatement(statement)) {
427✔
1636
                this.methods.push(statement);
246✔
1637
                this.memberMap[statement?.name?.text.toLowerCase()] = statement;
246!
1638
            } else if (isFieldStatement(statement)) {
181✔
1639
                this.fields.push(statement);
173✔
1640
                this.memberMap[statement.name?.text.toLowerCase()] = statement;
173!
1641
            }
1642
        }
1643

1644
        this.range = util.createBoundingRange(
453✔
1645
            classKeyword,
1646
            name,
1647
            extendsKeyword,
1648
            parentClassName,
1649
            ...(body ?? []),
1,359!
1650
            end
1651
        );
1652
    }
1653

1654
    /**
1655
     * Get the name of the wrapping namespace (if it exists)
1656
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
1657
     */
1658
    public get namespaceName() {
1659
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
1660
    }
1661

1662

1663
    public getName(parseMode: ParseMode) {
1664
        const name = this.name?.text;
891✔
1665
        if (name) {
891✔
1666
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
889✔
1667
            if (namespace) {
889✔
1668
                let namespaceName = namespace.getName(parseMode);
246✔
1669
                let separator = parseMode === ParseMode.BrighterScript ? '.' : '_';
246✔
1670
                return namespaceName + separator + name;
246✔
1671
            } else {
1672
                return name;
643✔
1673
            }
1674
        } else {
1675
            //return undefined which will allow outside callers to know that this class doesn't have a name
1676
            return undefined;
2✔
1677
        }
1678
    }
1679

1680
    public memberMap = {} as Record<string, MemberStatement>;
453✔
1681
    public methods = [] as MethodStatement[];
453✔
1682
    public fields = [] as FieldStatement[];
453✔
1683

1684
    public readonly range: Range | undefined;
1685

1686
    transpile(state: BrsTranspileState) {
1687
        let result = [] as TranspileResult;
44✔
1688
        //make the builder
1689
        result.push(...this.getTranspiledBuilder(state));
44✔
1690
        result.push(
44✔
1691
            '\n',
1692
            state.indent()
1693
        );
1694
        //make the class assembler (i.e. the public-facing class creator method)
1695
        result.push(...this.getTranspiledClassFunction(state));
44✔
1696
        return result;
44✔
1697
    }
1698

1699
    getTypedef(state: BrsTranspileState) {
1700
        const result = [] as TranspileResult;
15✔
1701
        for (let annotation of this.annotations ?? []) {
15!
1702
            result.push(
×
1703
                ...annotation.getTypedef(state),
1704
                state.newline,
1705
                state.indent()
1706
            );
1707
        }
1708
        result.push(
15✔
1709
            'class ',
1710
            this.name.text
1711
        );
1712
        if (this.extendsKeyword && this.parentClassName) {
15✔
1713
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
4✔
1714
            const fqName = util.getFullyQualifiedClassName(
4✔
1715
                this.parentClassName.getName(ParseMode.BrighterScript),
1716
                namespace?.getName(ParseMode.BrighterScript)
12✔
1717
            );
1718
            result.push(
4✔
1719
                ` extends ${fqName}`
1720
            );
1721
        }
1722
        result.push(state.newline);
15✔
1723
        state.blockDepth++;
15✔
1724

1725
        let body = this.body;
15✔
1726
        //inject an empty "new" method if missing
1727
        if (!this.getConstructorFunction()) {
15✔
1728
            const constructor = createMethodStatement('new', TokenKind.Sub);
11✔
1729
            constructor.parent = this;
11✔
1730
            //walk the constructor to set up parent links
1731
            constructor.link();
11✔
1732
            body = [
11✔
1733
                constructor,
1734
                ...this.body
1735
            ];
1736
        }
1737

1738
        for (const member of body) {
15✔
1739
            if (isTypedefProvider(member)) {
33!
1740
                result.push(
33✔
1741
                    state.indent(),
1742
                    ...member.getTypedef(state),
1743
                    state.newline
1744
                );
1745
            }
1746
        }
1747
        state.blockDepth--;
15✔
1748
        result.push(
15✔
1749
            state.indent(),
1750
            'end class'
1751
        );
1752
        return result;
15✔
1753
    }
1754

1755
    /**
1756
     * Find the parent index for this class's parent.
1757
     * For class inheritance, every class is given an index.
1758
     * The base class is index 0, its child is index 1, and so on.
1759
     */
1760
    public getParentClassIndex(state: BrsTranspileState) {
1761
        let myIndex = 0;
101✔
1762
        let stmt = this as ClassStatement;
101✔
1763
        while (stmt) {
101✔
1764
            if (stmt.parentClassName) {
162✔
1765
                const namespace = stmt.findAncestor<NamespaceStatement>(isNamespaceStatement);
62✔
1766
                //find the parent class
1767
                stmt = state.file.getClassFileLink(
62✔
1768
                    stmt.parentClassName.getName(ParseMode.BrighterScript),
1769
                    namespace?.getName(ParseMode.BrighterScript)
186✔
1770
                )?.item;
62✔
1771
                myIndex++;
62✔
1772
            } else {
1773
                break;
100✔
1774
            }
1775
        }
1776
        const result = myIndex - 1;
101✔
1777
        if (result >= 0) {
101✔
1778
            return result;
49✔
1779
        } else {
1780
            return null;
52✔
1781
        }
1782
    }
1783

1784
    public hasParentClass() {
1785
        return !!this.parentClassName;
44✔
1786
    }
1787

1788
    /**
1789
     * Get all ancestor classes, in closest-to-furthest order (i.e. 0 is parent, 1 is grandparent, etc...).
1790
     * This will return an empty array if no ancestors were found
1791
     */
1792
    public getAncestors(state: BrsTranspileState) {
1793
        let ancestors = [] as ClassStatement[];
88✔
1794
        let stmt = this as ClassStatement;
88✔
1795
        while (stmt) {
88✔
1796
            if (stmt.parentClassName) {
138✔
1797
                const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
52✔
1798
                stmt = state.file.getClassFileLink(
52✔
1799
                    stmt.parentClassName.getName(ParseMode.BrighterScript),
1800
                    namespace?.getName(ParseMode.BrighterScript)
156✔
1801
                )?.item;
52✔
1802
                ancestors.push(stmt);
52✔
1803
            } else {
1804
                break;
86✔
1805
            }
1806
        }
1807
        return ancestors;
88✔
1808
    }
1809

1810
    private getBuilderName(name: string) {
1811
        if (name.includes('.')) {
109✔
1812
            name = name.replace(/\./gi, '_');
6✔
1813
        }
1814
        return `__${name}_builder`;
109✔
1815
    }
1816

1817
    /**
1818
     * Get the constructor function for this class (if exists), or undefined if not exist
1819
     */
1820
    private getConstructorFunction() {
1821
        return this.body.find((stmt) => {
103✔
1822
            return (stmt as MethodStatement)?.name?.text?.toLowerCase() === 'new';
98!
1823
        }) as MethodStatement;
1824
    }
1825

1826
    /**
1827
     * Determine if the specified field was declared in one of the ancestor classes
1828
     */
1829
    public isFieldDeclaredByAncestor(fieldName: string, ancestors: ClassStatement[]) {
1830
        let lowerFieldName = fieldName.toLowerCase();
×
1831
        for (let ancestor of ancestors) {
×
1832
            if (ancestor.memberMap[lowerFieldName]) {
×
1833
                return true;
×
1834
            }
1835
        }
1836
        return false;
×
1837
    }
1838

1839
    /**
1840
     * The builder is a function that assigns all of the methods and property names to a class instance.
1841
     * This needs to be a separate function so that child classes can call the builder from their parent
1842
     * without instantiating the parent constructor at that point in time.
1843
     */
1844
    private getTranspiledBuilder(state: BrsTranspileState) {
1845
        let result = [] as TranspileResult;
44✔
1846
        result.push(`function ${this.getBuilderName(this.getName(ParseMode.BrightScript)!)}()\n`);
44✔
1847
        state.blockDepth++;
44✔
1848
        //indent
1849
        result.push(state.indent());
44✔
1850

1851
        /**
1852
         * The lineage of this class. index 0 is a direct parent, index 1 is index 0's parent, etc...
1853
         */
1854
        let ancestors = this.getAncestors(state);
44✔
1855

1856
        //construct parent class or empty object
1857
        if (ancestors[0]) {
44✔
1858
            const ancestorNamespace = ancestors[0].findAncestor<NamespaceStatement>(isNamespaceStatement);
21✔
1859
            let fullyQualifiedClassName = util.getFullyQualifiedClassName(
21✔
1860
                ancestors[0].getName(ParseMode.BrighterScript)!,
1861
                ancestorNamespace?.getName(ParseMode.BrighterScript)
63✔
1862
            );
1863
            result.push(
21✔
1864
                'instance = ',
1865
                this.getBuilderName(fullyQualifiedClassName), '()');
1866
        } else {
1867
            //use an empty object.
1868
            result.push('instance = {}');
23✔
1869
        }
1870
        result.push(
44✔
1871
            state.newline,
1872
            state.indent()
1873
        );
1874
        let parentClassIndex = this.getParentClassIndex(state);
44✔
1875

1876
        let body = this.body;
44✔
1877
        //inject an empty "new" method if missing
1878
        if (!this.getConstructorFunction()) {
44✔
1879
            body = [
24✔
1880
                createMethodStatement('new', TokenKind.Sub),
1881
                ...this.body
1882
            ];
1883
        }
1884

1885
        for (let statement of body) {
44✔
1886
            //is field statement
1887
            if (isFieldStatement(statement)) {
67✔
1888
                //do nothing with class fields in this situation, they are handled elsewhere
1889
                continue;
11✔
1890

1891
                //methods
1892
            } else if (isMethodStatement(statement)) {
56!
1893

1894
                //store overridden parent methods as super{parentIndex}_{methodName}
1895
                if (
56✔
1896
                    //is override method
1897
                    statement.override ||
152✔
1898
                    //is constructor function in child class
1899
                    (statement.name.text.toLowerCase() === 'new' && ancestors[0])
1900
                ) {
1901
                    result.push(
25✔
1902
                        `instance.super${parentClassIndex}_${statement.name.text} = instance.${statement.name.text}`,
1903
                        state.newline,
1904
                        state.indent()
1905
                    );
1906
                }
1907

1908
                state.classStatement = this;
56✔
1909
                result.push(
56✔
1910
                    'instance.',
1911
                    state.transpileToken(statement.name),
1912
                    ' = ',
1913
                    ...statement.transpile(state),
1914
                    state.newline,
1915
                    state.indent()
1916
                );
1917
                delete state.classStatement;
56✔
1918
            } else {
1919
                //other random statements (probably just comments)
1920
                result.push(
×
1921
                    ...statement.transpile(state),
1922
                    state.newline,
1923
                    state.indent()
1924
                );
1925
            }
1926
        }
1927
        //return the instance
1928
        result.push('return instance\n');
44✔
1929
        state.blockDepth--;
44✔
1930
        result.push(state.indent());
44✔
1931
        result.push(`end function`);
44✔
1932
        return result;
44✔
1933
    }
1934

1935
    /**
1936
     * The class function is the function with the same name as the class. This is the function that
1937
     * consumers should call to create a new instance of that class.
1938
     * This invokes the builder, gets an instance of the class, then invokes the "new" function on that class.
1939
     */
1940
    private getTranspiledClassFunction(state: BrsTranspileState) {
1941
        let result = [] as TranspileResult;
44✔
1942
        const constructorFunction = this.getConstructorFunction();
44✔
1943
        const constructorParams = constructorFunction ? constructorFunction.func.parameters : [];
44✔
1944

1945
        result.push(
44✔
1946
            state.sourceNode(this.classKeyword, 'function'),
1947
            state.sourceNode(this.classKeyword, ' '),
1948
            state.sourceNode(this.name, this.getName(ParseMode.BrightScript)!),
1949
            `(`
1950
        );
1951
        let i = 0;
44✔
1952
        for (let param of constructorParams) {
44✔
1953
            if (i > 0) {
8✔
1954
                result.push(', ');
2✔
1955
            }
1956
            result.push(
8✔
1957
                param.transpile(state)
1958
            );
1959
            i++;
8✔
1960
        }
1961
        result.push(
44✔
1962
            ')',
1963
            '\n'
1964
        );
1965

1966
        state.blockDepth++;
44✔
1967
        result.push(state.indent());
44✔
1968
        result.push(`instance = ${this.getBuilderName(this.getName(ParseMode.BrightScript)!)}()\n`);
44✔
1969

1970
        result.push(state.indent());
44✔
1971
        result.push(`instance.new(`);
44✔
1972

1973
        //append constructor arguments
1974
        i = 0;
44✔
1975
        for (let param of constructorParams) {
44✔
1976
            if (i > 0) {
8✔
1977
                result.push(', ');
2✔
1978
            }
1979
            result.push(
8✔
1980
                state.transpileToken(param.name)
1981
            );
1982
            i++;
8✔
1983
        }
1984
        result.push(
44✔
1985
            ')',
1986
            '\n'
1987
        );
1988

1989
        result.push(state.indent());
44✔
1990
        result.push(`return instance\n`);
44✔
1991

1992
        state.blockDepth--;
44✔
1993
        result.push(state.indent());
44✔
1994
        result.push(`end function`);
44✔
1995
        return result;
44✔
1996
    }
1997

1998
    walk(visitor: WalkVisitor, options: WalkOptions) {
1999
        //visitor-less walk function to do parent linking
2000
        walk(this, 'parentClassName', null, options);
885✔
2001

2002
        if (options.walkMode & InternalWalkMode.walkStatements) {
885!
2003
            walkArray(this.body, visitor, options, this);
885✔
2004
        }
2005
    }
2006
}
2007

2008
const accessModifiers = [
1✔
2009
    TokenKind.Public,
2010
    TokenKind.Protected,
2011
    TokenKind.Private
2012
];
2013
export class MethodStatement extends FunctionStatement {
1✔
2014
    constructor(
2015
        modifiers: Token | Token[],
2016
        name: Identifier,
2017
        func: FunctionExpression,
2018
        public override: Token
281✔
2019
    ) {
2020
        super(name, func);
281✔
2021
        if (modifiers) {
281✔
2022
            if (Array.isArray(modifiers)) {
30!
2023
                this.modifiers.push(...modifiers);
×
2024
            } else {
2025
                this.modifiers.push(modifiers);
30✔
2026
            }
2027
        }
2028
        this.range = util.createBoundingRange(
281✔
2029
            ...(this.modifiers),
2030
            override,
2031
            func
2032
        );
2033
    }
2034

2035
    public modifiers: Token[] = [];
281✔
2036

2037
    public get accessModifier() {
2038
        return this.modifiers.find(x => accessModifiers.includes(x.kind));
100✔
2039
    }
2040

2041
    public readonly range: Range | undefined;
2042

2043
    /**
2044
     * Get the name of this method.
2045
     */
2046
    public getName(parseMode: ParseMode) {
2047
        return this.name.text;
1✔
2048
    }
2049

2050
    transpile(state: BrsTranspileState) {
2051
        if (this.name.text.toLowerCase() === 'new') {
56✔
2052
            this.ensureSuperConstructorCall(state);
44✔
2053
            //TODO we need to undo this at the bottom of this method
2054
            this.injectFieldInitializersForConstructor(state);
44✔
2055
        }
2056
        //TODO - remove type information from these methods because that doesn't work
2057
        //convert the `super` calls into the proper methods
2058
        const parentClassIndex = state.classStatement.getParentClassIndex(state);
56✔
2059
        const visitor = createVisitor({
56✔
2060
            VariableExpression: e => {
2061
                if (e.name.text.toLocaleLowerCase() === 'super') {
55✔
2062
                    state.editor.setProperty(e.name, 'text', `m.super${parentClassIndex}_new`);
21✔
2063
                }
2064
            },
2065
            DottedGetExpression: e => {
2066
                const beginningVariable = util.findBeginningVariableExpression(e);
25✔
2067
                const lowerName = beginningVariable?.getName(ParseMode.BrighterScript).toLowerCase();
25!
2068
                if (lowerName === 'super') {
25✔
2069
                    state.editor.setProperty(beginningVariable.name, 'text', 'm');
7✔
2070
                    state.editor.setProperty(e.name, 'text', `super${parentClassIndex}_${e.name.text}`);
7✔
2071
                }
2072
            }
2073
        });
2074
        const walkOptions: WalkOptions = { walkMode: WalkMode.visitExpressions };
56✔
2075
        for (const statement of this.func.body.statements) {
56✔
2076
            visitor(statement, undefined);
60✔
2077
            statement.walk(visitor, walkOptions);
60✔
2078
        }
2079
        return this.func.transpile(state);
56✔
2080
    }
2081

2082
    getTypedef(state: BrsTranspileState) {
2083
        const result = [] as TranspileResult;
23✔
2084
        for (let annotation of this.annotations ?? []) {
23✔
2085
            result.push(
2✔
2086
                ...annotation.getTypedef(state),
2087
                state.newline,
2088
                state.indent()
2089
            );
2090
        }
2091
        if (this.accessModifier) {
23✔
2092
            result.push(
8✔
2093
                this.accessModifier.text,
2094
                ' '
2095
            );
2096
        }
2097
        if (this.override) {
23✔
2098
            result.push('override ');
1✔
2099
        }
2100
        result.push(
23✔
2101
            ...this.func.getTypedef(state)
2102
        );
2103
        return result;
23✔
2104
    }
2105

2106
    /**
2107
     * All child classes must call the parent constructor. The type checker will warn users when they don't call it in their own class,
2108
     * but we still need to call it even if they have omitted it. This injects the super call if it's missing
2109
     */
2110
    private ensureSuperConstructorCall(state: BrsTranspileState) {
2111
        //if this class doesn't extend another class, quit here
2112
        if (state.classStatement!.getAncestors(state).length === 0) {
44✔
2113
            return;
23✔
2114
        }
2115

2116
        //check whether any calls to super exist
2117
        let containsSuperCall =
2118
            this.func.body.statements.findIndex((x) => {
21✔
2119
                //is a call statement
2120
                return isExpressionStatement(x) && isCallExpression(x.expression) &&
9✔
2121
                    //is a call to super
2122
                    util.findBeginningVariableExpression(x.expression.callee as any)?.name.text.toLowerCase() === 'super';
21!
2123
            }) !== -1;
2124

2125
        //if a call to super exists, quit here
2126
        if (containsSuperCall) {
21✔
2127
            return;
7✔
2128
        }
2129

2130
        //this is a child class, and the constructor doesn't contain a call to super. Inject one
2131
        const superCall = new ExpressionStatement(
14✔
2132
            new CallExpression(
2133
                new VariableExpression(
2134
                    {
2135
                        kind: TokenKind.Identifier,
2136
                        text: 'super',
2137
                        isReserved: false,
2138
                        range: state.classStatement!.name.range,
2139
                        leadingWhitespace: ''
2140
                    }
2141
                ),
2142
                {
2143
                    kind: TokenKind.LeftParen,
2144
                    text: '(',
2145
                    isReserved: false,
2146
                    range: state.classStatement!.name.range,
2147
                    leadingWhitespace: ''
2148
                },
2149
                {
2150
                    kind: TokenKind.RightParen,
2151
                    text: ')',
2152
                    isReserved: false,
2153
                    range: state.classStatement!.name.range,
2154
                    leadingWhitespace: ''
2155
                },
2156
                []
2157
            )
2158
        );
2159
        state.editor.arrayUnshift(this.func.body.statements, superCall);
14✔
2160
    }
2161

2162
    /**
2163
     * Inject field initializers at the top of the `new` function (after any present `super()` call)
2164
     */
2165
    private injectFieldInitializersForConstructor(state: BrsTranspileState) {
2166
        let startingIndex = state.classStatement!.hasParentClass() ? 1 : 0;
44✔
2167

2168
        let newStatements = [] as Statement[];
44✔
2169
        //insert the field initializers in order
2170
        for (let field of state.classStatement!.fields) {
44✔
2171
            let thisQualifiedName = { ...field.name };
11✔
2172
            thisQualifiedName.text = 'm.' + field.name?.text;
11!
2173
            if (field.initialValue) {
11✔
2174
                newStatements.push(
9✔
2175
                    new AssignmentStatement(field.equal, thisQualifiedName, field.initialValue)
2176
                );
2177
            } else {
2178
                //if there is no initial value, set the initial value to `invalid`
2179
                newStatements.push(
2✔
2180
                    new AssignmentStatement(
2181
                        createToken(TokenKind.Equal, '=', field.name?.range),
6!
2182
                        thisQualifiedName,
2183
                        createInvalidLiteral('invalid', field.name?.range)
6!
2184
                    )
2185
                );
2186
            }
2187
        }
2188
        state.editor.arraySplice(this.func.body.statements, startingIndex, 0, ...newStatements);
44✔
2189
    }
2190

2191
    walk(visitor: WalkVisitor, options: WalkOptions) {
2192
        if (options.walkMode & InternalWalkMode.walkExpressions) {
733✔
2193
            walk(this, 'func', visitor, options);
528✔
2194
        }
2195
    }
2196
}
2197
/**
2198
 * @deprecated use `MethodStatement`
2199
 */
2200
export class ClassMethodStatement extends MethodStatement { }
1✔
2201

2202
export class FieldStatement extends Statement implements TypedefProvider {
1✔
2203

2204
    constructor(
2205
        readonly accessModifier?: Token,
173✔
2206
        readonly name?: Identifier,
173✔
2207
        readonly as?: Token,
173✔
2208
        readonly type?: Token,
173✔
2209
        readonly equal?: Token,
173✔
2210
        readonly initialValue?: Expression,
173✔
2211
        readonly optional?: Token
173✔
2212
    ) {
2213
        super();
173✔
2214
        this.range = util.createBoundingRange(
173✔
2215
            accessModifier,
2216
            name,
2217
            as,
2218
            type,
2219
            equal,
2220
            initialValue
2221
        );
2222
    }
2223

2224
    /**
2225
     * Derive a ValueKind from the type token, or the initial value.
2226
     * Defaults to `DynamicType`
2227
     */
2228
    getType() {
2229
        if (this.type) {
76✔
2230
            return util.tokenToBscType(this.type);
37✔
2231
        } else if (isLiteralExpression(this.initialValue)) {
39✔
2232
            return this.initialValue.type;
25✔
2233
        } else {
2234
            return new DynamicType();
14✔
2235
        }
2236
    }
2237

2238
    public readonly range: Range | undefined;
2239

2240
    public get isOptional() {
2241
        return !!this.optional;
17✔
2242
    }
2243

2244
    transpile(state: BrsTranspileState): TranspileResult {
2245
        throw new Error('transpile not implemented for ' + Object.getPrototypeOf(this).constructor.name);
×
2246
    }
2247

2248
    getTypedef(state: BrsTranspileState) {
2249
        const result = [] as TranspileResult;
10✔
2250
        if (this.name) {
10!
2251
            for (let annotation of this.annotations ?? []) {
10✔
2252
                result.push(
2✔
2253
                    ...annotation.getTypedef(state),
2254
                    state.newline,
2255
                    state.indent()
2256
                );
2257
            }
2258

2259
            let type = this.getType();
10✔
2260
            if (isInvalidType(type) || isVoidType(type)) {
10✔
2261
                type = new DynamicType();
1✔
2262
            }
2263

2264
            result.push(
10✔
2265
                this.accessModifier?.text ?? 'public',
60!
2266
                ' '
2267
            );
2268
            if (this.isOptional) {
10!
2269
                result.push(this.optional!.text, ' ');
×
2270
            }
2271
            result.push(this.name?.text,
10!
2272
                ' as ',
2273
                type.toTypeString()
2274
            );
2275
        }
2276
        return result;
10✔
2277
    }
2278

2279
    walk(visitor: WalkVisitor, options: WalkOptions) {
2280
        if (this.initialValue && options.walkMode & InternalWalkMode.walkExpressions) {
237✔
2281
            walk(this, 'initialValue', visitor, options);
60✔
2282
        }
2283
    }
2284
}
2285
/**
2286
 * @deprecated use `FieldStatement`
2287
 */
2288
export class ClassFieldStatement extends FieldStatement { }
1✔
2289

2290
export type MemberStatement = FieldStatement | MethodStatement;
2291

2292
/**
2293
 * @deprecated use `MemeberStatement`
2294
 */
2295
export type ClassMemberStatement = MemberStatement;
2296

2297
export class TryCatchStatement extends Statement {
1✔
2298
    constructor(
2299
        public tokens: {
25✔
2300
            try: Token;
2301
            endTry?: Token;
2302
        },
2303
        public tryBranch?: Block,
25✔
2304
        public catchStatement?: CatchStatement
25✔
2305
    ) {
2306
        super();
25✔
2307
        this.range = util.createBoundingRange(
25✔
2308
            tokens.try,
2309
            tryBranch,
2310
            catchStatement,
2311
            tokens.endTry
2312
        );
2313
    }
2314

2315
    public readonly range: Range | undefined;
2316

2317
    public transpile(state: BrsTranspileState): TranspileResult {
2318
        return [
3✔
2319
            state.transpileToken(this.tokens.try),
2320
            ...this.tryBranch!.transpile(state),
2321
            state.newline,
2322
            state.indent(),
2323
            ...(this.catchStatement?.transpile(state) ?? ['catch']),
18!
2324
            state.newline,
2325
            state.indent(),
2326
            state.transpileToken(this.tokens.endTry!)
2327
        ] as TranspileResult;
2328
    }
2329

2330
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2331
        if (this.tryBranch && options.walkMode & InternalWalkMode.walkStatements) {
34!
2332
            walk(this, 'tryBranch', visitor, options);
34✔
2333
            walk(this, 'catchStatement', visitor, options);
34✔
2334
        }
2335
    }
2336
}
2337

2338
export class CatchStatement extends Statement {
1✔
2339
    constructor(
2340
        public tokens: {
23✔
2341
            catch: Token;
2342
        },
2343
        public exceptionVariable?: Identifier,
23✔
2344
        public catchBranch?: Block
23✔
2345
    ) {
2346
        super();
23✔
2347
        this.range = util.createBoundingRange(
23✔
2348
            tokens.catch,
2349
            exceptionVariable,
2350
            catchBranch
2351
        );
2352
    }
2353

2354
    public range: Range | undefined;
2355

2356
    public transpile(state: BrsTranspileState): TranspileResult {
2357
        return [
3✔
2358
            state.transpileToken(this.tokens.catch),
2359
            ' ',
2360
            this.exceptionVariable?.text ?? 'e',
18!
2361
            ...(this.catchBranch?.transpile(state) ?? [])
18!
2362
        ];
2363
    }
2364

2365
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2366
        if (this.catchBranch && options.walkMode & InternalWalkMode.walkStatements) {
32!
2367
            walk(this, 'catchBranch', visitor, options);
32✔
2368
        }
2369
    }
2370
}
2371

2372
export class ThrowStatement extends Statement {
1✔
2373
    constructor(
2374
        public throwToken: Token,
8✔
2375
        public expression?: Expression
8✔
2376
    ) {
2377
        super();
8✔
2378
        this.range = util.createBoundingRange(
8✔
2379
            throwToken,
2380
            expression
2381
        );
2382
    }
2383
    public range: Range | undefined;
2384

2385
    public transpile(state: BrsTranspileState) {
2386
        const result = [
4✔
2387
            state.transpileToken(this.throwToken),
2388
            ' '
2389
        ] as TranspileResult;
2390

2391
        //if we have an expression, transpile it
2392
        if (this.expression) {
4!
2393
            result.push(
4✔
2394
                ...this.expression.transpile(state)
2395
            );
2396

2397
            //no expression found. Rather than emit syntax errors, provide a generic error message
2398
        } else {
2399
            result.push('"An error has occurred"');
×
2400
        }
2401
        return result;
4✔
2402
    }
2403

2404
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2405
        if (this.expression && options.walkMode & InternalWalkMode.walkExpressions) {
17✔
2406
            walk(this, 'expression', visitor, options);
11✔
2407
        }
2408
    }
2409
}
2410

2411

2412
export class EnumStatement extends Statement implements TypedefProvider {
1✔
2413

2414
    constructor(
2415
        public tokens: {
106✔
2416
            enum: Token;
2417
            name: Identifier;
2418
            endEnum: Token;
2419
        },
2420
        public body: Array<EnumMemberStatement | CommentStatement>
106✔
2421
    ) {
2422
        super();
106✔
2423
        this.body = this.body ?? [];
106!
2424
    }
2425

2426
    public get range(): Range | undefined {
2427
        return util.createBoundingRange(
15✔
2428
            this.tokens.enum,
2429
            this.tokens.name,
2430
            ...this.body,
2431
            this.tokens.endEnum
2432
        );
2433
    }
2434

2435
    /**
2436
     * Get the name of the wrapping namespace (if it exists)
2437
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
2438
     */
2439
    public get namespaceName() {
2440
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
2441
    }
2442

2443
    public getMembers() {
2444
        const result = [] as EnumMemberStatement[];
136✔
2445
        for (const statement of this.body) {
136✔
2446
            if (isEnumMemberStatement(statement)) {
300✔
2447
                result.push(statement);
287✔
2448
            }
2449
        }
2450
        return result;
136✔
2451
    }
2452

2453
    /**
2454
     * Get a map of member names and their values.
2455
     * All values are stored as their AST LiteralExpression representation (i.e. string enum values include the wrapping quotes)
2456
     */
2457
    public getMemberValueMap() {
2458
        const result = new Map<string, string>();
55✔
2459
        const members = this.getMembers();
55✔
2460
        let currentIntValue = 0;
55✔
2461
        for (const member of members) {
55✔
2462
            //if there is no value, assume an integer and increment the int counter
2463
            if (!member.value) {
141✔
2464
                result.set(member.name?.toLowerCase(), currentIntValue.toString());
32!
2465
                currentIntValue++;
32✔
2466

2467
                //if explicit integer value, use it and increment the int counter
2468
            } else if (isLiteralExpression(member.value) && member.value.token.kind === TokenKind.IntegerLiteral) {
109✔
2469
                //try parsing as integer literal, then as hex integer literal.
2470
                let tokenIntValue = util.parseInt(member.value.token.text) ?? util.parseInt(member.value.token.text.replace(/&h/i, '0x'));
31✔
2471
                if (tokenIntValue !== undefined) {
31!
2472
                    currentIntValue = tokenIntValue;
31✔
2473
                    currentIntValue++;
31✔
2474
                }
2475
                result.set(member.name?.toLowerCase(), member.value.token.text);
31!
2476

2477
                //simple unary expressions (like `-1`)
2478
            } else if (isUnaryExpression(member.value) && isLiteralExpression(member.value.right)) {
78✔
2479
                result.set(member.name?.toLowerCase(), member.value.operator.text + member.value.right.token.text);
1!
2480

2481
                //all other values
2482
            } else {
2483
                result.set(member.name?.toLowerCase(), (member.value as LiteralExpression)?.token?.text ?? 'invalid');
77!
2484
            }
2485
        }
2486
        return result;
55✔
2487
    }
2488

2489
    public getMemberValue(name: string) {
2490
        return this.getMemberValueMap().get(name.toLowerCase());
52✔
2491
    }
2492

2493
    /**
2494
     * The name of the enum (without the namespace prefix)
2495
     */
2496
    public get name() {
2497
        return this.tokens.name?.text;
16!
2498
    }
2499

2500
    /**
2501
     * The name of the enum WITH its leading namespace (if applicable)
2502
     */
2503
    public get fullName() {
2504
        const name = this.tokens.name?.text;
276!
2505
        if (name) {
276!
2506
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
276✔
2507

2508
            if (namespace) {
276✔
2509
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
126✔
2510
                return `${namespaceName}.${name}`;
126✔
2511
            } else {
2512
                return name;
150✔
2513
            }
2514
        } else {
2515
            //return undefined which will allow outside callers to know that this doesn't have a name
2516
            return undefined;
×
2517
        }
2518
    }
2519

2520
    transpile(state: BrsTranspileState) {
2521
        //enum declarations do not exist at runtime, so don't transpile anything...
2522
        return [];
23✔
2523
    }
2524

2525
    getTypedef(state: BrsTranspileState) {
2526
        const result = [] as TranspileResult;
1✔
2527
        for (let annotation of this.annotations ?? []) {
1!
2528
            result.push(
×
2529
                ...annotation.getTypedef(state),
2530
                state.newline,
2531
                state.indent()
2532
            );
2533
        }
2534
        result.push(
1✔
2535
            this.tokens.enum.text ?? 'enum',
3!
2536
            ' ',
2537
            this.tokens.name.text
2538
        );
2539
        result.push(state.newline);
1✔
2540
        state.blockDepth++;
1✔
2541
        for (const member of this.body) {
1✔
2542
            if (isTypedefProvider(member)) {
1!
2543
                result.push(
1✔
2544
                    state.indent(),
2545
                    ...member.getTypedef(state),
2546
                    state.newline
2547
                );
2548
            }
2549
        }
2550
        state.blockDepth--;
1✔
2551
        result.push(
1✔
2552
            state.indent(),
2553
            this.tokens.endEnum.text ?? 'end enum'
3!
2554
        );
2555
        return result;
1✔
2556
    }
2557

2558
    walk(visitor: WalkVisitor, options: WalkOptions) {
2559
        if (options.walkMode & InternalWalkMode.walkStatements) {
316!
2560
            walkArray(this.body, visitor, options, this);
316✔
2561

2562
        }
2563
    }
2564
}
2565

2566
export class EnumMemberStatement extends Statement implements TypedefProvider {
1✔
2567

2568
    public constructor(
2569
        public tokens: {
192✔
2570
            name: Identifier;
2571
            equal?: Token;
2572
        },
2573
        public value?: Expression
192✔
2574
    ) {
2575
        super();
192✔
2576
    }
2577

2578
    /**
2579
     * The name of the member
2580
     */
2581
    public get name() {
2582
        return this.tokens.name.text;
545✔
2583
    }
2584

2585
    public get range() {
2586
        return util.createBoundingRange(
64✔
2587
            this.tokens.name,
2588
            this.tokens.equal,
2589
            this.value
2590
        );
2591
    }
2592

2593
    /**
2594
     * Get the value of this enum. Requires that `.parent` is set
2595
     */
2596
    public getValue() {
2597
        return (this.parent as EnumStatement).getMemberValue(this.name);
52✔
2598
    }
2599

2600
    public transpile(state: BrsTranspileState): TranspileResult {
2601
        return [];
×
2602
    }
2603

2604
    getTypedef(state: BrsTranspileState): TranspileResult {
2605
        const result = [
1✔
2606
            this.tokens.name.text
2607
        ] as TranspileResult;
2608
        if (this.tokens.equal) {
1!
2609
            result.push(' ', this.tokens.equal.text, ' ');
×
2610
            if (this.value) {
×
2611
                result.push(
×
2612
                    ...this.value.transpile(state)
2613
                );
2614
            }
2615
        }
2616
        return result;
1✔
2617
    }
2618

2619
    walk(visitor: WalkVisitor, options: WalkOptions) {
2620
        if (this.value && options.walkMode & InternalWalkMode.walkExpressions) {
419✔
2621
            walk(this, 'value', visitor, options);
247✔
2622
        }
2623
    }
2624
}
2625

2626
export class ConstStatement extends Statement implements TypedefProvider {
1✔
2627

2628
    public constructor(
2629
        public tokens: {
50✔
2630
            const: Token;
2631
            name: Identifier;
2632
            equals: Token;
2633
        },
2634
        public value: Expression
50✔
2635
    ) {
2636
        super();
50✔
2637
        this.range = util.createBoundingRange(this.tokens.const, this.tokens.name, this.tokens.equals, this.value);
50✔
2638
    }
2639

2640
    public range: Range | undefined;
2641

2642
    public get name() {
2643
        return this.tokens.name.text;
3✔
2644
    }
2645

2646
    /**
2647
     * The name of the statement WITH its leading namespace (if applicable)
2648
     */
2649
    public get fullName() {
2650
        const name = this.tokens.name?.text;
72!
2651
        if (name) {
72!
2652
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
72✔
2653
            if (namespace) {
72✔
2654
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
53✔
2655
                return `${namespaceName}.${name}`;
53✔
2656
            } else {
2657
                return name;
19✔
2658
            }
2659
        } else {
2660
            //return undefined which will allow outside callers to know that this doesn't have a name
2661
            return undefined;
×
2662
        }
2663
    }
2664

2665
    public transpile(state: BrsTranspileState): TranspileResult {
2666
        //const declarations don't exist at runtime, so just transpile empty
2667
        return [];
13✔
2668
    }
2669

2670
    getTypedef(state: BrsTranspileState): TranspileResult {
2671
        return [
3✔
2672
            state.tokenToSourceNode(this.tokens.const),
2673
            ' ',
2674
            state.tokenToSourceNode(this.tokens.name),
2675
            ' ',
2676
            state.tokenToSourceNode(this.tokens.equals),
2677
            ' ',
2678
            ...this.value.transpile(state)
2679
        ];
2680
    }
2681

2682
    walk(visitor: WalkVisitor, options: WalkOptions) {
2683
        if (this.value && options.walkMode & InternalWalkMode.walkExpressions) {
105✔
2684
            walk(this, 'value', visitor, options);
103✔
2685
        }
2686
    }
2687
}
2688

2689
export class ContinueStatement extends Statement {
1✔
2690
    constructor(
2691
        public tokens: {
11✔
2692
            continue: Token;
2693
            loopType: Token;
2694
        }
2695
    ) {
2696
        super();
11✔
2697
        this.range = util.createBoundingRange(
11✔
2698
            tokens.continue,
2699
            tokens.loopType
2700
        );
2701
    }
2702

2703
    public range: Range | undefined;
2704

2705
    transpile(state: BrsTranspileState) {
2706
        return [
3✔
2707
            state.sourceNode(this.tokens.continue, this.tokens.continue?.text ?? 'continue'),
18!
2708
            this.tokens.loopType?.leadingWhitespace ?? ' ',
18✔
2709
            state.sourceNode(this.tokens.continue, this.tokens.loopType?.text)
9✔
2710
        ];
2711
    }
2712
    walk(visitor: WalkVisitor, options: WalkOptions) {
2713
        //nothing to walk
2714
    }
2715
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc