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

rokucommunity / brighterscript / #15048

01 Jan 2026 11:17PM UTC coverage: 87.048% (-0.9%) from 87.907%
#15048

push

web-flow
Merge 02ba2bb57 into 2ea4d2108

14498 of 17595 branches covered (82.4%)

Branch coverage included in aggregate %.

192 of 261 new or added lines in 12 files covered. (73.56%)

897 existing lines in 48 files now uncovered.

15248 of 16577 relevant lines covered (91.98%)

24112.76 hits per line

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

88.18
/src/parser/Statement.ts
1
/* eslint-disable no-bitwise */
2
import type { Token, Identifier } from '../lexer/Token';
3
import { TokenKind } from '../lexer/TokenKind';
1✔
4
import type { DottedGetExpression, FunctionParameterExpression, LiteralExpression, TypecastExpression, TypeExpression } from './Expression';
5
import { FunctionExpression } from './Expression';
1✔
6
import { CallExpression, VariableExpression } from './Expression';
1✔
7
import { util } from '../util';
1✔
8
import type { Location } from 'vscode-languageserver';
9
import type { BrsTranspileState } from './BrsTranspileState';
10
import { ParseMode } from './Parser';
1✔
11
import type { WalkVisitor, WalkOptions } from '../astUtils/visitors';
12
import { InternalWalkMode, walk, createVisitor, WalkMode, walkArray } from '../astUtils/visitors';
1✔
13
import { isCallExpression, isCatchStatement, isConditionalCompileStatement, isEnumMemberStatement, isExpressionStatement, isFieldStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isMethodStatement, isNamespaceStatement, isPrintSeparatorExpression, isTryCatchStatement, isTypedefProvider, isUnaryExpression, isUninitializedType, isVoidType, isWhileStatement } from '../astUtils/reflection';
1✔
14
import type { GetTypeOptions } from '../interfaces';
15
import { TypeChainEntry, type TranspileResult, type TypedefProvider } from '../interfaces';
1✔
16
import { createIdentifier, createInvalidLiteral, createMethodStatement, createToken } from '../astUtils/creators';
1✔
17
import { DynamicType } from '../types/DynamicType';
1✔
18
import type { BscType } from '../types/BscType';
19
import { SymbolTable } from '../SymbolTable';
1✔
20
import type { Expression } from './AstNode';
21
import { AstNodeKind, Statement } from './AstNode';
1✔
22
import { ClassType } from '../types/ClassType';
1✔
23
import { EnumMemberType, EnumType } from '../types/EnumType';
1✔
24
import { NamespaceType } from '../types/NamespaceType';
1✔
25
import { InterfaceType } from '../types/InterfaceType';
1✔
26
import { VoidType } from '../types/VoidType';
1✔
27
import { TypedFunctionType } from '../types/TypedFunctionType';
1✔
28
import { ArrayType } from '../types/ArrayType';
1✔
29
import { SymbolTypeFlag } from '../SymbolTypeFlag';
1✔
30
import brsDocParser from './BrightScriptDocParser';
1✔
31
export class EmptyStatement extends Statement {
1✔
32
    constructor(options?: { range?: Location }
33
    ) {
34
        super();
6✔
35
        this.location = undefined;
6✔
36
    }
37
    /**
38
     * Create a negative range to indicate this is an interpolated location
39
     */
40
    public readonly location?: Location;
41

42
    public readonly kind = AstNodeKind.EmptyStatement;
6✔
43

44
    transpile(state: BrsTranspileState) {
45
        return [];
2✔
46
    }
47
    walk(visitor: WalkVisitor, options: WalkOptions) {
48
        //nothing to walk
49
    }
50

51
    public clone() {
52
        return this.finalizeClone(
1✔
53
            new EmptyStatement({
54
                range: util.cloneLocation(this.location)
55
            })
56
        );
57
    }
58
}
59

60
/**
61
 * This is a top-level statement. Consider this the root of the AST
62
 */
63
export class Body extends Statement implements TypedefProvider {
1✔
64
    constructor(options?: {
65
        statements?: Statement[];
66
    }) {
67
        super();
9,040✔
68
        this.statements = options?.statements ?? [];
9,040!
69
    }
70

71
    public readonly statements: Statement[] = [];
9,040✔
72
    public readonly kind = AstNodeKind.Body;
9,040✔
73

74
    public readonly symbolTable = new SymbolTable('Body', () => this.parent?.getSymbolTable());
37,733✔
75

76
    public get location() {
77
        if (!this._location) {
1,013✔
78
            //this needs to be a getter because the body has its statements pushed to it after being constructed
79
            this._location = util.createBoundingLocation(
827✔
80
                ...(this.statements ?? [])
2,481✔
81
            );
82
        }
83
        return this._location;
1,013✔
84
    }
85
    public set location(value) {
UNCOV
86
        this._location = value;
×
87
    }
88
    private _location: Location;
89

90
    transpile(state: BrsTranspileState) {
91
        let result: TranspileResult = state.transpileAnnotations(this);
815✔
92
        for (let i = 0; i < this.statements.length; i++) {
815✔
93
            let statement = this.statements[i];
1,792✔
94
            let previousStatement = this.statements[i - 1];
1,792✔
95
            let nextStatement = this.statements[i + 1];
1,792✔
96

97
            if (!previousStatement) {
1,792✔
98
                //this is the first statement. do nothing related to spacing and newlines
99

100
                //if comment is on same line as prior sibling
101
            } else if (util.hasLeadingComments(statement) && previousStatement && util.getLeadingComments(statement)?.[0]?.location?.range?.start.line === previousStatement.location?.range?.end.line) {
982!
102
                result.push(
8✔
103
                    ' '
104
                );
105
                //add double newline if this is a comment, and next is a function
106
            } else if (util.hasLeadingComments(statement) && nextStatement && isFunctionStatement(nextStatement)) {
974✔
107
                result.push(state.newline, state.newline);
383✔
108

109
                //add double newline if is function not preceeded by a comment
110
            } else if (isFunctionStatement(statement) && previousStatement && !util.hasLeadingComments(statement)) {
591✔
111
                result.push(state.newline, state.newline);
109✔
112
            } else {
113
                //separate statements by a single newline
114
                result.push(state.newline);
482✔
115
            }
116

117
            result.push(...statement.transpile(state));
1,792✔
118
        }
119
        return result;
815✔
120
    }
121

122
    getTypedef(state: BrsTranspileState): TranspileResult {
123
        let result = [] as TranspileResult;
44✔
124
        for (const statement of this.statements) {
44✔
125
            //if the current statement supports generating typedef, call it
126
            if (isTypedefProvider(statement)) {
82!
127
                result.push(
82✔
128
                    state.indent(),
129
                    ...statement.getTypedef(state),
130
                    state.newline
131
                );
132
            }
133
        }
134
        return result;
44✔
135
    }
136

137
    walk(visitor: WalkVisitor, options: WalkOptions) {
138
        if (options.walkMode & InternalWalkMode.walkStatements) {
18,188!
139
            walkArray(this.statements, visitor, options, this);
18,188✔
140
        }
141
    }
142

143
    public clone() {
144
        return this.finalizeClone(
136✔
145
            new Body({
146
                statements: this.statements?.map(s => s?.clone())
143✔
147
            }),
148
            ['statements']
149
        );
150
    }
151
}
152

153
export class AssignmentStatement extends Statement {
1✔
154
    constructor(options: {
155
        name: Identifier;
156
        equals?: Token;
157
        value: Expression;
158
        as?: Token;
159
        typeExpression?: TypeExpression;
160
    }) {
161
        super();
1,764✔
162
        this.value = options.value;
1,764✔
163
        this.tokens = {
1,764✔
164
            equals: options.equals,
165
            name: options.name,
166
            as: options.as
167
        };
168
        this.typeExpression = options.typeExpression;
1,764✔
169
        this.location = util.createBoundingLocation(util.createBoundingLocationFromTokens(this.tokens), this.value);
1,764✔
170
    }
171

172
    public readonly tokens: {
173
        readonly equals?: Token;
174
        readonly name: Identifier;
175
        readonly as?: Token;
176
    };
177

178
    public readonly value: Expression;
179

180
    public readonly typeExpression?: TypeExpression;
181

182
    public readonly kind = AstNodeKind.AssignmentStatement;
1,764✔
183

184
    public readonly location: Location | undefined;
185

186
    transpile(state: BrsTranspileState) {
187
        return [
650✔
188
            state.transpileToken(this.tokens.name),
189
            ' ',
190
            state.transpileToken(this.tokens.equals ?? createToken(TokenKind.Equal), '='),
1,950!
191
            ' ',
192
            ...this.value.transpile(state)
193
        ];
194
    }
195

196
    walk(visitor: WalkVisitor, options: WalkOptions) {
197
        if (options.walkMode & InternalWalkMode.walkExpressions) {
7,854✔
198
            walk(this, 'typeExpression', visitor, options);
7,701✔
199
            walk(this, 'value', visitor, options);
7,701✔
200
        }
201
    }
202

203
    getType(options: GetTypeOptions) {
204
        const variableTypeFromCode = this.typeExpression?.getType({ ...options, typeChain: undefined });
1,683✔
205
        const docs = brsDocParser.parseNode(this);
1,683✔
206
        const variableTypeFromDocs = docs?.getTypeTagBscType(options);
1,683!
207
        const variableType = util.chooseTypeFromCodeOrDocComment(variableTypeFromCode, variableTypeFromDocs, options) ?? this.value.getType({ ...options, typeChain: undefined });
1,683✔
208

209
        // Note: compound assignments (eg. +=) are internally dealt with via the RHS being a BinaryExpression
210
        // so this.value will be a BinaryExpression, and BinaryExpressions can figure out their own types
211
        options.typeChain?.push(new TypeChainEntry({ name: this.tokens.name.text, type: variableType, data: options.data, location: this.tokens.name?.location, astNode: this }));
1,683!
212
        return variableType;
1,683✔
213
    }
214

215
    get leadingTrivia(): Token[] {
216
        return this.tokens.name.leadingTrivia;
9,614✔
217
    }
218

219
    public clone() {
220
        return this.finalizeClone(
16✔
221
            new AssignmentStatement({
222
                name: util.cloneToken(this.tokens.name),
223
                value: this.value?.clone(),
48✔
224
                as: util.cloneToken(this.tokens.as),
225
                equals: util.cloneToken(this.tokens.equals),
226
                typeExpression: this.typeExpression?.clone()
48!
227
            }),
228
            ['value', 'typeExpression']
229
        );
230
    }
231
}
232

233
export class AugmentedAssignmentStatement extends Statement {
1✔
234
    constructor(options: {
235
        item: Expression;
236
        operator: Token;
237
        value: Expression;
238
    }) {
239
        super();
107✔
240
        this.value = options.value;
107✔
241
        this.tokens = {
107✔
242
            operator: options.operator
243
        };
244
        this.item = options.item;
107✔
245
        this.value = options.value;
107✔
246
        this.location = util.createBoundingLocation(this.item, util.createBoundingLocationFromTokens(this.tokens), this.value);
107✔
247
    }
248

249
    public readonly tokens: {
250
        readonly operator?: Token;
251
    };
252

253
    public readonly item: Expression;
254

255
    public readonly value: Expression;
256

257
    public readonly kind = AstNodeKind.AugmentedAssignmentStatement;
107✔
258

259
    public readonly location: Location | undefined;
260

261
    transpile(state: BrsTranspileState) {
262
        return [
35✔
263
            this.item.transpile(state),
264
            ' ',
265
            state.transpileToken(this.tokens.operator),
266
            ' ',
267
            this.value.transpile(state)
268
        ];
269
    }
270

271
    walk(visitor: WalkVisitor, options: WalkOptions) {
272
        if (options.walkMode & InternalWalkMode.walkExpressions) {
491✔
273
            walk(this, 'item', visitor, options);
482✔
274
            walk(this, 'value', visitor, options);
482✔
275
        }
276
    }
277

278
    getType(options: GetTypeOptions) {
UNCOV
279
        const variableType = util.binaryOperatorResultType(this.item.getType(options), this.tokens.operator, this.value.getType(options));
×
280

281
        //const variableType = this.typeExpression?.getType({ ...options, typeChain: undefined }) ?? this.value.getType({ ...options, typeChain: undefined });
282

283
        // Note: compound assignments (eg. +=) are internally dealt with via the RHS being a BinaryExpression
284
        // so this.value will be a BinaryExpression, and BinaryExpressions can figure out their own types
285
        // options.typeChain?.push(new TypeChainEntry({ name: this.tokens.name.text, type: variableType, data: options.data, range: this.tokens.name.range, astNode: this }));
UNCOV
286
        return variableType;
×
287
    }
288

289
    get leadingTrivia(): Token[] {
290
        return this.item.leadingTrivia;
487✔
291
    }
292

293
    public clone() {
294
        return this.finalizeClone(
2✔
295
            new AugmentedAssignmentStatement({
296
                item: this.item?.clone(),
6!
297
                operator: util.cloneToken(this.tokens.operator),
298
                value: this.value?.clone()
6!
299
            }),
300
            ['item', 'value']
301
        );
302
    }
303
}
304

305
export class Block extends Statement {
1✔
306
    constructor(options: {
307
        statements: Statement[];
308
    }) {
309
        super();
7,855✔
310
        this.statements = options.statements;
7,855✔
311
        this.symbolTable = new SymbolTable('Block', () => this.parent.getSymbolTable());
50,877✔
312
    }
313

314
    public readonly statements: Statement[];
315

316
    public readonly kind = AstNodeKind.Block;
7,855✔
317

318
    private buildLocation(): Location {
319
        if (this.statements?.length > 0) {
4,020✔
320
            return util.createBoundingLocation(...this.statements ?? []);
3,740!
321
        }
322
        let lastBitBefore: Location;
323
        let firstBitAfter: Location;
324

325
        if (isFunctionExpression(this.parent)) {
280✔
326
            lastBitBefore = util.createBoundingLocation(
128✔
327
                this.parent.tokens.functionType,
328
                this.parent.tokens.leftParen,
329
                ...(this.parent.parameters ?? []),
384!
330
                this.parent.tokens.rightParen,
331
                this.parent.tokens.as,
332
                this.parent.returnTypeExpression
333
            );
334
            firstBitAfter = this.parent.tokens.endFunctionType?.location;
128!
335
        } else if (isIfStatement(this.parent)) {
152✔
336
            if (this.parent.thenBranch === this) {
18✔
337
                lastBitBefore = util.createBoundingLocation(
16✔
338
                    this.parent.tokens.then,
339
                    this.parent.condition
340
                );
341
                firstBitAfter = util.createBoundingLocation(
16✔
342
                    this.parent.tokens.else,
343
                    this.parent.elseBranch,
344
                    this.parent.tokens.endIf
345
                );
346
            } else if (this.parent.elseBranch === this) {
2!
347
                lastBitBefore = this.parent.tokens.else?.location;
2!
348
                firstBitAfter = this.parent.tokens.endIf?.location;
2!
349
            }
350
        } else if (isConditionalCompileStatement(this.parent)) {
134✔
351
            if (this.parent.thenBranch === this) {
3!
352
                lastBitBefore = util.createBoundingLocation(
3✔
353
                    this.parent.tokens.condition,
354
                    this.parent.tokens.not,
355
                    this.parent.tokens.hashIf
356
                );
357
                firstBitAfter = util.createBoundingLocation(
3✔
358
                    this.parent.tokens.hashElse,
359
                    this.parent.elseBranch,
360
                    this.parent.tokens.hashEndIf
361
                );
UNCOV
362
            } else if (this.parent.elseBranch === this) {
×
UNCOV
363
                lastBitBefore = this.parent.tokens.hashElse?.location;
×
UNCOV
364
                firstBitAfter = this.parent.tokens.hashEndIf?.location;
×
365
            }
366
        } else if (isForStatement(this.parent)) {
131✔
367
            lastBitBefore = util.createBoundingLocation(
4✔
368
                this.parent.increment,
369
                this.parent.tokens.step,
370
                this.parent.finalValue,
371
                this.parent.tokens.to,
372
                this.parent.counterDeclaration,
373
                this.parent.tokens.for
374
            );
375
            firstBitAfter = this.parent.tokens.endFor?.location;
4!
376
        } else if (isForEachStatement(this.parent)) {
127✔
377
            lastBitBefore = util.createBoundingLocation(
3✔
378
                this.parent.target,
379
                this.parent.tokens.in,
380
                this.parent.tokens.item,
381
                this.parent.tokens.forEach
382
            );
383
            firstBitAfter = this.parent.tokens.endFor?.location;
3!
384
        } else if (isWhileStatement(this.parent)) {
124✔
385
            lastBitBefore = util.createBoundingLocation(
2✔
386
                this.parent.condition,
387
                this.parent.tokens.while
388
            );
389
            firstBitAfter = this.parent.tokens.endWhile?.location;
2!
390
        } else if (isTryCatchStatement(this.parent)) {
122✔
391
            lastBitBefore = util.createBoundingLocation(
1✔
392
                this.parent.tokens.try
393
            );
394
            firstBitAfter = util.createBoundingLocation(
1✔
395
                this.parent.tokens.endTry,
396
                this.parent.catchStatement
397
            );
398
        } else if (isCatchStatement(this.parent) && isTryCatchStatement(this.parent?.parent)) {
121!
399
            lastBitBefore = util.createBoundingLocation(
6✔
400
                this.parent.tokens.catch,
401
                this.parent.exceptionVariableExpression
402
            );
403
            firstBitAfter = this.parent.parent.tokens.endTry?.location;
6!
404
        }
405
        if (lastBitBefore?.range && firstBitAfter?.range) {
280✔
406
            return util.createLocation(
139✔
407
                lastBitBefore.range.end.line,
408
                lastBitBefore.range.end.character,
409
                firstBitAfter.range.start.line,
410
                firstBitAfter.range.start.character,
411
                lastBitBefore.uri ?? firstBitAfter.uri
417✔
412
            );
413
        }
414
    }
415

416
    public get location() {
417
        if (!this._location) {
5,572✔
418
            //this needs to be a getter because the body has its statements pushed to it after being constructed
419
            this._location = this.buildLocation();
4,020✔
420
        }
421
        return this._location;
5,572✔
422
    }
423
    public set location(value) {
424
        this._location = value;
21✔
425
    }
426
    private _location: Location;
427

428
    transpile(state: BrsTranspileState) {
429
        state.blockDepth++;
4,846✔
430
        let results = [] as TranspileResult;
4,846✔
431
        for (let i = 0; i < this.statements.length; i++) {
4,846✔
432
            let previousStatement = this.statements[i - 1];
5,842✔
433
            let statement = this.statements[i];
5,842✔
434
            //is not a comment
435
            //if comment is on same line as parent
436
            if (util.isLeadingCommentOnSameLine(state.lineage[0]?.location, statement) ||
5,842!
437
                util.isLeadingCommentOnSameLine(previousStatement?.location, statement)
17,478✔
438
            ) {
439
                results.push(' ');
50✔
440

441
                //is not a comment
442
            } else {
443
                //add a newline and indent
444
                results.push(
5,792✔
445
                    state.newline,
446
                    state.indent()
447
                );
448
            }
449

450
            //push block onto parent list
451
            state.lineage.unshift(this);
5,842✔
452
            results.push(
5,842✔
453
                ...statement.transpile(state)
454
            );
455
            state.lineage.shift();
5,842✔
456
        }
457
        state.blockDepth--;
4,846✔
458
        return results;
4,846✔
459
    }
460

461
    public get leadingTrivia(): Token[] {
462
        return this.statements[0]?.leadingTrivia ?? [];
12,651✔
463
    }
464

465
    walk(visitor: WalkVisitor, options: WalkOptions) {
466
        if (options.walkMode & InternalWalkMode.walkStatements) {
35,480✔
467
            walkArray(this.statements, visitor, options, this);
35,474✔
468
        }
469
    }
470

471
    public clone() {
472
        return this.finalizeClone(
127✔
473
            new Block({
474
                statements: this.statements?.map(s => s?.clone())
117✔
475
            }),
476
            ['statements']
477
        );
478
    }
479
}
480

481
export class ExpressionStatement extends Statement {
1✔
482
    constructor(options: {
483
        expression: Expression;
484
    }) {
485
        super();
995✔
486
        this.expression = options.expression;
995✔
487
        this.location = this.expression?.location;
995✔
488
    }
489
    public readonly expression: Expression;
490
    public readonly kind = AstNodeKind.ExpressionStatement;
995✔
491

492
    public readonly location: Location | undefined;
493

494
    transpile(state: BrsTranspileState) {
495
        return [
71✔
496
            state.transpileAnnotations(this),
497
            this.expression.transpile(state)
498
        ];
499
    }
500

501
    walk(visitor: WalkVisitor, options: WalkOptions) {
502
        if (options.walkMode & InternalWalkMode.walkExpressions) {
3,890✔
503
            walk(this, 'expression', visitor, options);
3,861✔
504
        }
505
    }
506

507
    get leadingTrivia(): Token[] {
508
        return this.expression.leadingTrivia;
4,092✔
509
    }
510

511
    public clone() {
512
        return this.finalizeClone(
14✔
513
            new ExpressionStatement({
514
                expression: this.expression?.clone()
42✔
515
            }),
516
            ['expression']
517
        );
518
    }
519
}
520

