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

rokucommunity / brighterscript / #15035

15 Dec 2025 08:42PM UTC coverage: 86.889%. Remained the same
#15035

push

web-flow
Merge a60226157 into 2ea4d2108

14466 of 17575 branches covered (82.31%)

Branch coverage included in aggregate %.

113 of 217 new or added lines in 8 files covered. (52.07%)

116 existing lines in 6 files now uncovered.

15185 of 16550 relevant lines covered (91.75%)

24075.2 hits per line

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

91.13
/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,117✔
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,117✔
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,055✔
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,117✔
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,340✔
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,087✔
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,094✔
191
        options = this.sanitizeParseOptions(options);
4,094✔
192
        this.options = options;
4,094✔
193

194
        let tokens: Token[];
195
        if (typeof toParse === 'string') {
4,094✔
196
            tokens = Lexer.scan(toParse, {
612✔
197
                trackLocations: options.trackLocations,
198
                srcPath: options?.srcPath
1,836!
199
            }).tokens;
200
        } else {
201
            tokens = toParse;
3,482✔
202
        }
203
        this.tokens = tokens;
4,094✔
204
        this.allowedLocalIdentifiers = [
4,094✔
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,094✔
208
        ];
209
        this.current = 0;
4,094✔
210
        this.diagnostics = [];
4,094✔
211
        this.namespaceAndFunctionDepth = 0;
4,094✔
212
        this.pendingAnnotations = [];
4,094✔
213

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

221
    private logger: Logger;
222

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

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

230
            try {
4,761✔
231
                while (
4,761✔
232
                    //not at end of tokens
233
                    !this.isAtEnd() &&
19,177✔
234
                    //the next token is not one of the end terminators
235
                    !this.checkAny(...this.peekGlobalTerminators())
236
                ) {
237
                    let dec = this.declaration();
6,875✔
238
                    if (dec) {
6,875✔
239
                        if (!isAnnotationExpression(dec)) {
6,833✔
240
                            this.consumePendingAnnotations(dec);
6,782✔
241
                            body.statements.push(dec);
6,782✔
242
                            //ensure statement separator
243
                            this.consumeStatementSeparators(false);
6,782✔
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,762✔
256
        return body;
4,762✔
257
    }
258

259
    private sanitizeParseOptions(options: ParseOptions) {
260
        options ??= {
4,094✔
261
            srcPath: undefined
262
        };
263
        options.mode ??= ParseMode.BrightScript;
4,094✔
264
        options.trackLocations ??= true;
4,094✔
265
        return options;
4,094✔
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;
59,348✔
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,824✔
280
            let diagnostic = {
168✔
281
                ...DiagnosticMessages.bsFeatureNotSupportedInBrsFiles(featureName),
282
                location: this.peek().location
283
            };
284
            this.diagnostics.push(diagnostic);
168✔
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');
112!
293
        (error as any).isDiagnostic = true;
112✔
294
        return error;
112✔
295
    }
296

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

309
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
15,779✔
310
                return this.functionDeclaration(false);
3,756✔
311
            }
312

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

317
            if (this.checkAlias()) {
12,010✔
318
                return this.aliasStatement();
33✔
319
            }
320
            if (this.checkTypeStatement()) {
11,977✔
321
                return this.typeStatement();
24✔
322
            }
323

324
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
11,953✔
325
                return this.constDeclaration();
191✔
326
            }
327

328
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
11,762✔
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,704!
334
                return;
×
335
            }
336

337
            return this.statement();
11,704✔
338
        } catch (error: any) {
339
            //if the error is not a diagnostic, then log the error for debugging purposes
340
            if (!error.isDiagnostic) {
102✔
341
                this.logger.error(error);
1✔
342
            }
343
            this.synchronize();
102✔
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,013✔
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,013✔
371
        return identifier;
1,013✔
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);
238✔
396
        let asToken;
397
        let typeExpression;
398
        if (this.check(TokenKind.As)) {
238✔
399
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
219✔
400
        }
401
        return new InterfaceFieldStatement({ name: name, as: asToken, typeExpression: typeExpression, optional: optionalKeyword });
238✔
402
    }
403

404
    private consumeAsTokenAndTypeExpression(ignoreDiagnostics = false): [Token, TypeExpression] {
1,782✔
405
        let asToken = this.consumeToken(TokenKind.As);
1,799✔
406
        let typeExpression: TypeExpression;
407
        if (asToken) {
1,799!
408
            //if there's nothing after the `as`, add a diagnostic and continue
409
            if (this.checkEndOfStatement()) {
1,799✔
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,797✔
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,788✔
427
            }
428
        }
429
        return [asToken, typeExpression];
1,799✔
430
    }
431

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

440
        let params = [] as FunctionParameterExpression[];
45✔
441
        if (!this.check(TokenKind.RightParen)) {
45✔
442
            do {
10✔
443
                if (params.length >= CallExpression.MaximumArguments) {
12!
444
                    this.diagnostics.push({
×
445
                        ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments),
446
                        location: this.peek().location
447
                    });
448
                }
449

450
                params.push(this.functionParameter());
12✔
451
            } while (this.match(TokenKind.Comma));
452
        }
453
        const rightParen = this.consumeToken(TokenKind.RightParen);
45✔
454
        // let asToken = null as Token;
455
        // let returnTypeExpression: TypeExpression;
456
        let asToken: Token;
457
        let returnTypeExpression: TypeExpression;
458
        if (this.check(TokenKind.As)) {
45✔
459
            [asToken, returnTypeExpression] = this.consumeAsTokenAndTypeExpression();
32✔
460
        }
461

462
        return new InterfaceMethodStatement({
45✔
463
            functionType: functionType,
464
            name: name,
465
            leftParen: leftParen,
466
            params: params,
467
            rightParen: rightParen,
468
            as: asToken,
469
            returnTypeExpression: returnTypeExpression,
470
            optional: optionalKeyword
471
        });
472
    }
473

474
    private interfaceDeclaration(): InterfaceStatement {
475
        this.warnIfNotBrighterScriptMode('interface declarations');
205✔
476

477
        const parentAnnotations = this.enterAnnotationBlock();
205✔
478

479
        const interfaceToken = this.consume(
205✔
480
            DiagnosticMessages.expectedKeyword(TokenKind.Interface),
481
            TokenKind.Interface
482
        );
483
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
205✔
484

485
        let extendsToken: Token;
486
        let parentInterfaceName: TypeExpression;
487

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

509
                let decl: Statement;
510

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

525
                    //methods (function/sub keyword followed by opening paren)
526
                } else if (this.checkAny(TokenKind.Function, TokenKind.Sub) && this.checkAnyNext(TokenKind.Identifier, ...AllowedProperties)) {
47✔
527
                    decl = this.interfaceMethodStatement(optionalKeyword);
45✔
528

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

542
            //ensure statement separator
543
            this.consumeStatementSeparators();
285✔
544
        }
545

546
        //consume the final `end interface` token
547
        const endInterfaceToken = this.consumeToken(TokenKind.EndInterface);
205✔
548

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

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

568
        this.warnIfNotBrighterScriptMode('enum declarations');
187✔
569

570
        const parentAnnotations = this.enterAnnotationBlock();
187✔
571

572
        this.consumeStatementSeparators();
187✔
573

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

580
                //collect leading annotations
581
                if (this.check(TokenKind.At)) {
367!
582
                    this.annotationExpression();
×
583
                }
584

585
                //members
586
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
367!
587
                    decl = this.enumMemberStatement();
367✔
588
                }
589

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

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

610
        //consume the final `end interface` token
611
        const endEnumToken = this.consumeToken(TokenKind.EndEnum);
187✔
612

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

620
        this.exitAnnotationBlock(parentAnnotations);
186✔
621
        return result;
186✔
622
    }
623

624
    /**
625
     * A BrighterScript class declaration
626
     */
627
    private classDeclaration(): ClassStatement {
628
        this.warnIfNotBrighterScriptMode('class declarations');
708✔
629

630
        const parentAnnotations = this.enterAnnotationBlock();
708✔
631

632
        let classKeyword = this.consume(
708✔
633
            DiagnosticMessages.expectedKeyword(TokenKind.Class),
634
            TokenKind.Class
635
        );
636
        let extendsKeyword: Token;
637
        let parentClassName: TypeExpression;
638

639
        //get the class name
640
        let className = this.tryConsume(DiagnosticMessages.expectedIdentifier('class'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
708✔
641

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

655
        //ensure statement separator
656
        this.consumeStatementSeparators();
708✔
657

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

665
                if (this.check(TokenKind.At)) {
732✔
666
                    this.annotationExpression();
15✔
667
                }
668

669
                if (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private)) {
731✔
670
                    //use actual access modifier
671
                    accessModifier = this.advance();
97✔
672
                }
673

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

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

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

697
                    //fields
698
                } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
360✔
699

700
                    decl = this.fieldDeclaration(accessModifier);
346✔
701

702
                    //class fields cannot be overridden
703
                    if (overrideKeyword) {
345!
704
                        this.diagnostics.push({
×
705
                            ...DiagnosticMessages.classFieldCannotBeOverridden(),
706
                            location: overrideKeyword.location
707
                        });
708
                    }
709

710
                }
711

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

721
            //ensure statement separator
722
            this.consumeStatementSeparators();
732✔
723
        }
724

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

733
        const result = new ClassStatement({
708✔
734
            class: classKeyword,
735
            name: className,
736
            body: body,
737
            endClass: endingKeyword,
738
            extends: extendsKeyword,
739
            parentClassName: parentClassName
740
        });
741

742
        this.exitAnnotationBlock(parentAnnotations);
708✔
743
        return result;
708✔
744
    }
745

746
    private fieldDeclaration(accessModifier: Token | null) {
747

748
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
346✔
749

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

773
        let name = this.consume(
346✔
774
            DiagnosticMessages.expectedIdentifier(),
775
            TokenKind.Identifier,
776
            ...AllowedProperties
777
        ) as Identifier;
778

779
        let asToken: Token;
780
        let fieldTypeExpression: TypeExpression;
781
        //look for `as SOME_TYPE`
782
        if (this.check(TokenKind.As)) {
346✔
783
            [asToken, fieldTypeExpression] = this.consumeAsTokenAndTypeExpression();
231✔
784
        }
785

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

794
        return new FieldStatement({
345✔
795
            accessModifier: accessModifier,
796
            name: name,
797
            as: asToken,
798
            typeExpression: fieldTypeExpression,
799
            equals: equal,
800
            initialValue: initialValue,
801
            optional: optionalKeyword
802
        });
803
    }
804

805
    /**
806
     * An array of CallExpression for the current function body
807
     */
808
    private callExpressions = [];
4,117✔
809

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

842
            if (isAnonymous) {
4,222✔
843
                leftParen = this.consume(
95✔
844
                    DiagnosticMessages.expectedToken('('),
845
                    TokenKind.LeftParen
846
                );
847
            } else {
848
                name = this.consume(
4,127✔
849
                    DiagnosticMessages.expectedIdentifier(functionTypeText),
850
                    TokenKind.Identifier,
851
                    ...AllowedProperties
852
                ) as Identifier;
853
                leftParen = this.consume(
4,125✔
854
                    DiagnosticMessages.expectedToken('('),
855
                    TokenKind.LeftParen
856
                );
857

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

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

877
            let params = [] as FunctionParameterExpression[];
4,216✔
878
            let asToken: Token;
879
            let typeExpression: TypeExpression;
880
            if (!this.check(TokenKind.RightParen)) {
4,216✔
881
                do {
1,979✔
882
                    params.push(this.functionParameter());
3,411✔
883
                } while (this.match(TokenKind.Comma));
884
            }
885
            let rightParen = this.advance();
4,215✔
886

887
            if (this.check(TokenKind.As)) {
4,215✔
888
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
390✔
889
            }
890

891
            params.reduce((haveFoundOptional: boolean, param: FunctionParameterExpression) => {
4,215✔
892
                if (haveFoundOptional && !param.defaultValue) {
3,409!
893
                    this.diagnostics.push({
×
894
                        ...DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter(param.tokens.name.text),
895
                        location: param.location
896
                    });
897
                }
898

899
                return haveFoundOptional || !!param.defaultValue;
3,409✔
900
            }, false);
901

902
            this.consumeStatementSeparators(true);
4,215✔
903

904

905
            //support ending the function with `end sub` OR `end function`
906
            let body = this.block();
4,215✔
907
            //if the parser was unable to produce a block, make an empty one so the AST makes some sense...
908

909
            // consume 'end sub' or 'end function'
910
            const endFunctionType = this.advance();
4,215✔
911
            let expectedEndKind = isSub ? TokenKind.EndSub : TokenKind.EndFunction;
4,215✔
912

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

922
            if (!body) {
4,215✔
923
                body = new Block({ statements: [] });
3✔
924
            }
925

926
            let func = new FunctionExpression({
4,215✔
927
                parameters: params,
928
                body: body,
929
                functionType: functionType,
930
                endFunctionType: endFunctionType,
931
                leftParen: leftParen,
932
                rightParen: rightParen,
933
                as: asToken,
934
                returnTypeExpression: typeExpression
935
            });
936

937
            if (isAnonymous) {
4,215✔
938
                return func;
94✔
939
            } else {
940
                let result = new FunctionStatement({ name: name, func: func });
4,121✔
941
                return result;
4,121✔
942
            }
943
        } finally {
944
            this.namespaceAndFunctionDepth--;
4,222✔
945
            //restore the previous CallExpression list
946
            this.callExpressions = previousCallExpressions;
4,222✔
947
        }
948
    }
949

950
    private functionParameter(): FunctionParameterExpression {
951
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
3,423✔
952
            this.diagnostics.push({
1✔
953
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
954
                location: this.peek().location
955
            });
956
            throw this.lastDiagnosticAsError();
1✔
957
        }
958

959
        let name = this.advance() as Identifier;
3,422✔
960
        // force the name into an identifier so the AST makes some sense
961
        name.kind = TokenKind.Identifier;
3,422✔
962

