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

rokucommunity / brighterscript / #12852

25 Jul 2024 01:32PM UTC coverage: 86.202%. Remained the same
#12852

push

web-flow
Merge 242a1aefa into 5f3ffa3fa

10593 of 13078 branches covered (81.0%)

Branch coverage included in aggregate %.

91 of 97 new or added lines in 15 files covered. (93.81%)

309 existing lines in 18 files now uncovered.

12292 of 13470 relevant lines covered (91.25%)

26623.24 hits per line

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

90.74
/src/parser/Parser.ts
1
import type { Token, Identifier } from '../lexer/Token';
2
import { isToken } from '../lexer/Token';
1✔
3
import type { BlockTerminator } from '../lexer/TokenKind';
4
import { Lexer } from '../lexer/Lexer';
1✔
5
import {
1✔
6
    AllowedLocalIdentifiers,
7
    AllowedTypeIdentifiers,
8
    DeclarableTypes,
9
    AllowedProperties,
10
    AssignmentOperators,
11
    BrighterScriptSourceLiterals,
12
    DisallowedFunctionIdentifiersText,
13
    DisallowedLocalIdentifiersText,
14
    TokenKind,
15
    BlockTerminators,
16
    ReservedWords,
17
    CompoundAssignmentOperators
18
} from '../lexer/TokenKind';
19
import type {
20
    PrintSeparatorSpace,
21
    PrintSeparatorTab
22
} from './Statement';
23
import {
1✔
24
    AliasStatement,
25
    AssignmentStatement,
26
    Block,
27
    Body,
28
    CatchStatement,
29
    ContinueStatement,
30
    ClassStatement,
31
    ConstStatement,
32
    ConditionalCompileStatement,
33
    DimStatement,
34
    DottedSetStatement,
35
    EndStatement,
36
    EnumMemberStatement,
37
    EnumStatement,
38
    ExitForStatement,
39
    ExitWhileStatement,
40
    ExpressionStatement,
41
    ForEachStatement,
42
    FieldStatement,
43
    ForStatement,
44
    FunctionStatement,
45
    GotoStatement,
46
    IfStatement,
47
    ImportStatement,
48
    IncrementStatement,
49
    IndexedSetStatement,
50
    InterfaceFieldStatement,
51
    InterfaceMethodStatement,
52
    InterfaceStatement,
53
    LabelStatement,
54
    LibraryStatement,
55
    MethodStatement,
56
    NamespaceStatement,
57
    PrintStatement,
58
    ReturnStatement,
59
    StopStatement,
60
    ThrowStatement,
61
    TryCatchStatement,
62
    WhileStatement,
63
    TypecastStatement,
64
    ConditionalCompileConstStatement,
65
    ConditionalCompileErrorStatement,
66
    AugmentedAssignmentStatement
67
} from './Statement';
68
import type { DiagnosticInfo } from '../DiagnosticMessages';
69
import { DiagnosticMessages } from '../DiagnosticMessages';
1✔
70
import { util } from '../util';
1✔
71
import {
1✔
72
    AALiteralExpression,
73
    AAMemberExpression,
74
    AnnotationExpression,
75
    ArrayLiteralExpression,
76
    BinaryExpression,
77
    CallExpression,
78
    CallfuncExpression,
79
    DottedGetExpression,
80
    EscapedCharCodeLiteralExpression,
81
    FunctionExpression,
82
    FunctionParameterExpression,
83
    GroupingExpression,
84
    IndexedGetExpression,
85
    LiteralExpression,
86
    NewExpression,
87
    NullCoalescingExpression,
88
    RegexLiteralExpression,
89
    SourceLiteralExpression,
90
    TaggedTemplateStringExpression,
91
    TemplateStringExpression,
92
    TemplateStringQuasiExpression,
93
    TernaryExpression,
94
    TypecastExpression,
95
    TypeExpression,
96
    TypedArrayExpression,
97
    UnaryExpression,
98
    VariableExpression,
99
    XmlAttributeGetExpression
100
} from './Expression';
101
import type { Range } from 'vscode-languageserver';
102
import type { Logger } from '../logging';
103
import { createLogger } from '../logging';
1✔
104
import { isAnnotationExpression, isCallExpression, isCallfuncExpression, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression, isConditionalCompileStatement, isLiteralBoolean, isTypecastExpression } from '../astUtils/reflection';
1✔
105
import { createStringLiteral, createToken } from '../astUtils/creators';
1✔
106
import type { Expression, Statement } from './AstNode';
107
import type { BsDiagnostic, DeepWriteable } from '../interfaces';
108

109
export class Parser {
1✔
110
    /**
111
     * The array of tokens passed to `parse()`
112
     */
113
    public tokens = [] as Token[];
2,977✔
114

115
    /**
116
     * The current token index
117
     */
118
    public current: number;
119

120
    /**
121
     * The list of statements for the parsed file
122
     */
123
    public ast = new Body({ statements: [] });
2,977✔
124

125
    public get eofToken(): Token {
126
        const lastToken = this.tokens?.[this.tokens.length - 1];
590!
127
        if (lastToken?.kind === TokenKind.Eof) {
590!
128
            return lastToken;
590✔
129
        }
130
    }
131

132
    /**
133
     * The top-level symbol table for the body of this file.
134
     */
135
    public get symbolTable() {
136
        return this.ast.symbolTable;
10,208✔
137
    }
138

139
    /**
140
     * The list of diagnostics found during the parse process
141
     */
142
    public diagnostics: BsDiagnostic[];
143

144
    /**
145
     * The depth of the calls to function declarations. Helps some checks know if they are at the root or not.
146
     */
147
    private namespaceAndFunctionDepth: number;
148

149
    /**
150
     * The options used to parse the file
151
     */
152
    public options: ParseOptions;
153

154
    private globalTerminators = [] as TokenKind[][];
2,977✔
155

156
    /**
157
     * A list of identifiers that are permitted to be used as local variables. We store this in a property because we augment the list in the constructor
158
     * based on the parse mode
159
     */
160
    private allowedLocalIdentifiers: TokenKind[];
161

162
    /**
163
     * Annotations collected which should be attached to the next statement
164
     */
165
    private pendingAnnotations: AnnotationExpression[];
166

167
    /**
168
     * Get the currently active global terminators
169
     */
170
    private peekGlobalTerminators() {
171
        return this.globalTerminators[this.globalTerminators.length - 1] ?? [];
14,883✔
172
    }
173

174
    /**
175
     * Static wrapper around creating a new parser and parsing a list of tokens
176
     */
177
    public static parse(toParse: Token[] | string, options?: ParseOptions): Parser {
178
        return new Parser().parse(toParse, options);
2,960✔
179
    }
180

181
    /**
182
     * Parses an array of `Token`s into an abstract syntax tree
183
     * @param toParse the array of tokens to parse. May not contain any whitespace tokens
184
     * @returns the same instance of the parser which contains the diagnostics and statements
185
     */
186
    public parse(toParse: Token[] | string, options?: ParseOptions) {
187
        this.logger = options?.logger ?? createLogger();
2,961✔
188
        options = this.sanitizeParseOptions(options);
2,961✔
189
        this.options = options;
2,961✔
190

191
        let tokens: Token[];
192
        if (typeof toParse === 'string') {
2,961✔
193
            tokens = Lexer.scan(toParse, {
158✔
194
                trackLocations: options.trackLocations,
195
                srcPath: options?.srcPath
474!
196
            }).tokens;
197
        } else {
198
            tokens = toParse;
2,803✔
199
        }
200
        this.tokens = tokens;
2,961✔
201
        this.allowedLocalIdentifiers = [
2,961✔
202
            ...AllowedLocalIdentifiers,
203
            //when in plain brightscript mode, the BrighterScript source literals can be used as regular variables
204
            ...(this.options.mode === ParseMode.BrightScript ? BrighterScriptSourceLiterals : [])
2,961✔
205
        ];
206
        this.current = 0;
2,961✔
207
        this.diagnostics = [];
2,961✔
208
        this.namespaceAndFunctionDepth = 0;
2,961✔
209
        this.pendingAnnotations = [];
2,961✔
210

211
        this.ast = this.body();
2,961✔
212
        this.ast.bsConsts = options.bsConsts;
2,961✔
213
        //now that we've built the AST, link every node to its parent
214
        this.ast.link();
2,961✔
215
        return this;
2,961✔
216
    }
217

218
    private logger: Logger;
219

220
    private body() {
221
        const parentAnnotations = this.enterAnnotationBlock();
3,536✔
222

223
        let body = new Body({ statements: [] });
3,536✔
224
        if (this.tokens.length > 0) {
3,536✔
225
            this.consumeStatementSeparators(true);
3,535✔
226

227
            try {
3,535✔
228
                while (
3,535✔
229
                    //not at end of tokens
230
                    !this.isAtEnd() &&
14,634✔
231
                    //the next token is not one of the end terminators
232
                    !this.checkAny(...this.peekGlobalTerminators())
233
                ) {
234
                    let dec = this.declaration();
5,263✔
235
                    if (dec) {
5,263✔
236
                        if (!isAnnotationExpression(dec)) {
5,230✔
237
                            this.consumePendingAnnotations(dec);
5,203✔
238
                            body.statements.push(dec);
5,203✔
239
                            //ensure statement separator
240
                            this.consumeStatementSeparators(false);
5,203✔
241
                        } else {
242
                            this.consumeStatementSeparators(true);
27✔
243
                        }
244
                    }
245
                }
246
            } catch (parseError) {
247
                //do nothing with the parse error for now. perhaps we can remove this?
248
                console.error(parseError);
×
249
            }
250
        }
251

252
        this.exitAnnotationBlock(parentAnnotations);
3,536✔
253
        return body;
3,536✔
254
    }
255

256
    private sanitizeParseOptions(options: ParseOptions) {
257
        options ??= {
2,961✔
258
            srcPath: undefined
259
        };
260
        options.mode ??= ParseMode.BrightScript;
2,961✔
261
        options.trackLocations ??= true;
2,961✔
262
        return options;
2,961✔
263
    }
264

265
    /**
266
     * Determine if the parser is currently parsing tokens at the root level.
267
     */
268
    private isAtRootLevel() {
269
        return this.namespaceAndFunctionDepth === 0;
36,247✔
270
    }
271

272
    /**
273
     * Throws an error if the input file type is not BrighterScript
274
     */
275
    private warnIfNotBrighterScriptMode(featureName: string) {
276
        if (this.options.mode !== ParseMode.BrighterScript) {
2,355✔
277
            let diagnostic = {
119✔
278
                ...DiagnosticMessages.bsFeatureNotSupportedInBrsFiles(featureName),
279
                location: this.peek().location
280
            };
281
            this.diagnostics.push(diagnostic);
119✔
282
        }
283
    }
284

285
    /**
286
     * Throws an exception using the last diagnostic message
287
     */
288
    private lastDiagnosticAsError() {
289
        let error = new Error(this.diagnostics[this.diagnostics.length - 1]?.message ?? 'Unknown error');
94!
290
        (error as any).isDiagnostic = true;
94✔
291
        return error;
94✔
292
    }
293

294
    private declaration(): Statement | AnnotationExpression | undefined {
295
        try {
12,045✔
296
            if (this.checkAny(TokenKind.HashConst)) {
12,045✔
297
                return this.conditionalCompileConstStatement();
20✔
298
            }
299
            if (this.checkAny(TokenKind.HashIf)) {
12,025✔
300
                return this.conditionalCompileStatement();
35✔
301
            }
302
            if (this.checkAny(TokenKind.HashError)) {
11,990✔
303
                return this.conditionalCompileErrorStatement();
9✔
304
            }
305

306
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
11,981✔
307
                return this.functionDeclaration(false);
2,793✔
308
            }
309

310
            if (this.checkLibrary()) {
9,188✔
311
                return this.libraryStatement();
12✔
312
            }
313

314
            if (this.checkAlias()) {
9,176✔
315
                return this.aliasStatement();
32✔
316
            }
317

318
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
9,144✔
319
                return this.constDeclaration();
146✔
320
            }
321

322
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
8,998✔
323
                return this.annotationExpression();
31✔
324
            }
325

326
            //catch certain global terminators to prevent unnecessary lookahead (i.e. like `end namespace`, no need to continue)
327
            if (this.checkAny(...this.peekGlobalTerminators())) {
8,967!
328
                return;
×
329
            }
330

331
            return this.statement();
8,967✔
332
        } catch (error: any) {
333
            //if the error is not a diagnostic, then log the error for debugging purposes
334
            if (!error.isDiagnostic) {
82!
335
                this.logger.error(error);
×
336
            }
337
            this.synchronize();
82✔
338
        }
339
    }
340

341
    /**
342
     * Try to get an identifier. If not found, add diagnostic and return undefined
343
     */
344
    private tryIdentifier(...additionalTokenKinds: TokenKind[]): Identifier | undefined {
345
        const identifier = this.tryConsume(
157✔
346
            DiagnosticMessages.expectedIdentifier(),
347
            TokenKind.Identifier,
348
            ...additionalTokenKinds
349
        ) as Identifier;
350
        if (identifier) {
157✔
351
            // force the name into an identifier so the AST makes some sense
352
            identifier.kind = TokenKind.Identifier;
156✔
353
            return identifier;
156✔
354
        }
355
    }
356

357
    private identifier(...additionalTokenKinds: TokenKind[]) {
358
        const identifier = this.consume(
740✔
359
            DiagnosticMessages.expectedIdentifier(),
360
            TokenKind.Identifier,
361
            ...additionalTokenKinds
362
        ) as Identifier;
363
        // force the name into an identifier so the AST makes some sense
364
        identifier.kind = TokenKind.Identifier;
740✔
365
        return identifier;
740✔
366
    }
367

368
    private enumMemberStatement() {
369
        const name = this.consume(
311✔
370
            DiagnosticMessages.expectedClassFieldIdentifier(),
371
            TokenKind.Identifier,
372
            ...AllowedProperties
373
        ) as Identifier;
374
        let equalsToken: Token;
375
        let value: Expression;
376
        //look for `= SOME_EXPRESSION`
377
        if (this.check(TokenKind.Equal)) {
311✔
378
            equalsToken = this.advance();
171✔
379
            value = this.expression();
171✔
380
        }
381
        const statement = new EnumMemberStatement({ name: name, equals: equalsToken, value: value });
311✔
382
        return statement;
311✔
383
    }
384

385
    /**
386
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration`
387
     */
388
    private interfaceFieldStatement(optionalKeyword?: Token) {
389
        const name = this.identifier(...AllowedProperties);
179✔
390
        let asToken;
391
        let typeExpression;
392
        if (this.check(TokenKind.As)) {
179✔
393
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
161✔
394
        }
395
        return new InterfaceFieldStatement({ name: name, as: asToken, typeExpression: typeExpression, optional: optionalKeyword });
179✔
396
    }
397

398
    private consumeAsTokenAndTypeExpression(ignoreDiagnostics = false): [Token, TypeExpression] {
1,300✔
399
        let asToken = this.consumeToken(TokenKind.As);
1,309✔
400
        let typeExpression: TypeExpression;
401
        if (asToken) {
1,309!
402
            //if there's nothing after the `as`, add a diagnostic and continue
403
            if (this.checkEndOfStatement()) {
1,309✔
404
                if (!ignoreDiagnostics) {
2✔
405
                    this.diagnostics.push({
1✔
406
                        ...DiagnosticMessages.expectedIdentifierAfterKeyword(asToken.text),
407
                        location: asToken.location
408
                    });
409
                }
410
                //consume the statement separator
411
                this.consumeStatementSeparators();
2✔
412
            } else if (this.peek().kind !== TokenKind.Identifier && !this.checkAny(...DeclarableTypes, ...AllowedTypeIdentifiers)) {
1,307✔
413
                if (!ignoreDiagnostics) {
6!
414
                    this.diagnostics.push({
6✔
415
                        ...DiagnosticMessages.expectedIdentifierAfterKeyword(asToken.text),
416
                        location: asToken.location
417
                    });
418
                }
419
            } else {
420
                typeExpression = this.typeExpression();
1,301✔
421
            }
422
        }
423
        return [asToken, typeExpression];
1,309✔
424
    }
425

426
    /**
427
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration()`
428
     */
