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

rokucommunity / brighterscript / #15136

28 Jan 2026 04:38PM UTC coverage: 87.192% (-0.006%) from 87.198%
#15136

push

web-flow
Merge f28626da6 into 610607efc

14642 of 17747 branches covered (82.5%)

Branch coverage included in aggregate %.

76 of 78 new or added lines in 11 files covered. (97.44%)

201 existing lines in 9 files now uncovered.

15401 of 16709 relevant lines covered (92.17%)

24803.9 hits per line

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

91.25
/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
    AALiteralExpression,
70
    AAMemberExpression,
71
    AnnotationExpression,
72
    ArrayLiteralExpression,
73
    BinaryExpression,
74
    CallExpression,
75
    CallfuncExpression,
76
    DottedGetExpression,
77
    EscapedCharCodeLiteralExpression,
78
    FunctionExpression,
79
    FunctionParameterExpression,
80
    GroupingExpression,
81
    IndexedGetExpression,
82
    LiteralExpression,
83
    NewExpression,
84
    NullCoalescingExpression,
85
    RegexLiteralExpression,
86
    SourceLiteralExpression,
87
    TaggedTemplateStringExpression,
88
    TemplateStringExpression,
89
    TemplateStringQuasiExpression,
90
    TernaryExpression,
91
    TypecastExpression,
92
    TypeExpression,
93
    TypedArrayExpression,
94
    UnaryExpression,
95
    VariableExpression,
96
    XmlAttributeGetExpression,
97
    PrintSeparatorExpression,
98
    InlineInterfaceExpression,
99
    InlineInterfaceMemberExpression
100
} from './Expression';
101
import type { Range } from 'vscode-languageserver';
102
import type { Logger } from '../logging';
103
import { createLogger } from '../logging';
1✔
104
import { isAnnotationExpression, isCallExpression, isCallfuncExpression, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression, isConditionalCompileStatement, isLiteralBoolean, isTypecastExpression } from '../astUtils/reflection';
1✔
105
import { createStringLiteral, createToken } from '../astUtils/creators';
1✔
106
import type { Expression, Statement } from './AstNode';
107
import type { BsDiagnostic, DeepWriteable } from '../interfaces';
108

109

110
const declarableTypesLower = DeclarableTypes.map(tokenKind => tokenKind.toLowerCase());
10✔
111

112
export class Parser {
1✔
113
    /**
114
     * The array of tokens passed to `parse()`
115
     */
116
    public tokens = [] as Token[];
4,187✔
117

118
    /**
119
     * The current token index
120
     */
121
    public current: number;
122

123
    /**
124
     * The list of statements for the parsed file
125
     */
126
    public ast = new Body({ statements: [] });
4,187✔
127

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

135
    /**
136
     * The top-level symbol table for the body of this file.
137
     */
138
    public get symbolTable() {
139
        return this.ast.symbolTable;
19,509✔
140
    }
141

142
    /**
143
     * The list of diagnostics found during the parse process
144
     */
145
    public diagnostics: BsDiagnostic[];
146

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

152
    /**
153
     * The options used to parse the file
154
     */
155
    public options: ParseOptions;
156

157
    private globalTerminators = [] as TokenKind[][];
4,187✔
158

159
    /**
160
     * 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
161
     * based on the parse mode
162
     */
163
    private allowedLocalIdentifiers: TokenKind[];
164

165
    /**
166
     * Annotations collected which should be attached to the next statement
167
     */
168
    private pendingAnnotations: AnnotationExpression[];
169

170
    /**
171
     * Get the currently active global terminators
172
     */
173
    private peekGlobalTerminators() {
174
        return this.globalTerminators[this.globalTerminators.length - 1] ?? [];
19,658✔
175
    }
176

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

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

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

214
        this.ast = this.body();
4,164✔
215
        this.ast.bsConsts = options.bsConsts;
4,164✔
216
        //now that we've built the AST, link every node to its parent
217
        this.ast.link();
4,164✔
218
        return this;
4,164✔
219
    }
220

221
    private logger: Logger;
222

223
    private body() {
224
        const parentAnnotations = this.enterAnnotationBlock();
4,835✔
225

226
        let body = new Body({ statements: [] });
4,835✔
227
        if (this.tokens.length > 0) {
4,835✔
228
            this.consumeStatementSeparators(true);
4,834✔
229

230
            try {
4,834✔
231
                while (
4,834✔
232
                    //not at end of tokens
233
                    !this.isAtEnd() &&
19,525✔
234
                    //the next token is not one of the end terminators
235
                    !this.checkAny(...this.peekGlobalTerminators())
236
                ) {
237
                    let dec = this.declaration();
7,011✔
238
                    if (dec) {
7,011✔
239
                        if (!isAnnotationExpression(dec)) {
6,947✔
240
                            this.consumePendingAnnotations(dec);
6,896✔
241
                            body.statements.push(dec);
6,896✔
242
                            //ensure statement separator
243
                            this.consumeStatementSeparators(false);
6,896✔
244
                        } else {
245
                            this.consumeStatementSeparators(true);
51✔
246
                        }
247
                    }
248
                }
249
            } catch (parseError) {
250
                //do nothing with the parse error for now. perhaps we can remove this?
251
                console.error(parseError);
×
252
            }
253
        }
254

255
        this.exitAnnotationBlock(parentAnnotations);
4,835✔
256
        return body;
4,835✔
257
    }
258

259
    private sanitizeParseOptions(options: ParseOptions) {
260
        options ??= {
4,164✔
261
            srcPath: undefined
262
        };
263
        options.mode ??= ParseMode.BrightScript;
4,164✔
264
        options.trackLocations ??= true;
4,164✔
265
        return options;
4,164✔
266
    }
267

268
    /**
269
     * Determine if the parser is currently parsing tokens at the root level.
270
     */
271
    private isAtRootLevel() {
272
        return this.namespaceAndFunctionDepth === 0;
60,270✔
273
    }
274

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

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

297
    private declaration(): Statement | AnnotationExpression | undefined {
298
        try {
16,116✔
299
            if (this.checkAny(TokenKind.HashConst)) {
16,116✔
300
                return this.conditionalCompileConstStatement();
21✔
301
            }
302
            if (this.checkAny(TokenKind.HashIf)) {
16,095✔
303
                return this.conditionalCompileStatement();
43✔
304
            }
305
            if (this.checkAny(TokenKind.HashError)) {
16,052✔
306
                return this.conditionalCompileErrorStatement();
10✔
307
            }
308

309
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
16,042✔
310
                return this.functionDeclaration(false);
3,833✔
311
            }
312

313
            if (this.checkLibrary()) {
12,209✔
314
                return this.libraryStatement();
13✔
315
            }
316

317
            if (this.checkAlias()) {
12,196✔
318
                return this.aliasStatement();
33✔
319
            }
320
            if (this.checkTypeStatement()) {
12,163✔
321
                return this.typeStatement();
27✔
322
            }
323

324
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
12,136✔
325
                return this.constDeclaration();
192✔
326
            }
327

328
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
11,944✔
329
                return this.annotationExpression();
58✔
330
            }
331

332
            //catch certain global terminators to prevent unnecessary lookahead (i.e. like `end namespace`, no need to continue)
333
            if (this.checkAny(...this.peekGlobalTerminators())) {
11,886!
334
                return;
×
335
            }
336

337
            return this.statement();
11,886✔
338
        } catch (error: any) {
339
            //if the error is not a diagnostic, then log the error for debugging purposes
340
            if (!error.isDiagnostic) {
110✔
341
                this.logger.error(error);
1✔
342
            }
343
            this.synchronize();
110✔
344
        }
345
    }
346

347
    /**
348
     * Try to get an identifier. If not found, add diagnostic and return undefined
349
     */
350
    private tryIdentifier(...additionalTokenKinds: TokenKind[]): Identifier | undefined {
351
        const identifier = this.tryConsume(
187✔
352
            DiagnosticMessages.expectedIdentifier(),
353
            TokenKind.Identifier,
354
            ...additionalTokenKinds
355
        ) as Identifier;
356
        if (identifier) {
187✔
357
            // force the name into an identifier so the AST makes some sense
358
            identifier.kind = TokenKind.Identifier;
186✔
359
            return identifier;
186✔
360
        }
361
    }
362

363
    private identifier(...additionalTokenKinds: TokenKind[]) {
364
        const identifier = this.consume(
1,113✔
365
            DiagnosticMessages.expectedIdentifier(),
366
            TokenKind.Identifier,
367
            ...additionalTokenKinds
368
        ) as Identifier;
369
        // force the name into an identifier so the AST makes some sense
370
        identifier.kind = TokenKind.Identifier;
1,113✔
371
        return identifier;
1,113✔
372
    }
373

374
    private enumMemberStatement() {
375
        const name = this.consume(
367✔
376
            DiagnosticMessages.expectedIdentifier(),
377
            TokenKind.Identifier,
378
            ...AllowedProperties
379
        ) as Identifier;
380
        let equalsToken: Token;
381
        let value: Expression;
382
        //look for `= SOME_EXPRESSION`
383
        if (this.check(TokenKind.Equal)) {
367✔
384
            equalsToken = this.advance();
207✔
385
            value = this.expression();
207✔
386
        }
387
        const statement = new EnumMemberStatement({ name: name, equals: equalsToken, value: value });
367✔
388
        return statement;
367✔
389
    }
390

391
    /**
392
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration`
393
     */
394
    private interfaceFieldStatement(optionalKeyword?: Token) {
395
        const name = this.identifier(...AllowedProperties);
258✔
396
        let asToken;
397
        let typeExpression;
398
        if (this.check(TokenKind.As)) {
258✔
399
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
239✔
400
        }
401
        return new InterfaceFieldStatement({ name: name, as: asToken, typeExpression: typeExpression, optional: optionalKeyword });
258✔
402
    }
403

404
    private consumeAsTokenAndTypeExpression(ignoreDiagnostics = false): [Token, TypeExpression] {
1,950✔
405
        let asToken = this.consumeToken(TokenKind.As);
1,969✔
406
        let typeExpression: TypeExpression;
407
        if (asToken) {
1,969!
408
            //if there's nothing after the `as`, add a diagnostic and continue
409
            if (this.checkEndOfStatement()) {
1,969✔
410
                if (!ignoreDiagnostics) {
2✔
411
                    this.diagnostics.push({
1✔
412
                        ...DiagnosticMessages.expectedIdentifier(asToken.text),
413
                        location: asToken.location
414
                    });
415
                }
416
                //consume the statement separator
417
                this.consumeStatementSeparators();
2✔
418
            } else if (!this.checkAny(TokenKind.Identifier, TokenKind.LeftCurlyBrace, TokenKind.LeftParen, ...DeclarableTypes, ...AllowedTypeIdentifiers)) {
1,967✔
419
                if (!ignoreDiagnostics) {
9!
420
                    this.diagnostics.push({
9✔
421
                        ...DiagnosticMessages.expectedIdentifier(asToken.text),
422
                        location: asToken.location
423
                    });
424
                }
425
            } else {
426
                typeExpression = this.typeExpression();
1,958✔
427
            }
428
        }
429
        if (this.checkAny(TokenKind.And, TokenKind.Or)) {
1,968✔
430
            this.warnIfNotBrighterScriptMode('custom types');
2✔
431
        }
432
        return [asToken, typeExpression];
1,968✔
433
    }
434

435
    /**
436
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration()`
437
     */
438
    private interfaceMethodStatement(optionalKeyword?: Token) {
439
        const functionType = this.advance();
53✔
440
        const name = this.identifier(...AllowedProperties);
53✔
441
        const leftParen = this.consume(DiagnosticMessages.expectedToken(TokenKind.LeftParen), TokenKind.LeftParen);
53✔
442

443
        let params = [] as FunctionParameterExpression[];
53✔
444
        if (!this.check(TokenKind.RightParen)) {
53✔
445
            do {
18✔
446
                if (params.length >= CallExpression.MaximumArguments) {
28!
447
                    this.diagnostics.push({
×
448
                        ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments),
449
                        location: this.peek().location
450
                    });
451
                }
452

453
                params.push(this.functionParameter());
28✔
454
            } while (this.match(TokenKind.Comma));
455
        }
456
        const rightParen = this.consumeToken(TokenKind.RightParen);
53✔
457
        // let asToken = null as Token;
458
        // let returnTypeExpression: TypeExpression;
459
        let asToken: Token;
460
        let returnTypeExpression: TypeExpression;
461
        if (this.check(TokenKind.As)) {
53✔
462
            [asToken, returnTypeExpression] = this.consumeAsTokenAndTypeExpression();
40✔
463
        }
464

465
        return new InterfaceMethodStatement({
53✔
466
            functionType: functionType,
467
            name: name,
468
            leftParen: leftParen,
469
            params: params,
470
            rightParen: rightParen,
471
            as: asToken,
472
            returnTypeExpression: returnTypeExpression,
473
            optional: optionalKeyword
474
        });
475
    }
476

477
    private interfaceDeclaration(): InterfaceStatement {
478
        this.warnIfNotBrighterScriptMode('interface declarations');
227✔
479

480
        const parentAnnotations = this.enterAnnotationBlock();
227✔
481

482
        const interfaceToken = this.consume(
227✔
483
            DiagnosticMessages.expectedKeyword(TokenKind.Interface),
484
            TokenKind.Interface
485
        );
486
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
227✔
487

488
        let extendsToken: Token;
489
        let parentInterfaceName: TypeExpression;
490

491
        if (this.peek().text.toLowerCase() === 'extends') {
227✔
492
            extendsToken = this.advance();
11✔
493
            if (this.checkEndOfStatement()) {
11!
494
                this.diagnostics.push({
×
495
                    ...DiagnosticMessages.expectedIdentifier(extendsToken.text),
496
                    location: extendsToken.location
497
                });
498
            } else {
499
                parentInterfaceName = this.typeExpression();
11✔
500
            }
501
        }
502
        this.consumeStatementSeparators();
227✔
503
        //gather up all interface members (Fields, Methods)
504
        let body = [] as Statement[];
227✔
505
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
227✔
506
            try {
540✔
507
                //break out of this loop if we encountered the `EndInterface` token not followed by `as`
508
                if (this.check(TokenKind.EndInterface) && !this.checkNext(TokenKind.As)) {
540✔
509
                    break;
227✔
510
                }
511

512
                let decl: Statement;
513

514
                //collect leading annotations
515
                if (this.check(TokenKind.At)) {
313✔
516
                    this.annotationExpression();
2✔
517
                }
518
                const optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
313✔
519
                //fields
520
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkAnyNext(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
313✔
521
                    decl = this.interfaceFieldStatement(optionalKeyword);
256✔
522
                    //field with name = 'optional'
523
                } else if (optionalKeyword && this.checkAny(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
57✔
524
                    //rewind one place, so that 'optional' is the field name
525
                    this.current--;
2✔
526
                    decl = this.interfaceFieldStatement();
2✔
527

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

532
                }
533
                if (decl) {
313✔
534
                    this.consumePendingAnnotations(decl);
311✔
535
                    body.push(decl);
311✔
536
                } else {
537
                    //we didn't find a declaration...flag tokens until next line
538
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
539
                }
540
            } catch (e) {
541
                //throw out any failed members and move on to the next line
542
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
543
            }
544

545
            //ensure statement separator
546
            this.consumeStatementSeparators();
313✔
547
        }
548

549
        //consume the final `end interface` token
550
        const endInterfaceToken = this.consumeToken(TokenKind.EndInterface);
227✔
551

552
        const statement = new InterfaceStatement({
227✔
553
            interface: interfaceToken,
554
            name: nameToken,
555
            extends: extendsToken,
556
            parentInterfaceName: parentInterfaceName,
557
            body: body,
558
            endInterface: endInterfaceToken
559
        });
560
        this.exitAnnotationBlock(parentAnnotations);
227✔
561
        return statement;
227✔
562
    }
563