963
        //add diagnostic if name is a reserved word that cannot be used as an identifier
964
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
3,422✔
965
            this.diagnostics.push({
3✔
966
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
967
                location: name.location
968
            });
969
        }
970

971
        let typeExpression: TypeExpression;
972
        let defaultValue;
973
        let equalToken: Token;
974
        // parse argument default value
975
        if ((equalToken = this.consumeTokenIf(TokenKind.Equal))) {
3,422✔
976
            // it seems any expression is allowed here -- including ones that operate on other arguments!
977
            defaultValue = this.expression(false);
373✔
978
        }
979

980
        let asToken: Token = null;
3,422✔
981
        if (this.check(TokenKind.As)) {
3,422✔
982
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
774✔
983

984
        }
985
        return new FunctionParameterExpression({
3,422✔
986
            name: name,
987
            equals: equalToken,
988
            defaultValue: defaultValue,
989
            as: asToken,
990
            typeExpression: typeExpression
991
        });
992
    }
993

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

1006
        if (allowTypedAssignment) {
1,699✔
1007
            //look for `as SOME_TYPE`
1008
            if (this.check(TokenKind.As)) {
16!
1009
                this.warnIfNotBrighterScriptMode('typed assignment');
16✔
1010

1011
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
16✔
1012
            }
1013
        }
1014

1015
        let operator = this.consume(
1,699✔
1016
            DiagnosticMessages.expectedOperator([TokenKind.Equal], name.text),
1017
            ...[TokenKind.Equal]
1018
        );
1019
        let value = this.expression();
1,695✔
1020

1021
        let result = new AssignmentStatement({ equals: operator, name: name, value: value, as: asToken, typeExpression: typeExpression });
1,688✔
1022

1023
        return result;
1,688✔
1024
    }
1025

1026
    private augmentedAssignment(): AugmentedAssignmentStatement {
1027
        let item = this.expression();
75✔
1028

1029
        let operator = this.consume(
75✔
1030
            DiagnosticMessages.expectedToken(...CompoundAssignmentOperators),
1031
            ...CompoundAssignmentOperators
1032
        );
1033
        let value = this.expression();
75✔
1034

1035
        let result = new AugmentedAssignmentStatement({
75✔
1036
            item: item,
1037
            operator: operator,
1038
            value: value
1039
        });
1040

1041
        return result;
75✔
1042
    }
1043

1044
    private checkLibrary() {
1045
        let isLibraryToken = this.check(TokenKind.Library);
23,813✔
1046

1047
        //if we are at the top level, any line that starts with "library" should be considered a library statement
1048
        if (this.isAtRootLevel() && isLibraryToken) {
23,813✔
1049
            return true;
12✔
1050

1051
            //not at root level, library statements are all invalid here, but try to detect if the tokens look
1052
            //like a library statement (and let the libraryStatement function handle emitting the diagnostics)
1053
        } else if (isLibraryToken && this.checkNext(TokenKind.StringLiteral)) {
23,801✔
1054
            return true;
1✔
1055

1056
            //definitely not a library statement
1057
        } else {
1058
            return false;
23,800✔
1059
        }
1060
    }
1061

1062
    private checkAlias() {
1063
        let isAliasToken = this.check(TokenKind.Alias);
23,558✔
1064

1065
        //if we are at the top level, any line that starts with "alias" should be considered a alias statement
1066
        if (this.isAtRootLevel() && isAliasToken) {
23,558✔
1067
            return true;
31✔
1068

1069
            //not at root level, alias statements are all invalid here, but try to detect if the tokens look
1070
            //like a alias statement (and let the alias function handle emitting the diagnostics)
1071
        } else if (isAliasToken && this.checkNext(TokenKind.Identifier)) {
23,527✔
1072
            return true;
2✔
1073

1074
            //definitely not a alias statement
1075
        } else {
1076
            return false;
23,525✔
1077
        }
1078
    }
1079

1080
    private checkTypeStatement() {
1081
        let isTypeToken = this.check(TokenKind.Type);
11,977✔
1082

1083
        //if we are at the top level, any line that starts with "type" should be considered a type statement
1084
        if (this.isAtRootLevel() && isTypeToken) {
11,977✔
1085
            return true;
22✔
1086

1087
            //not at root level, type statements are all invalid here, but try to detect if the tokens look
1088
            //like a type statement (and let the type function handle emitting the diagnostics)
1089
        } else if (isTypeToken && this.checkNext(TokenKind.Identifier)) {
11,955✔
1090
            return true;
2✔
1091

1092
            //definitely not a type statement
1093
        } else {
1094
            return false;
11,953✔
1095
        }
1096
    }
1097

1098
    private statement(): Statement | undefined {
1099
        if (this.checkLibrary()) {
11,790!
1100
            return this.libraryStatement();
×
1101
        }
1102

1103
        if (this.check(TokenKind.Import)) {
11,790✔
1104
            return this.importStatement();
215✔
1105
        }
1106

1107
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
11,575✔
1108
            return this.typecastStatement();
27✔
1109
        }
1110

1111
        if (this.checkAlias()) {
11,548!
1112
            return this.aliasStatement();
×
1113
        }
1114

1115
        if (this.check(TokenKind.Stop)) {
11,548✔
1116
            return this.stopStatement();
16✔
1117
        }
1118

1119
        if (this.check(TokenKind.If)) {
11,532✔
1120
            return this.ifStatement();
1,271✔
1121
        }
1122

1123
        //`try` must be followed by a block, otherwise it could be a local variable
1124
        if (this.check(TokenKind.Try) && this.checkAnyNext(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
10,261✔
1125
            return this.tryCatchStatement();
41✔
1126
        }
1127

1128
        if (this.check(TokenKind.Throw)) {
10,220✔
1129
            return this.throwStatement();
12✔
1130
        }
1131

1132
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
10,208✔
1133
            return this.printStatement();
1,375✔
1134
        }
1135
        if (this.check(TokenKind.Dim)) {
8,833✔
1136
            return this.dimStatement();
43✔
1137
        }
1138

1139
        if (this.check(TokenKind.While)) {
8,790✔
1140
            return this.whileStatement();
33✔
1141
        }
1142

1143
        if (this.checkAny(TokenKind.Exit, TokenKind.ExitWhile)) {
8,757✔
1144
            return this.exitStatement();
22✔
1145
        }
1146

1147
        if (this.check(TokenKind.For)) {
8,735✔
1148
            return this.forStatement();
41✔
1149
        }
1150

1151
        if (this.check(TokenKind.ForEach)) {
8,694✔
1152
            return this.forEachStatement();
42✔
1153
        }
1154

1155
        if (this.check(TokenKind.End)) {
8,652✔
1156
            return this.endStatement();
8✔
1157
        }
1158

1159
        if (this.match(TokenKind.Return)) {
8,644✔
1160
            return this.returnStatement();
3,720✔
1161
        }
1162

1163
        if (this.check(TokenKind.Goto)) {
4,924✔
1164
            return this.gotoStatement();
12✔
1165
        }
1166

1167
        //the continue keyword (followed by `for`, `while`, or a statement separator)
1168
        if (this.check(TokenKind.Continue) && this.checkAnyNext(TokenKind.While, TokenKind.For, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
4,912✔
1169
            return this.continueStatement();
12✔
1170
        }
1171

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

1184
        // BrightScript is like python, in that variables can be declared without a `var`,
1185
        // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
1186
        // out what to do with it.
1187
        if (
4,890✔
1188
            this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)
1189
        ) {
1190
            if (this.checkAnyNext(...AssignmentOperators)) {
4,621✔
1191
                if (this.checkAnyNext(...CompoundAssignmentOperators)) {
1,697✔
1192
                    return this.augmentedAssignment();
75✔
1193
                }
1194
                return this.assignment();
1,622✔
1195
            } else if (this.checkNext(TokenKind.As)) {
2,924✔
1196
                // may be a typed assignment
1197
                const backtrack = this.current;
17✔
1198
                let validTypeExpression = false;
17✔
1199

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

1217
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1218
        if (this.check(TokenKind.Interface)) {
3,177✔
1219
            return this.interfaceDeclaration();
205✔
1220
        }
1221

1222
        if (this.check(TokenKind.Class)) {
2,972✔
1223
            return this.classDeclaration();
708✔
1224
        }
1225

1226
        if (this.check(TokenKind.Namespace)) {
2,264✔
1227
            return this.namespaceStatement();
669✔
1228
        }
1229

1230
        if (this.check(TokenKind.Enum)) {
1,595✔
1231
            return this.enumDeclaration();
187✔
1232
        }
1233

1234
        // TODO: support multi-statements
1235
        return this.setStatement();
1,408✔
1236
    }
1237

1238
    private whileStatement(): WhileStatement {
1239
        const whileKeyword = this.advance();
33✔
1240
        const condition = this.expression();
33✔
1241

1242
        this.consumeStatementSeparators();
32✔
1243

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

1258
        return new WhileStatement({
32✔
1259
            while: whileKeyword,
1260
            endWhile: endWhile,
1261
            condition: condition,
1262
            body: whileBlock
1263
        });
1264
    }
1265

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

1272
            const exitText = exitToken.text.substring(0, 4);
5✔
1273
            const whileText = exitToken.text.substring(4);
5✔
1274
            const originalRange = exitToken.location?.range;
5✔
1275
            const originalStart = originalRange?.start;
5✔
1276

1277
            const exitRange = util.createRange(
5✔
1278
                originalStart.line,
1279
                originalStart.character,
1280
                originalStart.line,
1281
                originalStart.character + 4);
1282
            const whileRange = util.createRange(
4✔
1283
                originalStart.line,
1284
                originalStart.character + 4,
1285
                originalStart.line,
1286
                originalStart.character + exitToken.text.length);
1287

1288
            exitToken = createToken(TokenKind.Exit, exitText, util.createLocationFromRange(exitToken.location.uri, exitRange));
4✔
1289
            this.tokens[this.current - 1] = exitToken;
4✔
1290
            const newLoopToken = createToken(TokenKind.While, whileText, util.createLocationFromRange(exitToken.location.uri, whileRange));
4✔
1291
            this.tokens.splice(this.current, 0, newLoopToken);
4✔
1292
        }
1293

1294
        const loopTypeToken = this.tryConsume(
21✔
1295
            DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
1296
            TokenKind.While, TokenKind.For
1297
        );
1298

1299
        return new ExitStatement({
21✔
1300
            exit: exitToken,
1301
            loopType: loopTypeToken
1302
        });
1303
    }
1304

1305
    private forStatement(): ForStatement {
1306
        const forToken = this.advance();
41✔
1307
        const initializer = this.assignment();
41✔
1308

1309
        //TODO: newline allowed?
1310

1311
        const toToken = this.advance();
40✔
1312
        const finalValue = this.expression();
40✔
1313
        let incrementExpression: Expression | undefined;
1314
        let stepToken: Token | undefined;
1315

1316
        if (this.check(TokenKind.Step)) {
40✔
1317
            stepToken = this.advance();
10✔
1318
            incrementExpression = this.expression();
10✔
1319
        } else {
1320
            // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
1321
        }
1322

1323
        this.consumeStatementSeparators();
40✔
1324

1325
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
40✔
1326
        let endForToken: Token;
1327
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
40✔
1328
            this.diagnostics.push({
2✔
1329
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(forToken.text),
1330
                location: this.peek().location
1331
            });
1332
            if (!body) {
2!
1333
                throw this.lastDiagnosticAsError();
×
1334
            }
1335
        } else {
1336
            endForToken = this.advance();
38✔
1337
        }
1338

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

1353
    private forEachStatement(): ForEachStatement {
1354
        let forEach = this.advance();
42✔
1355
        let name = this.advance();
42✔
1356

1357
        let maybeIn = this.peek();
42✔
1358
        if (this.check(TokenKind.Identifier) && maybeIn.text.toLowerCase() === 'in') {
42!
1359
            this.advance();
42✔
1360
        } else {
1361
            this.diagnostics.push({
×
1362
                ...DiagnosticMessages.expectedToken(TokenKind.In),
1363
                location: this.peek().location
1364
            });
1365
            throw this.lastDiagnosticAsError();
×
1366
        }
1367
        maybeIn.kind = TokenKind.In;
42✔
1368

1369
        let target = this.expression();
42✔
1370
        if (!target) {
42!
1371
            this.diagnostics.push({
×
1372
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1373
                location: this.peek().location
1374
            });
1375
            throw this.lastDiagnosticAsError();
×
1376
        }
1377

1378
        this.consumeStatementSeparators();
42✔
1379

1380
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
42✔
1381
        let endForToken: Token;
1382
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
42✔
1383

1384
            this.diagnostics.push({
1✔
1385
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(forEach.text),
1386
                location: this.peek().location
1387
            });
1388
            throw this.lastDiagnosticAsError();
1✔
1389
        }
1390
        endForToken = this.advance();
41✔
1391

1392
        return new ForEachStatement({
41✔
1393
            forEach: forEach,
1394
            in: maybeIn,
1395
            endFor: endForToken,
1396
            item: name,
1397
            target: target,
1398
            body: body
1399
        });
1400
    }
1401

1402
    private namespaceStatement(): NamespaceStatement | undefined {
1403
        this.warnIfNotBrighterScriptMode('namespace');
669✔
1404
        let keyword = this.advance();
669✔
1405

1406
        this.namespaceAndFunctionDepth++;
669✔
1407

1408
        let name = this.identifyingExpression();
669✔
1409
        //set the current namespace name
1410

1411
        this.globalTerminators.push([TokenKind.EndNamespace]);
668✔
1412
        let body = this.body();
668✔
1413
        this.globalTerminators.pop();
668✔
1414

1415
        let endKeyword: Token;
1416
        if (this.check(TokenKind.EndNamespace)) {
668✔
1417
            endKeyword = this.advance();
666✔
1418
        } else {
1419
            //the `end namespace` keyword is missing. add a diagnostic, but keep parsing
1420
            this.diagnostics.push({
2✔
1421
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'),
1422
                location: keyword.location
1423
            });
1424
        }
1425

1426
        this.namespaceAndFunctionDepth--;
668✔
1427

1428
        let result = new NamespaceStatement({
668✔
1429
            namespace: keyword,
1430
            nameExpression: name,
1431
            body: body,
1432
            endNamespace: endKeyword
1433
        });
1434

1435
        //cache the range property so that plugins can't affect it
1436
        result.cacheLocation();
668✔
1437
        result.body.symbolTable.name += `: namespace '${result.name}'`;
668✔
1438
        return result;
668✔
1439
    }
