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

rokucommunity / brighterscript / #15056

05 Jan 2026 01:09PM UTC coverage: 87.055% (+0.008%) from 87.047%
#15056

push

web-flow
Merge 8f72d5597 into 2ea4d2108

14501 of 17597 branches covered (82.41%)

Branch coverage included in aggregate %.

195 of 263 new or added lines in 12 files covered. (74.14%)

138 existing lines in 8 files now uncovered.

15250 of 16578 relevant lines covered (91.99%)

24160.24 hits per line

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

91.23
/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,134✔
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,134✔
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,129✔
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,134✔
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,436✔
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,104✔
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,111✔
191
        options = this.sanitizeParseOptions(options);
4,111✔
192
        this.options = options;
4,111✔
193

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

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

221
    private logger: Logger;
222

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

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

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

259
    private sanitizeParseOptions(options: ParseOptions) {
260
        options ??= {
4,111✔
261
            srcPath: undefined
262
        };
263
        options.mode ??= ParseMode.BrightScript;
4,111✔
264
        options.trackLocations ??= true;
4,111✔
265
        return options;
4,111✔
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,554✔
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,845✔
280
            let diagnostic = {
170✔
281
                ...DiagnosticMessages.bsFeatureNotSupportedInBrsFiles(featureName),
282
                location: this.peek().location
283
            };
284
            this.diagnostics.push(diagnostic);
170✔
285
        }
286
    }
287

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

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

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

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

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

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

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

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

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

363
    private identifier(...additionalTokenKinds: TokenKind[]) {
364
        const identifier = this.consume(
1,041✔
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,041✔
371
        return identifier;
1,041✔
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);
242✔
396
        let asToken;
397
        let typeExpression;
398
        if (this.check(TokenKind.As)) {
242✔
399
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
223✔
400
        }
401
        return new InterfaceFieldStatement({ name: name, as: asToken, typeExpression: typeExpression, optional: optionalKeyword });
242✔
402
    }
403

404
    private consumeAsTokenAndTypeExpression(ignoreDiagnostics = false): [Token, TypeExpression] {
1,837✔
405
        let asToken = this.consumeToken(TokenKind.As);
1,854✔
406
        let typeExpression: TypeExpression;
407
        if (asToken) {
1,854!
408
            //if there's nothing after the `as`, add a diagnostic and continue
409
            if (this.checkEndOfStatement()) {
1,854✔
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,852✔
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,843✔
427
            }
428
        }
429
        if (this.checkAny(TokenKind.And, TokenKind.Or)) {
1,853✔
430
            this.warnIfNotBrighterScriptMode('custom types');
2✔
431
        }
432
        return [asToken, typeExpression];
1,853✔
433
    }
434

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

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

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

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

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

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

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

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

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

512
                let decl: Statement;
513

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

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

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

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

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

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

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

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

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

575
        this.consumeStatementSeparators();
187✔
576

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

713
                }
714

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

909

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1028
        return result;
1,699✔
1029
    }
1030

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

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

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

1046
        return result;
75✔
1047
    }
1048

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1247
        this.consumeStatementSeparators();
32✔
1248

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

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

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

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

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

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

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

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

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

1314
        //TODO: newline allowed?
1315

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

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

1328
        this.consumeStatementSeparators();
40✔
1329

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

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

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

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

1374
        let target = this.expression();
42✔
1375
        if (!target) {
42!
1376
            this.diagnostics.push({
×
1377
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1378
                location: this.peek().location
1379
            });
1380
            throw this.lastDiagnosticAsError();
×
1381
        }
1382

1383
        this.consumeStatementSeparators();
42✔
1384

1385
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
42✔
1386
        let endForToken: Token;
1387
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
42✔
1388

1389
            this.diagnostics.push({
1✔
1390
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(forEach.text),
1391
                location: this.peek().location
1392
            });
1393
            throw this.lastDiagnosticAsError();
1✔
1394
        }
1395
        endForToken = this.advance();
41✔
1396

1397
        return new ForEachStatement({
41✔
1398
            forEach: forEach,
1399
            in: maybeIn,
1400
            endFor: endForToken,
1401
            item: name,
1402
            target: target,
1403
            body: body
1404
        });
1405
    }
1406

1407
    private namespaceStatement(): NamespaceStatement | undefined {
1408
        this.warnIfNotBrighterScriptMode('namespace');
671✔
1409
        let keyword = this.advance();
671✔
1410

1411
        this.namespaceAndFunctionDepth++;
671✔
1412

1413
        let name = this.identifyingExpression();
671✔
1414
        //set the current namespace name
1415

1416
        this.globalTerminators.push([TokenKind.EndNamespace]);
670✔
1417
        let body = this.body();
670✔
1418
        this.globalTerminators.pop();
670✔
1419

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

1431
        this.namespaceAndFunctionDepth--;
670✔
1432

1433
        let result = new NamespaceStatement({
670✔
1434
            namespace: keyword,
1435
            nameExpression: name,
1436
            body: body,
1437
            endNamespace: endKeyword
1438
        });
1439

1440
        //cache the range property so that plugins can't affect it
1441
        result.cacheLocation();
670✔
1442
        result.body.symbolTable.name += `: namespace '${result.name}'`;
670✔
1443
        return result;
670✔
1444
    }
1445

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

1457
        let expr: DottedGetExpression | VariableExpression;
1458

1459
        if (firstIdentifier) {
1,486!
1460
            // force it into an identifier so the AST makes some sense
1461
            firstIdentifier.kind = TokenKind.Identifier;
1,486✔
1462
            const varExpr = new VariableExpression({ name: firstIdentifier });
1,486✔
1463
            expr = varExpr;
1,486✔
1464

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

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

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

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

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

1543
        return libStatement;
13✔
1544
    }
1545

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

1557
        return importStatement;
215✔
1558
    }
1559

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

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

1593
        let aliasStmt = new AliasStatement({
33✔
1594
            alias: aliasToken,
1595
            name: name,
1596
            equals: equals,
1597
            value: value
1598

1599
        });
1600

1601
        return aliasStmt;
33✔
1602
    }
1603

1604
    private typeStatement(): TypeStatement | undefined {
1605
        this.warnIfNotBrighterScriptMode('type statements');
26✔
1606
        const typeToken = this.advance();
26✔
1607
        const name = this.tryConsume(
26✔
1608
            DiagnosticMessages.expectedIdentifier('type'),
1609
            TokenKind.Identifier
1610
        );
1611
        const equals = this.tryConsume(
26✔
1612
            DiagnosticMessages.expectedToken(TokenKind.Equal),
1613
            TokenKind.Equal
1614
        );
1615
        let value = this.typeExpression();
26✔
1616

1617
        let typeStmt = new TypeStatement({
25✔
1618
            type: typeToken,
1619
            name: name,
1620
            equals: equals,
1621
            value: value
1622

1623
        });
1624

1625
        return typeStmt;
25✔
1626
    }
1627

1628
    private annotationExpression() {
1629
        const atToken = this.advance();
75✔
1630
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
75✔
1631
        if (identifier) {
75✔
1632
            identifier.kind = TokenKind.Identifier;
74✔
1633
        }
1634
        let annotation = new AnnotationExpression({ at: atToken, name: identifier });
75✔
1635
        this.pendingAnnotations.push(annotation);
74✔
1636

1637
        //optional arguments
1638
        if (this.check(TokenKind.LeftParen)) {
74✔
1639
            let leftParen = this.advance();
30✔
1640
            annotation.call = this.finishCall(leftParen, annotation, false);
30✔
1641
        }
1642
        return annotation;
74✔
1643
    }
1644

