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

rokucommunity / brighterscript / #15903

11 May 2026 06:41PM UTC coverage: 86.896% (-2.2%) from 89.094%
#15903

push

web-flow
Merge 70dfd6181 into ce68f5cb7

15597 of 18958 branches covered (82.27%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 3 files covered. (100.0%)

955 existing lines in 53 files now uncovered.

16351 of 17808 relevant lines covered (91.82%)

27326.16 hits per line

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

91.31
/src/parser/Parser.ts
1
import type { Token, Identifier } from '../lexer/Token';
2
import { isToken } from '../lexer/Token';
1✔
3
import type { BlockTerminator, PrintSeparatorToken } 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
    BinaryExpressionOperatorTokens
19
} from '../lexer/TokenKind';
20
import {
1✔
21
    AliasStatement,
22
    AssignmentStatement,
23
    Block,
24
    Body,
25
    CatchStatement,
26
    ContinueStatement,
27
    ClassStatement,
28
    ConstStatement,
29
    ConditionalCompileStatement,
30
    DimStatement,
31
    DottedSetStatement,
32
    EndStatement,
33
    EnumMemberStatement,
34
    EnumStatement,
35
    ExitStatement,
36
    ExpressionStatement,
37
    ForEachStatement,
38
    FieldStatement,
39
    ForStatement,
40
    FunctionStatement,
41
    GotoStatement,
42
    IfStatement,
43
    ImportStatement,
44
    IncrementStatement,
45
    IndexedSetStatement,
46
    InterfaceFieldStatement,
47
    InterfaceMethodStatement,
48
    InterfaceStatement,
49
    LabelStatement,
50
    LibraryStatement,
51
    MethodStatement,
52
    NamespaceStatement,
53
    PrintStatement,
54
    ReturnStatement,
55
    StopStatement,
56
    ThrowStatement,
57
    TryCatchStatement,
58
    WhileStatement,
59
    TypecastStatement,
60
    ConditionalCompileConstStatement,
61
    ConditionalCompileErrorStatement,
62
    AugmentedAssignmentStatement,
63
    TypeStatement
64
} from './Statement';
65
import type { DiagnosticInfo } from '../DiagnosticMessages';
66
import { DiagnosticMessages } from '../DiagnosticMessages';
1✔
67
import { util } from '../util';
1✔
68
import {
1✔
69
    AAIndexedMemberExpression,
70
    AALiteralExpression,
71
    AAMemberExpression,
72
    AnnotationExpression,
73
    ArrayLiteralExpression,
74
    BinaryExpression,
75
    CallExpression,
76
    CallfuncExpression,
77
    DottedGetExpression,
78
    EscapedCharCodeLiteralExpression,
79
    FunctionExpression,
80
    FunctionParameterExpression,
81
    GroupingExpression,
82
    IndexedGetExpression,
83
    LiteralExpression,
84
    NewExpression,
85
    NullCoalescingExpression,
86
    RegexLiteralExpression,
87
    SourceLiteralExpression,
88
    TaggedTemplateStringExpression,
89
    TemplateStringExpression,
90
    TemplateStringQuasiExpression,
91
    TernaryExpression,
92
    TypecastExpression,
93
    TypeExpression,
94
    TypedArrayExpression,
95
    UnaryExpression,
96
    VariableExpression,
97
    XmlAttributeGetExpression,
98
    PrintSeparatorExpression,
99
    InlineInterfaceExpression,
100
    InlineInterfaceMemberExpression,
101
    TypedFunctionTypeExpression
102
} from './Expression';
103
import type { Range } from 'vscode-languageserver';
104
import type { Logger } from '../logging';
105
import { createLogger } from '../logging';
1✔
106
import { isAnnotationExpression, isCallExpression, isCallfuncExpression, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression, isConditionalCompileStatement, isLiteralBoolean, isTypecastExpression, isXmlAttributeGetExpression } from '../astUtils/reflection';
1✔
107
import { createStringLiteral, createToken } from '../astUtils/creators';
1✔
108
import type { Expression, Statement } from './AstNode';
109
import type { BsDiagnostic, DeepWriteable } from '../interfaces';
110
import * as semver from 'semver';
1✔
111

112
const declarableTypesLower = DeclarableTypes.map(tokenKind => tokenKind.toLowerCase());
10✔
113

114
export class Parser {
1✔
115
    /**
116
     * The minimum Roku firmware version that added native support for multi-line expressions
117
     * (line continuation) in plain BrightScript (`.brs`) files.
118
     */
119
    private static readonly LINE_CONTINUATION_MIN_FIRMWARE_VERSION = '15.3.0';
1✔
120

121
    /**
122
     * The array of tokens passed to `parse()`
123
     */
124
    public tokens = [] as Token[];
4,634✔
125

126
    /**
127
     * The current token index
128
     */
129
    public current: number;
130

131
    /**
132
     * The list of statements for the parsed file
133
     */
134
    public ast = new Body({ statements: [] });
4,634✔
135

136
    public get eofToken(): Token {
137
        const lastToken = this.tokens?.[this.tokens.length - 1];
864!
138
        if (lastToken?.kind === TokenKind.Eof) {
864!
139
            return lastToken;
864✔
140
        }
141
    }
142

143
    /**
144
     * The top-level symbol table for the body of this file.
145
     */
146
    public get symbolTable() {
147
        return this.ast.symbolTable;
22,359✔
148
    }
149

150
    /**
151
     * The list of diagnostics found during the parse process
152
     */
153
    public diagnostics: BsDiagnostic[];
154

155
    /**
156
     * The depth of the calls to function declarations. Helps some checks know if they are at the root or not.
157
     */
158
    private namespaceAndFunctionDepth: number;
159

160
    /**
161
     * The options used to parse the file
162
     */
163
    public options: ParseOptions;
164

165
    /**
166
     * Whether line continuation after binary operators is allowed.
167
     * Enabled in BrighterScript mode, or when minFirmwareVersion >= 15.3.
168
     */
169
    private allowLineContinuation: boolean;
170

171
    /**
172
     * If line continuation is enabled, consumes all immediately following Newline tokens.
173
     * Call this after matching a binary operator to allow the right-hand operand on the next line.
174
     */
175
    private consumeNewlinesIfAllowed() {
176
        if (this.allowLineContinuation) {
12,765✔
177
            while (this.match(TokenKind.Newline)) { }
3,512✔
178
        }
179
    }
180

181
    private globalTerminators = [] as TokenKind[][];
4,634✔
182

183
    /**
184
     * 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
185
     * based on the parse mode
186
     */
187
    private allowedLocalIdentifiers: TokenKind[];
188

189
    /**
190
     * Annotations collected which should be attached to the next statement
191
     */
192
    private pendingAnnotations: AnnotationExpression[];
193

194
    /**
195
     * Get the currently active global terminators
196
     */
197
    private peekGlobalTerminators() {
198
        return this.globalTerminators[this.globalTerminators.length - 1] ?? [];
21,863✔
199
    }
200

201
    /**
202
     * Static wrapper around creating a new parser and parsing a list of tokens
203
     */
204
    public static parse(toParse: Token[] | string, options?: ParseOptions): Parser {
205
        return new Parser().parse(toParse, options);
4,595✔
206
    }
207

208
    /**
209
     * Parses an array of `Token`s into an abstract syntax tree
210
     * @param toParse the array of tokens to parse. May not contain any whitespace tokens
211
     * @returns the same instance of the parser which contains the diagnostics and statements
212
     */
213
    public parse(toParse: Token[] | string, options?: ParseOptions) {
214
        this.logger = options?.logger ?? createLogger();
4,602✔
215
        options = this.sanitizeParseOptions(options);
4,602✔
216
        this.options = options;
4,602✔
217
        const coercedMinFirmwareVersion = semver.coerce(this.options.minFirmwareVersion);
4,602✔
218
        this.allowLineContinuation = options.mode === ParseMode.BrighterScript || (!!coercedMinFirmwareVersion && semver.gte(coercedMinFirmwareVersion, Parser.LINE_CONTINUATION_MIN_FIRMWARE_VERSION));
4,602✔
219

220
        let tokens: Token[];
221
        if (typeof toParse === 'string') {
4,602✔
222
            tokens = Lexer.scan(toParse, {
623✔
223
                trackLocations: options.trackLocations,
224
                srcPath: options?.srcPath
1,869!
225
            }).tokens;
226
        } else {
227
            tokens = toParse;
3,979✔
228
        }
229
        this.tokens = tokens;
4,602✔
230
        this.allowedLocalIdentifiers = [
4,602✔
231
            ...AllowedLocalIdentifiers,
232
            //when in plain brightscript mode, the BrighterScript source literals can be used as regular variables
233
            ...(this.options.mode === ParseMode.BrightScript ? BrighterScriptSourceLiterals : [])
4,602✔
234
        ];
235
        this.current = 0;
4,602✔
236
        this.diagnostics = [];
4,602✔
237
        this.namespaceAndFunctionDepth = 0;
4,602✔
238
        this.pendingAnnotations = [];
4,602✔
239

240
        this.ast = this.body();
4,602✔
241
        this.ast.bsConsts = options.bsConsts;
4,602✔
242
        //now that we've built the AST, link every node to its parent
243
        this.ast.link();
4,602✔
244
        return this;
4,602✔
245
    }
246

247
    private logger: Logger;
248

249
    private body() {
250
        const parentAnnotations = this.enterAnnotationBlock();
5,359✔
251

252
        let body = new Body({ statements: [] });
5,359✔
253
        if (this.tokens.length > 0) {
5,359✔
254
            this.consumeStatementSeparators(true);
5,358✔
255

256
            try {
5,358✔
257
                while (
5,358✔
258
                    //not at end of tokens
259
                    !this.isAtEnd() &&
21,693✔
260
                    //the next token is not one of the end terminators
261
                    !this.checkAny(...this.peekGlobalTerminators())
262
                ) {
263
                    let dec = this.declaration();
7,790✔
264
                    if (dec) {
7,790✔
265
                        if (!isAnnotationExpression(dec)) {
7,725✔
266
                            this.consumePendingAnnotations(dec);
7,674✔
267
                            body.statements.push(dec);
7,674✔
268
                            //ensure statement separator
269
                            this.consumeStatementSeparators(false);
7,674✔
270
                        } else {
271
                            this.consumeStatementSeparators(true);
51✔
272
                        }
273
                    }
274
                }
275
            } catch (parseError) {
276
                //do nothing with the parse error for now. perhaps we can remove this?
UNCOV
277
                console.error(parseError);
×
278
            }
279
        }
280

281
        this.exitAnnotationBlock(parentAnnotations);
5,359✔
282
        return body;
5,359✔
283
    }
284

285
    private sanitizeParseOptions(options: ParseOptions) {
286
        options ??= {
4,602✔
287
            srcPath: undefined
288
        };
289
        options.mode ??= ParseMode.BrightScript;
4,602✔
290
        options.trackLocations ??= true;
4,602✔
291
        return options;
4,602✔
292
    }
293

294
    /**
295
     * Determine if the parser is currently parsing tokens at the root level.
296
     */
297
    private isAtRootLevel() {
298
        return this.namespaceAndFunctionDepth === 0;
80,083✔
299
    }
300

301
    /**
302
     * Throws an error if the input file type is not BrighterScript
303
     */
304
    private warnIfNotBrighterScriptMode(featureName: string) {
305
        if (this.options.mode !== ParseMode.BrighterScript) {
3,258✔
306
            let diagnostic = {
180✔
307
                ...DiagnosticMessages.bsFeatureNotSupportedInBrsFiles(featureName),
308
                location: this.peek().location
309
            };
310
            this.diagnostics.push(diagnostic);
180✔
311
        }
312
    }
313

314
    /**
315
     * Throws an exception using the last diagnostic message
316
     */
317
    private lastDiagnosticAsError() {
318
        let error = new Error(this.diagnostics[this.diagnostics.length - 1]?.message ?? 'Unknown error');
125!
319
        (error as any).isDiagnostic = true;
125✔
320
        return error;
125✔
321
    }
322

323
    private declaration(): Statement | AnnotationExpression | undefined {
324
        try {
18,043✔
325
            if (this.checkAny(TokenKind.HashConst)) {
18,043✔
326
                return this.conditionalCompileConstStatement();
21✔
327
            }
328
            if (this.checkAny(TokenKind.HashIf)) {
18,022✔
329
                return this.conditionalCompileStatement();
43✔
330
            }
331
            if (this.checkAny(TokenKind.HashError)) {
17,979✔
332
                return this.conditionalCompileErrorStatement();
10✔
333
            }
334

335
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
17,969✔
336
                return this.functionDeclaration(false);
4,375✔
337
            }
338

339
            if (this.checkLibrary()) {
13,594✔
340
                return this.libraryStatement();
13✔
341
            }
342

343
            if (this.checkAlias()) {
13,581✔
344
                return this.aliasStatement();
33✔
345
            }
346
            if (this.checkTypeStatement()) {
13,548✔
347
                return this.typeStatement();
55✔
348
            }
349

350
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
13,493✔
351
                return this.constDeclaration();
225✔
352
            }
353

354
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
13,268✔
355
                return this.annotationExpression();
58✔
356
            }
357

358
            //catch certain global terminators to prevent unnecessary lookahead (i.e. like `end namespace`, no need to continue)
359
            if (this.checkAny(...this.peekGlobalTerminators())) {
13,210!
UNCOV
360
                return;
×
361
            }
362

363
            return this.statement();
13,210✔
364
        } catch (error: any) {
365
            //if the error is not a diagnostic, then log the error for debugging purposes
366
            if (!error.isDiagnostic) {
121!
UNCOV
367
                this.logger.error(error);
×
368
            }
369
            this.synchronize();
121✔
370
        }
371
    }
372

373
    /**
374
     * Try to get an identifier. If not found, add diagnostic and return undefined
375
     */
376
    private tryIdentifier(...additionalTokenKinds: TokenKind[]): Identifier | undefined {
377
        const identifier = this.tryConsume(
202✔
378
            DiagnosticMessages.expectedIdentifier(),
379
            TokenKind.Identifier,
380
            ...additionalTokenKinds
381
        ) as Identifier;
382
        if (identifier) {
202✔
383
            // force the name into an identifier so the AST makes some sense
384
            identifier.kind = TokenKind.Identifier;
201✔
385
            return identifier;
201✔
386
        }
387
    }
388

389
    private identifier(...additionalTokenKinds: TokenKind[]) {
390
        const identifier = this.consume(
1,243✔
391
            DiagnosticMessages.expectedIdentifier(),
392
            TokenKind.Identifier,
393
            ...additionalTokenKinds
394
        ) as Identifier;
395
        // force the name into an identifier so the AST makes some sense
396
        identifier.kind = TokenKind.Identifier;
1,243✔
397
        return identifier;
1,243✔
398
    }
399

400
    private enumMemberStatement() {
401
        const name = this.consume(
388✔
402
            DiagnosticMessages.expectedIdentifier(),
403
            TokenKind.Identifier,
404
            ...AllowedProperties
405
        ) as Identifier;
406
        let equalsToken: Token;
407
        let value: Expression;
408
        //look for `= SOME_EXPRESSION`
409
        if (this.check(TokenKind.Equal)) {
388✔
410
            equalsToken = this.advance();
227✔
411
            value = this.expression();
227✔
412
        }
413
        const statement = new EnumMemberStatement({ name: name, equals: equalsToken, value: value });
388✔
414
        return statement;
388✔
415
    }
416

417
    /**
418
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration`
419
     */
420
    private interfaceFieldStatement(optionalKeyword?: Token) {
421
        const name = this.identifier(...AllowedProperties);
293✔
422
        let asToken;
423
        let typeExpression;
424
        if (this.check(TokenKind.As)) {
293✔
425
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
274✔
426
        }
427
        return new InterfaceFieldStatement({ name: name, as: asToken, typeExpression: typeExpression, optional: optionalKeyword });
293✔
428
    }
429

430
    private consumeAsTokenAndTypeExpression(ignoreDiagnostics = false): [Token, TypeExpression] {
2,279✔
431
        let asToken = this.consumeToken(TokenKind.As);
2,298✔
432
        let typeExpression: TypeExpression;
433
        if (asToken) {
2,298!
434
            //if there's nothing after the `as`, add a diagnostic and continue
435
            if (this.checkEndOfStatement()) {
2,298✔
436
                if (!ignoreDiagnostics) {
2✔
437
                    this.diagnostics.push({
1✔
438
                        ...DiagnosticMessages.expectedIdentifier(asToken.text),
439
                        location: asToken.location
440
                    });
441
                }
442
                //consume the statement separator
443
                this.consumeStatementSeparators();
2✔
444
            } else if (!this.checkAny(TokenKind.Identifier, TokenKind.LeftCurlyBrace, TokenKind.LeftParen, ...DeclarableTypes, ...AllowedTypeIdentifiers)) {
2,296✔
445
                if (!ignoreDiagnostics) {
9!
446
                    this.diagnostics.push({
9✔
447
                        ...DiagnosticMessages.expectedIdentifier(asToken.text),
448
                        location: asToken.location
449
                    });
450
                }
451
            } else {
452
                typeExpression = this.typeExpression();
2,287✔
453
            }
454
        }
455
        if (this.checkAny(TokenKind.And, TokenKind.Or)) {
2,296✔
456
            this.warnIfNotBrighterScriptMode('custom types');
2✔
457
        }
458
        return [asToken, typeExpression];
2,296✔
459
    }
460

461
    /**
462
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration()`
463
     */
464
    private interfaceMethodStatement(optionalKeyword?: Token) {
465
        const functionType = this.advance();
53✔
466
        const name = this.identifier(...AllowedProperties);
53✔
467
        const leftParen = this.consume(DiagnosticMessages.expectedToken(TokenKind.LeftParen), TokenKind.LeftParen);
53✔
468

469
        let params = [] as FunctionParameterExpression[];
53✔
470
        if (!this.check(TokenKind.RightParen)) {
53✔
471
            do {
18✔
472
                if (params.length >= CallExpression.MaximumArguments) {
28!
UNCOV
473
                    this.diagnostics.push({
×
474
                        ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments),
475
                        location: this.peek().location
476
                    });
477
                }
478

479
                params.push(this.functionParameter());
28✔
480
            } while (this.match(TokenKind.Comma));
481
        }
482
        const rightParen = this.consumeToken(TokenKind.RightParen);
53✔
483
        // let asToken = null as Token;
484
        // let returnTypeExpression: TypeExpression;
485
        let asToken: Token;