1440

1441
    /**
1442
     * Get an expression with identifiers separated by periods. Useful for namespaces and class extends
1443
     */
1444
    private identifyingExpression(allowedTokenKinds?: TokenKind[]): DottedGetExpression | VariableExpression {
1445
        allowedTokenKinds = allowedTokenKinds ?? this.allowedLocalIdentifiers;
1,467✔
1446
        let firstIdentifier = this.consume(
1,467✔
1447
            DiagnosticMessages.expectedIdentifier(this.previous().text),
1448
            TokenKind.Identifier,
1449
            ...allowedTokenKinds
1450
        ) as Identifier;
1451

1452
        let expr: DottedGetExpression | VariableExpression;
1453

1454
        if (firstIdentifier) {
1,465!
1455
            // force it into an identifier so the AST makes some sense
1456
            firstIdentifier.kind = TokenKind.Identifier;
1,465✔
1457
            const varExpr = new VariableExpression({ name: firstIdentifier });
1,465✔
1458
            expr = varExpr;
1,465✔
1459

1460
            //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
1461
            while (this.check(TokenKind.Dot)) {
1,465✔
1462
                let dot = this.tryConsume(
472✔
1463
                    DiagnosticMessages.unexpectedToken(this.peek().text),
1464
                    TokenKind.Dot
1465
                );
1466
                if (!dot) {
472!
1467
                    break;
×
1468
                }
1469
                let identifier = this.tryConsume(
472✔
1470
                    DiagnosticMessages.expectedIdentifier(),
1471
                    TokenKind.Identifier,
1472
                    ...allowedTokenKinds,
1473
                    ...AllowedProperties
1474
                ) as Identifier;
1475

1476
                if (!identifier) {
472✔
1477
                    break;
3✔
1478
                }
1479
                // force it into an identifier so the AST makes some sense
1480
                identifier.kind = TokenKind.Identifier;
469✔
1481
                expr = new DottedGetExpression({ obj: expr, name: identifier, dot: dot });
469✔
1482
            }
1483
        }
1484
        return expr;
1,465✔
1485
    }
1486
    /**
1487
     * Add an 'unexpected token' diagnostic for any token found between current and the first stopToken found.
1488
     */
1489
    private flagUntil(...stopTokens: TokenKind[]) {
1490
        while (!this.checkAny(...stopTokens) && !this.isAtEnd()) {
4!
1491
            let token = this.advance();
×
1492
            this.diagnostics.push({
×
1493
                ...DiagnosticMessages.unexpectedToken(token.text),
1494
                location: token.location
1495
            });
1496
        }
1497
    }
1498

1499
    /**
1500
     * Consume tokens until one of the `stopTokenKinds` is encountered
1501
     * @param stopTokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
1502
     * @returns - the list of tokens consumed, EXCLUDING the `stopTokenKind` (you can use `this.peek()` to see which one it was)
1503
     */
1504
    private consumeUntil(...stopTokenKinds: TokenKind[]) {
1505
        let result = [] as Token[];
71✔
1506
        //take tokens until we encounter one of the stopTokenKinds
1507
        while (!stopTokenKinds.includes(this.peek().kind)) {
71✔
1508
            result.push(this.advance());
150✔
1509
        }
1510
        return result;
71✔
1511
    }
1512

1513
    private constDeclaration(): ConstStatement | undefined {
1514
        this.warnIfNotBrighterScriptMode('const declaration');
191✔
1515
        const constToken = this.advance();
191✔
1516
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
191✔
1517
        const equalToken = this.consumeToken(TokenKind.Equal);
191✔
1518
        const expression = this.expression();
191✔
1519
        const statement = new ConstStatement({
191✔
1520
            const: constToken,
1521
            name: nameToken,
1522
            equals: equalToken,
1523
            value: expression
1524
        });
1525
        return statement;
191✔
1526
    }
1527

1528
    private libraryStatement(): LibraryStatement | undefined {
1529
        let libStatement = new LibraryStatement({
13✔
1530
            library: this.advance(),
1531
            //grab the next token only if it's a string
1532
            filePath: this.tryConsume(
1533
                DiagnosticMessages.expectedStringLiteralAfterKeyword('library'),
1534
                TokenKind.StringLiteral
1535
            )
1536
        });
1537

1538
        return libStatement;
13✔
1539
    }
1540

1541
    private importStatement() {
1542
        this.warnIfNotBrighterScriptMode('import statements');
215✔
1543
        let importStatement = new ImportStatement({
215✔
1544
            import: this.advance(),
1545
            //grab the next token only if it's a string
1546
            path: this.tryConsume(
1547
                DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
1548
                TokenKind.StringLiteral
1549
            )
1550
        });
1551

1552
        return importStatement;
215✔
1553
    }
1554

1555
    private typecastStatement() {
1556
        this.warnIfNotBrighterScriptMode('typecast statements');
27✔
1557
        const typecastToken = this.advance();
27✔
1558
        const typecastExpr = this.expression();
27✔
1559
        if (isTypecastExpression(typecastExpr)) {
27✔
1560
            return new TypecastStatement({
26✔
1561
                typecast: typecastToken,
1562
                typecastExpression: typecastExpr
1563
            });
1564
        }
1565
        this.diagnostics.push({
1✔
1566
            ...DiagnosticMessages.expectedIdentifier('typecast'),
1567
            location: {
1568
                uri: typecastToken.location.uri,
1569
                range: util.createBoundingRange(typecastToken, this.peek())
1570
            }
1571
        });
1572
        throw this.lastDiagnosticAsError();
1✔
1573
    }
1574

1575
    private aliasStatement(): AliasStatement | undefined {
1576
        this.warnIfNotBrighterScriptMode('alias statements');
33✔
1577
        const aliasToken = this.advance();
33✔
1578
        const name = this.tryConsume(
33✔
1579
            DiagnosticMessages.expectedIdentifier('alias'),
1580
            TokenKind.Identifier
1581
        );
1582
        const equals = this.tryConsume(
33✔
1583
            DiagnosticMessages.expectedToken(TokenKind.Equal),
1584
            TokenKind.Equal
1585
        );
1586
        let value = this.identifyingExpression();
33✔
1587

1588
        let aliasStmt = new AliasStatement({
33✔
1589
            alias: aliasToken,
1590
            name: name,
1591
            equals: equals,
1592
            value: value
1593

1594
        });
1595

1596
        return aliasStmt;
33✔
1597
    }
1598

1599
    private typeStatement(): TypeStatement | undefined {
1600
        this.warnIfNotBrighterScriptMode('type statements');
24✔
1601
        const typeToken = this.advance();
24✔
1602
        const name = this.tryConsume(
24✔
1603
            DiagnosticMessages.expectedIdentifier('type'),
1604
            TokenKind.Identifier
1605
        );
1606
        const equals = this.tryConsume(
24✔
1607
            DiagnosticMessages.expectedToken(TokenKind.Equal),
1608
            TokenKind.Equal
1609
        );
1610
        let value = this.typeExpression();
24✔
1611

1612
        let typeStmt = new TypeStatement({
23✔
1613
            type: typeToken,
1614
            name: name,
1615
            equals: equals,
1616
            value: value
1617

1618
        });
1619

1620
        return typeStmt;
23✔
1621
    }
1622

1623
    private annotationExpression() {
1624
        const atToken = this.advance();
75✔
1625
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
75✔
1626
        if (identifier) {
75✔
1627
            identifier.kind = TokenKind.Identifier;
74✔
1628
        }
1629
        let annotation = new AnnotationExpression({ at: atToken, name: identifier });
75✔
1630
        this.pendingAnnotations.push(annotation);
74✔
1631

1632
        //optional arguments
1633
        if (this.check(TokenKind.LeftParen)) {
74✔
1634
            let leftParen = this.advance();
30✔
1635
            annotation.call = this.finishCall(leftParen, annotation, false);
30✔
1636
        }
1637
        return annotation;
74✔
1638
    }
1639

1640
    private ternaryExpression(test?: Expression): TernaryExpression {
1641
        this.warnIfNotBrighterScriptMode('ternary operator');
98✔
1642
        if (!test) {
98!
1643
            test = this.expression();
×
1644
        }
1645
        const questionMarkToken = this.advance();
98✔
1646

1647
        //consume newlines or comments
1648
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
98✔
1649
            this.advance();
7✔
1650
        }
1651

1652
        let consequent: Expression;
1653
        try {
98✔
1654
            consequent = this.expression();
98✔
1655
        } catch { }
1656

1657
        //consume newlines or comments
1658
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
98✔
1659
            this.advance();
5✔
1660
        }
1661

1662
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
98✔
1663

1664
        //consume newlines
1665
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
98✔
1666
            this.advance();
11✔
1667
        }
1668
        let alternate: Expression;
1669
        try {
98✔
1670
            alternate = this.expression();
98✔
1671
        } catch { }
1672

1673
        return new TernaryExpression({
98✔
1674
            test: test,
1675
            questionMark: questionMarkToken,
1676
            consequent: consequent,
1677
            colon: colonToken,
1678
            alternate: alternate
1679
        });
1680
    }
1681

1682
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1683
        this.warnIfNotBrighterScriptMode('null coalescing operator');
35✔
1684
        const questionQuestionToken = this.advance();
35✔
1685
        const alternate = this.expression();
35✔
1686
        return new NullCoalescingExpression({
35✔
1687
            consequent: test,
1688
            questionQuestion: questionQuestionToken,
1689
            alternate: alternate
1690
        });
1691
    }
1692

1693
    private regexLiteralExpression() {
1694
        this.warnIfNotBrighterScriptMode('regular expression literal');
45✔
1695
        return new RegexLiteralExpression({
45✔
1696
            regexLiteral: this.advance()
1697
        });
1698
    }
1699

1700
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1701
        this.warnIfNotBrighterScriptMode('template string');
55✔
1702

1703
        //get the tag name
1704
        let tagName: Identifier;
1705
        if (isTagged) {
55✔
1706
            tagName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties) as Identifier;
8✔
1707
            // force it into an identifier so the AST makes some sense
1708
            tagName.kind = TokenKind.Identifier;
8✔
1709
        }
1710

1711
        let quasis = [] as TemplateStringQuasiExpression[];
55✔
1712
        let expressions = [];
55✔
1713
        let openingBacktick = this.peek();
55✔
1714
        this.advance();
55✔
1715
        let currentQuasiExpressionParts = [];
55✔
1716
        while (!this.isAtEnd() && !this.check(TokenKind.BackTick)) {
55✔
1717
            let next = this.peek();
206✔
1718
            if (next.kind === TokenKind.TemplateStringQuasi) {
206✔
1719
                //a quasi can actually be made up of multiple quasis when it includes char literals
1720
                currentQuasiExpressionParts.push(
130✔
1721
                    new LiteralExpression({ value: next })
1722
                );
1723
                this.advance();
130✔
1724
            } else if (next.kind === TokenKind.EscapedCharCodeLiteral) {
76✔
1725
                currentQuasiExpressionParts.push(
33✔
1726
                    new EscapedCharCodeLiteralExpression({ value: next as Token & { charCode: number } })
1727
                );
1728
                this.advance();
33✔
1729
            } else {
1730
                //finish up the current quasi
1731
                quasis.push(
43✔
1732
                    new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1733
                );
1734
                currentQuasiExpressionParts = [];
43✔
1735

1736
                if (next.kind === TokenKind.TemplateStringExpressionBegin) {
43!
1737
                    this.advance();
43✔
1738
                }
1739
                //now keep this expression
1740
                expressions.push(this.expression());
43✔
1741
                if (!this.isAtEnd() && this.check(TokenKind.TemplateStringExpressionEnd)) {
43!
1742
                    //TODO is it an error if this is not present?
1743
                    this.advance();
43✔
1744
                } else {
1745
                    this.diagnostics.push({
×
1746
                        ...DiagnosticMessages.unterminatedTemplateExpression(),
1747
                        location: {
1748
                            uri: openingBacktick.location.uri,
1749
                            range: util.createBoundingRange(openingBacktick, this.peek())
1750
                        }
1751
                    });
1752
                    throw this.lastDiagnosticAsError();
×
1753
                }
1754
            }
1755
        }
1756

1757
        //store the final set of quasis
1758
        quasis.push(
55✔
1759
            new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1760
        );
1761

1762
        if (this.isAtEnd()) {
55✔
1763
            //error - missing backtick
1764
            this.diagnostics.push({
2✔
1765
                ...DiagnosticMessages.unterminatedTemplateString(),
1766
                location: {
1767
                    uri: openingBacktick.location.uri,
1768
                    range: util.createBoundingRange(openingBacktick, this.peek())
1769
                }
1770
            });
1771
            throw this.lastDiagnosticAsError();
2✔
1772

1773
        } else {
1774
            let closingBacktick = this.advance();
53✔
1775
            if (isTagged) {
53✔
1776
                return new TaggedTemplateStringExpression({
8✔
1777
                    tagName: tagName,
1778
                    openingBacktick: openingBacktick,
1779
                    quasis: quasis,
1780
                    expressions: expressions,
1781
                    closingBacktick: closingBacktick
1782
                });
1783
            } else {
1784
                return new TemplateStringExpression({
45✔
1785
                    openingBacktick: openingBacktick,
1786
                    quasis: quasis,
1787
                    expressions: expressions,
1788
                    closingBacktick: closingBacktick
1789
                });
1790
            }
1791
        }
1792
    }
1793