1645
    private ternaryExpression(test?: Expression): TernaryExpression {
1646
        this.warnIfNotBrighterScriptMode('ternary operator');
98✔
1647
        if (!test) {
98!
1648
            test = this.expression();
×
1649
        }
1650
        const questionMarkToken = this.advance();
98✔
1651

1652
        //consume newlines or comments
1653
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
98✔
1654
            this.advance();
7✔
1655
        }
1656

1657
        let consequent: Expression;
1658
        try {
98✔
1659
            consequent = this.expression();
98✔
1660
        } catch { }
1661

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

1667
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
98✔
1668

1669
        //consume newlines
1670
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
98✔
1671
            this.advance();
11✔
1672
        }
1673
        let alternate: Expression;
1674
        try {
98✔
1675
            alternate = this.expression();
98✔
1676
        } catch { }
1677

1678
        return new TernaryExpression({
98✔
1679
            test: test,
1680
            questionMark: questionMarkToken,
1681
            consequent: consequent,
1682
            colon: colonToken,
1683
            alternate: alternate
1684
        });
1685
    }
1686

1687
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1688
        this.warnIfNotBrighterScriptMode('null coalescing operator');
35✔
1689
        const questionQuestionToken = this.advance();
35✔
1690
        const alternate = this.expression();
35✔
1691
        return new NullCoalescingExpression({
35✔
1692
            consequent: test,
1693
            questionQuestion: questionQuestionToken,
1694
            alternate: alternate
1695
        });
1696
    }
1697

1698
    private regexLiteralExpression() {
1699
        this.warnIfNotBrighterScriptMode('regular expression literal');
45✔
1700
        return new RegexLiteralExpression({
45✔
1701
            regexLiteral: this.advance()
1702
        });
1703
    }
1704

1705
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1706
        this.warnIfNotBrighterScriptMode('template string');
55✔
1707

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

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

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

1762
        //store the final set of quasis
1763
        quasis.push(
55✔
1764
            new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1765
        );
1766

1767
        if (this.isAtEnd()) {
55✔
1768
            //error - missing backtick
1769
            this.diagnostics.push({
2✔
1770
                ...DiagnosticMessages.unterminatedTemplateString(),
1771
                location: {
1772
                    uri: openingBacktick.location.uri,
1773
                    range: util.createBoundingRange(openingBacktick, this.peek())
1774
                }
1775
            });
1776
            throw this.lastDiagnosticAsError();
2✔
1777

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

1799
    private tryCatchStatement(): TryCatchStatement {
1800
        const tryToken = this.advance();
41✔
1801
        let endTryToken: Token;
1802
        let catchStmt: CatchStatement;
1803
        //ensure statement separator
1804
        this.consumeStatementSeparators();
41✔
1805

1806
        let tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
41✔
1807

1808
        const peek = this.peek();
41✔
1809
        if (peek.kind !== TokenKind.Catch) {
41✔
1810
            this.diagnostics.push({
2✔
1811
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1812
                location: this.peek()?.location
6!
1813
            });
1814
        } else {
1815
            const catchToken = this.advance();
39✔
1816

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

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

1843
        const statement = new TryCatchStatement({
41✔
1844
            try: tryToken,
1845
            tryBranch: tryBranch,
1846
            catchStatement: catchStmt,
1847
            endTry: endTryToken
1848
        }
1849
        );
1850
        return statement;
41✔
1851
    }
1852

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

1867
    private dimStatement() {
1868
        const dim = this.advance();
43✔
1869

1870
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
43✔
1871
        // force to an identifier so the AST makes some sense
1872
        if (identifier) {
43✔
1873
            identifier.kind = TokenKind.Identifier;
41✔
1874
        }
1875

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

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

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

1910
    private nestedInlineConditionalCount = 0;
4,134✔
1911

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

1926
        const ifToken = this.advance();
2,359✔
1927

1928
        const condition = this.expression();
2,359✔
1929
        let thenBranch: Block;
1930
        let elseBranch: IfStatement | Block | undefined;
1931

1932
        let thenToken: Token | undefined;
1933
        let endIfToken: Token | undefined;
1934
        let elseToken: Token | undefined;
1935

1936
        //optional `then`
1937
        if (this.check(TokenKind.Then)) {
2,357✔
1938
            thenToken = this.advance();
1,875✔
1939
        }
1940

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

1944
        if (isInlineIfThen) {
2,357✔
1945
            /*** PARSE INLINE IF STATEMENT ***/
1946
            if (!incrementNestedCount) {
48✔
1947
                this.nestedInlineConditionalCount++;
5✔
1948
            }
1949

1950
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
48✔
1951

1952
            if (!thenBranch) {
48!
1953
                this.diagnostics.push({
×
1954
                    ...DiagnosticMessages.expectedStatement(ifToken.text, 'statement'),
1955
                    location: this.peek().location
1956
                });
1957
                throw this.lastDiagnosticAsError();
×
1958
            } else {
1959
                this.ensureInline(thenBranch.statements);
48✔
1960
            }
1961

1962
            //else branch
1963
            if (this.check(TokenKind.Else)) {
48✔
1964
                elseToken = this.advance();
33✔
1965

1966
                if (this.check(TokenKind.If)) {
33✔
1967
                    // recurse-read `else if`
1968
                    elseBranch = this.ifStatement(false);
10✔
1969

1970
                    //no multi-line if chained with an inline if
1971
                    if (!elseBranch.isInline) {
9✔
1972
                        this.diagnostics.push({
4✔
1973
                            ...DiagnosticMessages.expectedInlineIfStatement(),
1974
                            location: elseBranch.location
1975
                        });
1976
                    }
1977

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

1988
                    if (elseBranch) {
20!
1989
                        this.ensureInline(elseBranch.statements);
20✔
1990
                    }
1991
                }
1992

1993
                if (!elseBranch) {
29!
1994
                    //missing `else` branch
1995
                    this.diagnostics.push({
×
1996
                        ...DiagnosticMessages.expectedStatement('else', 'statement'),
1997
                        location: this.peek().location
1998
                    });
1999
                    throw this.lastDiagnosticAsError();
×
2000
                }
2001
            }
2002

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

2023
            thenBranch = this.blockConditionalBranch(ifToken);
2,309✔
2024

2025
            //ensure newline/colon before next keyword
2026
            this.ensureNewLineOrColon();
2,306✔
2027

2028
            //else branch
2029
            if (this.check(TokenKind.Else)) {
2,306✔
2030
                elseToken = this.advance();
1,822✔
2031

2032
                if (this.check(TokenKind.If)) {
1,822✔
2033
                    // recurse-read `else if`
2034
                    elseBranch = this.ifStatement();
1,077✔
2035

2036
                } else {
2037
                    elseBranch = this.blockConditionalBranch(ifToken);
745✔
2038

2039
                    //ensure newline/colon before next keyword
2040
                    this.ensureNewLineOrColon();
745✔
2041
                }
2042
            }
2043

2044
            if (!isIfStatement(elseBranch)) {
2,306✔
2045
                if (this.check(TokenKind.EndIf)) {
1,229✔
2046
                    endIfToken = this.advance();
1,224✔
2047

2048
                } else {
2049
                    //missing endif
2050
                    this.diagnostics.push({
5✔
2051
                        ...DiagnosticMessages.expectedTerminator('end if', 'if'),
2052
                        location: ifToken.location
2053
                    });
2054
                }
2055
            }
2056
        }
2057

2058
        return new IfStatement({
2,350✔
2059
            if: ifToken,
2060
            then: thenToken,
2061
            endIf: endIfToken,
2062
            else: elseToken,
2063
            condition: condition,
2064
            thenBranch: thenBranch,
2065
            elseBranch: elseBranch
2066
        });
2067
    }
2068

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

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

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

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

2095
    private conditionalCompileStatement(): ConditionalCompileStatement {
2096
        const hashIfToken = this.advance();
58✔
2097
        let notToken: Token | undefined;
2098

2099
        if (this.check(TokenKind.Not)) {
58✔
2100
            notToken = this.advance();
7✔
2101
        }
2102

2103
        if (!this.checkAny(TokenKind.True, TokenKind.False, TokenKind.Identifier)) {
58✔
2104
            this.diagnostics.push({
1✔
2105
                ...DiagnosticMessages.invalidHashIfValue(),
2106
                location: this.peek()?.location
3!
2107
            });
2108
        }
2109

2110

2111
        const condition = this.advance();
58✔
2112

2113
        let thenBranch: Block;
2114
        let elseBranch: ConditionalCompileStatement | Block | undefined;
2115

2116
        let hashEndIfToken: Token | undefined;
2117
        let hashElseToken: Token | undefined;
2118

2119
        //keep track of the current error count
2120
        //if this is `#if false` remove all diagnostics.
2121
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
58✔
2122

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

2130
        this.ensureNewLine();
57✔
2131
        this.advance();
57✔
2132

2133
        //else branch
2134
        if (this.check(TokenKind.HashElseIf)) {
57✔
2135
            // recurse-read `#else if`
2136
            elseBranch = this.conditionalCompileStatement();
15✔
2137
            this.ensureNewLine();
15✔
2138

2139
        } else if (this.check(TokenKind.HashElse)) {
42✔
2140
            hashElseToken = this.advance();
11✔
2141
            let diagnosticsLengthBeforeBlock = this.diagnostics.length;
11✔
2142
            elseBranch = this.blockConditionalCompileBranch(hashIfToken);
11✔
2143

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

2152
        if (!isConditionalCompileStatement(elseBranch)) {
57✔
2153

2154
            if (this.check(TokenKind.HashEndIf)) {
42!
2155
                hashEndIfToken = this.advance();
42✔
2156

2157
            } else {
2158
                //missing #endif
2159
                this.diagnostics.push({
×
2160
                    ...DiagnosticMessages.expectedTerminator('#end if', '#if'),
2161
                    location: hashIfToken.location
2162
                });
2163
            }
2164
        }
2165

2166
        return new ConditionalCompileStatement({
57✔
2167
            hashIf: hashIfToken,
2168
            hashElse: hashElseToken,
2169
            hashEndIf: hashEndIfToken,
2170
            not: notToken,
2171
            condition: condition,
2172
            thenBranch: thenBranch,
2173
            elseBranch: elseBranch
2174
        });
2175
    }
2176

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

2183
        //parsing until trailing "#end if", "#else", "#else if"
2184
        let branch = this.conditionalCompileBlock();
69✔
2185

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

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

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

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

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

2233
            } else {
2234
                //something went wrong. reset to the top of the loop
2235
                this.current = loopCurrent;
1✔
2236

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

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

2244
                //consume potential separators
2245
                this.consumeStatementSeparators(true);
1✔
2246
            }
2247
        }
2248
        this.globalTerminators.pop();
69✔
2249

2250

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

2282
    private conditionalCompileConstStatement() {
2283
        const hashConstToken = this.advance();
21✔
2284

2285
        const constName = this.peek();
21✔
2286
        //disallow using keywords for const names
2287
        if (ReservedWords.has(constName?.text.toLowerCase())) {
21!
2288
            this.diagnostics.push({
1✔
2289
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(constName?.text),
3!
2290
                location: constName?.location
3!
2291
            });
2292

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

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

2321
        if (!this.check(TokenKind.Newline)) {
18!
2322
            this.diagnostics.push({
×
2323
                ...DiagnosticMessages.unexpectedToken(this.peek().text),
2324
                location: this.peek().location
2325
            });
2326
            throw this.lastDiagnosticAsError();
×
2327
        }
2328

2329
        return new ConditionalCompileConstStatement({ hashConst: hashConstToken, assignment: assignment });
18✔
2330
    }
2331

2332
    private conditionalCompileErrorStatement() {
2333
        const hashErrorToken = this.advance();
10✔
2334
        const tokensUntilEndOfLine = this.consumeUntil(TokenKind.Newline);
10✔
2335
        const message = createToken(TokenKind.HashErrorMessage, tokensUntilEndOfLine.map(t => t.text).join(' '));
10✔
2336
        return new ConditionalCompileErrorStatement({ hashError: hashErrorToken, message: message });
10✔
2337
    }
2338

2339
    private ensureNewLine() {
2340
        //ensure newline before next keyword
2341
        if (!this.check(TokenKind.Newline)) {
83!
2342
            this.diagnostics.push({
×
2343
                ...DiagnosticMessages.unexpectedToken(this.peek().text),
2344
                location: this.peek().location
2345
            });
2346
            throw this.lastDiagnosticAsError();
×
2347
        }
2348
    }
2349

2350
    private ensureNewLineOrColon(silent = false) {
3,051✔
2351
        const prev = this.previous().kind;
3,273✔
2352
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
3,273✔
2353
            if (!silent) {
162✔
2354
                this.diagnostics.push({
8✔
2355
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2356
                    location: this.peek().location
2357
                });
2358
            }
2359
            return false;
162✔
2360
        }
2361
        return true;
3,111✔
2362
    }
2363

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

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

2387
        //look for colon statement separator
2388
        let foundColon = false;
86✔
2389
        while (this.match(TokenKind.Colon)) {
86✔
2390
            foundColon = true;
23✔
2391
        }
2392

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

2414
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2415
        let expressionStart = this.peek();
977✔
2416

2417
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
977✔
2418
            let operator = this.advance();
27✔
2419

2420
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
27✔
2421
                this.diagnostics.push({
1✔
2422
                    ...DiagnosticMessages.unexpectedOperator(),
2423
                    location: this.peek().location
2424
                });
2425
                throw this.lastDiagnosticAsError();
1✔
2426
            } else if (isCallExpression(expr)) {
26✔
2427
                this.diagnostics.push({
1✔
2428
                    ...DiagnosticMessages.unexpectedOperator(),
2429
                    location: expressionStart.location
2430
                });
2431
                throw this.lastDiagnosticAsError();
1✔
2432
            }