564
    private enumDeclaration(): EnumStatement {
565
        const enumToken = this.consume(
187✔
566
            DiagnosticMessages.expectedKeyword(TokenKind.Enum),
567
            TokenKind.Enum
568
        );
569
        const nameToken = this.tryIdentifier(...this.allowedLocalIdentifiers);
187✔
570

571
        this.warnIfNotBrighterScriptMode('enum declarations');
187✔
572

573
        const parentAnnotations = this.enterAnnotationBlock();
187✔
574

575
        this.consumeStatementSeparators();
187✔
576

577
        const body: Array<EnumMemberStatement> = [];
187✔
578
        //gather up all members
579
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
187✔
580
            try {
367✔
581
                let decl: EnumMemberStatement;
582

583
                //collect leading annotations
584
                if (this.check(TokenKind.At)) {
367!
585
                    this.annotationExpression();
×
586
                }
587

588
                //members
589
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
367!
590
                    decl = this.enumMemberStatement();
367✔
591
                }
592

593
                if (decl) {
367!
594
                    this.consumePendingAnnotations(decl);
367✔
595
                    body.push(decl);
367✔
596
                } else {
597
                    //we didn't find a declaration...flag tokens until next line
598
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
599
                }
600
            } catch (e) {
601
                //throw out any failed members and move on to the next line
602
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
603
            }
604

605
            //ensure statement separator
606
            this.consumeStatementSeparators();
367✔
607
            //break out of this loop if we encountered the `EndEnum` token
608
            if (this.check(TokenKind.EndEnum)) {
367✔
609
                break;
175✔
610
            }
611
        }
612

613
        //consume the final `end interface` token
614
        const endEnumToken = this.consumeToken(TokenKind.EndEnum);
187✔
615

616
        const result = new EnumStatement({
186✔
617
            enum: enumToken,
618
            name: nameToken,
619
            body: body,
620
            endEnum: endEnumToken
621
        });
622

623
        this.exitAnnotationBlock(parentAnnotations);
186✔
624
        return result;
186✔
625
    }
626

627
    /**
628
     * A BrighterScript class declaration
629
     */
630
    private classDeclaration(): ClassStatement {
631
        this.warnIfNotBrighterScriptMode('class declarations');
716✔
632

633
        const parentAnnotations = this.enterAnnotationBlock();
716✔
634

635
        let classKeyword = this.consume(
716✔
636
            DiagnosticMessages.expectedKeyword(TokenKind.Class),
637
            TokenKind.Class
638
        );
639
        let extendsKeyword: Token;
640
        let parentClassName: TypeExpression;
641

642
        //get the class name
643
        let className = this.tryConsume(DiagnosticMessages.expectedIdentifier('class'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
716✔
644

645
        //see if the class inherits from parent
646
        if (this.peek().text.toLowerCase() === 'extends') {
716✔
647
            extendsKeyword = this.advance();
107✔
648
            if (this.checkEndOfStatement()) {
107✔
649
                this.diagnostics.push({
1✔
650
                    ...DiagnosticMessages.expectedIdentifier(extendsKeyword.text),
651
                    location: extendsKeyword.location
652
                });
653
            } else {
654
                parentClassName = this.typeExpression();
106✔
655
            }
656
        }
657

658
        //ensure statement separator
659
        this.consumeStatementSeparators();
716✔
660

661
        //gather up all class members (Fields, Methods)
662
        let body = [] as Statement[];
716✔
663
        while (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private, TokenKind.Function, TokenKind.Sub, TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
716✔
664
            try {
738✔
665
                let decl: Statement;
666
                let accessModifier: Token;
667

668
                if (this.check(TokenKind.At)) {
738✔
669
                    this.annotationExpression();
15✔
670
                }
671

672
                if (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private)) {
737✔
673
                    //use actual access modifier
674
                    accessModifier = this.advance();
97✔
675
                }
676

677
                let overrideKeyword: Token;
678
                if (this.peek().text.toLowerCase() === 'override') {
737✔
679
                    overrideKeyword = this.advance();
17✔
680
                }
681
                //methods (function/sub keyword OR identifier followed by opening paren)
682
                if (this.checkAny(TokenKind.Function, TokenKind.Sub) || (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.LeftParen))) {
737✔
683
                    const funcDeclaration = this.functionDeclaration(false, false);
371✔
684

685
                    //if we have an overrides keyword AND this method is called 'new', that's not allowed
686
                    if (overrideKeyword && funcDeclaration.tokens.name.text.toLowerCase() === 'new') {
371!
687
                        this.diagnostics.push({
×
688
                            ...DiagnosticMessages.cannotUseOverrideKeywordOnConstructorFunction(),
689
                            location: overrideKeyword.location
690
                        });
691
                    }
692

693
                    decl = new MethodStatement({
371✔
694
                        modifiers: accessModifier,
695
                        name: funcDeclaration.tokens.name,
696
                        func: funcDeclaration.func,
697
                        override: overrideKeyword
698
                    });
699

700
                    //fields
701
                } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
366✔
702

703
                    decl = this.fieldDeclaration(accessModifier);
352✔
704

705
                    //class fields cannot be overridden
706
                    if (overrideKeyword) {
351!
707
                        this.diagnostics.push({
×
708
                            ...DiagnosticMessages.classFieldCannotBeOverridden(),
709
                            location: overrideKeyword.location
710
                        });
711
                    }
712

713
                }
714

715
                if (decl) {
736✔
716
                    this.consumePendingAnnotations(decl);
722✔
717
                    body.push(decl);
722✔
718
                }
719
            } catch (e) {
720
                //throw out any failed members and move on to the next line
721
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
722
            }
723

724
            //ensure statement separator
725
            this.consumeStatementSeparators();
738✔
726
        }
727

728
        let endingKeyword = this.advance();
716✔
729
        if (endingKeyword.kind !== TokenKind.EndClass) {
716✔
730
            this.diagnostics.push({
5✔
731
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('class'),
732
                location: endingKeyword.location
733
            });
734
        }
735

736
        const result = new ClassStatement({
716✔
737
            class: classKeyword,
738
            name: className,
739
            body: body,
740
            endClass: endingKeyword,
741
            extends: extendsKeyword,
742
            parentClassName: parentClassName
743
        });
744

745
        this.exitAnnotationBlock(parentAnnotations);
716✔
746
        return result;
716✔
747
    }
748

749
    private fieldDeclaration(accessModifier: Token | null) {
750

751
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
352✔
752

753
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
352✔
754
            if (this.check(TokenKind.As)) {
351✔
755
                if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
5✔
756
                    // as <EOL>
757
                    // `as` is the field name
758
                } else if (this.checkNext(TokenKind.As)) {
4✔
759
                    //  as as ____
760
                    // first `as` is the field name
761
                } else if (optionalKeyword) {
2!
762
                    // optional as ____
763
                    // optional is the field name, `as` starts type
764
                    // rewind current token
765
                    optionalKeyword = null;
2✔
766
                    this.current--;
2✔
767
                }
768
            }
769
        } else {
770
            // no name after `optional` ... optional is the name
771
            // rewind current token
772
            optionalKeyword = null;
1✔
773
            this.current--;
1✔
774
        }
775

776
        let name = this.consume(
352✔
777
            DiagnosticMessages.expectedIdentifier(),
778
            TokenKind.Identifier,
779
            ...AllowedProperties
780
        ) as Identifier;
781

782
        let asToken: Token;
783
        let fieldTypeExpression: TypeExpression;
784
        //look for `as SOME_TYPE`
785
        if (this.check(TokenKind.As)) {
352✔
786
            [asToken, fieldTypeExpression] = this.consumeAsTokenAndTypeExpression();
241✔
787
        }
788

789
        let initialValue: Expression;
790
        let equal: Token;
791
        //if there is a field initializer
792
        if (this.check(TokenKind.Equal)) {
352✔
793
            equal = this.advance();
81✔
794
            initialValue = this.expression();
81✔
795
        }
796

797
        return new FieldStatement({
351✔
798
            accessModifier: accessModifier,
799
            name: name,
800
            as: asToken,
801
            typeExpression: fieldTypeExpression,
802
            equals: equal,
803
            initialValue: initialValue,
804
            optional: optionalKeyword
805
        });
806
    }
807

808
    /**
809
     * An array of CallExpression for the current function body
810
     */
811
    private callExpressions = [];
4,187✔
812

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

845
            if (isAnonymous) {
4,303✔
846
                leftParen = this.consume(
99✔
847
                    DiagnosticMessages.expectedToken('('),
848
                    TokenKind.LeftParen
849
                );
850
            } else {
851
                name = this.consume(
4,204✔
852
                    DiagnosticMessages.expectedIdentifier(functionTypeText),
853
                    TokenKind.Identifier,
854
                    ...AllowedProperties
855
                ) as Identifier;
856
                leftParen = this.consume(
4,202✔
857
                    DiagnosticMessages.expectedToken('('),
858
                    TokenKind.LeftParen
859
                );
860

861
                //prevent functions from ending with type designators
862
                let lastChar = name.text[name.text.length - 1];
4,199✔
863
                if (['$', '%', '!', '#', '&'].includes(lastChar)) {
4,199✔
864
                    //don't throw this error; let the parser continue
865
                    this.diagnostics.push({
13✔
866
                        ...DiagnosticMessages.invalidIdentifier(name.text, lastChar),
867
                        location: name.location
868
                    });
869
                }
870

871
                //flag functions with keywords for names (only for standard functions)
872
                if (checkIdentifier && DisallowedFunctionIdentifiersText.has(name.text.toLowerCase())) {
4,199✔
873
                    this.diagnostics.push({
2✔
874
                        ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
875
                        location: name.location
876
                    });
877
                }
878
            }
879

880
            let params = [] as FunctionParameterExpression[];
4,297✔
881
            let asToken: Token;
882
            let typeExpression: TypeExpression;
883
            if (!this.check(TokenKind.RightParen)) {
4,297✔
884
                do {
2,038✔
885
                    params.push(this.functionParameter());
3,476✔
886
                } while (this.match(TokenKind.Comma));
887
            }
888
            let rightParen = this.consume(
4,295✔
889
                DiagnosticMessages.unmatchedLeftToken(leftParen.text, 'function parameter list'),
890
                TokenKind.RightParen
891
            );
892
            if (this.check(TokenKind.As)) {
4,285✔
893
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
406✔
894
            }
895

896
            params.reduce((haveFoundOptional: boolean, param: FunctionParameterExpression) => {
4,285✔
897
                if (haveFoundOptional && !param.defaultValue) {
3,463!
898
                    this.diagnostics.push({
×
899
                        ...DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter(param.tokens.name.text),
900
                        location: param.location
901
                    });
902
                }
903

904
                return haveFoundOptional || !!param.defaultValue;
3,463✔
905
            }, false);
906

907
            this.consumeStatementSeparators(true);
4,285✔
908

909

910
            //support ending the function with `end sub` OR `end function`
911
            let body = this.block();
4,285✔
912
            //if the parser was unable to produce a block, make an empty one so the AST makes some sense...
913

914
            // consume 'end sub' or 'end function'
915
            const endFunctionType = this.advance();
4,285✔
916
            let expectedEndKind = isSub ? TokenKind.EndSub : TokenKind.EndFunction;
4,285✔
917

918
            //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
919
            //add an error but don't hard-fail so the AST can continue more gracefully
920
            if (endFunctionType.kind !== expectedEndKind) {
4,285✔
921
                this.diagnostics.push({
9✔
922
                    ...DiagnosticMessages.closingKeywordMismatch(functionTypeText, endFunctionType.text),
923
                    location: endFunctionType.location
924
                });
925
            }
926

927
            if (!body) {
4,285✔
928
                body = new Block({ statements: [] });
3✔
929
            }
930

931
            let func = new FunctionExpression({
4,285✔
932
                parameters: params,
933
                body: body,
934
                functionType: functionType,
935
                endFunctionType: endFunctionType,
936
                leftParen: leftParen,
937
                rightParen: rightParen,
938
                as: asToken,
939
                returnTypeExpression: typeExpression
940
            });
941

942
            if (isAnonymous) {
4,285✔
943
                return func;
98✔
944
            } else {
945
                let result = new FunctionStatement({ name: name, func: func });
4,187✔
946
                return result;
4,187✔
947
            }
948
        } finally {
949
            this.namespaceAndFunctionDepth--;
4,303✔
950
            //restore the previous CallExpression list
951
            this.callExpressions = previousCallExpressions;
4,303✔
952
        }
953
    }
954

955
    private functionParameter(): FunctionParameterExpression {
956
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
3,504✔
957
            this.diagnostics.push({
1✔
958
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
959
                location: this.peek().location
960
            });
961
            throw this.lastDiagnosticAsError();
1✔
962
        }
963

964
        let name = this.advance() as Identifier;
3,503✔
965
        // force the name into an identifier so the AST makes some sense
966
        name.kind = TokenKind.Identifier;
3,503✔
967

968
        //add diagnostic if name is a reserved word that cannot be used as an identifier
969
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
3,503✔
970
            this.diagnostics.push({
3✔
971
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
972
                location: name.location
973
            });
974
        }
975

976
        let typeExpression: TypeExpression;
977
        let defaultValue;
978
        let equalToken: Token;
979
        // parse argument default value
980
        if ((equalToken = this.consumeTokenIf(TokenKind.Equal))) {
3,503✔
981
            // it seems any expression is allowed here -- including ones that operate on other arguments!
982
            defaultValue = this.expression(false);
375✔
983
        }
984

985
        let asToken: Token = null;
3,503✔
986
        if (this.check(TokenKind.As)) {
3,503✔
987
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
853✔
988

989
        }
990
        return new FunctionParameterExpression({
3,502✔
991
            name: name,
992
            equals: equalToken,
993
            defaultValue: defaultValue,
994
            as: asToken,
995
            typeExpression: typeExpression
996
        });
997
    }
998

999
    private assignment(allowTypedAssignment = false): AssignmentStatement {
1,724✔
1000
        let name = this.advance() as Identifier;
1,742✔
1001
        //add diagnostic if name is a reserved word that cannot be used as an identifier
1002
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
1,742✔
1003
            this.diagnostics.push({
13✔
1004
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
1005
                location: name.location
1006
            });
1007
        }
1008
        let asToken: Token;
1009
        let typeExpression: TypeExpression;
1010

1011
        if (allowTypedAssignment) {
1,742✔
1012
            //look for `as SOME_TYPE`
1013
            if (this.check(TokenKind.As)) {
18!
1014
                this.warnIfNotBrighterScriptMode('typed assignment');
18✔
1015

1016
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
18✔
1017
            }
1018
        }
1019

1020
        let operator = this.consume(
1,742✔
1021
            DiagnosticMessages.expectedOperator([TokenKind.Equal], name.text),
1022
            ...[TokenKind.Equal]
1023
        );
1024
        let value = this.expression();
1,738✔
1025

1026
        let result = new AssignmentStatement({ equals: operator, name: name, value: value, as: asToken, typeExpression: typeExpression });
1,731✔
1027

1028
        return result;
1,731✔
1029
    }
1030

1031
    private augmentedAssignment(): AugmentedAssignmentStatement {
1032
        let item = this.expression();
76✔
1033

1034
        let operator = this.consume(
76✔
1035
            DiagnosticMessages.expectedToken(...CompoundAssignmentOperators),
1036
            ...CompoundAssignmentOperators
1037
        );
1038
        let value = this.expression();
76✔
1039

1040
        let result = new AugmentedAssignmentStatement({
76✔
1041
            item: item,
1042
            operator: operator,
1043
            value: value
1044
        });
1045

1046
        return result;
76✔
1047
    }
1048

1049
    private checkLibrary() {
1050
        let isLibraryToken = this.check(TokenKind.Library);
24,181✔
1051

1052
        //if we are at the top level, any line that starts with "library" should be considered a library statement
1053
        if (this.isAtRootLevel() && isLibraryToken) {
24,181✔
1054
            return true;
12✔
1055

1056
            //not at root level, library statements are all invalid here, but try to detect if the tokens look
1057
            //like a library statement (and let the libraryStatement function handle emitting the diagnostics)
1058
        } else if (isLibraryToken && this.checkNext(TokenKind.StringLiteral)) {
24,169✔
1059
            return true;
1✔
1060

1061
            //definitely not a library statement
1062
        } else {
1063
            return false;
24,168✔
1064
        }
1065
    }
1066

1067
    private checkAlias() {
1068
        let isAliasToken = this.check(TokenKind.Alias);
23,926✔
1069

1070
        //if we are at the top level, any line that starts with "alias" should be considered a alias statement
1071
        if (this.isAtRootLevel() && isAliasToken) {
23,926✔
1072
            return true;
31✔
1073

1074
            //not at root level, alias statements are all invalid here, but try to detect if the tokens look
1075
            //like a alias statement (and let the alias function handle emitting the diagnostics)
1076
        } else if (isAliasToken && this.checkNext(TokenKind.Identifier)) {
23,895✔
1077
            return true;
2✔
1078

1079
            //definitely not a alias statement
1080
        } else {
1081
            return false;
23,893✔
1082
        }
1083
    }