1794
    private tryCatchStatement(): TryCatchStatement {
1795
        const tryToken = this.advance();
41✔
1796
        let endTryToken: Token;
1797
        let catchStmt: CatchStatement;
1798
        //ensure statement separator
1799
        this.consumeStatementSeparators();
41✔
1800

1801
        let tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
41✔
1802

1803
        const peek = this.peek();
41✔
1804
        if (peek.kind !== TokenKind.Catch) {
41✔
1805
            this.diagnostics.push({
2✔
1806
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1807
                location: this.peek()?.location
6!
1808
            });
1809
        } else {
1810
            const catchToken = this.advance();
39✔
1811

1812
            //get the exception variable as an expression
1813
            let exceptionVariableExpression: Expression;
1814
            //if we consumed any statement separators, that means we don't have an exception variable
1815
            if (this.consumeStatementSeparators(true)) {
39✔
1816
                //no exception variable. That's fine in BrighterScript but not in brightscript. But that'll get caught by the validator later...
1817
            } else {
1818
                exceptionVariableExpression = this.expression(true);
33✔
1819
                this.consumeStatementSeparators();
33✔
1820
            }
1821

1822
            const catchBranch = this.block(TokenKind.EndTry);
39✔
1823
            catchStmt = new CatchStatement({
39✔
1824
                catch: catchToken,
1825
                exceptionVariableExpression: exceptionVariableExpression,
1826
                catchBranch: catchBranch
1827
            });
1828
        }
1829
        if (this.peek().kind !== TokenKind.EndTry) {
41✔
1830
            this.diagnostics.push({
2✔
1831
                ...DiagnosticMessages.expectedTerminator('end try', 'try-catch'),
1832
                location: this.peek().location
1833
            });
1834
        } else {
1835
            endTryToken = this.advance();
39✔
1836
        }
1837

1838
        const statement = new TryCatchStatement({
41✔
1839
            try: tryToken,
1840
            tryBranch: tryBranch,
1841
            catchStatement: catchStmt,
1842
            endTry: endTryToken
1843
        }
1844
        );
1845
        return statement;
41✔
1846
    }
1847

1848
    private throwStatement() {
1849
        const throwToken = this.advance();
12✔
1850
        let expression: Expression;
1851
        if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
12✔
1852
            this.diagnostics.push({
2✔
1853
                ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(),
1854
                location: throwToken.location
1855
            });
1856
        } else {
1857
            expression = this.expression();
10✔
1858
        }
1859
        return new ThrowStatement({ throw: throwToken, expression: expression });
10✔
1860
    }
1861

1862
    private dimStatement() {
1863
        const dim = this.advance();
43✔
1864

1865
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
43✔
1866
        // force to an identifier so the AST makes some sense
1867
        if (identifier) {
43✔
1868
            identifier.kind = TokenKind.Identifier;
41✔
1869
        }
1870

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

1873
        let expressions: Expression[] = [];
43✔
1874
        let expression: Expression;
1875
        do {
43✔
1876
            try {
82✔
1877
                expression = this.expression();
82✔
1878
                expressions.push(expression);
77✔
1879
                if (this.check(TokenKind.Comma)) {
77✔
1880
                    this.advance();
39✔
1881
                } else {
1882
                    // will also exit for right square braces
1883
                    break;
38✔
1884
                }
1885
            } catch (error) {
1886
            }
1887
        } while (expression);
1888

1889
        if (expressions.length === 0) {
43✔
1890
            this.diagnostics.push({
5✔
1891
                ...DiagnosticMessages.missingExpressionsInDimStatement(),
1892
                location: this.peek().location
1893
            });
1894
        }
1895
        let rightSquareBracket = this.tryConsume(DiagnosticMessages.unmatchedLeftToken('[', 'dim identifier'), TokenKind.RightSquareBracket);
43✔
1896
        return new DimStatement({
43✔
1897
            dim: dim,
1898
            name: identifier,
1899
            openingSquare: leftSquareBracket,
1900
            dimensions: expressions,
1901
            closingSquare: rightSquareBracket
1902
        });
1903
    }
1904

1905
    private nestedInlineConditionalCount = 0;
4,117✔
1906

1907
    private ifStatement(incrementNestedCount = true): IfStatement {
2,348✔
1908
        // colon before `if` is usually not allowed, unless it's after `then`
1909
        if (this.current > 0) {
2,358✔
1910
            const prev = this.previous();
2,353✔
1911
            if (prev.kind === TokenKind.Colon) {
2,353✔
1912
                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then && this.nestedInlineConditionalCount === 0) {
4✔
1913
                    this.diagnostics.push({
1✔
1914
                        ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
1915
                        location: prev.location
1916
                    });
1917
                }
1918
            }
1919
        }
1920

1921
        const ifToken = this.advance();
2,358✔
1922

1923
        const condition = this.expression();
2,358✔
1924
        let thenBranch: Block;
1925
        let elseBranch: IfStatement | Block | undefined;
1926

1927
        let thenToken: Token | undefined;
1928
        let endIfToken: Token | undefined;
1929
        let elseToken: Token | undefined;
1930

1931
        //optional `then`
1932
        if (this.check(TokenKind.Then)) {
2,356✔
1933
            thenToken = this.advance();
1,875✔
1934
        }
1935

1936
        //is it inline or multi-line if?
1937
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
2,356✔
1938

1939
        if (isInlineIfThen) {
2,356✔
1940
            /*** PARSE INLINE IF STATEMENT ***/
1941
            if (!incrementNestedCount) {
48✔
1942
                this.nestedInlineConditionalCount++;
5✔
1943
            }
1944

1945
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
48✔
1946

1947
            if (!thenBranch) {
48!
1948
                this.diagnostics.push({
×
1949
                    ...DiagnosticMessages.expectedStatement(ifToken.text, 'statement'),
1950
                    location: this.peek().location
1951
                });
1952
                throw this.lastDiagnosticAsError();
×
1953
            } else {
1954
                this.ensureInline(thenBranch.statements);
48✔
1955
            }
1956

1957
            //else branch
1958
            if (this.check(TokenKind.Else)) {
48✔
1959
                elseToken = this.advance();
33✔
1960

1961
                if (this.check(TokenKind.If)) {
33✔
1962
                    // recurse-read `else if`
1963
                    elseBranch = this.ifStatement(false);
10✔
1964

1965
                    //no multi-line if chained with an inline if
1966
                    if (!elseBranch.isInline) {
9✔
1967
                        this.diagnostics.push({
4✔
1968
                            ...DiagnosticMessages.expectedInlineIfStatement(),
1969
                            location: elseBranch.location
1970
                        });
1971
                    }
1972

1973
                } else if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
23✔
1974
                    //expecting inline else branch
1975
                    this.diagnostics.push({
3✔
1976
                        ...DiagnosticMessages.expectedInlineIfStatement(),
1977
                        location: this.peek().location
1978
                    });
1979
                    throw this.lastDiagnosticAsError();
3✔
1980
                } else {
1981
                    elseBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
20✔
1982

1983
                    if (elseBranch) {
20!
1984
                        this.ensureInline(elseBranch.statements);
20✔
1985
                    }
1986
                }
1987

1988
                if (!elseBranch) {
29!
1989
                    //missing `else` branch
1990
                    this.diagnostics.push({
×
1991
                        ...DiagnosticMessages.expectedStatement('else', 'statement'),
1992
                        location: this.peek().location
1993
                    });
1994
                    throw this.lastDiagnosticAsError();
×
1995
                }
1996
            }
1997

1998
            if (!elseBranch || !isIfStatement(elseBranch)) {
44✔
1999
                //enforce newline at the end of the inline if statement
2000
                const peek = this.peek();
35✔
2001
                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && peek.kind !== TokenKind.Else && !this.isAtEnd()) {
35✔
2002
                    //ignore last error if it was about a colon
2003
                    if (this.previous().kind === TokenKind.Colon) {
3!
2004
                        this.diagnostics.pop();
3✔
2005
                        this.current--;
3✔
2006
                    }
2007
                    //newline is required
2008
                    this.diagnostics.push({
3✔
2009
                        ...DiagnosticMessages.expectedFinalNewline(),
2010
                        location: this.peek().location
2011
                    });
2012
                }
2013
            }
2014
            this.nestedInlineConditionalCount--;
44✔
2015
        } else {
2016
            /*** PARSE MULTI-LINE IF STATEMENT ***/
2017

2018
            thenBranch = this.blockConditionalBranch(ifToken);
2,308✔
2019

2020
            //ensure newline/colon before next keyword
2021
            this.ensureNewLineOrColon();
2,305✔
2022

2023
            //else branch
2024
            if (this.check(TokenKind.Else)) {
2,305✔
2025
                elseToken = this.advance();
1,822✔
2026

2027
                if (this.check(TokenKind.If)) {
1,822✔
2028
                    // recurse-read `else if`
2029
                    elseBranch = this.ifStatement();
1,077✔
2030

2031
                } else {
2032
                    elseBranch = this.blockConditionalBranch(ifToken);
745✔
2033

2034
                    //ensure newline/colon before next keyword
2035
                    this.ensureNewLineOrColon();
745✔
2036
                }
2037
            }
2038

2039
            if (!isIfStatement(elseBranch)) {
2,305✔
2040
                if (this.check(TokenKind.EndIf)) {
1,228✔
2041
                    endIfToken = this.advance();
1,223✔
2042

2043
                } else {
2044
                    //missing endif
2045
                    this.diagnostics.push({
5✔
2046
                        ...DiagnosticMessages.expectedTerminator('end if', 'if'),
2047
                        location: ifToken.location
2048
                    });
2049
                }
2050
            }
2051
        }
2052

2053
        return new IfStatement({
2,349✔
2054
            if: ifToken,
2055
            then: thenToken,
2056
            endIf: endIfToken,
2057
            else: elseToken,
2058
            condition: condition,
2059
            thenBranch: thenBranch,
2060
            elseBranch: elseBranch
2061
        });
2062
    }
2063

2064
    //consume a `then` or `else` branch block of an `if` statement
2065
    private blockConditionalBranch(ifToken: Token) {
2066
        //keep track of the current error count, because if the then branch fails,
2067
        //we will trash them in favor of a single error on if
2068
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
3,053✔
2069

2070
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
2071
        // a trailing "end if" or "else if"
2072
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
3,053✔
2073

2074
        if (!branch) {
3,053✔
2075
            //throw out any new diagnostics created as a result of a `then` block parse failure.
2076
            //the block() function will discard the current line, so any discarded diagnostics will
2077
            //resurface if they are legitimate, and not a result of a malformed if statement
2078
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
3✔
2079

2080
            //this whole if statement is bogus...add error to the if token and hard-fail
2081
            this.diagnostics.push({
3✔
2082
                ...DiagnosticMessages.expectedTerminator(['end if', 'else if', 'else'], 'then', 'block'),
2083
                location: ifToken.location
2084
            });
2085
            throw this.lastDiagnosticAsError();
3✔
2086
        }
2087
        return branch;
3,050✔
2088
    }
2089

2090
    private conditionalCompileStatement(): ConditionalCompileStatement {
2091
        const hashIfToken = this.advance();
58✔
2092
        let notToken: Token | undefined;
2093

2094
        if (this.check(TokenKind.Not)) {
58✔
2095
            notToken = this.advance();
7✔
2096
        }
2097

2098
        if (!this.checkAny(TokenKind.True, TokenKind.False, TokenKind.Identifier)) {
58✔
2099
            this.diagnostics.push({
1✔
2100
                ...DiagnosticMessages.invalidHashIfValue(),
2101
                location: this.peek()?.location
3!
2102
            });
2103
        }
2104

2105

2106
        const condition = this.advance();
58✔
2107

2108
        let thenBranch: Block;
2109
        let elseBranch: ConditionalCompileStatement | Block | undefined;
2110

2111
        let hashEndIfToken: Token | undefined;
2112
        let hashElseToken: Token | undefined;
2113

2114
        //keep track of the current error count
2115
        //if this is `#if false` remove all diagnostics.
2116
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
58✔
2117

2118
        thenBranch = this.blockConditionalCompileBranch(hashIfToken);
58✔
2119
        const conditionTextLower = condition.text.toLowerCase();
57✔
2120
        if (!this.options.bsConsts?.get(conditionTextLower) || conditionTextLower === 'false') {
57✔
2121
            //throw out any new diagnostics created as a result of a false block
2122
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
43✔
2123
        }
2124

2125
        this.ensureNewLine();
57✔
2126
        this.advance();
57✔
2127

2128
        //else branch
2129
        if (this.check(TokenKind.HashElseIf)) {
57✔
2130
            // recurse-read `#else if`
2131
            elseBranch = this.conditionalCompileStatement();
15✔
2132
            this.ensureNewLine();
15✔
2133

2134
        } else if (this.check(TokenKind.HashElse)) {
42✔
2135
            hashElseToken = this.advance();
11✔
2136
            let diagnosticsLengthBeforeBlock = this.diagnostics.length;
11✔
2137
            elseBranch = this.blockConditionalCompileBranch(hashIfToken);
11✔
2138

2139
            if (condition.text.toLowerCase() === 'true') {
11✔
2140
                //throw out any new diagnostics created as a result of a false block
2141
                this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
1✔
2142
            }
2143
            this.ensureNewLine();
11✔
2144
            this.advance();
11✔
2145
        }
2146

2147
        if (!isConditionalCompileStatement(elseBranch)) {
57✔
2148

2149
            if (this.check(TokenKind.HashEndIf)) {
42!
2150
                hashEndIfToken = this.advance();
42✔
2151

2152
            } else {
2153
                //missing #endif
2154
                this.diagnostics.push({
×
2155
                    ...DiagnosticMessages.expectedTerminator('#end if', '#if'),
2156
                    location: hashIfToken.location
2157
                });
2158
            }
2159
        }
2160

2161
        return new ConditionalCompileStatement({
57✔
2162
            hashIf: hashIfToken,
2163
            hashElse: hashElseToken,
2164
            hashEndIf: hashEndIfToken,
2165
            not: notToken,
2166
            condition: condition,
2167
            thenBranch: thenBranch,
2168
            elseBranch: elseBranch
2169
        });
2170
    }