429
    private interfaceMethodStatement(optionalKeyword?: Token) {
430
        const functionType = this.advance();
38✔
431
        const name = this.identifier(...AllowedProperties);
38✔
432
        const leftParen = this.consume(DiagnosticMessages.expectedToken(TokenKind.LeftParen), TokenKind.LeftParen);
38✔
433

434
        let params = [] as FunctionParameterExpression[];
38✔
435
        if (!this.check(TokenKind.RightParen)) {
38✔
436
            do {
7✔
437
                if (params.length >= CallExpression.MaximumArguments) {
8!
438
                    this.diagnostics.push({
×
439
                        ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments),
440
                        location: this.peek().location
441
                    });
442
                }
443

444
                params.push(this.functionParameter());
8✔
445
            } while (this.match(TokenKind.Comma));
446
        }
447
        const rightParen = this.consumeToken(TokenKind.RightParen);
38✔
448
        // let asToken = null as Token;
449
        // let returnTypeExpression: TypeExpression;
450
        let asToken: Token;
451
        let returnTypeExpression: TypeExpression;
452
        if (this.check(TokenKind.As)) {
38✔
453
            [asToken, returnTypeExpression] = this.consumeAsTokenAndTypeExpression();
27✔
454
        }
455

456
        return new InterfaceMethodStatement({
38✔
457
            functionType: functionType,
458
            name: name,
459
            leftParen: leftParen,
460
            params: params,
461
            rightParen: rightParen,
462
            as: asToken,
463
            returnTypeExpression: returnTypeExpression,
464
            optional: optionalKeyword
465
        });
466
    }
467

468
    private interfaceDeclaration(): InterfaceStatement {
469
        this.warnIfNotBrighterScriptMode('interface declarations');
145✔
470

471
        const parentAnnotations = this.enterAnnotationBlock();
145✔
472

473
        const interfaceToken = this.consume(
145✔
474
            DiagnosticMessages.expectedKeyword(TokenKind.Interface),
475
            TokenKind.Interface
476
        );
477
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
145✔
478

479
        let extendsToken: Token;
480
        let parentInterfaceName: TypeExpression;
481

482
        if (this.peek().text.toLowerCase() === 'extends') {
145✔
483
            extendsToken = this.advance();
7✔
484
            if (this.checkEndOfStatement()) {
7!
485
                this.diagnostics.push({
×
486
                    ...DiagnosticMessages.expectedIdentifierAfterKeyword(extendsToken.text),
487
                    location: extendsToken.location
488
                });
489
            } else {
490
                parentInterfaceName = this.typeExpression();
7✔
491
            }
492
        }
493
        this.consumeStatementSeparators();
145✔
494
        //gather up all interface members (Fields, Methods)
495
        let body = [] as Statement[];
145✔
496
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
145✔
497
            try {
364✔
498
                //break out of this loop if we encountered the `EndInterface` token not followed by `as`
499
                if (this.check(TokenKind.EndInterface) && !this.checkNext(TokenKind.As)) {
364✔
500
                    break;
145✔
501
                }
502

503
                let decl: Statement;
504

505
                //collect leading annotations
506
                if (this.check(TokenKind.At)) {
219✔
507
                    this.annotationExpression();
2✔
508
                }
509
                const optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
219✔
510
                //fields
511
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkAnyNext(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
219✔
512
                    decl = this.interfaceFieldStatement(optionalKeyword);
177✔
513
                    //field with name = 'optional'
514
                } else if (optionalKeyword && this.checkAny(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
42✔
515
                    //rewind one place, so that 'optional' is the field name
516
                    this.current--;
2✔
517
                    decl = this.interfaceFieldStatement();
2✔
518

519
                    //methods (function/sub keyword followed by opening paren)
520
                } else if (this.checkAny(TokenKind.Function, TokenKind.Sub) && this.checkAnyNext(TokenKind.Identifier, ...AllowedProperties)) {
40✔
521
                    decl = this.interfaceMethodStatement(optionalKeyword);
38✔
522

523
                }
524
                if (decl) {
219✔
525
                    this.consumePendingAnnotations(decl);
217✔
526
                    body.push(decl);
217✔
527
                } else {
528
                    //we didn't find a declaration...flag tokens until next line
529
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
530
                }
531
            } catch (e) {
532
                //throw out any failed members and move on to the next line
533
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
534
            }
535

536
            //ensure statement separator
537
            this.consumeStatementSeparators();
219✔
538
        }
539

540
        //consume the final `end interface` token
541
        const endInterfaceToken = this.consumeToken(TokenKind.EndInterface);
145✔
542

543
        const statement = new InterfaceStatement({
145✔
544
            interface: interfaceToken,
545
            name: nameToken,
546
            extends: extendsToken,
547
            parentInterfaceName: parentInterfaceName,
548
            body: body,
549
            endInterface: endInterfaceToken
550
        });
551
        this.exitAnnotationBlock(parentAnnotations);
145✔
552
        return statement;
145✔
553
    }
554

555
    private enumDeclaration(): EnumStatement {
556
        const enumToken = this.consume(
157✔
557
            DiagnosticMessages.expectedKeyword(TokenKind.Enum),
558
            TokenKind.Enum
559
        );
560
        const nameToken = this.tryIdentifier(...this.allowedLocalIdentifiers);
157✔
561

562
        this.warnIfNotBrighterScriptMode('enum declarations');
157✔
563

564
        const parentAnnotations = this.enterAnnotationBlock();
157✔
565

566
        this.consumeStatementSeparators();
157✔
567

568
        const body: Array<EnumMemberStatement> = [];
157✔
569
        //gather up all members
570
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
157✔
571
            try {
311✔
572
                let decl: EnumMemberStatement;
573

574
                //collect leading annotations
575
                if (this.check(TokenKind.At)) {
311!
576
                    this.annotationExpression();
×
577
                }
578

579
                //members
580
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
311!
581
                    decl = this.enumMemberStatement();
311✔
582
                }
583

584
                if (decl) {
311!
585
                    this.consumePendingAnnotations(decl);
311✔
586
                    body.push(decl);
311✔
587
                } else {
588
                    //we didn't find a declaration...flag tokens until next line
589
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
590
                }
591
            } catch (e) {
592
                //throw out any failed members and move on to the next line
593
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
594
            }
595

596
            //ensure statement separator
597
            this.consumeStatementSeparators();
311✔
598
            //break out of this loop if we encountered the `EndEnum` token
599
            if (this.check(TokenKind.EndEnum)) {
311✔
600
                break;
150✔
601
            }
602
        }
603

604
        //consume the final `end interface` token
605
        const endEnumToken = this.consumeToken(TokenKind.EndEnum);
157✔
606

607
        const result = new EnumStatement({
156✔
608
            enum: enumToken,
609
            name: nameToken,
610
            body: body,
611
            endEnum: endEnumToken
612
        });
613

614
        this.exitAnnotationBlock(parentAnnotations);
156✔
615
        return result;
156✔
616
    }
617

618
    /**
619
     * A BrighterScript class declaration
620
     */
621
    private classDeclaration(): ClassStatement {
622
        this.warnIfNotBrighterScriptMode('class declarations');
654✔
623

624
        const parentAnnotations = this.enterAnnotationBlock();
654✔
625

626
        let classKeyword = this.consume(
654✔
627
            DiagnosticMessages.expectedKeyword(TokenKind.Class),
628
            TokenKind.Class
629
        );
630
        let extendsKeyword: Token;
631
        let parentClassName: TypeExpression;
632

633
        //get the class name
634
        let className = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('class'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
654✔
635

636
        //see if the class inherits from parent
637
        if (this.peek().text.toLowerCase() === 'extends') {
654✔
638
            extendsKeyword = this.advance();
98✔
639
            if (this.checkEndOfStatement()) {
98✔
640
                this.diagnostics.push({
1✔
641
                    ...DiagnosticMessages.expectedIdentifierAfterKeyword(extendsKeyword.text),
642
                    location: extendsKeyword.location
643
                });
644
            } else {
645
                parentClassName = this.typeExpression();
97✔
646
            }
647
        }
648

649
        //ensure statement separator
650
        this.consumeStatementSeparators();
654✔
651

652
        //gather up all class members (Fields, Methods)
653
        let body = [] as Statement[];
654✔
654
        while (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private, TokenKind.Function, TokenKind.Sub, TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
654✔
655
            try {
672✔
656
                let decl: Statement;
657
                let accessModifier: Token;
658

659
                if (this.check(TokenKind.At)) {
672✔
660
                    this.annotationExpression();
15✔
661
                }
662

663
                if (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private)) {
671✔
664
                    //use actual access modifier
665
                    accessModifier = this.advance();
93✔
666
                }
667

668
                let overrideKeyword: Token;
669
                if (this.peek().text.toLowerCase() === 'override') {
671✔
670
                    overrideKeyword = this.advance();
17✔
671
                }
672
                //methods (function/sub keyword OR identifier followed by opening paren)
673
                if (this.checkAny(TokenKind.Function, TokenKind.Sub) || (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.LeftParen))) {
671✔
674
                    const funcDeclaration = this.functionDeclaration(false, false);
346✔
675

676
                    //if we have an overrides keyword AND this method is called 'new', that's not allowed
677
                    if (overrideKeyword && funcDeclaration.tokens.name.text.toLowerCase() === 'new') {
346!
678
                        this.diagnostics.push({
×
679
                            ...DiagnosticMessages.cannotUseOverrideKeywordOnConstructorFunction(),
680
                            location: overrideKeyword.location
681
                        });
682
                    }
683

684
                    decl = new MethodStatement({
346✔
685
                        modifiers: accessModifier,
686
                        name: funcDeclaration.tokens.name,
687
                        func: funcDeclaration.func,
688
                        override: overrideKeyword
689
                    });
690

691
                    //fields
692
                } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
325✔
693

694
                    decl = this.fieldDeclaration(accessModifier);
311✔
695

696
                    //class fields cannot be overridden
697
                    if (overrideKeyword) {
310!
698
                        this.diagnostics.push({
×
699
                            ...DiagnosticMessages.classFieldCannotBeOverridden(),
700
                            location: overrideKeyword.location
701
                        });
702
                    }
703

704
                }
705

706
                if (decl) {
670✔
707
                    this.consumePendingAnnotations(decl);
656✔
708
                    body.push(decl);
656✔
709
                }
710
            } catch (e) {
711
                //throw out any failed members and move on to the next line
712
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
713
            }
714

715
            //ensure statement separator
716
            this.consumeStatementSeparators();
672✔
717
        }
718

719
        let endingKeyword = this.advance();
654✔
720
        if (endingKeyword.kind !== TokenKind.EndClass) {
654✔
721
            this.diagnostics.push({
4✔
722
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('class'),
723
                location: endingKeyword.location
724
            });
725
        }
726

727
        const result = new ClassStatement({
654✔
728
            class: classKeyword,
729
            name: className,
730
            body: body,
731
            endClass: endingKeyword,
732
            extends: extendsKeyword,
733
            parentClassName: parentClassName
734
        });
735

736
        this.exitAnnotationBlock(parentAnnotations);
654✔
737
        return result;
654✔
738
    }
739

740
    private fieldDeclaration(accessModifier: Token | null) {
741

742
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
311✔
743

744
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
311✔
745
            if (this.check(TokenKind.As)) {
310✔
746
                if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
5✔
747
                    // as <EOL>
748
                    // `as` is the field name
749
                } else if (this.checkNext(TokenKind.As)) {
4✔
750
                    //  as as ____
751
                    // first `as` is the field name
752
                } else if (optionalKeyword) {
2!
753
                    // optional as ____
754
                    // optional is the field name, `as` starts type
755
                    // rewind current token
756
                    optionalKeyword = null;
2✔
757
                    this.current--;
2✔
758
                }
759
            }
760
        } else {
761
            // no name after `optional` ... optional is the name
762
            // rewind current token
763
            optionalKeyword = null;
1✔
764
            this.current--;
1✔
765
        }
766

767
        let name = this.consume(
311✔
768
            DiagnosticMessages.expectedClassFieldIdentifier(),
769
            TokenKind.Identifier,
770
            ...AllowedProperties
771
        ) as Identifier;
772

773
        let asToken: Token;
774
        let fieldTypeExpression: TypeExpression;
775
        //look for `as SOME_TYPE`
776
        if (this.check(TokenKind.As)) {
311✔
777
            [asToken, fieldTypeExpression] = this.consumeAsTokenAndTypeExpression();
206✔
778
        }
779

780
        let initialValue: Expression;
781
        let equal: Token;
782
        //if there is a field initializer
783
        if (this.check(TokenKind.Equal)) {
311✔
784
            equal = this.advance();
77✔
785
            initialValue = this.expression();
77✔
786
        }
787

788
        return new FieldStatement({
310✔
789
            accessModifier: accessModifier,
790
            name: name,
791
            as: asToken,
792
            typeExpression: fieldTypeExpression,
793
            equals: equal,
794
            initialValue: initialValue,
795
            optional: optionalKeyword
796
        });
797
    }
798

799
    /**
800
     * An array of CallExpression for the current function body
801
     */
802
    private callExpressions = [];
2,977✔
803

804
    private functionDeclaration(isAnonymous: true, checkIdentifier?: boolean, onlyCallableAsMember?: boolean): FunctionExpression;
805
    private functionDeclaration(isAnonymous: false, checkIdentifier?: boolean, onlyCallableAsMember?: boolean): FunctionStatement;
806
    private functionDeclaration(isAnonymous: boolean, checkIdentifier = true, onlyCallableAsMember = false) {
6,088✔
807
        let previousCallExpressions = this.callExpressions;
3,217✔
808
        this.callExpressions = [];
3,217✔
809
        try {
3,217✔
810
            //track depth to help certain statements need to know if they are contained within a function body
811
            this.namespaceAndFunctionDepth++;
3,217✔
812
            let functionType: Token;
813
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
3,217✔
814
                functionType = this.advance();
3,215✔
815
            } else {
816
                this.diagnostics.push({
2✔
817
                    ...DiagnosticMessages.missingCallableKeyword(),
818
                    location: this.peek().location
819
                });
820
                //TODO we should probably eliminate this entirely, since it's not present in the source code
821
                functionType = {
2✔
822
                    isReserved: true,
823
                    kind: TokenKind.Function,
824
                    text: 'function',
825
                    //zero-length location means derived
826
                    location: this.peek().location,
827
                    leadingWhitespace: '',
828
                    leadingTrivia: []
829
                };
830
            }
831
            let isSub = functionType?.kind === TokenKind.Sub;
3,217!
832
            let functionTypeText = isSub ? 'sub' : 'function';
3,217✔
833
            let name: Identifier;
834
            let leftParen: Token;
835

836
            if (isAnonymous) {
3,217✔
837
                leftParen = this.consume(
78✔
838
                    DiagnosticMessages.expectedLeftParenAfterCallable(functionTypeText),
839
                    TokenKind.LeftParen
840
                );
841
            } else {
842
                name = this.consume(
3,139✔
843
                    DiagnosticMessages.expectedNameAfterCallableKeyword(functionTypeText),
844
                    TokenKind.Identifier,
845
                    ...AllowedProperties
846
                ) as Identifier;
847
                leftParen = this.consume(
3,137✔
848
                    DiagnosticMessages.expectedLeftParenAfterCallableName(functionTypeText),
849
                    TokenKind.LeftParen
850
                );
851

852
                //prevent functions from ending with type designators
853
                let lastChar = name.text[name.text.length - 1];
3,136✔
854
                if (['$', '%', '!', '#', '&'].includes(lastChar)) {
3,136✔
855
                    //don't throw this error; let the parser continue
856
                    this.diagnostics.push({
8✔
857
                        ...DiagnosticMessages.functionNameCannotEndWithTypeDesignator(functionTypeText, name.text, lastChar),
858
                        location: name.location
859
                    });
860
                }
861

862
                //flag functions with keywords for names (only for standard functions)
863
                if (checkIdentifier && DisallowedFunctionIdentifiersText.has(name.text.toLowerCase())) {
3,136✔
864
                    this.diagnostics.push({
1✔
865
                        ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
866
                        location: name.location
867
                    });
868
                }
869
            }
870

871
            let params = [] as FunctionParameterExpression[];
3,214✔
872
            let asToken: Token;
873
            let typeExpression: TypeExpression;
874
            if (!this.check(TokenKind.RightParen)) {
3,214✔
875
                do {
1,488✔
876
                    params.push(this.functionParameter());
2,645✔
877
                } while (this.match(TokenKind.Comma));
878
            }
879
            let rightParen = this.advance();
3,214✔
880

881
            if (this.check(TokenKind.As)) {
3,214✔
882
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
255✔
883
            }
884

885
            params.reduce((haveFoundOptional: boolean, param: FunctionParameterExpression) => {
3,214✔
886
                if (haveFoundOptional && !param.defaultValue) {
2,645!
887
                    this.diagnostics.push({
×
888
                        ...DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter(param.tokens.name.text),
889
                        location: param.location
890
                    });
891
                }
892

893
                return haveFoundOptional || !!param.defaultValue;
2,645✔
894
            }, false);
895

896
            this.consumeStatementSeparators(true);
3,214✔
897

898

899
            //support ending the function with `end sub` OR `end function`
900
            let body = this.block();
3,214✔
901
            //if the parser was unable to produce a block, make an empty one so the AST makes some sense...
902

903
            // consume 'end sub' or 'end function'
904
            const endFunctionType = this.advance();
3,214✔
905
            let expectedEndKind = isSub ? TokenKind.EndSub : TokenKind.EndFunction;
3,214✔
906

907
            //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
908
            //add an error but don't hard-fail so the AST can continue more gracefully
909
            if (endFunctionType.kind !== expectedEndKind) {
3,214✔
910
                this.diagnostics.push({
9✔
911
                    ...DiagnosticMessages.mismatchedEndCallableKeyword(functionTypeText, endFunctionType.text),
912
                    location: endFunctionType.location
913
                });
914
            }
915

916
            if (!body) {
3,214✔
917
                body = new Block({ statements: [] });
3✔
918
            }
919

920
            let func = new FunctionExpression({
3,214✔
921
                parameters: params,
922
                body: body,
923
                functionType: functionType,
924
                endFunctionType: endFunctionType,
925
                leftParen: leftParen,
926
                rightParen: rightParen,
927
                as: asToken,
928
                returnTypeExpression: typeExpression
929
            });
930

931
            if (isAnonymous) {
3,214✔
932
                return func;
78✔
933
            } else {
934
                let result = new FunctionStatement({ name: name, func: func });
3,136✔
935
                return result;
3,136✔
936
            }
937
        } finally {
938
            this.namespaceAndFunctionDepth--;
3,217✔
939
            //restore the previous CallExpression list
940
            this.callExpressions = previousCallExpressions;
3,217✔
941
        }