2433

2434
            const result = new IncrementStatement({ value: expr, operator: operator });
25✔
2435
            return result;
25✔
2436
        }
2437

2438
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
950✔
2439
            return new ExpressionStatement({ expression: expr });
568✔
2440
        }
2441

2442
        if (this.checkAny(...BinaryExpressionOperatorTokens)) {
382✔
2443
            expr = new BinaryExpression({ left: expr, operator: this.advance(), right: this.expression() });
6✔
2444
        }
2445

2446
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an inclosing ExpressionStatement
2447
        this.diagnostics.push({
382✔
2448
            ...DiagnosticMessages.expectedStatement(),
2449
            location: expressionStart.location
2450
        });
2451
        return new ExpressionStatement({ expression: expr });
382✔
2452
    }
2453

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

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

2499
    private printStatement(): PrintStatement {
2500
        let printKeyword = this.advance();
1,395✔
2501

2502
        let values: Expression[] = [];
1,395✔
2503

2504
        while (!this.checkEndOfStatement()) {
1,395✔
2505
            if (this.checkAny(TokenKind.Semicolon, TokenKind.Comma)) {
1,525✔
2506
                values.push(new PrintSeparatorExpression({ separator: this.advance() as PrintSeparatorToken }));
49✔
2507
            } else if (this.check(TokenKind.Else)) {
1,476✔
2508
                break; // inline branch
22✔
2509
            } else {
2510
                values.push(this.expression());
1,454✔
2511
            }
2512
        }
2513

2514
        //print statements can be empty, so look for empty print conditions
2515
        if (!values.length) {
1,393✔
2516
            const endOfStatementLocation = util.createBoundingLocation(printKeyword, this.peek());
9✔
2517
            let emptyStringLiteral = createStringLiteral('', endOfStatementLocation);
9✔
2518
            values.push(emptyStringLiteral);
9✔
2519
        }
2520

2521
        let last = values[values.length - 1];
1,393✔
2522
        if (isToken(last)) {
1,393!
2523
            // TODO: error, expected value
2524
        }
2525

2526
        return new PrintStatement({ print: printKeyword, expressions: values });
1,393✔
2527
    }