2171

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

2178
        //parsing until trailing "#end if", "#else", "#else if"
2179
        let branch = this.conditionalCompileBlock();
69✔
2180

2181
        if (!branch) {
68!
2182
            //throw out any new diagnostics created as a result of a `then` block parse failure.
2183
            //the block() function will discard the current line, so any discarded diagnostics will
2184
            //resurface if they are legitimate, and not a result of a malformed if statement
2185
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
×
2186

2187
            //this whole if statement is bogus...add error to the if token and hard-fail
2188
            this.diagnostics.push({
×
2189
                ...DiagnosticMessages.expectedTerminator(['#end if', '#else if', '#else'], 'conditional compilation', 'block'),
2190
                location: hashIfToken.location
2191
            });
2192
            throw this.lastDiagnosticAsError();
×
2193
        }
2194
        return branch;
68✔
2195
    }
2196

2197
    /**
2198
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2199
     * Always looks for `#end if` or `#else`
2200
     */
2201
    private conditionalCompileBlock(): Block | undefined {
2202
        const parentAnnotations = this.enterAnnotationBlock();
69✔
2203

2204
        this.consumeStatementSeparators(true);
69✔
2205
        const unsafeTerminators = BlockTerminators;
69✔
2206
        const conditionalEndTokens = [TokenKind.HashElse, TokenKind.HashElseIf, TokenKind.HashEndIf];
69✔
2207
        const terminators = [...conditionalEndTokens, ...unsafeTerminators];
69✔
2208
        this.globalTerminators.push(conditionalEndTokens);
69✔
2209
        const statements: Statement[] = [];
69✔
2210
        while (!this.isAtEnd() && !this.checkAny(...terminators)) {
69✔
2211
            //grab the location of the current token
2212
            let loopCurrent = this.current;
73✔
2213
            let dec = this.declaration();
73✔
2214
            if (dec) {
73✔
2215
                if (!isAnnotationExpression(dec)) {
72!
2216
                    this.consumePendingAnnotations(dec);
72✔
2217
                    statements.push(dec);
72✔
2218
                }
2219

2220
                const peekKind = this.peek().kind;
72✔
2221
                if (conditionalEndTokens.includes(peekKind)) {
72✔
2222
                    // current conditional compile branch was closed by other statement, rewind to preceding newline
2223
                    this.current--;
1✔
2224
                }
2225
                //ensure statement separator
2226
                this.consumeStatementSeparators();
72✔
2227

2228
            } else {
2229
                //something went wrong. reset to the top of the loop
2230
                this.current = loopCurrent;
1✔
2231

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

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

2239
                //consume potential separators
2240
                this.consumeStatementSeparators(true);
1✔
2241
            }
2242
        }
2243
        this.globalTerminators.pop();
69✔
2244

2245

2246
        if (this.isAtEnd()) {
69!
2247
            return undefined;
×
2248
            // TODO: Figure out how to handle unterminated blocks well
2249
        } else {
2250
            //did we  hit an unsafe terminator?
2251
            //if so, we need to restore the statement separator
2252
            let prev = this.previous();
69✔
2253
            let prevKind = prev.kind;
69✔
2254
            let peek = this.peek();
69✔
2255
            let peekKind = this.peek().kind;
69✔
2256
            if (
69✔
2257
                (peekKind === TokenKind.HashEndIf || peekKind === TokenKind.HashElse || peekKind === TokenKind.HashElseIf) &&
180✔
2258
                (prevKind === TokenKind.Newline)
2259
            ) {
2260
                this.current--;
68✔
2261
            } else if (unsafeTerminators.includes(peekKind) &&
1!
2262
                (prevKind === TokenKind.Newline || prevKind === TokenKind.Colon)
2263
            ) {
2264
                this.diagnostics.push({
1✔
2265
                    ...DiagnosticMessages.unsafeUnmatchedTerminatorInConditionalCompileBlock(peek.text),
2266
                    location: peek.location
2267
                });
2268
                throw this.lastDiagnosticAsError();
1✔
2269
            } else {
2270
                return undefined;
×
2271
            }
2272
        }
2273
        this.exitAnnotationBlock(parentAnnotations);
68✔
2274
        return new Block({ statements: statements });
68✔
2275
    }
2276

2277
    private conditionalCompileConstStatement() {
2278
        const hashConstToken = this.advance();
21✔
2279

2280
        const constName = this.peek();
21✔
2281
        //disallow using keywords for const names
2282
        if (ReservedWords.has(constName?.text.toLowerCase())) {
21!
2283
            this.diagnostics.push({
1✔
2284
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(constName?.text),
3!
2285
                location: constName?.location
3!
2286
            });
2287

2288
            this.lastDiagnosticAsError();
1✔
2289
            return;
1✔
2290
        }
2291
        const assignment = this.assignment();
20✔
2292
        if (assignment) {
18!
2293
            // check for something other than #const <name> = <otherName|true|false>
2294
            if (assignment.tokens.as || assignment.typeExpression) {
18!
2295
                this.diagnostics.push({
×
2296
                    ...DiagnosticMessages.unexpectedToken(assignment.tokens.as?.text || assignment.typeExpression?.getName(ParseMode.BrighterScript)),
×
2297
                    location: assignment.tokens.as?.location ?? assignment.typeExpression?.location
×
2298
                });
2299
                this.lastDiagnosticAsError();
×
2300
            }
2301

2302
            if (isVariableExpression(assignment.value) || isLiteralBoolean(assignment.value)) {
18✔
2303
                //value is an identifier or a boolean
2304
                //check for valid identifiers will happen in program validation
2305
            } else {
2306
                this.diagnostics.push({
2✔
2307
                    ...DiagnosticMessages.invalidHashConstValue(),
2308
                    location: assignment.value.location
2309
                });
2310
                this.lastDiagnosticAsError();
2✔
2311
            }
2312
        } else {
2313
            return undefined;
×
2314
        }
2315

2316
        if (!this.check(TokenKind.Newline)) {
18!
2317
            this.diagnostics.push({
×
2318
                ...DiagnosticMessages.unexpectedToken(this.peek().text),
2319
                location: this.peek().location
2320
            });
2321
            throw this.lastDiagnosticAsError();
×
2322
        }
2323

2324
        return new ConditionalCompileConstStatement({ hashConst: hashConstToken, assignment: assignment });
18✔
2325
    }
2326

2327
    private conditionalCompileErrorStatement() {
2328
        const hashErrorToken = this.advance();
10✔
2329
        const tokensUntilEndOfLine = this.consumeUntil(TokenKind.Newline);
10✔
2330
        const message = createToken(TokenKind.HashErrorMessage, tokensUntilEndOfLine.map(t => t.text).join(' '));
10✔
2331
        return new ConditionalCompileErrorStatement({ hashError: hashErrorToken, message: message });
10✔
2332
    }
2333

2334
    private ensureNewLine() {
2335
        //ensure newline before next keyword
2336
        if (!this.check(TokenKind.Newline)) {
83!
2337
            this.diagnostics.push({
×
2338
                ...DiagnosticMessages.unexpectedToken(this.peek().text),
2339
                location: this.peek().location
2340
            });
2341
            throw this.lastDiagnosticAsError();
×
2342
        }
2343
    }
2344

2345
    private ensureNewLineOrColon(silent = false) {
3,050✔
2346
        const prev = this.previous().kind;
3,257✔
2347
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
3,257✔
2348
            if (!silent) {
144✔
2349
                this.diagnostics.push({
8✔
2350
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2351
                    location: this.peek().location
2352
                });
2353
            }
2354
            return false;
144✔
2355
        }
2356
        return true;
3,113✔
2357
    }
2358

2359
    //ensure each statement of an inline block is single-line
2360
    private ensureInline(statements: Statement[]) {
2361
        for (const stat of statements) {
68✔
2362
            if (isIfStatement(stat) && !stat.isInline) {
86✔
2363
                this.diagnostics.push({
2✔
2364
                    ...DiagnosticMessages.expectedInlineIfStatement(),
2365
                    location: stat.location
2366
                });
2367
            }
2368
        }
2369
    }
2370

2371
    //consume inline branch of an `if` statement
2372
    private inlineConditionalBranch(...additionalTerminators: BlockTerminator[]): Block | undefined {
2373
        let statements = [];
86✔
2374
        //attempt to get the next statement without using `this.declaration`
2375
        //which seems a bit hackish to get to work properly
2376
        let statement = this.statement();
86✔
2377
        if (!statement) {
86!
2378
            return undefined;
×
2379
        }
2380
        statements.push(statement);
86✔
2381

2382
        //look for colon statement separator
2383
        let foundColon = false;
86✔
2384
        while (this.match(TokenKind.Colon)) {
86✔
2385
            foundColon = true;
23✔
2386
        }
2387

2388
        //if a colon was found, add the next statement or err if unexpected
2389
        if (foundColon) {
86✔
2390
            if (!this.checkAny(TokenKind.Newline, ...additionalTerminators)) {
23✔
2391
                //if not an ending keyword, add next statement
2392
                let extra = this.inlineConditionalBranch(...additionalTerminators);
18✔
2393
                if (!extra) {
18!
2394
                    return undefined;
×
2395
                }
2396
                statements.push(...extra.statements);
18✔
2397
            } else {
2398
                //error: colon before next keyword
2399
                const colon = this.previous();
5✔
2400
                this.diagnostics.push({
5✔
2401
                    ...DiagnosticMessages.unexpectedToken(colon.text),
2402
                    location: colon.location
2403
                });
2404
            }
2405
        }
2406
        return new Block({ statements: statements });
86✔
2407
    }
2408

2409
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2410
        let expressionStart = this.peek();
983✔
2411

2412
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
983✔
2413
            let operator = this.advance();
27✔
2414

2415
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
27✔
2416
                this.diagnostics.push({
1✔
2417
                    ...DiagnosticMessages.unexpectedOperator(),
2418
                    location: this.peek().location
2419
                });
2420
                throw this.lastDiagnosticAsError();
1✔
2421
            } else if (isCallExpression(expr)) {
26✔
2422
                this.diagnostics.push({
1✔
2423
                    ...DiagnosticMessages.unexpectedOperator(),
2424
                    location: expressionStart.location
2425
                });
2426
                throw this.lastDiagnosticAsError();
1✔
2427
            }
2428

2429
            const result = new IncrementStatement({ value: expr, operator: operator });
25✔
2430
            return result;
25✔
2431
        }
2432

2433
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
956✔
2434
            return new ExpressionStatement({ expression: expr });
563✔
2435
        }
2436

2437
        if (this.checkAny(...BinaryExpressionOperatorTokens)) {
393✔
2438
            expr = new BinaryExpression({ left: expr, operator: this.advance(), right: this.expression() });
7✔
2439
        }
2440

2441
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an inclosing ExpressionStatement
2442
        this.diagnostics.push({
393✔
2443
            ...DiagnosticMessages.expectedStatement(),
2444
            location: expressionStart.location
2445
        });
2446
        return new ExpressionStatement({ expression: expr });
393✔
2447
    }
2448

2449
    private setStatement(): DottedSetStatement | IndexedSetStatement | ExpressionStatement | IncrementStatement | AssignmentStatement | AugmentedAssignmentStatement {
2450
        /**
2451
         * Attempts to find an expression-statement or an increment statement.
2452
         * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
2453
         * statements aren't valid expressions. They _do_ however fall under the same parsing
2454
         * priority as standalone function calls though, so we can parse them in the same way.
2455
         */
2456
        let expr = this.call();
1,408✔
2457
        if (this.check(TokenKind.Equal) && !(isCallExpression(expr))) {
1,348✔
2458
            let left = expr;
346✔
2459
            let operator = this.advance();
346✔
2460
            let right = this.expression();
346✔
2461

2462
            // Create a dotted or indexed "set" based on the left-hand side's type
2463
            if (isIndexedGetExpression(left)) {
346✔
2464
                return new IndexedSetStatement({
33✔
2465
                    obj: left.obj,
2466
                    indexes: left.indexes,
2467
                    value: right,
2468
                    openingSquare: left.tokens.openingSquare,
2469
                    closingSquare: left.tokens.closingSquare,
2470
                    equals: operator
2471
                });
2472
            } else if (isDottedGetExpression(left)) {
313✔
2473
                return new DottedSetStatement({
310✔
2474
                    obj: left.obj,
2475
                    name: left.tokens.name,
2476
                    value: right,
2477
                    dot: left.tokens.dot,
2478
                    equals: operator
2479
                });
2480
            }
2481
        } else if (this.checkAny(...CompoundAssignmentOperators) && !(isCallExpression(expr))) {
1,002✔
2482
            let left = expr;
22✔
2483
            let operator = this.advance();
22✔
2484
            let right = this.expression();
22✔
2485
            return new AugmentedAssignmentStatement({
22✔
2486
                item: left,
2487
                operator: operator,
2488
                value: right
2489
            });
2490
        }
2491
        return this.expressionStatement(expr);
983✔
2492
    }
2493

2494
    private printStatement(): PrintStatement {
2495
        let printKeyword = this.advance();
1,375✔
2496

2497
        let values: Expression[] = [];
1,375✔
2498

2499
        while (!this.checkEndOfStatement()) {
1,375✔
2500
            if (this.checkAny(TokenKind.Semicolon, TokenKind.Comma)) {
1,505✔
2501
                values.push(new PrintSeparatorExpression({ separator: this.advance() as PrintSeparatorToken }));
49✔
2502
            } else if (this.check(TokenKind.Else)) {
1,456✔
2503
                break; // inline branch
22✔
2504
            } else {
2505
                values.push(this.expression());
1,434✔
2506
            }
2507
        }
2508

2509
        //print statements can be empty, so look for empty print conditions
2510
        if (!values.length) {
1,373✔
2511
            const endOfStatementLocation = util.createBoundingLocation(printKeyword, this.peek());
9✔
2512
            let emptyStringLiteral = createStringLiteral('', endOfStatementLocation);
9✔
2513
            values.push(emptyStringLiteral);
9✔
2514
        }
2515

2516
        let last = values[values.length - 1];
1,373✔
2517
        if (isToken(last)) {
1,373!
2518
            // TODO: error, expected value
2519
        }
2520

2521
        return new PrintStatement({ print: printKeyword, expressions: values });
1,373✔
2522
    }