521
export class ExitStatement extends Statement {
1✔
522
    constructor(options?: {
523
        exit?: Token;
524
        loopType: Token;
525
    }) {
526
        super();
26✔
527
        this.tokens = {
26✔
528
            exit: options?.exit,
78!
529
            loopType: options.loopType
530
        };
531
        this.location = util.createBoundingLocation(
26✔
532
            this.tokens.exit,
533
            this.tokens.loopType
534
        );
535
    }
536

537
    public readonly tokens: {
538
        readonly exit: Token;
539
        readonly loopType?: Token;
540
    };
541

542
    public readonly kind = AstNodeKind.ExitStatement;
26✔
543

544
    public readonly location?: Location;
545

546
    transpile(state: BrsTranspileState) {
547
        return [
11✔
548
            state.transpileToken(this.tokens.exit, 'exit'),
549
            this.tokens.loopType?.leadingWhitespace ?? ' ',
66!
550
            state.transpileToken(this.tokens.loopType)
551
        ];
552
    }
553

554
    walk(visitor: WalkVisitor, options: WalkOptions) {
555
        //nothing to walk
556
    }
557

558
    get leadingTrivia(): Token[] {
559
        return this.tokens.exit?.leadingTrivia;
124!
560
    }
561

562
    public clone() {
563
        return this.finalizeClone(
2✔
564
            new ExitStatement({
565
                loopType: util.cloneToken(this.tokens.loopType),
566
                exit: util.cloneToken(this.tokens.exit)
567
            })
568
        );
569
    }
570
}
571

572
export class FunctionStatement extends Statement implements TypedefProvider {
1✔
573
    constructor(options: {
574
        name: Identifier;
575
        func: FunctionExpression;
576
    }) {
577
        super();
4,660✔
578
        this.tokens = {
4,660✔
579
            name: options.name
580
        };
581
        this.func = options.func;
4,660✔
582
        if (this.func) {
4,660✔
583
            this.func.symbolTable.name += `: '${this.tokens.name?.text}'`;
4,658!
584
        }
585

586
        this.location = this.func?.location;
4,660✔
587
    }
588

589
    public readonly tokens: {
590
        readonly name: Identifier;
591
    };
592
    public readonly func: FunctionExpression;
593

594
    public readonly kind = AstNodeKind.FunctionStatement as AstNodeKind;
4,660✔
595

596
    public readonly location: Location | undefined;
597

598
    /**
599
     * Get the name of this expression based on the parse mode
600
     */
601
    public getName(parseMode: ParseMode) {
602
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
15,057✔
603
        if (namespace) {
15,057✔
604
            let delimiter = parseMode === ParseMode.BrighterScript ? '.' : '_';
3,248✔
605
            let namespaceName = namespace.getName(parseMode);
3,248✔
606
            return namespaceName + delimiter + this.tokens.name?.text;
3,248!
607
        } else {
608
            return this.tokens.name.text;
11,809✔
609
        }
610
    }
611

612
    public get leadingTrivia(): Token[] {
613
        return this.func.leadingTrivia;
20,060✔
614
    }
615

616
    transpile(state: BrsTranspileState): TranspileResult {
617
        //create a fake token using the full transpiled name
618
        let nameToken = {
1,536✔
619
            ...this.tokens.name,
620
            text: this.getName(ParseMode.BrightScript)
621
        };
622

623
        return [
1,536✔
624
            ...state.transpileAnnotations(this),
625
            ...this.func.transpile(state, nameToken)
626
        ];
627
    }
628

629
    getTypedef(state: BrsTranspileState) {
630
        let result: TranspileResult = [];
45✔
631
        for (let comment of util.getLeadingComments(this) ?? []) {
45!
632
            result.push(
168✔
633
                comment.text,
634
                state.newline,
635
                state.indent()
636
            );
637
        }
638
        for (let annotation of this.annotations ?? []) {
45✔
639
            result.push(
2✔
640
                ...annotation.getTypedef(state),
641
                state.newline,
642
                state.indent()
643
            );
644
        }
645

646
        result.push(
45✔
647
            ...this.func.getTypedef(state)
648
        );
649
        return result;
45✔
650
    }
651

652
    walk(visitor: WalkVisitor, options: WalkOptions) {
653
        if (options.walkMode & InternalWalkMode.walkExpressions) {
20,866✔
654
            walk(this, 'func', visitor, options);
18,679✔
655
        }
656
    }
657

658
    getType(options: GetTypeOptions) {
659
        const funcExprType = this.func.getType(options);
4,140✔
660
        funcExprType.setName(this.getName(ParseMode.BrighterScript));
4,140✔
661
        return funcExprType;
4,140✔
662
    }
663

664
    public clone() {
665
        return this.finalizeClone(
107✔
666
            new FunctionStatement({
667
                func: this.func?.clone(),
321✔
668
                name: util.cloneToken(this.tokens.name)
669
            }),
670
            ['func']
671
        );
672
    }
673
}
674

675
export class IfStatement extends Statement {
1✔
676
    constructor(options: {
677
        if?: Token;
678
        then?: Token;
679
        else?: Token;
680
        endIf?: Token;
681

682
        condition: Expression;
683
        thenBranch: Block;
684
        elseBranch?: IfStatement | Block;
685
    }) {
686
        super();
2,384✔
687
        this.condition = options.condition;
2,384✔
688
        this.thenBranch = options.thenBranch;
2,384✔
689
        this.elseBranch = options.elseBranch;
2,384✔
690

691
        this.tokens = {
2,384✔
692
            if: options.if,
693
            then: options.then,
694
            else: options.else,
695
            endIf: options.endIf
696
        };
697

698
        this.location = util.createBoundingLocation(
2,384✔
699
            util.createBoundingLocationFromTokens(this.tokens),
700
            this.condition,
701
            this.thenBranch,
702
            this.elseBranch
703
        );
704
    }
705

706
    readonly tokens: {
707
        readonly if?: Token;
708
        readonly then?: Token;
709
        readonly else?: Token;
710
        readonly endIf?: Token;
711
    };
712
    public readonly condition: Expression;
713
    public readonly thenBranch: Block;
714
    public readonly elseBranch?: IfStatement | Block;
715

716
    public readonly kind = AstNodeKind.IfStatement;
2,384✔
717

718
    public readonly location: Location | undefined;
719

720
    get isInline() {
721
        const allLeadingTrivia = [
12✔
722
            ...this.thenBranch.leadingTrivia,
723
            ...this.thenBranch.statements.map(s => s.leadingTrivia).flat(),
16✔
724
            ...(this.tokens.else?.leadingTrivia ?? []),
72✔
725
            ...(this.tokens.endIf?.leadingTrivia ?? [])
72✔
726
        ];
727

728
        const hasNewline = allLeadingTrivia.find(t => t.kind === TokenKind.Newline);
29✔
729
        return !hasNewline;
12✔
730
    }
731

732
    transpile(state: BrsTranspileState) {
733
        let results = [] as TranspileResult;
2,379✔
734
        //if   (already indented by block)
735
        results.push(state.transpileToken(this.tokens.if ?? createToken(TokenKind.If)));
2,379!
736
        results.push(' ');
2,379✔
737
        //conditions
738
        results.push(...this.condition.transpile(state));
2,379✔
739
        //then
740
        if (this.tokens.then) {
2,379✔
741
            results.push(' ');
1,984✔
742
            results.push(
1,984✔
743
                state.transpileToken(this.tokens.then, 'then')
744
            );
745
        }
746
        state.lineage.unshift(this);
2,379✔
747

748
        //if statement body
749
        let thenNodes = this.thenBranch.transpile(state);
2,379✔
750
        state.lineage.shift();
2,379✔
751
        if (thenNodes.length > 0) {
2,379✔
752
            results.push(thenNodes);
2,368✔
753
        }
754
        //else branch
755
        if (this.elseBranch) {
2,379✔
756
            //else
757
            results.push(...state.transpileEndBlockToken(this.thenBranch, this.tokens.else, 'else'));
1,976✔
758

759
            if (isIfStatement(this.elseBranch)) {
1,976✔
760
                //chained elseif
761
                state.lineage.unshift(this.elseBranch);
1,171✔
762
                let body = this.elseBranch.transpile(state);
1,171✔
763
                state.lineage.shift();
1,171✔
764

765
                if (body.length > 0) {
1,171!
766
                    //zero or more spaces between the `else` and the `if`
767
                    results.push(this.elseBranch.tokens.if.leadingWhitespace!);
1,171✔
768
                    results.push(...body);
1,171✔
769

770
                    // stop here because chained if will transpile the rest
771
                    return results;
1,171✔
772
                } else {
UNCOV
773
                    results.push('\n');
×
774
                }
775

776
            } else {
777
                //else body
778
                state.lineage.unshift(this.tokens.else!);
805✔
779
                let body = this.elseBranch.transpile(state);
805✔
780
                state.lineage.shift();
805✔
781

782
                if (body.length > 0) {
805✔
783
                    results.push(...body);
803✔
784
                }
785
            }
786
        }
787

788
        //end if
789
        results.push(...state.transpileEndBlockToken(this.elseBranch ?? this.thenBranch, this.tokens.endIf, 'end if'));
1,208✔
790

791
        return results;
1,208✔
792
    }
793

794
    walk(visitor: WalkVisitor, options: WalkOptions) {
795
        if (options.walkMode & InternalWalkMode.walkExpressions) {
10,033✔
796
            walk(this, 'condition', visitor, options);
10,016✔
797
        }
798
        if (options.walkMode & InternalWalkMode.walkStatements) {
10,033✔
799
            walk(this, 'thenBranch', visitor, options);
10,031✔
800
        }
801
        if (this.elseBranch && options.walkMode & InternalWalkMode.walkStatements) {
10,033✔
802
            walk(this, 'elseBranch', visitor, options);
7,863✔
803
        }
804
    }
805

806
    get leadingTrivia(): Token[] {
807
        return this.tokens.if?.leadingTrivia ?? [];
3,436!
808
    }
809

810
    get endTrivia(): Token[] {
811
        return this.tokens.endIf?.leadingTrivia ?? [];
19✔
812
    }
813

814
    public clone() {
815
        return this.finalizeClone(
5✔
816
            new IfStatement({
817
                if: util.cloneToken(this.tokens.if),
818
                else: util.cloneToken(this.tokens.else),
819
                endIf: util.cloneToken(this.tokens.endIf),
820
                then: util.cloneToken(this.tokens.then),
821
                condition: this.condition?.clone(),
15✔
822
                thenBranch: this.thenBranch?.clone(),
15✔
823
                elseBranch: this.elseBranch?.clone()
15✔
824
            }),
825
            ['condition', 'thenBranch', 'elseBranch']
826
        );
827
    }
828
}
829

830
export class IncrementStatement extends Statement {
1✔
831
    constructor(options: {
832
        value: Expression;
833
        operator: Token;
834
    }) {
835
        super();
29✔
836
        this.value = options.value;
29✔
837
        this.tokens = {
29✔
838
            operator: options.operator
839
        };
840
        this.location = util.createBoundingLocation(
29✔
841
            this.value,
842
            this.tokens.operator
843
        );
844
    }
845

846
    public readonly value: Expression;
847
    public readonly tokens: {
848
        readonly operator: Token;
849
    };
850

851
    public readonly kind = AstNodeKind.IncrementStatement;
29✔
852

853
    public readonly location: Location | undefined;
854

855
    transpile(state: BrsTranspileState) {
856
        return [
6✔
857
            ...this.value.transpile(state),
858
            state.transpileToken(this.tokens.operator)
859
        ];
860
    }
861

862
    walk(visitor: WalkVisitor, options: WalkOptions) {
863
        if (options.walkMode & InternalWalkMode.walkExpressions) {
94✔
864
            walk(this, 'value', visitor, options);
93✔
865
        }
866
    }
867

868
    get leadingTrivia(): Token[] {
869
        return this.value?.leadingTrivia ?? [];
94!
870
    }
871

872
    public clone() {
873
        return this.finalizeClone(
2✔
874
            new IncrementStatement({
875
                value: this.value?.clone(),
6✔
876
                operator: util.cloneToken(this.tokens.operator)
877
            }),
878
            ['value']
879
        );
880
    }
881
}
882

883
/**
884
 * Represents a `print` statement within BrightScript.
885
 */
886
export class PrintStatement extends Statement {
1✔
887
    /**
888
     * Creates a new internal representation of a BrightScript `print` statement.
889
     * @param options the options for this statement
890
     * @param options.print a print token
891
     * @param options.expressions an array of expressions to be evaluated and printed. Wrap PrintSeparator tokens (`;` or `,`) in `PrintSeparatorExpression`
892
     */
893
    constructor(options: {
894
        print?: Token;
895
        expressions: Array<Expression>;
896
    }) {
897
        super();
1,443✔
898
        this.tokens = {
1,443✔
899
            print: options.print
900
        };
901
        this.expressions = options.expressions;
1,443✔
902
        this.location = util.createBoundingLocation(
1,443✔
903
            this.tokens.print,
904
            ...(this.expressions ?? [])
4,329✔
905
        );
906
    }
907

908
    public readonly tokens: {
909
        readonly print?: Token;
910
    };
911

912
    public readonly expressions: Array<Expression>;
913

914
    public readonly kind = AstNodeKind.PrintStatement;
1,443✔
915

916
    public readonly location: Location | undefined;
917

918
    transpile(state: BrsTranspileState) {
919
        let result = [
247✔
920
            state.transpileToken(this.tokens.print, 'print')
921
        ] as TranspileResult;
922

923
        //if the first expression has no leading whitespace, add a single space between the `print` and the expression
924
        if (this.expressions.length > 0 && !this.expressions[0].leadingTrivia.find(t => t?.kind === TokenKind.Whitespace)) {
247✔
925
            result.push(' ');
11✔
926
        }
927

928
        // eslint-disable-next-line @typescript-eslint/prefer-for-of
929
        for (let i = 0; i < this.expressions.length; i++) {
247✔
930
            const expression = this.expressions[i];
311✔
931
            let leadingWhitespace = expression.leadingTrivia.find(t => t?.kind === TokenKind.Whitespace)?.text;
311✔
932
            if (leadingWhitespace) {
311✔
933
                result.push(leadingWhitespace);
260✔
934
                //if the previous expression was NOT a separator, and this one is not also, add a space between them
935
            } else if (i > 0 && !isPrintSeparatorExpression(this.expressions[i - 1]) && !isPrintSeparatorExpression(expression) && !leadingWhitespace) {
51✔
936
                result.push(' ');
6✔
937
            }
938

939
            result.push(
311✔
940
                ...expression.transpile(state)
941
            );
942
        }
943
        return result;
247✔
944
    }
945

946
    walk(visitor: WalkVisitor, options: WalkOptions) {
947
        if (options.walkMode & InternalWalkMode.walkExpressions) {
6,889✔
948
            walkArray(this.expressions, visitor, options, this);
6,801✔
949
        }
950
    }
951

952
    get leadingTrivia(): Token[] {
953
        return this.tokens.print?.leadingTrivia ?? [];
8,612✔
954
    }
955

956
    public clone() {
957
        return this.finalizeClone(
49✔
958
            new PrintStatement({
959
                print: util.cloneToken(this.tokens.print),
960
                expressions: this.expressions?.map(e => e?.clone())
49✔
961
            }),
962
            ['expressions' as any]
963
        );
964
    }
965
}
966

967
export class DimStatement extends Statement {
1✔
968
    constructor(options: {
969
        dim?: Token;
970
        name: Identifier;
971
        openingSquare?: Token;
972
        dimensions: Expression[];
973
        closingSquare?: Token;
974
    }) {
975
        super();
46✔
976
        this.tokens = {
46✔
977
            dim: options?.dim,
138!
978
            name: options.name,
979
            openingSquare: options.openingSquare,
980
            closingSquare: options.closingSquare
981
        };
982
        this.dimensions = options.dimensions;
46✔
983
        this.location = util.createBoundingLocation(
46✔
984
            options.dim,
985
            options.name,
986
            options.openingSquare,
987
            ...(this.dimensions ?? []),
138✔
988
            options.closingSquare
989
        );
990
    }
991

992
    public readonly tokens: {
993
        readonly dim?: Token;
994
        readonly name: Identifier;
995
        readonly openingSquare?: Token;
996
        readonly closingSquare?: Token;
997
    };
998
    public readonly dimensions: Expression[];
999

1000
    public readonly kind = AstNodeKind.DimStatement;
46✔
1001

1002
    public readonly location: Location | undefined;
1003

1004
    public transpile(state: BrsTranspileState) {
1005
        let result: TranspileResult = [
15✔
1006
            state.transpileToken(this.tokens.dim, 'dim'),
1007
            ' ',
1008
            state.transpileToken(this.tokens.name),
1009
            state.transpileToken(this.tokens.openingSquare, '[')
1010
        ];
1011
        for (let i = 0; i < this.dimensions.length; i++) {
15✔
1012
            if (i > 0) {
32✔
1013
                result.push(', ');
17✔
1014
            }
1015
            result.push(
32✔
1016
                ...this.dimensions![i].transpile(state)
1017
            );
1018
        }
1019
        result.push(state.transpileToken(this.tokens.closingSquare, ']'));
15✔
1020
        return result;
15✔
1021
    }
1022

1023
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1024
        if (this.dimensions?.length !== undefined && this.dimensions?.length > 0 && options.walkMode & InternalWalkMode.walkExpressions) {
148!
1025
            walkArray(this.dimensions, visitor, options, this);
133✔
1026

1027
        }
1028
    }
1029

1030
    public getType(options: GetTypeOptions): BscType {
1031
        const numDimensions = this.dimensions?.length ?? 1;
18!
1032
        let type = new ArrayType();
18✔
1033
        for (let i = 0; i < numDimensions - 1; i++) {
18✔
1034
            type = new ArrayType(type);
17✔
1035
        }
1036
        return type;
18✔
1037
    }
1038

1039
    get leadingTrivia(): Token[] {
1040
        return this.tokens.dim?.leadingTrivia ?? [];
137!
1041
    }
1042

1043
    public clone() {
1044
        return this.finalizeClone(
3✔
1045
            new DimStatement({
1046
                dim: util.cloneToken(this.tokens.dim),
1047
                name: util.cloneToken(this.tokens.name),
1048
                openingSquare: util.cloneToken(this.tokens.openingSquare),
1049
                dimensions: this.dimensions?.map(e => e?.clone()),
5✔
1050
                closingSquare: util.cloneToken(this.tokens.closingSquare)
1051
            }),
1052
            ['dimensions']
1053
        );
1054
    }
1055
}
1056

1057
export class GotoStatement extends Statement {
1✔
1058
    constructor(options: {
1059
        goto?: Token;
1060
        label: Token;
1061
    }) {
1062
        super();
13✔
1063
        this.tokens = {
13✔
1064
            goto: options.goto,
1065
            label: options.label
1066
        };
1067
        this.location = util.createBoundingLocation(
13✔
1068
            this.tokens.goto,
1069
            this.tokens.label
1070
        );
1071
    }
1072

1073
    public readonly tokens: {
1074
        readonly goto?: Token;
1075
        readonly label: Token;
1076
    };
1077

1078
    public readonly kind = AstNodeKind.GotoStatement;
13✔
1079

1080
    public readonly location: Location | undefined;
1081

1082
    transpile(state: BrsTranspileState) {
1083
        return [
2✔
1084
            state.transpileToken(this.tokens.goto, 'goto'),
1085
            ' ',
1086
            state.transpileToken(this.tokens.label)
1087
        ];
1088
    }
1089

1090
    walk(visitor: WalkVisitor, options: WalkOptions) {
1091
        //nothing to walk
1092
    }
1093

1094
    get leadingTrivia(): Token[] {
1095
        return this.tokens.goto?.leadingTrivia ?? [];
19!
1096
    }
1097

1098
    public clone() {
1099
        return this.finalizeClone(
1✔
1100
            new GotoStatement({
1101
                goto: util.cloneToken(this.tokens.goto),
1102
                label: util.cloneToken(this.tokens.label)
1103
            })
1104
        );
1105
    }
1106
}
1107

1108
export class LabelStatement extends Statement {
1✔
1109
    constructor(options: {
1110
        name: Token;
1111
        colon?: Token;
1112
    }) {
1113
        super();
13✔
1114
        this.tokens = {
13✔
1115
            name: options.name,
1116
            colon: options.colon
1117
        };
1118
        this.location = util.createBoundingLocation(
13✔
1119
            this.tokens.name,
1120
            this.tokens.colon
1121
        );
1122
    }
1123
    public readonly tokens: {
1124
        readonly name: Token;
1125
        readonly colon: Token;
1126
    };
1127
    public readonly kind = AstNodeKind.LabelStatement;
13✔
1128

1129
    public readonly location: Location | undefined;
1130

1131
    public get leadingTrivia(): Token[] {
1132
        return this.tokens.name.leadingTrivia;
29✔
1133
    }
1134

1135
    transpile(state: BrsTranspileState) {
1136
        return [
2✔
1137
            state.transpileToken(this.tokens.name),
1138
            state.transpileToken(this.tokens.colon, ':')
1139

1140
        ];
1141
    }
1142

1143
    walk(visitor: WalkVisitor, options: WalkOptions) {
1144
        //nothing to walk
1145
    }
1146

1147
    public clone() {
1148
        return this.finalizeClone(
1✔
1149
            new LabelStatement({
1150
                name: util.cloneToken(this.tokens.name),
1151
                colon: util.cloneToken(this.tokens.colon)
1152
            })
1153
        );
1154
    }
1155
}
1156

