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

rokucommunity / brighterscript / #15219

22 Feb 2026 02:27AM UTC coverage: 87.193% (-0.006%) from 87.199%
#15219

push

web-flow
Merge d0c9a16a7 into 1556715dd

14749 of 17875 branches covered (82.51%)

Branch coverage included in aggregate %.

107 of 117 new or added lines in 19 files covered. (91.45%)

161 existing lines in 16 files now uncovered.

15493 of 16809 relevant lines covered (92.17%)

25604.58 hits per line

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

91.26
/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
    TypedFunctionTypeExpression
101
} from './Expression';
102
import type { Range } from 'vscode-languageserver';
103
import type { Logger } from '../logging';
104
import { createLogger } from '../logging';
1✔
105
import { isAnnotationExpression, isCallExpression, isCallfuncExpression, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression, isConditionalCompileStatement, isLiteralBoolean, isTypecastExpression } from '../astUtils/reflection';
1✔
106
import { createStringLiteral, createToken } from '../astUtils/creators';
1✔
107
import type { Expression, Statement } from './AstNode';
108
import type { BsDiagnostic, DeepWriteable } from '../interfaces';
109

110

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

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

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

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

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

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

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

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

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

158
    private globalTerminators = [] as TokenKind[][];
4,239✔
159

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

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

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

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

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

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

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

222
    private logger: Logger;
223

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

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

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

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

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

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

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

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

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

310
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
16,233✔
311
                return this.functionDeclaration(false);
3,892✔
312
            }
313

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

318
            if (this.checkAlias()) {
12,328✔
319
                return this.aliasStatement();
33✔
320
            }
321
            if (this.checkTypeStatement()) {
12,295✔
322
                return this.typeStatement();
44✔
323
            }
324

325
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
12,251✔
326
                return this.constDeclaration();
193✔
327
            }
328

329
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
12,058✔
330
                return this.annotationExpression();
58✔
331
            }
332

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

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

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

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

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

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

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

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

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

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

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

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

481
        const parentAnnotations = this.enterAnnotationBlock();
230✔
482

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

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

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

513
                let decl: Statement;
514

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

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

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

546
            //ensure statement separator
547
            this.consumeStatementSeparators();
317✔
548
        }
549

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

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

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

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

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

576
        this.consumeStatementSeparators();
187✔
577

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

714
                }
715

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

908
            this.consumeStatementSeparators(true);
4,357✔
909

910

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1029
        return result;
1,745✔
1030
    }
1031

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

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

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

1047
        return result;
76✔
1048
    }
1049

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

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

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

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

1068
    private checkAlias() {
1069
        let isAliasToken = this.check(TokenKind.Alias);
24,165✔
1070

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

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

1080
            //definitely not a alias statement
1081
        } else {
1082
            return false;
24,132✔
1083
        }
1084
    }
1085

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

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

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

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

1104
    private statement(): Statement | undefined {
1105
        if (this.checkLibrary()) {
12,086!
UNCOV
1106
            return this.libraryStatement();
×
1107
        }
1108

1109
        if (this.check(TokenKind.Import)) {
12,086✔
1110
            return this.importStatement();
216✔
1111
        }
1112

1113
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
11,870✔
1114
            return this.typecastStatement();
33✔
1115
        }
1116

1117
        if (this.checkAlias()) {
11,837!
UNCOV
1118
            return this.aliasStatement();
×
1119
        }
1120

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

1125
        if (this.check(TokenKind.If)) {
11,821✔
1126
            return this.ifStatement();
1,275✔
1127
        }
1128

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

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

1138
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
10,493✔
1139
            return this.printStatement();
1,482✔
1140
        }
1141
        if (this.check(TokenKind.Dim)) {
9,011✔
1142
            return this.dimStatement();
43✔
1143
        }
1144

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

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

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

1157
        if (this.check(TokenKind.ForEach)) {
8,867✔
1158
            return this.forEachStatement();
69✔
1159
        }
1160

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

1165
        if (this.match(TokenKind.Return)) {
8,790✔
1166
            return this.returnStatement();
3,762✔
1167
        }