2528

2529
    /**
2530
     * Parses a return statement with an optional return value.
2531
     * @returns an AST representation of a return statement.
2532
     */
2533
    private returnStatement(): ReturnStatement {
2534
        let options = { return: this.previous() };
3,720✔
2535

2536
        if (this.checkEndOfStatement()) {
3,720✔
2537
            return new ReturnStatement(options);
24✔
2538
        }
2539

2540
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
3,696✔
2541
        return new ReturnStatement({ ...options, value: toReturn });
3,695✔
2542
    }
2543

2544
    /**
2545
     * Parses a `label` statement
2546
     * @returns an AST representation of an `label` statement.
2547
     */
2548
    private labelStatement() {
2549
        let options = {
12✔
2550
            name: this.advance(),
2551
            colon: this.advance()
2552
        };
2553

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

2561
        return new LabelStatement(options);
10✔
2562
    }
2563

2564
    /**
2565
     * Parses a `continue` statement
2566
     */
2567
    private continueStatement() {
2568
        return new ContinueStatement({
12✔
2569
            continue: this.advance(),
2570
            loopType: this.tryConsume(
2571
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2572
                TokenKind.While, TokenKind.For
2573
            )
2574
        });
2575
    }
2576

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

2590
        return new GotoStatement(tokens);
10✔
2591
    }
2592

2593
    /**
2594
     * Parses an `end` statement
2595
     * @returns an AST representation of an `end` statement.
2596
     */
2597
    private endStatement() {
2598
        let options = { end: this.advance() };
8✔
2599

2600
        return new EndStatement(options);
8✔
2601
    }
2602
    /**
2603
     * Parses a `stop` statement
2604
     * @returns an AST representation of a `stop` statement
2605
     */
2606
    private stopStatement() {
2607
        let options = { stop: this.advance() };
16✔
2608

2609
        return new StopStatement(options);
16✔
2610
    }
2611

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

2621
        this.consumeStatementSeparators(true);
7,471✔
2622
        const statements: Statement[] = [];
7,471✔
2623
        const flatGlobalTerminators = this.globalTerminators.flat().flat();
7,471✔
2624
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators, ...flatGlobalTerminators)) {
7,471✔
2625
            //grab the location of the current token
2626
            let loopCurrent = this.current;
8,907✔
2627
            let dec = this.declaration();
8,907✔
2628
            if (dec) {
8,907✔
2629
                if (!isAnnotationExpression(dec)) {
8,861✔
2630
                    this.consumePendingAnnotations(dec);
8,854✔
2631
                    statements.push(dec);
8,854✔
2632
                }
2633

2634
                //ensure statement separator
2635
                this.consumeStatementSeparators();
8,861✔
2636

2637
            } else {
2638
                //something went wrong. reset to the top of the loop
2639
                this.current = loopCurrent;
46✔
2640

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

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

2648
                //consume potential separators
2649
                this.consumeStatementSeparators(true);
46✔
2650
            }
2651
        }
2652

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

2669
        this.exitAnnotationBlock(parentAnnotations);
7,465✔
2670
        return new Block({ statements: statements });
7,465✔
2671
    }
2672

2673
    /**
2674
     * Attach pending annotations to the provided statement,
2675
     * and then reset the annotations array
2676
     */
2677
    consumePendingAnnotations(statement: Statement) {
2678
        if (this.pendingAnnotations.length) {
17,122✔
2679
            statement.annotations = this.pendingAnnotations;
51✔
2680
            this.pendingAnnotations = [];
51✔
2681
        }
2682
    }
2683

2684
    enterAnnotationBlock() {
2685
        const pending = this.pendingAnnotations;
13,436✔
2686
        this.pendingAnnotations = [];
13,436✔
2687
        return pending;
13,436✔
2688
    }
2689

2690
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2691
        // non consumed annotations are an error
2692
        if (this.pendingAnnotations.length) {
13,428✔
2693
            for (const annotation of this.pendingAnnotations) {
5✔
2694
                this.diagnostics.push({
7✔
2695
                    ...DiagnosticMessages.unusedAnnotation(),
2696
                    location: annotation.location
2697
                });
2698
            }
2699
        }
2700
        this.pendingAnnotations = parentAnnotations;
13,428✔
2701
    }
2702

2703
    private expression(findTypecast = true): Expression {
13,587✔
2704
        let expression = this.anonymousFunction();
13,995✔
2705
        let asToken: Token;
2706
        let typeExpression: TypeExpression;
2707
        if (findTypecast) {
13,956✔
2708
            do {
13,581✔
2709
                if (this.check(TokenKind.As)) {
13,662✔
2710
                    this.warnIfNotBrighterScriptMode('type cast');
83✔
2711
                    // Check if this expression is wrapped in any type casts
2712
                    // allows for multiple casts:
2713
                    // myVal = foo() as dynamic as string
2714
                    [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
83✔
2715
                    if (asToken && typeExpression) {
83✔
2716
                        expression = new TypecastExpression({ obj: expression, as: asToken, typeExpression: typeExpression });
81✔
2717
                    }
2718
                } else {
2719
                    break;
13,579✔
2720
                }
2721

2722
            } while (asToken && typeExpression);
166✔
2723
        }
2724
        return expression;
13,956✔
2725
    }
2726

2727
    private anonymousFunction(): Expression {
2728
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
13,995✔
2729
            const func = this.functionDeclaration(true);
97✔
2730
            //if there's an open paren after this, this is an IIFE
2731
            if (this.check(TokenKind.LeftParen)) {
96✔
2732
                return this.finishCall(this.advance(), func);
3✔
2733
            } else {
2734
                return func;
93✔
2735
            }
2736
        }
2737

2738
        let expr = this.boolean();
13,898✔
2739

2740
        if (this.check(TokenKind.Question)) {
13,860✔
2741
            return this.ternaryExpression(expr);
98✔
2742
        } else if (this.check(TokenKind.QuestionQuestion)) {
13,762✔
2743
            return this.nullCoalescingExpression(expr);
35✔
2744
        } else {
2745
            return expr;
13,727✔
2746
        }
2747
    }
2748

2749
    private boolean(): Expression {
2750
        let expr = this.relational();
13,898✔
2751

2752
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
13,860✔
2753
            let operator = this.previous();
34✔
2754
            let right = this.relational();
34✔
2755
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
34✔
2756
        }
2757

2758
        return expr;
13,860✔
2759
    }
2760

2761
    private relational(): Expression {
2762
        let expr = this.additive();
13,962✔
2763

2764
        while (
13,924✔
2765
            this.matchAny(
2766
                TokenKind.Equal,
2767
                TokenKind.LessGreater,
2768
                TokenKind.Greater,
2769
                TokenKind.GreaterEqual,
2770
                TokenKind.Less,
2771
                TokenKind.LessEqual
2772
            )
2773
        ) {
2774
            let operator = this.previous();
1,943✔
2775
            let right = this.additive();
1,943✔
2776
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,943✔
2777
        }
2778

2779
        return expr;
13,924✔
2780
    }
2781

2782
    // TODO: bitshift
2783

2784
    private additive(): Expression {
2785
        let expr = this.multiplicative();
15,905✔
2786

2787
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
15,867✔
2788
            let operator = this.previous();
1,555✔
2789
            let right = this.multiplicative();
1,555✔
2790
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,555✔
2791
        }
2792

2793
        return expr;
15,867✔
2794
    }
2795