1084

1085
    private checkTypeStatement() {
1086
        let isTypeToken = this.check(TokenKind.Type);
12,163✔
1087

1088
        //if we are at the top level, any line that starts with "type" should be considered a type statement
1089
        if (this.isAtRootLevel() && isTypeToken) {
12,163✔
1090
            return true;
25✔
1091

1092
            //not at root level, type statements are all invalid here, but try to detect if the tokens look
1093
            //like a type statement (and let the type function handle emitting the diagnostics)
1094
        } else if (isTypeToken && this.checkNext(TokenKind.Identifier)) {
12,138✔
1095
            return true;
2✔
1096

1097
            //definitely not a type statement
1098
        } else {
1099
            return false;
12,136✔
1100
        }
1101
    }
1102

1103
    private statement(): Statement | undefined {
1104
        if (this.checkLibrary()) {
11,972!
1105
            return this.libraryStatement();
×
1106
        }
1107

1108
        if (this.check(TokenKind.Import)) {
11,972✔
1109
            return this.importStatement();
215✔
1110
        }
1111

1112
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
11,757✔
1113
            return this.typecastStatement();
27✔
1114
        }
1115

1116
        if (this.checkAlias()) {
11,730!
1117
            return this.aliasStatement();
×
1118
        }
1119

1120
        if (this.check(TokenKind.Stop)) {
11,730✔
1121
            return this.stopStatement();
16✔
1122
        }
1123

1124
        if (this.check(TokenKind.If)) {
11,714✔
1125
            return this.ifStatement();
1,272✔
1126
        }
1127

1128
        //`try` must be followed by a block, otherwise it could be a local variable
1129
        if (this.check(TokenKind.Try) && this.checkAnyNext(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
10,442✔
1130
            return this.tryCatchStatement();
41✔
1131
        }
1132

1133
        if (this.check(TokenKind.Throw)) {
10,401✔
1134
            return this.throwStatement();
12✔
1135
        }
1136

1137
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
10,389✔
1138
            return this.printStatement();
1,443✔
1139
        }
1140
        if (this.check(TokenKind.Dim)) {
8,946✔
1141
            return this.dimStatement();
43✔
1142
        }
1143

1144
        if (this.check(TokenKind.While)) {
8,903✔
1145
            return this.whileStatement();
33✔
1146
        }
1147

1148
        if (this.checkAny(TokenKind.Exit, TokenKind.ExitWhile)) {
8,870✔
1149
            return this.exitStatement();
22✔
1150
        }
1151

1152
        if (this.check(TokenKind.For)) {
8,848✔
1153
            return this.forStatement();
46✔
1154
        }
1155

1156
        if (this.check(TokenKind.ForEach)) {
8,802✔
1157
            return this.forEachStatement();
64✔
1158
        }
1159

1160
        if (this.check(TokenKind.End)) {
8,738✔
1161
            return this.endStatement();
8✔
1162
        }
1163

1164
        if (this.match(TokenKind.Return)) {
8,730✔
1165
            return this.returnStatement();
3,732✔
1166
        }
1167

1168
        if (this.check(TokenKind.Goto)) {
4,998✔
1169
            return this.gotoStatement();
12✔
1170
        }
1171

1172
        //the continue keyword (followed by `for`, `while`, or a statement separator)
1173
        if (this.check(TokenKind.Continue) && this.checkAnyNext(TokenKind.While, TokenKind.For, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
4,986✔
1174
            return this.continueStatement();
12✔
1175
        }
1176

1177
        //does this line look like a label? (i.e.  `someIdentifier:` )
1178
        if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
4,974✔
1179
            try {
12✔
1180
                return this.labelStatement();
12✔
1181
            } catch (err) {
1182
                if (!(err instanceof CancelStatementError)) {
2!
1183
                    throw err;
×
1184
                }
1185
                //not a label, try something else
1186
            }
1187
        }
1188

1189
        // BrightScript is like python, in that variables can be declared without a `var`,
1190
        // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
1191
        // out what to do with it.
1192
        if (
4,964✔
1193
            this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)
1194
        ) {
1195
            if (this.checkAnyNext(...AssignmentOperators)) {
4,701✔
1196
                if (this.checkAnyNext(...CompoundAssignmentOperators)) {
1,734✔
1197
                    return this.augmentedAssignment();
76✔
1198
                }
1199
                return this.assignment();
1,658✔
1200
            } else if (this.checkNext(TokenKind.As)) {
2,967✔
1201
                // may be a typed assignment
1202
                const backtrack = this.current;
19✔
1203
                let validTypeExpression = false;
19✔
1204

1205
                try {
19✔
1206
                    // skip the identifier, and check for valid type expression
1207
                    this.advance();
19✔
1208
                    const parts = this.consumeAsTokenAndTypeExpression(true);
19✔
1209
                    validTypeExpression = !!(parts?.[0] && parts?.[1]);
19!
1210
                } catch (e) {
1211
                    // ignore any errors
1212
                } finally {
1213
                    this.current = backtrack;
19✔
1214
                }
1215
                if (validTypeExpression) {
19✔
1216
                    // there is a valid 'as' and type expression
1217
                    return this.assignment(true);
18✔
1218
                }
1219
            }
1220
        }
1221

1222
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1223
        if (this.check(TokenKind.Interface)) {
3,212✔
1224
            return this.interfaceDeclaration();
227✔
1225
        }
1226

1227
        if (this.check(TokenKind.Class)) {
2,985✔
1228
            return this.classDeclaration();
716✔
1229
        }
1230

1231
        if (this.check(TokenKind.Namespace)) {
2,269✔
1232
            return this.namespaceStatement();
672✔
1233
        }
1234

1235
        if (this.check(TokenKind.Enum)) {
1,597✔
1236
            return this.enumDeclaration();
187✔
1237
        }
1238

1239
        // TODO: support multi-statements
1240
        return this.setStatement();
1,410✔
1241
    }
1242

1243
    private whileStatement(): WhileStatement {
1244
        const whileKeyword = this.advance();
33✔
1245
        const condition = this.expression();
33✔
1246

1247
        this.consumeStatementSeparators();
32✔
1248

1249
        const whileBlock = this.block(TokenKind.EndWhile);
32✔
1250
        let endWhile: Token;
1251
        if (!whileBlock || this.peek().kind !== TokenKind.EndWhile) {
32✔
1252
            this.diagnostics.push({
1✔
1253
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('while'),
1254
                location: this.peek().location
1255
            });
1256
            if (!whileBlock) {
1!
1257
                throw this.lastDiagnosticAsError();
×
1258
            }
1259
        } else {
1260
            endWhile = this.advance();
31✔
1261
        }
1262

1263
        return new WhileStatement({
32✔
1264
            while: whileKeyword,
1265
            endWhile: endWhile,
1266
            condition: condition,
1267
            body: whileBlock
1268
        });
1269
    }
1270

1271
    private exitStatement(): ExitStatement {
1272
        let exitToken = this.advance();
22✔
1273
        if (exitToken.kind === TokenKind.ExitWhile) {
22✔
1274
            // `exitwhile` is allowed in code, and means `exit while`
1275
            // use an ExitStatement that is nicer to work with by breaking the `exit` and `while` tokens apart
1276

1277
            const exitText = exitToken.text.substring(0, 4);
5✔
1278
            const whileText = exitToken.text.substring(4);
5✔
1279
            const originalRange = exitToken.location?.range;
5✔
1280
            const originalStart = originalRange?.start;
5✔
1281

1282
            const exitRange = util.createRange(
5✔
1283
                originalStart.line,
1284
                originalStart.character,
1285
                originalStart.line,
1286
                originalStart.character + 4);
1287
            const whileRange = util.createRange(
4✔
1288
                originalStart.line,
1289
                originalStart.character + 4,
1290
                originalStart.line,
1291
                originalStart.character + exitToken.text.length);
1292

1293
            exitToken = createToken(TokenKind.Exit, exitText, util.createLocationFromRange(exitToken.location.uri, exitRange));
4✔
1294
            this.tokens[this.current - 1] = exitToken;
4✔
1295
            const newLoopToken = createToken(TokenKind.While, whileText, util.createLocationFromRange(exitToken.location.uri, whileRange));
4✔
1296
            this.tokens.splice(this.current, 0, newLoopToken);
4✔
1297
        }
1298

1299
        const loopTypeToken = this.tryConsume(
21✔
1300
            DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
1301
            TokenKind.While, TokenKind.For
1302
        );
1303

1304
        return new ExitStatement({
21✔
1305
            exit: exitToken,
1306
            loopType: loopTypeToken
1307
        });
1308
    }
1309

1310
    private forStatement(): ForStatement {
1311
        const forToken = this.advance();
46✔
1312
        const initializer = this.assignment();
46✔
1313

1314
        //TODO: newline allowed?
1315

1316
        const toToken = this.advance();
45✔
1317
        const finalValue = this.expression();
45✔
1318
        let incrementExpression: Expression | undefined;
1319
        let stepToken: Token | undefined;
1320

1321
        if (this.check(TokenKind.Step)) {
45✔
1322
            stepToken = this.advance();
13✔
1323
            incrementExpression = this.expression();
13✔
1324
        } else {
1325
            // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
1326
        }
1327

1328
        this.consumeStatementSeparators();
45✔
1329

1330
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
45✔
1331
        let endForToken: Token;
1332
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
45✔
1333
            this.diagnostics.push({
2✔
1334
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(forToken.text),
1335
                location: this.peek().location
1336
            });
1337
            if (!body) {
2!
1338
                throw this.lastDiagnosticAsError();
×
1339
            }
1340
        } else {
1341
            endForToken = this.advance();
43✔
1342
        }
1343

1344
        // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
1345
        // stays around in scope with whatever value it was when the loop exited.
1346
        return new ForStatement({
45✔
1347
            for: forToken,
1348
            counterDeclaration: initializer,
1349
            to: toToken,
1350
            finalValue: finalValue,
1351
            body: body,
1352
            endFor: endForToken,
1353
            step: stepToken,
1354
            increment: incrementExpression
1355
        });
1356
    }
1357

1358
    private forEachStatement(): ForEachStatement {
1359
        let forEach = this.advance();
64✔
1360
        let name = this.advance();
64✔
1361

1362
        let asToken: Token;
1363
        let typeExpression: TypeExpression;
1364

1365
        if (this.check(TokenKind.As)) {
64✔
1366
            this.warnIfNotBrighterScriptMode('typed for each loop variable');
7✔
1367
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
7✔
1368
        }
1369

1370
        let maybeIn = this.peek();
64✔
1371
        if (this.check(TokenKind.Identifier) && maybeIn.text.toLowerCase() === 'in') {
64!
1372
            this.advance();
64✔
1373
        } else {
UNCOV
1374
            this.diagnostics.push({
×
1375
                ...DiagnosticMessages.expectedToken(TokenKind.In),
1376
                location: this.peek().location
1377
            });
UNCOV
1378
            throw this.lastDiagnosticAsError();
×
1379
        }
1380
        maybeIn.kind = TokenKind.In;
64✔
1381

1382
        let target = this.expression();
64✔
1383
        if (!target) {
64!
UNCOV
1384
            this.diagnostics.push({
×
1385
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1386
                location: this.peek().location
1387
            });
UNCOV
1388
            throw this.lastDiagnosticAsError();
×
1389
        }
1390

1391
        this.consumeStatementSeparators();
64✔
1392

1393
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
64✔
1394
        let endForToken: Token;
1395
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
64✔
1396

1397
            this.diagnostics.push({
1✔
1398
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(forEach.text),
1399
                location: this.peek().location
1400
            });
1401
            throw this.lastDiagnosticAsError();
1✔
1402
        }
1403
        endForToken = this.advance();
63✔
1404

1405
        return new ForEachStatement({
63✔
1406
            forEach: forEach,
1407
            as: asToken,
1408
            typeExpression: typeExpression,
1409
            in: maybeIn,
1410
            endFor: endForToken,
1411
            item: name,
1412
            target: target,
1413
            body: body
1414
        });
1415
    }
1416

1417
    private namespaceStatement(): NamespaceStatement | undefined {
1418
        this.warnIfNotBrighterScriptMode('namespace');
672✔
1419
        let keyword = this.advance();
672✔
1420

1421
        this.namespaceAndFunctionDepth++;
672✔
1422

1423
        let name = this.identifyingExpression();
672✔
1424
        //set the current namespace name
1425

1426
        this.globalTerminators.push([TokenKind.EndNamespace]);
671✔
1427
        let body = this.body();
671✔
1428
        this.globalTerminators.pop();
671✔
1429

1430
        let endKeyword: Token;
1431
        if (this.check(TokenKind.EndNamespace)) {
671✔
1432
            endKeyword = this.advance();
669✔
1433
        } else {
1434
            //the `end namespace` keyword is missing. add a diagnostic, but keep parsing
1435
            this.diagnostics.push({
2✔
1436
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'),
1437
                location: keyword.location
1438
            });
1439
        }
1440

1441
        this.namespaceAndFunctionDepth--;
671✔
1442

1443
        let result = new NamespaceStatement({
671✔
1444
            namespace: keyword,
1445
            nameExpression: name,
1446
            body: body,
1447
            endNamespace: endKeyword
1448
        });
1449

1450
        //cache the range property so that plugins can't affect it
1451
        result.cacheLocation();
671✔
1452
        result.body.symbolTable.name += `: namespace '${result.name}'`;
671✔
1453
        return result;
671✔
1454
    }
1455

1456
    /**
1457
     * Get an expression with identifiers separated by periods. Useful for namespaces and class extends
1458
     */
1459
    private identifyingExpression(allowedTokenKinds?: TokenKind[]): DottedGetExpression | VariableExpression {
1460
        allowedTokenKinds = allowedTokenKinds ?? this.allowedLocalIdentifiers;
1,524✔
1461
        let firstIdentifier = this.consume(
1,524✔
1462
            DiagnosticMessages.expectedIdentifier(this.previous().text),
1463
            TokenKind.Identifier,
1464
            ...allowedTokenKinds
1465
        ) as Identifier;
1466

1467
        let expr: DottedGetExpression | VariableExpression;
1468

1469
        if (firstIdentifier) {
1,521!
1470
            // force it into an identifier so the AST makes some sense
1471
            firstIdentifier.kind = TokenKind.Identifier;
1,521✔
1472
            const varExpr = new VariableExpression({ name: firstIdentifier });
1,521✔
1473
            expr = varExpr;
1,521✔
1474

1475
            //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
1476
            while (this.check(TokenKind.Dot)) {
1,521✔
1477
                let dot = this.tryConsume(
474✔
1478
                    DiagnosticMessages.unexpectedToken(this.peek().text),
1479
                    TokenKind.Dot
1480
                );
1481
                if (!dot) {
474!
UNCOV
1482
                    break;
×
1483
                }
1484
                let identifier = this.tryConsume(
474✔
1485
                    DiagnosticMessages.expectedIdentifier(),
1486
                    TokenKind.Identifier,
1487
                    ...allowedTokenKinds,
1488
                    ...AllowedProperties
1489
                ) as Identifier;
1490

1491
                if (!identifier) {
474✔
1492
                    break;
3✔
1493
                }
1494
                // force it into an identifier so the AST makes some sense
1495
                identifier.kind = TokenKind.Identifier;
471✔
1496
                expr = new DottedGetExpression({ obj: expr, name: identifier, dot: dot });
471✔
1497
            }
1498
        }
1499
        return expr;
1,521✔
1500
    }
1501
    /**
1502
     * Add an 'unexpected token' diagnostic for any token found between current and the first stopToken found.
1503
     */
1504
    private flagUntil(...stopTokens: TokenKind[]) {
1505
        while (!this.checkAny(...stopTokens) && !this.isAtEnd()) {
4!
UNCOV
1506
            let token = this.advance();
×
UNCOV
1507
            this.diagnostics.push({
×
1508
                ...DiagnosticMessages.unexpectedToken(token.text),
1509
                location: token.location
1510
            });
1511
        }
1512
    }
1513

1514
    /**
1515
     * Consume tokens until one of the `stopTokenKinds` is encountered
1516
     * @param stopTokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
1517
     * @returns - the list of tokens consumed, EXCLUDING the `stopTokenKind` (you can use `this.peek()` to see which one it was)
1518
     */