1168

1169
        if (this.check(TokenKind.Goto)) {
5,028✔
1170
            return this.gotoStatement();
12✔
1171
        }
1172

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

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

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

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

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

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

1232
        if (this.check(TokenKind.Namespace)) {
2,282✔
1233
            return this.namespaceStatement();
673✔
1234
        }
1235

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

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

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

1248
        this.consumeStatementSeparators();
32✔
1249

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

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

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

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

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

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

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

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

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

1315
        //TODO: newline allowed?
1316

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

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

1329
        this.consumeStatementSeparators();
45✔
1330

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

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

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

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

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

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

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

1392
        this.consumeStatementSeparators();
69✔
1393

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

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

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

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

1422
        this.namespaceAndFunctionDepth++;
673✔
1423

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

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

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

1442
        this.namespaceAndFunctionDepth--;
672✔
1443

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

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

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

1468
        let expr: DottedGetExpression | VariableExpression;
1469

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

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

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

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

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

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

1554
        return libStatement;
13✔
1555
    }
1556

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

1568
        return importStatement;
216✔
1569
    }
1570

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

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

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

1610
        });
1611

1612
        return aliasStmt;
33✔
1613
    }
1614

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

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

1634
        });
1635

1636
        return typeStmt;
43✔
1637
    }
1638

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1921
    private nestedInlineConditionalCount = 0;
4,239✔
1922

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

1937
        const ifToken = this.advance();
2,362✔
1938

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2034
            thenBranch = this.blockConditionalBranch(ifToken);
2,312✔
2035

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

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

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

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

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

2055
            if (!isIfStatement(elseBranch)) {
2,309✔
2056
                if (this.check(TokenKind.EndIf)) {
1,232✔
2057
                    endIfToken = this.advance();
1,227✔
2058

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

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

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

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

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

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

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

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

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

2121

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2261

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2449
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
971✔
2450
            return new ExpressionStatement({ expression: expr });
589✔
2451
        }
2452

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

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

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

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

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

2513
        let values: Expression[] = [];
1,482✔
2514

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2645
                //ensure statement separator
2646
                this.consumeStatementSeparators();
9,092✔
2647

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

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

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

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

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

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

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

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

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

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

2733
            } while (asToken && typeExpression);
178✔
2734
        }
2735
        return expression;
14,292✔
2736
    }
2737

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

2749
        let expr = this.boolean();
14,218✔
2750

2751
        if (this.check(TokenKind.Question)) {
14,180✔
2752
            return this.ternaryExpression(expr);
98✔
2753
        } else if (this.check(TokenKind.QuestionQuestion)) {
14,082✔
2754
            return this.nullCoalescingExpression(expr);
35✔
2755
        } else {
2756
            return expr;
14,047✔
2757
        }
2758
    }
2759

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

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

2769
        return expr;
14,180✔
2770
    }
2771

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

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

2790
        return expr;
14,245✔
2791
    }
2792

2793
    // TODO: bitshift
2794

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

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

2804
        return expr;
16,191✔
2805
    }
2806

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

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

2823
        return expr;
17,756✔
2824
    }
2825

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

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

2835
        return expr;
17,816✔
2836
    }
2837

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

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

2861

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

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

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

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

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

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

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

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

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

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

2956
        while (true) {
19,018✔
2957
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
24,527✔
2958
                expr = this.finishCall(this.previous(), expr);
2,587✔
2959
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
21,940✔
2960
                expr = this.indexedGet(expr);
159✔
2961
            } else if (this.match(TokenKind.Callfunc)) {
21,781✔
2962
                expr = this.callfunc(expr);
79✔
2963
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
21,702✔
2964
                if (this.match(TokenKind.LeftSquareBracket)) {
2,732✔
2965
                    expr = this.indexedGet(expr);
2✔
2966
                } else {
2967
                    let dot = this.previous();
2,730✔
2968
                    let name = this.tryConsume(
2,730✔
2969
                        DiagnosticMessages.expectedIdentifier(),
2970
                        TokenKind.Identifier,
2971
                        ...AllowedProperties
2972
                    );
2973
                    if (!name) {
2,730✔
2974
                        break;
48✔
2975
                    }
2976

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

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

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

2999
            } else {
3000
                break;
18,959✔
3001
            }
3002
        }