942
    }
943

944
    private functionParameter(): FunctionParameterExpression {
945
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
2,653!
946
            this.diagnostics.push({
×
947
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
948
                location: this.peek().location
949
            });
950
            throw this.lastDiagnosticAsError();
×
951
        }
952

953
        let name = this.advance() as Identifier;
2,653✔
954
        // force the name into an identifier so the AST makes some sense
955
        name.kind = TokenKind.Identifier;
2,653✔
956

957
        let typeExpression: TypeExpression;
958
        let defaultValue;
959
        let equalToken: Token;
960
        // parse argument default value
961
        if ((equalToken = this.consumeTokenIf(TokenKind.Equal))) {
2,653✔
962
            // it seems any expression is allowed here -- including ones that operate on other arguments!
963
            defaultValue = this.expression(false);
353✔
964
        }
965

966
        let asToken: Token = null;
2,653✔
967
        if (this.check(TokenKind.As)) {
2,653✔
968
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
579✔
969

970
        }
971
        return new FunctionParameterExpression({
2,653✔
972
            name: name,
973
            equals: equalToken,
974
            defaultValue: defaultValue,
975
            as: asToken,
976
            typeExpression: typeExpression
977
        });
978
    }
979

980
    private assignment(allowTypedAssignment = false): AssignmentStatement {
1,362✔
981
        let name = this.advance() as Identifier;
1,370✔
982
        //add diagnostic if name is a reserved word that cannot be used as an identifier
983
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
1,370✔
984
            this.diagnostics.push({
12✔
985
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
986
                location: name.location
987
            });
988
        }
989
        let asToken: Token;
990
        let typeExpression: TypeExpression;
991

992
        if (allowTypedAssignment) {
1,370✔
993
            //look for `as SOME_TYPE`
994
            if (this.check(TokenKind.As)) {
8!
995
                this.warnIfNotBrighterScriptMode('typed assignment');
8✔
996

997
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
8✔
998
            }
999
        }
1000

1001
        let operator = this.consume(
1,370✔
1002
            DiagnosticMessages.expectedOperatorAfterIdentifier([TokenKind.Equal], name.text),
1003
            ...[TokenKind.Equal]
1004
        );
1005
        let value = this.expression();
1,367✔
1006

1007
        let result = new AssignmentStatement({ equals: operator, name: name, value: value, as: asToken, typeExpression: typeExpression });
1,360✔
1008

1009
        return result;
1,360✔
1010
    }
1011

1012
    private augmentedAssignment(): AugmentedAssignmentStatement {
1013
        let item = this.expression();
58✔
1014

1015
        let operator = this.consume(
58✔
1016
            DiagnosticMessages.expectedToken(...CompoundAssignmentOperators),
1017
            ...CompoundAssignmentOperators
1018
        );
1019
        let value = this.expression();
58✔
1020

1021
        let result = new AugmentedAssignmentStatement({
58✔
1022
            item: item,
1023
            operator: operator,
1024
            value: value
1025
        });
1026

1027
        return result;
58✔
1028
    }
1029

1030
    private checkLibrary() {
1031
        let isLibraryToken = this.check(TokenKind.Library);
18,241✔
1032

1033
        //if we are at the top level, any line that starts with "library" should be considered a library statement
1034
        if (this.isAtRootLevel() && isLibraryToken) {
18,241✔
1035
            return true;
11✔
1036

1037
            //not at root level, library statements are all invalid here, but try to detect if the tokens look
1038
            //like a library statement (and let the libraryStatement function handle emitting the diagnostics)
1039
        } else if (isLibraryToken && this.checkNext(TokenKind.StringLiteral)) {
18,230✔
1040
            return true;
1✔
1041

1042
            //definitely not a library statement
1043
        } else {
1044
            return false;
18,229✔
1045
        }
1046
    }
1047

1048
    private checkAlias() {
1049
        let isAliasToken = this.check(TokenKind.Alias);
18,006✔
1050

1051
        //if we are at the top level, any line that starts with "alias" should be considered a alias statement
1052
        if (this.isAtRootLevel() && isAliasToken) {
18,006✔
1053
            return true;
30✔
1054

1055
            //not at root level, alias statements are all invalid here, but try to detect if the tokens look
1056
            //like a alias statement (and let the alias function handle emitting the diagnostics)
1057
        } else if (isAliasToken && this.checkNext(TokenKind.Identifier)) {
17,976✔
1058
            return true;
2✔
1059

1060
            //definitely not a alias statement
1061
        } else {
1062
            return false;
17,974✔
1063
        }
1064
    }
1065

1066
    private statement(): Statement | undefined {
1067
        if (this.checkLibrary()) {
9,053!
1068
            return this.libraryStatement();
×
1069
        }
1070

1071
        if (this.check(TokenKind.Import)) {
9,053✔
1072
            return this.importStatement();
200✔
1073
        }
1074

1075
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
8,853✔
1076
            return this.typecastStatement();
23✔
1077
        }
1078

1079
        if (this.checkAlias()) {
8,830!
1080
            return this.aliasStatement();
×
1081
        }
1082

1083
        if (this.check(TokenKind.Stop)) {
8,830✔
1084
            return this.stopStatement();
15✔
1085
        }
1086

1087
        if (this.check(TokenKind.If)) {
8,815✔
1088
            return this.ifStatement();
969✔
1089
        }
1090

1091
        //`try` must be followed by a block, otherwise it could be a local variable
1092
        if (this.check(TokenKind.Try) && this.checkAnyNext(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
7,846✔
1093
            return this.tryCatchStatement();
26✔
1094
        }
1095

1096
        if (this.check(TokenKind.Throw)) {
7,820✔
1097
            return this.throwStatement();
10✔
1098
        }
1099

1100
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
7,810✔
1101
            return this.printStatement();
1,055✔
1102
        }
1103
        if (this.check(TokenKind.Dim)) {
6,755✔
1104
            return this.dimStatement();
40✔
1105
        }
1106

1107
        if (this.check(TokenKind.While)) {
6,715✔
1108
            return this.whileStatement();
22✔
1109
        }
1110

1111
        if (this.check(TokenKind.ExitWhile)) {
6,693✔
1112
            return this.exitWhile();
6✔
1113
        }
1114

1115
        if (this.check(TokenKind.For)) {
6,687✔
1116
            return this.forStatement();
30✔
1117
        }
1118

1119
        if (this.check(TokenKind.ForEach)) {
6,657✔
1120
            return this.forEachStatement();
32✔
1121
        }
1122

1123
        if (this.check(TokenKind.ExitFor)) {
6,625✔
1124
            return this.exitFor();
3✔
1125
        }
1126

1127
        if (this.check(TokenKind.End)) {
6,622✔
1128
            return this.endStatement();
7✔
1129
        }
1130

1131
        if (this.match(TokenKind.Return)) {
6,615✔
1132
            return this.returnStatement();
2,769✔
1133
        }
1134

1135
        if (this.check(TokenKind.Goto)) {
3,846✔
1136
            return this.gotoStatement();
11✔
1137
        }
1138

1139
        //the continue keyword (followed by `for`, `while`, or a statement separator)
1140
        if (this.check(TokenKind.Continue) && this.checkAnyNext(TokenKind.While, TokenKind.For, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
3,835✔
1141
            return this.continueStatement();
11✔
1142
        }
1143

1144
        //does this line look like a label? (i.e.  `someIdentifier:` )
1145
        if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
3,824✔
1146
            try {
11✔
1147
                return this.labelStatement();
11✔
1148
            } catch (err) {
1149
                if (!(err instanceof CancelStatementError)) {
2!
1150
                    throw err;
×
1151
                }
1152
                //not a label, try something else
1153
            }
1154
        }
1155

1156
        // BrightScript is like python, in that variables can be declared without a `var`,
1157
        // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
1158
        // out what to do with it.
1159
        if (
3,815✔
1160
            this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)
1161
        ) {
1162
            if (this.checkAnyNext(...AssignmentOperators)) {
3,595✔
1163
                if (this.checkAnyNext(...CompoundAssignmentOperators)) {
1,371✔
1164
                    return this.augmentedAssignment();
58✔
1165
                }
1166
                return this.assignment();
1,313✔
1167
            } else if (this.checkNext(TokenKind.As)) {
2,224✔
1168
                // may be a typed assignment
1169
                const backtrack = this.current;
9✔
1170
                let validTypeExpression = false;
9✔
1171

1172
                try {
9✔
1173
                    // skip the identifier, and check for valid type expression
1174
                    this.advance();
9✔
1175
                    const parts = this.consumeAsTokenAndTypeExpression(true);
9✔
1176
                    validTypeExpression = !!(parts?.[0] && parts?.[1]);
9!
1177
                } catch (e) {
1178
                    // ignore any errors
1179
                } finally {
1180
                    this.current = backtrack;
9✔
1181
                }
1182
                if (validTypeExpression) {
9✔
1183
                    // there is a valid 'as' and type expression
1184
                    return this.assignment(true);
8✔
1185
                }
1186
            }
1187
        }
1188

1189
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1190
        if (this.check(TokenKind.Interface)) {
2,436✔
1191
            return this.interfaceDeclaration();
145✔
1192
        }
1193

1194
        if (this.check(TokenKind.Class)) {
2,291✔
1195
            return this.classDeclaration();
654✔
1196
        }
1197

1198
        if (this.check(TokenKind.Namespace)) {
1,637✔
1199
            return this.namespaceStatement();
576✔
1200
        }
1201

1202
        if (this.check(TokenKind.Enum)) {
1,061✔
1203
            return this.enumDeclaration();
157✔
1204
        }
1205

1206
        // TODO: support multi-statements
1207
        return this.setStatement();
904✔
1208
    }
1209

1210
    private whileStatement(): WhileStatement {
1211
        const whileKeyword = this.advance();
22✔
1212
        const condition = this.expression();
22✔
1213

1214
        this.consumeStatementSeparators();
21✔
1215

1216
        const whileBlock = this.block(TokenKind.EndWhile);
21✔
1217
        let endWhile: Token;
1218
        if (!whileBlock || this.peek().kind !== TokenKind.EndWhile) {
21✔
1219
            this.diagnostics.push({
1✔
1220
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('while'),
1221
                location: this.peek().location
1222
            });
1223
            if (!whileBlock) {
1!
1224
                throw this.lastDiagnosticAsError();
×
1225
            }
1226
        } else {
1227
            endWhile = this.advance();
20✔
1228
        }
1229

1230
        return new WhileStatement({
21✔
1231
            while: whileKeyword,
1232
            endWhile: endWhile,
1233
            condition: condition,
1234
            body: whileBlock
1235
        });
1236
    }
1237

1238
    private exitWhile(): ExitWhileStatement {
1239
        let keyword = this.advance();
6✔
1240

1241
        return new ExitWhileStatement({ exitWhile: keyword });
6✔
1242
    }
1243

1244
    private forStatement(): ForStatement {
1245
        const forToken = this.advance();
30✔
1246
        const initializer = this.assignment();
30✔
1247

1248
        //TODO: newline allowed?
1249

1250
        const toToken = this.advance();
29✔
1251
        const finalValue = this.expression();
29✔
1252
        let incrementExpression: Expression | undefined;
1253
        let stepToken: Token | undefined;
1254

1255
        if (this.check(TokenKind.Step)) {
29✔
1256
            stepToken = this.advance();
8✔
1257
            incrementExpression = this.expression();
8✔
1258
        } else {
1259
            // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
1260
        }
1261

1262
        this.consumeStatementSeparators();
29✔
1263

1264
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
29✔
1265
        let endForToken: Token;
1266
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
29✔
1267
            this.diagnostics.push({
1✔
1268
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1269
                location: this.peek().location
1270
            });
1271
            if (!body) {
1!
1272
                throw this.lastDiagnosticAsError();
×
1273
            }
1274
        } else {
1275
            endForToken = this.advance();
28✔
1276
        }
1277

1278
        // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
1279
        // stays around in scope with whatever value it was when the loop exited.
1280
        return new ForStatement({
29✔
1281
            for: forToken,
1282
            counterDeclaration: initializer,
1283
            to: toToken,
1284
            finalValue: finalValue,
1285
            body: body,
1286
            endFor: endForToken,
1287
            step: stepToken,
1288
            increment: incrementExpression
1289
        });
1290
    }
1291

1292
    private forEachStatement(): ForEachStatement {
1293
        let forEach = this.advance();
32✔
1294
        let name = this.advance();
32✔
1295

1296
        let maybeIn = this.peek();
32✔
1297
        if (this.check(TokenKind.Identifier) && maybeIn.text.toLowerCase() === 'in') {
32!
1298
            this.advance();
32✔
1299
        } else {
1300
            this.diagnostics.push({
×
1301
                ...DiagnosticMessages.expectedInAfterForEach(name.text),
1302
                location: this.peek().location
1303
            });
1304
            throw this.lastDiagnosticAsError();
×
1305
        }
1306
        maybeIn.kind = TokenKind.In;
32✔
1307

1308
        let target = this.expression();
32✔
1309
        if (!target) {
32!
1310
            this.diagnostics.push({
×
1311
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1312
                location: this.peek().location
1313
            });
1314
            throw this.lastDiagnosticAsError();
×
1315
        }
1316

1317
        this.consumeStatementSeparators();
32✔
1318

1319
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
32✔
1320
        if (!body) {
32!
1321
            this.diagnostics.push({
×
1322
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1323
                location: this.peek().location
1324
            });
1325
            throw this.lastDiagnosticAsError();
×
1326
        }
1327

1328
        let endFor = this.advance();
32✔
1329

1330
        return new ForEachStatement({
32✔
1331
            forEach: forEach,
1332
            in: maybeIn,
1333
            endFor: endFor,
1334
            item: name,
1335
            target: target,
1336
            body: body
1337
        });
1338
    }