2796
    private multiplicative(): Expression {
2797
        let expr = this.exponential();
17,460✔
2798

2799
        while (this.matchAny(
17,422✔
2800
            TokenKind.Forwardslash,
2801
            TokenKind.Backslash,
2802
            TokenKind.Star,
2803
            TokenKind.Mod,
2804
            TokenKind.LeftShift,
2805
            TokenKind.RightShift
2806
        )) {
2807
            let operator = this.previous();
60✔
2808
            let right = this.exponential();
60✔
2809
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
60✔
2810
        }
2811

2812
        return expr;
17,422✔
2813
    }
2814

2815
    private exponential(): Expression {
2816
        let expr = this.prefixUnary();
17,520✔
2817

2818
        while (this.match(TokenKind.Caret)) {
17,482✔
2819
            let operator = this.previous();
9✔
2820
            let right = this.prefixUnary();
9✔
2821
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
9✔
2822
        }
2823

2824
        return expr;
17,482✔
2825
    }
2826

2827
    private prefixUnary(): Expression {
2828
        const nextKind = this.peek().kind;
17,565✔
2829
        if (nextKind === TokenKind.Not) {
17,565✔
2830
            this.current++; //advance
30✔
2831
            let operator = this.previous();
30✔
2832
            let right = this.relational();
30✔
2833
            return new UnaryExpression({ operator: operator, right: right });
30✔
2834
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
17,535✔
2835
            this.current++; //advance
36✔
2836
            let operator = this.previous();
36✔
2837
            let right = (nextKind as any) === TokenKind.Not
36✔
2838
                ? this.boolean()
36!
2839
                : this.prefixUnary();
2840
            return new UnaryExpression({ operator: operator, right: right });
36✔
2841
        }
2842
        return this.call();
17,499✔
2843
    }
2844

2845
    private indexedGet(expr: Expression) {
2846
        let openingSquare = this.previous();
161✔
2847
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
161✔
2848
        let indexes: Expression[] = [];
161✔
2849

2850

2851
        //consume leading newlines
2852
        while (this.match(TokenKind.Newline)) { }
161✔
2853

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

2872
        const closingSquare = this.tryConsume(
161✔
2873
            DiagnosticMessages.unmatchedLeftToken(openingSquare.text, 'array or object index'),
2874
            TokenKind.RightSquareBracket
2875
        );
2876

2877
        return new IndexedGetExpression({
161✔
2878
            obj: expr,
2879
            indexes: indexes,
2880
            openingSquare: openingSquare,
2881
            closingSquare: closingSquare,
2882
            questionDot: questionDotToken
2883
        });
2884
    }
2885

2886
    private newExpression() {
2887
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
141✔
2888
        let newToken = this.advance();
141✔
2889

2890
        let nameExpr = this.identifyingExpression();
141✔
2891
        let leftParen = this.tryConsume(
141✔
2892
            DiagnosticMessages.unexpectedToken(this.peek().text),
2893
            TokenKind.LeftParen,
2894
            TokenKind.QuestionLeftParen
2895
        );
2896

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

2905
        let call = this.finishCall(leftParen, nameExpr);
137✔
2906
        //pop the call from the  callExpressions list because this is technically something else
2907
        this.callExpressions.pop();
137✔
2908
        let result = new NewExpression({ new: newToken, call: call });
137✔
2909
        return result;
137✔
2910
    }
2911

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

2939
    private call(): Expression {
2940
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
18,898✔
2941
            return this.newExpression();
141✔
2942
        }
2943
        let expr = this.primary();
18,757✔
2944

2945
        while (true) {
18,662✔
2946
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
24,069✔
2947
                expr = this.finishCall(this.previous(), expr);
2,520✔
2948
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
21,549✔
2949
                expr = this.indexedGet(expr);
159✔
2950
            } else if (this.match(TokenKind.Callfunc)) {
21,390✔
2951
                expr = this.callfunc(expr);
74✔
2952
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
21,316✔
2953
                if (this.match(TokenKind.LeftSquareBracket)) {
2,698✔
2954
                    expr = this.indexedGet(expr);
2✔
2955
                } else {
2956
                    let dot = this.previous();
2,696✔
2957
                    let name = this.tryConsume(
2,696✔
2958
                        DiagnosticMessages.expectedIdentifier(),
2959
                        TokenKind.Identifier,
2960
                        ...AllowedProperties
2961
                    );
2962
                    if (!name) {
2,696✔
2963
                        break;
44✔
2964
                    }
2965

2966
                    // force it into an identifier so the AST makes some sense
2967
                    name.kind = TokenKind.Identifier;
2,652✔
2968
                    expr = new DottedGetExpression({ obj: expr, name: name as Identifier, dot: dot });
2,652✔
2969
                }
2970

2971
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
18,618✔
2972
                let dot = this.advance();
11✔
2973
                let name = this.tryConsume(
11✔
2974
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2975
                    TokenKind.Identifier,
2976
                    ...AllowedProperties
2977
                );
2978

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

2988
            } else {
2989
                break;
18,607✔
2990
            }
2991
        }
2992

2993
        return expr;
18,662✔
2994
    }
2995

2996
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
2,660✔
2997
        let args = [] as Expression[];
2,756✔
2998
        while (this.match(TokenKind.Newline)) { }
2,756✔
2999