1519
    private consumeUntil(...stopTokenKinds: TokenKind[]) {
1520
        let result = [] as Token[];
57✔
1521
        //take tokens until we encounter one of the stopTokenKinds
1522
        while (!stopTokenKinds.includes(this.peek().kind)) {
57✔
1523
            result.push(this.advance());
131✔
1524
        }
1525
        return result;
57✔
1526
    }
1527

1528
    private constDeclaration(): ConstStatement | undefined {
1529
        this.warnIfNotBrighterScriptMode('const declaration');
192✔
1530
        const constToken = this.advance();
192✔
1531
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
192✔
1532
        const equalToken = this.consumeToken(TokenKind.Equal);
192✔
1533
        const expression = this.expression();
192✔
1534
        const statement = new ConstStatement({
192✔
1535
            const: constToken,
1536
            name: nameToken,
1537
            equals: equalToken,
1538
            value: expression
1539
        });
1540
        return statement;
192✔
1541
    }
1542

1543
    private libraryStatement(): LibraryStatement | undefined {
1544
        let libStatement = new LibraryStatement({
13✔
1545
            library: this.advance(),
1546
            //grab the next token only if it's a string
1547
            filePath: this.tryConsume(
1548
                DiagnosticMessages.expectedStringLiteralAfterKeyword('library'),
1549
                TokenKind.StringLiteral
1550
            )
1551
        });
1552

1553
        return libStatement;
13✔
1554
    }
1555

1556
    private importStatement() {
1557
        this.warnIfNotBrighterScriptMode('import statements');
215✔
1558
        let importStatement = new ImportStatement({
215✔
1559
            import: this.advance(),
1560
            //grab the next token only if it's a string
1561
            path: this.tryConsume(
1562
                DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
1563
                TokenKind.StringLiteral
1564
            )
1565
        });
1566

1567
        return importStatement;
215✔
1568
    }
1569

1570
    private typecastStatement() {
1571
        this.warnIfNotBrighterScriptMode('typecast statements');
27✔
1572
        const typecastToken = this.advance();
27✔
1573
        const typecastExpr = this.expression();
27✔
1574
        if (isTypecastExpression(typecastExpr)) {
27✔
1575
            return new TypecastStatement({
26✔
1576
                typecast: typecastToken,
1577
                typecastExpression: typecastExpr
1578
            });
1579
        }
1580
        this.diagnostics.push({
1✔
1581
            ...DiagnosticMessages.expectedIdentifier('typecast'),
1582
            location: {
1583
                uri: typecastToken.location.uri,
1584
                range: util.createBoundingRange(typecastToken, this.peek())
1585
            }
1586
        });
1587
        throw this.lastDiagnosticAsError();
1✔
1588
    }
1589

1590
    private aliasStatement(): AliasStatement | undefined {
1591
        this.warnIfNotBrighterScriptMode('alias statements');
33✔
1592
        const aliasToken = this.advance();
33✔
1593
        const name = this.tryConsume(
33✔
1594
            DiagnosticMessages.expectedIdentifier('alias'),
1595
            TokenKind.Identifier
1596
        );
1597
        const equals = this.tryConsume(
33✔
1598
            DiagnosticMessages.expectedToken(TokenKind.Equal),
1599
            TokenKind.Equal
1600
        );
1601
        let value = this.identifyingExpression();
33✔
1602

1603
        let aliasStmt = new AliasStatement({
33✔
1604
            alias: aliasToken,
1605
            name: name,
1606
            equals: equals,
1607
            value: value
1608

1609
        });
1610

1611
        return aliasStmt;
33✔
1612
    }
1613

1614
    private typeStatement(): TypeStatement | undefined {
1615
        this.warnIfNotBrighterScriptMode('type statements');
27✔
1616
        const typeToken = this.advance();
27✔
1617
        const name = this.tryConsume(
27✔
1618
            DiagnosticMessages.expectedIdentifier('type'),
1619
            TokenKind.Identifier
1620
        );
1621
        const equals = this.tryConsume(
27✔
1622
            DiagnosticMessages.expectedToken(TokenKind.Equal),
1623
            TokenKind.Equal
1624
        );
1625
        let value = this.typeExpression();
27✔
1626

1627
        let typeStmt = new TypeStatement({
26✔
1628
            type: typeToken,
1629
            name: name,
1630
            equals: equals,
1631
            value: value
1632

1633
        });
1634

1635
        return typeStmt;
26✔
1636
    }
1637

1638
    private annotationExpression() {
1639
        const atToken = this.advance();
75✔
1640
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
75✔
1641
        if (identifier) {
75✔
1642
            identifier.kind = TokenKind.Identifier;
74✔
1643
        }
1644
        let annotation = new AnnotationExpression({ at: atToken, name: identifier });
75✔
1645
        this.pendingAnnotations.push(annotation);
74✔
1646

1647
        //optional arguments
1648
        if (this.check(TokenKind.LeftParen)) {
74✔
1649
            let leftParen = this.advance();
30✔
1650
            annotation.call = this.finishCall(leftParen, annotation, false);
30✔
1651
        }
1652
        return annotation;
74✔
1653
    }
1654

1655
    private ternaryExpression(test?: Expression): TernaryExpression {
1656
        this.warnIfNotBrighterScriptMode('ternary operator');
98✔
1657
        if (!test) {
98!
UNCOV
1658
            test = this.expression();
×
1659
        }
1660
        const questionMarkToken = this.advance();
98✔
1661

1662
        //consume newlines or comments
1663
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
98✔
1664
            this.advance();
7✔
1665
        }
1666

1667
        let consequent: Expression;
1668
        try {
98✔
1669
            consequent = this.expression();
98✔
1670
        } catch { }
1671

1672
        //consume newlines or comments
1673
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
98✔
1674
            this.advance();
5✔
1675
        }
1676

1677
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
98✔
1678

1679
        //consume newlines
1680
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
98✔
1681
            this.advance();
11✔
1682
        }
1683
        let alternate: Expression;
1684
        try {
98✔
1685
            alternate = this.expression();
98✔
1686
        } catch { }
1687

1688
        return new TernaryExpression({
98✔
1689
            test: test,
1690
            questionMark: questionMarkToken,
1691
            consequent: consequent,
1692
            colon: colonToken,
1693
            alternate: alternate
1694
        });
1695
    }
1696

1697
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1698
        this.warnIfNotBrighterScriptMode('null coalescing operator');
35✔
1699
        const questionQuestionToken = this.advance();
35✔
1700
        const alternate = this.expression();
35✔
1701
        return new NullCoalescingExpression({
35✔
1702
            consequent: test,
1703
            questionQuestion: questionQuestionToken,
1704
            alternate: alternate
1705
        });
1706
    }
1707

1708
    private regexLiteralExpression() {
1709
        this.warnIfNotBrighterScriptMode('regular expression literal');
45✔
1710
        return new RegexLiteralExpression({
45✔
1711
            regexLiteral: this.advance()
1712
        });
1713
    }
1714

1715
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1716
        this.warnIfNotBrighterScriptMode('template string');
55✔
1717

1718
        //get the tag name
1719
        let tagName: Identifier;
1720
        if (isTagged) {
55✔
1721
            tagName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties) as Identifier;
8✔
1722
            // force it into an identifier so the AST makes some sense
1723
            tagName.kind = TokenKind.Identifier;
8✔
1724
        }
1725

1726
        let quasis = [] as TemplateStringQuasiExpression[];
55✔
1727
        let expressions = [];
55✔
1728
        let openingBacktick = this.peek();
55✔
1729
        this.advance();
55✔
1730
        let currentQuasiExpressionParts = [];
55✔
1731
        while (!this.isAtEnd() && !this.check(TokenKind.BackTick)) {
55✔
1732
            let next = this.peek();
206✔
1733
            if (next.kind === TokenKind.TemplateStringQuasi) {
206✔
1734
                //a quasi can actually be made up of multiple quasis when it includes char literals
1735
                currentQuasiExpressionParts.push(
130✔
1736
                    new LiteralExpression({ value: next })
1737
                );
1738
                this.advance();
130✔
1739
            } else if (next.kind === TokenKind.EscapedCharCodeLiteral) {
76✔
1740
                currentQuasiExpressionParts.push(
33✔
1741
                    new EscapedCharCodeLiteralExpression({ value: next as Token & { charCode: number } })
1742
                );
1743
                this.advance();
33✔
1744
            } else {
1745
                //finish up the current quasi
1746
                quasis.push(
43✔
1747
                    new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1748
                );
1749
                currentQuasiExpressionParts = [];
43✔
1750

1751
                if (next.kind === TokenKind.TemplateStringExpressionBegin) {
43!
1752
                    this.advance();
43✔
1753
                }
1754
                //now keep this expression
1755
                expressions.push(this.expression());
43✔
1756
                if (!this.isAtEnd() && this.check(TokenKind.TemplateStringExpressionEnd)) {
43!
1757
                    //TODO is it an error if this is not present?
1758
                    this.advance();
43✔
1759
                } else {
UNCOV
1760
                    this.diagnostics.push({
×
1761
                        ...DiagnosticMessages.unterminatedTemplateExpression(),
1762
                        location: {
1763
                            uri: openingBacktick.location.uri,
1764
                            range: util.createBoundingRange(openingBacktick, this.peek())
1765
                        }
1766
                    });
UNCOV
1767
                    throw this.lastDiagnosticAsError();
×
1768
                }
1769
            }
1770
        }
1771

1772
        //store the final set of quasis
1773
        quasis.push(
55✔
1774
            new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1775
        );
1776

1777
        if (this.isAtEnd()) {
55✔
1778
            //error - missing backtick
1779
            this.diagnostics.push({
2✔
1780
                ...DiagnosticMessages.unterminatedTemplateString(),
1781
                location: {
1782
                    uri: openingBacktick.location.uri,
1783
                    range: util.createBoundingRange(openingBacktick, this.peek())
1784
                }
1785
            });
1786
            throw this.lastDiagnosticAsError();
2✔
1787

1788
        } else {
1789
            let closingBacktick = this.advance();
53✔
1790
            if (isTagged) {
53✔
1791
                return new TaggedTemplateStringExpression({
8✔
1792
                    tagName: tagName,
1793
                    openingBacktick: openingBacktick,
1794
                    quasis: quasis,
1795
                    expressions: expressions,
1796
                    closingBacktick: closingBacktick
1797
                });
1798
            } else {
1799
                return new TemplateStringExpression({
45✔
1800
                    openingBacktick: openingBacktick,
1801
                    quasis: quasis,
1802
                    expressions: expressions,
1803
                    closingBacktick: closingBacktick
1804
                });
1805
            }
1806
        }
1807
    }
1808

1809
    private tryCatchStatement(): TryCatchStatement {
1810
        const tryToken = this.advance();
41✔
1811
        let endTryToken: Token;
1812
        let catchStmt: CatchStatement;
1813
        //ensure statement separator
1814
        this.consumeStatementSeparators();
41✔
1815

1816
        let tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
41✔
1817

1818
        const peek = this.peek();
41✔
1819
        if (peek.kind !== TokenKind.Catch) {
41✔
1820
            this.diagnostics.push({
2✔
1821
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1822
                location: this.peek()?.location
6!
1823
            });
1824
        } else {
1825
            const catchToken = this.advance();
39✔
1826

1827
            //get the exception variable as an expression
1828
            let exceptionVariableExpression: Expression;
1829
            //if we consumed any statement separators, that means we don't have an exception variable
1830
            if (this.consumeStatementSeparators(true)) {
39✔
1831
                //no exception variable. That's fine in BrighterScript but not in brightscript. But that'll get caught by the validator later...
1832
            } else {
1833
                exceptionVariableExpression = this.expression(true);
33✔
1834
                this.consumeStatementSeparators();
33✔
1835
            }
1836

1837
            const catchBranch = this.block(TokenKind.EndTry);
39✔
1838
            catchStmt = new CatchStatement({
39✔
1839
                catch: catchToken,
1840
                exceptionVariableExpression: exceptionVariableExpression,
1841
                catchBranch: catchBranch
1842
            });
1843
        }
1844
        if (this.peek().kind !== TokenKind.EndTry) {
41✔
1845
            this.diagnostics.push({
2✔
1846
                ...DiagnosticMessages.expectedTerminator('end try', 'try-catch'),
1847
                location: this.peek().location
1848
            });
1849
        } else {
1850
            endTryToken = this.advance();
39✔
1851
        }
1852

1853
        const statement = new TryCatchStatement({
41✔
1854
            try: tryToken,
1855
            tryBranch: tryBranch,
1856
            catchStatement: catchStmt,
1857
            endTry: endTryToken
1858
        }
1859
        );
1860
        return statement;
41✔
1861
    }
1862

1863
    private throwStatement() {
1864
        const throwToken = this.advance();
12✔
1865
        let expression: Expression;
1866
        if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
12✔
1867
            this.diagnostics.push({
2✔
1868
                ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(),
1869
                location: throwToken.location
1870
            });
1871
        } else {
1872
            expression = this.expression();
10✔
1873
        }
1874
        return new ThrowStatement({ throw: throwToken, expression: expression });
10✔
1875
    }
1876

1877
    private dimStatement() {
1878
        const dim = this.advance();
43✔
1879

1880
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
43✔
1881
        // force to an identifier so the AST makes some sense
1882
        if (identifier) {
43✔
1883
            identifier.kind = TokenKind.Identifier;
41✔
1884
        }
1885

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

1888
        let expressions: Expression[] = [];
43✔
1889
        let expression: Expression;
1890
        do {
43✔
1891
            try {
82✔
1892
                expression = this.expression();
82✔
1893
                expressions.push(expression);
77✔
1894
                if (this.check(TokenKind.Comma)) {
77✔
1895
                    this.advance();
39✔
1896
                } else {
1897
                    // will also exit for right square braces
1898
                    break;
38✔
1899
                }
1900
            } catch (error) {
1901
            }
1902
        } while (expression);
1903

1904
        if (expressions.length === 0) {
43✔
1905
            this.diagnostics.push({
5✔
1906
                ...DiagnosticMessages.missingExpressionsInDimStatement(),
1907
                location: this.peek().location
1908
            });
1909
        }
1910
        let rightSquareBracket = this.tryConsume(DiagnosticMessages.unmatchedLeftToken('[', 'dim identifier'), TokenKind.RightSquareBracket);
43✔
1911
        return new DimStatement({
43✔
1912
            dim: dim,
1913
            name: identifier,
1914
            openingSquare: leftSquareBracket,
1915
            dimensions: expressions,
1916
            closingSquare: rightSquareBracket
1917
        });
1918
    }
1919

1920
    private nestedInlineConditionalCount = 0;
4,187✔
1921

1922
    private ifStatement(incrementNestedCount = true): IfStatement {
2,349✔
1923
        // colon before `if` is usually not allowed, unless it's after `then`
1924
        if (this.current > 0) {
2,359✔
1925
            const prev = this.previous();
2,354✔
1926
            if (prev.kind === TokenKind.Colon) {
2,354✔
1927
                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then && this.nestedInlineConditionalCount === 0) {
4✔
1928
                    this.diagnostics.push({
1✔
1929
                        ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
1930
                        location: prev.location
1931
                    });
1932
                }
1933
            }
1934
        }
1935

1936
        const ifToken = this.advance();
2,359✔
1937

1938
        const condition = this.expression();
2,359✔
1939
        let thenBranch: Block;
1940
        let elseBranch: IfStatement | Block | undefined;
1941

1942
        let thenToken: Token | undefined;
1943
        let endIfToken: Token | undefined;
1944
        let elseToken: Token | undefined;
1945

1946
        //optional `then`
1947
        if (this.check(TokenKind.Then)) {
2,357✔
1948
            thenToken = this.advance();
1,875✔
1949
        }
1950

1951
        //is it inline or multi-line if?
1952
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
2,357✔
1953