3003

3004
        return expr;
19,018✔
3005
    }
3006

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

3011
        if (!this.check(TokenKind.RightParen)) {
2,828✔
3012
            do {
1,459✔
3013
                while (this.match(TokenKind.Newline)) { }
2,073✔
3014

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

3032
        while (this.match(TokenKind.Newline)) { }
2,828✔
3033

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

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

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

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

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

3088
            combineExpressions(TokenKind.And);
2,318✔
3089
            combineExpressions(TokenKind.Or);
2,318✔
3090

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

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

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

3114
        if (this.checkAny(TokenKind.Sub, TokenKind.Function) && this.checkNext(TokenKind.LeftParen)) {
2,458✔
3115
            // this is a tyyed function type expression, eg. "function(type1, type2) as type3"
3116
            this.warnIfNotBrighterScriptMode('typed function types');
45✔
3117
            expr = this.typedFunctionTypeExpression();
45✔
3118
        } else if (this.checkAny(...DeclarableTypes)) {
2,413✔
3119
            // if this is just a type, just use directly
3120
            expr = new VariableExpression({ name: this.advance() as Identifier });
1,609✔
3121
        } else {
3122
            if (this.options.mode === ParseMode.BrightScript && !declarableTypesLower.includes(this.peek()?.text?.toLowerCase())) {
804!
3123
                // custom types arrays not allowed in Brightscript
3124
                this.warnIfNotBrighterScriptMode('custom types');
17✔
3125
                this.advance(); // skip custom type token
17✔
3126
                return expr;
17✔
3127
            }
3128

3129
            if (this.match(TokenKind.LeftCurlyBrace)) {
787✔
3130
                expr = this.inlineInterface();
61✔
3131
            } else if (this.match(TokenKind.LeftParen)) {
726✔
3132
                // this is a grouping type expression, ie. "(typeExpr)"
3133
                let left = this.previous();
23✔
3134
                let typeExpr = this.typeExpression();
23✔
3135
                let right = this.consume(
23✔
3136
                    DiagnosticMessages.unmatchedLeftToken(left.text, 'type expression'),
3137
                    TokenKind.RightParen
3138
                );
3139
                expr = new GroupingExpression({ leftParen: left, rightParen: right, expression: typeExpr });
23✔
3140
            } else {
3141
                if (this.checkAny(...AllowedTypeIdentifiers)) {
703✔
3142
                    // Since the next token is allowed as a type identifier, change the kind
3143
                    let nextToken = this.peek();
2✔
3144
                    changedTokens.push({ token: nextToken, oldKind: nextToken.kind });
2✔
3145
                    nextToken.kind = TokenKind.Identifier;
2✔
3146
                }
3147
                expr = this.identifyingExpression(AllowedTypeIdentifiers);
703✔
3148
            }
3149
        }
3150

3151
        //Check if it has square brackets, thus making it an array
3152
        if (expr && this.check(TokenKind.LeftSquareBracket)) {
2,438✔
3153
            if (this.options.mode === ParseMode.BrightScript) {
46✔
3154
                // typed arrays not allowed in Brightscript
3155
                this.warnIfNotBrighterScriptMode('typed arrays');
1✔
3156
                return expr;
1✔
3157
            }
3158

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

3171
        return expr;
2,437✔
3172
    }
3173

3174
    private typedFunctionTypeExpression() {
3175
        const funcOrSub = this.advance();
45✔
3176
        const openParen = this.consume(DiagnosticMessages.expectedToken(TokenKind.LeftParen), TokenKind.LeftParen);
45✔
3177
        const params: FunctionParameterExpression[] = [];
45✔
3178

3179
        if (!this.check(TokenKind.RightParen)) {
45✔
3180
            do {
33✔
3181
                if (params.length >= CallExpression.MaximumArguments) {
42!
NEW
3182
                    this.diagnostics.push({
×
3183
                        ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments),
3184
                        location: this.peek().location
3185
                    });
3186
                }
3187

3188
                params.push(this.functionParameter());
42✔
3189
            } while (this.match(TokenKind.Comma));
3190
        }
3191

3192
        const closeParen = this.consume(
45✔
3193
            DiagnosticMessages.unmatchedLeftToken(openParen.text, 'function type expression'),
3194
            TokenKind.RightParen
3195
        );
3196

3197
        let asToken: Token;
3198
        let returnType: TypeExpression;
3199

3200
        if (this.check(TokenKind.As)) {
44✔
3201
            [asToken, returnType] = this.consumeAsTokenAndTypeExpression();
42✔
3202
        }
3203
        return new TypedFunctionTypeExpression({
44✔
3204
            functionType: funcOrSub,
3205
            rightParen: openParen,
3206
            params: params,
3207
            leftParen: closeParen,
3208
            as: asToken,
3209
            returnType: returnType
3210
        });
3211

3212
    }
3213

3214

3215
    private inlineInterface() {
3216
        let expr: InlineInterfaceExpression;
3217
        const openToken = this.previous();
61✔
3218
        const members: InlineInterfaceMemberExpression[] = [];
61✔
3219
        while (this.match(TokenKind.Newline)) { }
61✔
3220
        while (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral, TokenKind.Optional)) {
61✔
3221
            const member = this.inlineInterfaceMember();
70✔
3222
            members.push(member);
70✔
3223
            while (this.matchAny(TokenKind.Comma, TokenKind.Newline)) { }
70✔
3224
        }
3225
        if (!this.check(TokenKind.RightCurlyBrace)) {
61!
UNCOV
3226
            this.diagnostics.push({
×
3227
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
3228
                location: this.peek().location
3229
            });
UNCOV
3230
            throw this.lastDiagnosticAsError();
×
3231
        }
3232
        const closeToken = this.advance();
61✔
3233

3234
        expr = new InlineInterfaceExpression({ open: openToken, members: members, close: closeToken });
61✔
3235
        return expr;
61✔
3236
    }