2523

2524
    /**
2525
     * Parses a return statement with an optional return value.
2526
     * @returns an AST representation of a return statement.
2527
     */
2528
    private returnStatement(): ReturnStatement {
2529
        let options = { return: this.previous() };
3,720✔
2530

2531
        if (this.checkEndOfStatement()) {
3,720✔
2532
            return new ReturnStatement(options);
24✔
2533
        }
2534

2535
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
3,696✔
2536
        return new ReturnStatement({ ...options, value: toReturn });
3,695✔
2537
    }
2538

2539
    /**
2540
     * Parses a `label` statement
2541
     * @returns an AST representation of an `label` statement.
2542
     */
2543
    private labelStatement() {
2544
        let options = {
12✔
2545
            name: this.advance(),
2546
            colon: this.advance()
2547
        };
2548

2549
        //label must be alone on its line, this is probably not a label
2550
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
12✔
2551
            //rewind and cancel
2552
            this.current -= 2;
2✔
2553
            throw new CancelStatementError();
2✔
2554
        }
2555

2556
        return new LabelStatement(options);
10✔
2557
    }
2558

2559
    /**
2560
     * Parses a `continue` statement
2561
     */
2562
    private continueStatement() {
2563
        return new ContinueStatement({
12✔
2564
            continue: this.advance(),
2565
            loopType: this.tryConsume(
2566
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2567
                TokenKind.While, TokenKind.For
2568
            )
2569
        });
2570
    }
2571

2572
    /**
2573
     * Parses a `goto` statement
2574
     * @returns an AST representation of an `goto` statement.
2575
     */
2576
    private gotoStatement() {
2577
        let tokens = {
12✔
2578
            goto: this.advance(),
2579
            label: this.consume(
2580
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2581
                TokenKind.Identifier
2582
            )
2583
        };
2584

2585
        return new GotoStatement(tokens);
10✔
2586
    }
2587

2588
    /**
2589
     * Parses an `end` statement
2590
     * @returns an AST representation of an `end` statement.
2591
     */
2592
    private endStatement() {
2593
        let options = { end: this.advance() };
8✔
2594

2595
        return new EndStatement(options);
8✔
2596
    }
2597
    /**
2598
     * Parses a `stop` statement
2599
     * @returns an AST representation of a `stop` statement
2600
     */
2601
    private stopStatement() {
2602
        let options = { stop: this.advance() };
16✔
2603

2604
        return new StopStatement(options);
16✔
2605
    }
2606

2607
    /**
2608
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2609
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2610
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2611
     *                    ignored.
2612
     */
2613
    private block(...terminators: BlockTerminator[]): Block | undefined {
2614
        const parentAnnotations = this.enterAnnotationBlock();
7,462✔
2615

2616
        this.consumeStatementSeparators(true);
7,462✔
2617
        const statements: Statement[] = [];
7,462✔
2618
        const flatGlobalTerminators = this.globalTerminators.flat().flat();
7,462✔
2619
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators, ...flatGlobalTerminators)) {
7,462✔
2620
            //grab the location of the current token
2621
            let loopCurrent = this.current;
8,905✔
2622
            let dec = this.declaration();
8,905✔
2623
            if (dec) {
8,905✔
2624
                if (!isAnnotationExpression(dec)) {
8,845✔
2625
                    this.consumePendingAnnotations(dec);
8,838✔
2626
                    statements.push(dec);
8,838✔
2627
                }
2628

2629
                //ensure statement separator
2630
                this.consumeStatementSeparators();
8,845✔
2631

2632
            } else {
2633
                //something went wrong. reset to the top of the loop
2634
                this.current = loopCurrent;
60✔
2635

2636
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2637
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
60✔
2638

2639
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2640
                //but there's already an error in the file being parsed, so just leave this line here
2641
                this.advance();
60✔
2642

2643
                //consume potential separators
2644
                this.consumeStatementSeparators(true);
60✔
2645
            }
2646
        }
2647

2648
        if (this.isAtEnd()) {
7,462✔
2649
            return undefined;
6✔
2650
            // TODO: Figure out how to handle unterminated blocks well
2651
        } else if (terminators.length > 0) {
7,456✔
2652
            //did we hit end-sub / end-function while looking for some other terminator?
2653
            //if so, we need to restore the statement separator
2654
            let prev = this.previous().kind;
3,244✔
2655
            let peek = this.peek().kind;
3,244✔
2656
            if (
3,244✔
2657
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
6,492!
2658
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2659
            ) {
2660
                this.current--;
10✔
2661
            }
2662
        }
2663

2664
        this.exitAnnotationBlock(parentAnnotations);
7,456✔
2665
        return new Block({ statements: statements });
7,456✔
2666
    }
2667

2668
    /**
2669
     * Attach pending annotations to the provided statement,
2670
     * and then reset the annotations array
2671
     */
2672
    consumePendingAnnotations(statement: Statement) {
2673
        if (this.pendingAnnotations.length) {
17,058✔
2674
            statement.annotations = this.pendingAnnotations;
51✔
2675
            this.pendingAnnotations = [];
51✔
2676
        }
2677
    }
2678

2679
    enterAnnotationBlock() {
2680
        const pending = this.pendingAnnotations;
13,393✔
2681
        this.pendingAnnotations = [];
13,393✔
2682
        return pending;
13,393✔
2683
    }
2684

2685
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2686
        // non consumed annotations are an error
2687
        if (this.pendingAnnotations.length) {
13,385✔
2688
            for (const annotation of this.pendingAnnotations) {
5✔
2689
                this.diagnostics.push({
7✔
2690
                    ...DiagnosticMessages.unusedAnnotation(),
2691
                    location: annotation.location
2692
                });
2693
            }
2694
        }
2695
        this.pendingAnnotations = parentAnnotations;
13,385✔
2696
    }
2697

2698
    private expression(findTypecast = true): Expression {
13,545✔
2699
        let expression = this.anonymousFunction();
13,951✔
2700
        let asToken: Token;
2701
        let typeExpression: TypeExpression;
2702
        if (findTypecast) {
13,912✔
2703
            do {
13,539✔
2704
                if (this.check(TokenKind.As)) {
13,620✔
2705
                    this.warnIfNotBrighterScriptMode('type cast');
83✔
2706
                    // Check if this expression is wrapped in any type casts
2707
                    // allows for multiple casts:
2708
                    // myVal = foo() as dynamic as string
2709
                    [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
83✔
2710
                    if (asToken && typeExpression) {
83✔
2711
                        expression = new TypecastExpression({ obj: expression, as: asToken, typeExpression: typeExpression });
81✔
2712
                    }
2713
                } else {
2714
                    break;
13,537✔
2715
                }
2716

2717
            } while (asToken && typeExpression);
166✔
2718
        }
2719
        return expression;
13,912✔
2720
    }
2721

2722
    private anonymousFunction(): Expression {
2723
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
13,951✔
2724
            const func = this.functionDeclaration(true);
95✔
2725
            //if there's an open paren after this, this is an IIFE
2726
            if (this.check(TokenKind.LeftParen)) {
94✔
2727
                return this.finishCall(this.advance(), func);
3✔
2728
            } else {
2729
                return func;
91✔
2730
            }
2731
        }
2732

2733
        let expr = this.boolean();
13,856✔
2734

2735
        if (this.check(TokenKind.Question)) {
13,818✔
2736
            return this.ternaryExpression(expr);
98✔
2737
        } else if (this.check(TokenKind.QuestionQuestion)) {
13,720✔
2738
            return this.nullCoalescingExpression(expr);
35✔
2739
        } else {
2740
            return expr;
13,685✔
2741
        }
2742
    }
2743

2744
    private boolean(): Expression {
2745
        let expr = this.relational();
13,856✔
2746

2747
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
13,818✔
2748
            let operator = this.previous();
34✔
2749
            let right = this.relational();
34✔
2750
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
34✔
2751
        }
2752

2753
        return expr;
13,818✔
2754
    }
2755

2756
    private relational(): Expression {
2757
        let expr = this.additive();
13,920✔
2758

2759
        while (
13,882✔
2760
            this.matchAny(
2761
                TokenKind.Equal,
2762
                TokenKind.LessGreater,
2763
                TokenKind.Greater,
2764
                TokenKind.GreaterEqual,
2765
                TokenKind.Less,
2766
                TokenKind.LessEqual
2767
            )
2768
        ) {
2769
            let operator = this.previous();
1,943✔
2770
            let right = this.additive();
1,943✔
2771
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,943✔
2772
        }
2773

2774
        return expr;
13,882✔
2775
    }
2776

2777
    // TODO: bitshift
2778

2779
    private additive(): Expression {
2780
        let expr = this.multiplicative();
15,863✔
2781

2782
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
15,825✔
2783
            let operator = this.previous();
1,554✔
2784
            let right = this.multiplicative();
1,554✔
2785
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,554✔
2786
        }
2787

2788
        return expr;
15,825✔
2789
    }
2790

2791
    private multiplicative(): Expression {
2792
        let expr = this.exponential();
17,417✔
2793

2794
        while (this.matchAny(
17,379✔
2795
            TokenKind.Forwardslash,
2796
            TokenKind.Backslash,
2797
            TokenKind.Star,
2798
            TokenKind.Mod,
2799
            TokenKind.LeftShift,
2800
            TokenKind.RightShift
2801
        )) {
2802
            let operator = this.previous();
60✔
2803
            let right = this.exponential();
60✔
2804
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
60✔
2805
        }
2806

2807
        return expr;
17,379✔
2808
    }
2809

2810
    private exponential(): Expression {
2811
        let expr = this.prefixUnary();
17,477✔
2812

2813
        while (this.match(TokenKind.Caret)) {
17,439✔
2814
            let operator = this.previous();
9✔
2815
            let right = this.prefixUnary();
9✔
2816
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
9✔
2817
        }
2818

2819
        return expr;
17,439✔
2820
    }
2821

2822
    private prefixUnary(): Expression {
2823
        const nextKind = this.peek().kind;
17,522✔
2824
        if (nextKind === TokenKind.Not) {
17,522✔
2825
            this.current++; //advance
30✔
2826
            let operator = this.previous();
30✔
2827
            let right = this.relational();
30✔
2828
            return new UnaryExpression({ operator: operator, right: right });
30✔
2829
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
17,492✔
2830
            this.current++; //advance
36✔
2831
            let operator = this.previous();
36✔
2832
            let right = (nextKind as any) === TokenKind.Not
36✔
2833
                ? this.boolean()
36!
2834
                : this.prefixUnary();
2835
            return new UnaryExpression({ operator: operator, right: right });
36✔
2836
        }
2837
        return this.call();
17,456✔
2838
    }
2839

2840
    private indexedGet(expr: Expression) {
2841
        let openingSquare = this.previous();
161✔
2842
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
161✔
2843
        let indexes: Expression[] = [];
161✔
2844

2845

2846
        //consume leading newlines
2847
        while (this.match(TokenKind.Newline)) { }
161✔
2848

2849
        try {
161✔
2850
            indexes.push(
161✔
2851
                this.expression()
2852
            );
2853
            //consume additional indexes separated by commas
2854
            while (this.check(TokenKind.Comma)) {
159✔
2855
                //discard the comma
2856
                this.advance();
17✔
2857
                indexes.push(
17✔
2858
                    this.expression()
2859
                );
2860
            }
2861
        } catch (error) {
2862
            this.rethrowNonDiagnosticError(error);
2✔
2863
        }
2864
        //consume trailing newlines
2865
        while (this.match(TokenKind.Newline)) { }
161✔
2866

2867
        const closingSquare = this.tryConsume(
161✔
2868
            DiagnosticMessages.unmatchedLeftToken(openingSquare.text, 'array or object index'),
2869
            TokenKind.RightSquareBracket
2870
        );
2871

2872
        return new IndexedGetExpression({
161✔
2873
            obj: expr,
2874
            indexes: indexes,
2875
            openingSquare: openingSquare,
2876
            closingSquare: closingSquare,
2877
            questionDot: questionDotToken
2878
        });
2879
    }
2880

2881
    private newExpression() {
2882
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
141✔
2883
        let newToken = this.advance();
141✔
2884

2885
        let nameExpr = this.identifyingExpression();
141✔
2886
        let leftParen = this.tryConsume(
141✔
2887
            DiagnosticMessages.unexpectedToken(this.peek().text),
2888
            TokenKind.LeftParen,
2889
            TokenKind.QuestionLeftParen
2890
        );
2891

2892
        if (!leftParen) {
141✔
2893
            // new expression without a following call expression
2894
            // wrap the name in an expression
2895
            const endOfStatementLocation = util.createBoundingLocation(newToken, this.peek());
4✔
2896
            const exprStmt = nameExpr ?? createStringLiteral('', endOfStatementLocation);
4!
2897
            return new ExpressionStatement({ expression: exprStmt });
4✔
2898
        }
2899

2900
        let call = this.finishCall(leftParen, nameExpr);
137✔
2901
        //pop the call from the  callExpressions list because this is technically something else
2902
        this.callExpressions.pop();
137✔
2903
        let result = new NewExpression({ new: newToken, call: call });
137✔
2904
        return result;
137✔
2905
    }
2906

2907
    /**
2908
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2909
     */
2910
    private callfunc(callee: Expression): Expression {
2911
        this.warnIfNotBrighterScriptMode('callfunc operator');
74✔
2912
        let operator = this.previous();
74✔
2913
        let methodName = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
74✔
2914
        let openParen: Token;
2915
        let call: CallExpression;
2916
        if (methodName) {
74✔
2917
            // force it into an identifier so the AST makes some sense
2918
            methodName.kind = TokenKind.Identifier;
66✔
2919
            openParen = this.tryConsume(DiagnosticMessages.expectedToken(TokenKind.LeftParen), TokenKind.LeftParen);
66✔
2920
            if (openParen) {
66!
2921
                call = this.finishCall(openParen, callee, false);
66✔
2922
            }
2923
        }
2924
        return new CallfuncExpression({
74✔
2925
            callee: callee,
2926
            operator: operator,
2927
            methodName: methodName as Identifier,
2928
            openingParen: openParen,
2929
            args: call?.args,
222✔
2930
            closingParen: call?.tokens?.closingParen
444✔
2931
        });
2932
    }