1954
        if (isInlineIfThen) {
2,357✔
1955
            /*** PARSE INLINE IF STATEMENT ***/
1956
            if (!incrementNestedCount) {
48✔
1957
                this.nestedInlineConditionalCount++;
5✔
1958
            }
1959

1960
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
48✔
1961

1962
            if (!thenBranch) {
48!
UNCOV
1963
                this.diagnostics.push({
×
1964
                    ...DiagnosticMessages.expectedStatement(ifToken.text, 'statement'),
1965
                    location: this.peek().location
1966
                });
UNCOV
1967
                throw this.lastDiagnosticAsError();
×
1968
            } else {
1969
                this.ensureInline(thenBranch.statements);
48✔
1970
            }
1971

1972
            //else branch
1973
            if (this.check(TokenKind.Else)) {
48✔
1974
                elseToken = this.advance();
33✔
1975

1976
                if (this.check(TokenKind.If)) {
33✔
1977
                    // recurse-read `else if`
1978
                    elseBranch = this.ifStatement(false);
10✔
1979

1980
                    //no multi-line if chained with an inline if
1981
                    if (!elseBranch.isInline) {
9✔
1982
                        this.diagnostics.push({
4✔
1983
                            ...DiagnosticMessages.expectedInlineIfStatement(),
1984
                            location: elseBranch.location
1985
                        });
1986
                    }
1987

1988
                } else if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
23✔
1989
                    //expecting inline else branch
1990
                    this.diagnostics.push({
3✔
1991
                        ...DiagnosticMessages.expectedInlineIfStatement(),
1992
                        location: this.peek().location
1993
                    });
1994
                    throw this.lastDiagnosticAsError();
3✔
1995
                } else {
1996
                    elseBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
20✔
1997

1998
                    if (elseBranch) {
20!
1999
                        this.ensureInline(elseBranch.statements);
20✔
2000
                    }
2001
                }
2002

2003
                if (!elseBranch) {
29!
2004
                    //missing `else` branch
UNCOV
2005
                    this.diagnostics.push({
×
2006
                        ...DiagnosticMessages.expectedStatement('else', 'statement'),
2007
                        location: this.peek().location
2008
                    });
UNCOV
2009
                    throw this.lastDiagnosticAsError();
×
2010
                }
2011
            }
2012

2013
            if (!elseBranch || !isIfStatement(elseBranch)) {
44✔
2014
                //enforce newline at the end of the inline if statement
2015
                const peek = this.peek();
35✔
2016
                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && peek.kind !== TokenKind.Else && !this.isAtEnd()) {
35✔
2017
                    //ignore last error if it was about a colon
2018
                    if (this.previous().kind === TokenKind.Colon) {
3!
2019
                        this.diagnostics.pop();
3✔
2020
                        this.current--;
3✔
2021
                    }
2022
                    //newline is required
2023
                    this.diagnostics.push({
3✔
2024
                        ...DiagnosticMessages.expectedFinalNewline(),
2025
                        location: this.peek().location
2026
                    });
2027
                }
2028
            }
2029
            this.nestedInlineConditionalCount--;
44✔
2030
        } else {
2031
            /*** PARSE MULTI-LINE IF STATEMENT ***/
2032

2033
            thenBranch = this.blockConditionalBranch(ifToken);
2,309✔
2034

2035
            //ensure newline/colon before next keyword
2036
            this.ensureNewLineOrColon();
2,306✔
2037

2038
            //else branch
2039
            if (this.check(TokenKind.Else)) {
2,306✔
2040
                elseToken = this.advance();
1,822✔
2041

2042
                if (this.check(TokenKind.If)) {
1,822✔
2043
                    // recurse-read `else if`
2044
                    elseBranch = this.ifStatement();
1,077✔
2045

2046
                } else {
2047
                    elseBranch = this.blockConditionalBranch(ifToken);
745✔
2048

2049
                    //ensure newline/colon before next keyword
2050
                    this.ensureNewLineOrColon();
745✔
2051
                }
2052
            }
2053

2054
            if (!isIfStatement(elseBranch)) {
2,306✔
2055
                if (this.check(TokenKind.EndIf)) {
1,229✔
2056
                    endIfToken = this.advance();
1,224✔
2057

2058
                } else {
2059
                    //missing endif
2060
                    this.diagnostics.push({
5✔
2061
                        ...DiagnosticMessages.expectedTerminator('end if', 'if'),
2062
                        location: ifToken.location
2063
                    });
2064
                }
2065
            }
2066
        }
2067

2068
        return new IfStatement({
2,350✔
2069
            if: ifToken,
2070
            then: thenToken,
2071
            endIf: endIfToken,
2072
            else: elseToken,
2073
            condition: condition,
2074
            thenBranch: thenBranch,
2075
            elseBranch: elseBranch
2076
        });
2077
    }
2078

2079
    //consume a `then` or `else` branch block of an `if` statement
2080
    private blockConditionalBranch(ifToken: Token) {
2081
        //keep track of the current error count, because if the then branch fails,
2082
        //we will trash them in favor of a single error on if
2083
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
3,054✔
2084

2085
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
2086
        // a trailing "end if" or "else if"
2087
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
3,054✔
2088

2089
        if (!branch) {
3,054✔
2090
            //throw out any new diagnostics created as a result of a `then` block parse failure.
2091
            //the block() function will discard the current line, so any discarded diagnostics will
2092
            //resurface if they are legitimate, and not a result of a malformed if statement
2093
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
3✔
2094

2095
            //this whole if statement is bogus...add error to the if token and hard-fail
2096
            this.diagnostics.push({
3✔
2097
                ...DiagnosticMessages.expectedTerminator(['end if', 'else if', 'else'], 'then', 'block'),
2098
                location: ifToken.location
2099
            });
2100
            throw this.lastDiagnosticAsError();
3✔
2101
        }
2102
        return branch;
3,051✔
2103
    }
2104

2105
    private conditionalCompileStatement(): ConditionalCompileStatement {
2106
        const hashIfToken = this.advance();
58✔
2107
        let notToken: Token | undefined;
2108

2109
        if (this.check(TokenKind.Not)) {
58✔
2110
            notToken = this.advance();
7✔
2111
        }
2112

2113
        if (!this.checkAny(TokenKind.True, TokenKind.False, TokenKind.Identifier)) {
58✔
2114
            this.diagnostics.push({
1✔
2115
                ...DiagnosticMessages.invalidHashIfValue(),
2116
                location: this.peek()?.location
3!
2117
            });
2118
        }
2119

2120

2121
        const condition = this.advance();
58✔
2122

2123
        let thenBranch: Block;
2124
        let elseBranch: ConditionalCompileStatement | Block | undefined;
2125

2126
        let hashEndIfToken: Token | undefined;
2127
        let hashElseToken: Token | undefined;
2128

2129
        //keep track of the current error count
2130
        //if this is `#if false` remove all diagnostics.
2131
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
58✔
2132

2133
        thenBranch = this.blockConditionalCompileBranch(hashIfToken);
58✔
2134
        const conditionTextLower = condition.text.toLowerCase();
57✔
2135
        if (!this.options.bsConsts?.get(conditionTextLower) || conditionTextLower === 'false') {
57✔
2136
            //throw out any new diagnostics created as a result of a false block
2137
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
43✔
2138
        }
2139

2140
        this.ensureNewLine();
57✔
2141
        this.advance();
57✔
2142

2143
        //else branch
2144
        if (this.check(TokenKind.HashElseIf)) {
57✔
2145
            // recurse-read `#else if`
2146
            elseBranch = this.conditionalCompileStatement();
15✔
2147
            this.ensureNewLine();
15✔
2148

2149
        } else if (this.check(TokenKind.HashElse)) {
42✔
2150
            hashElseToken = this.advance();
11✔
2151
            let diagnosticsLengthBeforeBlock = this.diagnostics.length;
11✔
2152
            elseBranch = this.blockConditionalCompileBranch(hashIfToken);
11✔
2153

2154
            if (condition.text.toLowerCase() === 'true') {
11✔
2155
                //throw out any new diagnostics created as a result of a false block
2156
                this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
1✔
2157
            }
2158
            this.ensureNewLine();
11✔
2159
            this.advance();
11✔
2160
        }
2161

2162
        if (!isConditionalCompileStatement(elseBranch)) {
57✔
2163

2164
            if (this.check(TokenKind.HashEndIf)) {
42!
2165
                hashEndIfToken = this.advance();
42✔
2166

2167
            } else {
2168
                //missing #endif
UNCOV
2169
                this.diagnostics.push({
×
2170
                    ...DiagnosticMessages.expectedTerminator('#end if', '#if'),
2171
                    location: hashIfToken.location
2172
                });
2173
            }
2174
        }
2175

2176
        return new ConditionalCompileStatement({
57✔
2177
            hashIf: hashIfToken,
2178
            hashElse: hashElseToken,
2179
            hashEndIf: hashEndIfToken,
2180
            not: notToken,
2181
            condition: condition,
2182
            thenBranch: thenBranch,
2183
            elseBranch: elseBranch
2184
        });
2185
    }
2186

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

2193
        //parsing until trailing "#end if", "#else", "#else if"
2194
        let branch = this.conditionalCompileBlock();
69✔
2195

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

2202
            //this whole if statement is bogus...add error to the if token and hard-fail
UNCOV
2203
            this.diagnostics.push({
×
2204
                ...DiagnosticMessages.expectedTerminator(['#end if', '#else if', '#else'], 'conditional compilation', 'block'),
2205
                location: hashIfToken.location
2206
            });
UNCOV
2207
            throw this.lastDiagnosticAsError();
×
2208
        }
2209
        return branch;
68✔
2210
    }
2211

2212
    /**
2213
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2214
     * Always looks for `#end if` or `#else`
2215
     */
2216
    private conditionalCompileBlock(): Block | undefined {
2217
        const parentAnnotations = this.enterAnnotationBlock();
69✔
2218

2219
        this.consumeStatementSeparators(true);
69✔
2220
        const unsafeTerminators = BlockTerminators;
69✔
2221
        const conditionalEndTokens = [TokenKind.HashElse, TokenKind.HashElseIf, TokenKind.HashEndIf];
69✔
2222
        const terminators = [...conditionalEndTokens, ...unsafeTerminators];
69✔
2223
        this.globalTerminators.push(conditionalEndTokens);
69✔
2224
        const statements: Statement[] = [];
69✔
2225
        while (!this.isAtEnd() && !this.checkAny(...terminators)) {
69✔
2226
            //grab the location of the current token
2227
            let loopCurrent = this.current;
73✔
2228
            let dec = this.declaration();
73✔
2229
            if (dec) {
73✔
2230
                if (!isAnnotationExpression(dec)) {
72!
2231
                    this.consumePendingAnnotations(dec);
72✔
2232
                    statements.push(dec);
72✔
2233
                }
2234

2235
                const peekKind = this.peek().kind;
72✔
2236
                if (conditionalEndTokens.includes(peekKind)) {
72✔
2237
                    // current conditional compile branch was closed by other statement, rewind to preceding newline
2238
                    this.current--;
1✔
2239
                }
2240
                //ensure statement separator
2241
                this.consumeStatementSeparators();
72✔
2242

2243
            } else {
2244
                //something went wrong. reset to the top of the loop
2245
                this.current = loopCurrent;
1✔
2246

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

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

2254
                //consume potential separators
2255
                this.consumeStatementSeparators(true);
1✔
2256
            }
2257
        }
2258
        this.globalTerminators.pop();
69✔
2259

2260

2261
        if (this.isAtEnd()) {
69!
UNCOV
2262
            return undefined;
×
2263
            // TODO: Figure out how to handle unterminated blocks well
2264
        } else {
2265
            //did we  hit an unsafe terminator?
2266
            //if so, we need to restore the statement separator
2267
            let prev = this.previous();
69✔
2268
            let prevKind = prev.kind;
69✔
2269
            let peek = this.peek();
69✔
2270
            let peekKind = this.peek().kind;
69✔
2271
            if (
69✔
2272
                (peekKind === TokenKind.HashEndIf || peekKind === TokenKind.HashElse || peekKind === TokenKind.HashElseIf) &&
180✔
2273
                (prevKind === TokenKind.Newline)
2274
            ) {
2275
                this.current--;
68✔
2276
            } else if (unsafeTerminators.includes(peekKind) &&
1!
2277
                (prevKind === TokenKind.Newline || prevKind === TokenKind.Colon)
2278
            ) {
2279
                this.diagnostics.push({
1✔
2280
                    ...DiagnosticMessages.unsafeUnmatchedTerminatorInConditionalCompileBlock(peek.text),
2281
                    location: peek.location
2282
                });
2283
                throw this.lastDiagnosticAsError();
1✔
2284
            } else {
UNCOV
2285
                return undefined;
×
2286
            }
2287
        }
2288
        this.exitAnnotationBlock(parentAnnotations);
68✔
2289
        return new Block({ statements: statements });
68✔
2290
    }
2291

2292
    private conditionalCompileConstStatement() {
2293
        const hashConstToken = this.advance();
21✔
2294

2295
        const constName = this.peek();
21✔
2296
        //disallow using keywords for const names
2297
        if (ReservedWords.has(constName?.text.toLowerCase())) {
21!
2298
            this.diagnostics.push({
1✔
2299
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(constName?.text),
3!
2300
                location: constName?.location
3!
2301
            });
2302

2303
            this.lastDiagnosticAsError();
1✔
2304
            return;
1✔
2305
        }
2306
        const assignment = this.assignment();
20✔
2307
        if (assignment) {
18!
2308
            // check for something other than #const <name> = <otherName|true|false>
2309
            if (assignment.tokens.as || assignment.typeExpression) {
18!
UNCOV
2310
                this.diagnostics.push({
×
2311
                    ...DiagnosticMessages.unexpectedToken(assignment.tokens.as?.text || assignment.typeExpression?.getName(ParseMode.BrighterScript)),
×
2312
                    location: assignment.tokens.as?.location ?? assignment.typeExpression?.location
×
2313
                });
UNCOV
2314
                this.lastDiagnosticAsError();
×
2315
            }
2316

2317
            if (isVariableExpression(assignment.value) || isLiteralBoolean(assignment.value)) {
18✔
2318
                //value is an identifier or a boolean
2319
                //check for valid identifiers will happen in program validation
2320
            } else {
2321
                this.diagnostics.push({
2✔
2322
                    ...DiagnosticMessages.invalidHashConstValue(),
2323
                    location: assignment.value.location
2324
                });
2325
                this.lastDiagnosticAsError();
2✔
2326
            }
2327
        } else {
UNCOV
2328
            return undefined;
×
2329
        }
2330

2331
        if (!this.check(TokenKind.Newline)) {
18!
UNCOV
2332
            this.diagnostics.push({
×
2333
                ...DiagnosticMessages.unexpectedToken(this.peek().text),
2334
                location: this.peek().location
2335
            });
UNCOV
2336
            throw this.lastDiagnosticAsError();
×
2337
        }
2338

2339
        return new ConditionalCompileConstStatement({ hashConst: hashConstToken, assignment: assignment });
18✔
2340
    }
2341

2342
    private conditionalCompileErrorStatement() {
2343
        const hashErrorToken = this.advance();
10✔
2344
        const tokensUntilEndOfLine = this.consumeUntil(TokenKind.Newline);
10✔
2345
        const message = createToken(TokenKind.HashErrorMessage, tokensUntilEndOfLine.map(t => t.text).join(' '));
10✔
2346
        return new ConditionalCompileErrorStatement({ hashError: hashErrorToken, message: message });
10✔
2347
    }
2348

2349
    private ensureNewLine() {
2350
        //ensure newline before next keyword
2351
        if (!this.check(TokenKind.Newline)) {
83!
UNCOV
2352
            this.diagnostics.push({
×
2353
                ...DiagnosticMessages.unexpectedToken(this.peek().text),
2354
                location: this.peek().location
2355
            });
UNCOV
2356
            throw this.lastDiagnosticAsError();
×
2357
        }
2358
    }
2359

2360
    private ensureNewLineOrColon(silent = false) {
3,051✔
2361
        const prev = this.previous().kind;
3,273✔
2362
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
3,273✔
2363
            if (!silent) {
162✔
2364
                this.diagnostics.push({
8✔
2365
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2366
                    location: this.peek().location
2367
                });
2368
            }
2369
            return false;
162✔
2370
        }
2371
        return true;
3,111✔
2372
    }
2373

2374
    //ensure each statement of an inline block is single-line
2375
    private ensureInline(statements: Statement[]) {
2376
        for (const stat of statements) {
68✔
2377
            if (isIfStatement(stat) && !stat.isInline) {
86✔
2378
                this.diagnostics.push({
2✔
2379
                    ...DiagnosticMessages.expectedInlineIfStatement(),
2380
                    location: stat.location
2381
                });
2382
            }
2383
        }
2384
    }
2385

2386
    //consume inline branch of an `if` statement