486
        let returnTypeExpression: TypeExpression;
487
        if (this.check(TokenKind.As)) {
53✔
488
            [asToken, returnTypeExpression] = this.consumeAsTokenAndTypeExpression();
40✔
489
        }
490

491
        return new InterfaceMethodStatement({
53✔
492
            functionType: functionType,
493
            name: name,
494
            leftParen: leftParen,
495
            params: params,
496
            rightParen: rightParen,
497
            as: asToken,
498
            returnTypeExpression: returnTypeExpression,
499
            optional: optionalKeyword
500
        });
501
    }
502

503
    private interfaceDeclaration(): InterfaceStatement {
504
        this.warnIfNotBrighterScriptMode('interface declarations');
247✔
505

506
        const parentAnnotations = this.enterAnnotationBlock();
247✔
507

508
        const interfaceToken = this.consume(
247✔
509
            DiagnosticMessages.expectedKeyword(TokenKind.Interface),
510
            TokenKind.Interface
511
        );
512
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
247✔
513

514
        let extendsToken: Token;
515
        let parentInterfaceName: TypeExpression;
516

517
        if (this.peek().text.toLowerCase() === 'extends') {
247✔
518
            extendsToken = this.advance();
11✔
519
            if (this.checkEndOfStatement()) {
11!
UNCOV
520
                this.diagnostics.push({
×
521
                    ...DiagnosticMessages.expectedIdentifier(extendsToken.text),
522
                    location: extendsToken.location
523
                });
524
            } else {
525
                parentInterfaceName = this.typeExpression();
11✔
526
            }
527
        }
528
        this.consumeStatementSeparators();
247✔
529
        //gather up all interface members (Fields, Methods)
530
        let body = [] as Statement[];
247✔
531
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
247✔
532
            try {
595✔
533
                //break out of this loop if we encountered the `EndInterface` token not followed by `as`
534
                if (this.check(TokenKind.EndInterface) && !this.checkNext(TokenKind.As)) {
595✔
535
                    break;
247✔
536
                }
537

538
                let decl: Statement;
539

540
                //collect leading annotations
541
                if (this.check(TokenKind.At)) {
348✔
542
                    this.annotationExpression();
2✔
543
                }
544
                const optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
348✔
545
                //fields
546
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkAnyNext(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
348✔
547
                    decl = this.interfaceFieldStatement(optionalKeyword);
291✔
548
                    //field with name = 'optional'
549
                } else if (optionalKeyword && this.checkAny(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
57✔
550
                    //rewind one place, so that 'optional' is the field name
551
                    this.current--;
2✔
552
                    decl = this.interfaceFieldStatement();
2✔
553

554
                    //methods (function/sub keyword followed by opening paren)
555
                } else if (this.checkAny(TokenKind.Function, TokenKind.Sub) && this.checkAnyNext(TokenKind.Identifier, ...AllowedProperties)) {
55✔
556
                    decl = this.interfaceMethodStatement(optionalKeyword);
53✔
557

558
                }
559
                if (decl) {
348✔
560
                    this.consumePendingAnnotations(decl);
346✔
561
                    body.push(decl);
346✔
562
                } else {
563
                    //we didn't find a declaration...flag tokens until next line
564
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
565
                }
566
            } catch (e) {
567
                //throw out any failed members and move on to the next line
UNCOV
568
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
569
            }
570

571
            //ensure statement separator
572
            this.consumeStatementSeparators();
348✔
573
        }
574

575
        //consume the final `end interface` token
576
        const endInterfaceToken = this.consumeToken(TokenKind.EndInterface);
247✔
577

578
        const statement = new InterfaceStatement({
247✔
579
            interface: interfaceToken,
580
            name: nameToken,
581
            extends: extendsToken,
582
            parentInterfaceName: parentInterfaceName,
583
            body: body,
584
            endInterface: endInterfaceToken
585
        });
586
        this.exitAnnotationBlock(parentAnnotations);
247✔
587
        return statement;
247✔
588
    }
589

590
    private enumDeclaration(): EnumStatement {
591
        const enumToken = this.consume(
202✔
592
            DiagnosticMessages.expectedKeyword(TokenKind.Enum),
593
            TokenKind.Enum
594
        );
595
        const nameToken = this.tryIdentifier(...this.allowedLocalIdentifiers);
202✔
596

597
        this.warnIfNotBrighterScriptMode('enum declarations');
202✔
598

599
        const parentAnnotations = this.enterAnnotationBlock();
202✔
600

601
        this.consumeStatementSeparators();
202✔
602

603
        const body: Array<EnumMemberStatement> = [];
202✔
604
        //gather up all members
605
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
202✔
606
            try {
388✔
607
                let decl: EnumMemberStatement;
608

609
                //collect leading annotations
610
                if (this.check(TokenKind.At)) {
388!
UNCOV
611
                    this.annotationExpression();
×
612
                }
613

614
                //members
615
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
388!
616
                    decl = this.enumMemberStatement();
388✔
617
                }
618

619
                if (decl) {
388!
620
                    this.consumePendingAnnotations(decl);
388✔
621
                    body.push(decl);
388✔
622
                } else {
623
                    //we didn't find a declaration...flag tokens until next line
624
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
625
                }
626
            } catch (e) {
627
                //throw out any failed members and move on to the next line
628
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
629
            }
630

631
            //ensure statement separator
632
            this.consumeStatementSeparators();
388✔
633
            //break out of this loop if we encountered the `EndEnum` token
634
            if (this.check(TokenKind.EndEnum)) {
388✔
635
                break;
190✔
636
            }
637
        }
638

639
        //consume the final `end interface` token
640
        const endEnumToken = this.consumeToken(TokenKind.EndEnum);
202✔
641

642
        const result = new EnumStatement({
201✔
643
            enum: enumToken,
644
            name: nameToken,
645
            body: body,
646
            endEnum: endEnumToken
647
        });
648

649
        this.exitAnnotationBlock(parentAnnotations);
201✔
650
        return result;
201✔
651
    }
652

653
    /**
654
     * A BrighterScript class declaration
655
     */
656
    private classDeclaration(): ClassStatement {
657
        this.warnIfNotBrighterScriptMode('class declarations');
745✔
658

659
        const parentAnnotations = this.enterAnnotationBlock();
745✔
660

661
        let classKeyword = this.consume(
745✔
662
            DiagnosticMessages.expectedKeyword(TokenKind.Class),
663
            TokenKind.Class
664
        );
665
        let extendsKeyword: Token;
666
        let parentClassName: TypeExpression;
667

668
        //get the class name
669
        let className = this.tryConsume(DiagnosticMessages.expectedIdentifier('class'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
745✔
670

671
        //see if the class inherits from parent
672
        if (this.peek().text.toLowerCase() === 'extends') {
745✔
673
            extendsKeyword = this.advance();
110✔
674
            if (this.checkEndOfStatement()) {
110✔
675
                this.diagnostics.push({
1✔
676
                    ...DiagnosticMessages.expectedIdentifier(extendsKeyword.text),
677
                    location: extendsKeyword.location
678
                });
679
            } else {
680
                parentClassName = this.typeExpression();
109✔
681
            }
682
        }
683

684
        //ensure statement separator
685
        this.consumeStatementSeparators();
745✔
686

687
        //gather up all class members (Fields, Methods)
688
        let body = [] as Statement[];
745✔
689
        while (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private, TokenKind.Function, TokenKind.Sub, TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
745✔
690
            try {
763✔
691
                let decl: Statement;
692
                let accessModifier: Token;
693

694
                if (this.check(TokenKind.At)) {
763✔
695
                    this.annotationExpression();
15✔
696
                }
697

698
                if (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private)) {
762✔
699
                    //use actual access modifier
700
                    accessModifier = this.advance();
98✔
701
                }
702

703
                let overrideKeyword: Token;
704
                if (this.peek().text.toLowerCase() === 'override') {
762✔
705
                    overrideKeyword = this.advance();
19✔
706
                }
707
                //methods (function/sub keyword OR identifier followed by opening paren)
708
                if (this.checkAny(TokenKind.Function, TokenKind.Sub) || (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.LeftParen))) {
762✔
709
                    const funcDeclaration = this.functionDeclaration(false, false);
385✔
710

711
                    //if we have an overrides keyword AND this method is called 'new', that's not allowed
712
                    if (overrideKeyword && funcDeclaration.tokens.name.text.toLowerCase() === 'new') {
385✔
713
                        this.diagnostics.push({
2✔
714
                            ...DiagnosticMessages.cannotUseOverrideKeywordOnConstructorFunction(),
715
                            location: overrideKeyword.location
716
                        });
717
                    }
718

719
                    decl = new MethodStatement({
385✔
720
                        modifiers: accessModifier,
721
                        name: funcDeclaration.tokens.name,
722
                        func: funcDeclaration.func,
723
                        override: overrideKeyword
724
                    });
725

726
                    //fields
727
                } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
377✔
728

729
                    decl = this.fieldDeclaration(accessModifier);
363✔
730

731
                    //class fields cannot be overridden
732
                    if (overrideKeyword) {
362!
UNCOV
733
                        this.diagnostics.push({
×
734
                            ...DiagnosticMessages.classFieldCannotBeOverridden(),
735
                            location: overrideKeyword.location
736
                        });
737
                    }
738

739
                }
740

741
                if (decl) {
761✔
742
                    this.consumePendingAnnotations(decl);
747✔
743
                    body.push(decl);
747✔
744
                }
745
            } catch (e) {
746
                //throw out any failed members and move on to the next line
747
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
748
            }
749

750
            //ensure statement separator
751
            this.consumeStatementSeparators();
763✔
752
        }
753

754
        let endingKeyword = this.advance();
745✔
755
        if (endingKeyword.kind !== TokenKind.EndClass) {
745✔
756
            this.diagnostics.push({
5✔
757
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('class'),
758
                location: endingKeyword.location
759
            });
760
        }
761

762
        const result = new ClassStatement({
745✔
763
            class: classKeyword,
764
            name: className,
765
            body: body,
766
            endClass: endingKeyword,
767
            extends: extendsKeyword,
768
            parentClassName: parentClassName
769
        });
770

771
        this.exitAnnotationBlock(parentAnnotations);
745✔
772
        return result;
745✔
773
    }
774

775
    private fieldDeclaration(accessModifier: Token | null) {
776

777
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
363✔
778

779
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
363✔
780
            if (this.check(TokenKind.As)) {
362✔
781
                if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
5✔
782
                    // as <EOL>
783
                    // `as` is the field name
784
                } else if (this.checkNext(TokenKind.As)) {
4✔
785
                    //  as as ____
786
                    // first `as` is the field name
787
                } else if (optionalKeyword) {
2!
788
                    // optional as ____
789
                    // optional is the field name, `as` starts type
790
                    // rewind current token
791
                    optionalKeyword = null;
2✔
792
                    this.current--;
2✔
793
                }
794
            }
795
        } else {
796
            // no name after `optional` ... optional is the name
797
            // rewind current token
798
            optionalKeyword = null;
1✔
799
            this.current--;
1✔
800
        }
801

802
        let name = this.consume(
363✔
803
            DiagnosticMessages.expectedIdentifier(),
804
            TokenKind.Identifier,
805
            ...AllowedProperties
806
        ) as Identifier;
807

808
        let asToken: Token;
809
        let fieldTypeExpression: TypeExpression;
810
        //look for `as SOME_TYPE`
811
        if (this.check(TokenKind.As)) {
363✔
812
            [asToken, fieldTypeExpression] = this.consumeAsTokenAndTypeExpression();
252✔
813
        }
814

815
        let initialValue: Expression;
816
        let equal: Token;
817
        //if there is a field initializer
818
        if (this.check(TokenKind.Equal)) {
363✔
819
            equal = this.advance();
81✔
820
            initialValue = this.expression();
81✔
821
        }
822

823
        return new FieldStatement({
362✔
824
            accessModifier: accessModifier,
825
            name: name,
826
            as: asToken,
827
            typeExpression: fieldTypeExpression,
828
            equals: equal,
829
            initialValue: initialValue,
830
            optional: optionalKeyword
831
        });
832
    }
833

834
    /**
835
     * An array of CallExpression for the current function body
836
     */
837
    private callExpressions = [];
4,634✔
838

839
    private functionDeclaration(isAnonymous: true, checkIdentifier?: boolean, onlyCallableAsMember?: boolean): FunctionExpression;
840
    private functionDeclaration(isAnonymous: false, checkIdentifier?: boolean, onlyCallableAsMember?: boolean): FunctionStatement;
841
    private functionDeclaration(isAnonymous: boolean, checkIdentifier = true, onlyCallableAsMember = false) {
9,363✔
842
        let previousCallExpressions = this.callExpressions;
4,874✔
843
        this.callExpressions = [];
4,874✔
844
        try {
4,874✔
845
            //track depth to help certain statements need to know if they are contained within a function body
846
            this.namespaceAndFunctionDepth++;
4,874✔
847
            let functionType: Token;
848
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
4,874✔
849
                functionType = this.advance();
4,872✔
850
            } else {
851
                this.diagnostics.push({
2✔
852
                    ...DiagnosticMessages.missingCallableKeyword(),
853
                    location: this.peek().location
854
                });
855
                //TODO we should probably eliminate this entirely, since it's not present in the source code
856
                functionType = {
2✔
857
                    isReserved: true,
858
                    kind: TokenKind.Function,
859
                    text: 'function',
860
                    //zero-length location means derived
861
                    location: this.peek().location,
862
                    leadingWhitespace: '',
863
                    leadingTrivia: []
864
                };
865
            }
866
            let isSub = functionType?.kind === TokenKind.Sub;
4,874!
867
            let functionTypeText = isSub ? 'sub' : 'function';
4,874✔
868
            let name: Identifier;
869
            let leftParen: Token;
870

871
            if (isAnonymous) {
4,874✔
872
                leftParen = this.consume(
114✔
873
                    DiagnosticMessages.expectedToken('('),
874
                    TokenKind.LeftParen
875
                );
876
            } else {
877
                name = this.consume(
4,760✔
878
                    DiagnosticMessages.expectedIdentifier(functionTypeText),
879
                    TokenKind.Identifier,
880
                    ...AllowedProperties
881
                ) as Identifier;
882
                leftParen = this.consume(
4,758✔
883
                    DiagnosticMessages.expectedToken('('),
884
                    TokenKind.LeftParen
885
                );
886

887
                //prevent functions from ending with type designators
888
                let lastChar = name.text[name.text.length - 1];
4,755✔
889
                if (['$', '%', '!', '#', '&'].includes(lastChar)) {
4,755✔
890
                    //don't throw this error; let the parser continue
891
                    this.diagnostics.push({
13✔
892
                        ...DiagnosticMessages.invalidIdentifier(name.text, lastChar),
893
                        location: name.location
894
                    });
895
                }
896

897
                //flag functions with keywords for names (only for standard functions)
898
                if (checkIdentifier && DisallowedFunctionIdentifiersText.has(name.text.toLowerCase())) {
4,755✔
899
                    this.diagnostics.push({
3✔
900
                        ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
901
                        location: name.location
902
                    });
903
                }
904
            }
905

906
            let params = [] as FunctionParameterExpression[];
4,868✔
907
            let asToken: Token;
908
            let typeExpression: TypeExpression;
909
            if (!this.check(TokenKind.RightParen)) {
4,868✔
910
                do {
2,328✔
911
                    params.push(this.functionParameter());
3,978✔
912
                } while (this.match(TokenKind.Comma));
913
            }
914
            let rightParen = this.consume(
4,865✔
915
                DiagnosticMessages.unmatchedLeftToken(leftParen.text, 'function parameter list'),
916
                TokenKind.RightParen
917
            );
918
            if (this.check(TokenKind.As)) {
4,856✔
919
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
461✔
920
            }
921

922
            params.reduce((haveFoundOptional: boolean, param: FunctionParameterExpression) => {
4,856✔
923
                if (haveFoundOptional && !param.defaultValue) {
3,965!
UNCOV
924
                    this.diagnostics.push({
×
925
                        ...DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter(param.tokens.name.text),
926
                        location: param.location
927
                    });
928
                }
929

930
                return haveFoundOptional || !!param.defaultValue;
3,965✔
931
            }, false);
932

933
            this.consumeStatementSeparators(true);
4,856✔
934

935

936
            //support ending the function with `end sub` OR `end function`
937
            let body = this.block();
4,856✔
938
            //if the parser was unable to produce a block, make an empty one so the AST makes some sense...
939

940
            // consume 'end sub' or 'end function'
941
            const endFunctionType = this.advance();
4,856✔
942
            let expectedEndKind = isSub ? TokenKind.EndSub : TokenKind.EndFunction;
4,856✔
943

944
            //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
945
            //add an error but don't hard-fail so the AST can continue more gracefully
946
            if (endFunctionType.kind !== expectedEndKind) {
4,856✔
947
                this.diagnostics.push({
9✔
948
                    ...DiagnosticMessages.closingKeywordMismatch(functionTypeText, endFunctionType.text),
949
                    location: endFunctionType.location
950
                });
951
            }
952

953
            if (!body) {
4,856✔
954
                body = new Block({ statements: [] });
3✔
955
            }
956

957
            let func = new FunctionExpression({
4,856✔
958
                parameters: params,
959
                body: body,
960
                functionType: functionType,
961
                endFunctionType: endFunctionType,
962
                leftParen: leftParen,
963
                rightParen: rightParen,
964
                as: asToken,
965
                returnTypeExpression: typeExpression
966
            });
967

968
            if (isAnonymous) {
4,856✔
969
                return func;
113✔
970
            } else {
971
                let result = new FunctionStatement({ name: name, func: func });
4,743✔
972
                return result;
4,743✔
973
            }
974
        } finally {
975
            this.namespaceAndFunctionDepth--;
4,874✔
976
            //restore the previous CallExpression list
977
            this.callExpressions = previousCallExpressions;
4,874✔
978
        }
979
    }
980

981
    private functionParameter(): FunctionParameterExpression {
982
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
4,050✔
983
            this.diagnostics.push({
1✔
984
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
985
                location: this.peek().location
986
            });
987
            throw this.lastDiagnosticAsError();
1✔
988
        }
989

990
        let name = this.advance() as Identifier;
4,049✔
991
        // force the name into an identifier so the AST makes some sense
992
        name.kind = TokenKind.Identifier;
4,049✔
993

994
        //add diagnostic if name is a reserved word that cannot be used as an identifier
995
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
4,049✔
996
            this.diagnostics.push({
3✔
997
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
998
                location: name.location
999
            });