3000
        if (!this.check(TokenKind.RightParen)) {
2,756✔
3001
            do {
1,402✔
3002
                while (this.match(TokenKind.Newline)) { }
2,003✔
3003

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

3021
        while (this.match(TokenKind.Newline)) { }
2,756✔
3022

3023
        const closingParen = this.tryConsume(
2,756✔
3024
            DiagnosticMessages.unmatchedLeftToken(openingParen.text, 'function call arguments'),
3025
            TokenKind.RightParen
3026
        );
3027

3028
        let expression = new CallExpression({
2,756✔
3029
            callee: callee,
3030
            openingParen: openingParen,
3031
            args: args,
3032
            closingParen: closingParen
3033
        });
3034
        if (addToCallExpressionList) {
2,756✔
3035
            this.callExpressions.push(expression);
2,660✔
3036
        }
3037
        return expression;
2,756✔
3038
    }
3039

3040
    /**
3041
     * Creates a TypeExpression, which wraps standard ASTNodes that represent a BscType
3042
     */
3043
    private typeExpression(): TypeExpression {
3044
        const changedTokens: { token: Token; oldKind: TokenKind }[] = [];
2,004✔
3045
        try {
2,004✔
3046
            // handle types with 'and'/'or' operators
3047
            const expressionsWithOperator: { expression: Expression; operator?: Token }[] = [];
2,004✔
3048

3049
            // find all expressions and operators
3050
            let expr: Expression = this.getTypeExpressionPart(changedTokens);
2,004✔
3051
            while (this.options.mode === ParseMode.BrighterScript && this.matchAny(TokenKind.Or, TokenKind.And)) {
2,003✔
3052
                let operator = this.previous();
102✔
3053
                expressionsWithOperator.push({ expression: expr, operator: operator });
102✔
3054
                expr = this.getTypeExpressionPart(changedTokens);
102✔
3055
            }
3056
            // add last expression
3057
            expressionsWithOperator.push({ expression: expr });
2,002✔
3058

3059
            // handle expressions with order of operations - first "and", then "or"
3060
            const combineExpressions = (opToken: TokenKind) => {
2,002✔
3061
                let exprWithOp = expressionsWithOperator[0];
4,004✔
3062
                let index = 0;
4,004✔
3063
                while (exprWithOp?.operator) {
4,004!
3064
                    if (exprWithOp.operator.kind === opToken) {
183✔
3065
                        const nextExpr = expressionsWithOperator[index + 1];
101✔
3066
                        const combinedExpr = new BinaryExpression({ left: exprWithOp.expression, operator: exprWithOp.operator, right: nextExpr.expression });
101✔
3067
                        // replace the two expressions with the combined one
3068
                        expressionsWithOperator.splice(index, 2, { expression: combinedExpr, operator: nextExpr.operator });
101✔
3069
                        exprWithOp = expressionsWithOperator[index];
101✔
3070
                    } else {
3071
                        index++;
82✔
3072
                        exprWithOp = expressionsWithOperator[index];
82✔
3073
                    }
3074
                }
3075
            };
3076

3077
            combineExpressions(TokenKind.And);
2,002✔
3078
            combineExpressions(TokenKind.Or);
2,002✔
3079

3080
            if (expressionsWithOperator[0]?.expression) {
2,002!
3081
                return new TypeExpression({ expression: expressionsWithOperator[0].expression });
1,985✔
3082
            }
3083

3084
        } catch (error) {
3085
            // Something went wrong - reset the kind to what it was previously
3086
            for (const changedToken of changedTokens) {
2✔
UNCOV
3087
                changedToken.token.kind = changedToken.oldKind;
×
3088
            }
3089
            throw error;
2✔
3090
        }
3091
    }
3092

3093
    /**
3094
     * Gets a single "part" of a type of a potential Union type
3095
     * Note: this does not NEED to be part of a union type, but the logic is the same
3096
     *
3097
     * @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
3098
     * @returns an expression that was successfully parsed
3099
     */
3100
    private getTypeExpressionPart(changedTokens: { token: Token; oldKind: TokenKind }[]) {
3101
        let expr: VariableExpression | DottedGetExpression | TypedArrayExpression | InlineInterfaceExpression | GroupingExpression;
3102

3103
        if (this.checkAny(...DeclarableTypes)) {
2,106✔
3104
            // if this is just a type, just use directly
3105
            expr = new VariableExpression({ name: this.advance() as Identifier });
1,386✔
3106
        } else {
3107
            if (this.options.mode === ParseMode.BrightScript && !declarableTypesLower.includes(this.peek()?.text?.toLowerCase())) {
720!
3108
                // custom types arrays not allowed in Brightscript
3109
                this.warnIfNotBrighterScriptMode('custom types');
17✔
3110
                this.advance(); // skip custom type token
17✔
3111
                return expr;
17✔
3112
            }
3113

3114
            if (this.match(TokenKind.LeftCurlyBrace)) {
703✔
3115
                expr = this.inlineInterface();
41✔
3116
            } else if (this.match(TokenKind.LeftParen)) {
662✔
3117
                let left = this.previous();
18✔
3118
                let typeExpr = this.typeExpression();
18✔
3119
                let right = this.consume(
18✔
3120
                    DiagnosticMessages.unmatchedLeftToken(left.text, 'type expression'),
3121
                    TokenKind.RightParen
3122
                );
3123
                expr = new GroupingExpression({ leftParen: left, rightParen: right, expression: typeExpr });
18✔
3124
            } else {
3125
                if (this.checkAny(...AllowedTypeIdentifiers)) {
644✔
3126
                    // Since the next token is allowed as a type identifier, change the kind
3127
                    let nextToken = this.peek();
2✔
3128
                    changedTokens.push({ token: nextToken, oldKind: nextToken.kind });
2✔
3129
                    nextToken.kind = TokenKind.Identifier;
2✔
3130
                }
3131
                expr = this.identifyingExpression(AllowedTypeIdentifiers);
644✔
3132
            }
3133
        }
3134

3135
        //Check if it has square brackets, thus making it an array
3136
        if (expr && this.check(TokenKind.LeftSquareBracket)) {
2,087✔
3137
            if (this.options.mode === ParseMode.BrightScript) {
36✔
3138
                // typed arrays not allowed in Brightscript
3139
                this.warnIfNotBrighterScriptMode('typed arrays');
1✔
3140
                return expr;
1✔
3141
            }
3142

3143
            // Check if it is an array - that is, if it has `[]` after the type
3144
            // eg. `string[]` or `SomeKlass[]`
3145
            // This is while loop, so it supports multidimensional arrays (eg. integer[][])
3146
            while (this.check(TokenKind.LeftSquareBracket)) {
35✔
3147
                const leftBracket = this.advance();
37✔
3148
                if (this.check(TokenKind.RightSquareBracket)) {
37!
3149
                    const rightBracket = this.advance();
37✔
3150
                    expr = new TypedArrayExpression({ innerType: expr, leftBracket: leftBracket, rightBracket: rightBracket });
37✔
3151
                }
3152
            }
3153
        }
3154

3155
        return expr;
2,086✔
3156
    }
3157

3158

3159
    private inlineInterface() {
3160
        let expr: InlineInterfaceExpression;
3161
        const openToken = this.previous();
41✔
3162
        const members: InlineInterfaceMemberExpression[] = [];
41✔
3163
        while (this.match(TokenKind.Newline)) { }
41✔
3164
        while (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral, TokenKind.Optional)) {
41✔
3165
            const member = this.inlineInterfaceMember();
48✔
3166
            members.push(member);
48✔
3167
            while (this.matchAny(TokenKind.Comma, TokenKind.Newline)) { }
48✔
3168
        }
3169
        if (!this.check(TokenKind.RightCurlyBrace)) {
41!
UNCOV
3170
            this.diagnostics.push({
×
3171
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
3172
                location: this.peek().location
3173
            });
UNCOV
3174
            throw this.lastDiagnosticAsError();
×
3175
        }
3176
        const closeToken = this.advance();
41✔
3177

3178
        expr = new InlineInterfaceExpression({ open: openToken, members: members, close: closeToken });
41✔
3179
        return expr;
41✔
3180
    }
3181

3182
    private inlineInterfaceMember(): InlineInterfaceMemberExpression {
3183
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
48✔
3184

3185
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral)) {
48!
3186
            if (this.check(TokenKind.As)) {
48!
UNCOV
3187
                if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
×
3188
                    // as <EOL>
3189
                    // `as` is the field name
UNCOV
3190
                } else if (this.checkNext(TokenKind.As)) {
×
3191
                    //  as as ____
3192
                    // first `as` is the field name
UNCOV
3193
                } else if (optionalKeyword) {
×
3194
                    // optional as ____
3195
                    // optional is the field name, `as` starts type
3196
                    // rewind current token
UNCOV
3197
                    optionalKeyword = null;
×
UNCOV
3198
                    this.current--;
×
3199
                }
3200
            }
3201
        } else {
3202
            // no name after `optional` ... optional is the name
3203
            // rewind current token
3204
            optionalKeyword = null;
×
UNCOV
3205
            this.current--;
×
3206
        }
3207

3208
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers, TokenKind.StringLiteral)) {
48!
UNCOV
3209
            this.diagnostics.push({
×
3210
                ...DiagnosticMessages.expectedIdentifier(this.peek().text),
3211
                location: this.peek().location
3212
            });
UNCOV
3213
            throw this.lastDiagnosticAsError();
×
3214
        }
3215
        let name: Token;
3216
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
48✔
3217
            name = this.identifier(...AllowedProperties);
46✔
3218
        } else {
3219
            name = this.advance();
2✔
3220
        }
3221

3222
        let typeExpression: TypeExpression;
3223

3224
        let asToken: Token = null;
48✔
3225
        if (this.check(TokenKind.As)) {
48✔
3226
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
46✔
3227

3228
        }
3229
        return new InlineInterfaceMemberExpression({
48✔
3230
            name: name,
3231
            as: asToken,
3232
            typeExpression: typeExpression,
3233
            optional: optionalKeyword
3234
        });
3235
    }
3236