2387
    private inlineConditionalBranch(...additionalTerminators: BlockTerminator[]): Block | undefined {
2388
        let statements = [];
86✔
2389
        //attempt to get the next statement without using `this.declaration`
2390
        //which seems a bit hackish to get to work properly
2391
        let statement = this.statement();
86✔
2392
        if (!statement) {
86!
UNCOV
2393
            return undefined;
×
2394
        }
2395
        statements.push(statement);
86✔
2396

2397
        //look for colon statement separator
2398
        let foundColon = false;
86✔
2399
        while (this.match(TokenKind.Colon)) {
86✔
2400
            foundColon = true;
23✔
2401
        }
2402

2403
        //if a colon was found, add the next statement or err if unexpected
2404
        if (foundColon) {
86✔
2405
            if (!this.checkAny(TokenKind.Newline, ...additionalTerminators)) {
23✔
2406
                //if not an ending keyword, add next statement
2407
                let extra = this.inlineConditionalBranch(...additionalTerminators);
18✔
2408
                if (!extra) {
18!
UNCOV
2409
                    return undefined;
×
2410
                }
2411
                statements.push(...extra.statements);
18✔
2412
            } else {
2413
                //error: colon before next keyword
2414
                const colon = this.previous();
5✔
2415
                this.diagnostics.push({
5✔
2416
                    ...DiagnosticMessages.unexpectedToken(colon.text),
2417
                    location: colon.location
2418
                });
2419
            }
2420
        }
2421
        return new Block({ statements: statements });
86✔
2422
    }
2423

2424
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2425
        let expressionStart = this.peek();
988✔
2426

2427
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
988✔
2428
            let operator = this.advance();
27✔
2429

2430
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
27✔
2431
                this.diagnostics.push({
1✔
2432
                    ...DiagnosticMessages.unexpectedOperator(),
2433
                    location: this.peek().location
2434
                });
2435
                throw this.lastDiagnosticAsError();
1✔
2436
            } else if (isCallExpression(expr)) {
26✔
2437
                this.diagnostics.push({
1✔
2438
                    ...DiagnosticMessages.unexpectedOperator(),
2439
                    location: expressionStart.location
2440
                });
2441
                throw this.lastDiagnosticAsError();
1✔
2442
            }
2443

2444
            const result = new IncrementStatement({ value: expr, operator: operator });
25✔
2445
            return result;
25✔
2446
        }
2447

2448
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
961✔
2449
            return new ExpressionStatement({ expression: expr });
579✔
2450
        }
2451

2452
        if (this.checkAny(...BinaryExpressionOperatorTokens)) {
382✔
2453
            expr = new BinaryExpression({ left: expr, operator: this.advance(), right: this.expression() });
6✔
2454
        }
2455

2456
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an inclosing ExpressionStatement
2457
        this.diagnostics.push({
382✔
2458
            ...DiagnosticMessages.expectedStatement(),
2459
            location: expressionStart.location
2460
        });
2461
        return new ExpressionStatement({ expression: expr });
382✔
2462
    }
2463

2464
    private setStatement(): DottedSetStatement | IndexedSetStatement | ExpressionStatement | IncrementStatement | AssignmentStatement | AugmentedAssignmentStatement {
2465
        /**
2466
         * Attempts to find an expression-statement or an increment statement.
2467
         * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
2468
         * statements aren't valid expressions. They _do_ however fall under the same parsing
2469
         * priority as standalone function calls though, so we can parse them in the same way.
2470
         */
2471
        let expr = this.call();
1,410✔
2472
        if (this.check(TokenKind.Equal) && !(isCallExpression(expr))) {
1,353✔
2473
            let left = expr;
346✔
2474
            let operator = this.advance();
346✔
2475
            let right = this.expression();
346✔
2476

2477
            // Create a dotted or indexed "set" based on the left-hand side's type
2478
            if (isIndexedGetExpression(left)) {
346✔
2479
                return new IndexedSetStatement({
33✔
2480
                    obj: left.obj,
2481
                    indexes: left.indexes,
2482
                    value: right,
2483
                    openingSquare: left.tokens.openingSquare,
2484
                    closingSquare: left.tokens.closingSquare,
2485
                    equals: operator
2486
                });
2487
            } else if (isDottedGetExpression(left)) {
313✔
2488
                return new DottedSetStatement({
310✔
2489
                    obj: left.obj,
2490
                    name: left.tokens.name,
2491
                    value: right,
2492
                    dot: left.tokens.dot,
2493
                    equals: operator
2494
                });
2495
            }
2496
        } else if (this.checkAny(...CompoundAssignmentOperators) && !(isCallExpression(expr))) {
1,007✔
2497
            let left = expr;
22✔
2498
            let operator = this.advance();
22✔
2499
            let right = this.expression();
22✔
2500
            return new AugmentedAssignmentStatement({
22✔
2501
                item: left,
2502
                operator: operator,
2503
                value: right
2504
            });
2505
        }
2506
        return this.expressionStatement(expr);
988✔
2507
    }
2508

2509
    private printStatement(): PrintStatement {
2510
        let printKeyword = this.advance();
1,443✔
2511

2512
        let values: Expression[] = [];
1,443✔
2513

2514
        while (!this.checkEndOfStatement()) {
1,443✔
2515
            if (this.checkAny(TokenKind.Semicolon, TokenKind.Comma)) {
1,573✔
2516
                values.push(new PrintSeparatorExpression({ separator: this.advance() as PrintSeparatorToken }));
49✔
2517
            } else if (this.check(TokenKind.Else)) {
1,524✔
2518
                break; // inline branch
22✔
2519
            } else {
2520
                values.push(this.expression());
1,502✔
2521
            }
2522
        }
2523

2524
        //print statements can be empty, so look for empty print conditions
2525
        if (!values.length) {
1,441✔
2526
            const endOfStatementLocation = util.createBoundingLocation(printKeyword, this.peek());
9✔
2527
            let emptyStringLiteral = createStringLiteral('', endOfStatementLocation);
9✔
2528
            values.push(emptyStringLiteral);
9✔
2529
        }
2530

2531
        let last = values[values.length - 1];
1,441✔
2532
        if (isToken(last)) {
1,441!
2533
            // TODO: error, expected value
2534
        }
2535

2536
        return new PrintStatement({ print: printKeyword, expressions: values });
1,441✔
2537
    }
2538

2539
    /**
2540
     * Parses a return statement with an optional return value.
2541
     * @returns an AST representation of a return statement.
2542
     */
2543
    private returnStatement(): ReturnStatement {
2544
        let options = { return: this.previous() };
3,732✔
2545

2546
        if (this.checkEndOfStatement()) {
3,732✔
2547
            return new ReturnStatement(options);
24✔
2548
        }
2549

2550
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
3,708✔
2551
        return new ReturnStatement({ ...options, value: toReturn });
3,707✔
2552
    }
2553

2554
    /**
2555
     * Parses a `label` statement
2556
     * @returns an AST representation of an `label` statement.
2557
     */
2558
    private labelStatement() {
2559
        let options = {
12✔
2560
            name: this.advance(),
2561
            colon: this.advance()
2562
        };
2563

2564
        //label must be alone on its line, this is probably not a label
2565
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
12✔
2566
            //rewind and cancel
2567
            this.current -= 2;
2✔
2568
            throw new CancelStatementError();
2✔
2569
        }
2570

2571
        return new LabelStatement(options);
10✔
2572
    }
2573

2574
    /**
2575
     * Parses a `continue` statement
2576
     */
2577
    private continueStatement() {
2578
        return new ContinueStatement({
12✔
2579
            continue: this.advance(),
2580
            loopType: this.tryConsume(
2581
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2582
                TokenKind.While, TokenKind.For
2583
            )
2584
        });
2585
    }
2586

2587
    /**
2588
     * Parses a `goto` statement
2589
     * @returns an AST representation of an `goto` statement.
2590
     */
2591
    private gotoStatement() {
2592
        let tokens = {
12✔
2593
            goto: this.advance(),
2594
            label: this.consume(
2595
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2596
                TokenKind.Identifier
2597
            )
2598
        };
2599

2600
        return new GotoStatement(tokens);
10✔
2601
    }
2602

2603
    /**
2604
     * Parses an `end` statement
2605
     * @returns an AST representation of an `end` statement.
2606
     */
2607
    private endStatement() {
2608
        let options = { end: this.advance() };
8✔
2609

2610
        return new EndStatement(options);
8✔
2611
    }
2612
    /**
2613
     * Parses a `stop` statement
2614
     * @returns an AST representation of a `stop` statement
2615
     */
2616
    private stopStatement() {
2617
        let options = { stop: this.advance() };
16✔
2618

2619
        return new StopStatement(options);
16✔
2620
    }
2621

2622
    /**
2623
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2624
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2625
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2626
     *                    ignored.
2627
     */
2628
    private block(...terminators: BlockTerminator[]): Block | undefined {
2629
        const parentAnnotations = this.enterAnnotationBlock();
7,560✔
2630

2631
        this.consumeStatementSeparators(true);
7,560✔
2632
        const statements: Statement[] = [];
7,560✔
2633
        const flatGlobalTerminators = this.globalTerminators.flat().flat();
7,560✔
2634
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators, ...flatGlobalTerminators)) {
7,560✔
2635
            //grab the location of the current token
2636
            let loopCurrent = this.current;
9,032✔
2637
            let dec = this.declaration();
9,032✔
2638
            if (dec) {
9,032✔
2639
                if (!isAnnotationExpression(dec)) {
8,986✔
2640
                    this.consumePendingAnnotations(dec);
8,979✔
2641
                    statements.push(dec);
8,979✔
2642
                }
2643

2644
                //ensure statement separator
2645
                this.consumeStatementSeparators();
8,986✔
2646

2647
            } else {
2648
                //something went wrong. reset to the top of the loop
2649
                this.current = loopCurrent;
46✔
2650

2651
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2652
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
46✔
2653

2654
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2655
                //but there's already an error in the file being parsed, so just leave this line here
2656
                this.advance();
46✔
2657

2658
                //consume potential separators
2659
                this.consumeStatementSeparators(true);
46✔
2660
            }
2661
        }
2662

2663
        if (this.isAtEnd()) {
7,560✔
2664
            return undefined;
6✔
2665
            // TODO: Figure out how to handle unterminated blocks well
2666
        } else if (terminators.length > 0) {
7,554✔
2667
            //did we hit end-sub / end-function while looking for some other terminator?
2668
            //if so, we need to restore the statement separator
2669
            let prev = this.previous().kind;
3,272✔
2670
            let peek = this.peek().kind;
3,272✔
2671
            if (
3,272✔
2672
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
6,548!
2673
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2674
            ) {
2675
                this.current--;
10✔
2676
            }
2677
        }
2678

2679
        this.exitAnnotationBlock(parentAnnotations);
7,554✔
2680
        return new Block({ statements: statements });
7,554✔
2681
    }
2682

2683
    /**
2684
     * Attach pending annotations to the provided statement,
2685
     * and then reset the annotations array
2686
     */
2687
    consumePendingAnnotations(statement: Statement) {
2688
        if (this.pendingAnnotations.length) {
17,347✔
2689
            statement.annotations = this.pendingAnnotations;
51✔
2690
            this.pendingAnnotations = [];
51✔
2691
        }
2692
    }
2693

2694
    enterAnnotationBlock() {
2695
        const pending = this.pendingAnnotations;
13,594✔
2696
        this.pendingAnnotations = [];
13,594✔
2697
        return pending;
13,594✔
2698
    }
2699

2700
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2701
        // non consumed annotations are an error
2702
        if (this.pendingAnnotations.length) {
13,586✔
2703
            for (const annotation of this.pendingAnnotations) {
5✔
2704
                this.diagnostics.push({
7✔
2705
                    ...DiagnosticMessages.unusedAnnotation(),
2706
                    location: annotation.location
2707
                });
2708
            }
2709
        }
2710
        this.pendingAnnotations = parentAnnotations;
13,586✔
2711
    }
2712

2713
    private expression(findTypecast = true): Expression {
13,765✔
2714
        let expression = this.anonymousFunction();
14,173✔
2715
        let asToken: Token;
2716
        let typeExpression: TypeExpression;
2717
        if (findTypecast) {
14,134✔
2718
            do {
13,759✔
2719
                if (this.check(TokenKind.As)) {
13,840✔
2720
                    this.warnIfNotBrighterScriptMode('type cast');
83✔
2721
                    // Check if this expression is wrapped in any type casts
2722
                    // allows for multiple casts:
2723
                    // myVal = foo() as dynamic as string
2724
                    [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
83✔
2725
                    if (asToken && typeExpression) {
83✔
2726
                        expression = new TypecastExpression({ obj: expression, as: asToken, typeExpression: typeExpression });
81✔
2727
                    }
2728
                } else {
2729
                    break;
13,757✔
2730
                }
2731

2732
            } while (asToken && typeExpression);
166✔
2733
        }
2734
        return expression;
14,134✔
2735
    }
2736

2737
    private anonymousFunction(): Expression {
2738
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
14,173✔
2739
            const func = this.functionDeclaration(true);
99✔
2740
            //if there's an open paren after this, this is an IIFE
2741
            if (this.check(TokenKind.LeftParen)) {
98✔
2742
                return this.finishCall(this.advance(), func);
3✔
2743
            } else {
2744
                return func;
95✔
2745
            }
2746
        }
2747

2748
        let expr = this.boolean();
14,074✔
2749

2750
        if (this.check(TokenKind.Question)) {
14,036✔
2751
            return this.ternaryExpression(expr);
98✔
2752
        } else if (this.check(TokenKind.QuestionQuestion)) {
13,938✔
2753
            return this.nullCoalescingExpression(expr);
35✔
2754
        } else {
2755
            return expr;
13,903✔
2756
        }
2757
    }
2758

2759
    private boolean(): Expression {
2760
        let expr = this.relational();
14,074✔
2761

2762
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
14,036✔
2763
            let operator = this.previous();
34✔
2764
            let right = this.relational();
34✔
2765
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
34✔
2766
        }
2767

2768
        return expr;
14,036✔
2769
    }
2770

2771
    private relational(): Expression {
2772
        let expr = this.additive();
14,138✔
2773

2774
        while (
14,100✔
2775
            this.matchAny(
2776
                TokenKind.Equal,
2777
                TokenKind.LessGreater,
2778
                TokenKind.Greater,
2779
                TokenKind.GreaterEqual,
2780
                TokenKind.Less,
2781
                TokenKind.LessEqual
2782
            )
2783
        ) {
2784
            let operator = this.previous();
1,943✔
2785
            let right = this.additive();
1,943✔
2786
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,943✔
2787
        }
2788

2789
        return expr;
14,100✔
2790
    }
2791

2792
    // TODO: bitshift
2793

2794
    private additive(): Expression {
2795
        let expr = this.multiplicative();
16,081✔
2796

2797
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
16,043✔
2798
            let operator = this.previous();
1,562✔
2799
            let right = this.multiplicative();
1,562✔
2800
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,562✔
2801
        }
2802

2803
        return expr;
16,043✔
2804
    }
2805

2806
    private multiplicative(): Expression {
2807
        let expr = this.exponential();
17,643✔
2808

2809
        while (this.matchAny(
17,605✔
2810
            TokenKind.Forwardslash,
2811
            TokenKind.Backslash,
2812
            TokenKind.Star,
2813
            TokenKind.Mod,
2814
            TokenKind.LeftShift,
2815
            TokenKind.RightShift
2816
        )) {
2817
            let operator = this.previous();
60✔
2818
            let right = this.exponential();
60✔
2819
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
60✔
2820
        }
2821

2822
        return expr;
17,605✔
2823
    }
2824

2825
    private exponential(): Expression {
2826
        let expr = this.prefixUnary();
17,703✔
2827

2828
        while (this.match(TokenKind.Caret)) {
17,665✔
2829
            let operator = this.previous();
9✔
2830
            let right = this.prefixUnary();
9✔
2831
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
9✔
2832
        }
2833

2834
        return expr;
17,665✔
2835
    }
2836

2837
    private prefixUnary(): Expression {
2838
        const nextKind = this.peek().kind;
17,748✔
2839
        if (nextKind === TokenKind.Not) {
17,748✔
2840
            this.current++; //advance
30✔
2841
            let operator = this.previous();
30✔
2842
            let right = this.relational();
30✔
2843
            return new UnaryExpression({ operator: operator, right: right });
30✔
2844
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
17,718✔
2845
            this.current++; //advance
36✔
2846
            let operator = this.previous();
36✔
2847
            let right = (nextKind as any) === TokenKind.Not
36✔
2848
                ? this.boolean()
36!
2849
                : this.prefixUnary();
2850
            return new UnaryExpression({ operator: operator, right: right });
36✔
2851
        }
2852
        return this.call();
17,682✔
2853
    }