1339

1340
    private exitFor(): ExitForStatement {
1341
        let keyword = this.advance();
3✔
1342

1343
        return new ExitForStatement({ exitFor: keyword });
3✔
1344
    }
1345

1346
    private namespaceStatement(): NamespaceStatement | undefined {
1347
        this.warnIfNotBrighterScriptMode('namespace');
576✔
1348
        let keyword = this.advance();
576✔
1349

1350
        this.namespaceAndFunctionDepth++;
576✔
1351

1352
        let name = this.identifyingExpression();
576✔
1353
        //set the current namespace name
1354

1355
        this.globalTerminators.push([TokenKind.EndNamespace]);
575✔
1356
        let body = this.body();
575✔
1357
        this.globalTerminators.pop();
575✔
1358

1359
        let endKeyword: Token;
1360
        if (this.check(TokenKind.EndNamespace)) {
575✔
1361
            endKeyword = this.advance();
573✔
1362
        } else {
1363
            //the `end namespace` keyword is missing. add a diagnostic, but keep parsing
1364
            this.diagnostics.push({
2✔
1365
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'),
1366
                location: keyword.location
1367
            });
1368
        }
1369

1370
        this.namespaceAndFunctionDepth--;
575✔
1371

1372
        let result = new NamespaceStatement({
575✔
1373
            namespace: keyword,
1374
            nameExpression: name,
1375
            body: body,
1376
            endNamespace: endKeyword
1377
        });
1378

1379
        //cache the range property so that plugins can't affect it
1380
        result.cacheLocation();
575✔
1381
        result.body.symbolTable.name += `: namespace '${result.name}'`;
575✔
1382
        return result;
575✔
1383
    }
1384

1385
    /**
1386
     * Get an expression with identifiers separated by periods. Useful for namespaces and class extends
1387
     */
1388
    private identifyingExpression(allowedTokenKinds?: TokenKind[]): DottedGetExpression | VariableExpression {
1389
        allowedTokenKinds = allowedTokenKinds ?? this.allowedLocalIdentifiers;
1,209✔
1390
        let firstIdentifier = this.consume(
1,209✔
1391
            DiagnosticMessages.expectedIdentifierAfterKeyword(this.previous().text),
1392
            TokenKind.Identifier,
1393
            ...allowedTokenKinds
1394
        ) as Identifier;
1395

1396
        let expr: DottedGetExpression | VariableExpression;
1397

1398
        if (firstIdentifier) {
1,208!
1399
            // force it into an identifier so the AST makes some sense
1400
            firstIdentifier.kind = TokenKind.Identifier;
1,208✔
1401
            const varExpr = new VariableExpression({ name: firstIdentifier });
1,208✔
1402
            expr = varExpr;
1,208✔
1403

1404
            //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
1405
            while (this.check(TokenKind.Dot)) {
1,208✔
1406
                let dot = this.tryConsume(
446✔
1407
                    DiagnosticMessages.unexpectedToken(this.peek().text),
1408
                    TokenKind.Dot
1409
                );
1410
                if (!dot) {
446!
1411
                    break;
×
1412
                }
1413
                let identifier = this.tryConsume(
446✔
1414
                    DiagnosticMessages.expectedIdentifier(),
1415
                    TokenKind.Identifier,
1416
                    ...allowedTokenKinds,
1417
                    ...AllowedProperties
1418
                ) as Identifier;
1419

1420
                if (!identifier) {
446✔
1421
                    break;
3✔
1422
                }
1423
                // force it into an identifier so the AST makes some sense
1424
                identifier.kind = TokenKind.Identifier;
443✔
1425
                expr = new DottedGetExpression({ obj: expr, name: identifier, dot: dot });
443✔
1426
            }
1427
        }
1428
        return expr;
1,208✔
1429
    }
1430
    /**
1431
     * Add an 'unexpected token' diagnostic for any token found between current and the first stopToken found.
1432
     */
1433
    private flagUntil(...stopTokens: TokenKind[]) {
1434
        while (!this.checkAny(...stopTokens) && !this.isAtEnd()) {
4!
1435
            let token = this.advance();
×
1436
            this.diagnostics.push({
×
1437
                ...DiagnosticMessages.unexpectedToken(token.text),
1438
                location: token.location
1439
            });
1440
        }
1441
    }
1442

1443
    /**
1444
     * Consume tokens until one of the `stopTokenKinds` is encountered
1445
     * @param stopTokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
1446
     * @returns - the list of tokens consumed, EXCLUDING the `stopTokenKind` (you can use `this.peek()` to see which one it was)
1447
     */
1448
    private consumeUntil(...stopTokenKinds: TokenKind[]) {
1449
        let result = [] as Token[];
59✔
1450
        //take tokens until we encounter one of the stopTokenKinds
1451
        while (!stopTokenKinds.includes(this.peek().kind)) {
59✔
1452
            result.push(this.advance());
140✔
1453
        }
1454
        return result;
59✔
1455
    }
1456

1457
    private constDeclaration(): ConstStatement | undefined {
1458
        this.warnIfNotBrighterScriptMode('const declaration');
146✔
1459
        const constToken = this.advance();
146✔
1460
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
146✔
1461
        const equalToken = this.consumeToken(TokenKind.Equal);
146✔
1462
        const expression = this.expression();
146✔
1463
        const statement = new ConstStatement({
146✔
1464
            const: constToken,
1465
            name: nameToken,
1466
            equals: equalToken,
1467
            value: expression
1468
        });
1469
        return statement;
146✔
1470
    }
1471

1472
    private libraryStatement(): LibraryStatement | undefined {
1473
        let libStatement = new LibraryStatement({
12✔
1474
            library: this.advance(),
1475
            //grab the next token only if it's a string
1476
            filePath: this.tryConsume(
1477
                DiagnosticMessages.expectedStringLiteralAfterKeyword('library'),
1478
                TokenKind.StringLiteral
1479
            )
1480
        });
1481

1482
        return libStatement;
12✔
1483
    }
1484

1485
    private importStatement() {
1486
        this.warnIfNotBrighterScriptMode('import statements');
200✔
1487
        let importStatement = new ImportStatement({
200✔
1488
            import: this.advance(),
1489
            //grab the next token only if it's a string
1490
            path: this.tryConsume(
1491
                DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
1492
                TokenKind.StringLiteral
1493
            )
1494
        });
1495

1496
        return importStatement;
200✔
1497
    }
1498

1499
    private typecastStatement() {
1500
        this.warnIfNotBrighterScriptMode('typecast statements');
23✔
1501
        const typecastToken = this.advance();
23✔
1502
        const typecastExpr = this.expression();
23✔
1503
        if (isTypecastExpression(typecastExpr)) {
23!
1504
            return new TypecastStatement({
23✔
1505
                typecast: typecastToken,
1506
                typecastExpression: typecastExpr
1507
            });
1508
        }
1509
        this.diagnostics.push({
×
1510
            ...DiagnosticMessages.expectedIdentifierAfterKeyword('typecast'),
1511
            location: {
1512
                uri: typecastToken.location.uri,
1513
                range: util.createBoundingRange(typecastToken, this.peek())
1514
            }
1515
        });
UNCOV
1516
        throw this.lastDiagnosticAsError();
×
1517
    }
1518

1519
    private aliasStatement(): AliasStatement | undefined {
1520
        this.warnIfNotBrighterScriptMode('alias statements');
32✔
1521
        const aliasToken = this.advance();
32✔
1522
        const name = this.tryConsume(
32✔
1523
            DiagnosticMessages.expectedIdentifierAfterKeyword('alias'),
1524
            TokenKind.Identifier
1525
        );
1526
        const equals = this.tryConsume(
32✔
1527
            DiagnosticMessages.expectedToken(TokenKind.Equal),
1528
            TokenKind.Equal
1529
        );
1530
        let value = this.identifyingExpression();
32✔
1531

1532
        let aliasStmt = new AliasStatement({
32✔
1533
            alias: aliasToken,
1534
            name: name,
1535
            equals: equals,
1536
            value: value
1537

1538
        });
1539

1540
        return aliasStmt;
32✔
1541
    }
1542

1543
    private annotationExpression() {
1544
        const atToken = this.advance();
48✔
1545
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
48✔
1546
        if (identifier) {
48✔
1547
            identifier.kind = TokenKind.Identifier;
47✔
1548
        }
1549
        let annotation = new AnnotationExpression({ at: atToken, name: identifier });
48✔
1550
        this.pendingAnnotations.push(annotation);
47✔
1551

1552
        //optional arguments
1553
        if (this.check(TokenKind.LeftParen)) {
47✔
1554
            let leftParen = this.advance();
9✔
1555
            annotation.call = this.finishCall(leftParen, annotation, false);
9✔
1556
        }
1557
        return annotation;
47✔
1558
    }
1559

1560
    private ternaryExpression(test?: Expression): TernaryExpression {
1561
        this.warnIfNotBrighterScriptMode('ternary operator');
76✔
1562
        if (!test) {
76!
UNCOV
1563
            test = this.expression();
×
1564
        }
1565
        const questionMarkToken = this.advance();
76✔
1566

1567
        //consume newlines or comments
1568
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
76✔
1569
            this.advance();
7✔
1570
        }
1571

1572
        let consequent: Expression;
1573
        try {
76✔
1574
            consequent = this.expression();
76✔
1575
        } catch { }
1576

1577
        //consume newlines or comments
1578
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
76✔
1579
            this.advance();
5✔
1580
        }
1581

1582
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
76✔
1583

1584
        //consume newlines
1585
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
76✔
1586
            this.advance();
11✔
1587
        }
1588
        let alternate: Expression;
1589
        try {
76✔
1590
            alternate = this.expression();
76✔
1591
        } catch { }
1592

1593
        return new TernaryExpression({
76✔
1594
            test: test,
1595
            questionMark: questionMarkToken,
1596
            consequent: consequent,
1597
            colon: colonToken,
1598
            alternate: alternate
1599
        });
1600
    }
1601

1602
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1603
        this.warnIfNotBrighterScriptMode('null coalescing operator');
32✔
1604
        const questionQuestionToken = this.advance();
32✔
1605
        const alternate = this.expression();
32✔
1606
        return new NullCoalescingExpression({
32✔
1607
            consequent: test,
1608
            questionQuestion: questionQuestionToken,
1609
            alternate: alternate
1610
        });
1611
    }
1612

1613
    private regexLiteralExpression() {
1614
        this.warnIfNotBrighterScriptMode('regular expression literal');
44✔
1615
        return new RegexLiteralExpression({
44✔
1616
            regexLiteral: this.advance()
1617
        });
1618
    }
1619

1620
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1621
        this.warnIfNotBrighterScriptMode('template string');
39✔
1622

1623
        //get the tag name
1624
        let tagName: Identifier;
1625
        if (isTagged) {
39✔
1626
            tagName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties) as Identifier;
5✔
1627
            // force it into an identifier so the AST makes some sense
1628
            tagName.kind = TokenKind.Identifier;
5✔
1629
        }
1630

1631
        let quasis = [] as TemplateStringQuasiExpression[];
39✔
1632
        let expressions = [];
39✔
1633
        let openingBacktick = this.peek();
39✔
1634
        this.advance();
39✔
1635
        let currentQuasiExpressionParts = [];
39✔
1636
        while (!this.isAtEnd() && !this.check(TokenKind.BackTick)) {
39✔
1637
            let next = this.peek();
150✔
1638
            if (next.kind === TokenKind.TemplateStringQuasi) {
150✔
1639
                //a quasi can actually be made up of multiple quasis when it includes char literals
1640
                currentQuasiExpressionParts.push(
94✔
1641
                    new LiteralExpression({ value: next })
1642
                );
1643
                this.advance();
94✔
1644
            } else if (next.kind === TokenKind.EscapedCharCodeLiteral) {
56✔
1645
                currentQuasiExpressionParts.push(
24✔
1646
                    new EscapedCharCodeLiteralExpression({ value: next as Token & { charCode: number } })
1647
                );
1648
                this.advance();
24✔
1649
            } else {
1650
                //finish up the current quasi
1651
                quasis.push(
32✔
1652
                    new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1653
                );
1654
                currentQuasiExpressionParts = [];
32✔
1655

1656
                if (next.kind === TokenKind.TemplateStringExpressionBegin) {
32!
1657
                    this.advance();
32✔
1658
                }
1659
                //now keep this expression
1660
                expressions.push(this.expression());
32✔
1661
                if (!this.isAtEnd() && this.check(TokenKind.TemplateStringExpressionEnd)) {
32!
1662
                    //TODO is it an error if this is not present?
1663
                    this.advance();
32✔
1664
                } else {
UNCOV
1665
                    this.diagnostics.push({
×
1666
                        ...DiagnosticMessages.unterminatedTemplateExpression(),
1667
                        location: {
1668
                            uri: openingBacktick.location.uri,
1669
                            range: util.createBoundingRange(openingBacktick, this.peek())
1670
                        }
1671
                    });
UNCOV
1672
                    throw this.lastDiagnosticAsError();
×
1673
                }
1674
            }
1675
        }
1676

1677
        //store the final set of quasis
1678
        quasis.push(
39✔
1679
            new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1680
        );
1681

1682
        if (this.isAtEnd()) {
39✔
1683
            //error - missing backtick
1684
            this.diagnostics.push({
2✔
1685
                ...DiagnosticMessages.unterminatedTemplateStringAtEndOfFile(),
1686
                location: {
1687
                    uri: openingBacktick.location.uri,
1688
                    range: util.createBoundingRange(openingBacktick, this.peek())
1689
                }
1690
            });
1691
            throw this.lastDiagnosticAsError();
2✔
1692

1693
        } else {
1694
            let closingBacktick = this.advance();
37✔
1695
            if (isTagged) {
37✔
1696
                return new TaggedTemplateStringExpression({
5✔
1697
                    tagName: tagName,
1698
                    openingBacktick: openingBacktick,
1699
                    quasis: quasis,
1700
                    expressions: expressions,
1701
                    closingBacktick: closingBacktick
1702
                });
1703
            } else {
1704
                return new TemplateStringExpression({
32✔
1705
                    openingBacktick: openingBacktick,
1706
                    quasis: quasis,
1707
                    expressions: expressions,
1708
                    closingBacktick: closingBacktick
1709
                });
1710
            }
1711
        }
1712
    }
1713

1714
    private tryCatchStatement(): TryCatchStatement {
1715
        const tryToken = this.advance();
26✔
1716
        let endTryToken: Token;
1717
        let catchStmt: CatchStatement;
1718
        //ensure statement separator
1719
        this.consumeStatementSeparators();
26✔
1720

1721
        let tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
26✔
1722

1723
        const peek = this.peek();
26✔
1724
        if (peek.kind !== TokenKind.Catch) {
26✔
1725
            this.diagnostics.push({
2✔
1726
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1727
                location: this.peek()?.location
6!
1728
            });
1729
        } else {
1730
            const catchToken = this.advance();
24✔
1731
            const exceptionVarToken = this.tryConsume(DiagnosticMessages.missingExceptionVarToFollowCatch(), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
24✔
1732
            if (exceptionVarToken) {
24✔
1733
                // force it into an identifier so the AST makes some sense
1734
                exceptionVarToken.kind = TokenKind.Identifier;
22✔
1735
            }
1736
            //ensure statement sepatator
1737
            this.consumeStatementSeparators();
24✔
1738
            const catchBranch = this.block(TokenKind.EndTry);
24✔
1739
            catchStmt = new CatchStatement({
24✔
1740
                catch: catchToken,
1741
                exceptionVariable: exceptionVarToken,
1742
                catchBranch: catchBranch
1743
            });
1744
        }
1745
        if (this.peek().kind !== TokenKind.EndTry) {
26✔
1746
            this.diagnostics.push({
2✔
1747
                ...DiagnosticMessages.expectedEndTryToTerminateTryCatch(),
1748
                location: this.peek().location
1749
            });
1750
        } else {
1751
            endTryToken = this.advance();
24✔
1752
        }
1753

1754
        const statement = new TryCatchStatement({
26✔
1755
            try: tryToken,
1756
            tryBranch: tryBranch,
1757
            catchStatement: catchStmt,
1758
            endTry: endTryToken
1759
        }
1760
        );
1761
        return statement;
26✔
1762
    }
1763

1764
    private throwStatement() {
1765
        const throwToken = this.advance();
10✔
1766
        let expression: Expression;
1767
        if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
10✔
1768
            this.diagnostics.push({
2✔
1769
                ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(),
1770
                location: throwToken.location
1771
            });
1772
        } else {
1773
            expression = this.expression();
8✔
1774
        }
1775
        return new ThrowStatement({ throw: throwToken, expression: expression });
8✔
1776
    }