1000
        }
1001

1002
        let typeExpression: TypeExpression;
1003
        let defaultValue;
1004
        let equalToken: Token;
1005
        // parse argument default value
1006
        if ((equalToken = this.consumeTokenIf(TokenKind.Equal))) {
4,049✔
1007
            // it seems any expression is allowed here -- including ones that operate on other arguments!
1008
            defaultValue = this.expression(false);
385✔
1009
        }
1010

1011
        let asToken: Token = null;
4,049✔
1012
        if (this.check(TokenKind.As)) {
4,049✔
1013
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
989✔
1014

1015
        }
1016
        return new FunctionParameterExpression({
4,047✔
1017
            name: name,
1018
            equals: equalToken,
1019
            defaultValue: defaultValue,
1020
            as: asToken,
1021
            typeExpression: typeExpression
1022
        });
1023
    }
1024

1025
    private assignment(allowTypedAssignment = false): AssignmentStatement {
1,897✔
1026
        let name = this.advance() as Identifier;
1,915✔
1027
        //add diagnostic if name is a reserved word that cannot be used as an identifier
1028
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
1,915✔
1029
            this.diagnostics.push({
14✔
1030
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
1031
                location: name.location
1032
            });
1033
        }
1034
        let asToken: Token;
1035
        let typeExpression: TypeExpression;
1036

1037
        if (allowTypedAssignment) {
1,915✔
1038
            //look for `as SOME_TYPE`
1039
            if (this.check(TokenKind.As)) {
18!
1040
                this.warnIfNotBrighterScriptMode('typed assignment');
18✔
1041

1042
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
18✔
1043
            }
1044
        }
1045

1046
        let operator = this.consume(
1,915✔
1047
            DiagnosticMessages.expectedOperator([TokenKind.Equal], name.text),
1048
            ...[TokenKind.Equal]
1049
        );
1050
        let value = this.expression();
1,912✔
1051

1052
        let result = new AssignmentStatement({ equals: operator, name: name, value: value, as: asToken, typeExpression: typeExpression });
1,901✔
1053

1054
        return result;
1,901✔
1055
    }
1056

1057
    private augmentedAssignment(): AugmentedAssignmentStatement {
1058
        let item = this.expression();
76✔
1059

1060
        let operator = this.consume(
76✔
1061
            DiagnosticMessages.expectedToken(...CompoundAssignmentOperators),
1062
            ...CompoundAssignmentOperators
1063
        );
1064
        let value = this.expression();
76✔
1065

1066
        let result = new AugmentedAssignmentStatement({
76✔
1067
            item: item,
1068
            operator: operator,
1069
            value: value
1070
        });
1071

1072
        return result;
76✔
1073
    }
1074

1075
    private checkLibrary() {
1076
        let isLibraryToken = this.check(TokenKind.Library);
26,890✔
1077

1078
        //if we are at the top level, any line that starts with "library" should be considered a library statement
1079
        if (this.isAtRootLevel() && isLibraryToken) {
26,890✔
1080
            return true;
12✔
1081

1082
            //not at root level, library statements are all invalid here, but try to detect if the tokens look
1083
            //like a library statement (and let the libraryStatement function handle emitting the diagnostics)
1084
        } else if (isLibraryToken && this.checkNext(TokenKind.StringLiteral)) {
26,878✔
1085
            return true;
1✔
1086

1087
            //definitely not a library statement
1088
        } else {
1089
            return false;
26,877✔
1090
        }
1091
    }
1092

1093
    private checkAlias() {
1094
        let isAliasToken = this.check(TokenKind.Alias);
26,613✔
1095

1096
        //if we are at the top level, any line that starts with "alias" should be considered a alias statement
1097
        if (this.isAtRootLevel() && isAliasToken) {
26,613✔
1098
            return true;
31✔
1099

1100
            //not at root level, alias statements are all invalid here, but try to detect if the tokens look
1101
            //like a alias statement (and let the alias function handle emitting the diagnostics)
1102
        } else if (isAliasToken && this.checkNext(TokenKind.Identifier)) {
26,582✔
1103
            return true;
2✔
1104

1105
            //definitely not a alias statement
1106
        } else {
1107
            return false;
26,580✔
1108
        }
1109
    }
1110

1111
    private checkTypeStatement() {
1112
        let isTypeToken = this.check(TokenKind.Type);
26,580✔
1113

1114
        //if we are at the top level, any line that starts with "type" should be considered a type statement
1115
        if (this.isAtRootLevel() && isTypeToken) {
26,580✔
1116
            return true;
50✔
1117

1118
            //not at root level, type statements are all invalid here, but try to detect if the tokens look
1119
            //like a type statement (and let the type function handle emitting the diagnostics)
1120
        } else if (isTypeToken && this.checkNext(TokenKind.Identifier)) {
26,530✔
1121
            return true;
5✔
1122

1123
            //definitely not a type statement
1124
        } else {
1125
            return false;
26,525✔
1126
        }
1127
    }
1128

1129
    private statement(): Statement | undefined {
1130
        if (this.checkLibrary()) {
13,296!
UNCOV
1131
            return this.libraryStatement();
×
1132
        }
1133

1134
        if (this.check(TokenKind.Import)) {
13,296✔
1135
            return this.importStatement();
231✔
1136
        }
1137

1138
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
13,065✔
1139
            return this.typecastStatement();
33✔
1140
        }
1141

1142
        if (this.checkAlias()) {
13,032!
UNCOV
1143
            return this.aliasStatement();
×
1144
        }
1145

1146
        if (this.checkTypeStatement()) {
13,032!
UNCOV
1147
            return this.typeStatement();
×
1148
        }
1149

1150
        if (this.check(TokenKind.Stop)) {
13,032✔
1151
            return this.stopStatement();
16✔
1152
        }
1153

1154
        if (this.check(TokenKind.If)) {
13,016✔
1155
            return this.ifStatement();
1,462✔
1156
        }
1157

1158
        //`try` must be followed by a block, otherwise it could be a local variable
1159
        if (this.check(TokenKind.Try) && this.checkAnyNext(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
11,554✔
1160
            return this.tryCatchStatement();
41✔
1161
        }
1162

1163
        if (this.check(TokenKind.Throw)) {
11,513✔
1164
            return this.throwStatement();
12✔
1165
        }
1166

1167
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
11,501✔
1168
            return this.printStatement();
1,515✔
1169
        }
1170
        if (this.check(TokenKind.Dim)) {
9,986✔
1171
            return this.dimStatement();
43✔
1172
        }
1173

1174
        if (this.check(TokenKind.While)) {
9,943✔
1175
            return this.whileStatement();
33✔
1176
        }
1177

1178
        if (this.checkAny(TokenKind.Exit, TokenKind.ExitWhile)) {
9,910✔
1179
            return this.exitStatement();
22✔
1180
        }
1181

1182
        if (this.check(TokenKind.For)) {
9,888✔
1183
            return this.forStatement();
47✔
1184
        }
1185

1186
        if (this.check(TokenKind.ForEach)) {
9,841✔
1187
            return this.forEachStatement();
74✔
1188
        }
1189

1190
        if (this.check(TokenKind.End)) {
9,767✔
1191
            return this.endStatement();
8✔
1192
        }
1193

1194
        if (this.match(TokenKind.Return)) {
9,759✔
1195
            return this.returnStatement();
4,357✔
1196
        }
1197

1198
        if (this.check(TokenKind.Goto)) {
5,402✔
1199
            return this.gotoStatement();
12✔
1200
        }
1201

1202
        //the continue keyword (followed by `for`, `while`, or a statement separator)
1203
        if (this.check(TokenKind.Continue) && this.checkAnyNext(TokenKind.While, TokenKind.For, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
5,390✔
1204
            return this.continueStatement();
12✔
1205
        }
1206

1207
        //does this line look like a label? (i.e.  `someIdentifier:` )
1208
        if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
5,378✔
1209
            try {
12✔
1210
                return this.labelStatement();
12✔
1211
            } catch (err) {
1212
                if (!(err instanceof CancelStatementError)) {
2!
UNCOV
1213
                    throw err;
×
1214
                }
1215
                //not a label, try something else
1216
            }
1217
        }
1218

1219
        // BrightScript is like python, in that variables can be declared without a `var`,
1220
        // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
1221
        // out what to do with it.
1222
        if (
5,368✔
1223
            this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)
1224
        ) {
1225
            if (this.checkAnyNext(...AssignmentOperators)) {
5,076✔
1226
                if (this.checkAnyNext(...CompoundAssignmentOperators)) {
1,906✔
1227
                    return this.augmentedAssignment();
76✔
1228
                }
1229
                return this.assignment();
1,830✔
1230
            } else if (this.checkNext(TokenKind.As)) {
3,170✔
1231
                // may be a typed assignment
1232
                const backtrack = this.current;
19✔
1233
                let validTypeExpression = false;
19✔
1234

1235
                try {
19✔
1236
                    // skip the identifier, and check for valid type expression
1237
                    this.advance();
19✔
1238
                    const parts = this.consumeAsTokenAndTypeExpression(true);
19✔
1239
                    validTypeExpression = !!(parts?.[0] && parts?.[1]);
19!
1240
                } catch (e) {
1241
                    // ignore any errors
1242
                } finally {
1243
                    this.current = backtrack;
19✔
1244
                }
1245
                if (validTypeExpression) {
19✔
1246
                    // there is a valid 'as' and type expression
1247
                    return this.assignment(true);
18✔
1248
                }
1249
            }
1250
        }
1251

1252
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1253
        if (this.check(TokenKind.Interface)) {
3,444✔
1254
            return this.interfaceDeclaration();
247✔
1255
        }
1256

1257
        if (this.check(TokenKind.Class)) {
3,197✔
1258
            return this.classDeclaration();
745✔
1259
        }
1260

1261
        if (this.check(TokenKind.Namespace)) {
2,452✔
1262
            return this.namespaceStatement();
758✔
1263
        }
1264

1265
        if (this.check(TokenKind.Enum)) {
1,694✔
1266
            return this.enumDeclaration();
202✔
1267
        }
1268

1269
        // TODO: support multi-statements
1270
        return this.setStatement();
1,492✔
1271
    }
1272

1273
    private whileStatement(): WhileStatement {
1274
        const whileKeyword = this.advance();
33✔
1275
        const condition = this.expression();
33✔
1276

1277
        this.consumeStatementSeparators();
32✔
1278

1279
        const whileBlock = this.block(TokenKind.EndWhile);
32✔
1280
        let endWhile: Token;
1281
        if (!whileBlock || this.peek().kind !== TokenKind.EndWhile) {
32✔
1282
            this.diagnostics.push({
1✔
1283
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('while'),
1284
                location: this.peek().location
1285
            });
1286
            if (!whileBlock) {
1!
UNCOV
1287
                throw this.lastDiagnosticAsError();
×
1288
            }
1289
        } else {
1290
            endWhile = this.advance();
31✔
1291
        }
1292

1293
        return new WhileStatement({
32✔
1294
            while: whileKeyword,
1295
            endWhile: endWhile,
1296
            condition: condition,
1297
            body: whileBlock
1298
        });
1299
    }
1300

1301
    private exitStatement(): ExitStatement {
1302
        let exitToken = this.advance();
22✔
1303
        if (exitToken.kind === TokenKind.ExitWhile) {
22✔
1304
            // `exitwhile` is allowed in code, and means `exit while`
1305
            // use an ExitStatement that is nicer to work with by breaking the `exit` and `while` tokens apart
1306

1307
            const exitText = exitToken.text.substring(0, 4);
5✔
1308
            const whileText = exitToken.text.substring(4);
5✔
1309
            const originalRange = exitToken.location?.range;
5!
1310
            const originalStart = originalRange?.start;
5!
1311

1312
            const exitRange = util.createRange(
5✔
1313
                originalStart.line,
1314
                originalStart.character,
1315
                originalStart.line,
1316
                originalStart.character + 4);
1317
            const whileRange = util.createRange(
5✔
1318
                originalStart.line,
1319
                originalStart.character + 4,
1320
                originalStart.line,
1321
                originalStart.character + exitToken.text.length);
1322

1323
            exitToken = createToken(TokenKind.Exit, exitText, util.createLocationFromRange(exitToken.location.uri, exitRange));
5✔
1324
            this.tokens[this.current - 1] = exitToken;
5✔
1325
            const newLoopToken = createToken(TokenKind.While, whileText, util.createLocationFromRange(exitToken.location.uri, whileRange));
5✔
1326
            this.tokens.splice(this.current, 0, newLoopToken);
5✔
1327
        }
1328

1329
        const loopTypeToken = this.tryConsume(
22✔
1330
            DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
1331
            TokenKind.While, TokenKind.For
1332
        );
1333

1334
        return new ExitStatement({
22✔
1335
            exit: exitToken,
1336
            loopType: loopTypeToken
1337
        });
1338
    }
1339

1340
    private forStatement(): ForStatement {
1341
        const forToken = this.advance();
47✔
1342
        const initializer = this.assignment();
47✔
1343

1344
        //TODO: newline allowed?
1345

1346
        const toToken = this.advance();
46✔
1347
        const finalValue = this.expression();
46✔
1348
        let incrementExpression: Expression | undefined;
1349
        let stepToken: Token | undefined;
1350

1351
        if (this.check(TokenKind.Step)) {
46✔
1352
            stepToken = this.advance();
13✔
1353
            incrementExpression = this.expression();
13✔
1354
        } else {
1355
            // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
1356
        }
1357

1358
        this.consumeStatementSeparators();
46✔
1359

1360
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
46✔
1361
        let endForToken: Token;
1362
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
46✔
1363
            this.diagnostics.push({
2✔
1364
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(forToken.text),
1365
                location: this.peek().location
1366
            });
1367
            if (!body) {
2!
UNCOV
1368
                throw this.lastDiagnosticAsError();
×
1369
            }
1370
        } else {
1371
            endForToken = this.advance();
44✔
1372
        }
1373

1374
        // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
1375
        // stays around in scope with whatever value it was when the loop exited.
1376
        return new ForStatement({
46✔
1377
            for: forToken,
1378
            counterDeclaration: initializer,
1379
            to: toToken,
1380
            finalValue: finalValue,
1381
            body: body,
1382
            endFor: endForToken,
1383
            step: stepToken,
1384
            increment: incrementExpression
1385
        });
1386
    }
1387

1388
    private forEachStatement(): ForEachStatement {
1389
        let forEach = this.advance();
74✔
1390
        let name = this.advance();
74✔
1391

1392
        let asToken: Token;
1393
        let typeExpression: TypeExpression;
1394

1395
        if (this.check(TokenKind.As)) {
74✔
1396
            this.warnIfNotBrighterScriptMode('typed for each loop variable');
11✔
1397
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
11✔
1398
        }
1399

1400
        let maybeIn = this.peek();
74✔
1401
        if (this.check(TokenKind.Identifier) && maybeIn.text.toLowerCase() === 'in') {
74!
1402
            this.advance();
74✔
1403
        } else {
UNCOV
1404
            this.diagnostics.push({
×
1405
                ...DiagnosticMessages.expectedToken(TokenKind.In),
1406
                location: this.peek().location
1407
            });
UNCOV
1408
            throw this.lastDiagnosticAsError();
×
1409
        }
1410
        maybeIn.kind = TokenKind.In;
74✔
1411

1412
        let target = this.expression();
74✔
1413
        if (!target) {
74!
UNCOV
1414
            this.diagnostics.push({
×
1415
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1416
                location: this.peek().location
1417
            });
UNCOV
1418
            throw this.lastDiagnosticAsError();
×
1419
        }
1420

1421
        this.consumeStatementSeparators();
74✔
1422

1423
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
74✔
1424
        let endForToken: Token;
1425
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
74✔
1426

1427
            this.diagnostics.push({
1✔
1428
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(forEach.text),
1429
                location: this.peek().location
1430
            });
1431
            throw this.lastDiagnosticAsError();
1✔
1432
        }
1433
        endForToken = this.advance();
73✔
1434

1435
        return new ForEachStatement({
73✔
1436
            forEach: forEach,
1437
            as: asToken,
1438
            typeExpression: typeExpression,
1439
            in: maybeIn,
1440
            endFor: endForToken,
1441
            item: name,
1442
            target: target,
1443
            body: body
1444
        });
1445
    }
1446

1447
    private namespaceStatement(): NamespaceStatement | undefined {
1448
        this.warnIfNotBrighterScriptMode('namespace');
758✔
1449
        let keyword = this.advance();
758✔
1450

1451
        this.namespaceAndFunctionDepth++;
758✔
1452

1453
        let name = this.identifyingExpression();
758✔
1454
        //set the current namespace name
1455

1456
        this.globalTerminators.push([TokenKind.EndNamespace]);
757✔
1457
        let body = this.body();
757✔
1458
        this.globalTerminators.pop();
757✔
1459

1460
        let endKeyword: Token;
1461
        if (this.check(TokenKind.EndNamespace)) {
757✔
1462
            endKeyword = this.advance();
755✔
1463
        } else {
1464
            //the `end namespace` keyword is missing. add a diagnostic, but keep parsing
1465
            this.diagnostics.push({
2✔
1466
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'),
1467
                location: keyword.location
1468
            });
1469
        }
1470

1471
        this.namespaceAndFunctionDepth--;
757✔
1472

1473
        let result = new NamespaceStatement({
757✔
1474
            namespace: keyword,
1475
            nameExpression: name,
1476
            body: body,
1477
            endNamespace: endKeyword
1478
        });
1479

1480
        //cache the range property so that plugins can't affect it
1481
        result.cacheLocation();
757✔
1482
        result.body.symbolTable.name += `: namespace '${result.name}'`;
757✔
1483
        return result;
757✔
1484
    }
1485

1486
    /**
1487
     * Get an expression with identifiers separated by periods. Useful for namespaces and class extends
1488
     */