2854

2855
    private indexedGet(expr: Expression) {
2856
        let openingSquare = this.previous();
161✔
2857
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
161✔
2858
        let indexes: Expression[] = [];
161✔
2859

2860

2861
        //consume leading newlines
2862
        while (this.match(TokenKind.Newline)) { }
161✔
2863

2864
        try {
161✔
2865
            indexes.push(
161✔
2866
                this.expression()
2867
            );
2868
            //consume additional indexes separated by commas
2869
            while (this.check(TokenKind.Comma)) {
159✔
2870
                //discard the comma
2871
                this.advance();
17✔
2872
                indexes.push(
17✔
2873
                    this.expression()
2874
                );
2875
            }
2876
        } catch (error) {
2877
            this.rethrowNonDiagnosticError(error);
2✔
2878
        }
2879
        //consume trailing newlines
2880
        while (this.match(TokenKind.Newline)) { }
161✔
2881

2882
        const closingSquare = this.tryConsume(
161✔
2883
            DiagnosticMessages.unmatchedLeftToken(openingSquare.text, 'array or object index'),
2884
            TokenKind.RightSquareBracket
2885
        );
2886

2887
        return new IndexedGetExpression({
161✔
2888
            obj: expr,
2889
            indexes: indexes,
2890
            openingSquare: openingSquare,
2891
            closingSquare: closingSquare,
2892
            questionDot: questionDotToken
2893
        });
2894
    }
2895

2896
    private newExpression() {
2897
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
141✔
2898
        let newToken = this.advance();
141✔
2899

2900
        let nameExpr = this.identifyingExpression();
141✔
2901
        let leftParen = this.tryConsume(
141✔
2902
            DiagnosticMessages.unexpectedToken(this.peek().text),
2903
            TokenKind.LeftParen,
2904
            TokenKind.QuestionLeftParen
2905
        );
2906

2907
        if (!leftParen) {
141✔
2908
            // new expression without a following call expression
2909
            // wrap the name in an expression
2910
            const endOfStatementLocation = util.createBoundingLocation(newToken, this.peek());
4✔
2911
            const exprStmt = nameExpr ?? createStringLiteral('', endOfStatementLocation);
4!
2912
            return new ExpressionStatement({ expression: exprStmt });
4✔
2913
        }
2914

2915
        let call = this.finishCall(leftParen, nameExpr);
137✔
2916
        //pop the call from the  callExpressions list because this is technically something else
2917
        this.callExpressions.pop();
137✔
2918
        let result = new NewExpression({ new: newToken, call: call });
137✔
2919
        return result;
137✔
2920
    }
2921

2922
    /**
2923
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2924
     */
2925
    private callfunc(callee: Expression): Expression {
2926
        this.warnIfNotBrighterScriptMode('callfunc operator');
79✔
2927
        let operator = this.previous();
79✔
2928
        let methodName = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
79✔
2929
        let openParen: Token;
2930
        let call: CallExpression;
2931
        if (methodName) {
79✔
2932
            // force it into an identifier so the AST makes some sense
2933
            methodName.kind = TokenKind.Identifier;
71✔
2934
            openParen = this.tryConsume(DiagnosticMessages.expectedToken(TokenKind.LeftParen), TokenKind.LeftParen);
71✔
2935
            if (openParen) {
71!
2936
                call = this.finishCall(openParen, callee, false);
71✔
2937
            }
2938
        }
2939
        return new CallfuncExpression({
79✔
2940
            callee: callee,
2941
            operator: operator,
2942
            methodName: methodName as Identifier,
2943
            openingParen: openParen,
2944
            args: call?.args,
237✔
2945
            closingParen: call?.tokens?.closingParen
474✔
2946
        });
2947
    }
2948

2949
    private call(): Expression {
2950
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
19,092✔
2951
            return this.newExpression();
141✔
2952
        }
2953
        let expr = this.primary();
18,951✔
2954

2955
        while (true) {
18,856✔
2956
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
24,306✔
2957
                expr = this.finishCall(this.previous(), expr);
2,537✔
2958
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
21,769✔
2959
                expr = this.indexedGet(expr);
159✔
2960
            } else if (this.match(TokenKind.Callfunc)) {
21,610✔
2961
                expr = this.callfunc(expr);
79✔
2962
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
21,531✔
2963
                if (this.match(TokenKind.LeftSquareBracket)) {
2,723✔
2964
                    expr = this.indexedGet(expr);
2✔
2965
                } else {
2966
                    let dot = this.previous();
2,721✔
2967
                    let name = this.tryConsume(
2,721✔
2968
                        DiagnosticMessages.expectedIdentifier(),
2969
                        TokenKind.Identifier,
2970
                        ...AllowedProperties
2971
                    );
2972
                    if (!name) {
2,721✔
2973
                        break;
48✔
2974
                    }
2975

2976
                    // force it into an identifier so the AST makes some sense
2977
                    name.kind = TokenKind.Identifier;
2,673✔
2978
                    expr = new DottedGetExpression({ obj: expr, name: name as Identifier, dot: dot });
2,673✔
2979
                }
2980

2981
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
18,808✔
2982
                let dot = this.advance();
11✔
2983
                let name = this.tryConsume(
11✔
2984
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2985
                    TokenKind.Identifier,
2986
                    ...AllowedProperties
2987
                );
2988

2989
                // force it into an identifier so the AST makes some sense
2990
                name.kind = TokenKind.Identifier;
11✔
2991
                if (!name) {
11!
UNCOV
2992
                    break;
×
2993
                }
2994
                expr = new XmlAttributeGetExpression({ obj: expr, name: name as Identifier, at: dot });
11✔
2995
                //only allow a single `@` expression
2996
                break;
11✔
2997

2998
            } else {
2999
                break;
18,797✔
3000
            }
3001
        }
3002

3003
        return expr;
18,856✔
3004
    }
3005

3006
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
2,677✔
3007
        let args = [] as Expression[];
2,778✔
3008
        while (this.match(TokenKind.Newline)) { }
2,778✔
3009

3010
        if (!this.check(TokenKind.RightParen)) {
2,778✔
3011
            do {
1,418✔
3012
                while (this.match(TokenKind.Newline)) { }
2,025✔
3013

3014
                if (args.length >= CallExpression.MaximumArguments) {
2,025!
UNCOV
3015
                    this.diagnostics.push({
×
3016
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
3017
                        location: this.peek()?.location
×
3018
                    });
UNCOV
3019
                    throw this.lastDiagnosticAsError();
×
3020
                }
3021
                try {
2,025✔
3022
                    args.push(this.expression());
2,025✔
3023
                } catch (error) {
3024
                    this.rethrowNonDiagnosticError(error);
6✔
3025
                    // we were unable to get an expression, so don't continue
3026
                    break;
6✔
3027
                }
3028
            } while (this.match(TokenKind.Comma));
3029
        }
3030

3031
        while (this.match(TokenKind.Newline)) { }
2,778✔
3032

3033
        const closingParen = this.tryConsume(
2,778✔
3034
            DiagnosticMessages.unmatchedLeftToken(openingParen.text, 'function call arguments'),
3035
            TokenKind.RightParen
3036
        );
3037

3038
        let expression = new CallExpression({
2,778✔
3039
            callee: callee,
3040
            openingParen: openingParen,
3041
            args: args,
3042
            closingParen: closingParen
3043
        });
3044
        if (addToCallExpressionList) {
2,778✔
3045
            this.callExpressions.push(expression);
2,677✔
3046
        }
3047
        return expression;
2,778✔
3048
    }
3049

3050
    /**
3051
     * Creates a TypeExpression, which wraps standard ASTNodes that represent a BscType
3052
     */
3053
    private typeExpression(): TypeExpression {
3054
        const changedTokens: { token: Token; oldKind: TokenKind }[] = [];
2,122✔
3055
        try {
2,122✔
3056
            // handle types with 'and'/'or' operators
3057
            const expressionsWithOperator: { expression: Expression; operator?: Token }[] = [];
2,122✔
3058

3059
            // find all expressions and operators
3060
            let expr: Expression = this.getTypeExpressionPart(changedTokens);
2,122✔
3061
            while (this.options.mode === ParseMode.BrighterScript && this.matchAny(TokenKind.Or, TokenKind.And)) {
2,121✔
3062
                let operator = this.previous();
127✔
3063
                expressionsWithOperator.push({ expression: expr, operator: operator });
127✔
3064
                expr = this.getTypeExpressionPart(changedTokens);
127✔
3065
            }
3066
            // add last expression
3067
            expressionsWithOperator.push({ expression: expr });
2,120✔
3068

3069
            // handle expressions with order of operations - first "and", then "or"
3070
            const combineExpressions = (opToken: TokenKind) => {
2,120✔
3071
                let exprWithOp = expressionsWithOperator[0];
4,240✔
3072
                let index = 0;
4,240✔
3073
                while (exprWithOp?.operator) {
4,240!
3074
                    if (exprWithOp.operator.kind === opToken) {
213✔
3075
                        const nextExpr = expressionsWithOperator[index + 1];
126✔
3076
                        const combinedExpr = new BinaryExpression({ left: exprWithOp.expression, operator: exprWithOp.operator, right: nextExpr.expression });
126✔
3077
                        // replace the two expressions with the combined one
3078
                        expressionsWithOperator.splice(index, 2, { expression: combinedExpr, operator: nextExpr.operator });
126✔
3079
                        exprWithOp = expressionsWithOperator[index];
126✔
3080
                    } else {
3081
                        index++;
87✔
3082
                        exprWithOp = expressionsWithOperator[index];
87✔
3083
                    }
3084
                }
3085
            };
3086

3087
            combineExpressions(TokenKind.And);
2,120✔
3088
            combineExpressions(TokenKind.Or);
2,120✔
3089

3090
            if (expressionsWithOperator[0]?.expression) {
2,120!
3091
                return new TypeExpression({ expression: expressionsWithOperator[0].expression });
2,103✔
3092
            }
3093

3094
        } catch (error) {
3095
            // Something went wrong - reset the kind to what it was previously
3096
            for (const changedToken of changedTokens) {
2✔
UNCOV
3097
                changedToken.token.kind = changedToken.oldKind;
×
3098
            }
3099
            throw error;
2✔
3100
        }
3101
    }
3102

3103
    /**
3104
     * Gets a single "part" of a type of a potential Union type
3105
     * Note: this does not NEED to be part of a union type, but the logic is the same
3106
     *
3107
     * @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
3108
     * @returns an expression that was successfully parsed
3109
     */
3110
    private getTypeExpressionPart(changedTokens: { token: Token; oldKind: TokenKind }[]) {
3111
        let expr: VariableExpression | DottedGetExpression | TypedArrayExpression | InlineInterfaceExpression | GroupingExpression;
3112

3113
        if (this.checkAny(...DeclarableTypes)) {
2,249✔
3114
            // if this is just a type, just use directly
3115
            expr = new VariableExpression({ name: this.advance() as Identifier });
1,476✔
3116
        } else {
3117
            if (this.options.mode === ParseMode.BrightScript && !declarableTypesLower.includes(this.peek()?.text?.toLowerCase())) {
773!
3118
                // custom types arrays not allowed in Brightscript
3119
                this.warnIfNotBrighterScriptMode('custom types');
17✔
3120
                this.advance(); // skip custom type token
17✔
3121
                return expr;
17✔
3122
            }
3123

3124
            if (this.match(TokenKind.LeftCurlyBrace)) {
756✔
3125
                expr = this.inlineInterface();
58✔
3126
            } else if (this.match(TokenKind.LeftParen)) {
698✔
3127
                // this is a grouping type expression, ie. "(typeExpr)"
3128
                let left = this.previous();
20✔
3129
                let typeExpr = this.typeExpression();
20✔
3130
                let right = this.consume(
20✔
3131
                    DiagnosticMessages.unmatchedLeftToken(left.text, 'type expression'),
3132
                    TokenKind.RightParen
3133
                );
3134
                expr = new GroupingExpression({ leftParen: left, rightParen: right, expression: typeExpr });
20✔
3135
            } else {
3136
                if (this.checkAny(...AllowedTypeIdentifiers)) {
678✔
3137
                    // Since the next token is allowed as a type identifier, change the kind
3138
                    let nextToken = this.peek();
2✔
3139
                    changedTokens.push({ token: nextToken, oldKind: nextToken.kind });
2✔
3140
                    nextToken.kind = TokenKind.Identifier;
2✔
3141
                }
3142
                expr = this.identifyingExpression(AllowedTypeIdentifiers);
678✔
3143
            }
3144
        }
3145

3146
        //Check if it has square brackets, thus making it an array
3147
        if (expr && this.check(TokenKind.LeftSquareBracket)) {
2,230✔
3148
            if (this.options.mode === ParseMode.BrightScript) {
39✔
3149
                // typed arrays not allowed in Brightscript
3150
                this.warnIfNotBrighterScriptMode('typed arrays');
1✔
3151
                return expr;
1✔
3152
            }
3153

3154
            // Check if it is an array - that is, if it has `[]` after the type
3155
            // eg. `string[]` or `SomeKlass[]`
3156
            // This is while loop, so it supports multidimensional arrays (eg. integer[][])
3157
            while (this.check(TokenKind.LeftSquareBracket)) {
38✔
3158
                const leftBracket = this.advance();
41✔
3159
                if (this.check(TokenKind.RightSquareBracket)) {
41!
3160
                    const rightBracket = this.advance();
41✔
3161
                    expr = new TypedArrayExpression({ innerType: expr, leftBracket: leftBracket, rightBracket: rightBracket });
41✔
3162
                }
3163
            }
3164
        }
3165

3166
        return expr;
2,229✔
3167
    }
3168

3169

3170
    private inlineInterface() {
3171
        let expr: InlineInterfaceExpression;
3172
        const openToken = this.previous();
58✔
3173
        const members: InlineInterfaceMemberExpression[] = [];
58✔
3174
        while (this.match(TokenKind.Newline)) { }
58✔
3175
        while (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral, TokenKind.Optional)) {
58✔
3176
            const member = this.inlineInterfaceMember();
65✔
3177
            members.push(member);
65✔
3178
            while (this.matchAny(TokenKind.Comma, TokenKind.Newline)) { }
65✔
3179
        }
3180
        if (!this.check(TokenKind.RightCurlyBrace)) {
58!
UNCOV
3181
            this.diagnostics.push({
×
3182
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
3183
                location: this.peek().location
3184
            });
UNCOV
3185
            throw this.lastDiagnosticAsError();
×
3186
        }
3187
        const closeToken = this.advance();
58✔
3188

3189
        expr = new InlineInterfaceExpression({ open: openToken, members: members, close: closeToken });
58✔
3190
        return expr;
58✔
3191
    }
3192

3193
    private inlineInterfaceMember(): InlineInterfaceMemberExpression {
3194
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
65✔
3195

3196
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral)) {
65!
3197
            if (this.check(TokenKind.As)) {
65!
UNCOV
3198
                if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
×
3199
                    // as <EOL>
3200
                    // `as` is the field name
UNCOV
3201
                } else if (this.checkNext(TokenKind.As)) {
×
3202
                    //  as as ____
3203
                    // first `as` is the field name
UNCOV
3204
                } else if (optionalKeyword) {
×
3205
                    // optional as ____
3206
                    // optional is the field name, `as` starts type
3207
                    // rewind current token
3208
                    optionalKeyword = null;
×
UNCOV
3209
                    this.current--;
×
3210
                }
3211
            }
3212
        } else {
3213
            // no name after `optional` ... optional is the name
3214
            // rewind current token
UNCOV
3215
            optionalKeyword = null;
×
UNCOV
3216
            this.current--;
×
3217
        }
3218

3219
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers, TokenKind.StringLiteral)) {
65!
UNCOV
3220
            this.diagnostics.push({
×
3221
                ...DiagnosticMessages.expectedIdentifier(this.peek().text),
3222
                location: this.peek().location
3223
            });
UNCOV
3224
            throw this.lastDiagnosticAsError();
×
3225
        }
3226
        let name: Token;
3227
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
65✔
3228
            name = this.identifier(...AllowedProperties);
63✔
3229
        } else {
3230
            name = this.advance();
2✔
3231
        }
3232

3233
        let typeExpression: TypeExpression;
3234

3235
        let asToken: Token = null;
65✔
3236
        if (this.check(TokenKind.As)) {
65✔
3237
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
63✔
3238

3239
        }