1777

1778
    private dimStatement() {
1779
        const dim = this.advance();
40✔
1780

1781
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
40✔
1782
        // force to an identifier so the AST makes some sense
1783
        if (identifier) {
40✔
1784
            identifier.kind = TokenKind.Identifier;
38✔
1785
        }
1786

1787
        let leftSquareBracket = this.tryConsume(DiagnosticMessages.missingLeftSquareBracketAfterDimIdentifier(), TokenKind.LeftSquareBracket);
40✔
1788

1789
        let expressions: Expression[] = [];
40✔
1790
        let expression: Expression;
1791
        do {
40✔
1792
            try {
76✔
1793
                expression = this.expression();
76✔
1794
                expressions.push(expression);
71✔
1795
                if (this.check(TokenKind.Comma)) {
71✔
1796
                    this.advance();
36✔
1797
                } else {
1798
                    // will also exit for right square braces
1799
                    break;
35✔
1800
                }
1801
            } catch (error) {
1802
            }
1803
        } while (expression);
1804

1805
        if (expressions.length === 0) {
40✔
1806
            this.diagnostics.push({
5✔
1807
                ...DiagnosticMessages.missingExpressionsInDimStatement(),
1808
                location: this.peek().location
1809
            });
1810
        }
1811
        let rightSquareBracket = this.tryConsume(DiagnosticMessages.missingRightSquareBracketAfterDimIdentifier(), TokenKind.RightSquareBracket);
40✔
1812
        return new DimStatement({
40✔
1813
            dim: dim,
1814
            name: identifier,
1815
            openingSquare: leftSquareBracket,
1816
            dimensions: expressions,
1817
            closingSquare: rightSquareBracket
1818
        });
1819
    }
1820

1821
    private nestedInlineConditionalCount = 0;
2,977✔
1822

1823
    private ifStatement(incrementNestedCount = true): IfStatement {
1,809✔
1824
        // colon before `if` is usually not allowed, unless it's after `then`
1825
        if (this.current > 0) {
1,819✔
1826
            const prev = this.previous();
1,814✔
1827
            if (prev.kind === TokenKind.Colon) {
1,814✔
1828
                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then && this.nestedInlineConditionalCount === 0) {
4✔
1829
                    this.diagnostics.push({
1✔
1830
                        ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
1831
                        location: prev.location
1832
                    });
1833
                }
1834
            }
1835
        }
1836

1837
        const ifToken = this.advance();
1,819✔
1838

1839
        const condition = this.expression();
1,819✔
1840
        let thenBranch: Block;
1841
        let elseBranch: IfStatement | Block | undefined;
1842

1843
        let thenToken: Token | undefined;
1844
        let endIfToken: Token | undefined;
1845
        let elseToken: Token | undefined;
1846

1847
        //optional `then`
1848
        if (this.check(TokenKind.Then)) {
1,817✔
1849
            thenToken = this.advance();
1,447✔
1850
        }
1851

1852
        //is it inline or multi-line if?
1853
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
1,817✔
1854

1855
        if (isInlineIfThen) {
1,817✔
1856
            /*** PARSE INLINE IF STATEMENT ***/
1857
            if (!incrementNestedCount) {
48✔
1858
                this.nestedInlineConditionalCount++;
5✔
1859
            }
1860

1861
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
48✔
1862

1863
            if (!thenBranch) {
48!
UNCOV
1864
                this.diagnostics.push({
×
1865
                    ...DiagnosticMessages.expectedStatementToFollowConditionalCondition(ifToken.text),
1866
                    location: this.peek().location
1867
                });
UNCOV
1868
                throw this.lastDiagnosticAsError();
×
1869
            } else {
1870
                this.ensureInline(thenBranch.statements);
48✔
1871
            }
1872

1873
            //else branch
1874
            if (this.check(TokenKind.Else)) {
48✔
1875
                elseToken = this.advance();
33✔
1876

1877
                if (this.check(TokenKind.If)) {
33✔
1878
                    // recurse-read `else if`
1879
                    elseBranch = this.ifStatement(false);
10✔
1880

1881
                    //no multi-line if chained with an inline if
1882
                    if (!elseBranch.isInline) {
9✔
1883
                        this.diagnostics.push({
4✔
1884
                            ...DiagnosticMessages.expectedInlineIfStatement(),
1885
                            location: elseBranch.location
1886
                        });
1887
                    }
1888

1889
                } else if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
23✔
1890
                    //expecting inline else branch
1891
                    this.diagnostics.push({
3✔
1892
                        ...DiagnosticMessages.expectedInlineIfStatement(),
1893
                        location: this.peek().location
1894
                    });
1895
                    throw this.lastDiagnosticAsError();
3✔
1896
                } else {
1897
                    elseBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
20✔
1898

1899
                    if (elseBranch) {
20!
1900
                        this.ensureInline(elseBranch.statements);
20✔
1901
                    }
1902
                }
1903

1904
                if (!elseBranch) {
29!
1905
                    //missing `else` branch
UNCOV
1906
                    this.diagnostics.push({
×
1907
                        ...DiagnosticMessages.expectedStatementToFollowElse(),
1908
                        location: this.peek().location
1909
                    });
UNCOV
1910
                    throw this.lastDiagnosticAsError();
×
1911
                }
1912
            }
1913

1914
            if (!elseBranch || !isIfStatement(elseBranch)) {
44✔
1915
                //enforce newline at the end of the inline if statement
1916
                const peek = this.peek();
35✔
1917
                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && peek.kind !== TokenKind.Else && !this.isAtEnd()) {
35✔
1918
                    //ignore last error if it was about a colon
1919
                    if (this.previous().kind === TokenKind.Colon) {
3!
1920
                        this.diagnostics.pop();
3✔
1921
                        this.current--;
3✔
1922
                    }
1923
                    //newline is required
1924
                    this.diagnostics.push({
3✔
1925
                        ...DiagnosticMessages.expectedFinalNewline(),
1926
                        location: this.peek().location
1927
                    });
1928
                }
1929
            }
1930
            this.nestedInlineConditionalCount--;
44✔
1931
        } else {
1932
            /*** PARSE MULTI-LINE IF STATEMENT ***/
1933

1934
            thenBranch = this.blockConditionalBranch(ifToken);
1,769✔
1935

1936
            //ensure newline/colon before next keyword
1937
            this.ensureNewLineOrColon();
1,766✔
1938

1939
            //else branch
1940
            if (this.check(TokenKind.Else)) {
1,766✔
1941
                elseToken = this.advance();
1,413✔
1942

1943
                if (this.check(TokenKind.If)) {
1,413✔
1944
                    // recurse-read `else if`
1945
                    elseBranch = this.ifStatement();
840✔
1946

1947
                } else {
1948
                    elseBranch = this.blockConditionalBranch(ifToken);
573✔
1949

1950
                    //ensure newline/colon before next keyword
1951
                    this.ensureNewLineOrColon();
573✔
1952
                }
1953
            }
1954

1955
            if (!isIfStatement(elseBranch)) {
1,766✔
1956
                if (this.check(TokenKind.EndIf)) {
926✔
1957
                    endIfToken = this.advance();
923✔
1958

1959
                } else {
1960
                    //missing endif
1961
                    this.diagnostics.push({
3✔
1962
                        ...DiagnosticMessages.expectedEndIfToCloseIfStatement(ifToken.location?.range.start),
9!
1963
                        location: ifToken.location
1964
                    });
1965
                }
1966
            }
1967
        }
1968

1969
        return new IfStatement({
1,810✔
1970
            if: ifToken,
1971
            then: thenToken,
1972
            endIf: endIfToken,
1973
            else: elseToken,
1974
            condition: condition,
1975
            thenBranch: thenBranch,
1976
            elseBranch: elseBranch
1977
        });
1978
    }
1979

1980
    //consume a `then` or `else` branch block of an `if` statement
1981
    private blockConditionalBranch(ifToken: Token) {
1982
        //keep track of the current error count, because if the then branch fails,
1983
        //we will trash them in favor of a single error on if
1984
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
2,342✔
1985

1986
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
1987
        // a trailing "end if" or "else if"
1988
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
2,342✔
1989

1990
        if (!branch) {
2,342✔
1991
            //throw out any new diagnostics created as a result of a `then` block parse failure.
1992
            //the block() function will discard the current line, so any discarded diagnostics will
1993
            //resurface if they are legitimate, and not a result of a malformed if statement
1994
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
3✔
1995

1996
            //this whole if statement is bogus...add error to the if token and hard-fail
1997
            this.diagnostics.push({
3✔
1998
                ...DiagnosticMessages.expectedEndIfElseIfOrElseToTerminateThenBlock(),
1999
                location: ifToken.location
2000
            });
2001
            throw this.lastDiagnosticAsError();
3✔
2002
        }
2003
        return branch;
2,339✔
2004
    }
2005

2006
    private conditionalCompileStatement(): ConditionalCompileStatement {
2007
        const hashIfToken = this.advance();
50✔
2008
        let notToken: Token | undefined;
2009

2010
        if (this.check(TokenKind.Not)) {
50✔
2011
            notToken = this.advance();
7✔
2012
        }
2013

2014
        if (!this.checkAny(TokenKind.True, TokenKind.False, TokenKind.Identifier)) {
50✔
2015
            this.diagnostics.push({
1✔
2016
                ...DiagnosticMessages.invalidHashIfValue(),
2017
                location: this.peek()?.location
3!
2018
            });
2019
        }
2020

2021

2022
        const condition = this.advance();
50✔
2023

2024
        let thenBranch: Block;
2025
        let elseBranch: ConditionalCompileStatement | Block | undefined;
2026

2027
        let hashEndIfToken: Token | undefined;
2028
        let hashElseToken: Token | undefined;
2029

2030
        //keep track of the current error count
2031
        //if this is `#if false` remove all diagnostics.
2032
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
50✔
2033

2034
        thenBranch = this.blockConditionalCompileBranch(hashIfToken);
50✔
2035
        const conditionTextLower = condition.text.toLowerCase();
49✔
2036
        if (!this.options.bsConsts?.get(conditionTextLower) || conditionTextLower === 'false') {
49!
2037
            //throw out any new diagnostics created as a result of a false block
2038
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
44✔
2039
        }
2040

2041
        this.ensureNewLine();
49✔
2042
        this.advance();
49✔
2043

2044
        //else branch
2045
        if (this.check(TokenKind.HashElseIf)) {
49✔
2046
            // recurse-read `#else if`
2047
            elseBranch = this.conditionalCompileStatement();
15✔
2048
            this.ensureNewLine();
15✔
2049

2050
        } else if (this.check(TokenKind.HashElse)) {
34✔
2051
            hashElseToken = this.advance();
9✔
2052
            let diagnosticsLengthBeforeBlock = this.diagnostics.length;
9✔
2053
            elseBranch = this.blockConditionalCompileBranch(hashIfToken);
9✔
2054

2055
            if (condition.text.toLowerCase() === 'true') {
9!
2056
                //throw out any new diagnostics created as a result of a false block
UNCOV
2057
                this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
×
2058
            }
2059
            this.ensureNewLine();
9✔
2060
            this.advance();
9✔
2061
        }
2062

2063
        if (!isConditionalCompileStatement(elseBranch)) {
49✔
2064

2065
            if (this.check(TokenKind.HashEndIf)) {
34!
2066
                hashEndIfToken = this.advance();
34✔
2067

2068
            } else {
2069
                //missing #endif
UNCOV
2070
                this.diagnostics.push({
×
2071
                    ...DiagnosticMessages.expectedHashEndIfToCloseHashIf(hashIfToken.location?.range.start.line),
×
2072
                    location: hashIfToken.location
2073
                });
2074
            }
2075
        }
2076

2077
        return new ConditionalCompileStatement({
49✔
2078
            hashIf: hashIfToken,
2079
            hashElse: hashElseToken,
2080
            hashEndIf: hashEndIfToken,
2081
            not: notToken,
2082
            condition: condition,
2083
            thenBranch: thenBranch,
2084
            elseBranch: elseBranch
2085
        });
2086
    }
2087

2088
    //consume a conditional compile branch block of an `#if` statement
2089
    private blockConditionalCompileBranch(hashIfToken: Token) {
2090
        //keep track of the current error count, because if the then branch fails,
2091
        //we will trash them in favor of a single error on if
2092
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
59✔
2093

2094
        //parsing until trailing "#end if", "#else", "#else if"
2095
        let branch = this.conditionalCompileBlock();
59✔
2096

2097
        if (!branch) {
58!
2098
            //throw out any new diagnostics created as a result of a `then` block parse failure.
2099
            //the block() function will discard the current line, so any discarded diagnostics will
2100
            //resurface if they are legitimate, and not a result of a malformed if statement
UNCOV
2101
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
×
2102

2103
            //this whole if statement is bogus...add error to the if token and hard-fail
UNCOV
2104
            this.diagnostics.push({
×
2105
                ...DiagnosticMessages.expectedTerminatorOnConditionalCompileBlock(),
2106
                location: hashIfToken.location
2107
            });
UNCOV
2108
            throw this.lastDiagnosticAsError();
×
2109
        }
2110
        return branch;
58✔
2111
    }
2112

2113
    /**
2114
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2115
     * Always looks for `#end if` or `#else`
2116
     */
2117
    private conditionalCompileBlock(): Block | undefined {
2118
        const parentAnnotations = this.enterAnnotationBlock();
59✔
2119

2120
        this.consumeStatementSeparators(true);
59✔
2121
        const unsafeTerminators = BlockTerminators;
59✔
2122
        const conditionalEndTokens = [TokenKind.HashElse, TokenKind.HashElseIf, TokenKind.HashEndIf];
59✔
2123
        const terminators = [...conditionalEndTokens, ...unsafeTerminators];
59✔
2124
        this.globalTerminators.push(conditionalEndTokens);
59✔
2125
        const statements: Statement[] = [];
59✔
2126
        while (!this.isAtEnd() && !this.checkAny(...terminators)) {
59✔
2127
            //grab the location of the current token
2128
            let loopCurrent = this.current;
57✔
2129
            let dec = this.declaration();
57✔
2130
            if (dec) {
57✔
2131
                if (!isAnnotationExpression(dec)) {
56!
2132
                    this.consumePendingAnnotations(dec);
56✔
2133
                    statements.push(dec);
56✔
2134
                }
2135

2136
                const peekKind = this.peek().kind;
56✔
2137
                if (conditionalEndTokens.includes(peekKind)) {
56✔
2138
                    // current conditional compile branch was closed by other statement, rewind to preceding newline
2139
                    this.current--;
1✔
2140
                }
2141
                //ensure statement separator
2142
                this.consumeStatementSeparators();
56✔
2143

2144
            } else {
2145
                //something went wrong. reset to the top of the loop
2146
                this.current = loopCurrent;
1✔
2147

2148
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2149
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
1✔
2150

2151
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2152
                //but there's already an error in the file being parsed, so just leave this line here
2153
                this.advance();
1✔
2154

2155
                //consume potential separators
2156
                this.consumeStatementSeparators(true);
1✔
2157
            }
2158
        }
2159
        this.globalTerminators.pop();
59✔
2160

2161

2162
        if (this.isAtEnd()) {
59!
UNCOV
2163
            return undefined;
×
2164
            // TODO: Figure out how to handle unterminated blocks well
2165
        } else {
2166
            //did we  hit an unsafe terminator?
2167
            //if so, we need to restore the statement separator
2168
            let prev = this.previous();
59✔
2169
            let prevKind = prev.kind;
59✔
2170
            let peek = this.peek();
59✔
2171
            let peekKind = this.peek().kind;
59✔
2172
            if (
59✔
2173
                (peekKind === TokenKind.HashEndIf || peekKind === TokenKind.HashElse || peekKind === TokenKind.HashElseIf) &&
158✔
2174
                (prevKind === TokenKind.Newline)
2175
            ) {
2176
                this.current--;
58✔
2177
            } else if (unsafeTerminators.includes(peekKind) &&
1!
2178
                (prevKind === TokenKind.Newline || prevKind === TokenKind.Colon)
2179
            ) {
2180
                this.diagnostics.push({
1✔
2181
                    ...DiagnosticMessages.unsafeUnmatchedTerminatorInConditionalCompileBlock(peek.text),
2182
                    location: peek.location
2183
                });
2184
                throw this.lastDiagnosticAsError();
1✔
2185
            } else {
UNCOV
2186
                return undefined;
×
2187
            }
2188
        }
2189
        this.exitAnnotationBlock(parentAnnotations);
58✔
2190
        return new Block({ statements: statements });
58✔
2191
    }