1157
export class ReturnStatement extends Statement {
1✔
1158
    constructor(options?: {
1159
        return?: Token;
1160
        value?: Expression;
1161
    }) {
1162
        super();
3,725✔
1163
        this.tokens = {
3,725✔
1164
            return: options?.return
11,175!
1165
        };
1166
        this.value = options?.value;
3,725!
1167
        this.location = util.createBoundingLocation(
3,725✔
1168
            this.tokens.return,
1169
            this.value
1170
        );
1171
    }
1172

1173
    public readonly tokens: {
1174
        readonly return?: Token;
1175
    };
1176
    public readonly value?: Expression;
1177
    public readonly kind = AstNodeKind.ReturnStatement;
3,725✔
1178

1179
    public readonly location: Location | undefined;
1180

1181
    transpile(state: BrsTranspileState) {
1182
        let result = [] as TranspileResult;
3,524✔
1183
        result.push(
3,524✔
1184
            state.transpileToken(this.tokens.return, 'return')
1185
        );
1186
        if (this.value) {
3,524✔
1187
            result.push(' ');
3,519✔
1188
            result.push(...this.value.transpile(state));
3,519✔
1189
        }
1190
        return result;
3,524✔
1191
    }
1192

1193
    walk(visitor: WalkVisitor, options: WalkOptions) {
1194
        if (options.walkMode & InternalWalkMode.walkExpressions) {
16,448✔
1195
            walk(this, 'value', visitor, options);
16,425✔
1196
        }
1197
    }
1198

1199
    get leadingTrivia(): Token[] {
1200
        return this.tokens.return?.leadingTrivia ?? [];
11,597!
1201
    }
1202

1203
    public clone() {
1204
        return this.finalizeClone(
3✔
1205
            new ReturnStatement({
1206
                return: util.cloneToken(this.tokens.return),
1207
                value: this.value?.clone()
9✔
1208
            }),
1209
            ['value']
1210
        );
1211
    }
1212
}
1213

1214
export class EndStatement extends Statement {
1✔
1215
    constructor(options?: {
1216
        end?: Token;
1217
    }) {
1218
        super();
11✔
1219
        this.tokens = {
11✔
1220
            end: options?.end
33!
1221
        };
1222
        this.location = this.tokens.end?.location;
11!
1223
    }
1224
    public readonly tokens: {
1225
        readonly end?: Token;
1226
    };
1227
    public readonly kind = AstNodeKind.EndStatement;
11✔
1228

1229
    public readonly location: Location;
1230

1231
    transpile(state: BrsTranspileState) {
1232
        return [
2✔
1233
            state.transpileToken(this.tokens.end, 'end')
1234
        ];
1235
    }
1236

1237
    walk(visitor: WalkVisitor, options: WalkOptions) {
1238
        //nothing to walk
1239
    }
1240

1241
    get leadingTrivia(): Token[] {
1242
        return this.tokens.end?.leadingTrivia ?? [];
19!
1243
    }
1244

1245
    public clone() {
1246
        return this.finalizeClone(
1✔
1247
            new EndStatement({
1248
                end: util.cloneToken(this.tokens.end)
1249
            })
1250
        );
1251
    }
1252
}
1253

1254
export class StopStatement extends Statement {
1✔
1255
    constructor(options?: {
1256
        stop?: Token;
1257
    }) {
1258
        super();
19✔
1259
        this.tokens = { stop: options?.stop };
19!
1260
        this.location = this.tokens?.stop?.location;
19!
1261
    }
1262
    public readonly tokens: {
1263
        readonly stop?: Token;
1264
    };
1265

1266
    public readonly kind = AstNodeKind.StopStatement;
19✔
1267

1268
    public readonly location: Location;
1269

1270
    transpile(state: BrsTranspileState) {
1271
        return [
2✔
1272
            state.transpileToken(this.tokens.stop, 'stop')
1273
        ];
1274
    }
1275

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

1280
    get leadingTrivia(): Token[] {
1281
        return this.tokens.stop?.leadingTrivia ?? [];
29!
1282
    }
1283

1284
    public clone() {
1285
        return this.finalizeClone(
1✔
1286
            new StopStatement({
1287
                stop: util.cloneToken(this.tokens.stop)
1288
            })
1289
        );
1290
    }
1291
}
1292

1293
export class ForStatement extends Statement {
1✔
1294
    constructor(options: {
1295
        for?: Token;
1296
        counterDeclaration: AssignmentStatement;
1297
        to?: Token;
1298
        finalValue: Expression;
1299
        body: Block;
1300
        endFor?: Token;
1301
        step?: Token;
1302
        increment?: Expression;
1303
    }) {
1304
        super();
47✔
1305
        this.tokens = {
47✔
1306
            for: options.for,
1307
            to: options.to,
1308
            endFor: options.endFor,
1309
            step: options.step
1310
        };
1311
        this.counterDeclaration = options.counterDeclaration;
47✔
1312
        this.finalValue = options.finalValue;
47✔
1313
        this.body = options.body;
47✔
1314
        this.increment = options.increment;
47✔
1315

1316
        this.location = util.createBoundingLocation(
47✔
1317
            this.tokens.for,
1318
            this.counterDeclaration,
1319
            this.tokens.to,
1320
            this.finalValue,
1321
            this.tokens.step,
1322
            this.increment,
1323
            this.body,
1324
            this.tokens.endFor
1325
        );
1326
    }
1327

1328
    public readonly tokens: {
1329
        readonly for?: Token;
1330
        readonly to?: Token;
1331
        readonly endFor?: Token;
1332
        readonly step?: Token;
1333
    };
1334

1335
    public readonly counterDeclaration: AssignmentStatement;
1336
    public readonly finalValue: Expression;
1337
    public readonly body: Block;
1338
    public readonly increment?: Expression;
1339

1340
    public readonly kind = AstNodeKind.ForStatement;
47✔
1341

1342
    public readonly location: Location | undefined;
1343

1344
    transpile(state: BrsTranspileState) {
1345
        let result = [] as TranspileResult;
11✔
1346
        //for
1347
        result.push(
11✔
1348
            state.transpileToken(this.tokens.for, 'for'),
1349
            ' '
1350
        );
1351
        //i=1
1352
        result.push(
11✔
1353
            ...this.counterDeclaration.transpile(state),
1354
            ' '
1355
        );
1356
        //to
1357
        result.push(
11✔
1358
            state.transpileToken(this.tokens.to, 'to'),
1359
            ' '
1360
        );
1361
        //final value
1362
        result.push(this.finalValue.transpile(state));
11✔
1363
        //step
1364
        if (this.increment) {
11✔
1365
            result.push(
4✔
1366
                ' ',
1367
                state.transpileToken(this.tokens.step, 'step'),
1368
                ' ',
1369
                this.increment!.transpile(state)
1370
            );
1371
        }
1372
        //loop body
1373
        state.lineage.unshift(this);
11✔
1374
        result.push(...this.body.transpile(state));
11✔
1375
        state.lineage.shift();
11✔
1376

1377
        //end for
1378
        result.push(...state.transpileEndBlockToken(this.body, this.tokens.endFor, 'end for'));
11✔
1379

1380
        return result;
11✔
1381
    }
1382

1383
    walk(visitor: WalkVisitor, options: WalkOptions) {
1384
        if (options.walkMode & InternalWalkMode.walkStatements) {
152✔
1385
            walk(this, 'counterDeclaration', visitor, options);
151✔
1386
        }
1387
        if (options.walkMode & InternalWalkMode.walkExpressions) {
152✔
1388
            walk(this, 'finalValue', visitor, options);
148✔
1389
            walk(this, 'increment', visitor, options);
148✔
1390
        }
1391
        if (options.walkMode & InternalWalkMode.walkStatements) {
152✔
1392
            walk(this, 'body', visitor, options);
151✔
1393
        }
1394
    }
1395

1396
    get leadingTrivia(): Token[] {
1397
        return this.tokens.for?.leadingTrivia ?? [];
152!
1398
    }
1399

1400
    public get endTrivia(): Token[] {
UNCOV
1401
        return this.tokens.endFor?.leadingTrivia ?? [];
×
1402
    }
1403

1404
    public clone() {
1405
        return this.finalizeClone(
5✔
1406
            new ForStatement({
1407
                for: util.cloneToken(this.tokens.for),
1408
                counterDeclaration: this.counterDeclaration?.clone(),
15✔
1409
                to: util.cloneToken(this.tokens.to),
1410
                finalValue: this.finalValue?.clone(),
15✔
1411
                body: this.body?.clone(),
15✔
1412
                endFor: util.cloneToken(this.tokens.endFor),
1413
                step: util.cloneToken(this.tokens.step),
1414
                increment: this.increment?.clone()
15✔
1415
            }),
1416
            ['counterDeclaration', 'finalValue', 'body', 'increment']
1417
        );
1418
    }
1419
}
1420

1421
export class ForEachStatement extends Statement {
1✔
1422
    constructor(options: {
1423
        forEach?: Token;
1424
        item: Token;
1425
        in?: Token;
1426
        target: Expression;
1427
        body: Block;
1428
        endFor?: Token;
1429
    }) {
1430
        super();
45✔
1431
        this.tokens = {
45✔
1432
            forEach: options.forEach,
1433
            item: options.item,
1434
            in: options.in,
1435
            endFor: options.endFor
1436
        };
1437
        this.body = options.body;
45✔
1438
        this.target = options.target;
45✔
1439

1440
        this.location = util.createBoundingLocation(
45✔
1441
            this.tokens.forEach,
1442
            this.tokens.item,
1443
            this.tokens.in,
1444
            this.target,
1445
            this.body,
1446
            this.tokens.endFor
1447
        );
1448
    }
1449

1450
    public readonly tokens: {
1451
        readonly forEach?: Token;
1452
        readonly item: Token;
1453
        readonly in?: Token;
1454
        readonly endFor?: Token;
1455
    };
1456
    public readonly body: Block;
1457
    public readonly target: Expression;
1458

1459
    public readonly kind = AstNodeKind.ForEachStatement;
45✔
1460

1461
    public readonly location: Location | undefined;
1462

1463
    transpile(state: BrsTranspileState) {
1464
        let result = [] as TranspileResult;
5✔
1465
        //for each
1466
        result.push(
5✔
1467
            state.transpileToken(this.tokens.forEach, 'for each'),
1468
            ' '
1469
        );
1470
        //item
1471
        result.push(
5✔
1472
            state.transpileToken(this.tokens.item),
1473
            ' '
1474
        );
1475
        //in
1476
        result.push(
5✔
1477
            state.transpileToken(this.tokens.in, 'in'),
1478
            ' '
1479
        );
1480
        //target
1481
        result.push(...this.target.transpile(state));
5✔
1482
        //body
1483
        state.lineage.unshift(this);
5✔
1484
        result.push(...this.body.transpile(state));
5✔
1485
        state.lineage.shift();
5✔
1486

1487
        //end for
1488
        result.push(...state.transpileEndBlockToken(this.body, this.tokens.endFor, 'end for'));
5✔
1489

1490
        return result;
5✔
1491
    }
1492

1493
    walk(visitor: WalkVisitor, options: WalkOptions) {
1494
        if (options.walkMode & InternalWalkMode.walkExpressions) {
211✔
1495
            walk(this, 'target', visitor, options);
204✔
1496
        }
1497
        if (options.walkMode & InternalWalkMode.walkStatements) {
211✔
1498
            walk(this, 'body', visitor, options);
210✔
1499
        }
1500
    }
1501

1502
    getType(options: GetTypeOptions): BscType {
1503
        return this.getSymbolTable().getSymbolType(this.tokens.item.text, { ...options, statementIndex: this.statementIndex });
35✔
1504
    }
1505

1506
    get leadingTrivia(): Token[] {
1507
        return this.tokens.forEach?.leadingTrivia ?? [];
211!
1508
    }
1509

1510
    public get endTrivia(): Token[] {
1511
        return this.tokens.endFor?.leadingTrivia ?? [];
1!
1512
    }
1513

1514
    public clone() {
1515
        return this.finalizeClone(
2✔
1516
            new ForEachStatement({
1517
                forEach: util.cloneToken(this.tokens.forEach),
1518
                in: util.cloneToken(this.tokens.in),
1519
                endFor: util.cloneToken(this.tokens.endFor),
1520
                item: util.cloneToken(this.tokens.item),
1521
                target: this.target?.clone(),
6✔
1522
                body: this.body?.clone()
6✔
1523
            }),
1524
            ['target', 'body']
1525
        );
1526
    }
1527
}
1528

1529
export class WhileStatement extends Statement {
1✔
1530
    constructor(options: {
1531
        while?: Token;
1532
        endWhile?: Token;
1533
        condition: Expression;
1534
        body: Block;
1535
    }) {
1536
        super();
38✔
1537
        this.tokens = {
38✔
1538
            while: options.while,
1539
            endWhile: options.endWhile
1540
        };
1541
        this.body = options.body;
38✔
1542
        this.condition = options.condition;
38✔
1543
        this.location = util.createBoundingLocation(
38✔
1544
            this.tokens.while,
1545
            this.condition,
1546
            this.body,
1547
            this.tokens.endWhile
1548
        );
1549
    }
1550

1551
    public readonly tokens: {
1552
        readonly while?: Token;
1553
        readonly endWhile?: Token;
1554
    };
1555
    public readonly condition: Expression;
1556
    public readonly body: Block;
1557

1558
    public readonly kind = AstNodeKind.WhileStatement;
38✔
1559

1560
    public readonly location: Location | undefined;
1561

1562
    transpile(state: BrsTranspileState) {
1563
        let result = [] as TranspileResult;
8✔
1564
        //while
1565
        result.push(
8✔
1566
            state.transpileToken(this.tokens.while, 'while'),
1567
            ' '
1568
        );
1569
        //condition
1570
        result.push(
8✔
1571
            ...this.condition.transpile(state)
1572
        );
1573
        state.lineage.unshift(this);
8✔
1574
        //body
1575
        result.push(...this.body.transpile(state));
8✔
1576
        state.lineage.shift();
8✔
1577

1578
        //end while
1579
        result.push(...state.transpileEndBlockToken(this.body, this.tokens.endWhile, 'end while'));
8✔
1580

1581
        return result;
8✔
1582
    }
1583

1584
    walk(visitor: WalkVisitor, options: WalkOptions) {
1585
        if (options.walkMode & InternalWalkMode.walkExpressions) {
128✔
1586
            walk(this, 'condition', visitor, options);
125✔
1587
        }
1588
        if (options.walkMode & InternalWalkMode.walkStatements) {
128✔
1589
            walk(this, 'body', visitor, options);
127✔
1590
        }
1591
    }
1592

1593
    get leadingTrivia(): Token[] {
1594
        return this.tokens.while?.leadingTrivia ?? [];
113!
1595
    }
1596

1597
    public get endTrivia(): Token[] {
1598
        return this.tokens.endWhile?.leadingTrivia ?? [];
1!
1599
    }
1600

1601
    public clone() {
1602
        return this.finalizeClone(
4✔
1603
            new WhileStatement({
1604
                while: util.cloneToken(this.tokens.while),
1605
                endWhile: util.cloneToken(this.tokens.endWhile),
1606
                condition: this.condition?.clone(),
12✔
1607
                body: this.body?.clone()
12✔
1608
            }),
1609
            ['condition', 'body']
1610
        );
1611
    }
1612
}
1613

1614
export class DottedSetStatement extends Statement {
1✔
1615
    constructor(options: {
1616
        obj: Expression;
1617
        name: Identifier;
1618
        value: Expression;
1619
        dot?: Token;
1620
        equals?: Token;
1621
    }) {
1622
        super();
320✔
1623
        this.tokens = {
320✔
1624
            name: options.name,
1625
            dot: options.dot,
1626
            equals: options.equals
1627
        };
1628
        this.obj = options.obj;
320✔
1629
        this.value = options.value;
320✔
1630
        this.location = util.createBoundingLocation(
320✔
1631
            this.obj,
1632
            this.tokens.dot,
1633
            this.tokens.equals,
1634
            this.tokens.name,
1635
            this.value
1636
        );
1637
    }
1638
    public readonly tokens: {
1639
        readonly name: Identifier;
1640
        readonly equals?: Token;
1641
        readonly dot?: Token;
1642
    };
1643

1644
    public readonly obj: Expression;
1645
    public readonly value: Expression;
1646

1647
    public readonly kind = AstNodeKind.DottedSetStatement;
320✔
1648

1649
    public readonly location: Location | undefined;
1650

1651
    transpile(state: BrsTranspileState) {
1652
        //if the value is a compound assignment, don't add the obj, dot, name, or operator...the expression will handle that
1653
        return [
15✔
1654
            //object
1655
            ...this.obj.transpile(state),
1656
            this.tokens.dot ? state.tokenToSourceNode(this.tokens.dot) : '.',
15✔
1657
            //name
1658
            state.transpileToken(this.tokens.name),
1659
            ' ',
1660
            state.transpileToken(this.tokens.equals, '='),
1661
            ' ',
1662
            //right-hand-side of assignment
1663
            ...this.value.transpile(state)
1664
        ];
1665

1666
    }
1667

1668
    walk(visitor: WalkVisitor, options: WalkOptions) {
1669
        if (options.walkMode & InternalWalkMode.walkExpressions) {
875✔
1670
            walk(this, 'obj', visitor, options);
873✔
1671
            walk(this, 'value', visitor, options);
873✔
1672
        }
1673
    }
1674

1675
    getType(options: GetTypeOptions) {
1676
        const objType = this.obj?.getType(options);
105!
1677
        const result = objType?.getMemberType(this.tokens.name?.text, options);
105!
1678
        options.typeChain?.push(new TypeChainEntry({
105✔
1679
            name: this.tokens.name?.text,
312!
1680
            type: result, data: options.data,
1681
            location: this.tokens.name?.location,
312!
1682
            astNode: this
1683
        }));
1684
        return result;
105✔
1685
    }
1686

1687
    get leadingTrivia(): Token[] {
1688
        return this.obj.leadingTrivia;
912✔
1689
    }
1690

1691
    public clone() {
1692
        return this.finalizeClone(
2✔
1693
            new DottedSetStatement({
1694
                obj: this.obj?.clone(),
6✔
1695
                dot: util.cloneToken(this.tokens.dot),
1696
                name: util.cloneToken(this.tokens.name),
1697
                equals: util.cloneToken(this.tokens.equals),
1698
                value: this.value?.clone()
6✔
1699
            }),
1700
            ['obj', 'value']
1701
        );
1702
    }
1703
}
1704

1705
export class IndexedSetStatement extends Statement {
1✔
1706
    constructor(options: {
1707
        obj: Expression;
1708
        indexes: Expression[];
1709
        value: Expression;
1710
        openingSquare?: Token;
1711
        closingSquare?: Token;
1712
        equals?: Token;
1713
    }) {
1714
        super();
47✔
1715
        this.tokens = {
47✔
1716
            openingSquare: options.openingSquare,
1717
            closingSquare: options.closingSquare,
1718
            equals: options.equals
1719
        };
1720
        this.obj = options.obj;
47✔
1721
        this.indexes = options.indexes ?? [];
47✔
1722
        this.value = options.value;
47✔
1723
        this.location = util.createBoundingLocation(
47✔
1724
            this.obj,
1725
            this.tokens.openingSquare,
1726
            ...this.indexes,
1727
            this.tokens.closingSquare,
1728
            this.value
1729
        );
1730
    }
1731

1732
    public readonly tokens: {
1733
        readonly openingSquare?: Token;
1734
        readonly closingSquare?: Token;
1735
        readonly equals?: Token;
1736
    };
1737
    public readonly obj: Expression;
1738
    public readonly indexes: Expression[];
1739
    public readonly value: Expression;
1740

1741
    public readonly kind = AstNodeKind.IndexedSetStatement;
47✔
1742

1743
    public readonly location: Location | undefined;
1744

1745
    transpile(state: BrsTranspileState) {
1746
        const result = [];
17✔
1747
        result.push(
17✔
1748
            //obj
1749
            ...this.obj.transpile(state),
1750
            //   [
1751
            state.transpileToken(this.tokens.openingSquare, '[')
1752
        );
1753
        for (let i = 0; i < this.indexes.length; i++) {
17✔
1754
            //add comma between indexes
1755
            if (i > 0) {
18✔
1756
                result.push(', ');
1✔
1757
            }
1758
            let index = this.indexes[i];
18✔
1759
            result.push(
18✔
1760
                ...(index?.transpile(state) ?? [])
108!
1761
            );
1762
        }
1763
        result.push(
17✔
1764
            state.transpileToken(this.tokens.closingSquare, ']'),
1765
            ' ',
1766
            state.transpileToken(this.tokens.equals, '='),
1767
            ' ',
1768
            ...this.value.transpile(state)
1769
        );
1770
        return result;
17✔
1771

1772
    }
1773

1774
    walk(visitor: WalkVisitor, options: WalkOptions) {
1775
        if (options.walkMode & InternalWalkMode.walkExpressions) {
166✔
1776
            walk(this, 'obj', visitor, options);
165✔
1777
            walkArray(this.indexes, visitor, options, this);
165✔
1778
            walk(this, 'value', visitor, options);
165✔
1779
        }
1780
    }
1781

1782
    get leadingTrivia(): Token[] {
1783
        return this.obj.leadingTrivia;
175✔
1784
    }
1785

1786
    public clone() {
1787
        return this.finalizeClone(
6✔
1788
            new IndexedSetStatement({
1789
                obj: this.obj?.clone(),
18✔
1790
                openingSquare: util.cloneToken(this.tokens.openingSquare),
1791
                indexes: this.indexes?.map(x => x?.clone()),
7✔
1792
                closingSquare: util.cloneToken(this.tokens.closingSquare),
1793
                equals: util.cloneToken(this.tokens.equals),
1794
                value: this.value?.clone()
18✔
1795
            }),
1796
            ['obj', 'indexes', 'value']
1797
        );
1798
    }
1799
}
1800

