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

rokucommunity / brighterscript / #13071

25 Sep 2024 04:16PM UTC coverage: 86.525% (-1.4%) from 87.933%
#13071

push

web-flow
Merge c610b9e4e into 56dcaaa63

10903 of 13389 branches covered (81.43%)

Branch coverage included in aggregate %.

6936 of 7533 new or added lines in 100 files covered. (92.07%)

83 existing lines in 18 files now uncovered.

12548 of 13714 relevant lines covered (91.5%)

27591.0 hits per line

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

85.68
/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, FunctionExpression, FunctionParameterExpression, LiteralExpression, TypeExpression, TypecastExpression } from './Expression';
5
import { CallExpression, VariableExpression } from './Expression';
1✔
6
import { util } from '../util';
1✔
7
import type { Location } from 'vscode-languageserver';
8
import type { BrsTranspileState } from './BrsTranspileState';
9
import { ParseMode } from './Parser';
1✔
10
import type { WalkVisitor, WalkOptions } from '../astUtils/visitors';
11
import { InternalWalkMode, walk, createVisitor, WalkMode, walkArray } from '../astUtils/visitors';
1✔
12
import { isCallExpression, isCatchStatement, isConditionalCompileStatement, isEnumMemberStatement, isExpression, isExpressionStatement, isFieldStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isMethodStatement, isNamespaceStatement, isTryCatchStatement, isTypedefProvider, isUnaryExpression, isVoidType, isWhileStatement } from '../astUtils/reflection';
1✔
13
import { TypeChainEntry, type GetTypeOptions, type TranspileResult, type TypedefProvider } from '../interfaces';
1✔
14
import { createInvalidLiteral, createMethodStatement, createToken } from '../astUtils/creators';
1✔
15
import { DynamicType } from '../types/DynamicType';
1✔
16
import type { BscType } from '../types/BscType';
17
import { SymbolTable } from '../SymbolTable';
1✔
18
import type { Expression } from './AstNode';
19
import { AstNodeKind } from './AstNode';
1✔
20
import { Statement } from './AstNode';
1✔
21
import { ClassType } from '../types/ClassType';
1✔
22
import { EnumMemberType, EnumType } from '../types/EnumType';
1✔
23
import { NamespaceType } from '../types/NamespaceType';
1✔
24
import { InterfaceType } from '../types/InterfaceType';
1✔
25
import { VoidType } from '../types/VoidType';
1✔
26
import { TypedFunctionType } from '../types/TypedFunctionType';
1✔
27
import { ArrayType } from '../types/ArrayType';
1✔
28
import { SymbolTypeFlag } from '../SymbolTypeFlag';
1✔
29
import brsDocParser from './BrightScriptDocParser';
1✔
30