3237

3238
    private inlineInterfaceMember(): InlineInterfaceMemberExpression {
3239
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
70✔
3240

3241
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral)) {
70!
3242
            if (this.check(TokenKind.As)) {
70!
UNCOV
3243
                if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
×
3244
                    // as <EOL>
3245
                    // `as` is the field name
UNCOV
3246
                } else if (this.checkNext(TokenKind.As)) {
×
3247
                    //  as as ____
3248
                    // first `as` is the field name
UNCOV
3249
                } else if (optionalKeyword) {
×
3250
                    // optional as ____
3251
                    // optional is the field name, `as` starts type
3252
                    // rewind current token
UNCOV
3253
                    optionalKeyword = null;
×
UNCOV
3254
                    this.current--;
×
3255
                }
3256
            }
3257
        } else {
3258
            // no name after `optional` ... optional is the name
3259
            // rewind current token
UNCOV
3260
            optionalKeyword = null;
×
UNCOV
3261
            this.current--;
×
3262
        }
3263

3264
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers, TokenKind.StringLiteral)) {
70!
UNCOV
3265
            this.diagnostics.push({
×
3266
                ...DiagnosticMessages.expectedIdentifier(this.peek().text),
3267
                location: this.peek().location
3268
            });
UNCOV
3269
            throw this.lastDiagnosticAsError();
×
3270
        }
3271
        let name: Token;
3272
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
70✔
3273
            name = this.identifier(...AllowedProperties);
68✔
3274
        } else {
3275
            name = this.advance();
2✔
3276
        }
3277

3278
        let typeExpression: TypeExpression;
3279

3280
        let asToken: Token = null;
70✔
3281
        if (this.check(TokenKind.As)) {
70✔
3282
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
68✔
3283

3284
        }
3285
        return new InlineInterfaceMemberExpression({
70✔
3286
            name: name,
3287
            as: asToken,
3288
            typeExpression: typeExpression,
3289
            optional: optionalKeyword
3290
        });