1801
export class LibraryStatement extends Statement implements TypedefProvider {
1✔
1802
    constructor(options: {
1803
        library: Token;
1804
        filePath?: Token;
1805
    }) {
1806
        super();
16✔
1807
        this.tokens = {
16✔
1808
            library: options?.library,
48!
1809
            filePath: options?.filePath
48!
1810
        };
1811
        this.location = util.createBoundingLocation(
16✔
1812
            this.tokens.library,
1813
            this.tokens.filePath
1814
        );
1815
    }
1816
    public readonly tokens: {
1817
        readonly library: Token;
1818
        readonly filePath?: Token;
1819
    };
1820

1821
    public readonly kind = AstNodeKind.LibraryStatement;
16✔
1822

1823
    public readonly location: Location | undefined;
1824

1825
    transpile(state: BrsTranspileState) {
1826
        let result = [] as TranspileResult;
2✔
1827
        result.push(
2✔
1828
            state.transpileToken(this.tokens.library)
1829
        );
1830
        //there will be a parse error if file path is missing, but let's prevent a runtime error just in case
1831
        if (this.tokens.filePath) {
2!
1832
            result.push(
2✔
1833
                ' ',
1834
                state.transpileToken(this.tokens.filePath)
1835
            );
1836
        }
1837
        return result;
2✔
1838
    }
1839

1840
    getTypedef(state: BrsTranspileState) {
UNCOV
1841
        return this.transpile(state);
×
1842
    }
1843

1844
    walk(visitor: WalkVisitor, options: WalkOptions) {
1845
        //nothing to walk
1846
    }
1847

1848
    get leadingTrivia(): Token[] {
1849
        return this.tokens.library?.leadingTrivia ?? [];
26!
1850
    }
1851

1852
    public clone() {
1853
        return this.finalizeClone(
1✔
1854
            new LibraryStatement({
1855
                library: util.cloneToken(this.tokens?.library),
3!
1856
                filePath: util.cloneToken(this.tokens?.filePath)
3!
1857
            })
1858
        );
1859
    }
1860
}
1861

1862
export class NamespaceStatement extends Statement implements TypedefProvider {
1✔
1863
    constructor(options: {
1864
        namespace?: Token;
1865
        nameExpression: VariableExpression | DottedGetExpression;
1866
        body: Body;
1867
        endNamespace?: Token;
1868
    }) {
1869
        super();
675✔
1870
        this.tokens = {
675✔
1871
            namespace: options.namespace,
1872
            endNamespace: options.endNamespace
1873
        };
1874
        this.nameExpression = options.nameExpression;
675✔
1875
        this.body = options.body;
675✔
1876
        this.symbolTable = new SymbolTable(`NamespaceStatement: '${this.name}'`, () => this.getRoot()?.getSymbolTable());
7,945!
1877
    }
1878

1879
    public readonly tokens: {
1880
        readonly namespace?: Token;
1881
        readonly endNamespace?: Token;
1882
    };
1883

1884
    public readonly nameExpression: VariableExpression | DottedGetExpression;
1885
    public readonly body: Body;
1886

1887
    public readonly kind = AstNodeKind.NamespaceStatement;
675✔
1888

1889
    /**
1890
     * The string name for this namespace
1891
     */
1892
    public get name(): string {
1893
        return this.getName(ParseMode.BrighterScript);
2,539✔
1894
    }
1895

1896
    public get location() {
1897
        return this.cacheLocation();
553✔
1898
    }
1899
    private _location: Location | undefined;
1900

1901
    public cacheLocation() {
1902
        if (!this._location) {
1,226✔
1903
            this._location = util.createBoundingLocation(
675✔
1904
                this.tokens.namespace,
1905
                this.nameExpression,
1906
                this.body,
1907
                this.tokens.endNamespace
1908
            );
1909
        }
1910
        return this._location;
1,226✔
1911
    }
1912

1913
    public getName(parseMode: ParseMode) {
1914
        const sep = parseMode === ParseMode.BrighterScript ? '.' : '_';
9,109✔
1915
        let name = util.getAllDottedGetPartsAsString(this.nameExpression, parseMode);
9,109✔
1916
        if ((this.parent as Body)?.parent?.kind === AstNodeKind.NamespaceStatement) {
9,109✔
1917
            name = (this.parent.parent as NamespaceStatement).getName(parseMode) + sep + name;
358✔
1918
        }
1919
        return name;
9,109✔
1920
    }
1921

1922
    public get leadingTrivia(): Token[] {
1923
        return this.tokens.namespace?.leadingTrivia;
1,973!
1924
    }
1925

1926
    public get endTrivia(): Token[] {
UNCOV
1927
        return this.tokens.endNamespace?.leadingTrivia;
×
1928
    }
1929

1930
    public getNameParts() {
1931
        let parts = util.getAllDottedGetParts(this.nameExpression);
1,508✔
1932

1933
        if ((this.parent as Body)?.parent?.kind === AstNodeKind.NamespaceStatement) {
1,508!
1934
            parts = (this.parent.parent as NamespaceStatement).getNameParts().concat(parts);
67✔
1935
        }
1936
        return parts;
1,508✔
1937
    }
1938

1939
    transpile(state: BrsTranspileState) {
1940
        //namespaces don't actually have any real content, so just transpile their bodies
1941
        return [
73✔
1942
            state.transpileAnnotations(this),
1943
            state.transpileLeadingComments(this.tokens.namespace),
1944
            this.body.transpile(state),
1945
            state.transpileLeadingComments(this.tokens.endNamespace)
1946
        ];
1947
    }
1948

1949
    getTypedef(state: BrsTranspileState) {
1950
        let result: TranspileResult = [];
8✔
1951
        for (let comment of util.getLeadingComments(this) ?? []) {
8!
UNCOV
1952
            result.push(
×
1953
                comment.text,
1954
                state.newline,
1955
                state.indent()
1956
            );
1957
        }
1958

1959
        result.push('namespace ',
8✔
1960
            ...this.getName(ParseMode.BrighterScript),
1961
            state.newline
1962
        );
1963
        state.blockDepth++;
8✔
1964
        result.push(
8✔
1965
            ...this.body.getTypedef(state)
1966
        );
1967
        state.blockDepth--;
8✔
1968

1969
        result.push(
8✔
1970
            state.indent(),
1971
            'end namespace'
1972
        );
1973
        return result;
8✔
1974
    }
1975

1976
    walk(visitor: WalkVisitor, options: WalkOptions) {
1977
        if (options.walkMode & InternalWalkMode.walkExpressions) {
3,443✔
1978
            walk(this, 'nameExpression', visitor, options);
2,820✔
1979
        }
1980

1981
        if (this.body.statements.length > 0 && options.walkMode & InternalWalkMode.walkStatements) {
3,443✔
1982
            walk(this, 'body', visitor, options);
3,240✔
1983
        }
1984
    }
1985

1986
    getType(options: GetTypeOptions) {
1987
        const resultType = new NamespaceType(this.name);
1,187✔
1988
        return resultType;
1,187✔
1989
    }
1990

1991
    public clone() {
1992
        const clone = this.finalizeClone(
3✔
1993
            new NamespaceStatement({
1994
                namespace: util.cloneToken(this.tokens.namespace),
1995
                nameExpression: this.nameExpression?.clone(),
9✔
1996
                body: this.body?.clone(),
9✔
1997
                endNamespace: util.cloneToken(this.tokens.endNamespace)
1998
            }),
1999
            ['nameExpression', 'body']
2000
        );
2001
        clone.cacheLocation();
3✔
2002
        return clone;
3✔
2003
    }
2004
}
2005