3240
        return new InlineInterfaceMemberExpression({
65✔
3241
            name: name,
3242
            as: asToken,
3243
            typeExpression: typeExpression,
3244
            optional: optionalKeyword
3245
        });
3246
    }
3247

3248
    private primary(): Expression {
3249
        switch (true) {
18,951✔
3250
            case this.matchAny(
18,951!
3251
                TokenKind.False,
3252
                TokenKind.True,
3253
                TokenKind.Invalid,
3254
                TokenKind.IntegerLiteral,
3255
                TokenKind.LongIntegerLiteral,
3256
                TokenKind.FloatLiteral,
3257
                TokenKind.DoubleLiteral,
3258
                TokenKind.StringLiteral
3259
            ):
3260
                return new LiteralExpression({ value: this.previous() });
8,255✔
3261

3262
            //capture source literals (LINE_NUM if brightscript, or a bunch of them if brighterscript)
3263
            case this.matchAny(TokenKind.LineNumLiteral, ...(this.options.mode === ParseMode.BrightScript ? [] : BrighterScriptSourceLiterals)):
10,696✔
3264
                return new SourceLiteralExpression({ value: this.previous() });
35✔
3265

3266
            //template string
3267
            case this.check(TokenKind.BackTick):
3268
                return this.templateString(false);
47✔
3269

3270
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
3271
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
20,525✔
3272
                return this.templateString(true);
8✔
3273

3274
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3275
                return new VariableExpression({ name: this.previous() as Identifier });
9,910✔
3276

3277
            case this.match(TokenKind.LeftParen):
3278
                let left = this.previous();
60✔
3279
                let expr = this.expression();
60✔
3280
                let right = this.consume(
59✔
3281
                    DiagnosticMessages.unmatchedLeftToken(left.text, 'expression'),
3282
                    TokenKind.RightParen
3283
                );
3284
                return new GroupingExpression({ leftParen: left, rightParen: right, expression: expr });
59✔
3285

3286
            case this.matchAny(TokenKind.LeftSquareBracket):
3287
                return this.arrayLiteral();
174✔
3288

3289
            case this.match(TokenKind.LeftCurlyBrace):
3290
                return this.aaLiteral();
325✔
3291

3292
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
UNCOV
3293
                let token = Object.assign(this.previous(), {
×
3294
                    kind: TokenKind.Identifier
3295
                }) as Identifier;
UNCOV
3296
                return new VariableExpression({ name: token });
×
3297

3298
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
UNCOV
3299
                return this.anonymousFunction();
×
3300

3301
            case this.check(TokenKind.RegexLiteral):
3302
                return this.regexLiteralExpression();
45✔
3303

3304
            default:
3305
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
3306
                if (this.checkAny(...this.peekGlobalTerminators())) {
92!
3307
                    //don't throw a diagnostic, just return undefined
3308

3309
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
3310
                } else {
3311
                    this.diagnostics.push({
92✔
3312
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
3313
                        location: this.peek()?.location
276!
3314
                    });
3315
                    throw this.lastDiagnosticAsError();
92✔
3316
                }
3317
        }
3318
    }
3319

3320
    private arrayLiteral() {
3321
        let elements: Array<Expression> = [];
174✔
3322
        let openingSquare = this.previous();
174✔
3323

3324
        while (this.match(TokenKind.Newline)) {
174✔
3325
        }
3326
        let closingSquare: Token;
3327

3328
        if (!this.match(TokenKind.RightSquareBracket)) {
174✔
3329
            try {
133✔
3330
                elements.push(this.expression());
133✔
3331

3332
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
132✔
3333

3334
                    while (this.match(TokenKind.Newline)) {
193✔
3335

3336
                    }
3337

3338
                    if (this.check(TokenKind.RightSquareBracket)) {
193✔
3339
                        break;
36✔
3340
                    }
3341

3342
                    elements.push(this.expression());
157✔
3343
                }
3344
            } catch (error: any) {
3345
                this.rethrowNonDiagnosticError(error);
2✔
3346
            }
3347

3348
            closingSquare = this.tryConsume(
133✔
3349
                DiagnosticMessages.unmatchedLeftToken(openingSquare.text, 'array literal'),
3350
                TokenKind.RightSquareBracket
3351
            );
3352
        } else {
3353
            closingSquare = this.previous();
41✔
3354
        }
3355

3356
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3357
        return new ArrayLiteralExpression({ elements: elements, open: openingSquare, close: closingSquare });
174✔
3358
    }
3359

3360
    private aaLiteral() {
3361
        let openingBrace = this.previous();
325✔
3362
        let members: Array<AAMemberExpression> = [];
325✔
3363

3364
        let key = () => {
325✔
3365
            let result = {
354✔
3366
                colonToken: null as Token,
3367
                keyToken: null as Token,
3368
                range: null as Range
3369
            };
3370
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
354✔
3371
                result.keyToken = this.identifier(...AllowedProperties);
320✔
3372
            } else if (this.check(TokenKind.StringLiteral)) {
34!
3373
                result.keyToken = this.advance();
34✔
3374
            } else {
UNCOV
3375
                this.diagnostics.push({
×
3376
                    ...DiagnosticMessages.unexpectedAAKey(),
3377
                    location: this.peek().location
3378
                });
UNCOV
3379
                throw this.lastDiagnosticAsError();
×
3380
            }
3381

3382
            result.colonToken = this.consume(
354✔
3383
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3384
                TokenKind.Colon
3385
            );
3386
            result.range = util.createBoundingRange(result.keyToken, result.colonToken);
353✔
3387
            return result;
353✔
3388
        };
3389

3390
        while (this.match(TokenKind.Newline)) { }
325✔
3391
        let closingBrace: Token;
3392
        if (!this.match(TokenKind.RightCurlyBrace)) {
325✔
3393
            let lastAAMember: AAMemberExpression;
3394
            try {
240✔
3395
                let k = key();
240✔
3396
                let expr = this.expression();
240✔
3397
                lastAAMember = new AAMemberExpression({
239✔
3398
                    key: k.keyToken,
3399
                    colon: k.colonToken,
3400
                    value: expr
3401
                });
3402
                members.push(lastAAMember);
239✔
3403

3404
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
239✔
3405
                    // collect comma at end of expression
3406
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
256✔
3407
                        (lastAAMember as DeepWriteable<AAMemberExpression>).tokens.comma = this.previous();
88✔
3408
                    }
3409

3410
                    this.consumeStatementSeparators(true);
256✔
3411

3412
                    if (this.check(TokenKind.RightCurlyBrace)) {
256✔
3413
                        break;
142✔
3414
                    }
3415
                    let k = key();
114✔
3416
                    let expr = this.expression();
113✔
3417
                    lastAAMember = new AAMemberExpression({
113✔
3418
                        key: k.keyToken,
3419
                        colon: k.colonToken,
3420
                        value: expr
3421
                    });
3422
                    members.push(lastAAMember);
113✔
3423

3424
                }
3425
            } catch (error: any) {
3426
                this.rethrowNonDiagnosticError(error);
2✔
3427
            }
3428

3429
            closingBrace = this.tryConsume(
240✔
3430
                DiagnosticMessages.unmatchedLeftToken(openingBrace.text, 'associative array literal'),
3431
                TokenKind.RightCurlyBrace
3432
            );
3433
        } else {
3434
            closingBrace = this.previous();
85✔
3435
        }
3436

3437
        const aaExpr = new AALiteralExpression({ elements: members, open: openingBrace, close: closingBrace });
325✔
3438
        return aaExpr;
325✔
3439
    }
3440

3441
    /**
3442
     * Pop token if we encounter specified token
3443
     */
3444
    private match(tokenKind: TokenKind) {
3445
        if (this.check(tokenKind)) {
72,628✔
3446
            this.current++; //advance
7,170✔
3447
            return true;
7,170✔
3448
        }
3449
        return false;
65,458✔
3450
    }
3451

3452
    /**
3453
     * Pop token if we encounter a token in the specified list
3454
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3455
     */
3456
    private matchAny(...tokenKinds: TokenKind[]) {
3457
        for (let tokenKind of tokenKinds) {
249,803✔
3458
            if (this.check(tokenKind)) {
723,182✔
3459
                this.current++; //advance
65,860✔
3460
                return true;
65,860✔
3461
            }
3462
        }
3463
        return false;
183,943✔
3464
    }
3465

3466
    /**
3467
     * If the next series of tokens matches the given set of tokens, pop them all
3468
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
3469
     */
3470
    private matchSequence(...tokenKinds: TokenKind[]) {
3471
        const endIndex = this.current + tokenKinds.length;
21,613✔
3472
        for (let i = 0; i < tokenKinds.length; i++) {
21,613✔
3473
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
21,637!
3474
                return false;
21,610✔
3475
            }
3476
        }
3477
        this.current = endIndex;
3✔
3478
        return true;
3✔
3479
    }
3480

3481
    /**
3482
     * Get next token matching a specified list, or fail with an error
3483
     */
3484
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
3485
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
22,238✔
3486
        if (token) {
22,238✔
3487
            return token;
22,211✔
3488
        } else {
3489
            let error = new Error(diagnosticInfo.message);
27✔
3490
            (error as any).isDiagnostic = true;
27✔
3491
            throw error;
27✔
3492
        }
3493
    }
3494

3495
    /**
3496
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3497
     */
3498
    private consumeTokenIf(tokenKind: TokenKind) {
3499
        if (this.match(tokenKind)) {
4,233✔
3500
            return this.previous();
417✔
3501
        }
3502
    }
3503

3504
    private consumeToken(tokenKind: TokenKind) {
3505
        return this.consume(
2,628✔
3506
            DiagnosticMessages.expectedToken(tokenKind),
3507
            tokenKind
3508
        );
3509
    }
3510

3511
    /**
3512
     * Consume, or add a message if not found. But then continue and return undefined
3513
     */
3514
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3515
        const nextKind = this.peek().kind;
31,107✔
3516
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
59,133✔
3517

3518
        if (foundTokenKind) {
31,107✔
3519
            return this.advance();
30,969✔
3520
        }
3521
        this.diagnostics.push({
138✔
3522
            ...diagnostic,
3523
            location: this.peek()?.location
414!
3524
        });
3525
    }
3526

3527
    private tryConsumeToken(tokenKind: TokenKind) {
3528
        return this.tryConsume(
98✔
3529
            DiagnosticMessages.expectedToken(tokenKind),
3530
            tokenKind
3531
        );
3532
    }
3533

3534
    private consumeStatementSeparators(optional = false) {
11,823✔
3535
        //a comment or EOF mark the end of the statement
3536
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
35,860✔
3537
            return true;
754✔
3538
        }
3539
        let consumed = false;
35,106✔
3540
        //consume any newlines and colons
3541
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
35,106✔
3542
            consumed = true;
37,884✔
3543
        }
3544
        if (!optional && !consumed) {
35,106✔
3545
            this.diagnostics.push({
69✔
3546
                ...DiagnosticMessages.expectedNewlineOrColon(),
3547
                location: this.peek()?.location
207!
3548
            });
3549
        }
3550
        return consumed;
35,106✔
3551
    }
3552

3553
    private advance(): Token {
3554
        if (!this.isAtEnd()) {
61,155✔
3555
            this.current++;
61,141✔
3556
        }
3557
        return this.previous();
61,155✔
3558
    }
3559

3560
    private checkEndOfStatement(): boolean {
3561
        const nextKind = this.peek().kind;
8,811✔
3562
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
8,811✔
3563
    }
3564

3565
    private checkPrevious(tokenKind: TokenKind): boolean {
3566
        return this.previous()?.kind === tokenKind;
269!
3567
    }
3568

3569
    /**
3570
     * Check that the next token kind is the expected kind
3571
     * @param tokenKind the expected next kind
3572
     * @returns true if the next tokenKind is the expected value
3573
     */
3574
    private check(tokenKind: TokenKind): boolean {
3575
        const nextKind = this.peek().kind;
1,164,500✔
3576
        if (nextKind === TokenKind.Eof) {
1,164,500✔
3577
            return false;
13,528✔
3578
        }
3579
        return nextKind === tokenKind;
1,150,972✔
3580
    }
3581

3582
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3583
        const nextKind = this.peek().kind;
196,554✔
3584
        if (nextKind === TokenKind.Eof) {
196,554✔
3585
            return false;
1,412✔
3586
        }
3587
        return tokenKinds.includes(nextKind);
195,142✔
3588
    }
3589

3590
    private checkNext(tokenKind: TokenKind): boolean {
3591
        if (this.isAtEnd()) {
16,444!
UNCOV
3592
            return false;
×
3593
        }
3594
        return this.peekNext().kind === tokenKind;
16,444✔
3595
    }
3596

3597
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3598
        if (this.isAtEnd()) {
7,221!
UNCOV
3599
            return false;
×
3600
        }
3601
        const nextKind = this.peekNext().kind;
7,221✔
3602
        return tokenKinds.includes(nextKind);
7,221✔
3603
    }
3604

3605
    private isAtEnd(): boolean {
3606
        const peekToken = this.peek();
181,179✔
3607
        return !peekToken || peekToken.kind === TokenKind.Eof;
181,179✔
3608
    }
3609

3610
    private peekNext(): Token {
3611
        if (this.isAtEnd()) {
23,665!
UNCOV
3612
            return this.peek();
×
3613
        }
3614
        return this.tokens[this.current + 1];
23,665✔
3615
    }
3616

3617
    private peek(): Token {
3618
        return this.tokens[this.current];
1,608,150✔
3619
    }
3620

3621
    private previous(): Token {
3622
        return this.tokens[this.current - 1];
104,423✔
3623
    }
3624

3625
    /**
3626
     * Sometimes we catch an error that is a diagnostic.
3627
     * If that's the case, we want to continue parsing.
3628
     * Otherwise, re-throw the error
3629
     *
3630
     * @param error error caught in a try/catch
3631
     */
3632
    private rethrowNonDiagnosticError(error) {
3633
        if (!error.isDiagnostic) {
12!
UNCOV
3634
            throw error;
×
3635
        }
3636
    }
3637

3638
    /**
3639
     * Get the token that is {offset} indexes away from {this.current}
3640
     * @param offset the number of index steps away from current index to fetch
3641
     * @param tokenKinds the desired token must match one of these
3642
     * @example
3643
     * getToken(-1); //returns the previous token.
3644
     * getToken(0);  //returns current token.
3645
     * getToken(1);  //returns next token
3646
     */
3647
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3648
        const token = this.tokens[this.current + offset];
161✔
3649
        if (tokenKinds.includes(token.kind)) {
161✔
3650
            return token;
3✔
3651
        }
3652
    }
3653

3654
    private synchronize() {
3655
        this.advance(); // skip the erroneous token
110✔
3656

3657
        while (!this.isAtEnd()) {
110✔
3658
            if (this.ensureNewLineOrColon(true)) {
222✔
3659
                // end of statement reached
3660
                return;
68✔
3661
            }
3662

3663
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
154✔
3664
                case TokenKind.Namespace:
2!
3665
                case TokenKind.Class:
3666
                case TokenKind.Function:
3667
                case TokenKind.Sub:
3668
                case TokenKind.If:
3669
                case TokenKind.For:
3670
                case TokenKind.ForEach:
3671
                case TokenKind.While:
3672
                case TokenKind.Print:
3673
                case TokenKind.Return:
3674
                    // start parsing again from the next block starter or obvious
3675
                    // expression start
3676
                    return;
1✔
3677
            }
3678

3679
            this.advance();
153✔
3680
        }
3681
    }
3682

3683

3684
    public dispose() {
3685
    }
3686
}
3687

3688
export enum ParseMode {
1✔
3689
    BrightScript = 'BrightScript',
1✔
3690
    BrighterScript = 'BrighterScript'
1✔
3691
}
3692

3693
export interface ParseOptions {
3694
    /**
3695
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3696
     */
3697
    mode?: ParseMode;
3698
    /**
3699
     * A logger that should be used for logging. If omitted, a default logger is used
3700
     */
3701
    logger?: Logger;
3702
    /**
3703
     * Path to the file where this source code originated
3704
     */
3705
    srcPath?: string;
3706
    /**
3707
     * Should locations be tracked. If false, the `range` property will be omitted
3708
     * @default true
3709
     */
3710
    trackLocations?: boolean;
3711
    /**
3712
     *
3713
     */
3714
    bsConsts?: Map<string, boolean>;
3715
}
3716

3717

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

© 2026 Coveralls, Inc