3291
    }
3292

3293
    private primary(): Expression {
3294
        switch (true) {
19,114✔
3295
            case this.matchAny(
19,114!
3296
                TokenKind.False,
3297
                TokenKind.True,
3298
                TokenKind.Invalid,
3299
                TokenKind.IntegerLiteral,
3300
                TokenKind.LongIntegerLiteral,
3301
                TokenKind.FloatLiteral,
3302
                TokenKind.DoubleLiteral,
3303
                TokenKind.StringLiteral
3304
            ):
3305
                return new LiteralExpression({ value: this.previous() });
8,314✔
3306

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

3311
            //template string
3312
            case this.check(TokenKind.BackTick):
3313
                return this.templateString(false);
47✔
3314

3315
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
3316
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
20,727✔
3317
                return this.templateString(true);
8✔
3318

3319
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3320
                return new VariableExpression({ name: this.previous() as Identifier });
10,008✔
3321

3322
            case this.match(TokenKind.LeftParen):
3323
                let left = this.previous();
60✔
3324
                let expr = this.expression();
60✔
3325
                let right = this.consume(
59✔
3326
                    DiagnosticMessages.unmatchedLeftToken(left.text, 'expression'),
3327
                    TokenKind.RightParen
3328
                );
3329
                return new GroupingExpression({ leftParen: left, rightParen: right, expression: expr });
59✔
3330

3331
            case this.matchAny(TokenKind.LeftSquareBracket):
3332
                return this.arrayLiteral();
174✔
3333

3334
            case this.match(TokenKind.LeftCurlyBrace):
3335
                return this.aaLiteral();
330✔
3336

3337
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
UNCOV
3338
                let token = Object.assign(this.previous(), {
×
3339
                    kind: TokenKind.Identifier
3340
                }) as Identifier;
UNCOV
3341
                return new VariableExpression({ name: token });
×
3342

3343
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
UNCOV
3344
                return this.anonymousFunction();
×
3345

3346
            case this.check(TokenKind.RegexLiteral):
3347
                return this.regexLiteralExpression();
45✔
3348

3349
            default:
3350
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
3351
                if (this.checkAny(...this.peekGlobalTerminators())) {
93!
3352
                    //don't throw a diagnostic, just return undefined
3353

3354
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
3355
                } else {
3356
                    this.diagnostics.push({
93✔
3357
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
3358
                        location: this.peek()?.location
279!
3359
                    });
3360
                    throw this.lastDiagnosticAsError();
93✔
3361
                }
3362
        }
3363
    }
3364

3365
    private arrayLiteral() {
3366
        let elements: Array<Expression> = [];
174✔
3367
        let openingSquare = this.previous();
174✔
3368

3369
        while (this.match(TokenKind.Newline)) {
174✔
3370
        }
3371
        let closingSquare: Token;
3372

3373
        if (!this.match(TokenKind.RightSquareBracket)) {
174✔
3374
            try {
133✔
3375
                elements.push(this.expression());
133✔
3376

3377
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
132✔
3378

3379
                    while (this.match(TokenKind.Newline)) {
193✔
3380

3381
                    }
3382

3383
                    if (this.check(TokenKind.RightSquareBracket)) {
193✔
3384
                        break;
36✔
3385
                    }
3386

3387
                    elements.push(this.expression());
157✔
3388
                }
3389
            } catch (error: any) {
3390
                this.rethrowNonDiagnosticError(error);
2✔
3391
            }
3392

3393
            closingSquare = this.tryConsume(
133✔
3394
                DiagnosticMessages.unmatchedLeftToken(openingSquare.text, 'array literal'),
3395
                TokenKind.RightSquareBracket
3396
            );
3397
        } else {
3398
            closingSquare = this.previous();
41✔
3399
        }
3400

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