2006
export class ImportStatement extends Statement implements TypedefProvider {
1✔
2007
    constructor(options: {
2008
        import?: Token;
2009
        path?: Token;
2010
    }) {
2011
        super();
219✔
2012
        this.tokens = {
219✔
2013
            import: options.import,
2014
            path: options.path
2015
        };
2016
        this.location = util.createBoundingLocation(
219✔
2017
            this.tokens.import,
2018
            this.tokens.path
2019
        );
2020
        if (this.tokens.path) {
219✔
2021
            //remove quotes
2022
            this.filePath = this.tokens.path.text.replace(/"/g, '');
217✔
2023
            if (this.tokens.path?.location?.range) {
217!
2024
                //adjust the range to exclude the quotes
2025
                this.tokens.path.location = util.createLocation(
213✔
2026
                    this.tokens.path.location.range.start.line,
2027
                    this.tokens.path.location.range.start.character + 1,
2028
                    this.tokens.path.location.range.end.line,
2029
                    this.tokens.path.location.range.end.character - 1,
2030
                    this.tokens.path.location.uri
2031
                );
2032
            }
2033
        }
2034
    }
2035

2036
    public readonly tokens: {
2037
        readonly import?: Token;
2038
        readonly path: Token;
2039
    };
2040

2041
    public readonly kind = AstNodeKind.ImportStatement;
219✔
2042

2043
    public readonly location: Location;
2044

2045
    public readonly filePath: string;
2046

2047
    transpile(state: BrsTranspileState) {
2048
        //The xml files are responsible for adding the additional script imports, but
2049
        //add the import statement as a comment just for debugging purposes
2050
        return [
13✔
2051
            state.transpileToken(this.tokens.import, 'import', true),
2052
            ' ',
2053
            state.transpileToken(this.tokens.path)
2054
        ];
2055
    }
2056

2057
    /**
2058
     * Get the typedef for this statement
2059
     */
2060
    public getTypedef(state: BrsTranspileState) {
2061
        return [
3✔
2062
            this.tokens.import?.text ?? 'import',
18!
2063
            ' ',
2064
            //replace any `.bs` extension with `.brs`
2065
            this.tokens.path.text.replace(/\.bs"?$/i, '.brs"')
2066
        ];
2067
    }
2068

2069
    walk(visitor: WalkVisitor, options: WalkOptions) {
2070
        //nothing to walk
2071
    }
2072

2073
    get leadingTrivia(): Token[] {
2074
        return this.tokens.import?.leadingTrivia ?? [];
612!
2075
    }
2076

2077
    public clone() {
2078
        return this.finalizeClone(
1✔
2079
            new ImportStatement({
2080
                import: util.cloneToken(this.tokens.import),
2081
                path: util.cloneToken(this.tokens.path)
2082
            })
2083
        );
2084
    }
2085
}
2086

2087
export class InterfaceStatement extends Statement implements TypedefProvider {
1✔
2088
    constructor(options: {
2089
        interface: Token;
2090
        name: Identifier;
2091
        extends?: Token;
2092
        parentInterfaceName?: TypeExpression;
2093
        body: Statement[];
2094
        endInterface?: Token;
2095
    }) {
2096
        super();
213✔
2097
        this.tokens = {
213✔
2098
            interface: options.interface,
2099
            name: options.name,
2100
            extends: options.extends,
2101
            endInterface: options.endInterface
2102
        };
2103
        this.parentInterfaceName = options.parentInterfaceName;
213✔
2104
        this.body = options.body;
213✔
2105
        this.location = util.createBoundingLocation(
213✔
2106
            this.tokens.interface,
2107
            this.tokens.name,
2108
            this.tokens.extends,
2109
            this.parentInterfaceName,
2110
            ...this.body ?? [],
639✔
2111
            this.tokens.endInterface
2112
        );
2113
    }
2114
    public readonly parentInterfaceName?: TypeExpression;
2115
    public readonly body: Statement[];
2116

2117
    public readonly kind = AstNodeKind.InterfaceStatement;
213✔
2118

2119
    public readonly tokens = {} as {
213✔
2120
        readonly interface?: Token;
2121
        readonly name: Identifier;
2122
        readonly extends?: Token;
2123
        readonly endInterface?: Token;
2124
    };
2125

2126
    public readonly location: Location | undefined;
2127

2128
    public get fields(): InterfaceFieldStatement[] {
2129
        return this.body.filter(x => isInterfaceFieldStatement(x)) as InterfaceFieldStatement[];
260✔
2130
    }
2131

2132
    public get methods(): InterfaceMethodStatement[] {
2133
        return this.body.filter(x => isInterfaceMethodStatement(x)) as InterfaceMethodStatement[];
255✔
2134
    }
2135

2136

2137
    public hasParentInterface() {
UNCOV
2138
        return !!this.parentInterfaceName;
×
2139
    }
2140

2141
    public get leadingTrivia(): Token[] {
2142
        return this.tokens.interface?.leadingTrivia;
548!
2143
    }
2144

2145
    public get endTrivia(): Token[] {
UNCOV
2146
        return this.tokens.endInterface?.leadingTrivia;
×
2147
    }
2148

2149

2150
    /**
2151
     * The name of the interface WITH its leading namespace (if applicable)
2152
     */
2153
    public get fullName() {
UNCOV
2154
        const name = this.tokens.name?.text;
×
UNCOV
2155
        if (name) {
×
UNCOV
2156
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
×
UNCOV
2157
            if (namespace) {
×
UNCOV
2158
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
×
UNCOV
2159
                return `${namespaceName}.${name}`;
×
2160
            } else {
UNCOV
2161
                return name;
×
2162
            }
2163
        } else {
2164
            //return undefined which will allow outside callers to know that this interface doesn't have a name
UNCOV
2165
            return undefined;
×
2166
        }
2167
    }
2168

2169
    /**
2170
     * The name of the interface (without the namespace prefix)
2171
     */
2172
    public get name() {
2173
        return this.tokens.name?.text;
188!
2174
    }
2175

2176
    /**
2177
     * Get the name of this expression based on the parse mode
2178
     */
2179
    public getName(parseMode: ParseMode) {
2180
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
188✔
2181
        if (namespace) {
188✔
2182
            let delimiter = parseMode === ParseMode.BrighterScript ? '.' : '_';
25!
2183
            let namespaceName = namespace.getName(parseMode);
25✔
2184
            return namespaceName + delimiter + this.name;
25✔
2185
        } else {
2186
            return this.name;
163✔
2187
        }
2188
    }
2189

2190
    public transpile(state: BrsTranspileState): TranspileResult {
2191
        //interfaces should completely disappear at runtime
2192
        return [
14✔
2193
            state.transpileLeadingComments(this.tokens.interface)
2194
        ];
2195
    }
2196

2197
    getTypedef(state: BrsTranspileState) {
2198
        const result = [] as TranspileResult;
7✔
2199
        for (let comment of util.getLeadingComments(this) ?? []) {
7!
UNCOV
2200
            result.push(
×
2201
                comment.text,
2202
                state.newline,
2203
                state.indent()
2204
            );
2205
        }
2206
        for (let annotation of this.annotations ?? []) {
7✔
2207
            result.push(
1✔
2208
                ...annotation.getTypedef(state),
2209
                state.newline,
2210
                state.indent()
2211
            );
2212
        }
2213
        result.push(
7✔
2214
            this.tokens.interface.text,
2215
            ' ',
2216
            this.tokens.name.text
2217
        );
2218
        const parentInterfaceName = this.parentInterfaceName?.getName();
7!
2219
        if (parentInterfaceName) {
7!
UNCOV
2220
            result.push(
×
2221
                ' extends ',
2222
                parentInterfaceName
2223
            );
2224
        }
2225
        const body = this.body ?? [];
7!
2226
        if (body.length > 0) {
7!
2227
            state.blockDepth++;
7✔
2228
        }
2229
        for (const statement of body) {
7✔
2230
            if (isInterfaceMethodStatement(statement) || isInterfaceFieldStatement(statement)) {
22!
2231
                result.push(
22✔
2232
                    state.newline,
2233
                    state.indent(),
2234
                    ...statement.getTypedef(state)
2235
                );
2236
            } else {
UNCOV
2237
                result.push(
×
2238
                    state.newline,
2239
                    state.indent(),
2240
                    ...statement.transpile(state)
2241
                );
2242
            }
2243
        }
2244
        if (body.length > 0) {
7!
2245
            state.blockDepth--;
7✔
2246
        }
2247
        result.push(
7✔
2248
            state.newline,
2249
            state.indent(),
2250
            'end interface',
2251
            state.newline
2252
        );
2253
        return result;
7✔
2254
    }
2255

2256
    walk(visitor: WalkVisitor, options: WalkOptions) {
2257
        //visitor-less walk function to do parent linking
2258
        walk(this, 'parentInterfaceName', null, options);
945✔
2259

2260
        if (options.walkMode & InternalWalkMode.walkStatements) {
945!
2261
            walkArray(this.body, visitor, options, this);
945✔
2262
        }
2263
    }
2264

2265
    getType(options: GetTypeOptions) {
2266
        const superIface = this.parentInterfaceName?.getType(options) as InterfaceType;
179✔
2267

2268
        const resultType = new InterfaceType(this.getName(ParseMode.BrighterScript), superIface);
179✔
2269
        for (const statement of this.methods) {
179✔
2270
            const memberType = statement?.getType({ ...options, typeChain: undefined }); // no typechain info needed
30!
2271
            const flag = statement.isOptional ? SymbolTypeFlag.runtime | SymbolTypeFlag.optional : SymbolTypeFlag.runtime;
30✔
2272
            resultType.addMember(statement?.tokens.name?.text, { definingNode: statement }, memberType, flag);
30!
2273
        }
2274
        for (const statement of this.fields) {
179✔
2275
            const memberType = statement?.getType({ ...options, typeChain: undefined }); // no typechain info needed
220!
2276
            const flag = statement.isOptional ? SymbolTypeFlag.runtime | SymbolTypeFlag.optional : SymbolTypeFlag.runtime;
220✔
2277
            resultType.addMember(statement?.tokens.name?.text, { definingNode: statement, isInstance: true }, memberType, flag);
220!
2278
        }
2279
        options.typeChain?.push(new TypeChainEntry({
179✔
2280
            name: this.getName(ParseMode.BrighterScript),
2281
            type: resultType,
2282
            data: options.data,
2283
            astNode: this
2284
        }));
2285
        return resultType;
179✔
2286
    }
2287

2288
    public clone() {
2289
        return this.finalizeClone(
8✔
2290
            new InterfaceStatement({
2291
                interface: util.cloneToken(this.tokens.interface),
2292
                name: util.cloneToken(this.tokens.name),
2293
                extends: util.cloneToken(this.tokens.extends),
2294
                parentInterfaceName: this.parentInterfaceName?.clone(),
24✔
2295
                body: this.body?.map(x => x?.clone()),
9✔
2296
                endInterface: util.cloneToken(this.tokens.endInterface)
2297
            }),
2298
            ['parentInterfaceName', 'body']
2299
        );
2300
    }
2301
}
2302

2303
export class InterfaceFieldStatement extends Statement implements TypedefProvider {
1✔
2304
    public transpile(state: BrsTranspileState): TranspileResult {
UNCOV
2305
        throw new Error('Method not implemented.');
×
2306
    }
2307
    constructor(options: {
2308
        name: Identifier;
2309
        as?: Token;
2310
        typeExpression?: TypeExpression;
2311
        optional?: Token;
2312
    }) {
2313
        super();
242✔
2314
        this.tokens = {
242✔
2315
            optional: options.optional,
2316
            name: options.name,
2317
            as: options.as
2318
        };
2319
        this.typeExpression = options.typeExpression;
242✔
2320
        this.location = util.createBoundingLocation(
242✔
2321
            this.tokens.optional,
2322
            this.tokens.name,
2323
            this.tokens.as,
2324
            this.typeExpression
2325
        );
2326
    }
2327

2328
    public readonly kind = AstNodeKind.InterfaceFieldStatement;
242✔
2329

2330
    public readonly typeExpression?: TypeExpression;
2331

2332
    public readonly location: Location | undefined;
2333

2334
    public readonly tokens: {
2335
        readonly name: Identifier;
2336
        readonly as: Token;
2337
        readonly optional?: Token;
2338
    };
2339

2340
    public get leadingTrivia(): Token[] {
2341
        return this.tokens.optional?.leadingTrivia ?? this.tokens.name.leadingTrivia;
653✔
2342
    }
2343

2344
    public get name() {
UNCOV
2345
        return this.tokens.name.text;
×
2346
    }
2347

2348
    public get isOptional() {
2349
        return !!this.tokens.optional;
240✔
2350
    }
2351

2352
    walk(visitor: WalkVisitor, options: WalkOptions) {
2353
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,563✔
2354
            walk(this, 'typeExpression', visitor, options);
1,356✔
2355
        }
2356
    }
2357

2358
    getTypedef(state: BrsTranspileState): TranspileResult {
2359
        const result = [] as TranspileResult;
12✔
2360
        for (let comment of util.getLeadingComments(this) ?? []) {
12!
UNCOV
2361
            result.push(
×
2362
                comment.text,
2363
                state.newline,
2364
                state.indent()
2365
            );
2366
        }
2367
        for (let annotation of this.annotations ?? []) {
12✔
2368
            result.push(
1✔
2369
                ...annotation.getTypedef(state),
2370
                state.newline,
2371
                state.indent()
2372
            );
2373
        }
2374
        if (this.isOptional) {
12✔
2375
            result.push(
1✔
2376
                this.tokens.optional!.text,
2377
                ' '
2378
            );
2379
        }
2380
        result.push(
12✔
2381
            this.tokens.name.text
2382
        );
2383

2384
        if (this.typeExpression) {
12!
2385
            result.push(
12✔
2386
                ' as ',
2387
                ...this.typeExpression.getTypedef(state)
2388
            );
2389
        }
2390
        return result;
12✔
2391
    }
2392

2393
    public getType(options: GetTypeOptions): BscType {
2394
        return this.typeExpression?.getType(options) ?? DynamicType.instance;
231✔
2395
    }
2396

2397
    public clone() {
2398
        return this.finalizeClone(
4✔
2399
            new InterfaceFieldStatement({
2400
                name: util.cloneToken(this.tokens.name),
2401
                as: util.cloneToken(this.tokens.as),
2402
                typeExpression: this.typeExpression?.clone(),
12✔
2403
                optional: util.cloneToken(this.tokens.optional)
2404
            })
2405
        );
2406
    }
2407

2408
}
2409

2410
//TODO: there is much that is similar with this and FunctionExpression.
2411
//It would be nice to refactor this so there is less duplicated code
2412
export class InterfaceMethodStatement extends Statement implements TypedefProvider {
1✔
2413
    public transpile(state: BrsTranspileState): TranspileResult {
UNCOV
2414
        throw new Error('Method not implemented.');
×
2415
    }
2416
    constructor(options: {
2417
        functionType?: Token;
2418
        name: Identifier;
2419
        leftParen?: Token;
2420
        params?: FunctionParameterExpression[];
2421
        rightParen?: Token;
2422
        as?: Token;
2423
        returnTypeExpression?: TypeExpression;
2424
        optional?: Token;
2425
    }) {
2426
        super();
49✔
2427
        this.tokens = {
49✔
2428
            optional: options.optional,
2429
            functionType: options.functionType,
2430
            name: options.name,
2431
            leftParen: options.leftParen,
2432
            rightParen: options.rightParen,
2433
            as: options.as
2434
        };
2435
        this.params = options.params ?? [];
49✔
2436
        this.returnTypeExpression = options.returnTypeExpression;
49✔
2437
    }
2438

2439
    public readonly kind = AstNodeKind.InterfaceMethodStatement;
49✔
2440

2441
    public get location() {
2442
        return util.createBoundingLocation(
80✔
2443
            this.tokens.optional,
2444
            this.tokens.functionType,
2445
            this.tokens.name,
2446
            this.tokens.leftParen,
2447
            ...(this.params ?? []),
240!
2448
            this.tokens.rightParen,
2449
            this.tokens.as,
2450
            this.returnTypeExpression
2451
        );
2452
    }
2453
    /**
2454
     * Get the name of this method.
2455
     */
2456
    public getName(parseMode: ParseMode) {
2457
        return this.tokens.name.text;
30✔
2458
    }
2459

2460
    public readonly tokens: {
2461
        readonly optional?: Token;
2462
        readonly functionType: Token;
2463
        readonly name: Identifier;
2464
        readonly leftParen?: Token;
2465
        readonly rightParen?: Token;
2466
        readonly as?: Token;
2467
    };
2468

2469
    public readonly params: FunctionParameterExpression[];
2470
    public readonly returnTypeExpression?: TypeExpression;
2471

2472
    public get isOptional() {
2473
        return !!this.tokens.optional;
43✔
2474
    }
2475

2476
    public get leadingTrivia(): Token[] {
2477
        return this.tokens.optional?.leadingTrivia ?? this.tokens.functionType.leadingTrivia;
94✔
2478
    }
2479

2480
    walk(visitor: WalkVisitor, options: WalkOptions) {
2481
        if (options.walkMode & InternalWalkMode.walkExpressions) {
230✔
2482
            walk(this, 'returnTypeExpression', visitor, options);
203✔
2483
        }
2484
    }
2485

2486
    getTypedef(state: BrsTranspileState) {
2487
        const result = [] as TranspileResult;
10✔
2488
        for (let comment of util.getLeadingComments(this) ?? []) {
10!
2489
            result.push(
1✔
2490
                comment.text,
2491
                state.newline,
2492
                state.indent()
2493
            );
2494
        }
2495
        for (let annotation of this.annotations ?? []) {
10✔
2496
            result.push(
1✔
2497
                ...annotation.getTypedef(state),
2498
                state.newline,
2499
                state.indent()
2500
            );
2501
        }
2502
        if (this.isOptional) {
10!
UNCOV
2503
            result.push(
×
2504
                this.tokens.optional!.text,
2505
                ' '
2506
            );
2507
        }
2508
        result.push(
10✔
2509
            this.tokens.functionType?.text ?? 'function',
60!
2510
            ' ',
2511
            this.tokens.name.text,
2512
            '('
2513
        );
2514
        const params = this.params ?? [];
10!
2515
        for (let i = 0; i < params.length; i++) {
10✔
2516
            if (i > 0) {
2✔
2517
                result.push(', ');
1✔
2518
            }
2519
            const param = params[i];
2✔
2520
            result.push(param.tokens.name.text);
2✔
2521
            if (param.typeExpression) {
2!
2522
                result.push(
2✔
2523
                    ' as ',
2524
                    ...param.typeExpression.getTypedef(state)
2525
                );
2526
            }
2527
        }
2528
        result.push(
10✔
2529
            ')'
2530
        );
2531
        if (this.returnTypeExpression) {
10!
2532
            result.push(
10✔
2533
                ' as ',
2534
                ...this.returnTypeExpression.getTypedef(state)
2535
            );
2536
        }
2537
        return result;
10✔
2538
    }
2539

2540
    public getType(options: GetTypeOptions): TypedFunctionType {
2541
        //if there's a defined return type, use that
2542
        let returnType = this.returnTypeExpression?.getType(options);
30✔
2543
        const isSub = this.tokens.functionType?.kind === TokenKind.Sub || !returnType;
30!
2544
        //if we don't have a return type and this is a sub, set the return type to `void`. else use `dynamic`
2545
        if (!returnType) {
30✔
2546
            returnType = isSub ? VoidType.instance : DynamicType.instance;
10!
2547
        }
2548
        const resultType = new TypedFunctionType(returnType);
30✔
2549
        resultType.isSub = isSub;
30✔
2550
        for (let param of this.params) {
30✔
2551
            resultType.addParameter(param.tokens.name.text, param.getType(options), !!param.defaultValue);
7✔
2552
        }
2553
        if (options.typeChain) {
30!
2554
            // need Interface type for type chain
UNCOV
2555
            this.parent?.getType(options);
×
2556
        }
2557
        let funcName = this.getName(ParseMode.BrighterScript);
30✔
2558
        resultType.setName(funcName);
30✔
2559
        options.typeChain?.push(new TypeChainEntry({ name: resultType.name, type: resultType, data: options.data, astNode: this }));
30!
2560
        return resultType;
30✔
2561
    }
2562

2563
    public clone() {
2564
        return this.finalizeClone(
4✔
2565
            new InterfaceMethodStatement({
2566
                optional: util.cloneToken(this.tokens.optional),
2567
                functionType: util.cloneToken(this.tokens.functionType),
2568
                name: util.cloneToken(this.tokens.name),
2569
                leftParen: util.cloneToken(this.tokens.leftParen),
2570
                params: this.params?.map(p => p?.clone()),
3✔
2571
                rightParen: util.cloneToken(this.tokens.rightParen),
2572
                as: util.cloneToken(this.tokens.as),
2573
                returnTypeExpression: this.returnTypeExpression?.clone()
12✔
2574
            }),
2575
            ['params']
2576
        );
2577
    }
2578
}
2579

2580
export class ClassStatement extends Statement implements TypedefProvider {
1✔
2581
    constructor(options: {
2582
        class?: Token;
2583
        /**
2584
         * The name of the class (without namespace prefix)
2585
         */
2586
        name: Identifier;
2587
        body: Statement[];
2588
        endClass?: Token;
2589
        extends?: Token;
2590
        parentClassName?: TypeExpression;
2591
    }) {
2592
        super();
728✔
2593
        this.body = options.body ?? [];
728✔
2594
        this.tokens = {
728✔
2595
            name: options.name,
2596
            class: options.class,
2597
            endClass: options.endClass,
2598
            extends: options.extends
2599
        };
2600
        this.parentClassName = options.parentClassName;
728✔
2601
        this.symbolTable = new SymbolTable(`ClassStatement: '${this.tokens.name?.text}'`, () => this.parent?.getSymbolTable());
2,312!
2602

2603
        for (let statement of this.body) {
728✔
2604
            if (isMethodStatement(statement)) {
736✔
2605
                this.methods.push(statement);
376✔
2606
                this.memberMap[statement?.tokens.name?.text.toLowerCase()] = statement;
376!
2607
            } else if (isFieldStatement(statement)) {
360✔
2608
                this.fields.push(statement);
359✔
2609
                this.memberMap[statement?.tokens.name?.text.toLowerCase()] = statement;
359!
2610
            }
2611
        }
2612

2613
        this.location = util.createBoundingLocation(
728✔
2614
            this.parentClassName,
2615
            ...(this.body ?? []),
2,184!
2616
            util.createBoundingLocationFromTokens(this.tokens)
2617
        );
2618
    }
2619

2620
    public readonly kind = AstNodeKind.ClassStatement;
728✔
2621

2622

2623
    public readonly tokens: {
2624
        readonly class?: Token;
2625
        /**
2626
         * The name of the class (without namespace prefix)
2627
         */
2628
        readonly name: Identifier;
2629
        readonly endClass?: Token;
2630
        readonly extends?: Token;
2631
    };
2632
    public readonly body: Statement[];
2633
    public readonly parentClassName: TypeExpression;
2634

2635

2636
    public getName(parseMode: ParseMode) {
2637
        const name = this.tokens.name?.text;
2,428✔
2638
        if (name) {
2,428✔
2639
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,427✔
2640
            if (namespace) {
2,427✔
2641
                let namespaceName = namespace.getName(parseMode);
923✔
2642
                let separator = parseMode === ParseMode.BrighterScript ? '.' : '_';
923✔
2643
                return namespaceName + separator + name;
923✔
2644
            } else {
2645
                return name;
1,504✔
2646
            }
2647
        } else {
2648
            //return undefined which will allow outside callers to know that this class doesn't have a name
2649
            return undefined;
1✔
2650
        }
2651
    }
2652

2653
    public get leadingTrivia(): Token[] {
2654
        return this.tokens.class?.leadingTrivia;
1,402!
2655
    }
2656

2657
    public get endTrivia(): Token[] {
UNCOV
2658
        return this.tokens.endClass?.leadingTrivia ?? [];
×
2659
    }
2660

2661
    public readonly memberMap = {} as Record<string, MemberStatement>;
728✔
2662
    public readonly methods = [] as MethodStatement[];
728✔
2663
    public readonly fields = [] as FieldStatement[];
728✔
2664

2665
    public readonly location: Location | undefined;
2666

2667
    transpile(state: BrsTranspileState) {
2668
        let result = [] as TranspileResult;
58✔
2669

2670
        const className = this.getName(ParseMode.BrightScript).replace(/\./g, '_');
58✔
2671
        const ancestors = this.getAncestors(state);
58✔
2672
        const body = this.getTranspiledClassBody(ancestors);
58✔
2673

2674
        //make the methods
2675
        result.push(...this.getTranspiledMethods(state, className, body));
58✔
2676
        //make the builder
2677
        result.push(...this.getTranspiledBuilder(state, className, ancestors, body));
58✔
2678
        result.push('\n', state.indent());
58✔
2679
        //make the class assembler (i.e. the public-facing class creator method)
2680
        result.push(...this.getTranspiledClassFunction(state, className));
58✔
2681

2682
        return result;
58✔
2683
    }
2684

2685
    getTypedef(state: BrsTranspileState) {
2686
        const result = [] as TranspileResult;
15✔
2687
        for (let comment of util.getLeadingComments(this) ?? []) {
15!
UNCOV
2688
            result.push(
×
2689
                comment.text,
2690
                state.newline,
2691
                state.indent()
2692
            );
2693
        }
2694
        for (let annotation of this.annotations ?? []) {
15!
UNCOV
2695
            result.push(
×
2696
                ...annotation.getTypedef(state),
2697
                state.newline,
2698
                state.indent()
2699
            );
2700
        }
2701
        result.push(
15✔
2702
            'class ',
2703
            this.tokens.name.text
2704
        );
2705
        if (this.parentClassName) {
15✔
2706
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
4✔
2707
            const fqName = util.getFullyQualifiedClassName(
4✔
2708
                this.parentClassName.getName(),
2709
                namespace?.getName(ParseMode.BrighterScript)
12✔
2710
            );
2711
            result.push(
4✔
2712
                ` extends ${fqName}`
2713
            );
2714
        }
2715
        result.push(state.newline);
15✔
2716
        state.blockDepth++;
15✔
2717

2718
        let body = this.body;
15✔
2719
        //inject an empty "new" method if missing
2720
        if (!this.getConstructorFunction()) {
15✔
2721
            const constructor = createMethodStatement('new', TokenKind.Sub);
11✔
2722
            constructor.parent = this;
11✔
2723
            //walk the constructor to set up parent links
2724
            constructor.link();
11✔
2725
            body = [
11✔
2726
                constructor,
2727
                ...this.body
2728
            ];
2729
        }
2730

2731
        for (const member of body) {
15✔
2732
            if (isTypedefProvider(member)) {
35!
2733
                result.push(
35✔
2734
                    state.indent(),
2735
                    ...member.getTypedef(state),
2736
                    state.newline
2737
                );
2738
            }
2739
        }
2740
        state.blockDepth--;
15✔
2741
        result.push(
15✔
2742
            state.indent(),
2743
            'end class'
2744
        );
2745
        return result;
15✔
2746
    }
2747

2748
    /**
2749
     * Find the parent index for this class's parent.
2750
     * For class inheritance, every class is given an index.
2751
     * The base class is index 0, its child is index 1, and so on.
2752
     */
2753
    public getParentClassIndex(state: BrsTranspileState) {
2754
        let myIndex = 0;
132✔
2755
        let stmt = this as ClassStatement;
132✔
2756
        while (stmt) {
132✔
2757
            if (stmt.parentClassName) {
205✔
2758
                const namespace = stmt.findAncestor<NamespaceStatement>(isNamespaceStatement);
74✔
2759
                //find the parent class
2760
                stmt = state.file.getClassFileLink(
74✔
2761
                    stmt.parentClassName.getName(),
2762
                    namespace?.getName(ParseMode.BrighterScript)
222✔
2763
                )?.item;
74✔
2764
                myIndex++;
74✔
2765
            } else {
2766
                break;
131✔
2767
            }
2768
        }
2769
        const result = myIndex - 1;
132✔
2770
        if (result >= 0) {
132✔
2771
            return result;
55✔
2772
        } else {
2773
            return null;
77✔
2774
        }
2775
    }
2776

2777
    public hasParentClass() {
2778
        return !!this.parentClassName;
301✔
2779
    }
2780

2781
    /**
2782
     * Get all ancestor classes, in closest-to-furthest order (i.e. 0 is parent, 1 is grandparent, etc...).
2783
     * This will return an empty array if no ancestors were found
2784
     */
2785
    public getAncestors(state: BrsTranspileState) {
2786
        let ancestors = [] as ClassStatement[];
150✔
2787
        let stmt = this as ClassStatement;
150✔
2788
        while (stmt) {
150✔
2789
            if (stmt.parentClassName) {
238✔
2790
                const namespace = stmt.findAncestor<NamespaceStatement>(isNamespaceStatement);
88✔
2791
                stmt = state.file.getClassFileLink(
88✔
2792
                    stmt.parentClassName.getName(),
2793
                    namespace?.getName(ParseMode.BrighterScript)
264✔
2794
                )?.item;
88!
2795
                ancestors.push(stmt);
88✔
2796
            } else {
2797
                break;
150✔
2798
            }
2799
        }
2800
        return ancestors;
150✔
2801
    }
2802

2803
    private getBuilderName(transpiledClassName: string) {
2804
        return `__${transpiledClassName}_builder`;
140✔
2805
    }
2806

2807
    private getMethodIdentifier(transpiledClassName: string, statement: MethodStatement) {
2808
        return { ...statement.tokens.name, text: `__${transpiledClassName}_method_${statement.tokens.name.text}` };
146✔
2809
    }
2810

2811
    public getConstructorType() {
2812
        const constructorType = this.getConstructorFunction()?.getType({ flags: SymbolTypeFlag.runtime }) ?? new TypedFunctionType(null);
137✔
2813
        constructorType.returnType = this.getType({ flags: SymbolTypeFlag.runtime });
137✔
2814
        return constructorType;
137✔
2815
    }
2816

2817
    /**
2818
     * Get the constructor function for this class (if exists), or undefined if not exist
2819
     */
2820
    private getConstructorFunction() {
2821
        return this.body.find((stmt) => {
308✔
2822
            return (stmt as MethodStatement)?.tokens.name?.text?.toLowerCase() === 'new';
258!
2823
        }) as MethodStatement;
2824
    }
2825

2826
    /**
2827
     * Return the parameters for the first constructor function for this class
2828
     * @param ancestors The list of ancestors for this class
2829
     * @returns The parameters for the first constructor function for this class
2830
     */
2831
    private getConstructorParams(ancestors: ClassStatement[]) {
2832
        for (let ancestor of ancestors) {
49✔
2833
            const ctor = ancestor?.getConstructorFunction();
40!
2834
            if (ctor) {
40✔
2835
                return ctor.func.parameters;
16✔
2836
            }
2837
        }
2838
        return [];
33✔
2839
    }
2840

2841
    /**
2842
     * Determine if the specified field was declared in one of the ancestor classes
2843
     */
2844
    public isFieldDeclaredByAncestor(fieldName: string, ancestors: ClassStatement[]) {
UNCOV
2845
        let lowerFieldName = fieldName.toLowerCase();
×
UNCOV
2846
        for (let ancestor of ancestors) {
×
UNCOV
2847
            if (ancestor.memberMap[lowerFieldName]) {
×
UNCOV
2848
                return true;
×
2849
            }
2850
        }
UNCOV
2851
        return false;
×
2852
    }
2853

2854
    /**
2855
     * The builder is a function that assigns all of the methods and property names to a class instance.
2856
     * This needs to be a separate function so that child classes can call the builder from their parent
2857
     * without instantiating the parent constructor at that point in time.
2858
     */
2859
    private getTranspiledBuilder(state: BrsTranspileState, transpiledClassName: string, ancestors: ClassStatement[], body: Statement[]) {
2860
        let result = [] as TranspileResult;
58✔
2861
        result.push(`function ${this.getBuilderName(transpiledClassName)}()\n`);
58✔
2862
        state.blockDepth++;
58✔
2863
        //indent
2864
        result.push(state.indent());
58✔
2865

2866
        //construct parent class or empty object
2867
        if (ancestors[0]) {
58✔
2868
            const ancestorNamespace = ancestors[0].findAncestor<NamespaceStatement>(isNamespaceStatement);
24✔
2869
            let fullyQualifiedClassName = util.getFullyQualifiedClassName(
24✔
2870
                ancestors[0].getName(ParseMode.BrighterScript)!,
2871
                ancestorNamespace?.getName(ParseMode.BrighterScript)
72✔
2872
            );
2873
            result.push(`instance = ${this.getBuilderName(fullyQualifiedClassName.replace(/\./g, '_'))}()`);
24✔
2874
        } else {
2875
            //use an empty object.
2876
            result.push('instance = {}');
34✔
2877
        }
2878
        result.push(
58✔
2879
            state.newline,
2880
            state.indent()
2881
        );
2882
        let parentClassIndex = this.getParentClassIndex(state);
58✔
2883

2884
        for (let statement of body) {
58✔
2885
            //is field statement
2886
            if (isFieldStatement(statement)) {
88✔
2887
                //do nothing with class fields in this situation, they are handled elsewhere
2888
                continue;
15✔
2889

2890
                //methods
2891
            } else if (isMethodStatement(statement)) {
73!
2892

2893
                //store overridden parent methods as super{parentIndex}_{methodName}
2894
                if (
73✔
2895
                    //is override method
2896
                    statement.tokens.override ||
200✔
2897
                    //is constructor function in child class
2898
                    (statement.tokens.name.text.toLowerCase() === 'new' && ancestors[0])
2899
                ) {
2900
                    result.push(
28✔
2901
                        `instance.super${parentClassIndex}_${statement.tokens.name.text} = instance.${statement.tokens.name.text}`,
2902
                        state.newline,
2903
                        state.indent()
2904
                    );
2905
                }
2906

2907
                state.classStatement = this;
73✔
2908
                state.skipLeadingComments = true;
73✔
2909
                //add leading comments
2910
                if (statement.leadingTrivia.filter(token => token.kind === TokenKind.Comment).length > 0) {
84✔
2911
                    result.push(
2✔
2912
                        ...state.transpileComments(statement.leadingTrivia),
2913
                        state.indent()
2914
                    );
2915
                }
2916
                result.push(
73✔
2917
                    'instance.',
2918
                    state.transpileToken(statement.tokens.name),
2919
                    ' = ',
2920
                    state.transpileToken(this.getMethodIdentifier(transpiledClassName, statement)),
2921
                    state.newline,
2922
                    state.indent()
2923
                );
2924
                state.skipLeadingComments = false;
73✔
2925
                delete state.classStatement;
73✔
2926
            } else {
2927
                //other random statements (probably just comments)
UNCOV
2928
                result.push(
×
2929
                    ...statement.transpile(state),
2930
                    state.newline,
2931
                    state.indent()
2932
                );
2933
            }
2934
        }
2935
        //return the instance
2936
        result.push('return instance\n');
58✔
2937
        state.blockDepth--;
58✔
2938
        result.push(state.indent());
58✔
2939
        result.push(`end function`);
58✔
2940
        return result;
58✔
2941
    }
2942

2943
    /**
2944
     * Returns a copy of the class' body, with the constructor function added if it doesn't exist.
2945
     */
2946
    private getTranspiledClassBody(ancestors: ClassStatement[]): Statement[] {
2947
        const body = [];
58✔
2948
        body.push(...this.body);
58✔
2949

2950
        //inject an empty "new" method if missing
2951
        if (!this.getConstructorFunction()) {
58✔
2952
            if (ancestors.length === 0) {
34✔
2953
                body.unshift(createMethodStatement('new', TokenKind.Sub));
19✔
2954
            } else {
2955
                const params = this.getConstructorParams(ancestors);
15✔
2956
                const call = new ExpressionStatement({
15✔
2957
                    expression: new CallExpression({
2958
                        callee: new VariableExpression({
2959
                            name: createToken(TokenKind.Identifier, 'super')
2960
                        }),
2961
                        openingParen: createToken(TokenKind.LeftParen),
2962
                        args: params.map(x => new VariableExpression({
6✔
2963
                            name: x.tokens.name
2964
                        })),
2965
                        closingParen: createToken(TokenKind.RightParen)
2966
                    })
2967
                });
2968
                body.unshift(
15✔
2969
                    new MethodStatement({
2970
                        modifiers: [],
2971
                        name: createIdentifier('new'),
2972
                        func: new FunctionExpression({
2973
                            parameters: params.map(x => x.clone()),
6✔
2974
                            body: new Block({ statements: [call] }),
2975
                            functionType: createToken(TokenKind.Sub),
2976
                            endFunctionType: createToken(TokenKind.EndSub),
2977
                            leftParen: createToken(TokenKind.LeftParen),
2978
                            rightParen: createToken(TokenKind.RightParen)
2979
                        }),
2980
                        override: null
2981
                    })
2982
                );
2983
            }
2984
        }
2985

2986
        return body;
58✔
2987
    }
2988

2989
    /**
2990
     * These are the methods that are defined in this class. They are transpiled outside of the class body
2991
     * to ensure they don't appear as "$anon_#" in stack traces and crash logs.
2992
     */
2993
    private getTranspiledMethods(state: BrsTranspileState, transpiledClassName: string, body: Statement[]) {
2994
        let result = [] as TranspileResult;
58✔
2995
        for (let statement of body) {
58✔
2996
            if (isMethodStatement(statement)) {
88✔
2997
                state.classStatement = this;
73✔
2998
                result.push(
73✔
2999
                    ...statement.transpile(state, this.getMethodIdentifier(transpiledClassName, statement)),
3000
                    state.newline,
3001
                    state.indent()
3002
                );
3003
                delete state.classStatement;
73✔
3004
            }
3005
        }
3006
        return result;
58✔
3007
    }
3008

3009
    /**
3010
     * The class function is the function with the same name as the class. This is the function that
3011
     * consumers should call to create a new instance of that class.
3012
     * This invokes the builder, gets an instance of the class, then invokes the "new" function on that class.
3013
     */
3014
    private getTranspiledClassFunction(state: BrsTranspileState, transpiledClassName: string) {
3015
        let result: TranspileResult = state.transpileAnnotations(this);
58✔
3016

3017
        const constructorFunction = this.getConstructorFunction();
58✔
3018
        let constructorParams = [];
58✔
3019
        if (constructorFunction) {
58✔
3020
            constructorParams = constructorFunction.func.parameters;
24✔
3021
        } else {
3022
            constructorParams = this.getConstructorParams(this.getAncestors(state));
34✔
3023
        }
3024

3025
        result.push(
58✔
3026
            state.transpileLeadingComments(this.tokens.class),
3027
            state.sourceNode(this.tokens.class, 'function'),
3028
            state.sourceNode(this.tokens.class, ' '),
3029
            state.sourceNode(this.tokens.name, this.getName(ParseMode.BrightScript)),
3030
            `(`
3031
        );
3032
        let i = 0;
58✔
3033
        for (let param of constructorParams) {
58✔
3034
            if (i > 0) {
17✔
3035
                result.push(', ');
4✔
3036
            }
3037
            result.push(
17✔
3038
                param.transpile(state)
3039
            );
3040
            i++;
17✔
3041
        }
3042
        result.push(
58✔
3043
            ')',
3044
            '\n'
3045
        );
3046

3047
        state.blockDepth++;
58✔
3048
        result.push(state.indent());
58✔
3049
        result.push(`instance = ${this.getBuilderName(transpiledClassName)}()\n`);
58✔
3050

3051
        result.push(state.indent());
58✔
3052
        result.push(`instance.new(`);
58✔
3053

3054
        //append constructor arguments
3055
        i = 0;
58✔
3056
        for (let param of constructorParams) {
58✔
3057
            if (i > 0) {
17✔
3058
                result.push(', ');
4✔
3059
            }
3060
            result.push(
17✔
3061
                state.transpileToken(param.tokens.name)
3062
            );
3063
            i++;
17✔
3064
        }
3065
        result.push(
58✔
3066
            ')',
3067
            '\n'
3068
        );
3069

3070
        result.push(state.indent());
58✔
3071
        result.push(`return instance\n`);
58✔
3072

3073
        state.blockDepth--;
58✔
3074
        result.push(state.indent());
58✔
3075
        result.push(`end function`);
58✔
3076
        return result;
58✔
3077
    }
3078

3079
    walk(visitor: WalkVisitor, options: WalkOptions) {
3080
        //visitor-less walk function to do parent linking
3081
        walk(this, 'parentClassName', null, options);
2,856✔
3082

3083
        if (options.walkMode & InternalWalkMode.walkStatements) {
2,856!
3084
            walkArray(this.body, visitor, options, this);
2,856✔
3085
        }
3086
    }
3087

3088
    getType(options: GetTypeOptions) {
3089
        const superClass = this.parentClassName?.getType(options) as ClassType;
581✔
3090

3091
        const resultType = new ClassType(this.getName(ParseMode.BrighterScript), superClass);
581✔
3092

3093
        for (const statement of this.methods) {
581✔
3094
            const funcType = statement?.func.getType({ ...options, typeChain: undefined }); //no typechain needed
299!
3095
            let flag = SymbolTypeFlag.runtime;
299✔
3096
            if (statement.accessModifier?.kind === TokenKind.Private) {
299✔
3097
                flag |= SymbolTypeFlag.private;
9✔
3098
            }
3099
            if (statement.accessModifier?.kind === TokenKind.Protected) {
299✔
3100
                flag |= SymbolTypeFlag.protected;
8✔
3101
            }
3102
            resultType.addMember(statement?.tokens.name?.text, { definingNode: statement }, funcType, flag);
299!
3103
        }
3104
        for (const statement of this.fields) {
581✔
3105
            const fieldType = statement.getType({ ...options, typeChain: undefined }); //no typechain needed
298✔
3106
            let flag = SymbolTypeFlag.runtime;
298✔
3107
            if (statement.isOptional) {
298✔
3108
                flag |= SymbolTypeFlag.optional;
7✔
3109
            }
3110
            if (statement.tokens.accessModifier?.kind === TokenKind.Private) {
298✔
3111
                flag |= SymbolTypeFlag.private;
20✔
3112
            }
3113
            if (statement.tokens.accessModifier?.kind === TokenKind.Protected) {
298✔
3114
                flag |= SymbolTypeFlag.protected;
9✔
3115
            }
3116
            resultType.addMember(statement?.tokens.name?.text, { definingNode: statement, isInstance: true }, fieldType, flag);
298!
3117
        }
3118
        options.typeChain?.push(new TypeChainEntry({ name: resultType.name, type: resultType, data: options.data, astNode: this }));
581✔
3119
        return resultType;
581✔
3120
    }
3121

3122
    public clone() {
3123
        return this.finalizeClone(
11✔
3124
            new ClassStatement({
3125
                class: util.cloneToken(this.tokens.class),
3126
                name: util.cloneToken(this.tokens.name),
3127
                body: this.body?.map(x => x?.clone()),
11✔
3128
                endClass: util.cloneToken(this.tokens.endClass),
3129
                extends: util.cloneToken(this.tokens.extends),
3130
                parentClassName: this.parentClassName?.clone()
33✔
3131
            }),
3132
            ['body', 'parentClassName']
3133
        );
3134
    }
3135
}
3136

3137
const accessModifiers = [
1✔
3138
    TokenKind.Public,
3139
    TokenKind.Protected,
3140
    TokenKind.Private
3141
];
3142
export class MethodStatement extends FunctionStatement {
1✔
3143
    constructor(
3144
        options: {
3145
            modifiers?: Token | Token[];
3146
            name: Identifier;
3147
            func: FunctionExpression;
3148
            override?: Token;
3149
        }
3150
    ) {
3151
        super(options);
421✔
3152
        if (options.modifiers) {
421✔
3153
            if (Array.isArray(options.modifiers)) {
55✔
3154
                this.modifiers.push(...options.modifiers);
19✔
3155
            } else {
3156
                this.modifiers.push(options.modifiers);
36✔
3157
            }
3158
        }
3159
        this.tokens = {
421✔
3160
            ...this.tokens,
3161
            override: options.override
3162
        };
3163
        this.location = util.createBoundingLocation(
421✔
3164
            ...(this.modifiers),
3165
            util.createBoundingLocationFromTokens(this.tokens),
3166
            this.func
3167
        );
3168
    }
3169

3170
    public readonly kind = AstNodeKind.MethodStatement as AstNodeKind;
421✔
3171

3172
    public readonly modifiers: Token[] = [];
421✔
3173

3174
    public readonly tokens: {
3175
        readonly name: Identifier;
3176
        readonly override?: Token;
3177
    };
3178

3179
    public get accessModifier() {
3180
        return this.modifiers.find(x => accessModifiers.includes(x.kind));
708✔
3181
    }
3182

3183
    public readonly location: Location | undefined;
3184

3185
    /**
3186
     * Get the name of this method.
3187
     */
3188
    public getName(parseMode: ParseMode) {
3189
        return this.tokens.name.text;
380✔
3190
    }
3191

3192
    public get leadingTrivia(): Token[] {
3193
        return this.func.leadingTrivia;
841✔
3194
    }
3195

3196
    transpile(state: BrsTranspileState, name?: Identifier) {
3197
        if (this.tokens.name.text.toLowerCase() === 'new') {
73✔
3198
            this.ensureSuperConstructorCall(state);
58✔
3199
            //TODO we need to undo this at the bottom of this method
3200
            this.injectFieldInitializersForConstructor(state);
58✔
3201
        }
3202
        //TODO - remove type information from these methods because that doesn't work
3203
        //convert the `super` calls into the proper methods
3204
        const parentClassIndex = state.classStatement.getParentClassIndex(state);
73✔
3205
        const visitor = createVisitor({
73✔
3206
            VariableExpression: e => {
3207
                if (e.tokens.name.text.toLocaleLowerCase() === 'super') {
76✔
3208
                    state.editor.setProperty(e.tokens.name, 'text', `m.super${parentClassIndex}_new`);
24✔
3209
                }
3210
            },
3211
            DottedGetExpression: e => {
3212
                const beginningVariable = util.findBeginningVariableExpression(e);
30✔
3213
                const lowerName = beginningVariable?.getName(ParseMode.BrighterScript).toLowerCase();
30!
3214
                if (lowerName === 'super') {
30✔
3215
                    state.editor.setProperty(beginningVariable.tokens.name, 'text', 'm');
7✔
3216
                    state.editor.setProperty(e.tokens.name, 'text', `super${parentClassIndex}_${e.tokens.name.text}`);
7✔
3217
                }
3218
            }
3219
        });
3220
        const walkOptions: WalkOptions = { walkMode: WalkMode.visitExpressions };
73✔
3221
        for (const statement of this.func.body.statements) {
73✔
3222
            visitor(statement, undefined);
71✔
3223
            statement.walk(visitor, walkOptions);
71✔
3224
        }
3225
        return this.func.transpile(state, name);
73✔
3226
    }
3227

3228
    getTypedef(state: BrsTranspileState) {
3229
        const result: TranspileResult = [];
23✔
3230
        for (let comment of util.getLeadingComments(this) ?? []) {
23!
UNCOV
3231
            result.push(
×
3232
                comment.text,
3233
                state.newline,
3234
                state.indent()
3235
            );
3236
        }
3237
        for (let annotation of this.annotations ?? []) {
23✔
3238
            result.push(
2✔
3239
                ...annotation.getTypedef(state),
3240
                state.newline,
3241
                state.indent()
3242
            );
3243
        }
3244
        if (this.accessModifier) {
23✔
3245
            result.push(
8✔
3246
                this.accessModifier.text,
3247
                ' '
3248
            );
3249
        }
3250
        if (this.tokens.override) {
23✔
3251
            result.push('override ');
1✔
3252
        }
3253
        result.push(
23✔
3254
            ...this.func.getTypedef(state)
3255
        );
3256
        return result;
23✔
3257
    }
3258

3259
    /**
3260
     * All child classes must call the parent constructor. The type checker will warn users when they don't call it in their own class,
3261
     * but we still need to call it even if they have omitted it. This injects the super call if it's missing
3262
     */
3263
    private ensureSuperConstructorCall(state: BrsTranspileState) {
3264
        //if this class doesn't extend another class, quit here
3265
        if (state.classStatement!.getAncestors(state).length === 0) {
58✔
3266
            return;
34✔
3267
        }
3268

3269
        //check whether any calls to super exist
3270
        let containsSuperCall =
3271
            this.func.body.statements.findIndex((x) => {
24✔
3272
                //is a call statement
3273
                return isExpressionStatement(x) && isCallExpression(x.expression) &&
24✔
3274
                    //is a call to super
3275
                    util.findBeginningVariableExpression(x.expression.callee as any).tokens.name?.text.toLowerCase() === 'super';
69!
3276
            }) !== -1;
3277

3278
        //if a call to super exists, quit here
3279
        if (containsSuperCall) {
24✔
3280
            return;
23✔
3281
        }
3282

3283
        //this is a child class, and the constructor doesn't contain a call to super. Inject one
3284
        const superCall = new ExpressionStatement({
1✔
3285
            expression: new CallExpression({
3286
                callee: new VariableExpression({
3287
                    name: {
3288
                        kind: TokenKind.Identifier,
3289
                        text: 'super',
3290
                        isReserved: false,
3291
                        location: state.classStatement.tokens.name.location,
3292
                        leadingWhitespace: '',
3293
                        leadingTrivia: []
3294
                    }
3295
                }),
3296
                openingParen: {
3297
                    kind: TokenKind.LeftParen,
3298
                    text: '(',
3299
                    isReserved: false,
3300
                    location: state.classStatement.tokens.name.location,
3301
                    leadingWhitespace: '',
3302
                    leadingTrivia: []
3303
                },
3304
                closingParen: {
3305
                    kind: TokenKind.RightParen,
3306
                    text: ')',
3307
                    isReserved: false,
3308
                    location: state.classStatement.tokens.name.location,
3309
                    leadingWhitespace: '',
3310
                    leadingTrivia: []
3311
                },
3312
                args: []
3313
            })
3314
        });
3315
        state.editor.arrayUnshift(this.func.body.statements, superCall);
1✔
3316
    }
3317

3318
    /**
3319
     * Inject field initializers at the top of the `new` function (after any present `super()` call)
3320
     */
3321
    private injectFieldInitializersForConstructor(state: BrsTranspileState) {
3322
        let startingIndex = state.classStatement!.hasParentClass() ? 1 : 0;
58✔
3323

3324
        let newStatements = [] as Statement[];
58✔
3325
        //insert the field initializers in order
3326
        for (let field of state.classStatement!.fields) {
58✔
3327
            let thisQualifiedName = { ...field.tokens.name };
15✔
3328
            thisQualifiedName.text = 'm.' + field.tokens.name?.text;
15!
3329
            const fieldAssignment = field.initialValue
15✔
3330
                ? new AssignmentStatement({
15✔
3331
                    equals: field.tokens.equals,
3332
                    name: thisQualifiedName,
3333
                    value: field.initialValue
3334
                })
3335
                : new AssignmentStatement({
3336
                    equals: createToken(TokenKind.Equal, '=', field.tokens.name.location),
3337
                    name: thisQualifiedName,
3338
                    //if there is no initial value, set the initial value to `invalid`
3339
                    value: createInvalidLiteral('invalid', field.tokens.name.location)
3340
                });
3341
            // Add parent so namespace lookups work
3342
            fieldAssignment.parent = state.classStatement;
15✔
3343
            newStatements.push(fieldAssignment);
15✔
3344
        }
3345
        state.editor.arraySplice(this.func.body.statements, startingIndex, 0, ...newStatements);
58✔
3346
    }
3347

3348
    walk(visitor: WalkVisitor, options: WalkOptions) {
3349
        if (options.walkMode & InternalWalkMode.walkExpressions) {
2,280✔
3350
            walk(this, 'func', visitor, options);
1,828✔
3351
        }
3352
    }
3353

3354
    public clone() {
3355
        return this.finalizeClone(
5✔
3356
            new MethodStatement({
3357
                modifiers: this.modifiers?.map(m => util.cloneToken(m)),
1✔
3358
                name: util.cloneToken(this.tokens.name),
3359
                func: this.func?.clone(),
15✔
3360
                override: util.cloneToken(this.tokens.override)
3361
            }),
3362
            ['func']
3363
        );
3364
    }
3365
}
3366

3367
export class FieldStatement extends Statement implements TypedefProvider {
1✔
3368
    constructor(options: {
3369
        accessModifier?: Token;
3370
        name: Identifier;
3371
        as?: Token;
3372
        typeExpression?: TypeExpression;
3373
        equals?: Token;
3374
        initialValue?: Expression;
3375
        optional?: Token;
3376
    }) {
3377
        super();
359✔
3378
        this.tokens = {
359✔
3379
            accessModifier: options.accessModifier,
3380
            name: options.name,
3381
            as: options.as,
3382
            equals: options.equals,
3383
            optional: options.optional
3384
        };
3385
        this.typeExpression = options.typeExpression;
359✔
3386
        this.initialValue = options.initialValue;
359✔
3387

3388
        this.location = util.createBoundingLocation(
359✔
3389
            util.createBoundingLocationFromTokens(this.tokens),
3390
            this.typeExpression,
3391
            this.initialValue
3392
        );
3393
    }
3394

3395
    public readonly tokens: {
3396
        readonly accessModifier?: Token;
3397
        readonly name: Identifier;
3398
        readonly as?: Token;
3399
        readonly equals?: Token;
3400
        readonly optional?: Token;
3401
    };
3402

3403
    public readonly typeExpression?: TypeExpression;
3404
    public readonly initialValue?: Expression;
3405

3406
    public readonly kind = AstNodeKind.FieldStatement;
359✔
3407

3408
    /**
3409
     * Derive a ValueKind from the type token, or the initial value.
3410
     * Defaults to `DynamicType`
3411
     */
3412
    getType(options: GetTypeOptions) {
3413
        let initialValueType = this.initialValue?.getType({ ...options, flags: SymbolTypeFlag.runtime });
348✔
3414

3415
        if (isInvalidType(initialValueType) || isVoidType(initialValueType) || isUninitializedType(initialValueType)) {
348✔
3416
            initialValueType = undefined;
4✔
3417
        }
3418

3419
        return this.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime }) ??
348✔
3420
            util.getDefaultTypeFromValueType(initialValueType) ??
348✔
3421
            DynamicType.instance;
3422
    }