3237
    private primary(): Expression {
3238
        switch (true) {
18,757✔
3239
            case this.matchAny(
18,757!
3240
                TokenKind.False,
3241
                TokenKind.True,
3242
                TokenKind.Invalid,
3243
                TokenKind.IntegerLiteral,
3244
                TokenKind.LongIntegerLiteral,
3245
                TokenKind.FloatLiteral,
3246
                TokenKind.DoubleLiteral,
3247
                TokenKind.StringLiteral
3248
            ):
3249
                return new LiteralExpression({ value: this.previous() });
8,196✔
3250

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

3255
            //template string
3256
            case this.check(TokenKind.BackTick):
3257
                return this.templateString(false);
47✔
3258

3259
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
3260
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
20,271✔
3261
                return this.templateString(true);
8✔
3262

3263
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3264
                return new VariableExpression({ name: this.previous() as Identifier });
9,791✔
3265

3266
            case this.match(TokenKind.LeftParen):
3267
                let left = this.previous();
60✔
3268
                let expr = this.expression();
60✔
3269
                let right = this.consume(
59✔
3270
                    DiagnosticMessages.unmatchedLeftToken(left.text, 'expression'),
3271
                    TokenKind.RightParen
3272
                );
3273
                return new GroupingExpression({ leftParen: left, rightParen: right, expression: expr });
59✔
3274

3275
            case this.matchAny(TokenKind.LeftSquareBracket):
3276
                return this.arrayLiteral();
169✔
3277

3278
            case this.match(TokenKind.LeftCurlyBrace):
3279
                return this.aaLiteral();
314✔
3280

3281
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
UNCOV
3282
                let token = Object.assign(this.previous(), {
×
3283
                    kind: TokenKind.Identifier
3284
                }) as Identifier;
UNCOV
3285
                return new VariableExpression({ name: token });
×
3286

3287
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
UNCOV
3288
                return this.anonymousFunction();
×
3289

3290
            case this.check(TokenKind.RegexLiteral):
3291
                return this.regexLiteralExpression();
45✔
3292

3293
            default:
3294
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
3295
                if (this.checkAny(...this.peekGlobalTerminators())) {
92!
3296
                    //don't throw a diagnostic, just return undefined
3297

3298
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
3299
                } else {
3300
                    this.diagnostics.push({
92✔
3301
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
3302
                        location: this.peek()?.location
276!
3303
                    });
3304
                    throw this.lastDiagnosticAsError();
92✔
3305
                }
3306
        }
3307
    }
3308

3309
    private arrayLiteral() {
3310
        let elements: Array<Expression> = [];
169✔
3311
        let openingSquare = this.previous();
169✔
3312

3313
        while (this.match(TokenKind.Newline)) {
169✔
3314
        }
3315
        let closingSquare: Token;
3316

3317
        if (!this.match(TokenKind.RightSquareBracket)) {
169✔
3318
            try {
128✔
3319
                elements.push(this.expression());
128✔
3320

3321
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
127✔
3322

3323
                    while (this.match(TokenKind.Newline)) {
187✔
3324

3325
                    }
3326

3327
                    if (this.check(TokenKind.RightSquareBracket)) {
187✔
3328
                        break;
36✔
3329
                    }
3330

3331
                    elements.push(this.expression());
151✔
3332
                }
3333
            } catch (error: any) {
3334
                this.rethrowNonDiagnosticError(error);
2✔
3335
            }
3336

3337
            closingSquare = this.tryConsume(
128✔
3338
                DiagnosticMessages.unmatchedLeftToken(openingSquare.text, 'array literal'),
3339
                TokenKind.RightSquareBracket
3340
            );
3341
        } else {
3342
            closingSquare = this.previous();
41✔
3343
        }
3344

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

3349
    private aaLiteral() {
3350
        let openingBrace = this.previous();
314✔
3351
        let members: Array<AAMemberExpression> = [];
314✔
3352

3353
        let key = () => {
314✔
3354
            let result = {
334✔
3355
                colonToken: null as Token,
3356
                keyToken: null as Token,
3357
                range: null as Range
3358
            };
3359
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
334✔
3360
                result.keyToken = this.identifier(...AllowedProperties);
300✔
3361
            } else if (this.check(TokenKind.StringLiteral)) {
34!
3362
                result.keyToken = this.advance();
34✔
3363
            } else {
UNCOV
3364
                this.diagnostics.push({
×
3365
                    ...DiagnosticMessages.unexpectedAAKey(),
3366
                    location: this.peek().location
3367
                });
UNCOV
3368
                throw this.lastDiagnosticAsError();
×
3369
            }
3370

3371
            result.colonToken = this.consume(
334✔
3372
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3373
                TokenKind.Colon
3374
            );
3375
            result.range = util.createBoundingRange(result.keyToken, result.colonToken);
333✔
3376
            return result;
333✔
3377
        };
3378

3379
        while (this.match(TokenKind.Newline)) { }
314✔
3380
        let closingBrace: Token;
3381
        if (!this.match(TokenKind.RightCurlyBrace)) {
314✔
3382
            let lastAAMember: AAMemberExpression;
3383
            try {
229✔
3384
                let k = key();
229✔
3385
                let expr = this.expression();
229✔
3386
                lastAAMember = new AAMemberExpression({
228✔
3387
                    key: k.keyToken,
3388
                    colon: k.colonToken,
3389
                    value: expr
3390
                });
3391
                members.push(lastAAMember);
228✔
3392

3393
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
228✔
3394
                    // collect comma at end of expression
3395
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
244✔
3396
                        (lastAAMember as DeepWriteable<AAMemberExpression>).tokens.comma = this.previous();
79✔
3397
                    }
3398

3399
                    this.consumeStatementSeparators(true);
244✔
3400

3401
                    if (this.check(TokenKind.RightCurlyBrace)) {
244✔
3402
                        break;
139✔
3403
                    }
3404
                    let k = key();
105✔
3405
                    let expr = this.expression();
104✔
3406
                    lastAAMember = new AAMemberExpression({
104✔
3407
                        key: k.keyToken,
3408
                        colon: k.colonToken,
3409
                        value: expr
3410
                    });
3411
                    members.push(lastAAMember);
104✔
3412

3413
                }
3414
            } catch (error: any) {
3415
                this.rethrowNonDiagnosticError(error);
2✔
3416
            }
3417

3418
            closingBrace = this.tryConsume(
229✔
3419
                DiagnosticMessages.unmatchedLeftToken(openingBrace.text, 'associative array literal'),
3420
                TokenKind.RightCurlyBrace
3421
            );
3422
        } else {
3423
            closingBrace = this.previous();
85✔
3424
        }
3425

3426
        const aaExpr = new AALiteralExpression({ elements: members, open: openingBrace, close: closingBrace });
314✔
3427
        return aaExpr;
314✔
3428
    }
3429

3430
    /**
3431
     * Pop token if we encounter specified token
3432
     */
3433
    private match(tokenKind: TokenKind) {
3434
        if (this.check(tokenKind)) {
71,725✔
3435
            this.current++; //advance
7,106✔
3436
            return true;
7,106✔
3437
        }
3438
        return false;
64,619✔
3439
    }
3440

3441
    /**
3442
     * Pop token if we encounter a token in the specified list
3443
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3444
     */
3445
    private matchAny(...tokenKinds: TokenKind[]) {
3446
        for (let tokenKind of tokenKinds) {
246,841✔
3447
            if (this.check(tokenKind)) {
713,805✔
3448
                this.current++; //advance
65,168✔
3449
                return true;
65,168✔
3450
            }
3451
        }
3452
        return false;
181,673✔
3453
    }
3454

3455
    /**
3456
     * If the next series of tokens matches the given set of tokens, pop them all
3457
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
3458
     */
3459
    private matchSequence(...tokenKinds: TokenKind[]) {
3460
        const endIndex = this.current + tokenKinds.length;
21,393✔
3461
        for (let i = 0; i < tokenKinds.length; i++) {
21,393✔
3462
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
21,417!
3463
                return false;
21,390✔
3464
            }
3465
        }
3466
        this.current = endIndex;
3✔
3467
        return true;