2192

2193
    private conditionalCompileConstStatement() {
2194
        const hashConstToken = this.advance();
20✔
2195

2196
        const constName = this.peek();
20✔
2197
        //disallow using keywords for const names
2198
        if (ReservedWords.has(constName?.text.toLowerCase())) {
20!
2199
            this.diagnostics.push({
1✔
2200
                ...DiagnosticMessages.constNameCannotBeReservedWord(),
2201
                location: constName?.location
3!
2202
            });
2203

2204
            this.lastDiagnosticAsError();
1✔
2205
            return;
1✔
2206
        }
2207
        const assignment = this.assignment();
19✔
2208
        if (assignment) {
17!
2209
            // check for something other than #const <name> = <otherName|true|false>
2210
            if (assignment.tokens.as || assignment.typeExpression) {
17!
UNCOV
2211
                this.diagnostics.push({
×
2212
                    ...DiagnosticMessages.unexpectedToken(assignment.tokens.as?.text || assignment.typeExpression?.getName(ParseMode.BrighterScript)),
×
2213
                    location: assignment.tokens.as?.location ?? assignment.typeExpression?.location
×
2214
                });
UNCOV
2215
                this.lastDiagnosticAsError();
×
2216
            }
2217

2218
            if (isVariableExpression(assignment.value) || isLiteralBoolean(assignment.value)) {
17✔
2219
                //value is an identifier or a boolean
2220
                //check for valid identifiers will happen in program validation
2221
            } else {
2222
                this.diagnostics.push({
2✔
2223
                    ...DiagnosticMessages.invalidHashConstValue(),
2224
                    location: assignment.value.location
2225
                });
2226
                this.lastDiagnosticAsError();
2✔
2227
            }
2228
        } else {
UNCOV
2229
            return undefined;
×
2230
        }
2231

2232
        if (!this.check(TokenKind.Newline)) {
17!
UNCOV
2233
            this.diagnostics.push({
×
2234
                ...DiagnosticMessages.expectedNewlineInConditionalCompile(),
2235
                location: this.peek().location
2236
            });
UNCOV
2237
            throw this.lastDiagnosticAsError();
×
2238
        }
2239

2240
        return new ConditionalCompileConstStatement({ hashConst: hashConstToken, assignment: assignment });
17✔
2241
    }
2242

2243
    private conditionalCompileErrorStatement() {
2244
        const hashErrorToken = this.advance();
9✔
2245
        const tokensUntilEndOfLine = this.consumeUntil(TokenKind.Newline);
9✔
2246
        const message = createToken(TokenKind.HashErrorMessage, tokensUntilEndOfLine.map(t => t.text).join(' '));
9✔
2247
        return new ConditionalCompileErrorStatement({ hashError: hashErrorToken, message: message });
9✔
2248
    }
2249

2250
    private ensureNewLine() {
2251
        //ensure newline before next keyword
2252
        if (!this.check(TokenKind.Newline)) {
73!
UNCOV
2253
            this.diagnostics.push({
×
2254
                ...DiagnosticMessages.expectedNewlineInConditionalCompile(),
2255
                location: this.peek().location
2256
            });
UNCOV
2257
            throw this.lastDiagnosticAsError();
×
2258
        }
2259
    }
2260

2261
    private ensureNewLineOrColon(silent = false) {
2,339✔
2262
        const prev = this.previous().kind;
2,519✔
2263
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
2,519✔
2264
            if (!silent) {
129✔
2265
                this.diagnostics.push({
6✔
2266
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2267
                    location: this.peek().location
2268
                });
2269
            }
2270
            return false;
129✔
2271
        }
2272
        return true;
2,390✔
2273
    }
2274

2275
    //ensure each statement of an inline block is single-line
2276
    private ensureInline(statements: Statement[]) {
2277
        for (const stat of statements) {
68✔
2278
            if (isIfStatement(stat) && !stat.isInline) {
86✔
2279
                this.diagnostics.push({
2✔
2280
                    ...DiagnosticMessages.expectedInlineIfStatement(),
2281
                    location: stat.location
2282
                });
2283
            }
2284
        }
2285
    }
2286

2287
    //consume inline branch of an `if` statement
2288
    private inlineConditionalBranch(...additionalTerminators: BlockTerminator[]): Block | undefined {
2289
        let statements = [];
86✔
2290
        //attempt to get the next statement without using `this.declaration`
2291
        //which seems a bit hackish to get to work properly
2292
        let statement = this.statement();
86✔
2293
        if (!statement) {
86!
UNCOV
2294
            return undefined;
×
2295
        }
2296
        statements.push(statement);
86✔
2297

2298
        //look for colon statement separator
2299
        let foundColon = false;
86✔
2300
        while (this.match(TokenKind.Colon)) {
86✔
2301
            foundColon = true;
23✔
2302
        }
2303

2304
        //if a colon was found, add the next statement or err if unexpected
2305
        if (foundColon) {
86✔
2306
            if (!this.checkAny(TokenKind.Newline, ...additionalTerminators)) {
23✔
2307
                //if not an ending keyword, add next statement
2308
                let extra = this.inlineConditionalBranch(...additionalTerminators);
18✔
2309
                if (!extra) {
18!
UNCOV
2310
                    return undefined;
×
2311
                }
2312
                statements.push(...extra.statements);
18✔
2313
            } else {
2314
                //error: colon before next keyword
2315
                const colon = this.previous();
5✔
2316
                this.diagnostics.push({
5✔
2317
                    ...DiagnosticMessages.unexpectedToken(colon.text),
2318
                    location: colon.location
2319
                });
2320
            }
2321
        }
2322
        return new Block({ statements: statements });
86✔
2323
    }
2324

2325
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2326
        let expressionStart = this.peek();
531✔
2327

2328
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
531✔
2329
            let operator = this.advance();
21✔
2330

2331
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
21✔
2332
                this.diagnostics.push({
1✔
2333
                    ...DiagnosticMessages.consecutiveIncrementDecrementOperatorsAreNotAllowed(),
2334
                    location: this.peek().location
2335
                });
2336
                throw this.lastDiagnosticAsError();
1✔
2337
            } else if (isCallExpression(expr)) {
20✔
2338
                this.diagnostics.push({
1✔
2339
                    ...DiagnosticMessages.incrementDecrementOperatorsAreNotAllowedAsResultOfFunctionCall(),
2340
                    location: expressionStart.location
2341
                });
2342
                throw this.lastDiagnosticAsError();
1✔
2343
            }
2344

2345
            const result = new IncrementStatement({ value: expr, operator: operator });
19✔
2346
            return result;
19✔
2347
        }
2348

2349
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
510✔
2350
            return new ExpressionStatement({ expression: expr });
431✔
2351
        }
2352

2353
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an inclosing ExpressionStatement
2354
        this.diagnostics.push({
79✔
2355
            ...DiagnosticMessages.expectedStatementOrFunctionCallButReceivedExpression(),
2356
            location: expressionStart.location
2357
        });
2358
        return new ExpressionStatement({ expression: expr });
79✔
2359
    }
2360

2361
    private setStatement(): DottedSetStatement | IndexedSetStatement | ExpressionStatement | IncrementStatement | AssignmentStatement | AugmentedAssignmentStatement {
2362
        /**
2363
         * Attempts to find an expression-statement or an increment statement.
2364
         * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
2365
         * statements aren't valid expressions. They _do_ however fall under the same parsing
2366
         * priority as standalone function calls though, so we can parse them in the same way.
2367
         */
2368
        let expr = this.call();
904✔
2369
        if (this.check(TokenKind.Equal) && !(isCallExpression(expr))) {
857✔
2370
            let left = expr;
309✔
2371
            let operator = this.advance();
309✔
2372
            let right = this.expression();
309✔
2373

2374
            // Create a dotted or indexed "set" based on the left-hand side's type
2375
            if (isIndexedGetExpression(left)) {
309✔
2376
                return new IndexedSetStatement({
23✔
2377
                    obj: left.obj,
2378
                    indexes: left.indexes,
2379
                    value: right,
2380
                    openingSquare: left.tokens.openingSquare,
2381
                    closingSquare: left.tokens.closingSquare,
2382
                    equals: operator
2383
                });
2384
            } else if (isDottedGetExpression(left)) {
286✔
2385
                return new DottedSetStatement({
283✔
2386
                    obj: left.obj,
2387
                    name: left.tokens.name,
2388
                    value: right,
2389
                    dot: left.tokens.dot,
2390
                    equals: operator
2391
                });
2392
            }
2393
        } else if (this.checkAny(...CompoundAssignmentOperators) && !(isCallExpression(expr))) {
548✔
2394
            let left = expr;
20✔
2395
            let operator = this.advance();
20✔
2396
            let right = this.expression();
20✔
2397
            return new AugmentedAssignmentStatement({
20✔
2398
                item: left,
2399
                operator: operator,
2400
                value: right
2401
            });
2402
        }
2403
        return this.expressionStatement(expr);
531✔
2404
    }
2405

2406
    private printStatement(): PrintStatement {
2407
        let printKeyword = this.advance();
1,055✔
2408

2409
        let values: (
2410
            | Expression
2411
            | PrintSeparatorTab
2412
            | PrintSeparatorSpace)[] = [];
1,055✔
2413

2414
        while (!this.checkEndOfStatement()) {
1,055✔
2415
            if (this.check(TokenKind.Semicolon)) {
1,164✔
2416
                values.push(this.advance() as PrintSeparatorSpace);
29✔
2417
            } else if (this.check(TokenKind.Comma)) {
1,135✔
2418
                values.push(this.advance() as PrintSeparatorTab);
13✔
2419
            } else if (this.check(TokenKind.Else)) {
1,122✔
2420
                break; // inline branch
22✔
2421
            } else {
2422
                values.push(this.expression());
1,100✔
2423
            }
2424
        }
2425

2426
        //print statements can be empty, so look for empty print conditions
2427
        if (!values.length) {
1,052✔
2428
            const endOfStatementLocation = util.createBoundingLocation(printKeyword, this.peek());
12✔
2429
            let emptyStringLiteral = createStringLiteral('', endOfStatementLocation);
12✔
2430
            values.push(emptyStringLiteral);
12✔
2431
        }
2432

2433
        let last = values[values.length - 1];
1,052✔
2434
        if (isToken(last)) {
1,052✔
2435
            // TODO: error, expected value
2436
        }
2437

2438
        return new PrintStatement({ print: printKeyword, expressions: values });
1,052✔
2439
    }
2440

2441
    /**
2442
     * Parses a return statement with an optional return value.
2443
     * @returns an AST representation of a return statement.
2444
     */
2445
    private returnStatement(): ReturnStatement {
2446
        let options = { return: this.previous() };
2,769✔
2447

2448
        if (this.checkEndOfStatement()) {
2,769✔
2449
            return new ReturnStatement(options);
9✔
2450
        }
2451

2452
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
2,760✔
2453
        return new ReturnStatement({ ...options, value: toReturn });
2,759✔
2454
    }
2455

2456
    /**
2457
     * Parses a `label` statement
2458
     * @returns an AST representation of an `label` statement.
2459
     */
2460
    private labelStatement() {
2461
        let options = {
11✔
2462
            name: this.advance(),
2463
            colon: this.advance()
2464
        };
2465

2466
        //label must be alone on its line, this is probably not a label
2467
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
11✔
2468
            //rewind and cancel
2469
            this.current -= 2;
2✔
2470
            throw new CancelStatementError();
2✔
2471
        }
2472

2473
        return new LabelStatement(options);
9✔
2474
    }
2475

2476
    /**
2477
     * Parses a `continue` statement
2478
     */
2479
    private continueStatement() {
2480
        return new ContinueStatement({
11✔
2481
            continue: this.advance(),
2482
            loopType: this.tryConsume(
2483
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2484
                TokenKind.While, TokenKind.For
2485
            )
2486
        });
2487
    }
2488

2489
    /**
2490
     * Parses a `goto` statement
2491
     * @returns an AST representation of an `goto` statement.
2492
     */
2493
    private gotoStatement() {
2494
        let tokens = {
11✔
2495
            goto: this.advance(),
2496
            label: this.consume(
2497
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2498
                TokenKind.Identifier
2499
            )
2500
        };
2501

2502
        return new GotoStatement(tokens);
9✔
2503
    }
2504

2505
    /**
2506
     * Parses an `end` statement
2507
     * @returns an AST representation of an `end` statement.
2508
     */
2509
    private endStatement() {
2510
        let options = { end: this.advance() };
7✔
2511

2512
        return new EndStatement(options);
7✔
2513
    }
2514
    /**
2515
     * Parses a `stop` statement
2516
     * @returns an AST representation of a `stop` statement
2517
     */
2518
    private stopStatement() {
2519
        let options = { stop: this.advance() };
15✔
2520

2521
        return new StopStatement(options);
15✔
2522
    }
2523

2524
    /**
2525
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2526
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2527
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2528
     *                    ignored.
2529
     */
2530
    private block(...terminators: BlockTerminator[]): Block | undefined {
2531
        const parentAnnotations = this.enterAnnotationBlock();
5,688✔
2532

2533
        this.consumeStatementSeparators(true);
5,688✔
2534
        const statements: Statement[] = [];
5,688✔
2535
        const flatGlobalTerminators = this.globalTerminators.flat().flat();
5,688✔
2536
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators, ...flatGlobalTerminators)) {
5,688✔
2537
            //grab the location of the current token
2538
            let loopCurrent = this.current;
6,725✔
2539
            let dec = this.declaration();
6,725✔
2540
            if (dec) {
6,725✔
2541
                if (!isAnnotationExpression(dec)) {
6,676✔
2542
                    this.consumePendingAnnotations(dec);
6,672✔
2543
                    statements.push(dec);
6,672✔
2544
                }
2545

2546
                //ensure statement separator
2547
                this.consumeStatementSeparators();
6,676✔
2548

2549
            } else {
2550
                //something went wrong. reset to the top of the loop
2551
                this.current = loopCurrent;
49✔
2552

2553
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2554
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
49✔
2555

2556
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2557
                //but there's already an error in the file being parsed, so just leave this line here
2558
                this.advance();
49✔
2559

2560
                //consume potential separators
2561
                this.consumeStatementSeparators(true);
49✔
2562
            }
2563
        }
2564

2565
        if (this.isAtEnd()) {
5,688✔
2566
            return undefined;
6✔
2567
            // TODO: Figure out how to handle unterminated blocks well
2568
        } else if (terminators.length > 0) {
5,682✔
2569
            //did we hit end-sub / end-function while looking for some other terminator?
2570
            //if so, we need to restore the statement separator
2571
            let prev = this.previous().kind;
2,471✔
2572
            let peek = this.peek().kind;
2,471✔
2573
            if (
2,471✔
2574
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
4,946!
2575
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2576
            ) {
2577
                this.current--;
6✔
2578
            }
2579
        }
2580

2581
        this.exitAnnotationBlock(parentAnnotations);
5,682✔
2582
        return new Block({ statements: statements });
5,682✔
2583
    }
2584

2585
    /**
2586
     * Attach pending annotations to the provided statement,
2587
     * and then reset the annotations array
2588
     */
2589
    consumePendingAnnotations(statement: Statement) {
2590
        if (this.pendingAnnotations.length) {
13,115✔
2591
            statement.annotations = this.pendingAnnotations;
31✔
2592
            this.pendingAnnotations = [];
31✔
2593
        }
2594
    }
2595