3423

3424
    public readonly location: Location | undefined;
3425

3426
    public get leadingTrivia(): Token[] {
3427
        return this.tokens.accessModifier?.leadingTrivia ?? this.tokens.optional?.leadingTrivia ?? this.tokens.name.leadingTrivia;
710✔
3428
    }
3429

3430
    public get isOptional() {
3431
        return !!this.tokens.optional;
317✔
3432
    }
3433

3434
    transpile(state: BrsTranspileState): TranspileResult {
UNCOV
3435
        throw new Error('transpile not implemented for ' + Object.getPrototypeOf(this).constructor.name);
×
3436
    }
3437

3438
    getTypedef(state: BrsTranspileState) {
3439
        const result = [];
12✔
3440
        if (this.tokens.name) {
12!
3441
            for (let comment of util.getLeadingComments(this) ?? []) {
12!
UNCOV
3442
                result.push(
×
3443
                    comment.text,
3444
                    state.newline,
3445
                    state.indent()
3446
                );
3447
            }
3448
            for (let annotation of this.annotations ?? []) {
12✔
3449
                result.push(
2✔
3450
                    ...annotation.getTypedef(state),
3451
                    state.newline,
3452
                    state.indent()
3453
                );
3454
            }
3455

3456
            let type = this.getType({ flags: SymbolTypeFlag.typetime });
12✔
3457
            if (isInvalidType(type) || isVoidType(type) || isUninitializedType(type)) {
12!
UNCOV
3458
                type = DynamicType.instance;
×
3459
            }
3460

3461
            result.push(
12✔
3462
                this.tokens.accessModifier?.text ?? 'public',
72✔
3463
                ' '
3464
            );
3465
            if (this.isOptional) {
12!
UNCOV
3466
                result.push(this.tokens.optional.text, ' ');
×
3467
            }
3468
            result.push(this.tokens.name?.text,
12!
3469
                ' as ',
3470
                type.toTypeString()
3471
            );
3472
        }
3473
        return result;
12✔
3474
    }