3405
    private aaLiteral() {
3406
        let openingBrace = this.previous();
330✔
3407
        let members: Array<AAMemberExpression> = [];
330✔
3408

3409
        let key = () => {
330✔
3410
            let result = {
359✔
3411
                colonToken: null as Token,
3412
                keyToken: null as Token,
3413
                range: null as Range
3414
            };
3415
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
359✔
3416
                result.keyToken = this.identifier(...AllowedProperties);
325✔
3417
            } else if (this.check(TokenKind.StringLiteral)) {
34!
3418
                result.keyToken = this.advance();
34✔
3419
            } else {
UNCOV
3420
                this.diagnostics.push({
×
3421
                    ...DiagnosticMessages.unexpectedAAKey(),
3422
                    location: this.peek().location
3423
                });
UNCOV
3424
                throw this.lastDiagnosticAsError();
×
3425
            }
3426

3427
            result.colonToken = this.consume(
359✔
3428
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3429
                TokenKind.Colon
3430
            );
3431
            result.range = util.createBoundingRange(result.keyToken, result.colonToken);
358✔
3432
            return result;
358✔
3433
        };
3434

3435
        while (this.match(TokenKind.Newline)) { }
330✔
3436
        let closingBrace: Token;
3437
        if (!this.match(TokenKind.RightCurlyBrace)) {
330✔
3438
            let lastAAMember: AAMemberExpression;
3439
            try {
245✔
3440
                let k = key();
245✔
3441
                let expr = this.expression();
245✔
3442
                lastAAMember = new AAMemberExpression({
244✔
3443
                    key: k.keyToken,
3444
                    colon: k.colonToken,
3445
                    value: expr
3446
                });
3447
                members.push(lastAAMember);
244✔
3448

3449
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
244✔
3450
                    // collect comma at end of expression
3451
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
256✔
3452
                        (lastAAMember as DeepWriteable<AAMemberExpression>).tokens.comma = this.previous();
88✔
3453
                    }
3454

3455
                    this.consumeStatementSeparators(true);
256✔
3456

3457
                    if (this.check(TokenKind.RightCurlyBrace)) {
256✔
3458
                        break;
142✔
3459
                    }
3460
                    let k = key();
114✔
3461
                    let expr = this.expression();
113✔
3462
                    lastAAMember = new AAMemberExpression({
113✔
3463
                        key: k.keyToken,
3464
                        colon: k.colonToken,
3465
                        value: expr
3466
                    });
3467
                    members.push(lastAAMember);
113✔
3468

3469
                }
3470
            } catch (error: any) {
3471
                this.rethrowNonDiagnosticError(error);
2✔
3472
            }
3473

3474
            closingBrace = this.tryConsume(
245✔
3475
                DiagnosticMessages.unmatchedLeftToken(openingBrace.text, 'associative array literal'),
3476
                TokenKind.RightCurlyBrace
3477
            );
3478
        } else {
3479
            closingBrace = this.previous();
85✔
3480
        }
3481

3482
        const aaExpr = new AALiteralExpression({ elements: members, open: openingBrace, close: closingBrace });
330✔
3483
        return aaExpr;
330✔
3484
    }
3485

3486
    /**
3487
     * Pop token if we encounter specified token
3488
     */
3489
    private match(tokenKind: TokenKind) {
3490
        if (this.check(tokenKind)) {
73,517✔
3491
            this.current++; //advance
7,241✔
3492
            return true;
7,241✔
3493
        }
3494
        return false;
66,276✔
3495
    }
3496

3497
    /**
3498
     * Pop token if we encounter a token in the specified list
3499
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3500
     */
3501
    private matchAny(...tokenKinds: TokenKind[]) {
3502
        for (let tokenKind of tokenKinds) {
252,321✔
3503
            if (this.check(tokenKind)) {
730,778✔
3504
                this.current++; //advance
66,452✔
3505
                return true;
66,452✔
3506
            }
3507
        }
3508
        return false;
185,869✔
3509
    }
3510

3511
    /**
3512
     * If the next series of tokens matches the given set of tokens, pop them all
3513
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
3514
     */
3515
    private matchSequence(...tokenKinds: TokenKind[]) {
3516
        const endIndex = this.current + tokenKinds.length;
21,784✔
3517
        for (let i = 0; i < tokenKinds.length; i++) {
21,784✔
3518
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
21,808!
3519
                return false;
21,781✔
3520
            }
3521
        }