2596
    enterAnnotationBlock() {
2597
        const pending = this.pendingAnnotations;
10,239✔
2598
        this.pendingAnnotations = [];
10,239✔
2599
        return pending;
10,239✔
2600
    }
2601

2602
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2603
        // non consumed annotations are an error
2604
        if (this.pendingAnnotations.length) {
10,231✔
2605
            for (const annotation of this.pendingAnnotations) {
4✔
2606
                this.diagnostics.push({
6✔
2607
                    ...DiagnosticMessages.unusedAnnotation(),
2608
                    location: annotation.location
2609
                });
2610
            }
2611
        }
2612
        this.pendingAnnotations = parentAnnotations;
10,231✔
2613
    }
2614

2615
    private expression(findTypecast = true): Expression {
10,515✔
2616
        let expression = this.anonymousFunction();
10,868✔
2617
        let asToken: Token;
2618
        let typeExpression: TypeExpression;
2619
        if (findTypecast) {
10,829✔
2620
            do {
10,476✔
2621
                if (this.check(TokenKind.As)) {
10,539✔
2622
                    this.warnIfNotBrighterScriptMode('type cast');
64✔
2623
                    // Check if this expression is wrapped in any type casts
2624
                    // allows for multiple casts:
2625
                    // myVal = foo() as dynamic as string
2626
                    [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
64✔
2627
                    if (asToken && typeExpression) {
64✔
2628
                        expression = new TypecastExpression({ obj: expression, as: asToken, typeExpression: typeExpression });
63✔
2629
                    }
2630
                } else {
2631
                    break;
10,475✔
2632
                }
2633

2634
            } while (asToken && typeExpression);
128✔
2635
        }
2636
        return expression;
10,829✔
2637
    }
2638

2639
    private anonymousFunction(): Expression {
2640
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
10,868✔
2641
            const func = this.functionDeclaration(true);
78✔
2642
            //if there's an open paren after this, this is an IIFE
2643
            if (this.check(TokenKind.LeftParen)) {
78✔
2644
                return this.finishCall(this.advance(), func);
3✔
2645
            } else {
2646
                return func;
75✔
2647
            }
2648
        }
2649

2650
        let expr = this.boolean();
10,790✔
2651

2652
        if (this.check(TokenKind.Question)) {
10,751✔
2653
            return this.ternaryExpression(expr);
76✔
2654
        } else if (this.check(TokenKind.QuestionQuestion)) {
10,675✔
2655
            return this.nullCoalescingExpression(expr);
32✔
2656
        } else {
2657
            return expr;
10,643✔
2658
        }
2659
    }
2660

2661
    private boolean(): Expression {
2662
        let expr = this.relational();
10,790✔
2663

2664
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
10,751✔
2665
            let operator = this.previous();
29✔
2666
            let right = this.relational();
29✔
2667
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
29✔
2668
        }
2669

2670
        return expr;
10,751✔
2671
    }
2672

2673
    private relational(): Expression {
2674
        let expr = this.additive();
10,844✔
2675

2676
        while (
10,805✔
2677
            this.matchAny(
2678
                TokenKind.Equal,
2679
                TokenKind.LessGreater,
2680
                TokenKind.Greater,
2681
                TokenKind.GreaterEqual,
2682
                TokenKind.Less,
2683
                TokenKind.LessEqual
2684
            )
2685
        ) {
2686
            let operator = this.previous();
1,505✔
2687
            let right = this.additive();
1,505✔
2688
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,505✔
2689
        }
2690

2691
        return expr;
10,805✔
2692
    }
2693

2694
    // TODO: bitshift
2695

2696
    private additive(): Expression {
2697
        let expr = this.multiplicative();
12,349✔
2698

2699
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
12,310✔
2700
            let operator = this.previous();
1,198✔
2701
            let right = this.multiplicative();
1,198✔
2702
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,198✔
2703
        }
2704

2705
        return expr;
12,310✔
2706
    }
2707

2708
    private multiplicative(): Expression {
2709
        let expr = this.exponential();
13,547✔
2710

2711
        while (this.matchAny(
13,508✔
2712
            TokenKind.Forwardslash,
2713
            TokenKind.Backslash,
2714
            TokenKind.Star,
2715
            TokenKind.Mod,
2716
            TokenKind.LeftShift,
2717
            TokenKind.RightShift
2718
        )) {
2719
            let operator = this.previous();
53✔
2720
            let right = this.exponential();
53✔
2721
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
53✔
2722
        }
2723

2724
        return expr;
13,508✔
2725
    }
2726

2727
    private exponential(): Expression {
2728
        let expr = this.prefixUnary();
13,600✔
2729

2730
        while (this.match(TokenKind.Caret)) {
13,561✔
2731
            let operator = this.previous();
8✔
2732
            let right = this.prefixUnary();
8✔
2733
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
8✔
2734
        }
2735

2736
        return expr;
13,561✔
2737
    }
2738

2739
    private prefixUnary(): Expression {
2740
        const nextKind = this.peek().kind;
13,644✔
2741
        if (nextKind === TokenKind.Not) {
13,644✔
2742
            this.current++; //advance
25✔
2743
            let operator = this.previous();
25✔
2744
            let right = this.relational();
25✔
2745
            return new UnaryExpression({ operator: operator, right: right });
25✔
2746
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
13,619✔
2747
            this.current++; //advance
36✔
2748
            let operator = this.previous();
36✔
2749
            let right = (nextKind as any) === TokenKind.Not
36✔
2750
                ? this.boolean()
36!
2751
                : this.prefixUnary();
2752
            return new UnaryExpression({ operator: operator, right: right });
36✔
2753
        }
2754
        return this.call();
13,583✔
2755
    }
2756

2757
    private indexedGet(expr: Expression) {
2758
        let openingSquare = this.previous();
142✔
2759
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
142✔
2760
        let indexes: Expression[] = [];
142✔
2761

2762

2763
        //consume leading newlines
2764
        while (this.match(TokenKind.Newline)) { }
142✔
2765

2766
        try {
142✔
2767
            indexes.push(
142✔
2768
                this.expression()
2769
            );
2770
            //consume additional indexes separated by commas
2771
            while (this.check(TokenKind.Comma)) {
140✔
2772
                //discard the comma
2773
                this.advance();
13✔
2774
                indexes.push(
13✔
2775
                    this.expression()
2776
                );
2777
            }
2778
        } catch (error) {
2779
            this.rethrowNonDiagnosticError(error);
2✔
2780
        }
2781
        //consume trailing newlines
2782
        while (this.match(TokenKind.Newline)) { }
142✔
2783

2784
        const closingSquare = this.tryConsume(
142✔
2785
            DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
2786
            TokenKind.RightSquareBracket
2787
        );
2788

2789
        return new IndexedGetExpression({
142✔
2790
            obj: expr,
2791
            indexes: indexes,
2792
            openingSquare: openingSquare,
2793
            closingSquare: closingSquare,
2794
            questionDot: questionDotToken
2795
        });
2796
    }
2797

2798
    private newExpression() {
2799
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
130✔
2800
        let newToken = this.advance();
130✔
2801

2802
        let nameExpr = this.identifyingExpression();
130✔
2803
        let leftParen = this.tryConsume(
130✔
2804
            DiagnosticMessages.unexpectedToken(this.peek().text),
2805
            TokenKind.LeftParen,
2806
            TokenKind.QuestionLeftParen
2807
        );
2808

2809
        if (!leftParen) {
130✔
2810
            // new expression without a following call expression
2811
            // wrap the name in an expression
2812
            const endOfStatementLocation = util.createBoundingLocation(newToken, this.peek());
4✔
2813
            const exprStmt = nameExpr ?? createStringLiteral('', endOfStatementLocation);
4!
2814
            return new ExpressionStatement({ expression: exprStmt });
4✔
2815
        }
2816

2817
        let call = this.finishCall(leftParen, nameExpr);
126✔
2818
        //pop the call from the  callExpressions list because this is technically something else
2819
        this.callExpressions.pop();
126✔
2820
        let result = new NewExpression({ new: newToken, call: call });
126✔
2821
        return result;
126✔
2822
    }
2823

2824
    /**
2825
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2826
     */
2827
    private callfunc(callee: Expression): Expression {
2828
        this.warnIfNotBrighterScriptMode('callfunc operator');
28✔
2829
        let operator = this.previous();
28✔
2830
        let methodName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
28✔
2831
        // force it into an identifier so the AST makes some sense
2832
        methodName.kind = TokenKind.Identifier;
25✔
2833
        let openParen = this.consume(DiagnosticMessages.expectedOpenParenToFollowCallfuncIdentifier(), TokenKind.LeftParen);
25✔
2834
        let call = this.finishCall(openParen, callee, false);
25✔
2835

2836
        return new CallfuncExpression({
25✔
2837
            callee: callee,
2838
            operator: operator,
2839
            methodName: methodName as Identifier,
2840
            openingParen: openParen,
2841
            args: call.args,
2842
            closingParen: call.tokens.closingParen
2843
        });
2844
    }
2845

2846
    private call(): Expression {
2847
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
14,487✔
2848
            return this.newExpression();
130✔
2849
        }
2850
        let expr = this.primary();
14,357✔
2851

2852
        while (true) {
14,274✔
2853
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
18,532✔
2854
                expr = this.finishCall(this.previous(), expr);
1,957✔
2855
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
16,575✔
2856
                expr = this.indexedGet(expr);
140✔
2857
            } else if (this.match(TokenKind.Callfunc)) {
16,435✔
2858
                expr = this.callfunc(expr);
28✔
2859
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
16,407✔
2860
                if (this.match(TokenKind.LeftSquareBracket)) {
2,175✔
2861
                    expr = this.indexedGet(expr);
2✔
2862
                } else {
2863
                    let dot = this.previous();
2,173✔
2864
                    let name = this.tryConsume(
2,173✔
2865
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2866
                        TokenKind.Identifier,
2867
                        ...AllowedProperties
2868
                    );
2869
                    if (!name) {
2,173✔
2870
                        break;
39✔
2871
                    }
2872

2873
                    // force it into an identifier so the AST makes some sense
2874
                    name.kind = TokenKind.Identifier;
2,134✔
2875
                    expr = new DottedGetExpression({ obj: expr, name: name as Identifier, dot: dot });
2,134✔
2876
                }
2877

2878
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
14,232✔
2879
                let dot = this.advance();
9✔
2880
                let name = this.tryConsume(
9✔
2881
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2882
                    TokenKind.Identifier,
2883
                    ...AllowedProperties
2884
                );
2885

2886
                // force it into an identifier so the AST makes some sense
2887
                name.kind = TokenKind.Identifier;
9✔
2888
                if (!name) {
9!
UNCOV
2889
                    break;
×
2890
                }
2891
                expr = new XmlAttributeGetExpression({ obj: expr, name: name as Identifier, at: dot });
9✔
2892
                //only allow a single `@` expression
2893
                break;
9✔
2894

2895
            } else {
2896
                break;
14,223✔
2897
            }
2898
        }
2899

2900
        return expr;
14,271✔
2901
    }
2902

2903
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
2,086✔
2904
        let args = [] as Expression[];
2,120✔
2905
        while (this.match(TokenKind.Newline)) { }
2,120✔
2906

2907
        if (!this.check(TokenKind.RightParen)) {
2,120✔
2908
            do {
1,086✔
2909
                while (this.match(TokenKind.Newline)) { }
1,553✔
2910

2911
                if (args.length >= CallExpression.MaximumArguments) {
1,553!
UNCOV
2912
                    this.diagnostics.push({
×
2913
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
2914
                        location: this.peek()?.location
×
2915
                    });
UNCOV
2916
                    throw this.lastDiagnosticAsError();
×
2917
                }
2918
                try {
1,553✔
2919
                    args.push(this.expression());
1,553✔
2920
                } catch (error) {
2921
                    this.rethrowNonDiagnosticError(error);
5✔
2922
                    // we were unable to get an expression, so don't continue
2923
                    break;
5✔
2924
                }
2925
            } while (this.match(TokenKind.Comma));
2926
        }
2927

2928
        while (this.match(TokenKind.Newline)) { }
2,120✔
2929

2930
        const closingParen = this.tryConsume(
2,120✔
2931
            DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
2932
            TokenKind.RightParen
2933
        );
2934

2935
        let expression = new CallExpression({
2,120✔
2936
            callee: callee,
2937
            openingParen: openingParen,
2938
            args: args,
2939
            closingParen: closingParen
2940
        });
2941
        if (addToCallExpressionList) {
2,120✔
2942
            this.callExpressions.push(expression);
2,086✔
2943
        }
2944
        return expression;
2,120✔
2945
    }
2946

2947
    /**
2948
     * Creates a TypeExpression, which wraps standard ASTNodes that represent a BscType
2949
     */
2950
    private typeExpression(): TypeExpression {
2951
        const changedTokens: { token: Token; oldKind: TokenKind }[] = [];
1,405✔
2952
        try {
1,405✔
2953
            let expr: Expression = this.getTypeExpressionPart(changedTokens);
1,405✔
2954
            while (this.options.mode === ParseMode.BrighterScript && this.matchAny(TokenKind.Or)) {
1,405✔
2955
                // If we're in Brighterscript mode, allow union types with "or" between types
2956
                // TODO: Handle Union types in parens? eg. "(string or integer)"
2957
                let operator = this.previous();
32✔
2958
                let right = this.getTypeExpressionPart(changedTokens);
32✔
2959
                if (right) {
32!
2960
                    expr = new BinaryExpression({ left: expr, operator: operator, right: right });
32✔
2961
                } else {
UNCOV
2962
                    break;
×
2963
                }
2964
            }
2965
            if (expr) {
1,405!
2966
                return new TypeExpression({ expression: expr });
1,405✔
2967
            }
2968

2969
        } catch (error) {
2970
            // Something went wrong - reset the kind to what it was previously
2971
            for (const changedToken of changedTokens) {
×
UNCOV
2972
                changedToken.token.kind = changedToken.oldKind;
×
2973
            }
UNCOV
2974
            throw error;
×
2975
        }
2976
    }
2977

2978
    /**
2979
     * Gets a single "part" of a type of a potential Union type
2980
     * Note: this does not NEED to be part of a union type, but the logic is the same
2981
     *
2982
     * @param changedTokens an array that is modified with any tokens that have been changed from their default kind to identifiers - eg. when a keyword is used as type
2983
     * @returns an expression that was successfully parsed
2984
     */
2985
    private getTypeExpressionPart(changedTokens: { token: Token; oldKind: TokenKind }[]) {
2986
        let expr: VariableExpression | DottedGetExpression | TypedArrayExpression;
2987
        if (this.checkAny(...DeclarableTypes)) {
1,437✔
2988
            // if this is just a type, just use directly
2989
            expr = new VariableExpression({ name: this.advance() as Identifier });
966✔
2990
        } else {
2991
            if (this.checkAny(...AllowedTypeIdentifiers)) {
471✔
2992
                // Since the next token is allowed as a type identifier, change the kind
2993
                let nextToken = this.peek();
1✔
2994
                changedTokens.push({ token: nextToken, oldKind: nextToken.kind });
1✔
2995
                nextToken.kind = TokenKind.Identifier;
1✔
2996
            }
2997
            expr = this.identifyingExpression(AllowedTypeIdentifiers);
471✔
2998
        }
2999

3000
        //Check if it has square brackets, thus making it an array
3001
        if (expr && this.check(TokenKind.LeftSquareBracket)) {
1,437✔
3002
            if (this.options.mode === ParseMode.BrightScript) {
26✔
3003
                // typed arrays not allowed in Brightscript
3004
                this.warnIfNotBrighterScriptMode('typed arrays');
1✔
3005
                return expr;
1✔
3006
            }
3007

3008
            // Check if it is an array - that is, if it has `[]` after the type
3009
            // eg. `string[]` or `SomeKlass[]`
3010
            // This is while loop, so it supports multidimensional arrays (eg. integer[][])
3011
            while (this.check(TokenKind.LeftSquareBracket)) {
25✔
3012
                const leftBracket = this.advance();
27✔
3013
                if (this.check(TokenKind.RightSquareBracket)) {
27!
3014
                    const rightBracket = this.advance();
27✔
3015
                    expr = new TypedArrayExpression({ innerType: expr, leftBracket: leftBracket, rightBracket: rightBracket });
27✔
3016
                }
3017
            }
3018
        }
3019

3020
        return expr;
1,436✔
3021
    }