3475

3476
    walk(visitor: WalkVisitor, options: WalkOptions) {
3477
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,780✔
3478
            walk(this, 'typeExpression', visitor, options);
1,553✔
3479
            walk(this, 'initialValue', visitor, options);
1,553✔
3480
        }
3481
    }
3482

3483
    public clone() {
3484
        return this.finalizeClone(
5✔
3485
            new FieldStatement({
3486
                accessModifier: util.cloneToken(this.tokens.accessModifier),
3487
                name: util.cloneToken(this.tokens.name),
3488
                as: util.cloneToken(this.tokens.as),
3489
                typeExpression: this.typeExpression?.clone(),
15✔
3490
                equals: util.cloneToken(this.tokens.equals),
3491
                initialValue: this.initialValue?.clone(),
15✔
3492
                optional: util.cloneToken(this.tokens.optional)
3493
            }),
3494
            ['initialValue']
3495
        );
3496
    }
3497
}
3498

3499
export type MemberStatement = FieldStatement | MethodStatement;
3500

3501
export class TryCatchStatement extends Statement {
1✔
3502
    constructor(options?: {
3503
        try?: Token;
3504
        endTry?: Token;
3505
        tryBranch?: Block;
3506
        catchStatement?: CatchStatement;
3507
    }) {
3508
        super();
46✔
3509
        this.tokens = {
46✔
3510
            try: options.try,
3511
            endTry: options.endTry
3512
        };
3513
        this.tryBranch = options.tryBranch;
46✔
3514
        this.catchStatement = options.catchStatement;
46✔
3515
        this.location = util.createBoundingLocation(
46✔
3516
            this.tokens.try,
3517
            this.tryBranch,
3518
            this.catchStatement,
3519
            this.tokens.endTry
3520
        );
3521
    }
3522

3523
    public readonly tokens: {
3524
        readonly try?: Token;
3525
        readonly endTry?: Token;
3526
    };
3527

3528
    public readonly tryBranch: Block;
3529
    public readonly catchStatement: CatchStatement;
3530

3531
    public readonly kind = AstNodeKind.TryCatchStatement;
46✔
3532

3533
    public readonly location: Location | undefined;
3534

3535
    public transpile(state: BrsTranspileState): TranspileResult {
3536
        return [
7✔
3537
            state.transpileToken(this.tokens.try, 'try'),
3538
            ...this.tryBranch.transpile(state),
3539
            state.newline,
3540
            state.indent(),
3541
            ...(this.catchStatement?.transpile(state) ?? ['catch']),
42!
3542
            state.newline,
3543
            state.indent(),
3544
            state.transpileToken(this.tokens.endTry!, 'end try')
3545
        ];
3546
    }
3547

3548
    public walk(visitor: WalkVisitor, options: WalkOptions) {
3549
        if (this.tryBranch && options.walkMode & InternalWalkMode.walkStatements) {
134!
3550
            walk(this, 'tryBranch', visitor, options);
134✔
3551
            walk(this, 'catchStatement', visitor, options);
134✔
3552
        }
3553
    }
3554

3555
    public get leadingTrivia(): Token[] {
3556
        return this.tokens.try?.leadingTrivia ?? [];
131!
3557
    }
3558

3559
    public get endTrivia(): Token[] {
3560
        return this.tokens.endTry?.leadingTrivia ?? [];
1!
3561
    }
3562

3563
    public clone() {
3564
        return this.finalizeClone(
3✔
3565
            new TryCatchStatement({
3566
                try: util.cloneToken(this.tokens.try),
3567
                endTry: util.cloneToken(this.tokens.endTry),
3568
                tryBranch: this.tryBranch?.clone(),
9✔
3569
                catchStatement: this.catchStatement?.clone()
9✔
3570
            }),
3571
            ['tryBranch', 'catchStatement']
3572
        );
3573
    }
3574
}
3575

3576
export class CatchStatement extends Statement {
1✔
3577
    constructor(options?: {
3578
        catch?: Token;
3579
        exceptionVariableExpression?: Expression;
3580
        catchBranch?: Block;
3581
    }) {
3582
        super();
43✔
3583
        this.tokens = {
43✔
3584
            catch: options?.catch
129!
3585
        };
3586
        this.exceptionVariableExpression = options?.exceptionVariableExpression;
43!
3587
        this.catchBranch = options?.catchBranch;
43!
3588
        this.location = util.createBoundingLocation(
43✔
3589
            this.tokens.catch,
3590
            this.exceptionVariableExpression,
3591
            this.catchBranch
3592
        );
3593
    }
3594

3595
    public readonly tokens: {
3596
        readonly catch?: Token;
3597
    };
3598

3599
    public readonly exceptionVariableExpression?: Expression;
3600

3601
    public readonly catchBranch?: Block;
3602

3603
    public readonly kind = AstNodeKind.CatchStatement;
43✔
3604

3605
    public readonly location: Location | undefined;
3606

3607
    public transpile(state: BrsTranspileState): TranspileResult {
3608
        return [
7✔
3609
            state.transpileToken(this.tokens.catch, 'catch'),
3610
            ' ',
3611
            this.exceptionVariableExpression?.transpile(state) ?? [
42✔
3612
                //use the variable named `e` if it doesn't exist in this function body. otherwise use '__bsc_error' just to make sure we're out of the way
3613
                this.getSymbolTable()?.hasSymbol('e', SymbolTypeFlag.runtime)
6!
3614
                    ? '__bsc_error'
2✔
3615
                    : 'e'
3616
            ],
3617
            ...(this.catchBranch?.transpile(state) ?? [])
42!
3618
        ];
3619
    }
3620

3621
    public walk(visitor: WalkVisitor, options: WalkOptions) {
3622
        if (this.catchBranch && options.walkMode & InternalWalkMode.walkStatements) {
131!
3623
            walk(this, 'catchBranch', visitor, options);
131✔
3624
        }
3625
    }
3626

3627
    public get leadingTrivia(): Token[] {
3628
        return this.tokens.catch?.leadingTrivia ?? [];
83!
3629
    }
3630

3631
    public clone() {
3632
        return this.finalizeClone(
2✔
3633
            new CatchStatement({
3634
                catch: util.cloneToken(this.tokens.catch),
3635
                exceptionVariableExpression: this.exceptionVariableExpression?.clone(),
6!
3636
                catchBranch: this.catchBranch?.clone()
6✔
3637
            }),
3638
            ['catchBranch']
3639
        );
3640
    }
3641
}
3642

3643
export class ThrowStatement extends Statement {
1✔
3644
    constructor(options?: {
3645
        throw?: Token;
3646
        expression?: Expression;
3647
    }) {
3648
        super();
14✔
3649
        this.tokens = {
14✔
3650
            throw: options.throw
3651
        };
3652
        this.expression = options.expression;
14✔
3653
        this.location = util.createBoundingLocation(
14✔
3654
            this.tokens.throw,
3655
            this.expression
3656
        );
3657
    }
3658

3659
    public readonly tokens: {
3660
        readonly throw?: Token;
3661
    };
3662
    public readonly expression?: Expression;
3663

3664
    public readonly kind = AstNodeKind.ThrowStatement;
14✔
3665

3666
    public readonly location: Location | undefined;
3667

3668
    public transpile(state: BrsTranspileState) {
3669
        const result = [
5✔
3670
            state.transpileToken(this.tokens.throw, 'throw'),
3671
            ' '
3672
        ] as TranspileResult;
3673

3674
        //if we have an expression, transpile it
3675
        if (this.expression) {
5✔
3676
            result.push(
4✔
3677
                ...this.expression.transpile(state)
3678
            );
3679

3680
            //no expression found. Rather than emit syntax errors, provide a generic error message
3681
        } else {
3682
            result.push('"User-specified exception"');
1✔
3683
        }
3684
        return result;
5✔
3685
    }
3686

3687
    public walk(visitor: WalkVisitor, options: WalkOptions) {
3688
        if (this.expression && options.walkMode & InternalWalkMode.walkExpressions) {
38✔
3689
            walk(this, 'expression', visitor, options);
30✔
3690
        }
3691
    }
3692

3693
    public get leadingTrivia(): Token[] {
3694
        return this.tokens.throw?.leadingTrivia ?? [];
54!
3695
    }
3696

3697
    public clone() {
3698
        return this.finalizeClone(
2✔
3699
            new ThrowStatement({
3700
                throw: util.cloneToken(this.tokens.throw),
3701
                expression: this.expression?.clone()
6✔
3702
            }),
3703
            ['expression']
3704
        );
3705
    }
3706
}
3707

3708

3709
export class EnumStatement extends Statement implements TypedefProvider {
1✔
3710
    constructor(options: {
3711
        enum?: Token;
3712
        name: Identifier;
3713
        endEnum?: Token;
3714
        body: Array<EnumMemberStatement>;
3715
    }) {
3716
        super();
192✔
3717
        this.tokens = {
192✔
3718
            enum: options.enum,
3719
            name: options.name,
3720
            endEnum: options.endEnum
3721
        };
3722
        this.symbolTable = new SymbolTable('Enum');
192✔
3723
        this.body = options.body ?? [];
192✔
3724
    }
3725

3726
    public readonly tokens: {
3727
        readonly enum?: Token;
3728
        readonly name: Identifier;
3729
        readonly endEnum?: Token;
3730
    };
3731
    public readonly body: Array<EnumMemberStatement>;
3732

3733
    public readonly kind = AstNodeKind.EnumStatement;
192✔
3734

3735
    public get location(): Location | undefined {
3736
        return util.createBoundingLocation(
185✔
3737
            this.tokens.enum,
3738
            this.tokens.name,
3739
            ...this.body,
3740
            this.tokens.endEnum
3741
        );
3742
    }
3743

3744
    public getMembers() {
3745
        const result = [] as EnumMemberStatement[];
382✔
3746
        for (const statement of this.body) {
382✔
3747
            if (isEnumMemberStatement(statement)) {
797!
3748
                result.push(statement);
797✔
3749
            }
3750
        }
3751
        return result;
382✔
3752
    }
3753

3754
    public get leadingTrivia(): Token[] {
3755
        return this.tokens.enum?.leadingTrivia;
542!
3756
    }
3757

3758
    public get endTrivia(): Token[] {
UNCOV
3759
        return this.tokens.endEnum?.leadingTrivia ?? [];
×
3760
    }
3761

3762
    /**
3763
     * Get a map of member names and their values.
3764
     * All values are stored as their AST LiteralExpression representation (i.e. string enum values include the wrapping quotes)
3765
     */
3766
    public getMemberValueMap() {
3767
        const result = new Map<string, string>();
66✔
3768
        const members = this.getMembers();
66✔
3769
        let currentIntValue = 0;
66✔
3770
        for (const member of members) {
66✔
3771
            //if there is no value, assume an integer and increment the int counter
3772
            if (!member.value) {
162✔
3773
                result.set(member.name?.toLowerCase(), currentIntValue.toString());
33!
3774
                currentIntValue++;
33✔
3775

3776
                //if explicit integer value, use it and increment the int counter
3777
            } else if (isLiteralExpression(member.value) && member.value.tokens.value.kind === TokenKind.IntegerLiteral) {
129✔
3778
                //try parsing as integer literal, then as hex integer literal.
3779
                let tokenIntValue = util.parseInt(member.value.tokens.value.text) ?? util.parseInt(member.value.tokens.value.text.replace(/&h/i, '0x'));
29✔
3780
                if (tokenIntValue !== undefined) {
29!
3781
                    currentIntValue = tokenIntValue;
29✔
3782
                    currentIntValue++;
29✔
3783
                }
3784
                result.set(member.name?.toLowerCase(), member.value.tokens.value.text);
29!
3785

3786
                //simple unary expressions (like `-1`)
3787
            } else if (isUnaryExpression(member.value) && isLiteralExpression(member.value.right)) {
100✔
3788
                result.set(member.name?.toLowerCase(), member.value.tokens.operator.text + member.value.right.tokens.value.text);
1!
3789

3790
                //all other values
3791
            } else {
3792
                result.set(member.name?.toLowerCase(), (member.value as LiteralExpression)?.tokens?.value?.text ?? 'invalid');
99!
3793
            }
3794
        }
3795
        return result;
66✔
3796
    }
3797

3798
    public getMemberValue(name: string) {
3799
        return this.getMemberValueMap().get(name.toLowerCase());
63✔
3800
    }
3801

3802
    /**
3803
     * The name of the enum (without the namespace prefix)
3804
     */
3805
    public get name() {
3806
        return this.tokens.name?.text;
1!
3807
    }
3808

3809
    /**
3810
     * The name of the enum WITH its leading namespace (if applicable)
3811
     */
3812
    public get fullName() {
3813
        const name = this.tokens.name?.text;
767!
3814
        if (name) {
767!
3815
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
767✔
3816

3817
            if (namespace) {
767✔
3818
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
252✔
3819
                return `${namespaceName}.${name}`;
252✔
3820
            } else {
3821
                return name;
515✔
3822
            }
3823
        } else {
3824
            //return undefined which will allow outside callers to know that this doesn't have a name
UNCOV
3825
            return undefined;
×
3826
        }
3827
    }
3828

3829
    transpile(state: BrsTranspileState) {
3830
        //enum declarations don't exist at runtime, so just transpile comments and trivia
3831
        return [
29✔
3832
            state.transpileAnnotations(this),
3833
            state.transpileLeadingComments(this.tokens.enum)
3834
        ];
3835
    }
3836

3837
    getTypedef(state: BrsTranspileState) {
3838
        const result = [] as TranspileResult;
1✔
3839
        for (let comment of util.getLeadingComments(this) ?? []) {
1!
UNCOV
3840
            result.push(
×
3841
                comment.text,
3842
                state.newline,
3843
                state.indent()
3844
            );
3845
        }
3846
        for (let annotation of this.annotations ?? []) {
1!
UNCOV
3847
            result.push(
×
3848
                ...annotation.getTypedef(state),
3849
                state.newline,
3850
                state.indent()
3851
            );
3852
        }
3853
        result.push(
1✔
3854
            this.tokens.enum?.text ?? 'enum',
6!
3855
            ' ',
3856
            this.tokens.name.text
3857
        );
3858
        result.push(state.newline);
1✔
3859
        state.blockDepth++;
1✔
3860
        for (const member of this.body) {
1✔
3861
            if (isTypedefProvider(member)) {
1!
3862
                result.push(
1✔
3863
                    state.indent(),
3864
                    ...member.getTypedef(state),
3865
                    state.newline
3866
                );
3867
            }
3868
        }
3869
        state.blockDepth--;
1✔
3870
        result.push(
1✔
3871
            state.indent(),
3872
            this.tokens.endEnum?.text ?? 'end enum'
6!
3873
        );
3874
        return result;
1✔
3875
    }
3876

3877
    walk(visitor: WalkVisitor, options: WalkOptions) {
3878
        if (options.walkMode & InternalWalkMode.walkStatements) {
1,201!
3879
            walkArray(this.body, visitor, options, this);
1,201✔
3880

3881
        }
3882
    }
3883

3884
    getType(options: GetTypeOptions) {
3885
        const members = this.getMembers();
158✔
3886

3887
        const resultType = new EnumType(
158✔
3888
            this.fullName,
3889
            members[0]?.getType(options).underlyingType
474✔
3890
        );
3891
        resultType.pushMemberProvider(() => this.getSymbolTable());
659✔
3892
        for (const statement of members) {
158✔
3893
            const memberType = statement.getType({ ...options, typeChain: undefined });
316✔
3894
            memberType.parentEnumType = resultType;
316✔
3895
            resultType.addMember(statement?.tokens?.name?.text, { definingNode: statement }, memberType, SymbolTypeFlag.runtime);
316!
3896
        }
3897
        return resultType;
158✔
3898
    }
3899

3900
    public clone() {
3901
        return this.finalizeClone(
6✔
3902
            new EnumStatement({
3903
                enum: util.cloneToken(this.tokens.enum),
3904
                name: util.cloneToken(this.tokens.name),
3905
                endEnum: util.cloneToken(this.tokens.endEnum),
3906
                body: this.body?.map(x => x?.clone())
4✔
3907
            }),
3908
            ['body']
3909
        );
3910
    }
3911
}
3912

3913
export class EnumMemberStatement extends Statement implements TypedefProvider {
1✔
3914
    public constructor(options: {
3915
        name: Identifier;
3916
        equals?: Token;
3917
        value?: Expression;
3918
    }) {
3919
        super();
370✔
3920
        this.tokens = {
370✔
3921
            name: options.name,
3922
            equals: options.equals
3923
        };
3924
        this.value = options.value;
370✔
3925
    }
3926

3927
    public readonly tokens: {
3928
        readonly name: Identifier;
3929
        readonly equals?: Token;
3930
    };
3931
    public readonly value?: Expression;
3932

3933
    public readonly kind = AstNodeKind.EnumMemberStatement;
370✔
3934

3935
    public get location() {
3936
        return util.createBoundingLocation(
612✔
3937
            this.tokens.name,
3938
            this.tokens.equals,
3939
            this.value
3940
        );
3941
    }
3942

3943
    /**
3944
     * The name of the member
3945
     */
3946
    public get name() {
3947
        return this.tokens.name.text;
471✔
3948
    }
3949

3950
    public get leadingTrivia(): Token[] {
3951
        return this.tokens.name.leadingTrivia;
1,483✔
3952
    }
3953

3954
    public transpile(state: BrsTranspileState): TranspileResult {
UNCOV
3955
        return [];
×
3956
    }
3957

3958
    getTypedef(state: BrsTranspileState): TranspileResult {
3959
        const result: TranspileResult = [];
1✔
3960
        for (let comment of util.getLeadingComments(this) ?? []) {
1!
UNCOV
3961
            result.push(
×
3962
                comment.text,
3963
                state.newline,
3964
                state.indent()
3965
            );
3966
        }
3967
        result.push(this.tokens.name.text);
1✔
3968
        if (this.tokens.equals) {
1!
UNCOV
3969
            result.push(' ', this.tokens.equals.text, ' ');
×
UNCOV
3970
            if (this.value) {
×
UNCOV
3971
                result.push(
×
3972
                    ...this.value.transpile(state)
3973
                );
3974
            }
3975
        }
3976
        return result;
1✔
3977
    }
3978

3979
    walk(visitor: WalkVisitor, options: WalkOptions) {
3980
        if (this.value && options.walkMode & InternalWalkMode.walkExpressions) {
2,910✔
3981
            walk(this, 'value', visitor, options);
1,434✔
3982
        }
3983
    }
3984

3985
    getType(options: GetTypeOptions) {
3986
        return new EnumMemberType(
471✔
3987
            (this.parent as EnumStatement)?.fullName,
1,413!
3988
            this.tokens?.name?.text,
2,826!
3989
            this.value?.getType(options)
1,413✔
3990
        );
3991
    }
3992

3993
    public clone() {
3994
        return this.finalizeClone(
3✔
3995
            new EnumMemberStatement({
3996
                name: util.cloneToken(this.tokens.name),
3997
                equals: util.cloneToken(this.tokens.equals),
3998
                value: this.value?.clone()
9✔
3999
            }),
4000
            ['value']
4001
        );
4002
    }
4003
}
4004