3522
        this.current = endIndex;
3✔
3523
        return true;
3✔
3524
    }
3525

3526
    /**
3527
     * Get next token matching a specified list, or fail with an error
3528
     */
3529
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
3530
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
22,784✔
3531
        if (token) {
22,784✔
3532
            return token;
22,756✔
3533
        } else {
3534
            let error = new Error(diagnosticInfo.message);
28✔
3535
            (error as any).isDiagnostic = true;
28✔
3536
            throw error;
28✔
3537
        }
3538
    }
3539

3540
    /**
3541
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3542
     */
3543
    private consumeTokenIf(tokenKind: TokenKind) {
3544
        if (this.match(tokenKind)) {
4,346✔
3545
            return this.previous();
423✔
3546
        }
3547
    }
3548

3549
    private consumeToken(tokenKind: TokenKind) {
3550
        return this.consume(
2,811✔
3551
            DiagnosticMessages.expectedToken(tokenKind),
3552
            tokenKind
3553
        );
3554
    }
3555

3556
    /**
3557
     * Consume, or add a message if not found. But then continue and return undefined
3558
     */
3559
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3560
        const nextKind = this.peek().kind;
31,754✔
3561
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
59,780✔
3562

3563
        if (foundTokenKind) {
31,754✔
3564
            return this.advance();
31,615✔
3565
        }
3566
        this.diagnostics.push({
139✔
3567
            ...diagnostic,
3568
            location: this.peek()?.location
417!
3569
        });
3570
    }
3571

3572
    private tryConsumeToken(tokenKind: TokenKind) {
3573
        return this.tryConsume(
98✔
3574
            DiagnosticMessages.expectedToken(tokenKind),
3575
            tokenKind
3576
        );
3577
    }
3578

3579
    private consumeStatementSeparators(optional = false) {
11,941✔
3580
        //a comment or EOF mark the end of the statement
3581
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
36,266✔
3582
            return true;
754✔
3583
        }
3584
        let consumed = false;
35,512✔
3585
        //consume any newlines and colons
3586
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
35,512✔
3587
            consumed = true;
38,239✔
3588
        }
3589
        if (!optional && !consumed) {
35,512✔
3590
            this.diagnostics.push({
69✔
3591
                ...DiagnosticMessages.expectedNewlineOrColon(),
3592
                location: this.peek()?.location
207!
3593
            });
3594
        }
3595
        return consumed;
35,512✔
3596
    }
3597

3598
    private advance(): Token {
3599
        if (!this.isAtEnd()) {
62,362✔
3600
            this.current++;
62,348✔
3601
        }
3602
        return this.previous();
62,362✔
3603
    }
3604

3605
    private checkEndOfStatement(): boolean {
3606
        const nextKind = this.peek().kind;
9,098✔
3607
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
9,098✔
3608
    }
3609

3610
    private checkPrevious(tokenKind: TokenKind): boolean {
3611
        return this.previous()?.kind === tokenKind;
269!
3612
    }
3613

3614
    /**
3615
     * Check that the next token kind is the expected kind
3616
     * @param tokenKind the expected next kind
3617
     * @returns true if the next tokenKind is the expected value
3618
     */
3619
    private check(tokenKind: TokenKind): boolean {
3620
        const nextKind = this.peek().kind;
1,176,850✔
3621
        if (nextKind === TokenKind.Eof) {
1,176,850✔
3622
            return false;
13,630✔
3623
        }
3624
        return nextKind === tokenKind;
1,163,220✔
3625
    }
3626

3627
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3628
        const nextKind = this.peek().kind;
201,616✔
3629
        if (nextKind === TokenKind.Eof) {
201,616✔
3630
            return false;
1,412✔
3631
        }
3632
        return tokenKinds.includes(nextKind);
200,204✔
3633
    }
3634

3635
    private checkNext(tokenKind: TokenKind): boolean {
3636
        if (this.isAtEnd()) {
16,654!
UNCOV
3637
            return false;
×
3638
        }
3639
        return this.peekNext().kind === tokenKind;
16,654✔
3640
    }