2933

2934
    private call(): Expression {
2935
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
18,864✔
2936
            return this.newExpression();
141✔
2937
        }
2938
        let expr = this.primary();
18,723✔
2939

2940
        while (true) {
18,625✔
2941
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
24,009✔
2942
                expr = this.finishCall(this.previous(), expr);
2,514✔
2943
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
21,495✔
2944
                expr = this.indexedGet(expr);
159✔
2945
            } else if (this.match(TokenKind.Callfunc)) {
21,336✔
2946
                expr = this.callfunc(expr);
74✔
2947
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
21,262✔
2948
                if (this.match(TokenKind.LeftSquareBracket)) {
2,681✔
2949
                    expr = this.indexedGet(expr);
2✔
2950
                } else {
2951
                    let dot = this.previous();
2,679✔
2952
                    let name = this.tryConsume(
2,679✔
2953
                        DiagnosticMessages.expectedIdentifier(),
2954
                        TokenKind.Identifier,
2955
                        ...AllowedProperties
2956
                    );
2957
                    if (!name) {
2,679✔
2958
                        break;
44✔
2959
                    }
2960

2961
                    // force it into an identifier so the AST makes some sense
2962
                    name.kind = TokenKind.Identifier;
2,635✔
2963
                    expr = new DottedGetExpression({ obj: expr, name: name as Identifier, dot: dot });
2,635✔
2964
                }
2965

2966
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
18,581✔
2967
                let dot = this.advance();
11✔
2968
                let name = this.tryConsume(
11✔
2969
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2970
                    TokenKind.Identifier,
2971
                    ...AllowedProperties
2972
                );
2973

2974
                // force it into an identifier so the AST makes some sense
2975
                name.kind = TokenKind.Identifier;
11✔
2976
                if (!name) {
11!
2977
                    break;
×
2978
                }
2979
                expr = new XmlAttributeGetExpression({ obj: expr, name: name as Identifier, at: dot });
11✔
2980
                //only allow a single `@` expression
2981
                break;
11✔
2982

2983
            } else {
2984
                break;
18,570✔
2985
            }
2986
        }
2987

2988
        return expr;
18,625✔
2989
    }
2990

2991
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
2,654✔
2992
        let args = [] as Expression[];
2,750✔
2993
        while (this.match(TokenKind.Newline)) { }
2,750✔
2994

2995
        if (!this.check(TokenKind.RightParen)) {
2,750✔
2996
            do {
1,397✔
2997
                while (this.match(TokenKind.Newline)) { }
1,996✔
2998

2999
                if (args.length >= CallExpression.MaximumArguments) {
1,996!
3000
                    this.diagnostics.push({
×
3001
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
3002
                        location: this.peek()?.location
×
3003
                    });
3004
                    throw this.lastDiagnosticAsError();
×
3005
                }
3006
                try {
1,996✔
3007
                    args.push(this.expression());
1,996✔
3008
                } catch (error) {
3009
                    this.rethrowNonDiagnosticError(error);
6✔
3010
                    // we were unable to get an expression, so don't continue
3011
                    break;
6✔
3012
                }
3013
            } while (this.match(TokenKind.Comma));
3014
        }
3015

3016
        while (this.match(TokenKind.Newline)) { }
2,750✔
3017

3018
        const closingParen = this.tryConsume(
2,750✔
3019
            DiagnosticMessages.unmatchedLeftToken(openingParen.text, 'function call arguments'),
3020
            TokenKind.RightParen
3021
        );
3022

3023
        let expression = new CallExpression({
2,750✔
3024
            callee: callee,
3025
            openingParen: openingParen,
3026
            args: args,
3027
            closingParen: closingParen
3028
        });
3029
        if (addToCallExpressionList) {
2,750✔
3030
            this.callExpressions.push(expression);
2,654✔
3031
        }
3032
        return expression;
2,750✔
3033
    }
3034

3035
    /**
3036
     * Creates a TypeExpression, which wraps standard ASTNodes that represent a BscType
3037
     */
3038
    private typeExpression(): TypeExpression {
3039
        const changedTokens: { token: Token; oldKind: TokenKind }[] = [];
1,943✔
3040
        try {
1,943✔
3041
            let expr: Expression = this.getTypeExpressionPart(changedTokens);
1,943✔
3042
            while (this.options.mode === ParseMode.BrighterScript && this.matchAny(TokenKind.Or, TokenKind.And)) {
1,942✔
3043
                // If we're in Brighterscript mode, allow union types with "or" between types
3044
                // TODO: Handle Union types in parens? eg. "(string or integer)"
3045
                let operator = this.previous();
81✔
3046
                let right = this.getTypeExpressionPart(changedTokens);
81✔
3047
                if (right) {
81!
3048
                    expr = new BinaryExpression({ left: expr, operator: operator, right: right });
81✔
3049
                } else {
3050
                    break;
×
3051
                }
3052
            }
3053
            if (expr) {
1,942✔
3054
                return new TypeExpression({ expression: expr });
1,925✔
3055
            }
3056

3057
        } catch (error) {
3058
            // Something went wrong - reset the kind to what it was previously
3059
            for (const changedToken of changedTokens) {
1✔
3060
                changedToken.token.kind = changedToken.oldKind;
×
3061
            }
3062
            throw error;
1✔
3063
        }
3064
    }
3065

3066
    /**
3067
     * Gets a single "part" of a type of a potential Union type
3068
     * Note: this does not NEED to be part of a union type, but the logic is the same
3069
     *
3070
     * @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
3071
     * @returns an expression that was successfully parsed
3072
     */
3073
    private getTypeExpressionPart(changedTokens: { token: Token; oldKind: TokenKind }[]) {
3074
        let expr: VariableExpression | DottedGetExpression | TypedArrayExpression | InlineInterfaceExpression | GroupingExpression;
3075

3076
        if (this.checkAny(...DeclarableTypes)) {
2,024✔
3077
            // if this is just a type, just use directly
3078
            expr = new VariableExpression({ name: this.advance() as Identifier });
1,334✔
3079
        } else {
3080
            if (this.options.mode === ParseMode.BrightScript && !declarableTypesLower.includes(this.peek()?.text?.toLowerCase())) {
690!
3081
                // custom types arrays not allowed in Brightscript
3082
                this.warnIfNotBrighterScriptMode('custom types');
17✔
3083
                return expr;
17✔
3084
            }
3085

3086
            if (this.match(TokenKind.LeftCurlyBrace)) {
673✔
3087
                expr = this.inlineInterface();
35✔
3088
            } else if (this.match(TokenKind.LeftParen)) {
638✔
3089
                let left = this.previous();
14✔
3090
                let typeExpr = this.typeExpression();
14✔
3091
                let right = this.consume(
14✔
3092
                    DiagnosticMessages.unmatchedLeftToken(left.text, 'type expression'),
3093
                    TokenKind.RightParen
3094
                );
3095
                expr = new GroupingExpression({ leftParen: left, rightParen: right, expression: typeExpr });
14✔
3096
            } else {
3097
                if (this.checkAny(...AllowedTypeIdentifiers)) {
624✔
3098
                    // Since the next token is allowed as a type identifier, change the kind
3099
                    let nextToken = this.peek();
1✔
3100
                    changedTokens.push({ token: nextToken, oldKind: nextToken.kind });
1✔
3101
                    nextToken.kind = TokenKind.Identifier;
1✔
3102
                }
3103
                expr = this.identifyingExpression(AllowedTypeIdentifiers);
624✔
3104
            }
3105
        }
3106

3107
        //Check if it has square brackets, thus making it an array
3108
        if (expr && this.check(TokenKind.LeftSquareBracket)) {
2,006✔
3109
            if (this.options.mode === ParseMode.BrightScript) {
36✔
3110
                // typed arrays not allowed in Brightscript
3111
                this.warnIfNotBrighterScriptMode('typed arrays');
1✔
3112
                return expr;
1✔
3113
            }
3114

3115
            // Check if it is an array - that is, if it has `[]` after the type
3116
            // eg. `string[]` or `SomeKlass[]`
3117
            // This is while loop, so it supports multidimensional arrays (eg. integer[][])
3118
            while (this.check(TokenKind.LeftSquareBracket)) {
35✔
3119
                const leftBracket = this.advance();
37✔
3120
                if (this.check(TokenKind.RightSquareBracket)) {
37!
3121
                    const rightBracket = this.advance();
37✔
3122
                    expr = new TypedArrayExpression({ innerType: expr, leftBracket: leftBracket, rightBracket: rightBracket });
37✔
3123
                }
3124
            }
3125
        }
3126

3127
        return expr;
2,005✔
3128
    }
3129

3130

3131
    private inlineInterface() {
3132
        let expr: InlineInterfaceExpression;
3133
        const openToken = this.previous();
35✔
3134
        const members: InlineInterfaceMemberExpression[] = [];
35✔
3135
        while (this.match(TokenKind.Newline)) { }
35✔
3136
        while (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral, TokenKind.Optional)) {
35✔
3137
            const member = this.inlineInterfaceMember();
39✔
3138
            members.push(member);
39✔
3139
            while (this.matchAny(TokenKind.Comma, TokenKind.Newline)) { }
39✔
3140
        }
3141
        if (!this.check(TokenKind.RightCurlyBrace)) {
35!
UNCOV
3142
            this.diagnostics.push({
×
3143
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
3144
                location: this.peek().location
3145
            });
UNCOV
3146
            throw this.lastDiagnosticAsError();
×
3147
        }
3148
        const closeToken = this.advance();
35✔
3149

3150
        expr = new InlineInterfaceExpression({ open: openToken, members: members, close: closeToken });
35✔
3151
        return expr;
35✔
3152
    }
3153

3154
    private inlineInterfaceMember(): InlineInterfaceMemberExpression {
3155
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
39✔
3156

3157
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral)) {
39!
3158
            if (this.check(TokenKind.As)) {
39!
UNCOV
3159
                if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
×
3160
                    // as <EOL>
3161
                    // `as` is the field name
UNCOV
3162
                } else if (this.checkNext(TokenKind.As)) {
×
3163
                    //  as as ____
3164
                    // first `as` is the field name
UNCOV
3165
                } else if (optionalKeyword) {
×
3166
                    // optional as ____
3167
                    // optional is the field name, `as` starts type
3168
                    // rewind current token
UNCOV
3169
                    optionalKeyword = null;
×
3170
                    this.current--;
×
3171
                }
3172
            }
3173
        } else {
3174
            // no name after `optional` ... optional is the name
3175
            // rewind current token
UNCOV
3176
            optionalKeyword = null;
×
3177
            this.current--;
×
3178
        }
3179

3180
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers, TokenKind.StringLiteral)) {
39!
UNCOV
3181
            this.diagnostics.push({
×
3182
                ...DiagnosticMessages.expectedIdentifier(this.peek().text),
3183
                location: this.peek().location
3184
            });
3185
            throw this.lastDiagnosticAsError();
×
3186
        }
3187
        let name: Token;
3188
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
39✔
3189
            name = this.identifier(...AllowedProperties);
37✔
3190
        } else {
3191
            name = this.advance();
2✔
3192
        }
3193

3194
        let typeExpression: TypeExpression;
3195

3196
        let asToken: Token = null;
39✔
3197
        if (this.check(TokenKind.As)) {
39✔
3198
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
37✔
3199

3200
        }
3201
        return new InlineInterfaceMemberExpression({
39✔
3202
            name: name,
3203
            as: asToken,
3204
            typeExpression: typeExpression,
3205
            optional: optionalKeyword
3206
        });
3207
    }
3208

3209
    private primary(): Expression {
3210
        switch (true) {
18,723✔
3211
            case this.matchAny(
18,723!
3212
                TokenKind.False,
3213
                TokenKind.True,
3214
                TokenKind.Invalid,
3215
                TokenKind.IntegerLiteral,
3216
                TokenKind.LongIntegerLiteral,
3217
                TokenKind.FloatLiteral,
3218
                TokenKind.DoubleLiteral,
3219
                TokenKind.StringLiteral
3220
            ):
3221
                return new LiteralExpression({ value: this.previous() });
8,191✔
3222

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

3227
            //template string
3228
            case this.check(TokenKind.BackTick):
3229
                return this.templateString(false);
47✔
3230

3231
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
3232
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
20,211✔
3233
                return this.templateString(true);
8✔
3234

3235
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3236
                return new VariableExpression({ name: this.previous() as Identifier });
9,760✔
3237

3238
            case this.match(TokenKind.LeftParen):
3239
                let left = this.previous();
60✔
3240
                let expr = this.expression();
60✔
3241
                let right = this.consume(
59✔
3242
                    DiagnosticMessages.unmatchedLeftToken(left.text, 'expression'),
3243
                    TokenKind.RightParen
3244
                );
3245
                return new GroupingExpression({ leftParen: left, rightParen: right, expression: expr });
59✔
3246

3247
            case this.matchAny(TokenKind.LeftSquareBracket):
3248
                return this.arrayLiteral();
169✔
3249

3250
            case this.match(TokenKind.LeftCurlyBrace):
3251
                return this.aaLiteral();
313✔
3252

3253
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
UNCOV
3254
                let token = Object.assign(this.previous(), {
×
3255
                    kind: TokenKind.Identifier
3256
                }) as Identifier;
UNCOV
3257
                return new VariableExpression({ name: token });
×
3258

3259
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
UNCOV
3260
                return this.anonymousFunction();
×
3261

3262
            case this.check(TokenKind.RegexLiteral):
3263
                return this.regexLiteralExpression();
45✔
3264

3265
            default:
3266
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
3267
                if (this.checkAny(...this.peekGlobalTerminators())) {
95!
3268
                    //don't throw a diagnostic, just return undefined
3269

3270
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
3271
                } else {
3272
                    this.diagnostics.push({
95✔
3273
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
3274
                        location: this.peek()?.location
285!
3275
                    });
3276
                    throw this.lastDiagnosticAsError();
95✔
3277
                }
3278
        }
3279
    }