1489
    private identifyingExpression(allowedTokenKinds?: TokenKind[]): DottedGetExpression | VariableExpression {
1490
        allowedTokenKinds = allowedTokenKinds ?? this.allowedLocalIdentifiers;
1,689✔
1491
        let firstIdentifier = this.consume(
1,689✔
1492
            DiagnosticMessages.expectedIdentifier(this.previous().text),
1493
            TokenKind.Identifier,
1494
            ...allowedTokenKinds
1495
        ) as Identifier;
1496

1497
        let expr: DottedGetExpression | VariableExpression;
1498

1499
        if (firstIdentifier) {
1,685!
1500
            // force it into an identifier so the AST makes some sense
1501
            firstIdentifier.kind = TokenKind.Identifier;
1,685✔
1502
            const varExpr = new VariableExpression({ name: firstIdentifier });
1,685✔
1503
            expr = varExpr;
1,685✔
1504

1505
            //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
1506
            while (this.check(TokenKind.Dot)) {
1,685✔
1507
                let dot = this.tryConsume(
529✔
1508
                    DiagnosticMessages.unexpectedToken(this.peek().text),
1509
                    TokenKind.Dot
1510
                );
1511
                if (!dot) {
529!
UNCOV
1512
                    break;
×
1513
                }
1514
                let identifier = this.tryConsume(
529✔
1515
                    DiagnosticMessages.expectedIdentifier(),
1516
                    TokenKind.Identifier,
1517
                    ...allowedTokenKinds,
1518
                    ...AllowedProperties
1519
                ) as Identifier;
1520

1521
                if (!identifier) {
529✔
1522
                    break;
3✔
1523
                }
1524
                // force it into an identifier so the AST makes some sense
1525
                identifier.kind = TokenKind.Identifier;
526✔
1526
                expr = new DottedGetExpression({ obj: expr, name: identifier, dot: dot });
526✔
1527
            }
1528
        }
1529
        return expr;
1,685✔
1530
    }
1531
    /**
1532
     * Add an 'unexpected token' diagnostic for any token found between current and the first stopToken found.
1533
     */
1534
    private flagUntil(...stopTokens: TokenKind[]) {
1535
        while (!this.checkAny(...stopTokens) && !this.isAtEnd()) {
4!
UNCOV
1536
            let token = this.advance();
×
UNCOV
1537
            this.diagnostics.push({
×
1538
                ...DiagnosticMessages.unexpectedToken(token.text),
1539
                location: token.location
1540
            });
1541
        }
1542
    }
1543

1544
    /**
1545
     * Consume tokens until one of the `stopTokenKinds` is encountered
1546
     * @param stopTokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
1547
     * @returns - the list of tokens consumed, EXCLUDING the `stopTokenKind` (you can use `this.peek()` to see which one it was)
1548
     */
1549
    private consumeUntil(...stopTokenKinds: TokenKind[]) {
1550
        let result = [] as Token[];
67✔
1551
        //take tokens until we encounter one of the stopTokenKinds
1552
        while (!stopTokenKinds.includes(this.peek().kind)) {
67✔
1553
            result.push(this.advance());
150✔
1554
        }
1555
        return result;
67✔
1556
    }
1557

1558
    private constDeclaration(): ConstStatement | undefined {
1559
        this.warnIfNotBrighterScriptMode('const declaration');
225✔
1560
        const constToken = this.advance();
225✔
1561
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
225✔
1562
        const equalToken = this.consumeToken(TokenKind.Equal);
225✔
1563
        const expression = this.expression();
225✔
1564
        const statement = new ConstStatement({
225✔
1565
            const: constToken,
1566
            name: nameToken,
1567
            equals: equalToken,
1568
            value: expression
1569
        });
1570
        return statement;
225✔
1571
    }
1572

1573
    private libraryStatement(): LibraryStatement | undefined {
1574
        let libStatement = new LibraryStatement({
13✔
1575
            library: this.advance(),
1576
            //grab the next token only if it's a string
1577
            filePath: this.tryConsume(
1578
                DiagnosticMessages.expectedStringLiteralAfterKeyword('library'),
1579
                TokenKind.StringLiteral
1580
            )
1581
        });
1582

1583
        return libStatement;
13✔
1584
    }
1585

1586
    private importStatement() {
1587
        this.warnIfNotBrighterScriptMode('import statements');
231✔
1588
        let importStatement = new ImportStatement({
231✔
1589
            import: this.advance(),
1590
            //grab the next token only if it's a string
1591
            path: this.tryConsume(
1592
                DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
1593
                TokenKind.StringLiteral
1594
            )
1595
        });
1596

1597
        return importStatement;
231✔
1598
    }
1599

1600
    private typecastStatement() {
1601
        this.warnIfNotBrighterScriptMode('typecast statements');
33✔
1602
        const typecastToken = this.advance();
33✔
1603
        const typecastExpr = this.expression();
33✔
1604
        if (isTypecastExpression(typecastExpr)) {
33✔
1605
            return new TypecastStatement({
32✔
1606
                typecast: typecastToken,
1607
                typecastExpression: typecastExpr
1608
            });
1609
        }
1610
        this.diagnostics.push({
1✔
1611
            ...DiagnosticMessages.expectedIdentifier('typecast'),
1612
            location: {
1613
                uri: typecastToken.location.uri,
1614
                range: util.createBoundingRange(typecastToken, this.peek())
1615
            }
1616
        });
1617
        throw this.lastDiagnosticAsError();
1✔
1618
    }
1619

1620
    private aliasStatement(): AliasStatement | undefined {
1621
        this.warnIfNotBrighterScriptMode('alias statements');
33✔
1622
        const aliasToken = this.advance();
33✔
1623
        const name = this.tryConsume(
33✔
1624
            DiagnosticMessages.expectedIdentifier('alias'),
1625
            TokenKind.Identifier
1626
        );
1627
        const equals = this.tryConsume(
33✔
1628
            DiagnosticMessages.expectedToken(TokenKind.Equal),
1629
            TokenKind.Equal
1630
        );
1631
        let value = this.identifyingExpression();
33✔
1632

1633
        let aliasStmt = new AliasStatement({
33✔
1634
            alias: aliasToken,
1635
            name: name,
1636
            equals: equals,
1637
            value: value
1638

1639
        });
1640

1641
        return aliasStmt;
33✔
1642
    }
1643

1644
    private typeStatement(): TypeStatement | undefined {
1645
        this.warnIfNotBrighterScriptMode('type statements');
55✔
1646
        const typeToken = this.advance();
55✔
1647
        const name = this.tryConsume(
55✔
1648
            DiagnosticMessages.expectedIdentifier('type'),
1649
            TokenKind.Identifier
1650
        );
1651
        const equals = this.tryConsume(
55✔
1652
            DiagnosticMessages.expectedToken(TokenKind.Equal),
1653
            TokenKind.Equal
1654
        );
1655
        let value = this.typeExpression();
55✔
1656

1657
        let typeStmt = new TypeStatement({
53✔
1658
            type: typeToken,
1659
            name: name,
1660
            equals: equals,
1661
            value: value
1662

1663
        });
1664

1665
        return typeStmt;
53✔
1666
    }
1667

1668
    private annotationExpression() {
1669
        const atToken = this.advance();
75✔
1670
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
75✔
1671
        if (identifier) {
75✔
1672
            identifier.kind = TokenKind.Identifier;
74✔
1673
        }
1674
        let annotation = new AnnotationExpression({ at: atToken, name: identifier });
75✔
1675
        this.pendingAnnotations.push(annotation);
74✔
1676

1677
        //optional arguments
1678
        if (this.check(TokenKind.LeftParen)) {
74✔
1679
            let leftParen = this.advance();
30✔
1680
            annotation.call = this.finishCall(leftParen, annotation, false);
30✔
1681
        }
1682
        return annotation;
74✔
1683
    }
1684

1685
    private ternaryExpression(test?: Expression): TernaryExpression {
1686
        this.warnIfNotBrighterScriptMode('ternary operator');
98✔
1687
        if (!test) {
98!
UNCOV
1688
            test = this.expression();
×
1689
        }
1690
        const questionMarkToken = this.advance();
98✔
1691

1692
        //consume newlines or comments
1693
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
98✔
1694
            this.advance();
7✔
1695
        }
1696

1697
        let consequent: Expression;
1698
        try {
98✔
1699
            consequent = this.expression();
98✔
1700
        } catch { }
1701

1702
        //consume newlines or comments
1703
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
98✔
1704
            this.advance();
5✔
1705
        }
1706

1707
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
98✔
1708

1709
        //consume newlines
1710
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
98✔
1711
            this.advance();
11✔
1712
        }
1713
        let alternate: Expression;
1714
        try {
98✔
1715
            alternate = this.expression();
98✔
1716
        } catch { }
1717

1718
        return new TernaryExpression({
98✔
1719
            test: test,
1720
            questionMark: questionMarkToken,
1721
            consequent: consequent,
1722
            colon: colonToken,
1723
            alternate: alternate
1724
        });
1725
    }
1726

1727
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1728
        this.warnIfNotBrighterScriptMode('null coalescing operator');
35✔
1729
        const questionQuestionToken = this.advance();
35✔
1730
        const alternate = this.expression();
35✔
1731
        return new NullCoalescingExpression({
35✔
1732
            consequent: test,
1733
            questionQuestion: questionQuestionToken,
1734
            alternate: alternate
1735
        });
1736
    }
1737

1738
    private regexLiteralExpression() {
1739
        this.warnIfNotBrighterScriptMode('regular expression literal');
45✔
1740
        return new RegexLiteralExpression({
45✔
1741
            regexLiteral: this.advance()
1742
        });
1743
    }
1744

1745
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1746
        this.warnIfNotBrighterScriptMode('template string');
55✔
1747

1748
        //get the tag name
1749
        let tagName: Identifier;
1750
        if (isTagged) {
55✔
1751
            tagName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties) as Identifier;
8✔
1752
            // force it into an identifier so the AST makes some sense
1753
            tagName.kind = TokenKind.Identifier;
8✔
1754
        }
1755

1756
        let quasis = [] as TemplateStringQuasiExpression[];
55✔
1757
        let expressions = [];
55✔
1758
        let openingBacktick = this.peek();
55✔
1759
        this.advance();
55✔
1760
        let currentQuasiExpressionParts = [];
55✔
1761
        while (!this.isAtEnd() && !this.check(TokenKind.BackTick)) {
55✔
1762
            let next = this.peek();
206✔
1763
            if (next.kind === TokenKind.TemplateStringQuasi) {
206✔
1764
                //a quasi can actually be made up of multiple quasis when it includes char literals
1765
                currentQuasiExpressionParts.push(
130✔
1766
                    new LiteralExpression({ value: next })
1767
                );
1768
                this.advance();
130✔
1769
            } else if (next.kind === TokenKind.EscapedCharCodeLiteral) {
76✔
1770
                currentQuasiExpressionParts.push(
33✔
1771
                    new EscapedCharCodeLiteralExpression({ value: next as Token & { charCode: number } })
1772
                );
1773
                this.advance();
33✔
1774
            } else {
1775
                //finish up the current quasi
1776
                quasis.push(
43✔
1777
                    new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1778
                );
1779
                currentQuasiExpressionParts = [];
43✔
1780

1781
                if (next.kind === TokenKind.TemplateStringExpressionBegin) {
43!
1782
                    this.advance();
43✔
1783
                }
1784
                //now keep this expression
1785
                expressions.push(this.expression());
43✔
1786
                if (!this.isAtEnd() && this.check(TokenKind.TemplateStringExpressionEnd)) {
43!
1787
                    //TODO is it an error if this is not present?
1788
                    this.advance();
43✔
1789
                } else {
UNCOV
1790
                    this.diagnostics.push({
×
1791
                        ...DiagnosticMessages.unterminatedTemplateExpression(),
1792
                        location: {
1793
                            uri: openingBacktick.location.uri,
1794
                            range: util.createBoundingRange(openingBacktick, this.peek())
1795
                        }
1796
                    });
UNCOV
1797
                    throw this.lastDiagnosticAsError();
×
1798
                }
1799
            }
1800
        }
1801

1802
        //store the final set of quasis
1803
        quasis.push(
55✔
1804
            new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1805
        );
1806

1807
        if (this.isAtEnd()) {
55✔
1808
            //error - missing backtick
1809
            this.diagnostics.push({
2✔
1810
                ...DiagnosticMessages.unterminatedTemplateString(),
1811
                location: {
1812
                    uri: openingBacktick.location.uri,
1813
                    range: util.createBoundingRange(openingBacktick, this.peek())
1814
                }
1815
            });
1816
            throw this.lastDiagnosticAsError();
2✔
1817

1818
        } else {
1819
            let closingBacktick = this.advance();
53✔
1820
            if (isTagged) {
53✔
1821
                return new TaggedTemplateStringExpression({
8✔
1822
                    tagName: tagName,
1823
                    openingBacktick: openingBacktick,
1824
                    quasis: quasis,
1825
                    expressions: expressions,
1826
                    closingBacktick: closingBacktick
1827
                });
1828
            } else {
1829
                return new TemplateStringExpression({
45✔
1830
                    openingBacktick: openingBacktick,
1831
                    quasis: quasis,
1832
                    expressions: expressions,
1833
                    closingBacktick: closingBacktick
1834
                });
1835
            }
1836
        }
1837
    }
1838

1839
    private tryCatchStatement(): TryCatchStatement {
1840
        const tryToken = this.advance();
41✔
1841
        let endTryToken: Token;
1842
        let catchStmt: CatchStatement;
1843
        //ensure statement separator
1844
        this.consumeStatementSeparators();
41✔
1845

1846
        let tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
41✔
1847

1848
        const peek = this.peek();
41✔
1849
        if (peek.kind !== TokenKind.Catch) {
41✔
1850
            this.diagnostics.push({
2✔
1851
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1852
                location: this.peek()?.location
6!
1853
            });
1854
        } else {
1855
            const catchToken = this.advance();
39✔
1856

1857
            //get the exception variable as an expression
1858
            let exceptionVariableExpression: Expression;
1859
            //if we consumed any statement separators, that means we don't have an exception variable
1860
            if (this.consumeStatementSeparators(true)) {
39✔
1861
                //no exception variable. That's fine in BrighterScript but not in brightscript. But that'll get caught by the validator later...
1862
            } else {
1863
                exceptionVariableExpression = this.expression(true);
33✔
1864
                this.consumeStatementSeparators();
33✔
1865
            }
1866

1867
            const catchBranch = this.block(TokenKind.EndTry);
39✔
1868
            catchStmt = new CatchStatement({
39✔
1869
                catch: catchToken,
1870
                exceptionVariableExpression: exceptionVariableExpression,
1871
                catchBranch: catchBranch
1872
            });
1873
        }
1874
        if (this.peek().kind !== TokenKind.EndTry) {
41✔
1875
            this.diagnostics.push({
2✔
1876
                ...DiagnosticMessages.expectedTerminator('end try', 'try-catch'),
1877
                location: this.peek().location
1878
            });
1879
        } else {
1880
            endTryToken = this.advance();
39✔
1881
        }
1882

1883
        const statement = new TryCatchStatement({
41✔
1884
            try: tryToken,
1885
            tryBranch: tryBranch,
1886
            catchStatement: catchStmt,
1887
            endTry: endTryToken
1888
        }
1889
        );
1890
        return statement;
41✔
1891
    }
1892

1893
    private throwStatement() {
1894
        const throwToken = this.advance();
12✔
1895
        let expression: Expression;
1896
        if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
12✔
1897
            this.diagnostics.push({
2✔
1898
                ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(),
1899
                location: throwToken.location
1900
            });
1901
        } else {
1902
            expression = this.expression();
10✔
1903
        }
1904
        return new ThrowStatement({ throw: throwToken, expression: expression });
10✔
1905
    }
1906

1907
    private dimStatement() {
1908
        const dim = this.advance();
43✔
1909

1910
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
43✔
1911
        // force to an identifier so the AST makes some sense
1912
        if (identifier) {
43✔
1913
            identifier.kind = TokenKind.Identifier;
41✔
1914
        }
1915

1916
        let leftSquareBracket = this.tryConsume(DiagnosticMessages.expectedToken('['), TokenKind.LeftSquareBracket);
43✔
1917

1918
        let expressions: Expression[] = [];
43✔
1919
        let expression: Expression;
1920
        do {
43✔
1921
            try {
82✔
1922
                expression = this.expression();
82✔
1923
                expressions.push(expression);
77✔
1924
                if (this.check(TokenKind.Comma)) {
77✔
1925
                    this.advance();
39✔
1926
                } else {
1927
                    // will also exit for right square braces
1928
                    break;
38✔
1929
                }
1930
            } catch (error) {
1931
            }
1932
        } while (expression);
1933

1934
        if (expressions.length === 0) {
43✔
1935
            this.diagnostics.push({
5✔
1936
                ...DiagnosticMessages.missingExpressionsInDimStatement(),
1937
                location: this.peek().location
1938
            });
1939
        }
1940
        let rightSquareBracket = this.tryConsume(DiagnosticMessages.unmatchedLeftToken('[', 'dim identifier'), TokenKind.RightSquareBracket);
43✔
1941
        return new DimStatement({
43✔
1942
            dim: dim,
1943
            name: identifier,
1944
            openingSquare: leftSquareBracket,
1945
            dimensions: expressions,
1946
            closingSquare: rightSquareBracket
1947
        });
1948
    }
1949

1950
    private nestedInlineConditionalCount = 0;
4,634✔
1951

1952
    private ifStatement(incrementNestedCount = true): IfStatement {
2,725✔
1953
        // colon before `if` is usually not allowed, unless it's after `then`
1954
        if (this.current > 0) {
2,735✔
1955
            const prev = this.previous();
2,730✔
1956
            if (prev.kind === TokenKind.Colon) {
2,730✔
1957
                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then && this.nestedInlineConditionalCount === 0) {
4✔
1958
                    this.diagnostics.push({
1✔
1959
                        ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
1960
                        location: prev.location
1961
                    });
1962
                }
1963
            }
1964
        }
1965

1966
        const ifToken = this.advance();
2,735✔
1967

1968
        const condition = this.expression();
2,735✔
1969
        let thenBranch: Block;
1970
        let elseBranch: IfStatement | Block | undefined;
1971

1972
        let thenToken: Token | undefined;
1973
        let endIfToken: Token | undefined;
1974
        let elseToken: Token | undefined;
1975

1976
        //optional `then`
1977
        if (this.check(TokenKind.Then)) {
2,733✔
1978
            thenToken = this.advance();
2,186✔
1979
        }
1980

1981
        //is it inline or multi-line if?
1982
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
2,733✔
1983