3✔
3468
    }
3469

3470
    /**
3471
     * Get next token matching a specified list, or fail with an error
3472
     */
3473
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
3474
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
21,738✔
3475
        if (token) {
21,738✔
3476
            return token;
21,711✔
3477
        } else {
3478
            let error = new Error(diagnosticInfo.message);
27✔
3479
            (error as any).isDiagnostic = true;
27✔
3480
            throw error;
27✔
3481
        }
3482
    }
3483

3484
    /**
3485
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3486
     */
3487
    private consumeTokenIf(tokenKind: TokenKind) {
3488
        if (this.match(tokenKind)) {
4,141✔
3489
            return this.previous();
417✔
3490
        }
3491
    }
3492

3493
    private consumeToken(tokenKind: TokenKind) {
3494
        return this.consume(
2,494✔
3495
            DiagnosticMessages.expectedToken(tokenKind),
3496
            tokenKind
3497
        );
3498
    }
3499

3500
    /**
3501
     * Consume, or add a message if not found. But then continue and return undefined
3502
     */
3503
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3504
        const nextKind = this.peek().kind;
30,531✔
3505
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
58,204✔
3506

3507
        if (foundTokenKind) {
30,531✔
3508
            return this.advance();
30,397✔
3509
        }
3510
        this.diagnostics.push({
134✔
3511
            ...diagnostic,
3512
            location: this.peek()?.location
402!
3513
        });
3514
    }
3515

3516
    private tryConsumeToken(tokenKind: TokenKind) {
3517
        return this.tryConsume(
98✔
3518
            DiagnosticMessages.expectedToken(tokenKind),
3519
            tokenKind
3520
        );
3521
    }
3522

3523
    private consumeStatementSeparators(optional = false) {
11,635✔
3524
        //a comment or EOF mark the end of the statement
3525
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
35,376✔
3526
            return true;
754✔
3527
        }
3528
        let consumed = false;
34,622✔
3529
        //consume any newlines and colons
3530
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
34,622✔
3531
            consumed = true;
37,467✔
3532
        }
3533
        if (!optional && !consumed) {
34,622✔
3534
            this.diagnostics.push({
69✔
3535
                ...DiagnosticMessages.expectedNewlineOrColon(),
3536
                location: this.peek()?.location
207!
3537
            });
3538
        }
3539
        return consumed;
34,622✔
3540
    }
3541

3542
    private advance(): Token {
3543
        if (!this.isAtEnd()) {
60,093✔
3544
            this.current++;
60,079✔
3545
        }
3546
        return this.previous();
60,093✔
3547
    }
3548

3549
    private checkEndOfStatement(): boolean {
3550
        const nextKind = this.peek().kind;
8,588✔
3551
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
8,588✔
3552
    }
3553

3554
    private checkPrevious(tokenKind: TokenKind): boolean {
3555
        return this.previous()?.kind === tokenKind;
257!
3556
    }
3557

3558
    /**
3559
     * Check that the next token kind is the expected kind
3560
     * @param tokenKind the expected next kind
3561
     * @returns true if the next tokenKind is the expected value
3562
     */
3563
    private check(tokenKind: TokenKind): boolean {
3564
        const nextKind = this.peek().kind;
1,149,651✔
3565
        if (nextKind === TokenKind.Eof) {
1,149,651✔
3566
            return false;
13,422✔
3567
        }
3568
        return nextKind === tokenKind;
1,136,229✔
3569
    }
3570

3571
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3572
        const nextKind = this.peek().kind;
193,718✔
3573
        if (nextKind === TokenKind.Eof) {
193,718✔
3574
            return false;
1,412✔
3575
        }
3576
        return tokenKinds.includes(nextKind);
192,306✔
3577
    }
3578

3579
    private checkNext(tokenKind: TokenKind): boolean {
3580
        if (this.isAtEnd()) {
16,242!
UNCOV
3581
            return false;
×
3582
        }
3583
        return this.peekNext().kind === tokenKind;
16,242✔
3584
    }
3585

3586
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3587
        if (this.isAtEnd()) {
7,115!
UNCOV
3588
            return false;
×
3589
        }
3590
        const nextKind = this.peekNext().kind;
7,115✔
3591
        return tokenKinds.includes(nextKind);
7,115✔
3592
    }
3593

3594
    private isAtEnd(): boolean {
3595
        const peekToken = this.peek();
178,581✔
3596
        return !peekToken || peekToken.kind === TokenKind.Eof;
178,581✔
3597
    }
3598

3599
    private peekNext(): Token {
3600
        if (this.isAtEnd()) {
23,357!
UNCOV
3601
            return this.peek();
×
3602
        }
3603
        return this.tokens[this.current + 1];
23,357✔
3604
    }
3605

3606
    private peek(): Token {
3607
        return this.tokens[this.current];
1,586,805✔
3608
    }
3609

3610
    private previous(): Token {
3611
        return this.tokens[this.current - 1];
102,974✔
3612
    }
3613

3614
    /**
3615
     * Sometimes we catch an error that is a diagnostic.
3616
     * If that's the case, we want to continue parsing.
3617
     * Otherwise, re-throw the error
3618
     *
3619
     * @param error error caught in a try/catch
3620
     */
3621
    private rethrowNonDiagnosticError(error) {
3622
        if (!error.isDiagnostic) {
12!
UNCOV
3623
            throw error;
×
3624
        }
3625
    }
3626

3627
    /**
3628
     * Get the token that is {offset} indexes away from {this.current}
3629
     * @param offset the number of index steps away from current index to fetch
3630
     * @param tokenKinds the desired token must match one of these
3631
     * @example
3632
     * getToken(-1); //returns the previous token.
3633
     * getToken(0);  //returns current token.
3634
     * getToken(1);  //returns next token
3635
     */
3636
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3637
        const token = this.tokens[this.current + offset];
161✔
3638
        if (tokenKinds.includes(token.kind)) {
161✔
3639
            return token;
3✔
3640
        }
3641
    }
3642

3643
    private synchronize() {
3644
        this.advance(); // skip the erroneous token
110✔
3645

3646
        while (!this.isAtEnd()) {
110✔
3647
            if (this.ensureNewLineOrColon(true)) {
222✔
3648
                // end of statement reached
3649
                return;
68✔
3650
            }
3651

3652
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
154✔
3653
                case TokenKind.Namespace:
2!
3654
                case TokenKind.Class:
3655
                case TokenKind.Function:
3656
                case TokenKind.Sub:
3657
                case TokenKind.If:
3658
                case TokenKind.For:
3659
                case TokenKind.ForEach:
3660
                case TokenKind.While:
3661
                case TokenKind.Print:
3662
                case TokenKind.Return:
3663
                    // start parsing again from the next block starter or obvious
3664
                    // expression start
3665
                    return;
1✔
3666
            }
3667

3668
            this.advance();
153✔
3669
        }
3670
    }
3671

3672

3673
    public dispose() {
3674
    }
3675
}
3676

3677
export enum ParseMode {
1✔
3678
    BrightScript = 'BrightScript',
1✔
3679
    BrighterScript = 'BrighterScript'
1✔
3680
}
3681

3682
export interface ParseOptions {
3683
    /**
3684
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3685
     */
3686
    mode?: ParseMode;
3687
    /**
3688
     * A logger that should be used for logging. If omitted, a default logger is used
3689
     */
3690
    logger?: Logger;
3691
    /**
3692
     * Path to the file where this source code originated
3693
     */
3694
    srcPath?: string;
3695
    /**
3696
     * Should locations be tracked. If false, the `range` property will be omitted
3697
     * @default true
3698
     */
3699
    trackLocations?: boolean;
3700
    /**
3701
     *
3702
     */
3703
    bsConsts?: Map<string, boolean>;
3704
}
3705

3706

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

© 2026 Coveralls, Inc