3280

3281
    private arrayLiteral() {
3282
        let elements: Array<Expression> = [];
169✔
3283
        let openingSquare = this.previous();
169✔
3284

3285
        while (this.match(TokenKind.Newline)) {
169✔
3286
        }
3287
        let closingSquare: Token;
3288

3289
        if (!this.match(TokenKind.RightSquareBracket)) {
169✔
3290
            try {
128✔
3291
                elements.push(this.expression());
128✔
3292

3293
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
127✔
3294

3295
                    while (this.match(TokenKind.Newline)) {
187✔
3296

3297
                    }
3298

3299
                    if (this.check(TokenKind.RightSquareBracket)) {
187✔
3300
                        break;
36✔
3301
                    }
3302

3303
                    elements.push(this.expression());
151✔
3304
                }
3305
            } catch (error: any) {
3306
                this.rethrowNonDiagnosticError(error);
2✔
3307
            }
3308

3309
            closingSquare = this.tryConsume(
128✔
3310
                DiagnosticMessages.unmatchedLeftToken(openingSquare.text, 'array literal'),
3311
                TokenKind.RightSquareBracket
3312
            );
3313
        } else {
3314
            closingSquare = this.previous();
41✔
3315
        }
3316

3317
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3318
        return new ArrayLiteralExpression({ elements: elements, open: openingSquare, close: closingSquare });
169✔
3319
    }
3320

3321
    private aaLiteral() {
3322
        let openingBrace = this.previous();
313✔
3323
        let members: Array<AAMemberExpression> = [];
313✔
3324

3325
        let key = () => {
313✔
3326
            let result = {
331✔
3327
                colonToken: null as Token,
3328
                keyToken: null as Token,
3329
                range: null as Range
3330
            };
3331
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
331✔
3332
                result.keyToken = this.identifier(...AllowedProperties);
297✔
3333
            } else if (this.check(TokenKind.StringLiteral)) {
34!
3334
                result.keyToken = this.advance();
34✔
3335
            } else {
UNCOV
3336
                this.diagnostics.push({
×
3337
                    ...DiagnosticMessages.unexpectedAAKey(),
3338
                    location: this.peek().location
3339
                });
UNCOV
3340
                throw this.lastDiagnosticAsError();
×
3341
            }
3342

3343
            result.colonToken = this.consume(
331✔
3344
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3345
                TokenKind.Colon
3346
            );
3347
            result.range = util.createBoundingRange(result.keyToken, result.colonToken);
329✔
3348
            return result;
329✔
3349
        };
3350

3351
        while (this.match(TokenKind.Newline)) { }
313✔
3352
        let closingBrace: Token;
3353
        if (!this.match(TokenKind.RightCurlyBrace)) {
313✔
3354
            let lastAAMember: AAMemberExpression;
3355
            try {
228✔
3356
                let k = key();
228✔
3357
                let expr = this.expression();
227✔
3358
                lastAAMember = new AAMemberExpression({
226✔
3359
                    key: k.keyToken,
3360
                    colon: k.colonToken,
3361
                    value: expr
3362
                });
3363
                members.push(lastAAMember);
226✔
3364

3365
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
226✔
3366
                    // collect comma at end of expression
3367
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
240✔
3368
                        (lastAAMember as DeepWriteable<AAMemberExpression>).tokens.comma = this.previous();
77✔
3369
                    }
3370

3371
                    this.consumeStatementSeparators(true);
240✔
3372

3373
                    if (this.check(TokenKind.RightCurlyBrace)) {
240✔
3374
                        break;
137✔
3375
                    }
3376
                    let k = key();
103✔
3377
                    let expr = this.expression();
102✔
3378
                    lastAAMember = new AAMemberExpression({
102✔
3379
                        key: k.keyToken,
3380
                        colon: k.colonToken,
3381
                        value: expr
3382
                    });
3383
                    members.push(lastAAMember);
102✔
3384

3385
                }
3386
            } catch (error: any) {
3387
                this.rethrowNonDiagnosticError(error);
3✔
3388
            }
3389

3390
            closingBrace = this.tryConsume(
228✔
3391
                DiagnosticMessages.unmatchedLeftToken(openingBrace.text, 'associative array literal'),
3392
                TokenKind.RightCurlyBrace
3393
            );
3394
        } else {
3395
            closingBrace = this.previous();
85✔
3396
        }
3397

3398
        const aaExpr = new AALiteralExpression({ elements: members, open: openingBrace, close: closingBrace });
313✔
3399
        return aaExpr;
313✔
3400
    }
3401

3402
    /**
3403
     * Pop token if we encounter specified token
3404
     */
3405
    private match(tokenKind: TokenKind) {
3406
        if (this.check(tokenKind)) {
71,431✔
3407
            this.current++; //advance
7,083✔
3408
            return true;
7,083✔
3409
        }
3410
        return false;
64,348✔
3411
    }
3412

3413
    /**
3414
     * Pop token if we encounter a token in the specified list
3415
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3416
     */
3417
    private matchAny(...tokenKinds: TokenKind[]) {
3418
        for (let tokenKind of tokenKinds) {
246,059✔
3419
            if (this.check(tokenKind)) {
711,740✔
3420
                this.current++; //advance
64,925✔
3421
                return true;
64,925✔
3422
            }
3423
        }
3424
        return false;
181,134✔
3425
    }
3426

3427
    /**
3428
     * If the next series of tokens matches the given set of tokens, pop them all
3429
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
3430
     */
3431
    private matchSequence(...tokenKinds: TokenKind[]) {
3432
        const endIndex = this.current + tokenKinds.length;
21,339✔
3433
        for (let i = 0; i < tokenKinds.length; i++) {
21,339✔
3434
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
21,363!
3435
                return false;
21,336✔
3436
            }
3437
        }
3438
        this.current = endIndex;
3✔
3439
        return true;
3✔
3440
    }
3441

3442
    /**
3443
     * Get next token matching a specified list, or fail with an error
3444
     */
3445
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
3446
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
17,310✔
3447
        if (token) {
17,310✔
3448
            return token;
17,293✔
3449
        } else {
3450
            let error = new Error(diagnosticInfo.message);
17✔
3451
            (error as any).isDiagnostic = true;
17✔
3452
            throw error;
17✔
3453
        }
3454
    }
3455

3456
    /**
3457
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3458
     */
3459
    private consumeTokenIf(tokenKind: TokenKind) {
3460
        if (this.match(tokenKind)) {
4,092✔
3461
            return this.previous();
415✔
3462
        }
3463
    }
3464

3465
    private consumeToken(tokenKind: TokenKind) {
3466
        return this.consume(
2,427✔
3467
            DiagnosticMessages.expectedToken(tokenKind),
3468
            tokenKind
3469
        );
3470
    }
3471

3472
    /**
3473
     * Consume, or add a message if not found. But then continue and return undefined
3474
     */
3475
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3476
        const nextKind = this.peek().kind;
26,064✔
3477
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
53,650✔
3478

3479
        if (foundTokenKind) {
26,064✔
3480
            return this.advance();
25,939✔
3481
        }
3482
        this.diagnostics.push({
125✔
3483
            ...diagnostic,
3484
            location: this.peek()?.location
375!
3485
        });
3486
    }
3487

3488
    private tryConsumeToken(tokenKind: TokenKind) {
3489
        return this.tryConsume(
98✔
3490
            DiagnosticMessages.expectedToken(tokenKind),
3491
            tokenKind
3492
        );
3493
    }
3494

3495
    private consumeStatementSeparators(optional = false) {
11,591✔
3496
        //a comment or EOF mark the end of the statement
3497
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
35,271✔
3498
            return true;
754✔
3499
        }
3500
        let consumed = false;
34,517✔
3501
        //consume any newlines and colons
3502
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
34,517✔
3503
            consumed = true;
37,312✔
3504
        }
3505
        if (!optional && !consumed) {
34,517✔
3506
            this.diagnostics.push({
81✔
3507
                ...DiagnosticMessages.expectedNewlineOrColon(),
3508
                location: this.peek()?.location
243!
3509
            });
3510
        }
3511
        return consumed;
34,517✔
3512
    }
3513

3514
    private advance(): Token {
3515
        if (!this.isAtEnd()) {
59,679✔
3516
            this.current++;
59,665✔
3517
        }
3518
        return this.previous();
59,679✔
3519
    }
3520

3521
    private checkEndOfStatement(): boolean {
3522
        const nextKind = this.peek().kind;
8,493✔
3523
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
8,493✔
3524
    }
3525

3526
    private checkPrevious(tokenKind: TokenKind): boolean {
3527
        return this.previous()?.kind === tokenKind;
253!
3528
    }
3529

3530
    /**
3531
     * Check that the next token kind is the expected kind
3532
     * @param tokenKind the expected next kind
3533
     * @returns true if the next tokenKind is the expected value
3534
     */
3535
    private check(tokenKind: TokenKind): boolean {
3536
        const nextKind = this.peek().kind;
1,146,064✔
3537
        if (nextKind === TokenKind.Eof) {
1,146,064✔
3538
            return false;
13,410✔
3539
        }
3540
        return nextKind === tokenKind;
1,132,654✔
3541
    }
3542

3543
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3544
        const nextKind = this.peek().kind;
191,027✔
3545
        if (nextKind === TokenKind.Eof) {
191,027✔
3546
            return false;
1,412✔
3547
        }
3548
        return tokenKinds.includes(nextKind);
189,615✔
3549
    }
3550

3551
    private checkNext(tokenKind: TokenKind): boolean {
3552
        if (this.isAtEnd()) {
16,171!
UNCOV
3553
            return false;
×
3554
        }
3555
        return this.peekNext().kind === tokenKind;
16,171✔
3556
    }
3557

3558
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3559
        if (this.isAtEnd()) {
7,067!
UNCOV
3560
            return false;
×
3561
        }
3562
        const nextKind = this.peekNext().kind;
7,067✔
3563
        return tokenKinds.includes(nextKind);
7,067✔
3564
    }
3565

3566
    private isAtEnd(): boolean {
3567
        const peekToken = this.peek();
177,702✔
3568
        return !peekToken || peekToken.kind === TokenKind.Eof;
177,702✔
3569
    }
3570

3571
    private peekNext(): Token {
3572
        if (this.isAtEnd()) {
23,238!
UNCOV
3573
            return this.peek();
×
3574
        }
3575
        return this.tokens[this.current + 1];
23,238✔
3576
    }
3577

3578
    private peek(): Token {
3579
        return this.tokens[this.current];
1,575,047✔
3580
    }
3581

3582
    private previous(): Token {
3583
        return this.tokens[this.current - 1];
102,420✔
3584
    }
3585

3586
    /**
3587
     * Sometimes we catch an error that is a diagnostic.
3588
     * If that's the case, we want to continue parsing.
3589
     * Otherwise, re-throw the error
3590
     *
3591
     * @param error error caught in a try/catch
3592
     */
3593
    private rethrowNonDiagnosticError(error) {
3594
        if (!error.isDiagnostic) {
13!
UNCOV
3595
            throw error;
×
3596
        }
3597
    }
3598

3599
    /**
3600
     * Get the token that is {offset} indexes away from {this.current}
3601
     * @param offset the number of index steps away from current index to fetch
3602
     * @param tokenKinds the desired token must match one of these
3603
     * @example
3604
     * getToken(-1); //returns the previous token.
3605
     * getToken(0);  //returns current token.
3606
     * getToken(1);  //returns next token
3607
     */
3608
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3609
        const token = this.tokens[this.current + offset];
161✔
3610
        if (tokenKinds.includes(token.kind)) {
161✔
3611
            return token;
3✔
3612
        }
3613
    }
3614

3615
    private synchronize() {
3616
        this.advance(); // skip the erroneous token
102✔
3617

3618
        while (!this.isAtEnd()) {
102✔
3619
            if (this.ensureNewLineOrColon(true)) {
207✔
3620
                // end of statement reached
3621
                return;
71✔
3622
            }
3623

3624
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
136✔
3625
                case TokenKind.Namespace:
2!
3626
                case TokenKind.Class:
3627
                case TokenKind.Function:
3628
                case TokenKind.Sub:
3629
                case TokenKind.If:
3630
                case TokenKind.For:
3631
                case TokenKind.ForEach:
3632
                case TokenKind.While:
3633
                case TokenKind.Print:
3634
                case TokenKind.Return:
3635
                    // start parsing again from the next block starter or obvious
3636
                    // expression start
3637
                    return;
1✔
3638
            }
3639

3640
            this.advance();
135✔
3641
        }
3642
    }
3643

3644

3645
    public dispose() {
3646
    }
3647
}
3648

3649
export enum ParseMode {
1✔
3650
    BrightScript = 'BrightScript',
1✔
3651
    BrighterScript = 'BrighterScript'
1✔
3652
}
3653

3654
export interface ParseOptions {
3655
    /**
3656
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3657
     */
3658
    mode?: ParseMode;
3659
    /**
3660
     * A logger that should be used for logging. If omitted, a default logger is used
3661
     */
3662
    logger?: Logger;
3663
    /**
3664
     * Path to the file where this source code originated
3665
     */
3666
    srcPath?: string;
3667
    /**
3668
     * Should locations be tracked. If false, the `range` property will be omitted
3669
     * @default true
3670
     */
3671
    trackLocations?: boolean;
3672
    /**
3673
     *
3674
     */
3675
    bsConsts?: Map<string, boolean>;
3676
}
3677

3678

3679
class CancelStatementError extends Error {
3680
    constructor() {
3681
        super('CancelStatement');
2✔
3682
    }
3683
}
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

© 2025 Coveralls, Inc