1984
        if (isInlineIfThen) {
2,733✔
1985
            /*** PARSE INLINE IF STATEMENT ***/
1986
            if (!incrementNestedCount) {
48✔
1987
                this.nestedInlineConditionalCount++;
5✔
1988
            }
1989

1990
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
48✔
1991

1992
            if (!thenBranch) {
48!
UNCOV
1993
                this.diagnostics.push({
×
1994
                    ...DiagnosticMessages.expectedStatement(ifToken.text, 'statement'),
1995
                    location: this.peek().location
1996
                });
UNCOV
1997
                throw this.lastDiagnosticAsError();
×
1998
            } else {
1999
                this.ensureInline(thenBranch.statements);
48✔
2000
            }
2001

2002
            //else branch
2003
            if (this.check(TokenKind.Else)) {
48✔
2004
                elseToken = this.advance();
33✔
2005

2006
                if (this.check(TokenKind.If)) {
33✔
2007
                    // recurse-read `else if`
2008
                    elseBranch = this.ifStatement(false);
10✔
2009

2010
                    //no multi-line if chained with an inline if
2011
                    if (!elseBranch.isInline) {
9✔
2012
                        this.diagnostics.push({
4✔
2013
                            ...DiagnosticMessages.expectedInlineIfStatement(),
2014
                            location: elseBranch.location
2015
                        });
2016
                    }
2017

2018
                } else if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
23✔
2019
                    //expecting inline else branch
2020
                    this.diagnostics.push({
3✔
2021
                        ...DiagnosticMessages.expectedInlineIfStatement(),
2022
                        location: this.peek().location
2023
                    });
2024
                    throw this.lastDiagnosticAsError();
3✔
2025
                } else {
2026
                    elseBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
20✔
2027

2028
                    if (elseBranch) {
20!
2029
                        this.ensureInline(elseBranch.statements);
20✔
2030
                    }
2031
                }
2032

2033
                if (!elseBranch) {
29!
2034
                    //missing `else` branch
UNCOV
2035
                    this.diagnostics.push({
×
2036
                        ...DiagnosticMessages.expectedStatement('else', 'statement'),
2037
                        location: this.peek().location
2038
                    });
UNCOV
2039
                    throw this.lastDiagnosticAsError();
×
2040
                }
2041
            }
2042

2043
            if (!elseBranch || !isIfStatement(elseBranch)) {
44✔
2044
                //enforce newline at the end of the inline if statement
2045
                const peek = this.peek();
35✔
2046
                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && peek.kind !== TokenKind.Else && !this.isAtEnd()) {
35✔
2047
                    //ignore last error if it was about a colon
2048
                    if (this.previous().kind === TokenKind.Colon) {
3!
2049
                        this.diagnostics.pop();
3✔
2050
                        this.current--;
3✔
2051
                    }
2052
                    //newline is required
2053
                    this.diagnostics.push({
3✔
2054
                        ...DiagnosticMessages.expectedFinalNewline(),
2055
                        location: this.peek().location
2056
                    });
2057
                }
2058
            }
2059
            this.nestedInlineConditionalCount--;
44✔
2060
        } else {
2061
            /*** PARSE MULTI-LINE IF STATEMENT ***/
2062

2063
            thenBranch = this.blockConditionalBranch(ifToken);
2,685✔
2064

2065
            //ensure newline/colon before next keyword
2066
            this.ensureNewLineOrColon();
2,682✔
2067

2068
            //else branch
2069
            if (this.check(TokenKind.Else)) {
2,682✔
2070
                elseToken = this.advance();
2,132✔
2071

2072
                if (this.check(TokenKind.If)) {
2,132✔
2073
                    // recurse-read `else if`
2074
                    elseBranch = this.ifStatement();
1,263✔
2075

2076
                } else {
2077
                    elseBranch = this.blockConditionalBranch(ifToken);
869✔
2078

2079
                    //ensure newline/colon before next keyword
2080
                    this.ensureNewLineOrColon();
869✔
2081
                }
2082
            }
2083

2084
            if (!isIfStatement(elseBranch)) {
2,682✔
2085
                if (this.check(TokenKind.EndIf)) {
1,419✔
2086
                    endIfToken = this.advance();
1,414✔
2087

2088
                } else {
2089
                    //missing endif
2090
                    this.diagnostics.push({
5✔
2091
                        ...DiagnosticMessages.expectedTerminator('end if', 'if'),
2092
                        location: ifToken.location
2093
                    });
2094
                }
2095
            }
2096
        }
2097

2098
        return new IfStatement({
2,726✔
2099
            if: ifToken,
2100
            then: thenToken,
2101
            endIf: endIfToken,
2102
            else: elseToken,
2103
            condition: condition,
2104
            thenBranch: thenBranch,
2105
            elseBranch: elseBranch
2106
        });
2107
    }
2108

2109
    //consume a `then` or `else` branch block of an `if` statement
2110
    private blockConditionalBranch(ifToken: Token) {
2111
        //keep track of the current error count, because if the then branch fails,
2112
        //we will trash them in favor of a single error on if
2113
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
3,554✔
2114

2115
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
2116
        // a trailing "end if" or "else if"
2117
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
3,554✔
2118

2119
        if (!branch) {
3,554✔
2120
            //throw out any new diagnostics created as a result of a `then` block parse failure.
2121
            //the block() function will discard the current line, so any discarded diagnostics will
2122
            //resurface if they are legitimate, and not a result of a malformed if statement
2123
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
3✔
2124

2125
            //this whole if statement is bogus...add error to the if token and hard-fail
2126
            this.diagnostics.push({
3✔
2127
                ...DiagnosticMessages.expectedTerminator(['end if', 'else if', 'else'], 'then', 'block'),
2128
                location: ifToken.location
2129
            });
2130
            throw this.lastDiagnosticAsError();
3✔
2131
        }
2132
        return branch;
3,551✔
2133
    }
2134

2135
    private conditionalCompileStatement(): ConditionalCompileStatement {
2136
        const hashIfToken = this.advance();
58✔
2137
        let notToken: Token | undefined;
2138

2139
        if (this.check(TokenKind.Not)) {
58✔
2140
            notToken = this.advance();
7✔
2141
        }
2142

2143
        if (!this.checkAny(TokenKind.True, TokenKind.False, TokenKind.Identifier)) {
58✔
2144
            this.diagnostics.push({
1✔
2145
                ...DiagnosticMessages.invalidHashIfValue(),
2146
                location: this.peek()?.location
3!
2147
            });
2148
        }
2149

2150

2151
        const condition = this.advance();
58✔
2152

2153
        let thenBranch: Block;
2154
        let elseBranch: ConditionalCompileStatement | Block | undefined;
2155

2156
        let hashEndIfToken: Token | undefined;
2157
        let hashElseToken: Token | undefined;
2158

2159
        //keep track of the current error count
2160
        //if this is `#if false` remove all diagnostics.
2161
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
58✔
2162

2163
        thenBranch = this.blockConditionalCompileBranch(hashIfToken);
58✔
2164
        const conditionTextLower = condition.text.toLowerCase();
57✔
2165
        if (!this.options.bsConsts?.get(conditionTextLower) || conditionTextLower === 'false') {
57✔
2166
            //throw out any new diagnostics created as a result of a false block
2167
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
43✔
2168
        }
2169

2170
        this.ensureNewLine();
57✔
2171
        this.advance();
57✔
2172

2173
        //else branch
2174
        if (this.check(TokenKind.HashElseIf)) {
57✔
2175
            // recurse-read `#else if`
2176
            elseBranch = this.conditionalCompileStatement();
15✔
2177
            this.ensureNewLine();
15✔
2178

2179
        } else if (this.check(TokenKind.HashElse)) {
42✔
2180
            hashElseToken = this.advance();
11✔
2181
            let diagnosticsLengthBeforeBlock = this.diagnostics.length;
11✔
2182
            elseBranch = this.blockConditionalCompileBranch(hashIfToken);
11✔
2183

2184
            if (condition.text.toLowerCase() === 'true') {
11✔
2185
                //throw out any new diagnostics created as a result of a false block
2186
                this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
1✔
2187
            }
2188
            this.ensureNewLine();
11✔
2189
            this.advance();
11✔
2190
        }
2191

2192
        if (!isConditionalCompileStatement(elseBranch)) {
57✔
2193

2194
            if (this.check(TokenKind.HashEndIf)) {
42!
2195
                hashEndIfToken = this.advance();
42✔
2196

2197
            } else {
2198
                //missing #endif
UNCOV
2199
                this.diagnostics.push({
×
2200
                    ...DiagnosticMessages.expectedTerminator('#end if', '#if'),
2201
                    location: hashIfToken.location
2202
                });
2203
            }
2204
        }
2205

2206
        return new ConditionalCompileStatement({
57✔
2207
            hashIf: hashIfToken,
2208
            hashElse: hashElseToken,
2209
            hashEndIf: hashEndIfToken,
2210
            not: notToken,
2211
            condition: condition,
2212
            thenBranch: thenBranch,
2213
            elseBranch: elseBranch
2214
        });
2215
    }
2216

2217
    //consume a conditional compile branch block of an `#if` statement
2218
    private blockConditionalCompileBranch(hashIfToken: Token) {
2219
        //keep track of the current error count, because if the then branch fails,
2220
        //we will trash them in favor of a single error on if
2221
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
69✔
2222

2223
        //parsing until trailing "#end if", "#else", "#else if"
2224
        let branch = this.conditionalCompileBlock();
69✔
2225

2226
        if (!branch) {
68!
2227
            //throw out any new diagnostics created as a result of a `then` block parse failure.
2228
            //the block() function will discard the current line, so any discarded diagnostics will
2229
            //resurface if they are legitimate, and not a result of a malformed if statement
UNCOV
2230
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
×
2231

2232
            //this whole if statement is bogus...add error to the if token and hard-fail
UNCOV
2233
            this.diagnostics.push({
×
2234
                ...DiagnosticMessages.expectedTerminator(['#end if', '#else if', '#else'], 'conditional compilation', 'block'),
2235
                location: hashIfToken.location
2236
            });
UNCOV
2237
            throw this.lastDiagnosticAsError();
×
2238
        }
2239
        return branch;
68✔
2240
    }
2241

2242
    /**
2243
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2244
     * Always looks for `#end if` or `#else`
2245
     */
2246
    private conditionalCompileBlock(): Block | undefined {
2247
        const parentAnnotations = this.enterAnnotationBlock();
69✔
2248

2249
        this.consumeStatementSeparators(true);
69✔
2250
        const unsafeTerminators = BlockTerminators;
69✔
2251
        const conditionalEndTokens = [TokenKind.HashElse, TokenKind.HashElseIf, TokenKind.HashEndIf];
69✔
2252
        const terminators = [...conditionalEndTokens, ...unsafeTerminators];
69✔
2253
        this.globalTerminators.push(conditionalEndTokens);
69✔
2254
        const statements: Statement[] = [];
69✔
2255
        while (!this.isAtEnd() && !this.checkAny(...terminators)) {
69✔
2256
            //grab the location of the current token
2257
            let loopCurrent = this.current;
73✔
2258
            let dec = this.declaration();
73✔
2259
            if (dec) {
73✔
2260
                if (!isAnnotationExpression(dec)) {
72!
2261
                    this.consumePendingAnnotations(dec);
72✔
2262
                    statements.push(dec);
72✔
2263
                }
2264

2265
                const peekKind = this.peek().kind;
72✔
2266
                if (conditionalEndTokens.includes(peekKind)) {
72✔
2267
                    // current conditional compile branch was closed by other statement, rewind to preceding newline
2268
                    this.current--;
1✔
2269
                }
2270
                //ensure statement separator
2271
                this.consumeStatementSeparators();
72✔
2272

2273
            } else {
2274
                //something went wrong. reset to the top of the loop
2275
                this.current = loopCurrent;
1✔
2276

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

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

2284
                //consume potential separators
2285
                this.consumeStatementSeparators(true);
1✔
2286
            }
2287
        }
2288
        this.globalTerminators.pop();
69✔
2289

2290

2291
        if (this.isAtEnd()) {
69!
UNCOV
2292
            return undefined;
×
2293
            // TODO: Figure out how to handle unterminated blocks well
2294
        } else {
2295
            //did we  hit an unsafe terminator?
2296
            //if so, we need to restore the statement separator
2297
            let prev = this.previous();
69✔
2298
            let prevKind = prev.kind;
69✔
2299
            let peek = this.peek();
69✔
2300
            let peekKind = this.peek().kind;
69✔
2301
            if (
69✔
2302
                (peekKind === TokenKind.HashEndIf || peekKind === TokenKind.HashElse || peekKind === TokenKind.HashElseIf) &&
180✔
2303
                (prevKind === TokenKind.Newline)
2304
            ) {
2305
                this.current--;
68✔
2306
            } else if (unsafeTerminators.includes(peekKind) &&
1!
2307
                (prevKind === TokenKind.Newline || prevKind === TokenKind.Colon)
2308
            ) {
2309
                this.diagnostics.push({
1✔
2310
                    ...DiagnosticMessages.unsafeUnmatchedTerminatorInConditionalCompileBlock(peek.text),
2311
                    location: peek.location
2312
                });
2313
                throw this.lastDiagnosticAsError();
1✔
2314
            } else {
UNCOV
2315
                return undefined;
×
2316
            }
2317
        }
2318
        this.exitAnnotationBlock(parentAnnotations);
68✔
2319
        return new Block({ statements: statements });
68✔
2320
    }
2321

2322
    private conditionalCompileConstStatement() {
2323
        const hashConstToken = this.advance();
21✔
2324

2325
        const constName = this.peek();
21✔
2326
        //disallow using keywords for const names
2327
        if (ReservedWords.has(constName?.text.toLowerCase())) {
21!
2328
            this.diagnostics.push({
1✔
2329
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(constName?.text),
3!
2330
                location: constName?.location
3!
2331
            });
2332

2333
            this.lastDiagnosticAsError();
1✔
2334
            return;
1✔
2335
        }
2336
        const assignment = this.assignment();
20✔
2337
        if (assignment) {
18!
2338
            // check for something other than #const <name> = <otherName|true|false>
2339
            if (assignment.tokens.as || assignment.typeExpression) {
18!
UNCOV
2340
                this.diagnostics.push({
×
2341
                    ...DiagnosticMessages.unexpectedToken(assignment.tokens.as?.text || assignment.typeExpression?.getName(ParseMode.BrighterScript)),
×
2342
                    location: assignment.tokens.as?.location ?? assignment.typeExpression?.location
×
2343
                });
UNCOV
2344
                this.lastDiagnosticAsError();
×
2345
            }
2346

2347
            if (isVariableExpression(assignment.value) || isLiteralBoolean(assignment.value)) {
18✔
2348
                //value is an identifier or a boolean
2349
                //check for valid identifiers will happen in program validation
2350
            } else {
2351
                this.diagnostics.push({
2✔
2352
                    ...DiagnosticMessages.invalidHashConstValue(),
2353
                    location: assignment.value.location
2354
                });
2355
                this.lastDiagnosticAsError();
2✔
2356
            }
2357
        } else {
UNCOV
2358
            return undefined;
×
2359
        }
2360

2361
        if (!this.check(TokenKind.Newline)) {
18!
UNCOV
2362
            this.diagnostics.push({
×
2363
                ...DiagnosticMessages.unexpectedToken(this.peek().text),
2364
                location: this.peek().location
2365
            });
UNCOV
2366
            throw this.lastDiagnosticAsError();
×
2367
        }
2368

2369
        return new ConditionalCompileConstStatement({ hashConst: hashConstToken, assignment: assignment });
18✔
2370
    }
2371

2372
    private conditionalCompileErrorStatement() {
2373
        const hashErrorToken = this.advance();
10✔
2374
        const tokensUntilEndOfLine = this.consumeUntil(TokenKind.Newline);
10✔
2375
        const message = createToken(TokenKind.HashErrorMessage, tokensUntilEndOfLine.map(t => t.text).join(' '));
10✔
2376
        return new ConditionalCompileErrorStatement({ hashError: hashErrorToken, message: message });
10✔
2377
    }
2378

2379
    private ensureNewLine() {
2380
        //ensure newline before next keyword
2381
        if (!this.check(TokenKind.Newline)) {
83!
UNCOV
2382
            this.diagnostics.push({
×
2383
                ...DiagnosticMessages.unexpectedToken(this.peek().text),
2384
                location: this.peek().location
2385
            });
UNCOV
2386
            throw this.lastDiagnosticAsError();
×
2387
        }
2388
    }
2389

2390
    private ensureNewLineOrColon(silent = false) {
3,551✔
2391
        const prev = this.previous().kind;
3,793✔
2392
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
3,793✔
2393
            if (!silent) {
172✔
2394
                this.diagnostics.push({
8✔
2395
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2396
                    location: this.peek().location
2397
                });
2398
            }
2399
            return false;
172✔
2400
        }
2401
        return true;
3,621✔
2402
    }
2403

2404
    //ensure each statement of an inline block is single-line
2405
    private ensureInline(statements: Statement[]) {
2406
        for (const stat of statements) {
68✔
2407
            if (isIfStatement(stat) && !stat.isInline) {
86✔
2408
                this.diagnostics.push({
2✔
2409
                    ...DiagnosticMessages.expectedInlineIfStatement(),
2410
                    location: stat.location
2411
                });
2412
            }
2413
        }
2414
    }
2415

2416
    //consume inline branch of an `if` statement
2417
    private inlineConditionalBranch(...additionalTerminators: BlockTerminator[]): Block | undefined {
2418
        let statements = [];
86✔
2419
        //attempt to get the next statement without using `this.declaration`
2420
        //which seems a bit hackish to get to work properly
2421
        let statement = this.statement();
86✔
2422
        if (!statement) {
86!
UNCOV
2423
            return undefined;
×
2424
        }
2425
        statements.push(statement);
86✔
2426

2427
        //look for colon statement separator
2428
        let foundColon = false;
86✔
2429
        while (this.match(TokenKind.Colon)) {
86✔
2430
            foundColon = true;
23✔
2431
        }
2432

2433
        //if a colon was found, add the next statement or err if unexpected
2434
        if (foundColon) {
86✔
2435
            if (!this.checkAny(TokenKind.Newline, ...additionalTerminators)) {
23✔
2436
                //if not an ending keyword, add next statement
2437
                let extra = this.inlineConditionalBranch(...additionalTerminators);
18✔
2438
                if (!extra) {
18!
UNCOV
2439
                    return undefined;
×
2440
                }
2441
                statements.push(...extra.statements);
18✔
2442
            } else {
2443
                //error: colon before next keyword
2444
                const colon = this.previous();
5✔
2445
                this.diagnostics.push({
5✔
2446
                    ...DiagnosticMessages.unexpectedToken(colon.text),
2447
                    location: colon.location
2448
                });
2449
            }
2450
        }
2451
        return new Block({ statements: statements });
86✔
2452
    }
2453

2454
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2455
        let expressionStart = this.peek();