3641

3642
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3643
        if (this.isAtEnd()) {
7,275!
UNCOV
3644
            return false;
×
3645
        }
3646
        const nextKind = this.peekNext().kind;
7,275✔
3647
        return tokenKinds.includes(nextKind);
7,275✔
3648
    }
3649

3650
    private isAtEnd(): boolean {
3651
        const peekToken = this.peek();
183,734✔
3652
        return !peekToken || peekToken.kind === TokenKind.Eof;
183,734✔
3653
    }
3654

3655
    private peekNext(): Token {
3656
        if (this.isAtEnd()) {
23,929!
UNCOV
3657
            return this.peek();
×
3658
        }
3659
        return this.tokens[this.current + 1];
23,929✔
3660
    }
3661

3662
    private peek(): Token {
3663
        return this.tokens[this.current];
1,629,241✔
3664
    }
3665

3666
    private previous(): Token {
3667
        return this.tokens[this.current - 1];
105,959✔
3668
    }
3669

3670
    /**
3671
     * Sometimes we catch an error that is a diagnostic.
3672
     * If that's the case, we want to continue parsing.
3673
     * Otherwise, re-throw the error
3674
     *
3675
     * @param error error caught in a try/catch
3676
     */
3677
    private rethrowNonDiagnosticError(error) {
3678
        if (!error.isDiagnostic) {
12!
UNCOV
3679
            throw error;
×
3680
        }
3681
    }
3682

3683
    /**
3684
     * Get the token that is {offset} indexes away from {this.current}
3685
     * @param offset the number of index steps away from current index to fetch
3686
     * @param tokenKinds the desired token must match one of these
3687
     * @example
3688
     * getToken(-1); //returns the previous token.
3689
     * getToken(0);  //returns current token.
3690
     * getToken(1);  //returns next token
3691
     */
3692
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3693
        const token = this.tokens[this.current + offset];
161✔
3694
        if (tokenKinds.includes(token.kind)) {
161✔
3695
            return token;
3✔
3696
        }
3697
    }
3698

3699
    private synchronize() {
3700
        this.advance(); // skip the erroneous token
112✔
3701

3702
        while (!this.isAtEnd()) {
112✔
3703
            if (this.ensureNewLineOrColon(true)) {
231✔
3704
                // end of statement reached
3705
                return;
69✔
3706
            }
3707

3708
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
162✔
3709
                case TokenKind.Namespace:
2!
3710
                case TokenKind.Class:
3711
                case TokenKind.Function:
3712
                case TokenKind.Sub:
3713
                case TokenKind.If:
3714
                case TokenKind.For:
3715
                case TokenKind.ForEach:
3716
                case TokenKind.While:
3717
                case TokenKind.Print:
3718
                case TokenKind.Return:
3719
                    // start parsing again from the next block starter or obvious
3720
                    // expression start
3721
                    return;
1✔
3722
            }
3723

3724
            this.advance();
161✔
3725
        }
3726
    }
3727

3728

3729
    public dispose() {
3730
    }
3731
}
3732

3733
export enum ParseMode {
1✔
3734
    BrightScript = 'BrightScript',
1✔
3735
    BrighterScript = 'BrighterScript'
1✔
3736
}
3737

3738
export interface ParseOptions {
3739
    /**
3740
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3741
     */
3742
    mode?: ParseMode;
3743
    /**
3744
     * A logger that should be used for logging. If omitted, a default logger is used
3745
     */
3746
    logger?: Logger;
3747
    /**
3748
     * Path to the file where this source code originated
3749
     */
3750
    srcPath?: string;
3751
    /**
3752
     * Should locations be tracked. If false, the `range` property will be omitted
3753
     * @default true
3754
     */
3755
    trackLocations?: boolean;
3756
    /**
3757
     *
3758
     */
3759
    bsConsts?: Map<string, boolean>;
3760
}
3761

3762

3763
class CancelStatementError extends Error {
3764
    constructor() {
3765
        super('CancelStatement');
2✔
3766
    }
3767
}
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