4005
export class ConstStatement extends Statement implements TypedefProvider {
1✔
4006
    public constructor(options: {
4007
        const?: Token;
4008
        name: Identifier;
4009
        equals?: Token;
4010
        value: Expression;
4011
    }) {
4012
        super();
194✔
4013
        this.tokens = {
194✔
4014
            const: options.const,
4015
            name: options.name,
4016
            equals: options.equals
4017
        };
4018
        this.value = options.value;
194✔
4019
        this.location = util.createBoundingLocation(this.tokens.const, this.tokens.name, this.tokens.equals, this.value);
194✔
4020
    }
4021

4022
    public readonly tokens: {
4023
        readonly const: Token;
4024
        readonly name: Identifier;
4025
        readonly equals: Token;
4026
    };
4027
    public readonly value: Expression;
4028

4029
    public readonly kind = AstNodeKind.ConstStatement;
194✔
4030

4031
    public readonly location: Location | undefined;
4032

4033
    public get name() {
UNCOV
4034
        return this.tokens.name.text;
×
4035
    }
4036

4037
    public get leadingTrivia(): Token[] {
4038
        return this.tokens.const?.leadingTrivia;
606!
4039
    }
4040

4041
    /**
4042
     * The name of the statement WITH its leading namespace (if applicable)
4043
     */
4044
    public get fullName() {
4045
        const name = this.tokens.name?.text;
120!
4046
        if (name) {
120!
4047
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
120✔
4048
            if (namespace) {
120✔
4049
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
59✔
4050
                return `${namespaceName}.${name}`;
59✔
4051
            } else {
4052
                return name;
61✔
4053
            }
4054
        } else {
4055
            //return undefined which will allow outside callers to know that this doesn't have a name
UNCOV
4056
            return undefined;
×
4057
        }
4058
    }
4059

4060
    public transpile(state: BrsTranspileState): TranspileResult {
4061
        //const declarations don't exist at runtime, so just transpile comments and trivia
4062
        return [
49✔
4063
            state.transpileAnnotations(this),
4064
            state.transpileLeadingComments(this.tokens.const)
4065
        ];
4066
    }
4067

4068
    getTypedef(state: BrsTranspileState): TranspileResult {
4069
        const result: TranspileResult = [];
3✔
4070
        for (let comment of util.getLeadingComments(this) ?? []) {
3!
UNCOV
4071
            result.push(
×
4072
                comment.text,
4073
                state.newline,
4074
                state.indent()
4075
            );
4076
        }
4077
        result.push(
3✔
4078
            this.tokens.const ? state.tokenToSourceNode(this.tokens.const) : 'const',
3!
4079
            ' ',
4080
            state.tokenToSourceNode(this.tokens.name),
4081
            ' ',
4082
            this.tokens.equals ? state.tokenToSourceNode(this.tokens.equals) : '=',
3!
4083
            ' ',
4084
            ...this.value.transpile(state)
4085
        );
4086
        return result;
3✔
4087
    }
4088

4089
    walk(visitor: WalkVisitor, options: WalkOptions) {
4090
        if (this.value && options.walkMode & InternalWalkMode.walkExpressions) {
1,316✔
4091
            walk(this, 'value', visitor, options);
1,142✔
4092
        }
4093
    }
4094

4095
    getType(options: GetTypeOptions) {
4096
        return this.value.getType(options);
186✔
4097
    }
4098

4099
    public clone() {
4100
        return this.finalizeClone(
3✔
4101
            new ConstStatement({
4102
                const: util.cloneToken(this.tokens.const),
4103
                name: util.cloneToken(this.tokens.name),
4104
                equals: util.cloneToken(this.tokens.equals),
4105
                value: this.value?.clone()
9✔
4106
            }),
4107
            ['value']
4108
        );
4109
    }
4110
}
4111

4112
export class ContinueStatement extends Statement {
1✔
4113
    constructor(options: {
4114
        continue?: Token;
4115
        loopType: Token;
4116
    }
4117
    ) {
4118
        super();
13✔
4119
        this.tokens = {
13✔
4120
            continue: options.continue,
4121
            loopType: options.loopType
4122
        };
4123
        this.location = util.createBoundingLocation(
13✔
4124
            this.tokens.continue,
4125
            this.tokens.loopType
4126
        );
4127
    }
4128

4129
    public readonly tokens: {
4130
        readonly continue?: Token;
4131
        readonly loopType: Token;
4132
    };
4133

4134
    public readonly kind = AstNodeKind.ContinueStatement;
13✔
4135

4136
    public readonly location: Location | undefined;
4137

4138
    transpile(state: BrsTranspileState) {
4139
        return [
3✔
4140
            state.sourceNode(this.tokens.continue, this.tokens.continue?.text ?? 'continue'),
18!
4141
            this.tokens.loopType?.leadingWhitespace ?? ' ',
18✔
4142
            state.sourceNode(this.tokens.continue, this.tokens.loopType?.text)
9✔
4143
        ];
4144
    }
4145

4146
    walk(visitor: WalkVisitor, options: WalkOptions) {
4147
        //nothing to walk
4148
    }
4149

4150
    public get leadingTrivia(): Token[] {
4151
        return this.tokens.continue?.leadingTrivia ?? [];
86!
4152
    }
4153

4154
    public clone() {
4155
        return this.finalizeClone(
1✔
4156
            new ContinueStatement({
4157
                continue: util.cloneToken(this.tokens.continue),
4158
                loopType: util.cloneToken(this.tokens.loopType)
4159
            })
4160
        );
4161
    }
4162
}
4163

4164
export class TypecastStatement extends Statement {
1✔
4165
    constructor(options: {
4166
        typecast?: Token;
4167
        typecastExpression: TypecastExpression;
4168
    }
4169
    ) {
4170
        super();
27✔
4171
        this.tokens = {
27✔
4172
            typecast: options.typecast
4173
        };
4174
        this.typecastExpression = options.typecastExpression;
27✔
4175
        this.location = util.createBoundingLocation(
27✔
4176
            this.tokens.typecast,
4177
            this.typecastExpression
4178
        );
4179
    }
4180

4181
    public readonly tokens: {
4182
        readonly typecast?: Token;
4183
    };
4184

4185
    public readonly typecastExpression: TypecastExpression;
4186

4187
    public readonly kind = AstNodeKind.TypecastStatement;
27✔
4188

4189
    public readonly location: Location;
4190

4191
    transpile(state: BrsTranspileState) {
4192
        //the typecast statement is a comment just for debugging purposes
4193
        return [
2✔
4194
            state.transpileToken(this.tokens.typecast, 'typecast', true),
4195
            ' ',
4196
            this.typecastExpression.obj.transpile(state),
4197
            ' ',
4198
            state.transpileToken(this.typecastExpression.tokens.as, 'as'),
4199
            ' ',
4200
            this.typecastExpression.typeExpression.getName(ParseMode.BrighterScript)
4201
        ];
4202
    }
4203

4204
    walk(visitor: WalkVisitor, options: WalkOptions) {
4205
        if (options.walkMode & InternalWalkMode.walkExpressions) {
160✔
4206
            walk(this, 'typecastExpression', visitor, options);
147✔
4207
        }
4208
    }
4209

4210
    get leadingTrivia(): Token[] {
4211
        return this.tokens.typecast?.leadingTrivia ?? [];
119!
4212
    }
4213

4214
    getType(options: GetTypeOptions): BscType {
4215
        return this.typecastExpression.getType(options);
22✔
4216
    }
4217

4218
    public clone() {
4219
        return this.finalizeClone(
1✔
4220
            new TypecastStatement({
4221
                typecast: util.cloneToken(this.tokens.typecast),
4222
                typecastExpression: this.typecastExpression?.clone()
3!
4223
            }),
4224
            ['typecastExpression']
4225
        );
4226
    }
4227
}
4228

4229
export class ConditionalCompileErrorStatement extends Statement {
1✔
4230
    constructor(options: {
4231
        hashError?: Token;
4232
        message: Token;
4233
    }) {
4234
        super();
11✔
4235
        this.tokens = {
11✔
4236
            hashError: options.hashError,
4237
            message: options.message
4238
        };
4239
        this.location = util.createBoundingLocation(util.createBoundingLocationFromTokens(this.tokens));
11✔
4240
    }
4241

4242
    public readonly tokens: {
4243
        readonly hashError?: Token;
4244
        readonly message: Token;
4245
    };
4246

4247

4248
    public readonly kind = AstNodeKind.ConditionalCompileErrorStatement;
11✔
4249

4250
    public readonly location: Location | undefined;
4251

4252
    transpile(state: BrsTranspileState) {
4253
        return [
3✔
4254
            state.transpileToken(this.tokens.hashError, '#error'),
4255
            ' ',
4256
            state.transpileToken(this.tokens.message)
4257

4258
        ];
4259
    }
4260

4261
    walk(visitor: WalkVisitor, options: WalkOptions) {
4262
        // nothing to walk
4263
    }
4264

4265
    get leadingTrivia(): Token[] {
4266
        return this.tokens.hashError.leadingTrivia ?? [];
12!
4267
    }
4268

4269
    public clone() {
4270
        return this.finalizeClone(
1✔
4271
            new ConditionalCompileErrorStatement({
4272
                hashError: util.cloneToken(this.tokens.hashError),
4273
                message: util.cloneToken(this.tokens.message)
4274
            })
4275
        );
4276
    }
4277
}
4278

4279
export class AliasStatement extends Statement {
1✔
4280
    constructor(options: {
4281
        alias?: Token;
4282
        name: Token;
4283
        equals?: Token;
4284
        value: VariableExpression | DottedGetExpression;
4285
    }
4286
    ) {
4287
        super();
34✔
4288
        this.tokens = {
34✔
4289
            alias: options.alias,
4290
            name: options.name,
4291
            equals: options.equals
4292
        };
4293
        this.value = options.value;
34✔
4294
        this.location = util.createBoundingLocation(
34✔
4295
            this.tokens.alias,
4296
            this.tokens.name,
4297
            this.tokens.equals,
4298
            this.value
4299
        );
4300
    }
4301

4302
    public readonly tokens: {
4303
        readonly alias?: Token;
4304
        readonly name: Token;
4305
        readonly equals?: Token;
4306
    };
4307

4308
    public readonly value: Expression;
4309

4310
    public readonly kind = AstNodeKind.AliasStatement;
34✔
4311

4312
    public readonly location: Location;
4313

4314
    transpile(state: BrsTranspileState) {
4315
        //transpile to a comment just for debugging purposes
4316
        return [
12✔
4317
            state.transpileToken(this.tokens.alias, 'alias', true),
4318
            ' ',
4319
            state.transpileToken(this.tokens.name),
4320
            ' ',
4321
            state.transpileToken(this.tokens.equals, '='),
4322
            ' ',
4323
            this.value.transpile(state)
4324
        ];
4325
    }
4326

4327
    walk(visitor: WalkVisitor, options: WalkOptions) {
4328
        if (options.walkMode & InternalWalkMode.walkExpressions) {
235✔
4329
            walk(this, 'value', visitor, options);
206✔
4330
        }
4331
    }
4332

4333
    get leadingTrivia(): Token[] {
4334
        return this.tokens.alias?.leadingTrivia ?? [];
121!
4335
    }
4336

4337
    getType(options: GetTypeOptions): BscType {
4338
        return this.value.getType(options);
1✔
4339
    }
4340

4341
    public clone() {
4342
        return this.finalizeClone(
1✔
4343
            new AliasStatement({
4344
                alias: util.cloneToken(this.tokens.alias),
4345
                name: util.cloneToken(this.tokens.name),
4346
                equals: util.cloneToken(this.tokens.equals),
4347
                value: this.value?.clone()
3!
4348
            }),
4349
            ['value']
4350
        );
4351
    }
4352
}
4353

4354
export class ConditionalCompileStatement extends Statement {
1✔
4355
    constructor(options: {
4356
        hashIf?: Token;
4357
        not?: Token;
4358
        condition: Token;
4359
        hashElse?: Token;
4360
        hashEndIf?: Token;
4361
        thenBranch: Block;
4362
        elseBranch?: ConditionalCompileStatement | Block;
4363
    }) {
4364
        super();
58✔
4365
        this.thenBranch = options.thenBranch;
58✔
4366
        this.elseBranch = options.elseBranch;
58✔
4367

4368
        this.tokens = {
58✔
4369
            hashIf: options.hashIf,
4370
            not: options.not,
4371
            condition: options.condition,
4372
            hashElse: options.hashElse,
4373
            hashEndIf: options.hashEndIf
4374
        };
4375

4376
        this.location = util.createBoundingLocation(
58✔
4377
            util.createBoundingLocationFromTokens(this.tokens),
4378
            this.thenBranch,
4379
            this.elseBranch
4380
        );
4381
    }
4382

4383
    readonly tokens: {
4384
        readonly hashIf?: Token;
4385
        readonly not?: Token;
4386
        readonly condition: Token;
4387
        readonly hashElse?: Token;
4388
        readonly hashEndIf?: Token;
4389
    };
4390
    public readonly thenBranch: Block;
4391
    public readonly elseBranch?: ConditionalCompileStatement | Block;
4392

4393
    public readonly kind = AstNodeKind.ConditionalCompileStatement;
58✔
4394

4395
    public readonly location: Location | undefined;
4396

4397
    transpile(state: BrsTranspileState) {
4398
        let results = [] as TranspileResult;
6✔
4399
        //if   (already indented by block)
4400
        if (!state.conditionalCompileStatement) {
6✔
4401
            // only transpile the #if in the case when we're not in a conditionalCompileStatement already
4402
            results.push(state.transpileToken(this.tokens.hashIf ?? createToken(TokenKind.HashIf)));
3!
4403
        }
4404

4405
        results.push(' ');
6✔
4406
        //conditions
4407
        if (this.tokens.not) {
6✔
4408
            results.push('not');
2✔
4409
            results.push(' ');
2✔
4410
        }
4411
        results.push(state.transpileToken(this.tokens.condition));
6✔
4412
        state.lineage.unshift(this);
6✔
4413

4414
        //if statement body
4415
        let thenNodes = this.thenBranch.transpile(state);
6✔
4416
        state.lineage.shift();
6✔
4417
        if (thenNodes.length > 0) {
6!
4418
            results.push(thenNodes);
6✔
4419
        }
4420
        //else branch
4421
        if (this.elseBranch) {
6!
4422
            const elseIsCC = isConditionalCompileStatement(this.elseBranch);
6✔
4423
            const endBlockToken = elseIsCC ? (this.elseBranch as ConditionalCompileStatement).tokens.hashIf ?? createToken(TokenKind.HashElseIf) : this.tokens.hashElse;
6!
4424
            //else
4425

4426
            results.push(...state.transpileEndBlockToken(this.thenBranch, endBlockToken, createToken(TokenKind.HashElse).text));
6✔
4427

4428
            if (elseIsCC) {
6✔
4429
                //chained else if
4430
                state.lineage.unshift(this.elseBranch);
3✔
4431

4432
                // transpile following #if with knowledge of current
4433
                const existingCCStmt = state.conditionalCompileStatement;
3✔
4434
                state.conditionalCompileStatement = this;
3✔
4435
                let body = this.elseBranch.transpile(state);
3✔
4436
                state.conditionalCompileStatement = existingCCStmt;
3✔
4437

4438
                state.lineage.shift();
3✔
4439

4440
                if (body.length > 0) {
3!
4441
                    //zero or more spaces between the `else` and the `if`
4442
                    results.push(...body);
3✔
4443

4444
                    // stop here because chained if will transpile the rest
4445
                    return results;
3✔
4446
                } else {
UNCOV
4447
                    results.push('\n');
×
4448
                }
4449

4450
            } else {
4451
                //else body
4452
                state.lineage.unshift(this.tokens.hashElse!);
3✔
4453
                let body = this.elseBranch.transpile(state);
3✔
4454
                state.lineage.shift();
3✔
4455

4456
                if (body.length > 0) {
3!
4457
                    results.push(...body);
3✔
4458
                }
4459
            }
4460
        }
4461

4462
        //end if
4463
        results.push(...state.transpileEndBlockToken(this.elseBranch ?? this.thenBranch, this.tokens.hashEndIf, '#end if'));
3!
4464

4465
        return results;
3✔
4466
    }
4467

4468
    walk(visitor: WalkVisitor, options: WalkOptions) {
4469
        if (options.walkMode & InternalWalkMode.walkStatements) {
210!
4470
            const bsConsts = options.bsConsts ?? this.getBsConsts();
210✔
4471
            let conditionTrue = bsConsts?.get(this.tokens.condition.text.toLowerCase());
210✔
4472
            if (this.tokens.not) {
210✔
4473
                // flips the boolean value
4474
                conditionTrue = !conditionTrue;
25✔
4475
            }
4476
            const walkFalseBlocks = options.walkMode & InternalWalkMode.visitFalseConditionalCompilationBlocks;
210✔
4477
            if (conditionTrue || walkFalseBlocks) {
210✔
4478
                walk(this, 'thenBranch', visitor, options);
169✔
4479
            }
4480
            if (this.elseBranch && (!conditionTrue || walkFalseBlocks)) {
210✔
4481
                walk(this, 'elseBranch', visitor, options);
71✔
4482
            }
4483
        }
4484
    }
4485

4486
    get leadingTrivia(): Token[] {
4487
        return this.tokens.hashIf?.leadingTrivia ?? [];
175!
4488
    }
4489

4490
    public clone() {
4491
        return this.finalizeClone(
1✔
4492
            new ConditionalCompileStatement({
4493
                hashIf: util.cloneToken(this.tokens.hashIf),
4494
                not: util.cloneToken(this.tokens.not),
4495
                condition: util.cloneToken(this.tokens.condition),
4496
                hashElse: util.cloneToken(this.tokens.hashElse),
4497
                hashEndIf: util.cloneToken(this.tokens.hashEndIf),
4498
                thenBranch: this.thenBranch?.clone(),
3!
4499
                elseBranch: this.elseBranch?.clone()
3!
4500
            }),
4501
            ['thenBranch', 'elseBranch']
4502
        );
4503
    }
4504

4505
    public getBranchStatementIndex(stmt: Statement) {
UNCOV
4506
        if (this.thenBranch === stmt) {
×
UNCOV
4507
            return 0;
×
UNCOV
4508
        } else if (this.elseBranch === stmt) {
×
UNCOV
4509
            return 1;
×
4510
        }
UNCOV
4511
        return -1;
×
4512
    }
4513
}
4514

4515

4516
export class ConditionalCompileConstStatement extends Statement {
1✔
4517
    constructor(options: {
4518
        hashConst?: Token;
4519
        assignment: AssignmentStatement;
4520
    }) {
4521
        super();
19✔
4522
        this.tokens = {
19✔
4523
            hashConst: options.hashConst
4524
        };
4525
        this.assignment = options.assignment;
19✔
4526
        this.location = util.createBoundingLocation(util.createBoundingLocationFromTokens(this.tokens), this.assignment);
19✔
4527
    }
4528

4529
    public readonly tokens: {
4530
        readonly hashConst?: Token;
4531
    };
4532

4533
    public readonly assignment: AssignmentStatement;
4534

4535
    public readonly kind = AstNodeKind.ConditionalCompileConstStatement;
19✔
4536

4537
    public readonly location: Location | undefined;
4538

4539
    transpile(state: BrsTranspileState) {
4540
        return [
3✔
4541
            state.transpileToken(this.tokens.hashConst, '#const'),
4542
            ' ',
4543
            state.transpileToken(this.assignment.tokens.name),
4544
            ' ',
4545
            state.transpileToken(this.assignment.tokens.equals, '='),
4546
            ' ',
4547
            ...this.assignment.value.transpile(state)
4548
        ];
4549

4550
    }
4551

4552
    walk(visitor: WalkVisitor, options: WalkOptions) {
4553
        // nothing to walk
4554
    }
4555

4556

4557
    get leadingTrivia(): Token[] {
4558
        return this.tokens.hashConst.leadingTrivia ?? [];
78!
4559
    }
4560

4561
    public clone() {
4562
        return this.finalizeClone(
1✔
4563
            new ConditionalCompileConstStatement({
4564
                hashConst: util.cloneToken(this.tokens.hashConst),
4565
                assignment: this.assignment?.clone()
3!
4566
            }),
4567
            ['assignment']
4568
        );
4569
    }
4570
}
4571

4572

4573
export class TypeStatement extends Statement {
1✔
4574
    constructor(options: {
4575
        type?: Token;
4576
        name: Token;
4577
        equals?: Token;
4578
        value: TypeExpression;
4579
    }
4580
    ) {
4581
        super();
25✔
4582
        this.tokens = {
25✔
4583
            type: options.type,
4584
            name: options.name,
4585
            equals: options.equals
4586
        };
4587
        this.value = options.value;
25✔
4588
        this.location = util.createBoundingLocation(
25✔
4589
            this.tokens.type,
4590
            this.tokens.name,
4591
            this.tokens.equals,
4592
            this.value
4593
        );
4594
    }
4595

4596
    public readonly tokens: {
4597
        readonly type?: Token;
4598
        readonly name: Token;
4599
        readonly equals?: Token;
4600
    };
4601

4602
    public readonly value: TypeExpression;
4603

4604
    public readonly kind = AstNodeKind.TypeStatement;
25✔
4605

4606
    public readonly location: Location;
4607

4608
    transpile(state: BrsTranspileState) {
4609
        //transpile to a comment just for debugging purposes
UNCOV
4610
        return [
×
4611
            state.transpileToken(this.tokens.type, 'type', true),
4612
            ' ',
4613
            state.transpileToken(this.tokens.name),
4614
            ' ',
4615
            state.transpileToken(this.tokens.equals, '='),
4616
            ' ',
4617
            this.value.transpile(state)
4618
        ];
4619
    }
4620

4621
    walk(visitor: WalkVisitor, options: WalkOptions) {
4622
        if (options.walkMode & InternalWalkMode.walkExpressions) {
153✔
4623
            walk(this, 'value', visitor, options);
133✔
4624
        }
4625
    }
4626

4627
    get leadingTrivia(): Token[] {
4628
        return this.tokens.type?.leadingTrivia ?? [];
70!
4629
    }
4630

4631
    getType(options: GetTypeOptions): BscType {
4632
        return this.value.getType(options);
21✔
4633
    }
4634

4635
    public clone() {
UNCOV
4636
        return this.finalizeClone(
×
4637
            new TypeStatement({
4638
                type: util.cloneToken(this.tokens.type),
4639
                name: util.cloneToken(this.tokens.name),
4640
                equals: util.cloneToken(this.tokens.equals),
4641
                value: this.value?.clone()
×
4642
            }),
4643
            ['value']
4644
        );
4645
    }
4646
}
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