1,060✔
2456

2457
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
1,060✔
2458
            let operator = this.advance();
27✔
2459

2460
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
27✔
2461
                this.diagnostics.push({
1✔
2462
                    ...DiagnosticMessages.unexpectedOperator(),
2463
                    location: this.peek().location
2464
                });
2465
                throw this.lastDiagnosticAsError();
1✔
2466
            } else if (isCallExpression(expr)) {
26✔
2467
                this.diagnostics.push({
1✔
2468
                    ...DiagnosticMessages.unexpectedOperator(),
2469
                    location: expressionStart.location
2470
                });
2471
                throw this.lastDiagnosticAsError();
1✔
2472
            }
2473

2474
            const result = new IncrementStatement({ value: expr, operator: operator });
25✔
2475
            return result;
25✔
2476
        }
2477

2478
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
1,033✔
2479
            return new ExpressionStatement({ expression: expr });
632✔
2480
        }
2481

2482
        if (this.checkAny(...BinaryExpressionOperatorTokens)) {
401✔
2483
            expr = new BinaryExpression({ left: expr, operator: this.advance(), right: this.expression() });
6✔
2484
        }
2485

2486

2487
        //you're not allowed to do dottedGet or XmlAttrGet after a function call
2488
        if (isDottedGetExpression(expr)) {
401✔
2489
            this.diagnostics.push({
43✔
2490
                ...DiagnosticMessages.propAccessNotPermittedAfterFunctionCallInExpressionStatement('Property'),
2491
                location: util.createBoundingLocation(expr.tokens.dot, expr.tokens.name)
2492
            });
2493
            //we can recover gracefully here even though it's invalid syntax
2494
            return new ExpressionStatement({ expression: expr });
43✔
2495

2496
            //you're not allowed to do indexedGet expressions after a function call
2497
        } else if (isIndexedGetExpression(expr)) {
358✔
2498
            this.diagnostics.push({
2✔
2499
                ...DiagnosticMessages.propAccessNotPermittedAfterFunctionCallInExpressionStatement('Index'),
2500
                location: util.createBoundingLocation(expr.tokens.openingSquare, ...expr.indexes, expr.tokens.closingSquare)
2501
            });
2502
            //we can recover gracefully here even though it's invalid syntax
2503
            return new ExpressionStatement({ expression: expr });
2✔
2504
            //you're not allowed to do XmlAttrGet after a function call
2505
        } else if (isXmlAttributeGetExpression(expr)) {
356✔
2506
            this.diagnostics.push({
1✔
2507
                ...DiagnosticMessages.propAccessNotPermittedAfterFunctionCallInExpressionStatement('XML attribute'),
2508
                location: util.createBoundingLocation(expr.tokens.at, expr.tokens.name)
2509
            });
2510
            //we can recover gracefully here even though it's invalid syntax
2511
            return new ExpressionStatement({ expression: expr });
1✔
2512
        }
2513

2514

2515
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an assignment
2516
        this.diagnostics.push({
355✔
2517
            ...DiagnosticMessages.expectedStatement(),
2518
            location: expressionStart.location
2519
        });
2520
        return new ExpressionStatement({ expression: expr });
355✔
2521
    }
2522

2523
    private setStatement(): DottedSetStatement | IndexedSetStatement | ExpressionStatement | IncrementStatement | AssignmentStatement | AugmentedAssignmentStatement {
2524
        /**
2525
         * Attempts to find an expression-statement or an increment statement.
2526
         * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
2527
         * statements aren't valid expressions. They _do_ however fall under the same parsing
2528
         * priority as standalone function calls though, so we can parse them in the same way.
2529
         */
2530
        let expr = this.call();
1,492✔
2531
        if (this.check(TokenKind.Equal) && !(isCallExpression(expr))) {
1,427✔
2532
            let left = expr;
348✔
2533
            let operator = this.advance();
348✔
2534
            let right = this.expression();
348✔
2535

2536
            // Create a dotted or indexed "set" based on the left-hand side's type
2537
            if (isIndexedGetExpression(left)) {
348✔
2538
                return new IndexedSetStatement({
33✔
2539
                    obj: left.obj,
2540
                    indexes: left.indexes,
2541
                    value: right,
2542
                    openingSquare: left.tokens.openingSquare,
2543
                    closingSquare: left.tokens.closingSquare,
2544
                    equals: operator
2545
                });
2546
            } else if (isDottedGetExpression(left)) {
315✔
2547
                return new DottedSetStatement({
312✔
2548
                    obj: left.obj,
2549
                    name: left.tokens.name,
2550
                    value: right,
2551
                    dot: left.tokens.dot,
2552
                    equals: operator
2553
                });
2554
            }
2555
        } else if (this.checkAny(...CompoundAssignmentOperators) && !(isCallExpression(expr))) {
1,079✔
2556
            let left = expr;
22✔
2557
            let operator = this.advance();
22✔
2558
            let right = this.expression();
22✔
2559
            return new AugmentedAssignmentStatement({
22✔
2560
                item: left,
2561
                operator: operator,
2562
                value: right
2563
            });
2564
        }
2565
        return this.expressionStatement(expr);
1,060✔
2566
    }
2567

2568
    private printStatement(): PrintStatement {
2569
        let printKeyword = this.advance();
1,515✔
2570

2571
        let values: Expression[] = [];
1,515✔
2572

2573
        while (!this.checkEndOfStatement()) {
1,515✔
2574
            if (this.checkAny(TokenKind.Semicolon, TokenKind.Comma)) {
1,645✔
2575
                values.push(new PrintSeparatorExpression({ separator: this.advance() as PrintSeparatorToken }));
49✔
2576
            } else if (this.check(TokenKind.Else)) {
1,596✔
2577
                break; // inline branch
22✔
2578
            } else {
2579
                values.push(this.expression());
1,574✔
2580
            }
2581
        }
2582

2583
        //print statements can be empty, so look for empty print conditions
2584
        if (!values.length) {
1,513✔
2585
            const endOfStatementLocation = util.createBoundingLocation(printKeyword, this.peek());
9✔
2586
            let emptyStringLiteral = createStringLiteral('', endOfStatementLocation);
9✔
2587
            values.push(emptyStringLiteral);
9✔
2588
        }
2589

2590
        let last = values[values.length - 1];
1,513✔
2591
        if (isToken(last)) {
1,513!
2592
            // TODO: error, expected value
2593
        }
2594

2595
        return new PrintStatement({ print: printKeyword, expressions: values });
1,513✔
2596
    }
2597

2598
    /**
2599
     * Parses a return statement with an optional return value.
2600
     * @returns an AST representation of a return statement.
2601
     */
2602
    private returnStatement(): ReturnStatement {
2603
        let options = { return: this.previous() };
4,357✔
2604

2605
        if (this.checkEndOfStatement()) {
4,357✔
2606
            return new ReturnStatement(options);
34✔
2607
        }
2608

2609
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
4,323✔
2610
        return new ReturnStatement({ ...options, value: toReturn });
4,322✔
2611
    }
2612

2613
    /**
2614
     * Parses a `label` statement
2615
     * @returns an AST representation of an `label` statement.
2616
     */
2617
    private labelStatement() {
2618
        let options = {
12✔
2619
            name: this.advance(),
2620
            colon: this.advance()
2621
        };
2622

2623
        //label must be alone on its line, this is probably not a label
2624
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
12✔
2625
            //rewind and cancel
2626
            this.current -= 2;
2✔
2627
            throw new CancelStatementError();
2✔
2628
        }
2629

2630
        return new LabelStatement(options);
10✔
2631
    }
2632

2633
    /**
2634
     * Parses a `continue` statement
2635
     */
2636
    private continueStatement() {
2637
        return new ContinueStatement({
12✔
2638
            continue: this.advance(),
2639
            loopType: this.tryConsume(
2640
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2641
                TokenKind.While, TokenKind.For
2642
            )
2643
        });
2644
    }
2645

2646
    /**
2647
     * Parses a `goto` statement
2648
     * @returns an AST representation of an `goto` statement.
2649
     */
2650
    private gotoStatement() {
2651
        let tokens = {
12✔
2652
            goto: this.advance(),
2653
            label: this.consume(
2654
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2655
                TokenKind.Identifier
2656
            )
2657
        };
2658

2659
        return new GotoStatement(tokens);
10✔
2660
    }
2661

2662
    /**
2663
     * Parses an `end` statement
2664
     * @returns an AST representation of an `end` statement.
2665
     */
2666
    private endStatement() {
2667
        let options = { end: this.advance() };
8✔
2668

2669
        return new EndStatement(options);
8✔
2670
    }
2671
    /**
2672
     * Parses a `stop` statement
2673
     * @returns an AST representation of a `stop` statement
2674
     */
2675
    private stopStatement() {
2676
        let options = { stop: this.advance() };
16✔
2677

2678
        return new StopStatement(options);
16✔
2679
    }
2680

2681
    /**
2682
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2683
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2684
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2685
     *                    ignored.
2686
     */
2687
    private block(...terminators: BlockTerminator[]): Block | undefined {
2688
        const parentAnnotations = this.enterAnnotationBlock();
8,642✔
2689

2690
        this.consumeStatementSeparators(true);
8,642✔
2691
        const statements: Statement[] = [];
8,642✔
2692
        const flatGlobalTerminators = this.globalTerminators.flat().flat();
8,642✔
2693
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators, ...flatGlobalTerminators)) {
8,642✔
2694
            //grab the location of the current token
2695
            let loopCurrent = this.current;
10,180✔
2696
            let dec = this.declaration();
10,180✔
2697
            if (dec) {
10,180✔
2698
                if (!isAnnotationExpression(dec)) {
10,124✔
2699
                    this.consumePendingAnnotations(dec);
10,117✔
2700
                    statements.push(dec);
10,117✔
2701
                }
2702

2703
                //ensure statement separator
2704
                this.consumeStatementSeparators();
10,124✔
2705

2706
            } else {
2707
                //something went wrong. reset to the top of the loop
2708
                this.current = loopCurrent;
56✔
2709

2710
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2711
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
56✔
2712

2713
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2714
                //but there's already an error in the file being parsed, so just leave this line here
2715
                this.advance();
56✔
2716

2717
                //consume potential separators
2718
                this.consumeStatementSeparators(true);
56✔
2719
            }
2720
        }
2721

2722
        if (this.isAtEnd()) {
8,642✔
2723
            return undefined;
6✔
2724
            // TODO: Figure out how to handle unterminated blocks well
2725
        } else if (terminators.length > 0) {
8,636✔
2726
            //did we hit end-sub / end-function while looking for some other terminator?
2727
            //if so, we need to restore the statement separator
2728
            let prev = this.previous().kind;
3,783✔
2729
            let peek = this.peek().kind;
3,783✔
2730
            if (
3,783✔
2731
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
7,570!
2732
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2733
            ) {
2734
                this.current--;
10✔
2735
            }
2736
        }
2737

2738
        this.exitAnnotationBlock(parentAnnotations);
8,636✔
2739
        return new Block({ statements: statements });
8,636✔
2740
    }
2741

2742
    /**
2743
     * Attach pending annotations to the provided statement,
2744
     * and then reset the annotations array
2745
     */
2746
    consumePendingAnnotations(statement: Statement) {
2747
        if (this.pendingAnnotations.length) {
19,344✔
2748
            statement.annotations = this.pendingAnnotations;
51✔
2749
            this.pendingAnnotations = [];
51✔
2750
        }
2751
    }
2752

2753
    enterAnnotationBlock() {
2754
        const pending = this.pendingAnnotations;
15,264✔
2755
        this.pendingAnnotations = [];
15,264✔
2756
        return pending;
15,264✔
2757
    }
2758

2759
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2760
        // non consumed annotations are an error
2761
        if (this.pendingAnnotations.length) {
15,256✔
2762
            for (const annotation of this.pendingAnnotations) {
5✔
2763
                this.diagnostics.push({
7✔
2764
                    ...DiagnosticMessages.unusedAnnotation(),
2765
                    location: annotation.location
2766
                });
2767
            }
2768
        }
2769
        this.pendingAnnotations = parentAnnotations;
15,256✔
2770
    }
2771

2772
    private expression(findTypecast = true): Expression {
15,444✔
2773
        let expression = this.anonymousFunction();
15,862✔
2774
        let asToken: Token;
2775
        let typeExpression: TypeExpression;
2776
        if (findTypecast) {
15,815✔
2777
            do {
15,430✔
2778
                if (this.check(TokenKind.As)) {
15,528✔
2779
                    this.warnIfNotBrighterScriptMode('type cast');
100✔
2780
                    // Check if this expression is wrapped in any type casts
2781
                    // allows for multiple casts:
2782
                    // myVal = foo() as dynamic as string
2783
                    [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
100✔
2784
                    if (asToken && typeExpression) {
100✔
2785
                        expression = new TypecastExpression({ obj: expression, as: asToken, typeExpression: typeExpression });
98✔
2786
                    }
2787
                } else {
2788
                    break;
15,428✔
2789
                }
2790

2791
            } while (asToken && typeExpression);
200✔
2792
        }
2793
        return expression;
15,815✔
2794
    }
2795

2796
    private anonymousFunction(): Expression {
2797
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
15,862✔
2798
            const func = this.functionDeclaration(true);
114✔
2799
            //if there's an open paren after this, this is an IIFE
2800
            if (this.check(TokenKind.LeftParen)) {
113✔
2801
                return this.finishCall(this.advance(), func);
3✔
2802
            } else {
2803
                return func;
110✔
2804
            }
2805
        }
2806

2807
        let expr = this.boolean();
15,748✔
2808

2809
        if (this.check(TokenKind.Question)) {
15,702✔
2810
            return this.ternaryExpression(expr);
98✔
2811
        } else if (this.check(TokenKind.QuestionQuestion)) {
15,604✔
2812
            return this.nullCoalescingExpression(expr);
35✔
2813
        } else {
2814
            return expr;
15,569✔
2815
        }
2816
    }
2817

2818
    private boolean(): Expression {
2819
        let expr = this.relational();
15,748✔
2820

2821
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
15,702✔
2822
            let operator = this.previous();
37✔
2823
            this.consumeNewlinesIfAllowed();
37✔
2824
            let right = this.relational();
37✔
2825
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
37✔
2826
        }
2827

2828
        return expr;
15,702✔
2829
    }
2830

2831
    private relational(): Expression {
2832
        let expr = this.additive();
15,815✔
2833

2834
        while (
15,769✔
2835
            this.matchAny(
2836
                TokenKind.Equal,
2837
                TokenKind.LessGreater,
2838
                TokenKind.Greater,
2839
                TokenKind.GreaterEqual,
2840
                TokenKind.Less,
2841
                TokenKind.LessEqual
2842
            )
2843
        ) {
2844
            let operator = this.previous();
2,262✔
2845
            this.consumeNewlinesIfAllowed();
2,262✔
2846
            let right = this.additive();
2,262✔
2847
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
2,262✔
2848
        }
2849

2850
        return expr;
15,769✔
2851
    }
2852

2853
    // TODO: bitshift
2854

2855
    private additive(): Expression {
2856
        let expr = this.multiplicative();
18,077✔
2857

2858
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
18,035✔
2859
            let operator = this.previous();
1,829✔
2860
            this.consumeNewlinesIfAllowed();
1,829✔
2861
            let right = this.multiplicative();
1,829✔
2862
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,825✔
2863
        }
2864

2865
        return expr;
18,031✔
2866
    }
2867

2868
    private multiplicative(): Expression {
2869
        let expr = this.exponential();
19,906✔
2870

2871
        while (this.matchAny(
19,860✔
2872
            TokenKind.Forwardslash,
2873
            TokenKind.Backslash,
2874
            TokenKind.Star,
2875
            TokenKind.Mod,
2876
            TokenKind.LeftShift,
2877
            TokenKind.RightShift
2878
        )) {
2879
            let operator = this.previous();
64✔
2880
            this.consumeNewlinesIfAllowed();
64✔
2881
            let right = this.exponential();
64✔
2882
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
64✔
2883
        }
2884

2885
        return expr;
19,860✔
2886
    }
2887

2888
    private exponential(): Expression {
2889
        let expr = this.prefixUnary();
19,970✔
2890

2891
        while (this.match(TokenKind.Caret)) {
19,924✔
2892
            let operator = this.previous();
10✔
2893
            this.consumeNewlinesIfAllowed();
10✔
2894
            let right = this.prefixUnary();
10✔
2895
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
10✔
2896
        }
2897

2898
        return expr;
19,924✔
2899
    }
2900

2901
    private prefixUnary(): Expression {
2902
        const nextKind = this.peek().kind;
20,016✔
2903
        if (nextKind === TokenKind.Not) {
20,016✔
2904
            this.current++; //advance
30✔
2905
            let operator = this.previous();
30✔
2906
            let right = this.relational();
30✔
2907
            return new UnaryExpression({ operator: operator, right: right });
30✔
2908
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
19,986✔
2909
            this.current++; //advance
36✔
2910
            let operator = this.previous();
36✔
2911
            let right = (nextKind as any) === TokenKind.Not
36✔
2912
                ? this.boolean()
36!
2913
                : this.prefixUnary();
2914
            return new UnaryExpression({ operator: operator, right: right });
36✔
2915
        }
2916
        return this.call();
19,950✔
2917
    }
2918

2919
    private indexedGet(expr: Expression) {
2920
        let openingSquare = this.previous();
167✔
2921
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
167✔
2922
        let indexes: Expression[] = [];
167✔
2923

2924

2925
        //consume leading newlines
2926
        while (this.match(TokenKind.Newline)) { }
167✔
2927

2928
        try {
167✔
2929
            indexes.push(
167✔
2930
                this.expression()
2931
            );
2932
            //consume additional indexes separated by commas
2933
            while (this.check(TokenKind.Comma)) {
165✔
2934
                //discard the comma
2935
                this.advance();
17✔
2936
                indexes.push(
17✔
2937
                    this.expression()
2938
                );
2939
            }
2940
        } catch (error) {
2941
            this.rethrowNonDiagnosticError(error);
2✔
2942
        }
2943
        //consume trailing newlines
2944
        while (this.match(TokenKind.Newline)) { }
167✔
2945

2946
        const closingSquare = this.tryConsume(
167✔
2947
            DiagnosticMessages.unmatchedLeftToken(openingSquare.text, 'array or object index'),
2948
            TokenKind.RightSquareBracket
2949
        );
2950

2951
        return new IndexedGetExpression({
167✔
2952
            obj: expr,
2953
            indexes: indexes,
2954
            openingSquare: openingSquare,
2955
            closingSquare: closingSquare,
2956
            questionDot: questionDotToken
2957
        });
2958
    }