31
export class EmptyStatement extends Statement {
1✔
32
    constructor(options?: { range?: Location }
33
    ) {
34
        super();
4✔
35
        this.location = undefined;
4✔
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;
4✔
43

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

52
/**
53
 * This is a top-level statement. Consider this the root of the AST
54
 */
55
export class Body extends Statement implements TypedefProvider {
1✔
56
    constructor(options?: {
57
        statements?: Statement[];
58
    }) {
59
        super();
7,186✔
60
        this.statements = options?.statements ?? [];
7,186!
61
    }
62

63
    public readonly statements: Statement[] = [];
7,186✔
64
    public readonly kind = AstNodeKind.Body;
7,186✔
65

66
    public readonly symbolTable = new SymbolTable('Body', () => this.parent?.getSymbolTable());
29,915✔
67

68
    public get location() {
69
        //this needs to be a getter because the body has its statements pushed to it after being constructed
70
        return util.createBoundingLocation(
709✔
71
            ...(this.statements ?? [])
2,127!
72
        );
73
    }
74

75
    transpile(state: BrsTranspileState) {
76
        let result: TranspileResult = state.transpileAnnotations(this);
684✔
77
        for (let i = 0; i < this.statements.length; i++) {
684✔
78
            let statement = this.statements[i];
1,495✔
79
            let previousStatement = this.statements[i - 1];
1,495✔
80
            let nextStatement = this.statements[i + 1];
1,495✔
81

82
            if (!previousStatement) {
1,495✔
83
                //this is the first statement. do nothing related to spacing and newlines
84

85
                //if comment is on same line as prior sibling
86
            } else if (util.hasLeadingComments(statement) && previousStatement && util.getLeadingComments(statement)?.[0]?.location?.range?.start.line === previousStatement.location?.range?.end.line) {
816!
87
                result.push(
8✔
88
                    ' '
89
                );
90
                //add double newline if this is a comment, and next is a function
91
            } else if (util.hasLeadingComments(statement) && nextStatement && isFunctionStatement(nextStatement)) {
808✔
92
                result.push(state.newline, state.newline);
327✔
93

94
                //add double newline if is function not preceeded by a comment
95
            } else if (isFunctionStatement(statement) && previousStatement && !util.hasLeadingComments(statement)) {
481✔
96
                result.push(state.newline, state.newline);
80✔
97
            } else {
98
                //separate statements by a single newline
99
                result.push(state.newline);
401✔
100
            }
101

102
            result.push(...statement.transpile(state));
1,495✔
103
        }
104
        return result;
684✔
105
    }
106

107
    getTypedef(state: BrsTranspileState): TranspileResult {
108
        let result = [] as TranspileResult;
41✔
109
        for (const statement of this.statements) {
41✔
110
            //if the current statement supports generating typedef, call it
111
            if (isTypedefProvider(statement)) {
75!
112
                result.push(
75✔
113
                    state.indent(),
114
                    ...statement.getTypedef(state),
115
                    state.newline
116
                );
117
            }
118
        }
119
        return result;
41✔
120
    }
121

122
    walk(visitor: WalkVisitor, options: WalkOptions) {
123
        if (options.walkMode & InternalWalkMode.walkStatements) {
14,143!
124
            walkArray(this.statements, visitor, options, this);
14,143✔
125
        }
126
    }
127
}
128

129
export class AssignmentStatement extends Statement {
1✔
130
    constructor(options: {
131
        name: Identifier;
132
        equals?: Token;
133
        value: Expression;
134
        as?: Token;
135
        typeExpression?: TypeExpression;
136
    }) {
137
        super();
1,422✔
138
        this.value = options.value;
1,422✔
139
        this.tokens = {
1,422✔
140
            equals: options.equals,
141
            name: options.name,
142
            as: options.as
143
        };
144
        this.typeExpression = options.typeExpression;
1,422✔
145
        this.location = util.createBoundingLocation(util.createBoundingLocationFromTokens(this.tokens), this.value);
1,422✔
146
    }
147

148
    public readonly tokens: {
149
        readonly equals?: Token;
150
        readonly name: Identifier;
151
        readonly as?: Token;
152
    };
153

154
    public readonly value: Expression;
155

156
    public readonly typeExpression?: TypeExpression;
157

158
    public readonly kind = AstNodeKind.AssignmentStatement;
1,422✔
159

160
    public readonly location: Location | undefined;
161

162
    transpile(state: BrsTranspileState) {
163
        return [
558✔
164
            state.transpileToken(this.tokens.name),
165
            ' ',
166
            state.transpileToken(this.tokens.equals ?? createToken(TokenKind.Equal)),
1,674!
167
            ' ',
168
            ...this.value.transpile(state)
169
        ];
170
    }
171

172
    walk(visitor: WalkVisitor, options: WalkOptions) {
173
        if (options.walkMode & InternalWalkMode.walkExpressions) {
5,558✔
174
            walk(this, 'typeExpression', visitor, options);
5,432✔
175
            walk(this, 'value', visitor, options);
5,432✔
176
        }
177
    }
178

179
    getType(options: GetTypeOptions) {
180
        const variableTypeFromCode = this.typeExpression?.getType({ ...options, typeChain: undefined });
1,266✔
181
        const docs = brsDocParser.parseNode(this);
1,266✔
182
        const variableTypeFromDocs = docs?.getTypeTagBscType(options);
1,266!
183
        const variableType = util.chooseTypeFromCodeOrDocComment(variableTypeFromCode, variableTypeFromDocs, options) ?? this.value.getType({ ...options, typeChain: undefined });
1,266✔
184

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

191
    get leadingTrivia(): Token[] {
192
        return this.tokens.name.leadingTrivia;
7,217✔
193
    }
194
}
195

196
export class AugmentedAssignmentStatement extends Statement {
1✔
197
    constructor(options: {
198
        item: Expression;
199
        operator: Token;
200
        value: Expression;
201
    }) {
202
        super();
78✔
203
        this.value = options.value;
78✔
204
        this.tokens = {
78✔
205
            operator: options.operator
206
        };
207
        this.item = options.item;
78✔
208
        this.value = options.value;
78✔
209
        this.location = util.createBoundingLocation(this.item, util.createBoundingLocationFromTokens(this.tokens), this.value);
78✔
210
    }
211

212
    public readonly tokens: {
213
        readonly operator?: Token;
214
    };
215

216
    public readonly item: Expression;
217

218
    public readonly value: Expression;
219

220
    public readonly kind = AstNodeKind.AugmentedAssignmentStatement;
78✔
221

222
    public readonly location: Location | undefined;
223

224
    transpile(state: BrsTranspileState) {
225
        return [
27✔
226
            this.item.transpile(state),
227
            ' ',
228
            state.transpileToken(this.tokens.operator),
229
            ' ',
230
            this.value.transpile(state)
231
        ];
232
    }
233

234
    walk(visitor: WalkVisitor, options: WalkOptions) {
235
        if (options.walkMode & InternalWalkMode.walkExpressions) {
330✔
236
            walk(this, 'item', visitor, options);
322✔
237
            walk(this, 'value', visitor, options);
322✔
238
        }
239
    }
240

241
    getType(options: GetTypeOptions) {
NEW
242
        const variableType = util.binaryOperatorResultType(this.item.getType(options), this.tokens.operator, this.value.getType(options));
×
243

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

246
        // Note: compound assignments (eg. +=) are internally dealt with via the RHS being a BinaryExpression
247
        // so this.value will be a BinaryExpression, and BinaryExpressions can figure out their own types
248
        // options.typeChain?.push(new TypeChainEntry({ name: this.tokens.name.text, type: variableType, data: options.data, range: this.tokens.name.range, astNode: this }));
NEW
249
        return variableType;
×
250
    }
251

252
    get leadingTrivia(): Token[] {
253
        return this.item.leadingTrivia;
340✔
254
    }
255
}
256

257
export class Block extends Statement {
1✔
258
    constructor(options: {
259
        statements: Statement[];
260
    }) {
261
        super();
6,249✔
262
        this.statements = options.statements;
6,249✔
263
    }
264

265
    public readonly statements: Statement[];
266

267
    public readonly kind = AstNodeKind.Block;
6,249✔
268

269
    get location(): Location {
270
        if (this.statements.length > 0) {
3,916✔
271
            return util.createBoundingLocation(...this.statements);
3,454✔
272
        }
273
        let lastBitBefore: Location;
274
        let firstBitAfter: Location;
275

276
        if (isFunctionExpression(this.parent)) {
462✔
277
            lastBitBefore = util.createBoundingLocation(
392✔
278
                this.parent.tokens.functionType,
279
                this.parent.tokens.leftParen,
280
                ...(this.parent.parameters ?? []),
1,176!
281
                this.parent.tokens.rightParen,
282
                this.parent.tokens.as,
283
                this.parent.returnTypeExpression
284
            );
285
            firstBitAfter = this.parent.tokens.endFunctionType?.location;
392!
286
        } else if (isIfStatement(this.parent)) {
70!
NEW
287
            if (this.parent.thenBranch === this) {
×
NEW
288
                lastBitBefore = util.createBoundingLocation(
×
289
                    this.parent.tokens.then,
290
                    this.parent.condition
291
                );
NEW
292
                firstBitAfter = util.createBoundingLocation(
×
293
                    this.parent.tokens.else,
294
                    this.parent.elseBranch,
295
                    this.parent.tokens.endIf
296
                );
NEW
297
            } else if (this.parent.elseBranch === this) {
×
NEW
298
                lastBitBefore = this.parent.tokens.else?.location;
×
NEW
299
                firstBitAfter = this.parent.tokens.endIf?.location;
×
300
            }
301
        } else if (isConditionalCompileStatement(this.parent)) {
70!
NEW
302
            if (this.parent.thenBranch === this) {
×
NEW
303
                lastBitBefore = util.createBoundingLocation(
×
304
                    this.parent.tokens.condition,
305
                    this.parent.tokens.not,
306
                    this.parent.tokens.hashIf
307
                );
NEW
308
                firstBitAfter = util.createBoundingLocation(
×
309
                    this.parent.tokens.hashElse,
310
                    this.parent.elseBranch,
311
                    this.parent.tokens.hashEndIf
312
                );
NEW
313
            } else if (this.parent.elseBranch === this) {
×
NEW
314
                lastBitBefore = this.parent.tokens.hashElse?.location;
×
NEW
315
                firstBitAfter = this.parent.tokens.hashEndIf?.location;
×
316
            }
317
        } else if (isForStatement(this.parent)) {
70✔
318
            lastBitBefore = util.createBoundingLocation(
2✔
319
                this.parent.increment,
320
                this.parent.tokens.step,
321
                this.parent.finalValue,
322
                this.parent.tokens.to,
323
                this.parent.counterDeclaration,
324
                this.parent.tokens.for
325
            );
326
            firstBitAfter = this.parent.tokens.endFor?.location;
2!
327
        } else if (isForEachStatement(this.parent)) {
68✔
328
            lastBitBefore = util.createBoundingLocation(
2✔
329
                this.parent.target,
330
                this.parent.tokens.in,
331
                this.parent.tokens.item,
332
                this.parent.tokens.forEach
333
            );
334
            firstBitAfter = this.parent.tokens.endFor?.location;
2!
335
        } else if (isWhileStatement(this.parent)) {
66!
NEW
336
            lastBitBefore = util.createBoundingLocation(
×
337
                this.parent.condition,
338
                this.parent.tokens.while
339
            );
NEW
340
            firstBitAfter = this.parent.tokens.endWhile?.location;
×
341
        } else if (isTryCatchStatement(this.parent)) {
66!
NEW
342
            lastBitBefore = util.createBoundingLocation(
×
343
                this.parent.tokens.try
344
            );
NEW
345
            firstBitAfter = util.createBoundingLocation(
×
346
                this.parent.tokens.endTry,
347
                this.parent.catchStatement
348
            );
349
        } else if (isCatchStatement(this.parent) && isTryCatchStatement(this.parent?.parent)) {
66!
NEW
350
            lastBitBefore = util.createBoundingLocation(
×
351
                this.parent.tokens.catch,
352
                this.parent.tokens.exceptionVariable
353
            );
NEW
354
            firstBitAfter = this.parent.parent.tokens.endTry?.location;
×
355
        }
356
        if (lastBitBefore?.range && firstBitAfter?.range) {
462!
357
            return util.createLocation(
372✔
358
                lastBitBefore.range.end.line,
359
                lastBitBefore.range.end.character,
360
                firstBitAfter.range.start.line,
361
                firstBitAfter.range.start.character,
362
                lastBitBefore.uri ?? firstBitAfter.uri
1,116!
363
            );
364
        }
365
    }
366

367
    transpile(state: BrsTranspileState) {
368
        state.blockDepth++;
4,080✔
369
        let results = [] as TranspileResult;
4,080✔
370
        for (let i = 0; i < this.statements.length; i++) {
4,080✔
371
            let previousStatement = this.statements[i - 1];
4,932✔
372
            let statement = this.statements[i];
4,932✔
373
            //is not a comment
374
            //if comment is on same line as parent
375
            if (util.isLeadingCommentOnSameLine(state.lineage[0]?.location, statement) ||
4,932!
376
                util.isLeadingCommentOnSameLine(previousStatement?.location, statement)
14,748✔
377
            ) {
378
                results.push(' ');
50✔
379

380
                //is not a comment
381
            } else {
382
                //add a newline and indent
383
                results.push(
4,882✔
384
                    state.newline,
385
                    state.indent()
386
                );
387
            }
388

389
            //push block onto parent list
390
            state.lineage.unshift(this);
4,932✔
391
            results.push(
4,932✔
392
                ...statement.transpile(state)
393
            );
394
            state.lineage.shift();
4,932✔
395
        }
396
        state.blockDepth--;
4,080✔
397
        return results;
4,080✔
398
    }
399

400
    public get leadingTrivia(): Token[] {
401
        return this.statements[0]?.leadingTrivia ?? [];
9,685✔
402
    }
403

404
    walk(visitor: WalkVisitor, options: WalkOptions) {
405
        if (options.walkMode & InternalWalkMode.walkStatements) {
24,401✔
406
            walkArray(this.statements, visitor, options, this);
24,395✔
407
        }
408
    }
409

410
}
411

412
export class ExpressionStatement extends Statement {
1✔
413
    constructor(options: {
414
        expression: Expression;
415
    }) {
416
        super();
762✔
417
        this.expression = options.expression;
762✔
418
        this.location = this.expression.location;
762✔
419
    }
420
    public readonly expression: Expression;
421
    public readonly kind = AstNodeKind.ExpressionStatement;
762✔
422

423
    public readonly location: Location | undefined;
424

425
    transpile(state: BrsTranspileState) {
426
        return [
60✔
427
            state.transpileAnnotations(this),
428
            this.expression.transpile(state)
429
        ];
430
    }
431

432
    walk(visitor: WalkVisitor, options: WalkOptions) {
433
        if (options.walkMode & InternalWalkMode.walkExpressions) {
3,048✔
434
            walk(this, 'expression', visitor, options);
3,020✔
435
        }
436
    }
437

438
    get leadingTrivia(): Token[] {
439
        return this.expression.leadingTrivia;
3,195✔
440
    }
441
}
442

443
export class ExitStatement extends Statement {
1✔
444
    constructor(options?: {
445
        exit?: Token;
446
        loopType: Token;
447
    }) {
448
        super();
22✔
449
        this.tokens = {
22✔
450
            exit: options?.exit,
66!
451
            loopType: options.loopType
452
        };
453
        this.location = util.createBoundingLocation(
22✔
454
            this.tokens.exit,
455
            this.tokens.loopType
456
        );
457
    }
458

459
    public readonly tokens: {
460
        readonly exit: Token;
461
        readonly loopType?: Token;
462
    };
463

464
    public readonly kind = AstNodeKind.ExitStatement;
22✔
465

466
    public readonly location?: Location;
467

468
    transpile(state: BrsTranspileState) {
469
        return [
11✔
470
            state.transpileToken(this.tokens.exit, 'exit'),
471
            this.tokens.loopType?.leadingWhitespace ?? ' ',
66!
472
            state.transpileToken(this.tokens.loopType)
473
        ];
474
    }
475

476
    walk(visitor: WalkVisitor, options: WalkOptions) {
477
        //nothing to walk
478
    }
479

480
    get leadingTrivia(): Token[] {
481
        return this.tokens.exit?.leadingTrivia;
124!
482
    }
483
}
484

485
export class FunctionStatement extends Statement implements TypedefProvider {
1✔
486
    constructor(options: {
487
        name: Identifier;
488
        func: FunctionExpression;
489
    }) {
490
        super();
3,689✔
491
        this.tokens = {
3,689✔
492
            name: options.name
493
        };
494
        this.func = options.func;
3,689✔
495
        this.func.symbolTable.name += `: '${this.tokens.name?.text}'`;
3,689!
496

497
        this.location = this.func.location;
3,689✔
498
    }
499

500
    public readonly tokens: {
501
        readonly name: Identifier;
502
    };
503
    public readonly func: FunctionExpression;
504

505
    public readonly kind = AstNodeKind.FunctionStatement as AstNodeKind;
3,689✔
506

507
    public readonly location: Location | undefined;
508

509
    /**
510
     * Get the name of this expression based on the parse mode
511
     */
512
    public getName(parseMode: ParseMode) {
513
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
11,615✔
514
        if (namespace) {
11,615✔
515
            let delimiter = parseMode === ParseMode.BrighterScript ? '.' : '_';
2,854✔
516
            let namespaceName = namespace.getName(parseMode);
2,854✔
517
            return namespaceName + delimiter + this.tokens.name?.text;
2,854!
518
        } else {
519
            return this.tokens.name.text;
8,761✔
520
        }
521
    }
522

523
    public get leadingTrivia(): Token[] {
524
        return this.func.leadingTrivia;
15,688✔
525
    }
526

527
    transpile(state: BrsTranspileState): TranspileResult {
528
        //create a fake token using the full transpiled name
529
        let nameToken = {
1,294✔
530
            ...this.tokens.name,
531
            text: this.getName(ParseMode.BrightScript)
532
        };
533

534
        return [
1,294✔
535
            ...state.transpileAnnotations(this),
536
            ...this.func.transpile(state, nameToken)
537
        ];
538
    }
539

540
    getTypedef(state: BrsTranspileState) {
541
        let result: TranspileResult = [];
39✔
542
        for (let comment of util.getLeadingComments(this) ?? []) {
39!
543
            result.push(
154✔
544
                comment.text,
545
                state.newline,
546
                state.indent()
547
            );
548
        }
549
        for (let annotation of this.annotations ?? []) {
39✔
550
            result.push(
2✔
551
                ...annotation.getTypedef(state),
552
                state.newline,
553
                state.indent()
554
            );
555
        }
556

557
        result.push(
39✔
558
            ...this.func.getTypedef(state)
559
        );
560
        return result;
39✔
561
    }
562

563
    walk(visitor: WalkVisitor, options: WalkOptions) {
564
        if (options.walkMode & InternalWalkMode.walkExpressions) {
15,160✔
565
            walk(this, 'func', visitor, options);
13,453✔
566
        }
567
    }
568

569
    getType(options: GetTypeOptions) {
570
        const funcExprType = this.func.getType(options);
3,183✔
571
        funcExprType.setName(this.getName(ParseMode.BrighterScript));
3,183✔
572
        return funcExprType;
3,183✔
573
    }
574
}
575

576
export class IfStatement extends Statement {
1✔
577
    constructor(options: {
578
        if?: Token;
579
        then?: Token;
580
        else?: Token;
581
        endIf?: Token;
582

583
        condition: Expression;
584
        thenBranch: Block;
585
        elseBranch?: IfStatement | Block;
586
    }) {
587
        super();
1,956✔
588
        this.condition = options.condition;
1,956✔
589
        this.thenBranch = options.thenBranch;
1,956✔
590
        this.elseBranch = options.elseBranch;
1,956✔
591

592
        this.tokens = {
1,956✔
593
            if: options.if,
594
            then: options.then,
595
            else: options.else,
596
            endIf: options.endIf
597
        };
598

599
        this.location = util.createBoundingLocation(
1,956✔
600
            util.createBoundingLocationFromTokens(this.tokens),
601
            this.condition,
602
            this.thenBranch,
603
            this.elseBranch
604
        );
605
    }
606

607
    readonly tokens: {
608
        readonly if?: Token;
609
        readonly then?: Token;
610
        readonly else?: Token;
611
        readonly endIf?: Token;
612
    };
613
    public readonly condition: Expression;
614
    public readonly thenBranch: Block;
615
    public readonly elseBranch?: IfStatement | Block;
616

617
    public readonly kind = AstNodeKind.IfStatement;
1,956✔
618

619
    public readonly location: Location | undefined;
620

621
    get isInline() {
622
        const allLeadingTrivia = [
12✔
623
            ...this.thenBranch.leadingTrivia,
624
            ...this.thenBranch.statements.map(s => s.leadingTrivia).flat(),
16✔
625
            ...(this.tokens.else?.leadingTrivia ?? []),
72✔
626
            ...(this.tokens.endIf?.leadingTrivia ?? [])
72✔
627
        ];
628

629
        const hasNewline = allLeadingTrivia.find(t => t.kind === TokenKind.Newline);
29✔
630
        return !hasNewline;
12✔
631
    }
632

633
    transpile(state: BrsTranspileState) {
634
        let results = [] as TranspileResult;
2,010✔
635
        //if   (already indented by block)
636
        results.push(state.transpileToken(this.tokens.if ?? createToken(TokenKind.If)));
2,010!
637
        results.push(' ');
2,010✔
638
        //conditions
639
        results.push(...this.condition.transpile(state));
2,010✔
640
        //then
641
        if (this.tokens.then) {
2,010✔
642
            results.push(' ');
1,672✔
643
            results.push(
1,672✔
644
                state.transpileToken(this.tokens.then)
645
            );
646
        }
647
        state.lineage.unshift(this);
2,010✔
648

649
        //if statement body
650
        let thenNodes = this.thenBranch.transpile(state);
2,010✔
651
        state.lineage.shift();
2,010✔
652
        if (thenNodes.length > 0) {
2,010✔
653
            results.push(thenNodes);
1,999✔
654
        }
655
        //else branch
656
        if (this.elseBranch) {
2,010✔
657
            //else
658
            results.push(...state.transpileEndBlockToken(this.thenBranch, this.tokens.else, 'else'));
1,664✔
659

660
            if (isIfStatement(this.elseBranch)) {
1,664✔
661
                //chained elseif
662
                state.lineage.unshift(this.elseBranch);
1,000✔
663
                let body = this.elseBranch.transpile(state);
1,000✔
664
                state.lineage.shift();
1,000✔
665

666
                if (body.length > 0) {
1,000!
667
                    //zero or more spaces between the `else` and the `if`
668
                    results.push(this.elseBranch.tokens.if.leadingWhitespace!);
1,000✔
669
                    results.push(...body);
1,000✔
670

671
                    // stop here because chained if will transpile the rest
672
                    return results;
1,000✔
673
                } else {
674
                    results.push('\n');
×
675
                }
676

677
            } else {
678
                //else body
679
                state.lineage.unshift(this.tokens.else!);
664✔
680
                let body = this.elseBranch.transpile(state);
664✔
681
                state.lineage.shift();
664✔
682

683
                if (body.length > 0) {
664✔
684
                    results.push(...body);
662✔
685
                }
686
            }
687
        }
688

689
        //end if
690
        results.push(...state.transpileEndBlockToken(this.elseBranch ?? this.thenBranch, this.tokens.endIf, 'end if'));
1,010✔
691

692
        return results;
1,010✔
693
    }
694

695
    walk(visitor: WalkVisitor, options: WalkOptions) {
696
        if (options.walkMode & InternalWalkMode.walkExpressions) {
6,196✔
697
            walk(this, 'condition', visitor, options);
6,180✔
698
        }
699
        if (options.walkMode & InternalWalkMode.walkStatements) {
6,196✔
700
            walk(this, 'thenBranch', visitor, options);
6,194✔
701
        }
702
        if (this.elseBranch && options.walkMode & InternalWalkMode.walkStatements) {
6,196✔
703
            walk(this, 'elseBranch', visitor, options);
4,895✔
704
        }
705
    }
706

707
    get leadingTrivia(): Token[] {
708
        return this.tokens.if?.leadingTrivia ?? [];
2,598!
709
    }
710

711
    get endTrivia(): Token[] {
712
        return this.tokens.endIf?.leadingTrivia ?? [];
1!
713
    }
714

715
}
716

717
export class IncrementStatement extends Statement {
1✔
718
    constructor(options: {
719
        value: Expression;
720
        operator: Token;
721
    }) {
722
        super();
23✔
723
        this.value = options.value;
23✔
724
        this.tokens = {
23✔
725
            operator: options.operator
726
        };
727
        this.location = util.createBoundingLocation(
23✔
728
            this.value,
729
            this.tokens.operator
730
        );
731
    }
732

733
    public readonly value: Expression;
734
    public readonly tokens: {
735
        readonly operator: Token;
736
    };
737

738
    public readonly kind = AstNodeKind.IncrementStatement;
23✔
739

740
    public readonly location: Location | undefined;
741

742
    transpile(state: BrsTranspileState) {
743
        return [
6✔
744
            ...this.value.transpile(state),
745
            state.transpileToken(this.tokens.operator)
746
        ];
747
    }
748

749
    walk(visitor: WalkVisitor, options: WalkOptions) {
750
        if (options.walkMode & InternalWalkMode.walkExpressions) {
75✔
751
            walk(this, 'value', visitor, options);
74✔
752
        }
753
    }
754

755
    get leadingTrivia(): Token[] {
756
        return this.value?.leadingTrivia ?? [];
84!
757
    }
758
}
759

760
/** Used to indent the current `print` position to the next 16-character-width output zone. */
761
export interface PrintSeparatorTab extends Token {
762
    kind: TokenKind.Comma;
763
}
764

765
/** Used to insert a single whitespace character at the current `print` position. */
766
export interface PrintSeparatorSpace extends Token {
767
    kind: TokenKind.Semicolon;
768
}
769

770
/**
771
 * Represents a `print` statement within BrightScript.
772
 */
773
export class PrintStatement extends Statement {
1✔
774
    /**
775
     * Creates a new internal representation of a BrightScript `print` statement.
776
     * @param options the options for this statement
777
     * @param options.print a print token
778
     * @param options.expressions an array of expressions or `PrintSeparator`s to be evaluated and printed.
779
     */
780
    constructor(options: {
781
        print: Token;
782
        expressions: Array<Expression | PrintSeparatorTab | PrintSeparatorSpace>;
783
    }) {
784
        super();
1,084✔
785
        this.tokens = {
1,084✔
786
            print: options.print
787
        };
788
        this.expressions = options.expressions;
1,084✔
789
        this.location = util.createBoundingLocation(
1,084✔
790
            this.tokens.print,
791
            ...(this.expressions ?? [])
3,252!
792
        );
793
    }
794
    public readonly tokens: {
795
        readonly print: Token;
796
    };
797
    public readonly expressions: Array<Expression | PrintSeparatorTab | PrintSeparatorSpace>;
798
    public readonly kind = AstNodeKind.PrintStatement;
1,084✔
799

800
    public readonly location: Location | undefined;
801

802
    transpile(state: BrsTranspileState) {
803
        let result = [
223✔
804
            state.transpileToken(this.tokens.print),
805
            ' '
806
        ] as TranspileResult;
807
        for (let i = 0; i < this.expressions.length; i++) {
223✔
808
            const expressionOrSeparator: any = this.expressions[i];
287✔
809
            if (expressionOrSeparator.transpile) {
287✔
810
                result.push(...(expressionOrSeparator as ExpressionStatement).transpile(state));
261✔
811
            } else {
812
                result.push(
26✔
813
                    state.tokenToSourceNode(expressionOrSeparator)
814
                );
815
            }
816
            //if there's an expression after us, add a space
817
            if ((this.expressions[i + 1] as any)?.transpile) {
287✔
818
                result.push(' ');
38✔
819
            }
820
        }
821
        return result;
223✔
822
    }
823

824
    walk(visitor: WalkVisitor, options: WalkOptions) {
825
        if (options.walkMode & InternalWalkMode.walkExpressions) {
5,080✔
826
            //sometimes we have semicolon Tokens in the expressions list (should probably fix that...), so only walk the actual expressions
827
            walkArray(this.expressions, visitor, options, this, (item) => isExpression(item as any));
5,547✔
828
        }
829
    }
830

831
    get leadingTrivia(): Token[] {
832
        return this.tokens.print?.leadingTrivia ?? [];
6,874!
833
    }
834
}
835

836
export class DimStatement extends Statement {
1✔
837
    constructor(options: {
838
        dim?: Token;
839
        name: Identifier;
840
        openingSquare?: Token;
841
        dimensions: Expression[];
842
        closingSquare?: Token;
843
    }) {
844
        super();
40✔
845
        this.tokens = {
40✔
846
            dim: options?.dim,
120!
847
            name: options.name,
848
            openingSquare: options.openingSquare,
849
            closingSquare: options.closingSquare
850
        };
851
        this.dimensions = options.dimensions;
40✔
852
        this.location = util.createBoundingLocation(
40✔
853
            options.dim,
854
            options.name,
855
            options.openingSquare,
856
            ...(this.dimensions ?? []),
120!
857
            options.closingSquare
858
        );
859
    }
860

861
    public readonly tokens: {
862
        readonly dim?: Token;
863
        readonly name: Identifier;
864
        readonly openingSquare?: Token;
865
        readonly closingSquare?: Token;
866
    };
867
    public readonly dimensions: Expression[];
868

869
    public readonly kind = AstNodeKind.DimStatement;
40✔
870

871
    public readonly location: Location | undefined;
872

873
    public transpile(state: BrsTranspileState) {
874
        let result: TranspileResult = [
15✔
875
            state.transpileToken(this.tokens.dim, 'dim'),
876
            ' ',
877
            state.transpileToken(this.tokens.name),
878
            state.transpileToken(this.tokens.openingSquare, '[')
879
        ];
880
        for (let i = 0; i < this.dimensions.length; i++) {
15✔
881
            if (i > 0) {
32✔
882
                result.push(', ');
17✔
883
            }
884
            result.push(
32✔
885
                ...this.dimensions![i].transpile(state)
886
            );
887
        }
888
        result.push(state.transpileToken(this.tokens.closingSquare, ']'));
15✔
889
        return result;
15✔
890
    }
891

892
    public walk(visitor: WalkVisitor, options: WalkOptions) {
893
        if (this.dimensions?.length !== undefined && this.dimensions?.length > 0 && options.walkMode & InternalWalkMode.walkExpressions) {
131!
894
            walkArray(this.dimensions, visitor, options, this);
116✔
895

896
        }
897
    }
898

899
    public getType(options: GetTypeOptions): BscType {
900
        const numDimensions = this.dimensions?.length ?? 1;
18!
901
        let type = new ArrayType();
18✔
902
        for (let i = 0; i < numDimensions - 1; i++) {
18✔
903
            type = new ArrayType(type);
17✔
904
        }
905
        return type;
18✔
906
    }
907

908
    get leadingTrivia(): Token[] {
909
        return this.tokens.dim?.leadingTrivia ?? [];
137!
910
    }
911
}
912

913
export class GotoStatement extends Statement {
1✔
914
    constructor(options: {
915
        goto?: Token;
916
        label: Token;
917
    }) {
918
        super();
11✔
919
        this.tokens = {
11✔
920
            goto: options.goto,
921
            label: options.label
922
        };
923
        this.location = util.createBoundingLocation(
11✔
924
            this.tokens.goto,
925
            this.tokens.label
926
        );
927
    }
928

929
    public readonly tokens: {
930
        readonly goto?: Token;
931
        readonly label: Token;
932
    };
933

934
    public readonly kind = AstNodeKind.GotoStatement;
11✔
935

936
    public readonly location: Location | undefined;
937

938
    transpile(state: BrsTranspileState) {
939
        return [
2✔
940
            state.transpileToken(this.tokens.goto, 'goto'),
941
            ' ',
942
            state.transpileToken(this.tokens.label)
943
        ];
944
    }
945

946
    walk(visitor: WalkVisitor, options: WalkOptions) {
947
        //nothing to walk
948
    }
949

950
    get leadingTrivia(): Token[] {
951
        return this.tokens.goto?.leadingTrivia ?? [];
19!
952
    }
953
}
954

955
export class LabelStatement extends Statement {
1✔
956
    constructor(options: {
957
        name: Token;
958
        colon?: Token;
959
    }) {
960
        super();
11✔
961
        this.tokens = {
11✔
962
            name: options.name,
963
            colon: options.colon
964
        };
965
        this.location = util.createBoundingLocation(
11✔
966
            this.tokens.name,
967
            this.tokens.colon
968
        );
969
    }
970
    public readonly tokens: {
971
        readonly name: Token;
972
        readonly colon: Token;
973
    };
974
    public readonly kind = AstNodeKind.LabelStatement;
11✔
975

976
    public readonly location: Location | undefined;
977

978
    public get leadingTrivia(): Token[] {
979
        return this.tokens.name.leadingTrivia;
29✔
980
    }
981

982
    transpile(state: BrsTranspileState) {
983
        return [
2✔
984
            state.transpileToken(this.tokens.name),
985
            state.transpileToken(this.tokens.colon, ':')
986

987
        ];
988
    }
989

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

995
export class ReturnStatement extends Statement {
1✔
996
    constructor(options?: {
997
        return?: Token;
998
        value?: Expression;
999
    }) {
1000
        super();
3,011✔
1001
        this.tokens = {
3,011✔
1002
            return: options?.return
9,033!
1003
        };
1004
        this.value = options?.value;
3,011!
1005
        this.location = util.createBoundingLocation(
3,011✔
1006
            this.tokens.return,
1007
            this.value
1008
        );
1009
    }
1010

1011
    public readonly tokens: {
1012
        readonly return?: Token;
1013
    };
1014
    public readonly value?: Expression;
1015
    public readonly kind = AstNodeKind.ReturnStatement;
3,011✔
1016

1017
    public readonly location: Location | undefined;
1018

1019
    transpile(state: BrsTranspileState) {
1020
        let result = [] as TranspileResult;
2,963✔
1021
        result.push(
2,963✔
1022
            state.transpileToken(this.tokens.return, 'return')
1023
        );
1024
        if (this.value) {
2,963✔
1025
            result.push(' ');
2,962✔
1026
            result.push(...this.value.transpile(state));
2,962✔
1027
        }
1028
        return result;
2,963✔
1029
    }
1030

1031
    walk(visitor: WalkVisitor, options: WalkOptions) {
1032
        if (options.walkMode & InternalWalkMode.walkExpressions) {
10,129✔
1033
            walk(this, 'value', visitor, options);
10,111✔
1034
        }
1035
    }
1036

1037
    get leadingTrivia(): Token[] {
1038
        return this.tokens.return?.leadingTrivia ?? [];
8,525!
1039
    }
1040
}
1041

1042
export class EndStatement extends Statement {
1✔
1043
    constructor(options?: {
1044
        end?: Token;
1045
    }) {
1046
        super();
9✔
1047
        this.tokens = {
9✔
1048
            end: options?.end
27!
1049
        };
1050
        this.location = this.tokens.end?.location;
9!
1051
    }
1052
    public readonly tokens: {
1053
        readonly end?: Token;
1054
    };
1055
    public readonly kind = AstNodeKind.EndStatement;
9✔
1056

1057
    public readonly location: Location;
1058

1059
    transpile(state: BrsTranspileState) {
1060
        return [
2✔
1061
            state.transpileToken(this.tokens.end, 'end')
1062
        ];
1063
    }
1064

1065
    walk(visitor: WalkVisitor, options: WalkOptions) {
1066
        //nothing to walk
1067
    }
1068

1069
    get leadingTrivia(): Token[] {
1070
        return this.tokens.end?.leadingTrivia ?? [];
19!
1071
    }
1072
}
1073

1074
export class StopStatement extends Statement {
1✔
1075
    constructor(options?: {
1076
        stop?: Token;
1077
    }) {
1078
        super();
17✔
1079
        this.tokens = { stop: options?.stop };
17!
1080
        this.location = this.tokens?.stop?.location;
17!
1081
    }
1082
    public readonly tokens: {
1083
        readonly stop?: Token;
1084
    };
1085

1086
    public readonly kind = AstNodeKind.StopStatement;
17✔
1087

1088
    public readonly location: Location;
1089

1090
    transpile(state: BrsTranspileState) {
1091
        return [
2✔
1092
            state.transpileToken(this.tokens.stop, 'stop')
1093
        ];
1094
    }
1095

1096
    walk(visitor: WalkVisitor, options: WalkOptions) {
1097
        //nothing to walk
1098
    }
1099

1100
    get leadingTrivia(): Token[] {
1101
        return this.tokens.stop?.leadingTrivia ?? [];
29!
1102
    }
1103
}
1104

1105
export class ForStatement extends Statement {
1✔
1106
    constructor(options: {
1107
        for?: Token;
1108
        counterDeclaration: AssignmentStatement;
1109
        to?: Token;
1110
        finalValue: Expression;
1111
        body: Block;
1112
        endFor?: Token;
1113
        step?: Token;
1114
        increment?: Expression;
1115
    }) {
1116
        super();
35✔
1117
        this.tokens = {
35✔
1118
            for: options.for,
1119
            to: options.to,
1120
            endFor: options.endFor,
1121
            step: options.step
1122
        };
1123
        this.counterDeclaration = options.counterDeclaration;
35✔
1124
        this.finalValue = options.finalValue;
35✔
1125
        this.body = options.body;
35✔
1126
        this.increment = options.increment;
35✔
1127

1128
        this.location = util.createBoundingLocation(
35✔
1129
            this.tokens.for,
1130
            this.counterDeclaration,
1131
            this.tokens.to,
1132
            this.finalValue,
1133
            this.tokens.step,
1134
            this.increment,
1135
            this.body,
1136
            this.tokens.endFor
1137
        );
1138
    }
1139

1140
    public readonly tokens: {
1141
        readonly for?: Token;
1142
        readonly to?: Token;
1143
        readonly endFor?: Token;
1144
        readonly step?: Token;
1145
    };
1146

1147
    public readonly counterDeclaration: AssignmentStatement;
1148
    public readonly finalValue: Expression;
1149
    public readonly body: Block;
1150
    public readonly increment?: Expression;
1151

1152
    public readonly kind = AstNodeKind.ForStatement;
35✔
1153

1154
    public readonly location: Location | undefined;
1155

1156
    transpile(state: BrsTranspileState) {
1157
        let result = [] as TranspileResult;
11✔
1158
        //for
1159
        result.push(
11✔
1160
            state.transpileToken(this.tokens.for, 'for'),
1161
            ' '
1162
        );
1163
        //i=1
1164
        result.push(
11✔
1165
            ...this.counterDeclaration.transpile(state),
1166
            ' '
1167
        );
1168
        //to
1169
        result.push(
11✔
1170
            state.transpileToken(this.tokens.to, 'to'),
1171
            ' '
1172
        );
1173
        //final value
1174
        result.push(this.finalValue.transpile(state));
11✔
1175
        //step
1176
        if (this.increment) {
11✔
1177
            result.push(
4✔
1178
                ' ',
1179
                state.transpileToken(this.tokens.step, 'step'),
1180
                ' ',
1181
                this.increment!.transpile(state)
1182
            );
1183
        }
1184
        //loop body
1185
        state.lineage.unshift(this);
11✔
1186
        result.push(...this.body.transpile(state));
11✔
1187
        state.lineage.shift();
11✔
1188

1189
        //end for
1190
        result.push(...state.transpileEndBlockToken(this.body, this.tokens.endFor, 'end for'));
11✔
1191

1192
        return result;
11✔
1193
    }
1194

1195
    walk(visitor: WalkVisitor, options: WalkOptions) {
1196
        if (options.walkMode & InternalWalkMode.walkStatements) {
136✔
1197
            walk(this, 'counterDeclaration', visitor, options);
135✔
1198
        }
1199
        if (options.walkMode & InternalWalkMode.walkExpressions) {
136✔
1200
            walk(this, 'finalValue', visitor, options);
132✔
1201
            walk(this, 'increment', visitor, options);
132✔
1202
        }
1203
        if (options.walkMode & InternalWalkMode.walkStatements) {
136✔
1204
            walk(this, 'body', visitor, options);
135✔
1205
        }
1206
    }
1207

1208
    get leadingTrivia(): Token[] {
1209
        return this.tokens.for?.leadingTrivia ?? [];
152!
1210
    }
1211

1212
    public get endTrivia(): Token[] {
NEW
1213
        return this.tokens.endFor?.leadingTrivia ?? [];
×
1214
    }
1215
}
1216

1217
export class ForEachStatement extends Statement {
1✔
1218
    constructor(options: {
1219
        forEach?: Token;
1220
        item: Token;
1221
        in?: Token;
1222
        target: Expression;
1223
        body: Block;
1224
        endFor?: Token;
1225
    }) {
1226
        super();
36✔
1227
        this.tokens = {
36✔
1228
            forEach: options.forEach,
1229
            item: options.item,
1230
            in: options.in,
1231
            endFor: options.endFor
1232
        };
1233
        this.body = options.body;
36✔
1234
        this.target = options.target;
36✔
1235

1236
        this.location = util.createBoundingLocation(
36✔
1237
            this.tokens.forEach,
1238
            this.tokens.item,
1239
            this.tokens.in,
1240
            this.target,
1241
            this.body,
1242
            this.tokens.endFor
1243
        );
1244
    }
1245

1246
    public readonly tokens: {
1247
        readonly forEach?: Token;
1248
        readonly item: Token;
1249
        readonly in?: Token;
1250
        readonly endFor?: Token;
1251
    };
1252
    public readonly body: Block;
1253
    public readonly target: Expression;
1254

1255
    public readonly kind = AstNodeKind.ForEachStatement;
36✔
1256

1257
    public readonly location: Location | undefined;
1258

1259
    transpile(state: BrsTranspileState) {
1260
        let result = [] as TranspileResult;
5✔
1261
        //for each
1262
        result.push(
5✔
1263
            state.transpileToken(this.tokens.forEach, 'for each'),
1264
            ' '
1265
        );
1266
        //item
1267
        result.push(
5✔
1268
            state.transpileToken(this.tokens.item),
1269
            ' '
1270
        );
1271
        //in
1272
        result.push(
5✔
1273
            state.transpileToken(this.tokens.in, 'in'),
1274
            ' '
1275
        );
1276
        //target
1277
        result.push(...this.target.transpile(state));
5✔
1278
        //body
1279
        state.lineage.unshift(this);
5✔
1280
        result.push(...this.body.transpile(state));
5✔
1281
        state.lineage.shift();
5✔
1282

1283
        //end for
1284
        result.push(...state.transpileEndBlockToken(this.body, this.tokens.endFor, 'end for'));
5✔
1285

1286
        return result;
5✔
1287
    }
1288

1289
    walk(visitor: WalkVisitor, options: WalkOptions) {
1290
        if (options.walkMode & InternalWalkMode.walkExpressions) {
173✔
1291
            walk(this, 'target', visitor, options);
166✔
1292
        }
1293
        if (options.walkMode & InternalWalkMode.walkStatements) {
173✔
1294
            walk(this, 'body', visitor, options);
172✔
1295
        }
1296
    }
1297

1298
    getType(options: GetTypeOptions): BscType {
1299
        return this.getSymbolTable().getSymbolType(this.tokens.item.text, options);
29✔
1300
    }
1301

1302
    get leadingTrivia(): Token[] {
1303
        return this.tokens.forEach?.leadingTrivia ?? [];
187!
1304
    }
1305

1306
    public get endTrivia(): Token[] {
1307
        return this.tokens.endFor?.leadingTrivia ?? [];
1!
1308
    }
1309
}
1310

1311
export class WhileStatement extends Statement {
1✔
1312
    constructor(options: {
1313
        while?: Token;
1314
        endWhile?: Token;
1315
        condition: Expression;
1316
        body: Block;
1317
    }) {
1318
        super();
29✔
1319
        this.tokens = {
29✔
1320
            while: options.while,
1321
            endWhile: options.endWhile
1322
        };
1323
        this.body = options.body;
29✔
1324
        this.condition = options.condition;
29✔
1325
        this.location = util.createBoundingLocation(
29✔
1326
            this.tokens.while,
1327
            this.condition,
1328
            this.body,
1329
            this.tokens.endWhile
1330
        );
1331
    }
1332

1333
    public readonly tokens: {
1334
        readonly while?: Token;
1335
        readonly endWhile?: Token;
1336
    };
1337
    public readonly condition: Expression;
1338
    public readonly body: Block;
1339

1340
    public readonly kind = AstNodeKind.WhileStatement;
29✔
1341

1342
    public readonly location: Location | undefined;
1343

1344
    transpile(state: BrsTranspileState) {
1345
        let result = [] as TranspileResult;
8✔
1346
        //while
1347
        result.push(
8✔
1348
            state.transpileToken(this.tokens.while, 'while'),
1349
            ' '
1350
        );
1351
        //condition
1352
        result.push(
8✔
1353
            ...this.condition.transpile(state)
1354
        );
1355
        state.lineage.unshift(this);
8✔
1356
        //body
1357
        result.push(...this.body.transpile(state));
8✔
1358
        state.lineage.shift();
8✔
1359

1360
        //end while
1361
        result.push(...state.transpileEndBlockToken(this.body, this.tokens.endWhile, 'end while'));
8✔
1362

1363
        return result;
8✔
1364
    }
1365

1366
    walk(visitor: WalkVisitor, options: WalkOptions) {
1367
        if (options.walkMode & InternalWalkMode.walkExpressions) {
110✔
1368
            walk(this, 'condition', visitor, options);
107✔
1369
        }
1370
        if (options.walkMode & InternalWalkMode.walkStatements) {
110✔
1371
            walk(this, 'body', visitor, options);
109✔
1372
        }
1373
    }
1374

1375
    get leadingTrivia(): Token[] {
1376
        return this.tokens.while?.leadingTrivia ?? [];
107!
1377
    }
1378

1379
    public get endTrivia(): Token[] {
1380
        return this.tokens.endWhile?.leadingTrivia ?? [];
1!
1381
    }
1382
}
1383

1384
export class DottedSetStatement extends Statement {
1✔
1385
    constructor(options: {
1386
        obj: Expression;
1387
        name: Identifier;
1388
        value: Expression;
1389
        dot?: Token;
1390
        equals?: Token;
1391
    }) {
1392
        super();
293✔
1393
        this.tokens = {
293✔
1394
            name: options.name,
1395
            dot: options.dot,
1396
            equals: options.equals
1397
        };
1398
        this.obj = options.obj;
293✔
1399
        this.value = options.value;
293✔
1400
        this.location = util.createBoundingLocation(
293✔
1401
            this.obj,
1402
            this.tokens.dot,
1403
            this.tokens.name,
1404
            this.value
1405
        );
1406
    }
1407
    public readonly tokens: {
1408
        readonly name: Identifier;
1409
        readonly equals?: Token;
1410
        readonly dot?: Token;
1411
    };
1412

1413
    public readonly obj: Expression;
1414
    public readonly value: Expression;
1415

1416
    public readonly kind = AstNodeKind.DottedSetStatement;
293✔
1417

1418
    public readonly location: Location | undefined;
1419

1420
    transpile(state: BrsTranspileState) {
1421
        //if the value is a compound assignment, don't add the obj, dot, name, or operator...the expression will handle that
1422
        return [
9✔
1423
            //object
1424
            ...this.obj.transpile(state),
1425
            this.tokens.dot ? state.tokenToSourceNode(this.tokens.dot) : '.',
9!
1426
            //name
1427
            state.transpileToken(this.tokens.name),
1428
            ' ',
1429
            state.transpileToken(this.tokens.equals, '='),
1430
            ' ',
1431
            //right-hand-side of assignment
1432
            ...this.value.transpile(state)
1433
        ];
1434

1435
    }
1436

1437
    walk(visitor: WalkVisitor, options: WalkOptions) {
1438
        if (options.walkMode & InternalWalkMode.walkExpressions) {
737✔
1439
            walk(this, 'obj', visitor, options);
735✔
1440
            walk(this, 'value', visitor, options);
735✔
1441
        }
1442
    }
1443

1444
    getType(options: GetTypeOptions) {
1445
        const objType = this.obj?.getType(options);
84!
1446
        const result = objType?.getMemberType(this.tokens.name?.text, options);
84!
1447
        options.typeChain?.push(new TypeChainEntry({
84✔
1448
            name: this.tokens.name?.text,
249!
1449
            type: result, data: options.data,
1450
            location: this.tokens.name?.location,
249!
1451
            astNode: this
1452
        }));
1453
        return result;
84✔
1454
    }
1455

1456
    get leadingTrivia(): Token[] {
1457
        return this.obj.leadingTrivia;
744✔
1458
    }
1459
}
1460

1461
export class IndexedSetStatement extends Statement {
1✔
1462
    constructor(options: {
1463
        obj: Expression;
1464
        indexes: Expression[];
1465
        value: Expression;
1466
        openingSquare?: Token;
1467
        closingSquare?: Token;
1468
        equals?: Token;
1469
    }) {
1470
        super();
25✔
1471
        this.tokens = {
25✔
1472
            openingSquare: options.openingSquare,
1473
            closingSquare: options.closingSquare,
1474
            equals: options.equals
1475
        };
1476
        this.obj = options.obj;
25✔
1477
        this.indexes = options.indexes;
25✔
1478
        this.value = options.value;
25✔
1479
        this.location = util.createBoundingLocation(
25✔
1480
            this.obj,
1481
            this.tokens.openingSquare,
1482
            ...this.indexes,
1483
            this.tokens.closingSquare,
1484
            this.value
1485
        );
1486
    }
1487

1488
    public readonly tokens: {
1489
        readonly openingSquare?: Token;
1490
        readonly closingSquare?: Token;
1491
        readonly equals?: Token;
1492
    };
1493
    public readonly obj: Expression;
1494
    public readonly indexes: Expression[];
1495
    public readonly value: Expression;
1496

1497
    public readonly kind = AstNodeKind.IndexedSetStatement;
25✔
1498

1499
    public readonly location: Location | undefined;
1500

1501
    transpile(state: BrsTranspileState) {
1502
        const result = [];
10✔
1503
        result.push(
10✔
1504
            //obj
1505
            ...this.obj.transpile(state),
1506
            //   [
1507
            state.transpileToken(this.tokens.openingSquare, '[')
1508
        );
1509
        for (let i = 0; i < this.indexes.length; i++) {
10✔
1510
            //add comma between indexes
1511
            if (i > 0) {
11✔
1512
                result.push(', ');
1✔
1513
            }
1514
            let index = this.indexes[i];
11✔
1515
            result.push(
11✔
1516
                ...(index?.transpile(state) ?? [])
66!
1517
            );
1518
        }
1519
        result.push(
10✔
1520
            state.transpileToken(this.tokens.closingSquare, ']'),
1521
            ' ',
1522
            state.transpileToken(this.tokens.equals, '='),
1523
            ' ',
1524
            ...this.value.transpile(state)
1525
        );
1526
        return result;
10✔
1527

1528
    }
1529

1530
    walk(visitor: WalkVisitor, options: WalkOptions) {
1531
        if (options.walkMode & InternalWalkMode.walkExpressions) {
111✔
1532
            walk(this, 'obj', visitor, options);
110✔
1533
            walkArray(this.indexes, visitor, options, this);
110✔
1534
            walk(this, 'value', visitor, options);
110✔
1535
        }
1536
    }
1537

1538
    get leadingTrivia(): Token[] {
1539
        return this.obj.leadingTrivia;
126✔
1540
    }
1541
}
1542

1543
export class LibraryStatement extends Statement implements TypedefProvider {
1✔
1544
    constructor(options: {
1545
        library: Token;
1546
        filePath?: Token;
1547
    }) {
1548
        super();
14✔
1549
        this.tokens = {
14✔
1550
            library: options.library,
1551
            filePath: options.filePath
1552
        };
1553
        this.location = util.createBoundingLocation(
14✔
1554
            this.tokens.library,
1555
            this.tokens.filePath
1556
        );
1557
    }
1558
    public readonly tokens: {
1559
        readonly library: Token;
1560
        readonly filePath?: Token;
1561
    };
1562

1563
    public readonly kind = AstNodeKind.LibraryStatement;
14✔
1564

1565
    public readonly location: Location | undefined;
1566

1567
    transpile(state: BrsTranspileState) {
1568
        let result = [] as TranspileResult;
2✔
1569
        result.push(
2✔
1570
            state.transpileToken(this.tokens.library)
1571
        );
1572
        //there will be a parse error if file path is missing, but let's prevent a runtime error just in case
1573
        if (this.tokens.filePath) {
2!
1574
            result.push(
2✔
1575
                ' ',
1576
                state.transpileToken(this.tokens.filePath)
1577
            );
1578
        }
1579
        return result;
2✔
1580
    }
1581

1582
    getTypedef(state: BrsTranspileState) {
1583
        return this.transpile(state);
×
1584
    }
1585

1586
    walk(visitor: WalkVisitor, options: WalkOptions) {
1587
        //nothing to walk
1588
    }
1589

1590
    get leadingTrivia(): Token[] {
1591
        return this.tokens.library?.leadingTrivia ?? [];
26!
1592
    }
1593
}
1594

1595
export class NamespaceStatement extends Statement implements TypedefProvider {
1✔
1596
    constructor(options: {
1597
        namespace?: Token;
1598
        nameExpression: VariableExpression | DottedGetExpression;
1599
        body: Body;
1600
        endNamespace?: Token;
1601
    }) {
1602
        super();
607✔
1603
        this.tokens = {
607✔
1604
            namespace: options.namespace,
1605
            endNamespace: options.endNamespace
1606
        };
1607
        this.nameExpression = options.nameExpression;
607✔
1608
        this.body = options.body;
607✔
1609
        this.name = this.getName(ParseMode.BrighterScript);
607✔
1610
        this.symbolTable = new SymbolTable(`NamespaceStatement: '${this.name}'`, () => this.parent?.getSymbolTable());
4,933!
1611
    }
1612

1613
    public readonly tokens: {
1614
        readonly namespace?: Token;
1615
        readonly endNamespace?: Token;
1616
    };
1617

1618
    public readonly nameExpression: VariableExpression | DottedGetExpression;
1619
    public readonly body: Body;
1620

1621
    public readonly kind = AstNodeKind.NamespaceStatement;
607✔
1622

1623
    /**
1624
     * The string name for this namespace
1625
     */
1626
    public name: string;
1627

1628
    public get location() {
1629
        return this.cacheLocation();
349✔
1630
    }
1631
    private _location: Location | undefined;
1632

1633
    public cacheLocation() {
1634
        if (!this._location) {
954✔
1635
            this._location = util.createBoundingLocation(
607✔
1636
                this.tokens.namespace,
1637
                this.nameExpression,
1638
                this.body,
1639
                this.tokens.endNamespace
1640
            );
1641
        }
1642
        return this._location;
954✔
1643
    }
1644

1645
    public getName(parseMode: ParseMode) {
1646
        const sep = parseMode === ParseMode.BrighterScript ? '.' : '_';
6,515✔
1647
        let name = util.getAllDottedGetPartsAsString(this.nameExpression, parseMode);
6,515✔
1648
        if ((this.parent as Body)?.parent?.kind === AstNodeKind.NamespaceStatement) {
6,515✔
1649
            name = (this.parent.parent as NamespaceStatement).getName(parseMode) + sep + name;
243✔
1650
        }
1651
        return name;
6,515✔
1652
    }
1653

1654
    public get leadingTrivia(): Token[] {
1655
        return this.tokens.namespace?.leadingTrivia;
1,781!
1656
    }
1657

1658
    public get endTrivia(): Token[] {
NEW
1659
        return this.tokens.endNamespace?.leadingTrivia;
×
1660
    }
1661

1662
    public getNameParts() {
1663
        let parts = util.getAllDottedGetParts(this.nameExpression);
1,012✔
1664

1665
        if ((this.parent as Body)?.parent?.kind === AstNodeKind.NamespaceStatement) {
1,012!
1666
            parts = (this.parent.parent as NamespaceStatement).getNameParts().concat(parts);
56✔
1667
        }
1668
        return parts;
1,012✔
1669
    }
1670

1671
    transpile(state: BrsTranspileState) {
1672
        //namespaces don't actually have any real content, so just transpile their bodies
1673
        return [
55✔
1674
            state.transpileAnnotations(this),
1675
            state.transpileLeadingComments(this.tokens.namespace),
1676
            this.body.transpile(state),
1677
            state.transpileLeadingComments(this.tokens.endNamespace)
1678
        ];
1679
    }
1680

1681
    getTypedef(state: BrsTranspileState) {
1682
        let result: TranspileResult = [];
7✔
1683
        for (let comment of util.getLeadingComments(this) ?? []) {
7!
NEW
1684
            result.push(
×
1685
                comment.text,
1686
                state.newline,
1687
                state.indent()
1688
            );
1689
        }
1690

1691
        result.push('namespace ',
7✔
1692
            ...this.getName(ParseMode.BrighterScript),
1693
            state.newline
1694
        );
1695
        state.blockDepth++;
7✔
1696
        result.push(
7✔
1697
            ...this.body.getTypedef(state)
1698
        );
1699
        state.blockDepth--;
7✔
1700

1701
        result.push(
7✔
1702
            state.indent(),
1703
            'end namespace'
1704
        );
1705
        return result;
7✔
1706
    }
1707

1708
    walk(visitor: WalkVisitor, options: WalkOptions) {
1709
        if (options.walkMode & InternalWalkMode.walkExpressions) {
3,051✔
1710
            walk(this, 'nameExpression', visitor, options);
2,489✔
1711
        }
1712

1713
        if (this.body.statements.length > 0 && options.walkMode & InternalWalkMode.walkStatements) {
3,051✔
1714
            walk(this, 'body', visitor, options);
2,854✔
1715
        }
1716
    }
1717

1718
    getType(options: GetTypeOptions) {
1719
        const resultType = new NamespaceType(this.name);
1,067✔
1720
        return resultType;
1,067✔
1721
    }
1722

1723
}
1724

1725
export class ImportStatement extends Statement implements TypedefProvider {
1✔
1726
    constructor(options: {
1727
        import?: Token;
1728
        path?: Token;
1729
    }) {
1730
        super();
205✔
1731
        this.tokens = {
205✔
1732
            import: options.import,
1733
            path: options.path
1734
        };
1735
        this.location = util.createBoundingLocation(
205✔
1736
            this.tokens.import,
1737
            this.tokens.path
1738
        );
1739
        if (this.tokens.path) {
205✔
1740
            //remove quotes
1741
            this.filePath = this.tokens.path.text.replace(/"/g, '');
203✔
1742
            if (this.tokens.path?.location?.range) {
203!
1743
                //adjust the range to exclude the quotes
1744
                this.tokens.path.location = util.createLocation(
199✔
1745
                    this.tokens.path.location.range.start.line,
1746
                    this.tokens.path.location.range.start.character + 1,
1747
                    this.tokens.path.location.range.end.line,
1748
                    this.tokens.path.location.range.end.character - 1,
1749
                    this.tokens.path.location.uri
1750
                );
1751
            }
1752
        }
1753
    }
1754

1755
    public readonly tokens: {
1756
        readonly import?: Token;
1757
        readonly path: Token;
1758
    };
1759

1760
    public readonly kind = AstNodeKind.ImportStatement;
205✔
1761

1762
    public readonly location: Location;
1763

1764
    public readonly filePath: string;
1765

1766
    transpile(state: BrsTranspileState) {
1767
        //The xml files are responsible for adding the additional script imports, but
1768
        //add the import statement as a comment just for debugging purposes
1769
        return [
13✔
1770
            state.transpileToken(this.tokens.import, 'import', true),
1771
            ' ',
1772
            state.transpileToken(this.tokens.path)
1773
        ];
1774
    }
1775

1776
    /**
1777
     * Get the typedef for this statement
1778
     */
1779
    public getTypedef(state: BrsTranspileState) {
1780
        return [
3✔
1781
            this.tokens.import?.text ?? 'import',
18!
1782
            ' ',
1783
            //replace any `.bs` extension with `.brs`
1784
            this.tokens.path.text.replace(/\.bs"?$/i, '.brs"')
1785
        ];
1786
    }
1787

1788
    walk(visitor: WalkVisitor, options: WalkOptions) {
1789
        //nothing to walk
1790
    }
1791

1792
    get leadingTrivia(): Token[] {
1793
        return this.tokens.import?.leadingTrivia ?? [];
576!
1794
    }
1795
}
1796

1797
export class InterfaceStatement extends Statement implements TypedefProvider {
1✔
1798
    constructor(options: {
1799
        interface: Token;
1800
        name: Identifier;
1801
        extends?: Token;
1802
        parentInterfaceName?: TypeExpression;
1803
        body: Statement[];
1804
        endInterface?: Token;
1805
    }) {
1806
        super();
146✔
1807
        this.tokens = {
146✔
1808
            interface: options.interface,
1809
            name: options.name,
1810
            extends: options.extends,
1811
            endInterface: options.endInterface
1812
        };
1813
        this.parentInterfaceName = options.parentInterfaceName;
146✔
1814
        this.body = options.body;
146✔
1815
        this.location = util.createBoundingLocation(
146✔
1816
            this.tokens.interface,
1817
            this.tokens.name,
1818
            this.tokens.extends,
1819
            this.parentInterfaceName,
1820
            ...this.body,
1821
            this.tokens.endInterface
1822
        );
1823
    }
1824
    public readonly parentInterfaceName?: TypeExpression;
1825
    public readonly body: Statement[];
1826

1827
    public readonly kind = AstNodeKind.InterfaceStatement;
146✔
1828

1829
    public readonly tokens = {} as {
146✔
1830
        readonly interface?: Token;
1831
        readonly name: Identifier;
1832
        readonly extends?: Token;
1833
        readonly endInterface?: Token;
1834
    };
1835

1836
    public readonly location: Location | undefined;
1837

1838
    public get fields(): InterfaceFieldStatement[] {
1839
        return this.body.filter(x => isInterfaceFieldStatement(x)) as InterfaceFieldStatement[];
206✔
1840
    }
1841

1842
    public get methods(): InterfaceMethodStatement[] {
1843
        return this.body.filter(x => isInterfaceMethodStatement(x)) as InterfaceMethodStatement[];
201✔
1844
    }
1845

1846

1847
    public hasParentInterface() {
NEW
1848
        return !!this.parentInterfaceName;
×
1849
    }
1850

1851
    public get leadingTrivia(): Token[] {
1852
        return this.tokens.interface?.leadingTrivia;
404!
1853
    }
1854

1855
    public get endTrivia(): Token[] {
NEW
1856
        return this.tokens.endInterface?.leadingTrivia;
×
1857
    }
1858

1859

1860
    /**
1861
     * The name of the interface WITH its leading namespace (if applicable)
1862
     */
1863
    public get fullName() {
1864
        const name = this.tokens.name?.text;
2!
1865
        if (name) {
2!
1866
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
2✔
1867
            if (namespace) {
2✔
1868
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
1✔
1869
                return `${namespaceName}.${name}`;
1✔
1870
            } else {
1871
                return name;
1✔
1872
            }
1873
        } else {
1874
            //return undefined which will allow outside callers to know that this interface doesn't have a name
1875
            return undefined;
×
1876
        }
1877
    }
1878

1879
    /**
1880
     * The name of the interface (without the namespace prefix)
1881
     */
1882
    public get name() {
1883
        return this.tokens.name?.text;
137!
1884
    }
1885

1886
    /**
1887
     * Get the name of this expression based on the parse mode
1888
     */
1889
    public getName(parseMode: ParseMode) {
1890
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
137✔
1891
        if (namespace) {
137✔
1892
            let delimiter = parseMode === ParseMode.BrighterScript ? '.' : '_';
22!
1893
            let namespaceName = namespace.getName(parseMode);
22✔
1894
            return namespaceName + delimiter + this.name;
22✔
1895
        } else {
1896
            return this.name;
115✔
1897
        }
1898
    }
1899

1900
    public transpile(state: BrsTranspileState): TranspileResult {
1901
        //interfaces should completely disappear at runtime
1902
        return [
13✔
1903
            state.transpileLeadingComments(this.tokens.interface)
1904
        ];
1905
    }
1906

1907
    getTypedef(state: BrsTranspileState) {
1908
        const result = [] as TranspileResult;
7✔
1909
        for (let comment of util.getLeadingComments(this) ?? []) {
7!
NEW
1910
            result.push(
×
1911
                comment.text,
1912
                state.newline,
1913
                state.indent()
1914
            );
1915
        }
1916
        for (let annotation of this.annotations ?? []) {
7✔
1917
            result.push(
1✔
1918
                ...annotation.getTypedef(state),
1919
                state.newline,
1920
                state.indent()
1921
            );
1922
        }
1923
        result.push(
7✔
1924
            this.tokens.interface.text,
1925
            ' ',
1926
            this.tokens.name.text
1927
        );
1928
        const parentInterfaceName = this.parentInterfaceName?.getName();
7!
1929
        if (parentInterfaceName) {
7!
1930
            result.push(
×
1931
                ' extends ',
1932
                parentInterfaceName
1933
            );
1934
        }
1935
        const body = this.body ?? [];
7!
1936
        if (body.length > 0) {
7!
1937
            state.blockDepth++;
7✔
1938
        }
1939
        for (const statement of body) {
7✔
1940
            if (isInterfaceMethodStatement(statement) || isInterfaceFieldStatement(statement)) {
22!
1941
                result.push(
22✔
1942
                    state.newline,
1943
                    state.indent(),
1944
                    ...statement.getTypedef(state)
1945
                );
1946
            } else {
UNCOV
1947
                result.push(
×
1948
                    state.newline,
1949
                    state.indent(),
1950
                    ...statement.transpile(state)
1951
                );
1952
            }
1953
        }
1954
        if (body.length > 0) {
7!
1955
            state.blockDepth--;
7✔
1956
        }
1957
        result.push(
7✔
1958
            state.newline,
1959
            state.indent(),
1960
            'end interface',
1961
            state.newline
1962
        );
1963
        return result;
7✔
1964
    }
1965

1966
    walk(visitor: WalkVisitor, options: WalkOptions) {
1967
        //visitor-less walk function to do parent linking
1968
        walk(this, 'parentInterfaceName', null, options);
675✔
1969

1970
        if (options.walkMode & InternalWalkMode.walkStatements) {
675!
1971
            walkArray(this.body, visitor, options, this);
675✔
1972
        }
1973
    }
1974

1975
    getType(options: GetTypeOptions) {
1976
        const superIface = this.parentInterfaceName?.getType(options) as InterfaceType;
131✔
1977

1978
        const resultType = new InterfaceType(this.getName(ParseMode.BrighterScript), superIface);
131✔
1979
        for (const statement of this.methods) {
131✔
1980
            const memberType = statement?.getType({ ...options, typeChain: undefined }); // no typechain info needed
28!
1981
            const flag = statement.isOptional ? SymbolTypeFlag.runtime | SymbolTypeFlag.optional : SymbolTypeFlag.runtime;
28✔
1982
            resultType.addMember(statement?.tokens.name?.text, { definingNode: statement }, memberType, flag);
28!
1983
        }
1984
        for (const statement of this.fields) {
131✔
1985
            const memberType = statement?.getType({ ...options, typeChain: undefined }); // no typechain info needed
168!
1986
            const flag = statement.isOptional ? SymbolTypeFlag.runtime | SymbolTypeFlag.optional : SymbolTypeFlag.runtime;
168✔
1987
            resultType.addMember(statement?.tokens.name?.text, { definingNode: statement, isInstance: true }, memberType, flag);
168!
1988
        }
1989
        options.typeChain?.push(new TypeChainEntry({
131✔
1990
            name: this.getName(ParseMode.BrighterScript),
1991
            type: resultType,
1992
            data: options.data,
1993
            astNode: this
1994
        }));
1995
        return resultType;
131✔
1996
    }
1997
}
1998

1999
export class InterfaceFieldStatement extends Statement implements TypedefProvider {
1✔
2000
    public transpile(state: BrsTranspileState): TranspileResult {
2001
        throw new Error('Method not implemented.');
×
2002
    }
2003
    constructor(options: {
2004
        name: Identifier;
2005
        as?: Token;
2006
        typeExpression?: TypeExpression;
2007
        optional?: Token;
2008
    }) {
2009
        super();
180✔
2010
        this.tokens = {
180✔
2011
            optional: options.optional,
2012
            name: options.name,
2013
            as: options.as
2014
        };
2015
        this.typeExpression = options.typeExpression;
180✔
2016
        this.location = util.createBoundingLocation(
180✔
2017
            this.tokens.optional,
2018
            this.tokens.name,
2019
            this.tokens.as,
2020
            this.typeExpression
2021
        );
2022
    }
2023

2024
    public readonly kind = AstNodeKind.InterfaceFieldStatement;
180✔
2025

2026
    public readonly typeExpression?: TypeExpression;
2027

2028
    public readonly location: Location | undefined;
2029

2030
    public readonly tokens: {
2031
        readonly name: Identifier;
2032
        readonly as: Token;
2033
        readonly optional?: Token;
2034
    };
2035

2036
    public get leadingTrivia(): Token[] {
2037
        return this.tokens.optional?.leadingTrivia ?? this.tokens.name.leadingTrivia;
499✔
2038
    }
2039

2040
    public get name() {
2041
        return this.tokens.name.text;
×
2042
    }
2043

2044
    public get isOptional() {
2045
        return !!this.tokens.optional;
188✔
2046
    }
2047

2048
    walk(visitor: WalkVisitor, options: WalkOptions) {
2049
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,163✔
2050
            walk(this, 'typeExpression', visitor, options);
1,007✔
2051
        }
2052
    }
2053

2054
    getTypedef(state: BrsTranspileState): TranspileResult {
2055
        const result = [] as TranspileResult;
12✔
2056
        for (let comment of util.getLeadingComments(this) ?? []) {
12!
NEW
2057
            result.push(
×
2058
                comment.text,
2059
                state.newline,
2060
                state.indent()
2061
            );
2062
        }
2063
        for (let annotation of this.annotations ?? []) {
12✔
2064
            result.push(
1✔
2065
                ...annotation.getTypedef(state),
2066
                state.newline,
2067
                state.indent()
2068
            );
2069
        }
2070
        if (this.isOptional) {
12✔
2071
            result.push(
1✔
2072
                this.tokens.optional!.text,
2073
                ' '
2074
            );
2075
        }
2076
        result.push(
12✔
2077
            this.tokens.name.text
2078
        );
2079

2080
        if (this.typeExpression) {
12!
2081
            result.push(
12✔
2082
                ' as ',
2083
                ...this.typeExpression.getTypedef(state)
2084
            );
2085
        }
2086
        return result;
12✔
2087
    }
2088

2089
    public getType(options: GetTypeOptions): BscType {
2090
        return this.typeExpression?.getType(options) ?? DynamicType.instance;
172✔
2091
    }
2092

2093
}
2094

2095
//TODO: there is much that is similar with this and FunctionExpression.
2096
//It would be nice to refactor this so there is less duplicated code
2097
export class InterfaceMethodStatement extends Statement implements TypedefProvider {
1✔
2098
    public transpile(state: BrsTranspileState): TranspileResult {
2099
        throw new Error('Method not implemented.');
×
2100
    }
2101
    constructor(options: {
2102
        functionType?: Token;
2103
        name: Identifier;
2104
        leftParen?: Token;
2105
        params?: FunctionParameterExpression[];
2106
        rightParen?: Token;
2107
        as?: Token;
2108
        returnTypeExpression?: TypeExpression;
2109
        optional?: Token;
2110
    }) {
2111
        super();
38✔
2112
        this.tokens = {
38✔
2113
            optional: options.optional,
2114
            functionType: options.functionType,
2115
            name: options.name,
2116
            leftParen: options.leftParen,
2117
            rightParen: options.rightParen,
2118
            as: options.as
2119
        };
2120
        this.params = options.params ?? [];
38!
2121
        this.returnTypeExpression = options.returnTypeExpression;
38✔
2122
    }
2123

2124
    public readonly kind = AstNodeKind.InterfaceMethodStatement;
38✔
2125

2126
    public get location() {
2127
        return util.createBoundingLocation(
52✔
2128
            this.tokens.optional,
2129
            this.tokens.functionType,
2130
            this.tokens.name,
2131
            this.tokens.leftParen,
2132
            ...(this.params ?? []),
156!
2133
            this.tokens.rightParen,
2134
            this.tokens.as,
2135
            this.returnTypeExpression
2136
        );
2137
    }
2138
    /**
2139
     * Get the name of this method.
2140
     */
2141
    public getName(parseMode: ParseMode) {
2142
        return this.tokens.name.text;
28✔
2143
    }
2144

2145
    public readonly tokens: {
2146
        readonly optional?: Token;
2147
        readonly functionType: Token;
2148
        readonly name: Identifier;
2149
        readonly leftParen?: Token;
2150
        readonly rightParen?: Token;
2151
        readonly as?: Token;
2152
    };
2153

2154
    public readonly params: FunctionParameterExpression[];
2155
    public readonly returnTypeExpression?: TypeExpression;
2156

2157
    public get isOptional() {
2158
        return !!this.tokens.optional;
41✔
2159
    }
2160

2161
    public get leadingTrivia(): Token[] {
2162
        return this.tokens.optional?.leadingTrivia ?? this.tokens.functionType.leadingTrivia;
88✔
2163
    }
2164

2165
    walk(visitor: WalkVisitor, options: WalkOptions) {
2166
        if (options.walkMode & InternalWalkMode.walkExpressions) {
200✔
2167
            walk(this, 'returnTypeExpression', visitor, options);
175✔
2168
        }
2169
    }
2170

2171
    getTypedef(state: BrsTranspileState) {
2172
        const result = [] as TranspileResult;
10✔
2173
        for (let comment of util.getLeadingComments(this) ?? []) {
10!
2174
            result.push(
1✔
2175
                comment.text,
2176
                state.newline,
2177
                state.indent()
2178
            );
2179
        }
2180
        for (let annotation of this.annotations ?? []) {
10✔
2181
            result.push(
1✔
2182
                ...annotation.getTypedef(state),
2183
                state.newline,
2184
                state.indent()
2185
            );
2186
        }
2187
        if (this.isOptional) {
10!
UNCOV
2188
            result.push(
×
2189
                this.tokens.optional!.text,
2190
                ' '
2191
            );
2192
        }
2193
        result.push(
10✔
2194
            this.tokens.functionType?.text ?? 'function',
60!
2195
            ' ',
2196
            this.tokens.name.text,
2197
            '('
2198
        );
2199
        const params = this.params ?? [];
10!
2200
        for (let i = 0; i < params.length; i++) {
10✔
2201
            if (i > 0) {
2✔
2202
                result.push(', ');
1✔
2203
            }
2204
            const param = params[i];
2✔
2205
            result.push(param.tokens.name.text);
2✔
2206
            if (param.typeExpression) {
2!
2207
                result.push(
2✔
2208
                    ' as ',
2209
                    ...param.typeExpression.getTypedef(state)
2210
                );
2211
            }
2212
        }
2213
        result.push(
10✔
2214
            ')'
2215
        );
2216
        if (this.returnTypeExpression) {
10!
2217
            result.push(
10✔
2218
                ' as ',
2219
                ...this.returnTypeExpression.getTypedef(state)
2220
            );
2221
        }
2222
        return result;
10✔
2223
    }
2224

2225
    public getType(options: GetTypeOptions): TypedFunctionType {
2226
        //if there's a defined return type, use that
2227
        let returnType = this.returnTypeExpression?.getType(options);
28✔
2228
        const isSub = this.tokens.functionType?.kind === TokenKind.Sub || !returnType;
28!
2229
        //if we don't have a return type and this is a sub, set the return type to `void`. else use `dynamic`
2230
        if (!returnType) {
28✔
2231
            returnType = isSub ? VoidType.instance : DynamicType.instance;
10!
2232
        }
2233

2234
        const resultType = new TypedFunctionType(returnType);
28✔
2235
        resultType.isSub = isSub;
28✔
2236
        for (let param of this.params) {
28✔
2237
            resultType.addParameter(param.tokens.name.text, param.getType(options), !!param.defaultValue);
7✔
2238
        }
2239
        if (options.typeChain) {
28!
2240
            // need Interface type for type chain
NEW
2241
            this.parent?.getType(options);
×
2242
        }
2243
        let funcName = this.getName(ParseMode.BrighterScript);
28✔
2244
        resultType.setName(funcName);
28✔
2245
        options.typeChain?.push(new TypeChainEntry({ name: resultType.name, type: resultType, data: options.data, astNode: this }));
28!
2246
        return resultType;
28✔
2247
    }
2248
}
2249

2250
export class ClassStatement extends Statement implements TypedefProvider {
1✔
2251
    constructor(options: {
2252
        class?: Token;
2253
        /**
2254
         * The name of the class (without namespace prefix)
2255
         */
2256
        name: Identifier;
2257
        body: Statement[];
2258
        endClass?: Token;
2259
        extends?: Token;
2260
        parentClassName?: TypeExpression;
2261
    }) {
2262
        super();
666✔
2263
        this.body = options.body ?? [];
666!
2264
        this.tokens = {
666✔
2265
            name: options.name,
2266
            class: options.class,
2267
            endClass: options.endClass,
2268
            extends: options.extends
2269
        };
2270
        this.parentClassName = options.parentClassName;
666✔
2271
        this.symbolTable = new SymbolTable(`ClassStatement: '${this.tokens.name?.text}'`, () => this.parent?.getSymbolTable());
1,293!
2272

2273
        for (let statement of this.body) {
666✔
2274
            if (isMethodStatement(statement)) {
670✔
2275
                this.methods.push(statement);
351✔
2276
                this.memberMap[statement?.tokens.name?.text.toLowerCase()] = statement;
351!
2277
            } else if (isFieldStatement(statement)) {
319!
2278
                this.fields.push(statement);
319✔
2279
                this.memberMap[statement?.tokens.name?.text.toLowerCase()] = statement;
319!
2280
            }
2281
        }
2282

2283
        this.location = util.createBoundingLocation(
666✔
2284
            this.parentClassName,
2285
            ...(this.body ?? []),
1,998!
2286
            util.createBoundingLocationFromTokens(this.tokens)
2287
        );
2288
    }
2289

2290
    public readonly kind = AstNodeKind.ClassStatement;
666✔
2291

2292

2293
    public readonly tokens: {
2294
        readonly class?: Token;
2295
        /**
2296
         * The name of the class (without namespace prefix)
2297
         */
2298
        readonly name: Identifier;
2299
        readonly endClass?: Token;
2300
        readonly extends?: Token;
2301
    };
2302
    public readonly body: Statement[];
2303
    public readonly parentClassName: TypeExpression;
2304

2305

2306
    public getName(parseMode: ParseMode) {
2307
        const name = this.tokens.name?.text;
2,300✔
2308
        if (name) {
2,300✔
2309
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,298✔
2310
            if (namespace) {
2,298✔
2311
                let namespaceName = namespace.getName(parseMode);
864✔
2312
                let separator = parseMode === ParseMode.BrighterScript ? '.' : '_';
864✔
2313
                return namespaceName + separator + name;
864✔
2314
            } else {
2315
                return name;
1,434✔
2316
            }
2317
        } else {
2318
            //return undefined which will allow outside callers to know that this class doesn't have a name
2319
            return undefined;
2✔
2320
        }
2321
    }
2322

2323
    public get leadingTrivia(): Token[] {
2324
        return this.tokens.class?.leadingTrivia;
1,301!
2325
    }
2326

2327
    public get endTrivia(): Token[] {
NEW
2328
        return this.tokens.endClass?.leadingTrivia ?? [];
×
2329
    }
2330

2331
    public readonly memberMap = {} as Record<string, MemberStatement>;
666✔
2332
    public readonly methods = [] as MethodStatement[];
666✔
2333
    public readonly fields = [] as FieldStatement[];
666✔
2334

2335
    public readonly location: Location | undefined;
2336

2337
    transpile(state: BrsTranspileState) {
2338
        let result = [] as TranspileResult;
50✔
2339
        //make the builder
2340
        result.push(...this.getTranspiledBuilder(state));
50✔
2341
        result.push(
50✔
2342
            '\n',
2343
            state.indent()
2344
        );
2345
        //make the class assembler (i.e. the public-facing class creator method)
2346
        result.push(...this.getTranspiledClassFunction(state));
50✔
2347
        return result;
50✔
2348
    }
2349

2350
    getTypedef(state: BrsTranspileState) {
2351
        const result = [] as TranspileResult;
15✔
2352
        for (let comment of util.getLeadingComments(this) ?? []) {
15!
NEW
2353
            result.push(
×
2354
                comment.text,
2355
                state.newline,
2356
                state.indent()
2357
            );
2358
        }
2359
        for (let annotation of this.annotations ?? []) {
15!
2360
            result.push(
×
2361
                ...annotation.getTypedef(state),
2362
                state.newline,
2363
                state.indent()
2364
            );
2365
        }
2366
        result.push(
15✔
2367
            'class ',
2368
            this.tokens.name.text
2369
        );
2370
        if (this.parentClassName) {
15✔
2371
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
4✔
2372
            const fqName = util.getFullyQualifiedClassName(
4✔
2373
                this.parentClassName.getName(),
2374
                namespace?.getName(ParseMode.BrighterScript)
12✔
2375
            );
2376
            result.push(
4✔
2377
                ` extends ${fqName}`
2378
            );
2379
        }
2380
        result.push(state.newline);
15✔
2381
        state.blockDepth++;
15✔
2382

2383
        let body = this.body;
15✔
2384
        //inject an empty "new" method if missing
2385
        if (!this.getConstructorFunction()) {
15✔
2386
            const constructor = createMethodStatement('new', TokenKind.Sub);
11✔
2387
            constructor.parent = this;
11✔
2388
            //walk the constructor to set up parent links
2389
            constructor.link();
11✔
2390
            body = [
11✔
2391
                constructor,
2392
                ...this.body
2393
            ];
2394
        }
2395

2396
        for (const member of body) {
15✔
2397
            if (isTypedefProvider(member)) {
33!
2398
                result.push(
33✔
2399
                    state.indent(),
2400
                    ...member.getTypedef(state),
2401
                    state.newline
2402
                );
2403
            }
2404
        }
2405
        state.blockDepth--;
15✔
2406
        result.push(
15✔
2407
            state.indent(),
2408
            'end class'
2409
        );
2410
        return result;
15✔
2411
    }
2412

2413
    /**
2414
     * Find the parent index for this class's parent.
2415
     * For class inheritance, every class is given an index.
2416
     * The base class is index 0, its child is index 1, and so on.
2417
     */
2418
    public getParentClassIndex(state: BrsTranspileState) {
2419
        let myIndex = 0;
116✔
2420
        let stmt = this as ClassStatement;
116✔
2421
        while (stmt) {
116✔
2422
            if (stmt.parentClassName) {
165✔
2423
                const namespace = stmt.findAncestor<NamespaceStatement>(isNamespaceStatement);
50✔
2424
                //find the parent class
2425
                stmt = state.file.getClassFileLink(
50✔
2426
                    stmt.parentClassName.getName(),
2427
                    namespace?.getName(ParseMode.BrighterScript)
150✔
2428
                )?.item;
50✔
2429
                myIndex++;
50✔
2430
            } else {
2431
                break;
115✔
2432
            }
2433
        }
2434
        const result = myIndex - 1;
116✔
2435
        if (result >= 0) {
116✔
2436
            return result;
43✔
2437
        } else {
2438
            return null;
73✔
2439
        }
2440
    }
2441

2442
    public hasParentClass() {
2443
        return !!this.parentClassName;
282✔
2444
    }
2445

2446
    /**
2447
     * Get all ancestor classes, in closest-to-furthest order (i.e. 0 is parent, 1 is grandparent, etc...).
2448
     * This will return an empty array if no ancestors were found
2449
     */
2450
    public getAncestors(state: BrsTranspileState) {
2451
        let ancestors = [] as ClassStatement[];
100✔
2452
        let stmt = this as ClassStatement;
100✔
2453
        while (stmt) {
100✔
2454
            if (stmt.parentClassName) {
142✔
2455
                const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
42✔
2456
                stmt = state.file.getClassFileLink(
42✔
2457
                    stmt.parentClassName.getName(),
2458
                    namespace?.getName(ParseMode.BrighterScript)
126✔
2459
                )?.item;
42!
2460
                ancestors.push(stmt);
42✔
2461
            } else {
2462
                break;
100✔
2463
            }
2464
        }
2465
        return ancestors;
100✔
2466
    }
2467

2468
    private getBuilderName(name: string) {
2469
        if (name.includes('.')) {
118✔
2470
            name = name.replace(/\./gi, '_');
3✔
2471
        }
2472
        return `__${name}_builder`;
118✔
2473
    }
2474

2475
    public getConstructorType() {
2476
        const constructorType = this.getConstructorFunction()?.getType({ flags: SymbolTypeFlag.runtime }) ?? new TypedFunctionType(null);
128✔
2477
        constructorType.returnType = this.getType({ flags: SymbolTypeFlag.runtime });
128✔
2478
        return constructorType;
128✔
2479
    }
2480

2481
    /**
2482
     * Get the constructor function for this class (if exists), or undefined if not exist
2483
     */
2484
    private getConstructorFunction() {
2485
        return this.body.find((stmt) => {
243✔
2486
            return (stmt as MethodStatement)?.tokens.name?.text?.toLowerCase() === 'new';
212!
2487
        }) as MethodStatement;
2488
    }
2489

2490
    /**
2491
     * Determine if the specified field was declared in one of the ancestor classes
2492
     */
2493
    public isFieldDeclaredByAncestor(fieldName: string, ancestors: ClassStatement[]) {
2494
        let lowerFieldName = fieldName.toLowerCase();
×
2495
        for (let ancestor of ancestors) {
×
2496
            if (ancestor.memberMap[lowerFieldName]) {
×
2497
                return true;
×
2498
            }
2499
        }
2500
        return false;
×
2501
    }
2502

2503
    /**
2504
     * The builder is a function that assigns all of the methods and property names to a class instance.
2505
     * This needs to be a separate function so that child classes can call the builder from their parent
2506
     * without instantiating the parent constructor at that point in time.
2507
     */
2508
    private getTranspiledBuilder(state: BrsTranspileState) {
2509
        let result = [] as TranspileResult;
50✔
2510
        result.push(`function ${this.getBuilderName(this.getName(ParseMode.BrightScript)!)}()\n`);
50✔
2511
        state.blockDepth++;
50✔
2512
        //indent
2513
        result.push(state.indent());
50✔
2514

2515
        /**
2516
         * The lineage of this class. index 0 is a direct parent, index 1 is index 0's parent, etc...
2517
         */
2518
        let ancestors = this.getAncestors(state);
50✔
2519

2520
        //construct parent class or empty object
2521
        if (ancestors[0]) {
50✔
2522
            const ancestorNamespace = ancestors[0].findAncestor<NamespaceStatement>(isNamespaceStatement);
18✔
2523
            let fullyQualifiedClassName = util.getFullyQualifiedClassName(
18✔
2524
                ancestors[0].getName(ParseMode.BrighterScript)!,
2525
                ancestorNamespace?.getName(ParseMode.BrighterScript)
54✔
2526
            );
2527
            result.push(
18✔
2528
                'instance = ',
2529
                this.getBuilderName(fullyQualifiedClassName), '()');
2530
        } else {
2531
            //use an empty object.
2532
            result.push('instance = {}');
32✔
2533
        }
2534
        result.push(
50✔
2535
            state.newline,
2536
            state.indent()
2537
        );
2538
        let parentClassIndex = this.getParentClassIndex(state);
50✔
2539

2540
        let body = this.body;
50✔
2541
        //inject an empty "new" method if missing
2542
        if (!this.getConstructorFunction()) {
50✔
2543
            body = [
29✔
2544
                createMethodStatement('new', TokenKind.Sub),
2545
                ...this.body
2546
            ];
2547
        }
2548

2549
        for (let statement of body) {
50✔
2550
            //is field statement
2551
            if (isFieldStatement(statement)) {
79✔
2552
                //do nothing with class fields in this situation, they are handled elsewhere
2553
                continue;
14✔
2554

2555
                //methods
2556
            } else if (isMethodStatement(statement)) {
65!
2557

2558
                //store overridden parent methods as super{parentIndex}_{methodName}
2559
                if (
65✔
2560
                    //is override method
2561
                    statement.tokens.override ||
176✔
2562
                    //is constructor function in child class
2563
                    (statement.tokens.name.text.toLowerCase() === 'new' && ancestors[0])
2564
                ) {
2565
                    result.push(
22✔
2566
                        `instance.super${parentClassIndex}_${statement.tokens.name.text} = instance.${statement.tokens.name.text}`,
2567
                        state.newline,
2568
                        state.indent()
2569
                    );
2570
                }
2571

2572
                state.classStatement = this;
65✔
2573
                state.skipLeadingComments = true;
65✔
2574
                //add leading comments
2575
                if (statement.leadingTrivia.filter(token => token.kind === TokenKind.Comment).length > 0) {
78✔
2576
                    result.push(
2✔
2577
                        ...state.transpileComments(statement.leadingTrivia),
2578
                        state.indent()
2579
                    );
2580
                }
2581
                result.push(
65✔
2582
                    'instance.',
2583
                    state.transpileToken(statement.tokens.name),
2584
                    ' = ',
2585
                    ...statement.transpile(state),
2586
                    state.newline,
2587
                    state.indent()
2588
                );
2589
                state.skipLeadingComments = false;
65✔
2590
                delete state.classStatement;
65✔
2591
            } else {
2592
                //other random statements (probably just comments)
2593
                result.push(
×
2594
                    ...statement.transpile(state),
2595
                    state.newline,
2596
                    state.indent()
2597
                );
2598
            }
2599
        }
2600
        //return the instance
2601
        result.push('return instance\n');
50✔
2602
        state.blockDepth--;
50✔
2603
        result.push(state.indent());
50✔
2604
        result.push(`end function`);
50✔
2605
        return result;
50✔
2606
    }
2607

2608
    /**
2609
     * The class function is the function with the same name as the class. This is the function that
2610
     * consumers should call to create a new instance of that class.
2611
     * This invokes the builder, gets an instance of the class, then invokes the "new" function on that class.
2612
     */
2613
    private getTranspiledClassFunction(state: BrsTranspileState) {
2614
        let result: TranspileResult = state.transpileAnnotations(this);
50✔
2615
        const constructorFunction = this.getConstructorFunction();
50✔
2616
        const constructorParams = constructorFunction ? constructorFunction.func.parameters : [];
50✔
2617

2618
        result.push(
50✔
2619
            state.transpileLeadingComments(this.tokens.class),
2620
            state.sourceNode(this.tokens.class, 'function'),
2621
            state.sourceNode(this.tokens.class, ' '),
2622
            state.sourceNode(this.tokens.name, this.getName(ParseMode.BrightScript)),
2623
            `(`
2624
        );
2625
        let i = 0;
50✔
2626
        for (let param of constructorParams) {
50✔
2627
            if (i > 0) {
8✔
2628
                result.push(', ');
2✔
2629
            }
2630
            result.push(
8✔
2631
                param.transpile(state)
2632
            );
2633
            i++;
8✔
2634
        }
2635
        result.push(
50✔
2636
            ')',
2637
            '\n'
2638
        );
2639

2640
        state.blockDepth++;
50✔
2641
        result.push(state.indent());
50✔
2642
        result.push(`instance = ${this.getBuilderName(this.getName(ParseMode.BrightScript)!)}()\n`);
50✔
2643

2644
        result.push(state.indent());
50✔
2645
        result.push(`instance.new(`);
50✔
2646

2647
        //append constructor arguments
2648
        i = 0;
50✔
2649
        for (let param of constructorParams) {
50✔
2650
            if (i > 0) {
8✔
2651
                result.push(', ');
2✔
2652
            }
2653
            result.push(
8✔
2654
                state.transpileToken(param.tokens.name)
2655
            );
2656
            i++;
8✔
2657
        }
2658
        result.push(
50✔
2659
            ')',
2660
            '\n'
2661
        );
2662

2663
        result.push(state.indent());
50✔
2664
        result.push(`return instance\n`);
50✔
2665

2666
        state.blockDepth--;
50✔
2667
        result.push(state.indent());
50✔
2668
        result.push(`end function`);
50✔
2669
        return result;
50✔
2670
    }
2671

2672
    walk(visitor: WalkVisitor, options: WalkOptions) {
2673
        //visitor-less walk function to do parent linking
2674
        walk(this, 'parentClassName', null, options);
2,622✔
2675

2676
        if (options.walkMode & InternalWalkMode.walkStatements) {
2,622!
2677
            walkArray(this.body, visitor, options, this);
2,622✔
2678
        }
2679
    }
2680

2681
    getType(options: GetTypeOptions) {
2682
        const superClass = this.parentClassName?.getType(options) as ClassType;
542✔
2683

2684
        const resultType = new ClassType(this.getName(ParseMode.BrighterScript), superClass);
542✔
2685

2686
        for (const statement of this.methods) {
542✔
2687
            const funcType = statement?.func.getType({ ...options, typeChain: undefined }); //no typechain needed
285!
2688
            let flag = SymbolTypeFlag.runtime;
285✔
2689
            if (statement.accessModifier?.kind === TokenKind.Private) {
285✔
2690
                flag |= SymbolTypeFlag.private;
9✔
2691
            }
2692
            if (statement.accessModifier?.kind === TokenKind.Protected) {
285✔
2693
                flag |= SymbolTypeFlag.protected;
8✔
2694
            }
2695
            resultType.addMember(statement?.tokens.name?.text, { definingNode: statement }, funcType, flag);
285!
2696
        }
2697
        for (const statement of this.fields) {
542✔
2698
            const fieldType = statement.getType({ ...options, typeChain: undefined }); //no typechain needed
270✔
2699
            let flag = SymbolTypeFlag.runtime;
270✔
2700
            if (statement.isOptional) {
270✔
2701
                flag |= SymbolTypeFlag.optional;
7✔
2702
            }
2703
            if (statement.tokens.accessModifier?.kind === TokenKind.Private) {
270✔
2704
                flag |= SymbolTypeFlag.private;
19✔
2705
            }
2706
            if (statement.tokens.accessModifier?.kind === TokenKind.Protected) {
270✔
2707
                flag |= SymbolTypeFlag.protected;
9✔
2708
            }
2709
            resultType.addMember(statement?.tokens.name?.text, { definingNode: statement, isInstance: true }, fieldType, flag);
270!
2710
        }
2711
        options.typeChain?.push(new TypeChainEntry({ name: resultType.name, type: resultType, data: options.data, astNode: this }));
542✔
2712
        return resultType;
542✔
2713
    }
2714
}
2715

2716
const accessModifiers = [
1✔
2717
    TokenKind.Public,
2718
    TokenKind.Protected,
2719
    TokenKind.Private
2720
];
2721
export class MethodStatement extends FunctionStatement {
1✔
2722
    constructor(
2723
        options: {
2724
            modifiers?: Token | Token[];
2725
            name: Identifier;
2726
            func: FunctionExpression;
2727
            override?: Token;
2728
        }
2729
    ) {
2730
        super(options);
391✔
2731
        if (options.modifiers) {
391✔
2732
            if (Array.isArray(options.modifiers)) {
35!
NEW
2733
                this.modifiers.push(...options.modifiers);
×
2734
            } else {
2735
                this.modifiers.push(options.modifiers);
35✔
2736
            }
2737
        }
2738
        this.tokens = {
391✔
2739
            ...this.tokens,
2740
            override: options.override
2741
        };
2742
        this.location = util.createBoundingLocation(
391✔
2743
            ...(this.modifiers),
2744
            util.createBoundingLocationFromTokens(this.tokens),
2745
            this.func
2746
        );
2747
    }
2748

2749
    public readonly kind = AstNodeKind.MethodStatement as AstNodeKind;
391✔
2750

2751
    public readonly modifiers: Token[] = [];
391✔
2752

2753
    public readonly tokens: {
2754
        readonly name: Identifier;
2755
        readonly override?: Token;
2756
    };
2757

2758
    public get accessModifier() {
2759
        return this.modifiers.find(x => accessModifiers.includes(x.kind));
678✔
2760
    }
2761

2762
    public readonly location: Location | undefined;
2763

2764
    /**
2765
     * Get the name of this method.
2766
     */
2767
    public getName(parseMode: ParseMode) {
2768
        return this.tokens.name.text;
358✔
2769
    }
2770

2771
    public get leadingTrivia(): Token[] {
2772
        return this.func.leadingTrivia;
800✔
2773
    }
2774

2775
    transpile(state: BrsTranspileState) {
2776
        if (this.tokens.name.text.toLowerCase() === 'new') {
65✔
2777
            this.ensureSuperConstructorCall(state);
50✔
2778
            //TODO we need to undo this at the bottom of this method
2779
            this.injectFieldInitializersForConstructor(state);
50✔
2780
        }
2781
        //TODO - remove type information from these methods because that doesn't work
2782
        //convert the `super` calls into the proper methods
2783
        const parentClassIndex = state.classStatement.getParentClassIndex(state);
65✔
2784
        const visitor = createVisitor({
65✔
2785
            VariableExpression: e => {
2786
                if (e.tokens.name.text.toLocaleLowerCase() === 'super') {
61✔
2787
                    state.editor.setProperty(e.tokens.name, 'text', `m.super${parentClassIndex}_new`);
18✔
2788
                }
2789
            },
2790
            DottedGetExpression: e => {
2791
                const beginningVariable = util.findBeginningVariableExpression(e);
30✔
2792
                const lowerName = beginningVariable?.getName(ParseMode.BrighterScript).toLowerCase();
30!
2793
                if (lowerName === 'super') {
30✔
2794
                    state.editor.setProperty(beginningVariable.tokens.name, 'text', 'm');
7✔
2795
                    state.editor.setProperty(e.tokens.name, 'text', `super${parentClassIndex}_${e.tokens.name.text}`);
7✔
2796
                }
2797
            }
2798
        });
2799
        const walkOptions: WalkOptions = { walkMode: WalkMode.visitExpressions };
65✔
2800
        for (const statement of this.func.body.statements) {
65✔
2801
            visitor(statement, undefined);
62✔
2802
            statement.walk(visitor, walkOptions);
62✔
2803
        }
2804
        return this.func.transpile(state);
65✔
2805
    }
2806

2807
    getTypedef(state: BrsTranspileState) {
2808
        const result: TranspileResult = [];
23✔
2809
        for (let comment of util.getLeadingComments(this) ?? []) {
23!
NEW
2810
            result.push(
×
2811
                comment.text,
2812
                state.newline,
2813
                state.indent()
2814
            );
2815
        }
2816
        for (let annotation of this.annotations ?? []) {
23✔
2817
            result.push(
2✔
2818
                ...annotation.getTypedef(state),
2819
                state.newline,
2820
                state.indent()
2821
            );
2822
        }
2823
        if (this.accessModifier) {
23✔
2824
            result.push(
8✔
2825
                this.accessModifier.text,
2826
                ' '
2827
            );
2828
        }
2829
        if (this.tokens.override) {
23✔
2830
            result.push('override ');
1✔
2831
        }
2832
        result.push(
23✔
2833
            ...this.func.getTypedef(state)
2834
        );
2835
        return result;
23✔
2836
    }
2837

2838
    /**
2839
     * All child classes must call the parent constructor. The type checker will warn users when they don't call it in their own class,
2840
     * but we still need to call it even if they have omitted it. This injects the super call if it's missing
2841
     */
2842
    private ensureSuperConstructorCall(state: BrsTranspileState) {
2843
        //if this class doesn't extend another class, quit here
2844
        if (state.classStatement!.getAncestors(state).length === 0) {
50✔
2845
            return;
32✔
2846
        }
2847

2848
        //check whether any calls to super exist
2849
        let containsSuperCall =
2850
            this.func.body.statements.findIndex((x) => {
18✔
2851
                //is a call statement
2852
                return isExpressionStatement(x) && isCallExpression(x.expression) &&
8✔
2853
                    //is a call to super
2854
                    util.findBeginningVariableExpression(x.expression.callee as any).tokens.name?.text.toLowerCase() === 'super';
21!
2855
            }) !== -1;
2856

2857
        //if a call to super exists, quit here
2858
        if (containsSuperCall) {
18✔
2859
            return;
7✔
2860
        }
2861

2862
        //this is a child class, and the constructor doesn't contain a call to super. Inject one
2863
        const superCall = new ExpressionStatement({
11✔
2864
            expression: new CallExpression({
2865
                callee: new VariableExpression({
2866
                    name: {
2867
                        kind: TokenKind.Identifier,
2868
                        text: 'super',
2869
                        isReserved: false,
2870
                        location: state.classStatement.tokens.name.location,
2871
                        leadingWhitespace: '',
2872
                        leadingTrivia: []
2873
                    }
2874
                }),
2875
                openingParen: {
2876
                    kind: TokenKind.LeftParen,
2877
                    text: '(',
2878
                    isReserved: false,
2879
                    location: state.classStatement.tokens.name.location,
2880
                    leadingWhitespace: '',
2881
                    leadingTrivia: []
2882
                },
2883
                closingParen: {
2884
                    kind: TokenKind.RightParen,
2885
                    text: ')',
2886
                    isReserved: false,
2887
                    location: state.classStatement.tokens.name.location,
2888
                    leadingWhitespace: '',
2889
                    leadingTrivia: []
2890
                },
2891
                args: []
2892
            })
2893
        });
2894
        state.editor.arrayUnshift(this.func.body.statements, superCall);
11✔
2895
    }
2896

2897
    /**
2898
     * Inject field initializers at the top of the `new` function (after any present `super()` call)
2899
     */
2900
    private injectFieldInitializersForConstructor(state: BrsTranspileState) {
2901
        let startingIndex = state.classStatement!.hasParentClass() ? 1 : 0;
50✔
2902

2903
        let newStatements = [] as Statement[];
50✔
2904
        //insert the field initializers in order
2905
        for (let field of state.classStatement!.fields) {
50✔
2906
            let thisQualifiedName = { ...field.tokens.name };
14✔
2907
            thisQualifiedName.text = 'm.' + field.tokens.name?.text;
14!
2908
            const fieldAssignment = field.initialValue
14✔
2909
                ? new AssignmentStatement({
14✔
2910
                    equals: field.tokens.equals,
2911
                    name: thisQualifiedName,
2912
                    value: field.initialValue
2913
                })
2914
                : new AssignmentStatement({
2915
                    equals: createToken(TokenKind.Equal, '=', field.tokens.name.location),
2916
                    name: thisQualifiedName,
2917
                    //if there is no initial value, set the initial value to `invalid`
2918
                    value: createInvalidLiteral('invalid', field.tokens.name.location)
2919
                });
2920
            // Add parent so namespace lookups work
2921
            fieldAssignment.parent = state.classStatement;
14✔
2922
            newStatements.push(fieldAssignment);
14✔
2923
        }
2924
        state.editor.arraySplice(this.func.body.statements, startingIndex, 0, ...newStatements);
50✔
2925
    }
2926

2927
    walk(visitor: WalkVisitor, options: WalkOptions) {
2928
        if (options.walkMode & InternalWalkMode.walkExpressions) {
2,165✔
2929
            walk(this, 'func', visitor, options);
1,724✔
2930
        }
2931
    }
2932
}
2933

2934
export class FieldStatement extends Statement implements TypedefProvider {
1✔
2935
    constructor(options: {
2936
        accessModifier?: Token;
2937
        name: Identifier;
2938
        as?: Token;
2939
        typeExpression?: TypeExpression;
2940
        equals?: Token;
2941
        initialValue?: Expression;
2942
        optional?: Token;
2943
    }) {
2944
        super();
319✔
2945
        this.tokens = {
319✔
2946
            accessModifier: options.accessModifier,
2947
            name: options.name,
2948
            as: options.as,
2949
            equals: options.equals,
2950
            optional: options.optional
2951
        };
2952
        this.typeExpression = options.typeExpression;
319✔
2953
        this.initialValue = options.initialValue;
319✔
2954

2955
        this.location = util.createBoundingLocation(
319✔
2956
            util.createBoundingLocationFromTokens(this.tokens),
2957
            this.typeExpression,
2958
            this.initialValue
2959
        );
2960
    }
2961

2962
    public readonly tokens: {
2963
        readonly accessModifier?: Token;
2964
        readonly name: Identifier;
2965
        readonly as?: Token;
2966
        readonly equals?: Token;
2967
        readonly optional?: Token;
2968
    };
2969

2970
    public readonly typeExpression?: TypeExpression;
2971
    public readonly initialValue?: Expression;
2972

2973
    public readonly kind = AstNodeKind.FieldStatement;
319✔
2974

2975
    /**
2976
     * Derive a ValueKind from the type token, or the initial value.
2977
     * Defaults to `DynamicType`
2978
     */
2979
    getType(options: GetTypeOptions) {
2980
        return this.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime }) ??
311✔
2981
            this.initialValue?.getType({ ...options, flags: SymbolTypeFlag.runtime }) ?? DynamicType.instance;
725✔
2982
    }
2983

2984
    public readonly location: Location | undefined;
2985

2986
    public get leadingTrivia(): Token[] {
2987
        return this.tokens.accessModifier?.leadingTrivia ?? this.tokens.optional?.leadingTrivia ?? this.tokens.name.leadingTrivia;
639✔
2988
    }
2989

2990
    public get isOptional() {
2991
        return !!this.tokens.optional;
287✔
2992
    }
2993

2994
    transpile(state: BrsTranspileState): TranspileResult {
2995
        throw new Error('transpile not implemented for ' + Object.getPrototypeOf(this).constructor.name);
×
2996
    }
2997

2998
    getTypedef(state: BrsTranspileState) {
2999
        const result = [];
10✔
3000
        if (this.tokens.name) {
10!
3001
            for (let comment of util.getLeadingComments(this) ?? []) {
10!
NEW
3002
                result.push(
×
3003
                    comment.text,
3004
                    state.newline,
3005
                    state.indent()
3006
                );
3007
            }
3008
            for (let annotation of this.annotations ?? []) {
10✔
3009
                result.push(
2✔
3010
                    ...annotation.getTypedef(state),
3011
                    state.newline,
3012
                    state.indent()
3013
                );
3014
            }
3015

3016
            let type = this.getType({ flags: SymbolTypeFlag.typetime });
10✔
3017
            if (isInvalidType(type) || isVoidType(type)) {
10!
UNCOV
3018
                type = new DynamicType();
×
3019
            }
3020

3021
            result.push(
10✔
3022
                this.tokens.accessModifier?.text ?? 'public',
60!
3023
                ' '
3024
            );
3025
            if (this.isOptional) {
10!
NEW
3026
                result.push(this.tokens.optional.text, ' ');
×
3027
            }
3028
            result.push(this.tokens.name?.text,
10!
3029
                ' as ',
3030
                type.toTypeString()
3031
            );
3032
        }
3033
        return result;
10✔
3034
    }
3035

3036
    walk(visitor: WalkVisitor, options: WalkOptions) {
3037
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,592✔
3038
            walk(this, 'typeExpression', visitor, options);
1,388✔
3039
            walk(this, 'initialValue', visitor, options);
1,388✔
3040
        }
3041
    }
3042
}
3043

3044
export type MemberStatement = FieldStatement | MethodStatement;
3045

3046
export class TryCatchStatement extends Statement {
1✔
3047
    constructor(options?: {
3048
        try?: Token;
3049
        endTry?: Token;
3050
        tryBranch?: Block;
3051
        catchStatement?: CatchStatement;
3052
    }) {
3053
        super();
28✔
3054
        this.tokens = {
28✔
3055
            try: options.try,
3056
            endTry: options.endTry
3057
        };
3058
        this.tryBranch = options.tryBranch;
28✔
3059
        this.catchStatement = options.catchStatement;
28✔
3060
        this.location = util.createBoundingLocation(
28✔
3061
            this.tokens.try,
3062
            this.tryBranch,
3063
            this.catchStatement,
3064
            this.tokens.endTry
3065
        );
3066
    }
3067

3068
    public readonly tokens: {
3069
        readonly try?: Token;
3070
        readonly endTry?: Token;
3071
    };
3072

3073
    public readonly tryBranch: Block;
3074
    public readonly catchStatement: CatchStatement;
3075

3076
    public readonly kind = AstNodeKind.TryCatchStatement;
28✔
3077

3078
    public readonly location: Location | undefined;
3079

3080
    public transpile(state: BrsTranspileState): TranspileResult {
3081
        return [
4✔
3082
            state.transpileToken(this.tokens.try, 'try'),
3083
            ...this.tryBranch.transpile(state),
3084
            state.newline,
3085
            state.indent(),
3086
            ...(this.catchStatement?.transpile(state) ?? ['catch']),
24!
3087
            state.newline,
3088
            state.indent(),
3089
            state.transpileToken(this.tokens.endTry!, 'end try')
3090
        ];
3091
    }
3092

3093
    public walk(visitor: WalkVisitor, options: WalkOptions) {
3094
        if (this.tryBranch && options.walkMode & InternalWalkMode.walkStatements) {
59!
3095
            walk(this, 'tryBranch', visitor, options);
59✔
3096
            walk(this, 'catchStatement', visitor, options);
59✔
3097
        }
3098
    }
3099

3100
    public get leadingTrivia(): Token[] {
3101
        return this.tokens.try?.leadingTrivia ?? [];
57!
3102
    }
3103

3104
    public get endTrivia(): Token[] {
3105
        return this.tokens.endTry?.leadingTrivia ?? [];
1!
3106
    }
3107
}
3108

3109
export class CatchStatement extends Statement {
1✔
3110
    constructor(options?: {
3111
        catch?: Token;
3112
        exceptionVariable?: Identifier;
3113
        catchBranch?: Block;
3114
    }) {
3115
        super();
26✔
3116
        this.tokens = {
26✔
3117
            catch: options?.catch,
78!
3118
            exceptionVariable: options?.exceptionVariable
78!
3119
        };
3120
        this.catchBranch = options?.catchBranch;
26!
3121
        this.location = util.createBoundingLocation(
26✔
3122
            this.tokens.catch,
3123
            this.tokens.exceptionVariable,
3124
            this.catchBranch
3125
        );
3126
    }
3127

3128
    public readonly tokens: {
3129
        readonly catch?: Token;
3130
        readonly exceptionVariable?: Identifier;
3131
    };
3132

3133
    public readonly catchBranch?: Block;
3134

3135
    public readonly kind = AstNodeKind.CatchStatement;
26✔
3136

3137
    public readonly location: Location | undefined;
3138

3139
    public transpile(state: BrsTranspileState): TranspileResult {
3140
        return [
4✔
3141
            state.transpileToken(this.tokens.catch, 'catch'),
3142
            ' ',
3143
            this.tokens.exceptionVariable?.text ?? 'e',
24!
3144
            ...(this.catchBranch?.transpile(state) ?? [])
24!
3145
        ];
3146
    }
3147

3148
    public walk(visitor: WalkVisitor, options: WalkOptions) {
3149
        if (this.catchBranch && options.walkMode & InternalWalkMode.walkStatements) {
57!
3150
            walk(this, 'catchBranch', visitor, options);
57✔
3151
        }
3152
    }
3153

3154
    public get leadingTrivia(): Token[] {
3155
        return this.tokens.catch?.leadingTrivia ?? [];
35!
3156
    }
3157
}
3158

3159
export class ThrowStatement extends Statement {
1✔
3160
    constructor(options?: {
3161
        throw?: Token;
3162
        expression?: Expression;
3163
    }) {
3164
        super();
10✔
3165
        this.tokens = {
10✔
3166
            throw: options.throw
3167
        };
3168
        this.expression = options.expression;
10✔
3169
        this.location = util.createBoundingLocation(
10✔
3170
            this.tokens.throw,
3171
            this.expression
3172
        );
3173
    }
3174

3175
    public readonly tokens: {
3176
        readonly throw?: Token;
3177
    };
3178
    public readonly expression?: Expression;
3179

3180
    public readonly kind = AstNodeKind.ThrowStatement;
10✔
3181

3182
    public readonly location: Location | undefined;
3183

3184
    public transpile(state: BrsTranspileState) {
3185
        const result = [
5✔
3186
            state.transpileToken(this.tokens.throw, 'throw'),
3187
            ' '
3188
        ] as TranspileResult;
3189

3190
        //if we have an expression, transpile it
3191
        if (this.expression) {
5✔
3192
            result.push(
4✔
3193
                ...this.expression.transpile(state)
3194
            );
3195

3196
            //no expression found. Rather than emit syntax errors, provide a generic error message
3197
        } else {
3198
            result.push('"User-specified exception"');
1✔
3199
        }
3200
        return result;
5✔
3201
    }
3202

3203
    public walk(visitor: WalkVisitor, options: WalkOptions) {
3204
        if (this.expression && options.walkMode & InternalWalkMode.walkExpressions) {
33✔
3205
            walk(this, 'expression', visitor, options);
26✔
3206
        }
3207
    }
3208

3209
    public get leadingTrivia(): Token[] {
3210
        return this.tokens.throw?.leadingTrivia ?? [];
54!
3211
    }
3212
}
3213

3214

3215
export class EnumStatement extends Statement implements TypedefProvider {
1✔
3216
    constructor(options: {
3217
        enum?: Token;
3218
        name: Identifier;
3219
        endEnum?: Token;
3220
        body: Array<EnumMemberStatement>;
3221
    }) {
3222
        super();
158✔
3223
        this.tokens = {
158✔
3224
            enum: options.enum,
3225
            name: options.name,
3226
            endEnum: options.endEnum
3227
        };
3228
        this.symbolTable = new SymbolTable('Enum');
158✔
3229
        this.body = options.body ?? [];
158!
3230
    }
3231

3232
    public readonly tokens: {
3233
        readonly enum?: Token;
3234
        readonly name: Identifier;
3235
        readonly endEnum?: Token;
3236
    };
3237
    public readonly body: Array<EnumMemberStatement>;
3238

3239
    public readonly kind = AstNodeKind.EnumStatement;
158✔
3240

3241
    public get location(): Location | undefined {
3242
        return util.createBoundingLocation(
138✔
3243
            this.tokens.enum,
3244
            this.tokens.name,
3245
            ...this.body,
3246
            this.tokens.endEnum
3247
        );
3248
    }
3249

3250
    public getMembers() {
3251
        const result = [] as EnumMemberStatement[];
331✔
3252
        for (const statement of this.body) {
331✔
3253
            if (isEnumMemberStatement(statement)) {
685!
3254
                result.push(statement);
685✔
3255
            }
3256
        }
3257
        return result;
331✔
3258
    }
3259

3260
    public get leadingTrivia(): Token[] {
3261
        return this.tokens.enum?.leadingTrivia;
470!
3262
    }
3263

3264
    public get endTrivia(): Token[] {
NEW
3265
        return this.tokens.endEnum?.leadingTrivia ?? [];
×
3266
    }
3267

3268
    /**
3269
     * Get a map of member names and their values.
3270
     * All values are stored as their AST LiteralExpression representation (i.e. string enum values include the wrapping quotes)
3271
     */
3272
    public getMemberValueMap() {
3273
        const result = new Map<string, string>();
59✔
3274
        const members = this.getMembers();
59✔
3275
        let currentIntValue = 0;
59✔
3276
        for (const member of members) {
59✔
3277
            //if there is no value, assume an integer and increment the int counter
3278
            if (!member.value) {
148✔
3279
                result.set(member.name?.toLowerCase(), currentIntValue.toString());
33!
3280
                currentIntValue++;
33✔
3281

3282
                //if explicit integer value, use it and increment the int counter
3283
            } else if (isLiteralExpression(member.value) && member.value.tokens.value.kind === TokenKind.IntegerLiteral) {
115✔
3284
                //try parsing as integer literal, then as hex integer literal.
3285
                let tokenIntValue = util.parseInt(member.value.tokens.value.text) ?? util.parseInt(member.value.tokens.value.text.replace(/&h/i, '0x'));
29✔
3286
                if (tokenIntValue !== undefined) {
29!
3287
                    currentIntValue = tokenIntValue;
29✔
3288
                    currentIntValue++;
29✔
3289
                }
3290
                result.set(member.name?.toLowerCase(), member.value.tokens.value.text);
29!
3291

3292
                //simple unary expressions (like `-1`)
3293
            } else if (isUnaryExpression(member.value) && isLiteralExpression(member.value.right)) {
86✔
3294
                result.set(member.name?.toLowerCase(), member.value.tokens.operator.text + member.value.right.tokens.value.text);
1!
3295

3296
                //all other values
3297
            } else {
3298
                result.set(member.name?.toLowerCase(), (member.value as LiteralExpression)?.tokens?.value?.text ?? 'invalid');
85!
3299
            }
3300
        }
3301
        return result;
59✔
3302
    }
3303

3304
    public getMemberValue(name: string) {
3305
        return this.getMemberValueMap().get(name.toLowerCase());
56✔
3306
    }
3307

3308
    /**
3309
     * The name of the enum (without the namespace prefix)
3310
     */
3311
    public get name() {
3312
        return this.tokens.name?.text;
1!
3313
    }
3314

3315
    /**
3316
     * The name of the enum WITH its leading namespace (if applicable)
3317
     */
3318
    public get fullName() {
3319
        const name = this.tokens.name?.text;
683!
3320
        if (name) {
683!
3321
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
683✔
3322

3323
            if (namespace) {
683✔
3324
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
257✔
3325
                return `${namespaceName}.${name}`;
257✔
3326
            } else {
3327
                return name;
426✔
3328
            }
3329
        } else {
3330
            //return undefined which will allow outside callers to know that this doesn't have a name
3331
            return undefined;
×
3332
        }
3333
    }
3334

3335
    transpile(state: BrsTranspileState) {
3336
        //enum declarations don't exist at runtime, so just transpile comments and trivia
3337
        return [
25✔
3338
            state.transpileAnnotations(this),
3339
            state.transpileLeadingComments(this.tokens.enum)
3340
        ];
3341
    }
3342

3343
    getTypedef(state: BrsTranspileState) {
3344
        const result = [] as TranspileResult;
1✔
3345
        for (let comment of util.getLeadingComments(this) ?? []) {
1!
NEW
3346
            result.push(
×
3347
                comment.text,
3348
                state.newline,
3349
                state.indent()
3350
            );
3351
        }
3352
        for (let annotation of this.annotations ?? []) {
1!
3353
            result.push(
×
3354
                ...annotation.getTypedef(state),
3355
                state.newline,
3356
                state.indent()
3357
            );
3358
        }
3359
        result.push(
1✔
3360
            this.tokens.enum?.text ?? 'enum',
6!
3361
            ' ',
3362
            this.tokens.name.text
3363
        );
3364
        result.push(state.newline);
1✔
3365
        state.blockDepth++;
1✔
3366
        for (const member of this.body) {
1✔
3367
            if (isTypedefProvider(member)) {
1!
3368
                result.push(
1✔
3369
                    state.indent(),
3370
                    ...member.getTypedef(state),
3371
                    state.newline
3372
                );
3373
            }
3374
        }
3375
        state.blockDepth--;
1✔
3376
        result.push(
1✔
3377
            state.indent(),
3378
            this.tokens.endEnum?.text ?? 'end enum'
6!
3379
        );
3380
        return result;
1✔
3381
    }
3382

3383
    walk(visitor: WalkVisitor, options: WalkOptions) {
3384
        if (options.walkMode & InternalWalkMode.walkStatements) {
1,010!
3385
            walkArray(this.body, visitor, options, this);
1,010✔
3386

3387
        }
3388
    }
3389

3390
    getType(options: GetTypeOptions) {
3391
        const members = this.getMembers();
136✔
3392

3393
        const resultType = new EnumType(
136✔
3394
            this.fullName,
3395
            members[0]?.getType(options).underlyingType
408✔
3396
        );
3397
        resultType.pushMemberProvider(() => this.getSymbolTable());
136✔
3398
        for (const statement of members) {
136✔
3399
            resultType.addMember(statement?.tokens?.name?.text, { definingNode: statement }, statement.getType(options), SymbolTypeFlag.runtime);
267!
3400
        }
3401
        return resultType;
136✔
3402
    }
3403
}
3404

3405
export class EnumMemberStatement extends Statement implements TypedefProvider {
1✔
3406
    public constructor(options: {
3407
        name: Identifier;
3408
        equals?: Token;
3409
        value?: Expression;
3410
    }) {
3411
        super();
314✔
3412
        this.tokens = {
314✔
3413
            name: options.name,
3414
            equals: options.equals
3415
        };
3416
        this.value = options.value;
314✔
3417
    }
3418

3419
    public readonly tokens: {
3420
        readonly name: Identifier;
3421
        readonly equals?: Token;
3422
    };
3423
    public readonly value?: Expression;
3424

3425
    public readonly kind = AstNodeKind.EnumMemberStatement;
314✔
3426

3427
    public get location() {
3428
        return util.createBoundingLocation(
449✔
3429
            this.tokens.name,
3430
            this.tokens.equals,
3431
            this.value
3432
        );
3433
    }
3434

3435
    /**
3436
     * The name of the member
3437
     */
3438
    public get name() {
3439
        return this.tokens.name.text;
408✔
3440
    }
3441

3442
    public get leadingTrivia(): Token[] {
3443
        return this.tokens.name.leadingTrivia;
1,236✔
3444
    }
3445

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

3450
    getTypedef(state: BrsTranspileState): TranspileResult {
3451
        const result: TranspileResult = [];
1✔
3452
        for (let comment of util.getLeadingComments(this) ?? []) {
1!
NEW
3453
            result.push(
×
3454
                comment.text,
3455
                state.newline,
3456
                state.indent()
3457
            );
3458
        }
3459
        result.push(this.tokens.name.text);
1✔
3460
        if (this.tokens.equals) {
1!
NEW
3461
            result.push(' ', this.tokens.equals.text, ' ');
×
3462
            if (this.value) {
×
3463
                result.push(
×
3464
                    ...this.value.transpile(state)
3465
                );
3466
            }
3467
        }
3468
        return result;
1✔
3469
    }
3470

3471
    walk(visitor: WalkVisitor, options: WalkOptions) {
3472
        if (this.value && options.walkMode & InternalWalkMode.walkExpressions) {
2,411✔
3473
            walk(this, 'value', visitor, options);
1,139✔
3474
        }
3475
    }
3476

3477
    getType(options: GetTypeOptions) {
3478
        return new EnumMemberType(
403✔
3479
            (this.parent as EnumStatement)?.fullName,
1,209!
3480
            this.tokens?.name?.text,
2,418!
3481
            this.value?.getType(options)
1,209✔
3482
        );
3483
    }
3484
}
3485

3486
export class ConstStatement extends Statement implements TypedefProvider {
1✔
3487
    public constructor(options: {
3488
        const?: Token;
3489
        name: Identifier;
3490
        equals?: Token;
3491
        value: Expression;
3492
    }) {
3493
        super();
152✔
3494
        this.tokens = {
152✔
3495
            const: options.const,
3496
            name: options.name,
3497
            equals: options.equals
3498
        };
3499
        this.value = options.value;
152✔
3500
        this.location = util.createBoundingLocation(this.tokens.const, this.tokens.name, this.tokens.equals, this.value);
152✔
3501
    }
3502

3503
    public readonly tokens: {
3504
        readonly const: Token;
3505
        readonly name: Identifier;
3506
        readonly equals: Token;
3507
    };
3508
    public readonly value: Expression;
3509

3510
    public readonly kind = AstNodeKind.ConstStatement;
152✔
3511

3512
    public readonly location: Location | undefined;
3513

3514
    public get name() {
UNCOV
3515
        return this.tokens.name.text;
×
3516
    }
3517

3518
    public get leadingTrivia(): Token[] {
3519
        return this.tokens.const?.leadingTrivia;
471!
3520
    }
3521

3522
    /**
3523
     * The name of the statement WITH its leading namespace (if applicable)
3524
     */
3525
    public get fullName() {
3526
        const name = this.tokens.name?.text;
227!
3527
        if (name) {
227!
3528
            const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
227✔
3529
            if (namespace) {
227✔
3530
                let namespaceName = namespace.getName(ParseMode.BrighterScript);
204✔
3531
                return `${namespaceName}.${name}`;
204✔
3532
            } else {
3533
                return name;
23✔
3534
            }
3535
        } else {
3536
            //return undefined which will allow outside callers to know that this doesn't have a name
3537
            return undefined;
×
3538
        }
3539
    }
3540

3541
    public transpile(state: BrsTranspileState): TranspileResult {
3542
        //const declarations don't exist at runtime, so just transpile comments and trivia
3543
        return [
27✔
3544
            state.transpileAnnotations(this),
3545
            state.transpileLeadingComments(this.tokens.const)
3546
        ];
3547
    }
3548

3549
    getTypedef(state: BrsTranspileState): TranspileResult {
3550
        const result: TranspileResult = [];
3✔
3551
        for (let comment of util.getLeadingComments(this) ?? []) {
3!
NEW
3552
            result.push(
×
3553
                comment.text,
3554
                state.newline,
3555
                state.indent()
3556
            );
3557
        }
3558
        result.push(
3✔
3559
            this.tokens.const ? state.tokenToSourceNode(this.tokens.const) : 'const',
3!
3560
            ' ',
3561
            state.tokenToSourceNode(this.tokens.name),
3562
            ' ',
3563
            this.tokens.equals ? state.tokenToSourceNode(this.tokens.equals) : '=',
3!
3564
            ' ',
3565
            ...this.value.transpile(state)
3566
        );
3567
        return result;
3✔
3568
    }
3569

3570
    walk(visitor: WalkVisitor, options: WalkOptions) {
3571
        if (this.value && options.walkMode & InternalWalkMode.walkExpressions) {
1,012✔
3572
            walk(this, 'value', visitor, options);
874✔
3573
        }
3574
    }
3575

3576
    getType(options: GetTypeOptions) {
3577
        return this.value.getType(options);
150✔
3578
    }
3579
}
3580

3581
export class ContinueStatement extends Statement {
1✔
3582
    constructor(options: {
3583
        continue?: Token;
3584
        loopType: Token;
3585
    }
3586
    ) {
3587
        super();
11✔
3588
        this.tokens = {
11✔
3589
            continue: options.continue,
3590
            loopType: options.loopType
3591
        };
3592
        this.location = util.createBoundingLocation(
11✔
3593
            this.tokens.continue,
3594
            this.tokens.loopType
3595
        );
3596
    }
3597

3598
    public readonly tokens: {
3599
        readonly continue?: Token;
3600
        readonly loopType: Token;
3601
    };
3602

3603
    public readonly kind = AstNodeKind.ContinueStatement;
11✔
3604

3605
    public readonly location: Location | undefined;
3606

3607
    transpile(state: BrsTranspileState) {
3608
        return [
3✔
3609
            state.sourceNode(this.tokens.continue, this.tokens.continue?.text ?? 'continue'),
18!
3610
            this.tokens.loopType?.leadingWhitespace ?? ' ',
18✔
3611
            state.sourceNode(this.tokens.continue, this.tokens.loopType?.text)
9✔
3612
        ];
3613
    }
3614
    walk(visitor: WalkVisitor, options: WalkOptions) {
3615
        //nothing to walk
3616
    }
3617

3618
    public get leadingTrivia(): Token[] {
3619
        return this.tokens.continue?.leadingTrivia ?? [];
86!
3620
    }
3621
}
3622

3623

3624
export class TypecastStatement extends Statement {
1✔
3625
    constructor(options: {
3626
        typecast?: Token;
3627
        typecastExpression: TypecastExpression;
3628
    }
3629
    ) {
3630
        super();
23✔
3631
        this.tokens = {
23✔
3632
            typecast: options.typecast
3633
        };
3634
        this.typecastExpression = options.typecastExpression;
23✔
3635
        this.location = util.createBoundingLocation(
23✔
3636
            this.tokens.typecast,
3637
            this.typecastExpression
3638
        );
3639
    }
3640

3641
    public readonly tokens: {
3642
        readonly typecast?: Token;
3643
    };
3644

3645
    public readonly typecastExpression: TypecastExpression;
3646

3647
    public readonly kind = AstNodeKind.TypecastStatement;
23✔
3648

3649
    public readonly location: Location;
3650

3651
    transpile(state: BrsTranspileState) {
3652
        //the typecast statement is a comment just for debugging purposes
3653
        return [
1✔
3654
            state.transpileToken(this.tokens.typecast, 'typecast', true),
3655
            ' ',
3656
            this.typecastExpression.obj.transpile(state),
3657
            ' ',
3658
            state.transpileToken(this.typecastExpression.tokens.as, 'as'),
3659
            ' ',
3660
            this.typecastExpression.typeExpression.transpile(state)
3661
        ];
3662
    }
3663

3664
    walk(visitor: WalkVisitor, options: WalkOptions) {
3665
        if (options.walkMode & InternalWalkMode.walkExpressions) {
135✔
3666
            walk(this, 'typecastExpression', visitor, options);
125✔
3667
        }
3668
    }
3669

3670
    get leadingTrivia(): Token[] {
3671
        return this.tokens.typecast?.leadingTrivia ?? [];
110!
3672
    }
3673

3674
    getType(options: GetTypeOptions): BscType {
3675
        return this.typecastExpression.getType(options);
19✔
3676
    }
3677
}
3678

3679
export class ConditionalCompileErrorStatement extends Statement {
1✔
3680
    constructor(options: {
3681
        hashError?: Token;
3682
        message: Token;
3683
    }) {
3684
        super();
9✔
3685
        this.tokens = {
9✔
3686
            hashError: options.hashError,
3687
            message: options.message
3688
        };
3689
        this.location = util.createBoundingLocation(util.createBoundingLocationFromTokens(this.tokens));
9✔
3690
    }
3691

3692
    public readonly tokens: {
3693
        readonly hashError?: Token;
3694
        readonly message: Token;
3695
    };
3696

3697

3698
    public readonly kind = AstNodeKind.ConditionalCompileErrorStatement;
9✔
3699

3700
    public readonly location: Location | undefined;
3701

3702
    transpile(state: BrsTranspileState) {
3703
        return [
3✔
3704
            state.transpileToken(this.tokens.hashError, '#error'),
3705
            ' ',
3706
            state.transpileToken(this.tokens.message)
3707

3708
        ];
3709
    }
3710

3711
    walk(visitor: WalkVisitor, options: WalkOptions) {
3712
        // nothing to walk
3713
    }
3714

3715
    get leadingTrivia(): Token[] {
3716
        return this.tokens.hashError.leadingTrivia ?? [];
12!
3717
    }
3718
}
3719

3720
export class AliasStatement extends Statement {
1✔
3721
    constructor(options: {
3722
        alias?: Token;
3723
        name: Token;
3724
        equals?: Token;
3725
        value: VariableExpression | DottedGetExpression;
3726
    }
3727
    ) {
3728
        super();
32✔
3729
        this.tokens = {
32✔
3730
            alias: options.alias,
3731
            name: options.name,
3732
            equals: options.equals
3733
        };
3734
        this.value = options.value;
32✔
3735
        this.location = util.createBoundingLocation(
32✔
3736
            this.tokens.alias,
3737
            this.tokens.name,
3738
            this.tokens.equals,
3739
            this.value
3740
        );
3741
    }
3742

3743
    public readonly tokens: {
3744
        readonly alias?: Token;
3745
        readonly name: Token;
3746
        readonly equals?: Token;
3747
    };
3748

3749
    public readonly value: Expression;
3750

3751
    public readonly kind = AstNodeKind.AliasStatement;
32✔
3752

3753
    public readonly location: Location;
3754

3755
    transpile(state: BrsTranspileState) {
3756
        //the alias statement is a comment just for debugging purposes
3757
        return [
12✔
3758
            state.transpileToken(this.tokens.alias, 'alias', true),
3759
            ' ',
3760
            state.transpileToken(this.tokens.name),
3761
            ' ',
3762
            state.transpileToken(this.tokens.equals, '='),
3763
            ' ',
3764
            this.value.transpile(state)
3765
        ];
3766
    }
3767

3768
    walk(visitor: WalkVisitor, options: WalkOptions) {
3769
        if (options.walkMode & InternalWalkMode.walkExpressions) {
221✔
3770
            walk(this, 'value', visitor, options);
192✔
3771
        }
3772
    }
3773

3774
    get leadingTrivia(): Token[] {
3775
        return this.tokens.alias?.leadingTrivia ?? [];
121!
3776
    }
3777

3778
    getType(options: GetTypeOptions): BscType {
3779
        return this.value.getType(options);
1✔
3780
    }
3781
}
3782

3783
export class ConditionalCompileStatement extends Statement {
1✔
3784
    constructor(options: {
3785
        hashIf?: Token;
3786
        not?: Token;
3787
        condition: Token;
3788
        hashElse?: Token;
3789
        hashEndIf?: Token;
3790
        thenBranch: Block;
3791
        elseBranch?: ConditionalCompileStatement | Block;
3792
    }) {
3793
        super();
54✔
3794
        this.thenBranch = options.thenBranch;
54✔
3795
        this.elseBranch = options.elseBranch;
54✔
3796

3797
        this.tokens = {
54✔
3798
            hashIf: options.hashIf,
3799
            not: options.not,
3800
            condition: options.condition,
3801
            hashElse: options.hashElse,
3802
            hashEndIf: options.hashEndIf
3803
        };
3804

3805
        this.location = util.createBoundingLocation(
54✔
3806
            util.createBoundingLocationFromTokens(this.tokens),
3807
            this.thenBranch,
3808
            this.elseBranch
3809
        );
3810
    }
3811

3812
    readonly tokens: {
3813
        readonly hashIf?: Token;
3814
        readonly not?: Token;
3815
        readonly condition: Token;
3816
        readonly hashElse?: Token;
3817
        readonly hashEndIf?: Token;
3818
    };
3819
    public readonly thenBranch: Block;
3820
    public readonly elseBranch?: ConditionalCompileStatement | Block;
3821

3822
    public readonly kind = AstNodeKind.ConditionalCompileStatement;
54✔
3823

3824
    public readonly location: Location | undefined;
3825

3826
    transpile(state: BrsTranspileState) {
3827
        let results = [] as TranspileResult;
6✔
3828
        //if   (already indented by block)
3829
        if (!state.conditionalCompileStatement) {
6✔
3830
            // only transpile the #if in the case when we're not in a conditionalCompileStatement already
3831
            results.push(state.transpileToken(this.tokens.hashIf ?? createToken(TokenKind.HashIf)));
3!
3832
        }
3833

3834
        results.push(' ');
6✔
3835
        //conditions
3836
        if (this.tokens.not) {
6✔
3837
            results.push('not');
2✔
3838
            results.push(' ');
2✔
3839
        }
3840
        results.push(state.transpileToken(this.tokens.condition));
6✔
3841
        state.lineage.unshift(this);
6✔
3842

3843
        //if statement body
3844
        let thenNodes = this.thenBranch.transpile(state);
6✔
3845
        state.lineage.shift();
6✔
3846
        if (thenNodes.length > 0) {
6!
3847
            results.push(thenNodes);
6✔
3848
        }
3849
        //else branch
3850
        if (this.elseBranch) {
6!
3851
            const elseIsCC = isConditionalCompileStatement(this.elseBranch);
6✔
3852
            const endBlockToken = elseIsCC ? (this.elseBranch as ConditionalCompileStatement).tokens.hashIf ?? createToken(TokenKind.HashElseIf) : this.tokens.hashElse;
6!
3853
            //else
3854

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

3857
            if (elseIsCC) {
6✔
3858
                //chained else if
3859
                state.lineage.unshift(this.elseBranch);
3✔
3860

3861
                // transpile following #if with knowledge of current
3862
                const existingCCStmt = state.conditionalCompileStatement;
3✔
3863
                state.conditionalCompileStatement = this;
3✔
3864
                let body = this.elseBranch.transpile(state);
3✔
3865
                state.conditionalCompileStatement = existingCCStmt;
3✔
3866

3867
                state.lineage.shift();
3✔
3868

3869
                if (body.length > 0) {
3!
3870
                    //zero or more spaces between the `else` and the `if`
3871
                    results.push(...body);
3✔
3872

3873
                    // stop here because chained if will transpile the rest
3874
                    return results;
3✔
3875
                } else {
NEW
3876
                    results.push('\n');
×
3877
                }
3878

3879
            } else {
3880
                //else body
3881
                state.lineage.unshift(this.tokens.hashElse!);
3✔
3882
                let body = this.elseBranch.transpile(state);
3✔
3883
                state.lineage.shift();
3✔
3884

3885
                if (body.length > 0) {
3!
3886
                    results.push(...body);
3✔
3887
                }
3888
            }
3889
        }
3890

3891
        //end if
3892
        results.push(...state.transpileEndBlockToken(this.elseBranch ?? this.thenBranch, this.tokens.hashEndIf, '#end if'));
3!
3893

3894
        return results;
3✔
3895
    }
3896

3897
    walk(visitor: WalkVisitor, options: WalkOptions) {
3898
        if (options.walkMode & InternalWalkMode.walkStatements) {
185!
3899
            const bsConsts = options.bsConsts ?? this.getBsConsts();
185!
3900
            let conditionTrue = bsConsts?.get(this.tokens.condition.text.toLowerCase());
185!
3901
            if (this.tokens.not) {
185✔
3902
                // flips the boolean value
3903
                conditionTrue = !conditionTrue;
23✔
3904
            }
3905
            const walkFalseBlocks = options.walkMode & InternalWalkMode.visitFalseConditionalCompilationBlocks;
185✔
3906
            if (conditionTrue || walkFalseBlocks) {
185✔
3907
                walk(this, 'thenBranch', visitor, options);
123✔
3908
            }
3909
            if (this.elseBranch && (!conditionTrue || walkFalseBlocks)) {
185✔
3910
                walk(this, 'elseBranch', visitor, options);
52✔
3911
            }
3912
        }
3913
    }
3914

3915
    get leadingTrivia(): Token[] {
3916
        return this.tokens.hashIf?.leadingTrivia ?? [];
164!
3917
    }
3918
}
3919

3920

3921
export class ConditionalCompileConstStatement extends Statement {
1✔
3922
    constructor(options: {
3923
        hashConst?: Token;
3924
        assignment: AssignmentStatement;
3925
    }) {
3926
        super();
17✔
3927
        this.tokens = {
17✔
3928
            hashConst: options.hashConst
3929
        };
3930
        this.assignment = options.assignment;
17✔
3931
        this.location = util.createBoundingLocation(util.createBoundingLocationFromTokens(this.tokens), this.assignment);
17✔
3932
    }
3933

3934
    public readonly tokens: {
3935
        readonly hashConst?: Token;
3936
    };
3937

3938
    public readonly assignment: AssignmentStatement;
3939

3940
    public readonly kind = AstNodeKind.ConditionalCompileConstStatement;
17✔
3941

3942
    public readonly location: Location | undefined;
3943

3944
    transpile(state: BrsTranspileState) {
3945
        return [
3✔
3946
            state.transpileToken(this.tokens.hashConst, '#const'),
3947
            ' ',
3948
            state.transpileToken(this.assignment.tokens.name),
3949
            ' ',
3950
            state.transpileToken(this.assignment.tokens.equals, '='),
3951
            ' ',
3952
            ...this.assignment.value.transpile(state)
3953
        ];
3954

3955
    }
3956

3957
    walk(visitor: WalkVisitor, options: WalkOptions) {
3958
        // nothing to walk
3959
    }
3960

3961

3962
    get leadingTrivia(): Token[] {
3963
        return this.tokens.hashConst.leadingTrivia ?? [];
78!
3964
    }
3965
}
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