3022

3023
    private primary(): Expression {
3024
        switch (true) {
14,357✔
3025
            case this.matchAny(
14,357!
3026
                TokenKind.False,
3027
                TokenKind.True,
3028
                TokenKind.Invalid,
3029
                TokenKind.IntegerLiteral,
3030
                TokenKind.LongIntegerLiteral,
3031
                TokenKind.FloatLiteral,
3032
                TokenKind.DoubleLiteral,
3033
                TokenKind.StringLiteral
3034
            ):
3035
                return new LiteralExpression({ value: this.previous() });
6,537✔
3036

3037
            //capture source literals (LINE_NUM if brightscript, or a bunch of them if brighterscript)
3038
            case this.matchAny(TokenKind.LineNumLiteral, ...(this.options.mode === ParseMode.BrightScript ? [] : BrighterScriptSourceLiterals)):
7,820✔
3039
                return new SourceLiteralExpression({ value: this.previous() });
34✔
3040

3041
            //template string
3042
            case this.check(TokenKind.BackTick):
3043
                return this.templateString(false);
34✔
3044

3045
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
3046
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
14,953✔
3047
                return this.templateString(true);
5✔
3048

3049
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3050
                return new VariableExpression({ name: this.previous() as Identifier });
7,203✔
3051

3052
            case this.match(TokenKind.LeftParen):
3053
                let left = this.previous();
46✔
3054
                let expr = this.expression();
46✔
3055
                let right = this.consume(
45✔
3056
                    DiagnosticMessages.unmatchedLeftParenAfterExpression(),
3057
                    TokenKind.RightParen
3058
                );
3059
                return new GroupingExpression({ leftParen: left, rightParen: right, expression: expr });
45✔
3060

3061
            case this.matchAny(TokenKind.LeftSquareBracket):
3062
                return this.arrayLiteral();
127✔
3063

3064
            case this.match(TokenKind.LeftCurlyBrace):
3065
                return this.aaLiteral();
247✔
3066

3067
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
UNCOV
3068
                let token = Object.assign(this.previous(), {
×
3069
                    kind: TokenKind.Identifier
3070
                }) as Identifier;
UNCOV
3071
                return new VariableExpression({ name: token });
×
3072

3073
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
UNCOV
3074
                return this.anonymousFunction();
×
3075

3076
            case this.check(TokenKind.RegexLiteral):
3077
                return this.regexLiteralExpression();
44✔
3078

3079
            default:
3080
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
3081
                if (this.checkAny(...this.peekGlobalTerminators())) {
80!
3082
                    //don't throw a diagnostic, just return undefined
3083

3084
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
3085
                } else {
3086
                    this.diagnostics.push({
80✔
3087
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
3088
                        location: this.peek()?.location
240!
3089
                    });
3090
                    throw this.lastDiagnosticAsError();
80✔
3091
                }
3092
        }
3093
    }
3094

3095
    private arrayLiteral() {
3096
        let elements: Array<Expression> = [];
127✔
3097
        let openingSquare = this.previous();
127✔
3098

3099
        while (this.match(TokenKind.Newline)) {
127✔
3100
        }
3101
        let closingSquare: Token;
3102

3103
        if (!this.match(TokenKind.RightSquareBracket)) {
127✔
3104
            try {
97✔
3105
                elements.push(this.expression());
97✔
3106

3107
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
96✔
3108

3109
                    while (this.match(TokenKind.Newline)) {
128✔
3110

3111
                    }
3112

3113
                    if (this.check(TokenKind.RightSquareBracket)) {
128✔
3114
                        break;
23✔
3115
                    }
3116

3117
                    elements.push(this.expression());
105✔
3118
                }
3119
            } catch (error: any) {
3120
                this.rethrowNonDiagnosticError(error);
2✔
3121
            }
3122

3123
            closingSquare = this.tryConsume(
97✔
3124
                DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral(),
3125
                TokenKind.RightSquareBracket
3126
            );
3127
        } else {
3128
            closingSquare = this.previous();
30✔
3129
        }
3130

3131
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3132
        return new ArrayLiteralExpression({ elements: elements, open: openingSquare, close: closingSquare });
127✔
3133
    }
3134

3135
    private aaLiteral() {
3136
        let openingBrace = this.previous();
247✔
3137
        let members: Array<AAMemberExpression> = [];
247✔
3138

3139
        let key = () => {
247✔
3140
            let result = {
263✔
3141
                colonToken: null as Token,
3142
                keyToken: null as Token,
3143
                range: null as Range
3144
            };
3145
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
263✔
3146
                result.keyToken = this.identifier(...AllowedProperties);
232✔
3147
            } else if (this.check(TokenKind.StringLiteral)) {
31!
3148
                result.keyToken = this.advance();
31✔
3149
            } else {
UNCOV
3150
                this.diagnostics.push({
×
3151
                    ...DiagnosticMessages.unexpectedAAKey(),
3152
                    location: this.peek().location
3153
                });
UNCOV
3154
                throw this.lastDiagnosticAsError();
×
3155
            }
3156

3157
            result.colonToken = this.consume(
263✔
3158
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3159
                TokenKind.Colon
3160
            );
3161
            result.range = util.createBoundingRange(result.keyToken, result.colonToken);
262✔
3162
            return result;
262✔
3163
        };
3164

3165
        while (this.match(TokenKind.Newline)) { }
247✔
3166
        let closingBrace: Token;
3167
        if (!this.match(TokenKind.RightCurlyBrace)) {
247✔
3168
            let lastAAMember: AAMemberExpression;
3169
            try {
178✔
3170
                let k = key();
178✔
3171
                let expr = this.expression();
178✔
3172
                lastAAMember = new AAMemberExpression({
177✔
3173
                    key: k.keyToken,
3174
                    colon: k.colonToken,
3175
                    value: expr
3176
                });
3177
                members.push(lastAAMember);
177✔
3178

3179
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
177✔
3180
                    // collect comma at end of expression
3181
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
203✔
3182
                        (lastAAMember as DeepWriteable<AAMemberExpression>).tokens.comma = this.previous();
60✔
3183
                    }
3184

3185
                    this.consumeStatementSeparators(true);
203✔
3186

3187
                    if (this.check(TokenKind.RightCurlyBrace)) {
203✔
3188
                        break;
118✔
3189
                    }
3190
                    let k = key();
85✔
3191
                    let expr = this.expression();
84✔
3192
                    lastAAMember = new AAMemberExpression({
84✔
3193
                        key: k.keyToken,
3194
                        colon: k.colonToken,
3195
                        value: expr
3196
                    });
3197
                    members.push(lastAAMember);
84✔
3198

3199
                }
3200
            } catch (error: any) {
3201
                this.rethrowNonDiagnosticError(error);
2✔
3202
            }
3203

3204
            closingBrace = this.tryConsume(
178✔
3205
                DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral(),
3206
                TokenKind.RightCurlyBrace
3207
            );
3208
        } else {
3209
            closingBrace = this.previous();
69✔
3210
        }
3211

3212
        const aaExpr = new AALiteralExpression({ elements: members, open: openingBrace, close: closingBrace });
247✔
3213
        return aaExpr;
247✔
3214
    }
3215

3216
    /**
3217
     * Pop token if we encounter specified token
3218
     */
3219
    private match(tokenKind: TokenKind) {
3220
        if (this.check(tokenKind)) {
54,371✔
3221
            this.current++; //advance
5,455✔
3222
            return true;
5,455✔
3223
        }
3224
        return false;
48,916✔
3225
    }
3226

3227
    /**
3228
     * Pop token if we encounter a token in the specified list
3229
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3230
     */
3231
    private matchAny(...tokenKinds: TokenKind[]) {
3232
        for (let tokenKind of tokenKinds) {
189,609✔
3233
            if (this.check(tokenKind)) {
531,743✔
3234
                this.current++; //advance
50,336✔
3235
                return true;
50,336✔
3236
            }
3237
        }
3238
        return false;
139,273✔
3239
    }
3240

3241
    /**
3242
     * If the next series of tokens matches the given set of tokens, pop them all
3243
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
3244
     */
3245
    private matchSequence(...tokenKinds: TokenKind[]) {
3246
        const endIndex = this.current + tokenKinds.length;
16,438✔
3247
        for (let i = 0; i < tokenKinds.length; i++) {
16,438✔
3248
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
16,462!
3249
                return false;
16,435✔
3250
            }
3251
        }
3252
        this.current = endIndex;
3✔
3253
        return true;
3✔
3254
    }
3255

3256
    /**
3257
     * Get next token matching a specified list, or fail with an error
3258
     */
3259
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
3260
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
13,519✔
3261
        if (token) {
13,519✔
3262
            return token;
13,505✔
3263
        } else {
3264
            let error = new Error(diagnosticInfo.message);
14✔
3265
            (error as any).isDiagnostic = true;
14✔
3266
            throw error;
14✔
3267
        }
3268
    }
3269

3270
    /**
3271
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3272
     */
3273
    private consumeTokenIf(tokenKind: TokenKind) {
3274
        if (this.match(tokenKind)) {
3,183✔
3275
            return this.previous();
392✔
3276
        }
3277
    }
3278

3279
    private consumeToken(tokenKind: TokenKind) {
3280
        return this.consume(
1,795✔
3281
            DiagnosticMessages.expectedToken(tokenKind),
3282
            tokenKind
3283
        );
3284
    }
3285

3286
    /**
3287
     * Consume, or add a message if not found. But then continue and return undefined
3288
     */
3289
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3290
        const nextKind = this.peek().kind;
20,626✔
3291
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
45,698✔
3292

3293
        if (foundTokenKind) {
20,626✔
3294
            return this.advance();
20,518✔
3295
        }
3296
        this.diagnostics.push({
108✔
3297
            ...diagnostic,
3298
            location: this.peek()?.location
324!
3299
        });
3300
    }
3301

3302
    private tryConsumeToken(tokenKind: TokenKind) {
3303
        return this.tryConsume(
76✔
3304
            DiagnosticMessages.expectedToken(tokenKind),
3305
            tokenKind
3306
        );
3307
    }
3308

3309
    private consumeStatementSeparators(optional = false) {
9,024✔
3310
        //a comment or EOF mark the end of the statement
3311
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
27,003✔
3312
            return true;
383✔
3313
        }
3314
        let consumed = false;
26,620✔
3315
        //consume any newlines and colons
3316
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
26,620✔
3317
            consumed = true;
29,018✔
3318
        }
3319
        if (!optional && !consumed) {
26,620✔
3320
            this.diagnostics.push({
64✔
3321
                ...DiagnosticMessages.expectedNewlineOrColon(),
3322
                location: this.peek()?.location
192!
3323
            });
3324
        }
3325
        return consumed;
26,620✔
3326
    }
3327

3328
    private advance(): Token {
3329
        if (!this.isAtEnd()) {
46,819✔
3330
            this.current++;
46,805✔
3331
        }
3332
        return this.previous();
46,819✔
3333
    }
3334

3335
    private checkEndOfStatement(): boolean {
3336
        const nextKind = this.peek().kind;
6,377✔
3337
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
6,377✔
3338
    }
3339

3340
    private checkPrevious(tokenKind: TokenKind): boolean {
3341
        return this.previous()?.kind === tokenKind;
215!
3342
    }
3343

3344
    /**
3345
     * Check that the next token kind is the expected kind
3346
     * @param tokenKind the expected next kind
3347
     * @returns true if the next tokenKind is the expected value
3348
     */
3349
    private check(tokenKind: TokenKind): boolean {
3350
        const nextKind = this.peek().kind;
871,001✔
3351
        if (nextKind === TokenKind.Eof) {
871,001✔
3352
            return false;
9,505✔
3353
        }
3354
        return nextKind === tokenKind;
861,496✔
3355
    }
3356

3357
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3358
        const nextKind = this.peek().kind;
137,254✔
3359
        if (nextKind === TokenKind.Eof) {
137,254✔
3360
            return false;
225✔
3361
        }
3362
        return tokenKinds.includes(nextKind);
137,029✔
3363
    }
3364

3365
    private checkNext(tokenKind: TokenKind): boolean {
3366
        if (this.isAtEnd()) {
12,119!
UNCOV
3367
            return false;
×
3368
        }
3369
        return this.peekNext().kind === tokenKind;
12,119✔
3370
    }
3371

3372
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3373
        if (this.isAtEnd()) {
5,566!
UNCOV
3374
            return false;
×
3375
        }
3376
        const nextKind = this.peekNext().kind;
5,566✔
3377
        return tokenKinds.includes(nextKind);
5,566✔
3378
    }
3379

3380
    private isAtEnd(): boolean {
3381
        const peekToken = this.peek();
136,734✔
3382
        return !peekToken || peekToken.kind === TokenKind.Eof;
136,734✔
3383
    }
3384

3385
    private peekNext(): Token {
3386
        if (this.isAtEnd()) {
17,685!
UNCOV
3387
            return this.peek();
×
3388
        }
3389
        return this.tokens[this.current + 1];
17,685✔
3390
    }
3391

3392
    private peek(): Token {
3393
        return this.tokens[this.current];
1,193,335✔
3394
    }
3395

3396
    private previous(): Token {
3397
        return this.tokens[this.current - 1];
79,814✔
3398
    }
3399

3400
    /**
3401
     * Sometimes we catch an error that is a diagnostic.
3402
     * If that's the case, we want to continue parsing.
3403
     * Otherwise, re-throw the error
3404
     *
3405
     * @param error error caught in a try/catch
3406
     */
3407
    private rethrowNonDiagnosticError(error) {
3408
        if (!error.isDiagnostic) {
11!
UNCOV
3409
            throw error;
×
3410
        }
3411
    }
3412

3413
    /**
3414
     * Get the token that is {offset} indexes away from {this.current}
3415
     * @param offset the number of index steps away from current index to fetch
3416
     * @param tokenKinds the desired token must match one of these
3417
     * @example
3418
     * getToken(-1); //returns the previous token.
3419
     * getToken(0);  //returns current token.
3420
     * getToken(1);  //returns next token
3421
     */
3422
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3423
        const token = this.tokens[this.current + offset];
142✔
3424
        if (tokenKinds.includes(token.kind)) {
142✔
3425
            return token;
3✔
3426
        }
3427
    }
3428

3429
    private synchronize() {
3430
        this.advance(); // skip the erroneous token
82✔
3431

3432
        while (!this.isAtEnd()) {
82✔
3433
            if (this.ensureNewLineOrColon(true)) {
180✔
3434
                // end of statement reached
3435
                return;
57✔
3436
            }
3437

3438
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
123✔
3439
                case TokenKind.Namespace:
2!
3440
                case TokenKind.Class:
3441
                case TokenKind.Function:
3442
                case TokenKind.Sub:
3443
                case TokenKind.If:
3444
                case TokenKind.For:
3445
                case TokenKind.ForEach:
3446
                case TokenKind.While:
3447
                case TokenKind.Print:
3448
                case TokenKind.Return:
3449
                    // start parsing again from the next block starter or obvious
3450
                    // expression start
3451
                    return;
1✔
3452
            }
3453

3454
            this.advance();
122✔
3455
        }
3456
    }
3457

3458

3459
    public dispose() {
3460
    }
3461
}
3462

3463
export enum ParseMode {
1✔
3464
    BrightScript = 'BrightScript',
1✔
3465
    BrighterScript = 'BrighterScript'
1✔
3466
}
3467

3468
export interface ParseOptions {
3469
    /**
3470
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3471
     */
3472
    mode?: ParseMode;
3473
    /**
3474
     * A logger that should be used for logging. If omitted, a default logger is used
3475
     */
3476
    logger?: Logger;
3477
    /**
3478
     * Path to the file where this source code originated
3479
     */
3480
    srcPath?: string;
3481
    /**
3482
     * Should locations be tracked. If false, the `range` property will be omitted
3483
     * @default true
3484
     */
3485
    trackLocations?: boolean;
3486
    /**
3487
     *
3488
     */
3489
    bsConsts?: Map<string, boolean>;
3490
}
3491

3492

3493
class CancelStatementError extends Error {
3494
    constructor() {
3495
        super('CancelStatement');
2✔
3496
    }
3497
}
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