2959

2960
    private newExpression() {
2961
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
144✔
2962
        let newToken = this.advance();
144✔
2963

2964
        let nameExpr = this.identifyingExpression();
144✔
2965
        let leftParen = this.tryConsume(
144✔
2966
            DiagnosticMessages.unexpectedToken(this.peek().text),
2967
            TokenKind.LeftParen,
2968
            TokenKind.QuestionLeftParen
2969
        );
2970

2971
        if (!leftParen) {
144✔
2972
            // new expression without a following call expression
2973
            // wrap the name in an expression
2974
            const endOfStatementLocation = util.createBoundingLocation(newToken, this.peek());
4✔
2975
            const exprStmt = nameExpr ?? createStringLiteral('', endOfStatementLocation);
4!
2976
            return new ExpressionStatement({ expression: exprStmt });
4✔
2977
        }
2978

2979
        let call = this.finishCall(leftParen, nameExpr);
140✔
2980
        //pop the call from the  callExpressions list because this is technically something else
2981
        this.callExpressions.pop();
140✔
2982
        let result = new NewExpression({ new: newToken, call: call });
140✔
2983
        return result;
140✔
2984
    }
2985

2986
    /**
2987
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2988
     */
2989
    private callfunc(callee: Expression): Expression {
2990
        this.warnIfNotBrighterScriptMode('callfunc operator');
79✔
2991
        let operator = this.previous();
79✔
2992
        let methodName = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
79✔
2993
        let openParen: Token;
2994
        let call: CallExpression;
2995
        if (methodName) {
79✔
2996
            // force it into an identifier so the AST makes some sense
2997
            methodName.kind = TokenKind.Identifier;
71✔
2998
            openParen = this.tryConsume(DiagnosticMessages.expectedToken(TokenKind.LeftParen), TokenKind.LeftParen);
71✔
2999
            if (openParen) {
71!
3000
                call = this.finishCall(openParen, callee, false);
71✔
3001
            }
3002
        }
3003
        return new CallfuncExpression({
79✔
3004
            callee: callee,
3005
            operator: operator,
3006
            methodName: methodName as Identifier,
3007
            openingParen: openParen,
3008
            args: call?.args,
237✔
3009
            closingParen: call?.tokens?.closingParen
474✔
3010
        });
3011
    }
3012

3013
    private call(): Expression {
3014
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
21,442✔
3015
            return this.newExpression();
144✔
3016
        }
3017
        let expr = this.primary();
21,298✔
3018

3019
        while (true) {
21,187✔
3020
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
27,222✔
3021
                expr = this.finishCall(this.previous(), expr);
2,896✔
3022
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
24,326✔
3023
                expr = this.indexedGet(expr);
165✔
3024
            } else if (this.match(TokenKind.Callfunc)) {
24,161✔
3025
                expr = this.callfunc(expr);
79✔
3026
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
24,082✔
3027
                if (this.match(TokenKind.LeftSquareBracket)) {
2,943✔
3028
                    expr = this.indexedGet(expr);
2✔
3029
                } else {
3030
                    let dot = this.previous();
2,941✔
3031
                    let name = this.tryConsume(
2,941✔
3032
                        DiagnosticMessages.expectedIdentifier(),
3033
                        TokenKind.Identifier,
3034
                        ...AllowedProperties
3035
                    );
3036
                    if (!name) {
2,941✔
3037
                        break;
48✔
3038
                    }
3039

3040
                    // force it into an identifier so the AST makes some sense
3041
                    name.kind = TokenKind.Identifier;
2,893✔
3042
                    expr = new DottedGetExpression({ obj: expr, name: name as Identifier, dot: dot });
2,893✔
3043
                }
3044

3045
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
21,139✔
3046
                let dot = this.advance();
12✔
3047
                let name = this.tryConsume(
12✔
3048
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
3049
                    TokenKind.Identifier,
3050
                    ...AllowedProperties
3051
                );
3052

3053
                // force it into an identifier so the AST makes some sense
3054
                name.kind = TokenKind.Identifier;
12✔
3055
                if (!name) {
12!
UNCOV
3056
                    break;
×
3057
                }
3058
                expr = new XmlAttributeGetExpression({ obj: expr, name: name as Identifier, at: dot });
12✔
3059
                //only allow a single `@` expression
3060
                break;
12✔
3061

3062
            } else {
3063
                break;
21,127✔
3064
            }
3065
        }
3066

3067
        return expr;
21,187✔
3068
    }
3069

3070
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
3,039✔
3071
        let args = [] as Expression[];
3,140✔
3072
        this.consumeNewlinesIfAllowed();
3,140✔
3073

3074
        if (!this.check(TokenKind.RightParen)) {
3,140✔
3075
            do {
1,599✔
3076
                this.consumeNewlinesIfAllowed();
2,283✔
3077

3078
                if (args.length >= CallExpression.MaximumArguments) {
2,283!
UNCOV
3079
                    this.diagnostics.push({
×
3080
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
3081
                        location: this.peek()?.location
×
3082
                    });
UNCOV
3083
                    throw this.lastDiagnosticAsError();
×
3084
                }
3085
                //if a newline appears where we'd next read an argument and line continuation isn't
3086
                //allowed in this mode, peek past the newlines: if the following token can't begin an
3087
                //argument expression (e.g. `end function`, `end sub`, EOF), this is clearly an
3088
                //unclosed-paren situation and we swallow the newlines so the diagnostic from
3089
                //expression() reports the surprising token rather than the newline itself.
3090
                if (!this.allowLineContinuation && this.check(TokenKind.Newline)) {
2,283✔
3091
                    let lookahead = this.current;
8✔
3092
                    while (this.tokens[lookahead]?.kind === TokenKind.Newline) {
8!
3093
                        lookahead++;
8✔
3094
                    }
3095
                    const followingKind = this.tokens[lookahead]?.kind;
8!
3096
                    if (followingKind === TokenKind.EndFunction || followingKind === TokenKind.EndSub || followingKind === TokenKind.Eof) {
8✔
3097
                        while (this.match(TokenKind.Newline)) { }
4✔
3098
                    }
3099
                }
3100
                try {
2,283✔
3101
                    args.push(this.expression());
2,283✔
3102
                } catch (error) {
3103
                    this.rethrowNonDiagnosticError(error);
10✔
3104
                    // we were unable to get an expression, so don't continue
3105
                    break;
10✔
3106
                }
3107
            } while (this.match(TokenKind.Comma));
3108
        }
3109

3110
        this.consumeNewlinesIfAllowed();
3,140✔
3111

3112
        //if no closing `)` is in sight (e.g. we hit a newline that line-continuation is not allowed
3113
        //to swallow), consume newlines anyway as part of error recovery so the unmatched-left-token
3114
        //diagnostic points at the surprising token (e.g. `end sub`) rather than the `\n`. This also
3115
        //ensures the surrounding block sees a non-separator next token and emits its own
3116
        //`expectedNewlineOrColon` diagnostic.
3117
        const recoveredFromMissingRightParen = !this.check(TokenKind.RightParen);
3,140✔
3118
        if (recoveredFromMissingRightParen) {
3,140✔
3119
            while (this.match(TokenKind.Newline)) { }
17✔
3120
        }
3121

3122
        const closingParen = this.tryConsume(
3,140✔
3123
            DiagnosticMessages.unmatchedLeftToken(openingParen.text, 'function call arguments'),
3124
            TokenKind.RightParen
3125
        );
3126

3127
        let expression = new CallExpression({
3,140✔
3128
            callee: callee,
3129
            openingParen: openingParen,
3130
            args: args,
3131
            closingParen: closingParen
3132
        });
3133
        if (addToCallExpressionList) {
3,140✔
3134
            this.callExpressions.push(expression);
3,039✔
3135
        }
3136
        return expression;
3,140✔
3137
    }
3138

3139
    /**
3140
     * Creates a TypeExpression, which wraps standard ASTNodes that represent a BscType
3141
     */
3142
    private typeExpression(): TypeExpression {
3143
        const changedTokens: { token: Token; oldKind: TokenKind }[] = [];
2,486✔
3144
        try {
2,486✔
3145
            // handle types with 'and'/'or' operators
3146
            const expressionsWithOperator: { expression: Expression; operator?: Token }[] = [];
2,486✔
3147

3148
            // find all expressions and operators
3149
            let expr: Expression = this.getTypeExpressionPart(changedTokens);
2,486✔
3150
            while (this.options.mode === ParseMode.BrighterScript && this.matchAny(TokenKind.Or, TokenKind.And)) {
2,483✔
3151
                let operator = this.previous();
165✔
3152
                expressionsWithOperator.push({ expression: expr, operator: operator });
165✔
3153
                expr = this.getTypeExpressionPart(changedTokens);
165✔
3154
            }
3155
            // add last expression
3156
            expressionsWithOperator.push({ expression: expr });
2,482✔
3157

3158
            // handle expressions with order of operations - first "and", then "or"
3159
            const combineExpressions = (opToken: TokenKind) => {
2,482✔
3160
                let exprWithOp = expressionsWithOperator[0];
4,964✔
3161
                let index = 0;
4,964✔
3162
                while (exprWithOp?.operator) {
4,964!
3163
                    if (exprWithOp.operator.kind === opToken) {
282✔
3164
                        const nextExpr = expressionsWithOperator[index + 1];
164✔
3165
                        const combinedExpr = new BinaryExpression({ left: exprWithOp.expression, operator: exprWithOp.operator, right: nextExpr.expression });
164✔
3166
                        // replace the two expressions with the combined one
3167
                        expressionsWithOperator.splice(index, 2, { expression: combinedExpr, operator: nextExpr.operator });
164✔
3168
                        exprWithOp = expressionsWithOperator[index];
164✔
3169
                    } else {
3170
                        index++;
118✔
3171
                        exprWithOp = expressionsWithOperator[index];
118✔
3172
                    }
3173
                }
3174
            };
3175

3176
            combineExpressions(TokenKind.And);
2,482✔
3177
            combineExpressions(TokenKind.Or);
2,482✔
3178

3179
            if (expressionsWithOperator[0]?.expression) {
2,482!
3180
                return new TypeExpression({ expression: expressionsWithOperator[0].expression });
2,467✔
3181
            }
3182

3183
        } catch (error) {
3184
            // Something went wrong - reset the kind to what it was previously
3185
            for (const changedToken of changedTokens) {
4✔
UNCOV
3186
                changedToken.token.kind = changedToken.oldKind;
×
3187
            }
3188
            throw error;
4✔
3189
        }
3190
    }
3191

3192
    /**
3193
     * Gets a single "part" of a type of a potential Union type
3194
     * Note: this does not NEED to be part of a union type, but the logic is the same
3195
     *
3196
     * @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
3197
     * @returns an expression that was successfully parsed
3198
     */
3199
    private getTypeExpressionPart(changedTokens: { token: Token; oldKind: TokenKind }[]) {
3200
        let expr: VariableExpression | DottedGetExpression | TypedArrayExpression | InlineInterfaceExpression | GroupingExpression | TypedFunctionTypeExpression;
3201

3202
        if (this.checkAny(TokenKind.Sub, TokenKind.Function) && this.checkNext(TokenKind.LeftParen)) {
2,651✔
3203
            // this is a tyyed function type expression, eg. "function(type1, type2) as type3"
3204
            this.warnIfNotBrighterScriptMode('typed function types');
46✔
3205
            expr = this.typedFunctionTypeExpression();
46✔
3206
        } else if (this.checkAny(...DeclarableTypes)) {
2,605✔
3207
            // if this is just a type, just use directly
3208
            expr = new VariableExpression({ name: this.advance() as Identifier });
1,732✔
3209
        } else {
3210
            if (this.options.mode === ParseMode.BrightScript && !declarableTypesLower.includes(this.peek()?.text?.toLowerCase())) {
873!
3211
                //inline interfaces have their own diagnostic ('inline interface'); let `{`
3212
                //flow through to the matcher below so it fires the more-specific warning.
3213
                if (!this.check(TokenKind.LeftCurlyBrace)) {
17✔
3214
                    // custom types arrays not allowed in Brightscript
3215
                    this.warnIfNotBrighterScriptMode('custom types');
15✔
3216
                    this.advance(); // skip custom type token
15✔
3217
                    return expr;
15✔
3218
                }
3219
            }
3220

3221
            if (this.match(TokenKind.LeftCurlyBrace)) {
858✔
3222
                expr = this.inlineInterface();
80✔
3223
            } else if (this.match(TokenKind.LeftParen)) {
778✔
3224
                // this is a grouping type expression, ie. "(typeExpr)"
3225
                let left = this.previous();
24✔
3226
                let typeExpr = this.typeExpression();
24✔
3227
                let right = this.consume(
24✔
3228
                    DiagnosticMessages.unmatchedLeftToken(left.text, 'type expression'),
3229
                    TokenKind.RightParen
3230
                );
3231
                expr = new GroupingExpression({ leftParen: left, rightParen: right, expression: typeExpr });
24✔
3232
            } else {
3233
                if (this.checkAny(...AllowedTypeIdentifiers)) {
754✔
3234
                    // Since the next token is allowed as a type identifier, change the kind
3235
                    let nextToken = this.peek();
2✔
3236
                    changedTokens.push({ token: nextToken, oldKind: nextToken.kind });
2✔
3237
                    nextToken.kind = TokenKind.Identifier;
2✔
3238
                }
3239
                expr = this.identifyingExpression(AllowedTypeIdentifiers);
754✔
3240
            }
3241
        }
3242

3243
        //Check if it has square brackets, thus making it an array
3244
        if (expr && this.check(TokenKind.LeftSquareBracket)) {
2,632✔
3245
            if (this.options.mode === ParseMode.BrightScript) {
56✔
3246
                // typed arrays not allowed in Brightscript
3247
                this.warnIfNotBrighterScriptMode('typed arrays');
1✔
3248
                return expr;
1✔
3249
            }
3250

3251
            // Check if it is an array - that is, if it has `[]` after the type
3252
            // eg. `string[]` or `SomeKlass[]`
3253
            // This is while loop, so it supports multidimensional arrays (eg. integer[][])
3254
            while (this.check(TokenKind.LeftSquareBracket)) {
55✔
3255
                const leftBracket = this.advance();
62✔
3256
                if (this.check(TokenKind.RightSquareBracket)) {
62!
3257
                    const rightBracket = this.advance();
62✔
3258
                    expr = new TypedArrayExpression({ innerType: expr, leftBracket: leftBracket, rightBracket: rightBracket });
62✔
3259
                }
3260
            }
3261
        }
3262

3263
        return expr;
2,631✔
3264
    }
3265

3266
    private typedFunctionTypeExpression() {
3267
        const funcOrSub = this.advance();
46✔
3268
        const openParen = this.consume(DiagnosticMessages.expectedToken(TokenKind.LeftParen), TokenKind.LeftParen);
46✔
3269
        const params: FunctionParameterExpression[] = [];
46✔
3270

3271
        if (!this.check(TokenKind.RightParen)) {
46✔
3272
            do {
34✔
3273
                if (params.length >= CallExpression.MaximumArguments) {
44!
UNCOV
3274
                    this.diagnostics.push({
×
3275
                        ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments),
3276
                        location: this.peek().location
3277
                    });
3278
                }
3279

3280
                params.push(this.functionParameter());
44✔
3281
            } while (this.match(TokenKind.Comma));
3282
        }
3283

3284
        const closeParen = this.consume(
46✔
3285
            DiagnosticMessages.unmatchedLeftToken(openParen.text, 'function type expression'),
3286
            TokenKind.RightParen
3287
        );
3288

3289
        let asToken: Token;
3290
        let returnType: TypeExpression;
3291

3292
        if (this.check(TokenKind.As)) {
45✔
3293
            [asToken, returnType] = this.consumeAsTokenAndTypeExpression();
43✔
3294
        }
3295
        return new TypedFunctionTypeExpression({
45✔
3296
            functionType: funcOrSub,
3297
            rightParen: openParen,
3298
            params: params,
3299
            leftParen: closeParen,
3300
            as: asToken,
3301
            returnType: returnType
3302
        });
3303

3304
    }
3305

3306

3307
    private inlineInterface() {
3308
        let expr: InlineInterfaceExpression;
3309
        const openToken = this.previous();
80✔
3310
        this.warnIfNotBrighterScriptMode('inline interface');
80✔
3311
        const members: InlineInterfaceMemberExpression[] = [];
80✔
3312
        while (this.match(TokenKind.Newline)) { }
80✔
3313
        while (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral, TokenKind.Optional)) {
80✔
3314
            const member = this.inlineInterfaceMember();
95✔
3315
            members.push(member);
95✔
3316
            while (this.matchAny(TokenKind.Comma, TokenKind.Newline)) { }
95✔
3317
        }
3318
        if (!this.check(TokenKind.RightCurlyBrace)) {
80!
UNCOV
3319
            this.diagnostics.push({
×
3320
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
3321
                location: this.peek().location
3322
            });
UNCOV
3323
            throw this.lastDiagnosticAsError();
×
3324
        }
3325
        const closeToken = this.advance();
80✔
3326

3327
        expr = new InlineInterfaceExpression({ open: openToken, members: members, close: closeToken });
80✔
3328
        return expr;
80✔
3329
    }
3330

3331
    private inlineInterfaceMember(): InlineInterfaceMemberExpression {
3332
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
95✔
3333

3334
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral)) {
95!
3335
            if (this.check(TokenKind.As)) {
95!
UNCOV
3336
                if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
×
3337
                    // as <EOL>
3338
                    // `as` is the field name
UNCOV
3339
                } else if (this.checkNext(TokenKind.As)) {
×
3340
                    //  as as ____
3341
                    // first `as` is the field name
UNCOV
3342
                } else if (optionalKeyword) {
×
3343
                    // optional as ____
3344
                    // optional is the field name, `as` starts type
3345
                    // rewind current token
UNCOV
3346
                    optionalKeyword = null;
×
UNCOV
3347
                    this.current--;
×
3348
                }
3349
            }
3350
        } else {
3351
            // no name after `optional` ... optional is the name
3352
            // rewind current token
UNCOV
3353
            optionalKeyword = null;
×
UNCOV
3354
            this.current--;
×
3355
        }
3356

3357
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers, TokenKind.StringLiteral)) {
95!
UNCOV
3358
            this.diagnostics.push({
×
3359
                ...DiagnosticMessages.expectedIdentifier(this.peek().text),
3360
                location: this.peek().location
3361
            });
UNCOV
3362
            throw this.lastDiagnosticAsError();
×
3363
        }
3364
        let name: Token;
3365
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
95✔
3366
            name = this.identifier(...AllowedProperties);
92✔
3367
        } else {
3368
            name = this.advance();
3✔
3369
        }
3370

3371
        let typeExpression: TypeExpression;
3372

3373
        let asToken: Token = null;
95✔
3374
        if (this.check(TokenKind.As)) {
95✔
3375
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
91✔
3376

3377
        }
3378
        return new InlineInterfaceMemberExpression({
95✔
3379
            name: name,
3380
            as: asToken,
3381
            typeExpression: typeExpression,
3382
            optional: optionalKeyword
3383
        });
3384
    }
3385

3386
    private primary(): Expression {
3387
        switch (true) {
21,298✔
3388
            case this.matchAny(
21,298!
3389
                TokenKind.False,
3390
                TokenKind.True,
3391
                TokenKind.Invalid,
3392
                TokenKind.IntegerLiteral,
3393
                TokenKind.LongIntegerLiteral,
3394
                TokenKind.FloatLiteral,
3395
                TokenKind.DoubleLiteral,
3396
                TokenKind.StringLiteral
3397
            ):
3398
                return new LiteralExpression({ value: this.previous() });
9,156✔
3399

3400
            //capture source literals (LINE_NUM if brightscript, or a bunch of them if brighterscript)
3401
            case this.matchAny(TokenKind.LineNumLiteral, ...(this.options.mode === ParseMode.BrightScript ? [] : BrighterScriptSourceLiterals)):
12,142✔
3402
                return new SourceLiteralExpression({ value: this.previous() });
35✔
3403

3404
            //template string
3405
            case this.check(TokenKind.BackTick):
3406
                return this.templateString(false);
47✔
3407

3408
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
3409
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
23,329✔
3410
                return this.templateString(true);
8✔
3411

3412
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3413
                return new VariableExpression({ name: this.previous() as Identifier });
11,268✔
3414

3415
            case this.match(TokenKind.LeftParen):
3416
                let left = this.previous();
67✔
3417
                let expr = this.expression();
67✔
3418
                let right = this.consume(
66✔
3419
                    DiagnosticMessages.unmatchedLeftToken(left.text, 'expression'),
3420
                    TokenKind.RightParen
3421
                );
3422
                return new GroupingExpression({ leftParen: left, rightParen: right, expression: expr });
66✔
3423

3424
            case this.matchAny(TokenKind.LeftSquareBracket):
3425
                return this.arrayLiteral();
177✔
3426

3427
            case this.match(TokenKind.LeftCurlyBrace):
3428
                return this.aaLiteral();
387✔
3429

3430
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
UNCOV
3431
                let token = Object.assign(this.previous(), {
×
3432
                    kind: TokenKind.Identifier
3433
                }) as Identifier;
UNCOV
3434
                return new VariableExpression({ name: token });
×
3435

3436
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
UNCOV
3437
                return this.anonymousFunction();
×
3438

3439
            case this.check(TokenKind.RegexLiteral):
3440
                return this.regexLiteralExpression();
45✔
3441

3442
            default:
3443
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
3444
                if (this.checkAny(...this.peekGlobalTerminators())) {
108!
3445
                    //don't throw a diagnostic, just return undefined
3446

3447
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
3448
                } else {
3449
                    this.diagnostics.push({
108✔
3450
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
3451
                        location: this.peek()?.location
324!
3452
                    });
3453
                    throw this.lastDiagnosticAsError();
108✔
3454
                }
3455
        }
3456
    }
3457

3458
    private arrayLiteral() {
3459
        let elements: Array<Expression> = [];
177✔
3460
        let openingSquare = this.previous();
177✔
3461

3462
        while (this.match(TokenKind.Newline)) {
177✔
3463
        }
3464
        let closingSquare: Token;
3465

3466
        if (!this.match(TokenKind.RightSquareBracket)) {
177✔
3467
            try {
135✔
3468
                elements.push(this.expression());
135✔
3469

3470
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
134✔
3471

3472
                    while (this.match(TokenKind.Newline)) {
197✔
3473

3474
                    }
3475

3476
                    if (this.check(TokenKind.RightSquareBracket)) {
197✔
3477
                        break;
37✔
3478
                    }
3479

3480
                    elements.push(this.expression());
160✔
3481
                }
3482
            } catch (error: any) {
3483
                this.rethrowNonDiagnosticError(error);
2✔
3484
            }
3485

3486
            closingSquare = this.tryConsume(
135✔
3487
                DiagnosticMessages.unmatchedLeftToken(openingSquare.text, 'array literal'),
3488
                TokenKind.RightSquareBracket
3489
            );
3490
        } else {
3491
            closingSquare = this.previous();
42✔
3492
        }
3493

3494
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3495
        return new ArrayLiteralExpression({ elements: elements, open: openingSquare, close: closingSquare });
177✔
3496
    }
3497

3498
    private aaLiteral() {
3499
        let openingBrace = this.previous();
387✔
3500
        let members: Array<AAMemberExpression | AAIndexedMemberExpression> = [];
387✔
3501

3502
        let key = () => {
387✔
3503
            let result = {
417✔
3504
                colon: null as Token,
3505
                keyToken: null as Token,
3506
                key: null as Expression,
3507
                leftBracket: null as Token,
3508
                rightBracket: null as Token,
3509
                range: null as Range
3510
            };
3511
            if (this.check(TokenKind.LeftSquareBracket)) {
417✔
3512
                // Computed key: [expr]
3513
                result.leftBracket = this.advance();
31✔
3514
                result.key = this.expression();
31✔
3515
                result.rightBracket = this.tryConsumeToken(TokenKind.RightSquareBracket);
31✔
3516
            } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
386✔
3517
                result.keyToken = this.identifier(...AllowedProperties);
333✔
3518
            } else if (this.check(TokenKind.StringLiteral)) {
53!
3519
                result.keyToken = this.advance();
53✔
3520
            } else {
UNCOV
3521
                this.diagnostics.push({
×
3522
                    ...DiagnosticMessages.unexpectedAAKey(),
3523
                    location: this.peek().location
3524
                });
UNCOV
3525
                throw this.lastDiagnosticAsError();
×
3526
            }
3527

3528
            result.colon = this.consume(
417✔
3529
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3530
                TokenKind.Colon
3531
            );
3532
            result.range = util.createBoundingRange(result.keyToken ?? result.leftBracket, result.colon);
416✔
3533
            return result;
416✔
3534
        };
3535

3536
        while (this.match(TokenKind.Newline)) { }
387✔
3537
        let closingBrace: Token;
3538
        if (!this.match(TokenKind.RightCurlyBrace)) {
387✔
3539
            let lastAAMember: AAMemberExpression | AAIndexedMemberExpression;
3540
            try {
294✔
3541
                let k = key();
294✔
3542
                let expr = this.expression();
294✔
3543
                lastAAMember = k.key
293✔
3544
                    ? new AAIndexedMemberExpression({ leftBracket: k.leftBracket, key: k.key, rightBracket: k.rightBracket, colon: k.colon, value: expr })
293✔
3545
                    : new AAMemberExpression({
3546
                        key: k.keyToken,
3547
                        colon: k.colon,
3548
                        value: expr
3549
                    });
3550
                members.push(lastAAMember);
293✔
3551

3552
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
293✔
3553
                    // collect comma at end of expression
3554
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
297✔
3555
                        (lastAAMember as DeepWriteable<AAMemberExpression>).tokens.comma = this.previous();
94✔
3556
                    }
3557

3558
                    this.consumeStatementSeparators(true);
297✔
3559

3560
                    if (this.check(TokenKind.RightCurlyBrace)) {
297✔
3561
                        break;
174✔
3562
                    }
3563
                    let k = key();
123✔
3564
                    let expr = this.expression();
122✔
3565
                    lastAAMember = k.key
122✔
3566
                        ? new AAIndexedMemberExpression({ leftBracket: k.leftBracket, key: k.key, rightBracket: k.rightBracket, colon: k.colon, value: expr })
122✔
3567
                        : new AAMemberExpression({
3568
                            key: k.keyToken,
3569
                            colon: k.colon,
3570
                            value: expr
3571
                        });
3572
                    members.push(lastAAMember);
122✔
3573

3574
                }
3575
            } catch (error: any) {
3576
                this.rethrowNonDiagnosticError(error);
2✔
3577
            }
3578

3579
            closingBrace = this.tryConsume(
294✔
3580
                DiagnosticMessages.unmatchedLeftToken(openingBrace.text, 'associative array literal'),
3581
                TokenKind.RightCurlyBrace
3582
            );
3583
        } else {
3584
            closingBrace = this.previous();
93✔
3585
        }
3586

3587
        const aaExpr = new AALiteralExpression({ elements: members, open: openingBrace, close: closingBrace });
387✔
3588
        return aaExpr;
387✔
3589
    }
3590

3591
    /**
3592
     * Pop token if we encounter specified token
3593
     */
3594
    private match(tokenKind: TokenKind) {
3595
        if (this.check(tokenKind)) {
76,661✔
3596
            this.current++; //advance
8,294✔
3597
            return true;
8,294✔
3598
        }
3599
        return false;
68,367✔
3600
    }
3601

3602
    /**
3603
     * Pop token if we encounter a token in the specified list
3604
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3605
     */
3606
    private matchAny(...tokenKinds: TokenKind[]) {
3607
        for (let tokenKind of tokenKinds) {
280,982✔
3608
            if (this.check(tokenKind)) {
813,304✔
3609
                this.current++; //advance
74,280✔
3610
                return true;
74,280✔
3611
            }
3612
        }
3613
        return false;
206,702✔
3614
    }
3615

3616
    /**
3617
     * If the next series of tokens matches the given set of tokens, pop them all
3618
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
3619
     */
3620
    private matchSequence(...tokenKinds: TokenKind[]) {
3621
        const endIndex = this.current + tokenKinds.length;
24,164✔
3622
        for (let i = 0; i < tokenKinds.length; i++) {
24,164✔
3623
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
24,195!
3624
                return false;
24,161✔
3625
            }
3626
        }
3627
        this.current = endIndex;
3✔
3628
        return true;
3✔
3629
    }
3630

3631
    /**
3632
     * Get next token matching a specified list, or fail with an error
3633
     */
3634
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
3635
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
25,062✔
3636
        if (token) {
25,062✔
3637
            return token;
25,035✔
3638
        } else {
3639
            let error = new Error(diagnosticInfo.message);
27✔
3640
            (error as any).isDiagnostic = true;
27✔
3641
            throw error;
27✔
3642
        }
3643
    }
3644

3645
    /**
3646
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3647
     */
3648
    private consumeTokenIf(tokenKind: TokenKind) {
3649
        if (this.match(tokenKind)) {
4,855✔
3650
            return this.previous();
428✔
3651
        }
3652
    }
3653

3654
    private consumeToken(tokenKind: TokenKind) {
3655
        return this.consume(
3,025✔
3656
            DiagnosticMessages.expectedToken(tokenKind),
3657
            tokenKind
3658
        );
3659
    }
3660

3661
    /**
3662
     * Consume, or add a message if not found. But then continue and return undefined
3663
     */
3664
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3665
        const nextKind = this.peek().kind;
34,837✔
3666
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
63,120✔
3667

3668
        if (foundTokenKind) {
34,837✔
3669
            return this.advance();
34,694✔
3670
        }
3671
        this.diagnostics.push({
143✔
3672
            ...diagnostic,
3673
            location: this.peek()?.location
429!
3674
        });
3675
    }
3676

3677
    private tryConsumeToken(tokenKind: TokenKind) {
3678
        return this.tryConsume(
129✔
3679
            DiagnosticMessages.expectedToken(tokenKind),
3680
            tokenKind
3681
        );
3682
    }
3683

3684
    private consumeStatementSeparators(optional = false) {
13,117✔
3685
        //a comment or EOF mark the end of the statement
3686
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
40,160✔
3687
            return true;
791✔
3688
        }
3689
        let consumed = false;
39,369✔
3690
        //consume any newlines and colons
3691
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
39,369✔
3692
            consumed = true;
42,768✔
3693
        }
3694
        if (!optional && !consumed) {
39,369✔
3695
            this.diagnostics.push({
77✔
3696
                ...DiagnosticMessages.expectedNewlineOrColon(),
3697
                location: this.peek()?.location
231!
3698
            });
3699
        }
3700
        return consumed;
39,369✔
3701
    }
3702

3703
    private advance(): Token {
3704
        if (!this.isAtEnd()) {
68,824✔
3705
            this.current++;
68,810✔
3706
        }
3707
        return this.previous();
68,824✔
3708
    }
3709

3710
    private checkEndOfStatement(): boolean {
3711
        const nextKind = this.peek().kind;
9,912✔
3712
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
9,912✔
3713
    }
3714

3715
    private checkPrevious(tokenKind: TokenKind): boolean {
3716
        return this.previous()?.kind === tokenKind;
310!
3717
    }
3718

3719
    /**
3720
     * Check that the next token kind is the expected kind
3721
     * @param tokenKind the expected next kind
3722
     * @returns true if the next tokenKind is the expected value
3723
     */
3724
    private check(tokenKind: TokenKind): boolean {
3725
        const nextKind = this.peek().kind;
1,319,258✔
3726
        if (nextKind === TokenKind.Eof) {
1,319,258✔
3727
            return false;
14,362✔
3728
        }
3729
        return nextKind === tokenKind;
1,304,896✔
3730
    }
3731

3732
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3733
        const nextKind = this.peek().kind;
222,657✔
3734
        if (nextKind === TokenKind.Eof) {
222,657✔
3735
            return false;
1,428✔
3736
        }
3737
        return tokenKinds.includes(nextKind);
221,229✔
3738
    }
3739

3740
    private checkNext(tokenKind: TokenKind): boolean {
3741
        if (this.isAtEnd()) {
18,350!
UNCOV
3742
            return false;
×
3743
        }
3744
        return this.peekNext().kind === tokenKind;
18,350✔
3745
    }
3746

3747
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3748
        if (this.isAtEnd()) {
7,845!
UNCOV
3749
            return false;
×
3750
        }
3751
        const nextKind = this.peekNext().kind;
7,845✔
3752
        return tokenKinds.includes(nextKind);
7,845✔
3753
    }
3754

3755
    private isAtEnd(): boolean {
3756
        const peekToken = this.peek();
202,844✔
3757
        return !peekToken || peekToken.kind === TokenKind.Eof;
202,844✔
3758
    }
3759

3760
    private peekNext(): Token {
3761
        if (this.isAtEnd()) {
26,195!
UNCOV
3762
            return this.peek();
×
3763
        }
3764
        return this.tokens[this.current + 1];
26,195✔
3765
    }
3766

3767
    private peek(): Token {
3768
        return this.tokens[this.current];
1,818,594✔
3769
    }
3770

3771
    private previous(): Token {
3772
        return this.tokens[this.current - 1];
117,930✔
3773
    }
3774

3775
    /**
3776
     * Sometimes we catch an error that is a diagnostic.
3777
     * If that's the case, we want to continue parsing.
3778
     * Otherwise, re-throw the error
3779
     *
3780
     * @param error error caught in a try/catch
3781
     */
3782
    private rethrowNonDiagnosticError(error) {
3783
        if (!error.isDiagnostic) {
16!
UNCOV
3784
            throw error;
×
3785
        }
3786
    }
3787

3788
    /**
3789
     * Get the token that is {offset} indexes away from {this.current}
3790
     * @param offset the number of index steps away from current index to fetch
3791
     * @param tokenKinds the desired token must match one of these
3792
     * @example
3793
     * getToken(-1); //returns the previous token.
3794
     * getToken(0);  //returns current token.
3795
     * getToken(1);  //returns next token
3796
     */
3797
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3798
        const token = this.tokens[this.current + offset];
167✔
3799
        if (tokenKinds.includes(token.kind)) {
167✔
3800
            return token;
3✔
3801
        }
3802
    }
3803

3804
    private synchronize() {
3805
        this.advance(); // skip the erroneous token
121✔
3806

3807
        while (!this.isAtEnd()) {
121✔
3808
            if (this.ensureNewLineOrColon(true)) {
242✔
3809
                // end of statement reached
3810
                return;
78✔
3811
            }
3812

3813
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
164✔
3814
                case TokenKind.Namespace:
2!
3815
                case TokenKind.Class:
3816
                case TokenKind.Function:
3817
                case TokenKind.Sub:
3818
                case TokenKind.If:
3819
                case TokenKind.For:
3820
                case TokenKind.ForEach:
3821
                case TokenKind.While:
3822
                case TokenKind.Print:
3823
                case TokenKind.Return:
3824
                    // start parsing again from the next block starter or obvious
3825
                    // expression start
3826
                    return;
1✔
3827
            }
3828

3829
            this.advance();
163✔
3830
        }
3831
    }
3832

3833

3834
    public dispose() {
3835
    }
3836
}
3837

3838
export enum ParseMode {
1✔
3839
    BrightScript = 'BrightScript',
1✔
3840
    BrighterScript = 'BrighterScript'
1✔
3841
}
3842

3843
export interface ParseOptions {
3844
    /**
3845
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3846
     */
3847
    mode?: ParseMode;
3848
    /**
3849
     * A logger that should be used for logging. If omitted, a default logger is used
3850
     */
3851
    logger?: Logger;
3852
    /**
3853
     * Path to the file where this source code originated
3854
     */
3855
    srcPath?: string;
3856
    /**
3857
     * Should locations be tracked. If false, the `range` property will be omitted
3858
     * @default true
3859
     */
3860
    trackLocations?: boolean;
3861
    /**
3862
     * A map of BrightScript constants. If a constant is present in this map, it will be treated as a compile-time constant.
3863
     */
3864
    bsConsts?: Map<string, boolean>;
3865

3866
    /**
3867
     * The minimum Roku firmware version required to run this project.
3868
     * When set to '15.3' or higher, line continuation (multi-line expressions in `.brs` files)
3869
     * is enabled even in BrightScript mode because Roku OS 15.3 added native support for it.
3870
     * Should be a semver-compatible string (e.g. '15.3.0').
3871
     */
3872
    minFirmwareVersion?: string;
3873
}
3874

3875

3876
class CancelStatementError extends Error {
3877
    constructor() {
3878
        super('CancelStatement');
2✔
3879
    }
3880
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc