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

rokucommunity / brighterscript / #13182

14 Oct 2024 03:33PM UTC coverage: 86.831%. Remained the same
#13182

push

web-flow
Merge 7db2dd331 into d585c29e9

11596 of 14122 branches covered (82.11%)

Branch coverage included in aggregate %.

26 of 27 new or added lines in 6 files covered. (96.3%)

112 existing lines in 4 files now uncovered.

12721 of 13883 relevant lines covered (91.63%)

29829.73 hits per line

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

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

106
export class Parser {
1✔
107
    /**
108
     * The array of tokens passed to `parse()`
109
     */
110
    public tokens = [] as Token[];
3,470✔
111

112
    /**
113
     * The current token index
114
     */
115
    public current: number;
116

117
    /**
118
     * The list of statements for the parsed file
119
     */
120
    public ast = new Body({ statements: [] });
3,470✔
121

122
    public get eofToken(): Token {
123
        const lastToken = this.tokens?.[this.tokens.length - 1];
657!
124
        if (lastToken?.kind === TokenKind.Eof) {
657!
125
            return lastToken;
657✔
126
        }
127
    }
128

129
    /**
130
     * The top-level symbol table for the body of this file.
131
     */
132
    public get symbolTable() {
133
        return this.ast.symbolTable;
12,068✔
134
    }
135

136
    /**
137
     * The list of diagnostics found during the parse process
138
     */
139
    public diagnostics: BsDiagnostic[];
140

141
    /**
142
     * The depth of the calls to function declarations. Helps some checks know if they are at the root or not.
143
     */
144
    private namespaceAndFunctionDepth: number;
145

146
    /**
147
     * The options used to parse the file
148
     */
149
    public options: ParseOptions;
150

151
    private globalTerminators = [] as TokenKind[][];
3,470✔
152

153
    /**
154
     * 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
155
     * based on the parse mode
156
     */
157
    private allowedLocalIdentifiers: TokenKind[];
158

159
    /**
160
     * Annotations collected which should be attached to the next statement
161
     */
162
    private pendingAnnotations: AnnotationExpression[];
163

164
    /**
165
     * Get the currently active global terminators
166
     */
167
    private peekGlobalTerminators() {
168
        return this.globalTerminators[this.globalTerminators.length - 1] ?? [];
16,635✔
169
    }
170

171
    /**
172
     * Static wrapper around creating a new parser and parsing a list of tokens
173
     */
174
    public static parse(toParse: Token[] | string, options?: ParseOptions): Parser {
175
        return new Parser().parse(toParse, options);
3,451✔
176
    }
177

178
    /**
179
     * Parses an array of `Token`s into an abstract syntax tree
180
     * @param toParse the array of tokens to parse. May not contain any whitespace tokens
181
     * @returns the same instance of the parser which contains the diagnostics and statements
182
     */
183
    public parse(toParse: Token[] | string, options?: ParseOptions) {
184
        this.logger = options?.logger ?? createLogger();
3,458✔
185
        options = this.sanitizeParseOptions(options);
3,458✔
186
        this.options = options;
3,458✔
187

188
        let tokens: Token[];
189
        if (typeof toParse === 'string') {
3,458✔
190
            tokens = Lexer.scan(toParse, {
523✔
191
                trackLocations: options.trackLocations,
192
                srcPath: options?.srcPath
1,569!
193
            }).tokens;
194
        } else {
195
            tokens = toParse;
2,935✔
196
        }
197
        this.tokens = tokens;
3,458✔
198
        this.allowedLocalIdentifiers = [
3,458✔
199
            ...AllowedLocalIdentifiers,
200
            //when in plain brightscript mode, the BrighterScript source literals can be used as regular variables
201
            ...(this.options.mode === ParseMode.BrightScript ? BrighterScriptSourceLiterals : [])
3,458✔
202
        ];
203
        this.current = 0;
3,458✔
204
        this.diagnostics = [];
3,458✔
205
        this.namespaceAndFunctionDepth = 0;
3,458✔
206
        this.pendingAnnotations = [];
3,458✔
207

208
        this.ast = this.body();
3,458✔
209
        this.ast.bsConsts = options.bsConsts;
3,458✔
210
        //now that we've built the AST, link every node to its parent
211
        this.ast.link();
3,458✔
212
        return this;
3,458✔
213
    }
214

215
    private logger: Logger;
216

217
    private body() {
218
        const parentAnnotations = this.enterAnnotationBlock();
4,079✔
219

220
        let body = new Body({ statements: [] });
4,079✔
221
        if (this.tokens.length > 0) {
4,079✔
222
            this.consumeStatementSeparators(true);
4,078✔
223

224
            try {
4,078✔
225
                while (
4,078✔
226
                    //not at end of tokens
227
                    !this.isAtEnd() &&
16,561✔
228
                    //the next token is not one of the end terminators
229
                    !this.checkAny(...this.peekGlobalTerminators())
230
                ) {
231
                    let dec = this.declaration();
5,932✔
232
                    if (dec) {
5,932✔
233
                        if (!isAnnotationExpression(dec)) {
5,899✔
234
                            this.consumePendingAnnotations(dec);
5,848✔
235
                            body.statements.push(dec);
5,848✔
236
                            //ensure statement separator
237
                            this.consumeStatementSeparators(false);
5,848✔
238
                        } else {
239
                            this.consumeStatementSeparators(true);
51✔
240
                        }
241
                    }
242
                }
243
            } catch (parseError) {
244
                //do nothing with the parse error for now. perhaps we can remove this?
UNCOV
245
                console.error(parseError);
×
246
            }
247
        }
248

249
        this.exitAnnotationBlock(parentAnnotations);
4,079✔
250
        return body;
4,079✔
251
    }
252

253
    private sanitizeParseOptions(options: ParseOptions) {
254
        options ??= {
3,458✔
255
            srcPath: undefined
256
        };
257
        options.mode ??= ParseMode.BrightScript;
3,458✔
258
        options.trackLocations ??= true;
3,458✔
259
        return options;
3,458✔
260
    }
261

262
    /**
263
     * Determine if the parser is currently parsing tokens at the root level.
264
     */
265
    private isAtRootLevel() {
266
        return this.namespaceAndFunctionDepth === 0;
40,466✔
267
    }
268

269
    /**
270
     * Throws an error if the input file type is not BrighterScript
271
     */
272
    private warnIfNotBrighterScriptMode(featureName: string) {
273
        if (this.options.mode !== ParseMode.BrighterScript) {
2,484✔
274
            let diagnostic = {
169✔
275
                ...DiagnosticMessages.bsFeatureNotSupportedInBrsFiles(featureName),
276
                location: this.peek().location
277
            };
278
            this.diagnostics.push(diagnostic);
169✔
279
        }
280
    }
281

282
    /**
283
     * Throws an exception using the last diagnostic message
284
     */
285
    private lastDiagnosticAsError() {
286
        let error = new Error(this.diagnostics[this.diagnostics.length - 1]?.message ?? 'Unknown error');
94!
287
        (error as any).isDiagnostic = true;
94✔
288
        return error;
94✔
289
    }
290

291
    private declaration(): Statement | AnnotationExpression | undefined {
292
        try {
13,461✔
293
            if (this.checkAny(TokenKind.HashConst)) {
13,461✔
294
                return this.conditionalCompileConstStatement();
21✔
295
            }
296
            if (this.checkAny(TokenKind.HashIf)) {
13,440✔
297
                return this.conditionalCompileStatement();
41✔
298
            }
299
            if (this.checkAny(TokenKind.HashError)) {
13,399✔
300
                return this.conditionalCompileErrorStatement();
10✔
301
            }
302

303
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
13,389✔
304
                return this.functionDeclaration(false);
3,126✔
305
            }
306

307
            if (this.checkLibrary()) {
10,263✔
308
                return this.libraryStatement();
13✔
309
            }
310

311
            if (this.checkAlias()) {
10,250✔
312
                return this.aliasStatement();
33✔
313
            }
314

315
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
10,217✔
316
                return this.constDeclaration();
155✔
317
            }
318

319
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
10,062✔
320
                return this.annotationExpression();
58✔
321
            }
322

323
            //catch certain global terminators to prevent unnecessary lookahead (i.e. like `end namespace`, no need to continue)
324
            if (this.checkAny(...this.peekGlobalTerminators())) {
10,004!
UNCOV
325
                return;
×
326
            }
327

328
            return this.statement();
10,004✔
329
        } catch (error: any) {
330
            //if the error is not a diagnostic, then log the error for debugging purposes
331
            if (!error.isDiagnostic) {
83✔
332
                this.logger.error(error);
1✔
333
            }
334
            this.synchronize();
83✔
335
        }
336
    }
337

338
    /**
339
     * Try to get an identifier. If not found, add diagnostic and return undefined
340
     */
341
    private tryIdentifier(...additionalTokenKinds: TokenKind[]): Identifier | undefined {
342
        const identifier = this.tryConsume(
165✔
343
            DiagnosticMessages.expectedIdentifier(),
344
            TokenKind.Identifier,
345
            ...additionalTokenKinds
346
        ) as Identifier;
347
        if (identifier) {
165✔
348
            // force the name into an identifier so the AST makes some sense
349
            identifier.kind = TokenKind.Identifier;
164✔
350
            return identifier;
164✔
351
        }
352
    }
353

354
    private identifier(...additionalTokenKinds: TokenKind[]) {
355
        const identifier = this.consume(
779✔
356
            DiagnosticMessages.expectedIdentifier(),
357
            TokenKind.Identifier,
358
            ...additionalTokenKinds
359
        ) as Identifier;
360
        // force the name into an identifier so the AST makes some sense
361
        identifier.kind = TokenKind.Identifier;
779✔
362
        return identifier;
779✔
363
    }
364

365
    private enumMemberStatement() {
366
        const name = this.consume(
318✔
367
            DiagnosticMessages.expectedClassFieldIdentifier(),
368
            TokenKind.Identifier,
369
            ...AllowedProperties
370
        ) as Identifier;
371
        let equalsToken: Token;
372
        let value: Expression;
373
        //look for `= SOME_EXPRESSION`
374
        if (this.check(TokenKind.Equal)) {
318✔
375
            equalsToken = this.advance();
176✔
376
            value = this.expression();
176✔
377
        }
378
        const statement = new EnumMemberStatement({ name: name, equals: equalsToken, value: value });
318✔
379
        return statement;
318✔
380
    }
381

382
    /**
383
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration`
384
     */
385
    private interfaceFieldStatement(optionalKeyword?: Token) {
386
        const name = this.identifier(...AllowedProperties);
185✔
387
        let asToken;
388
        let typeExpression;
389
        if (this.check(TokenKind.As)) {
185✔
390
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
167✔
391
        }
392
        return new InterfaceFieldStatement({ name: name, as: asToken, typeExpression: typeExpression, optional: optionalKeyword });
185✔
393
    }
394

395
    private consumeAsTokenAndTypeExpression(ignoreDiagnostics = false): [Token, TypeExpression] {
1,357✔
396
        let asToken = this.consumeToken(TokenKind.As);
1,367✔
397
        let typeExpression: TypeExpression;
398
        if (asToken) {
1,367!
399
            //if there's nothing after the `as`, add a diagnostic and continue
400
            if (this.checkEndOfStatement()) {
1,367✔
401
                if (!ignoreDiagnostics) {
2✔
402
                    this.diagnostics.push({
1✔
403
                        ...DiagnosticMessages.expectedIdentifierAfterKeyword(asToken.text),
404
                        location: asToken.location
405
                    });
406
                }
407
                //consume the statement separator
408
                this.consumeStatementSeparators();
2✔
409
            } else if (this.peek().kind !== TokenKind.Identifier && !this.checkAny(...DeclarableTypes, ...AllowedTypeIdentifiers)) {
1,365✔
410
                if (!ignoreDiagnostics) {
6!
411
                    this.diagnostics.push({
6✔
412
                        ...DiagnosticMessages.expectedIdentifierAfterKeyword(asToken.text),
413
                        location: asToken.location
414
                    });
415
                }
416
            } else {
417
                typeExpression = this.typeExpression();
1,359✔
418
            }
419
        }
420
        return [asToken, typeExpression];
1,367✔
421
    }
422

423
    /**
424
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration()`
425
     */
426
    private interfaceMethodStatement(optionalKeyword?: Token) {
427
        const functionType = this.advance();
42✔
428
        const name = this.identifier(...AllowedProperties);
42✔
429
        const leftParen = this.consume(DiagnosticMessages.expectedToken(TokenKind.LeftParen), TokenKind.LeftParen);
42✔
430

431
        let params = [] as FunctionParameterExpression[];
42✔
432
        if (!this.check(TokenKind.RightParen)) {
42✔
433
            do {
9✔
434
                if (params.length >= CallExpression.MaximumArguments) {
11!
UNCOV
435
                    this.diagnostics.push({
×
436
                        ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments),
437
                        location: this.peek().location
438
                    });
439
                }
440

441
                params.push(this.functionParameter());
11✔
442
            } while (this.match(TokenKind.Comma));
443
        }
444
        const rightParen = this.consumeToken(TokenKind.RightParen);
42✔
445
        // let asToken = null as Token;
446
        // let returnTypeExpression: TypeExpression;
447
        let asToken: Token;
448
        let returnTypeExpression: TypeExpression;
449
        if (this.check(TokenKind.As)) {
42✔
450
            [asToken, returnTypeExpression] = this.consumeAsTokenAndTypeExpression();
30✔
451
        }
452

453
        return new InterfaceMethodStatement({
42✔
454
            functionType: functionType,
455
            name: name,
456
            leftParen: leftParen,
457
            params: params,
458
            rightParen: rightParen,
459
            as: asToken,
460
            returnTypeExpression: returnTypeExpression,
461
            optional: optionalKeyword
462
        });
463
    }
464

465
    private interfaceDeclaration(): InterfaceStatement {
466
        this.warnIfNotBrighterScriptMode('interface declarations');
155✔
467

468
        const parentAnnotations = this.enterAnnotationBlock();
155✔
469

470
        const interfaceToken = this.consume(
155✔
471
            DiagnosticMessages.expectedKeyword(TokenKind.Interface),
472
            TokenKind.Interface
473
        );
474
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
155✔
475

476
        let extendsToken: Token;
477
        let parentInterfaceName: TypeExpression;
478

479
        if (this.peek().text.toLowerCase() === 'extends') {
155✔
480
            extendsToken = this.advance();
8✔
481
            if (this.checkEndOfStatement()) {
8!
UNCOV
482
                this.diagnostics.push({
×
483
                    ...DiagnosticMessages.expectedIdentifierAfterKeyword(extendsToken.text),
484
                    location: extendsToken.location
485
                });
486
            } else {
487
                parentInterfaceName = this.typeExpression();
8✔
488
            }
489
        }
490
        this.consumeStatementSeparators();
155✔
491
        //gather up all interface members (Fields, Methods)
492
        let body = [] as Statement[];
155✔
493
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
155✔
494
            try {
384✔
495
                //break out of this loop if we encountered the `EndInterface` token not followed by `as`
496
                if (this.check(TokenKind.EndInterface) && !this.checkNext(TokenKind.As)) {
384✔
497
                    break;
155✔
498
                }
499

500
                let decl: Statement;
501

502
                //collect leading annotations
503
                if (this.check(TokenKind.At)) {
229✔
504
                    this.annotationExpression();
2✔
505
                }
506
                const optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
229✔
507
                //fields
508
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkAnyNext(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
229✔
509
                    decl = this.interfaceFieldStatement(optionalKeyword);
183✔
510
                    //field with name = 'optional'
511
                } else if (optionalKeyword && this.checkAny(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
46✔
512
                    //rewind one place, so that 'optional' is the field name
513
                    this.current--;
2✔
514
                    decl = this.interfaceFieldStatement();
2✔
515

516
                    //methods (function/sub keyword followed by opening paren)
517
                } else if (this.checkAny(TokenKind.Function, TokenKind.Sub) && this.checkAnyNext(TokenKind.Identifier, ...AllowedProperties)) {
44✔
518
                    decl = this.interfaceMethodStatement(optionalKeyword);
42✔
519

520
                }
521
                if (decl) {
229✔
522
                    this.consumePendingAnnotations(decl);
227✔
523
                    body.push(decl);
227✔
524
                } else {
525
                    //we didn't find a declaration...flag tokens until next line
526
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
527
                }
528
            } catch (e) {
529
                //throw out any failed members and move on to the next line
UNCOV
530
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
531
            }
532

533
            //ensure statement separator
534
            this.consumeStatementSeparators();
229✔
535
        }
536

537
        //consume the final `end interface` token
538
        const endInterfaceToken = this.consumeToken(TokenKind.EndInterface);
155✔
539

540
        const statement = new InterfaceStatement({
155✔
541
            interface: interfaceToken,
542
            name: nameToken,
543
            extends: extendsToken,
544
            parentInterfaceName: parentInterfaceName,
545
            body: body,
546
            endInterface: endInterfaceToken
547
        });
548
        this.exitAnnotationBlock(parentAnnotations);
155✔
549
        return statement;
155✔
550
    }
551

552
    private enumDeclaration(): EnumStatement {
553
        const enumToken = this.consume(
165✔
554
            DiagnosticMessages.expectedKeyword(TokenKind.Enum),
555
            TokenKind.Enum
556
        );
557
        const nameToken = this.tryIdentifier(...this.allowedLocalIdentifiers);
165✔
558

559
        this.warnIfNotBrighterScriptMode('enum declarations');
165✔
560

561
        const parentAnnotations = this.enterAnnotationBlock();
165✔
562

563
        this.consumeStatementSeparators();
165✔
564

565
        const body: Array<EnumMemberStatement> = [];
165✔
566
        //gather up all members
567
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
165✔
568
            try {
318✔
569
                let decl: EnumMemberStatement;
570

571
                //collect leading annotations
572
                if (this.check(TokenKind.At)) {
318!
UNCOV
573
                    this.annotationExpression();
×
574
                }
575

576
                //members
577
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
318!
578
                    decl = this.enumMemberStatement();
318✔
579
                }
580

581
                if (decl) {
318!
582
                    this.consumePendingAnnotations(decl);
318✔
583
                    body.push(decl);
318✔
584
                } else {
585
                    //we didn't find a declaration...flag tokens until next line
UNCOV
586
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
587
                }
588
            } catch (e) {
589
                //throw out any failed members and move on to the next line
UNCOV
590
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
591
            }
592

593
            //ensure statement separator
594
            this.consumeStatementSeparators();
318✔
595
            //break out of this loop if we encountered the `EndEnum` token
596
            if (this.check(TokenKind.EndEnum)) {
318✔
597
                break;
156✔
598
            }
599
        }
600

601
        //consume the final `end interface` token
602
        const endEnumToken = this.consumeToken(TokenKind.EndEnum);
165✔
603

604
        const result = new EnumStatement({
164✔
605
            enum: enumToken,
606
            name: nameToken,
607
            body: body,
608
            endEnum: endEnumToken
609
        });
610

611
        this.exitAnnotationBlock(parentAnnotations);
164✔
612
        return result;
164✔
613
    }
614

615
    /**
616
     * A BrighterScript class declaration
617
     */
618
    private classDeclaration(): ClassStatement {
619
        this.warnIfNotBrighterScriptMode('class declarations');
675✔
620

621
        const parentAnnotations = this.enterAnnotationBlock();
675✔
622

623
        let classKeyword = this.consume(
675✔
624
            DiagnosticMessages.expectedKeyword(TokenKind.Class),
625
            TokenKind.Class
626
        );
627
        let extendsKeyword: Token;
628
        let parentClassName: TypeExpression;
629

630
        //get the class name
631
        let className = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('class'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
675✔
632

633
        //see if the class inherits from parent
634
        if (this.peek().text.toLowerCase() === 'extends') {
675✔
635
            extendsKeyword = this.advance();
101✔
636
            if (this.checkEndOfStatement()) {
101✔
637
                this.diagnostics.push({
1✔
638
                    ...DiagnosticMessages.expectedIdentifierAfterKeyword(extendsKeyword.text),
639
                    location: extendsKeyword.location
640
                });
641
            } else {
642
                parentClassName = this.typeExpression();
100✔
643
            }
644
        }
645

646
        //ensure statement separator
647
        this.consumeStatementSeparators();
675✔
648

649
        //gather up all class members (Fields, Methods)
650
        let body = [] as Statement[];
675✔
651
        while (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private, TokenKind.Function, TokenKind.Sub, TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
675✔
652
            try {
695✔
653
                let decl: Statement;
654
                let accessModifier: Token;
655

656
                if (this.check(TokenKind.At)) {
695✔
657
                    this.annotationExpression();
15✔
658
                }
659

660
                if (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private)) {
694✔
661
                    //use actual access modifier
662
                    accessModifier = this.advance();
96✔
663
                }
664

665
                let overrideKeyword: Token;
666
                if (this.peek().text.toLowerCase() === 'override') {
694✔
667
                    overrideKeyword = this.advance();
17✔
668
                }
669
                //methods (function/sub keyword OR identifier followed by opening paren)
670
                if (this.checkAny(TokenKind.Function, TokenKind.Sub) || (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.LeftParen))) {
694✔
671
                    const funcDeclaration = this.functionDeclaration(false, false);
356✔
672

673
                    //if we have an overrides keyword AND this method is called 'new', that's not allowed
674
                    if (overrideKeyword && funcDeclaration.tokens.name.text.toLowerCase() === 'new') {
356!
UNCOV
675
                        this.diagnostics.push({
×
676
                            ...DiagnosticMessages.cannotUseOverrideKeywordOnConstructorFunction(),
677
                            location: overrideKeyword.location
678
                        });
679
                    }
680

681
                    decl = new MethodStatement({
356✔
682
                        modifiers: accessModifier,
683
                        name: funcDeclaration.tokens.name,
684
                        func: funcDeclaration.func,
685
                        override: overrideKeyword
686
                    });
687

688
                    //fields
689
                } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
338✔
690

691
                    decl = this.fieldDeclaration(accessModifier);
324✔
692

693
                    //class fields cannot be overridden
694
                    if (overrideKeyword) {
323!
UNCOV
695
                        this.diagnostics.push({
×
696
                            ...DiagnosticMessages.classFieldCannotBeOverridden(),
697
                            location: overrideKeyword.location
698
                        });
699
                    }
700

701
                }
702

703
                if (decl) {
693✔
704
                    this.consumePendingAnnotations(decl);
679✔
705
                    body.push(decl);
679✔
706
                }
707
            } catch (e) {
708
                //throw out any failed members and move on to the next line
709
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
710
            }
711

712
            //ensure statement separator
713
            this.consumeStatementSeparators();
695✔
714
        }
715

716
        let endingKeyword = this.advance();
675✔
717
        if (endingKeyword.kind !== TokenKind.EndClass) {
675✔
718
            this.diagnostics.push({
4✔
719
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('class'),
720
                location: endingKeyword.location
721
            });
722
        }
723

724
        const result = new ClassStatement({
675✔
725
            class: classKeyword,
726
            name: className,
727
            body: body,
728
            endClass: endingKeyword,
729
            extends: extendsKeyword,
730
            parentClassName: parentClassName
731
        });
732

733
        this.exitAnnotationBlock(parentAnnotations);
675✔
734
        return result;
675✔
735
    }
736

737
    private fieldDeclaration(accessModifier: Token | null) {
738

739
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
324✔
740

741
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
324✔
742
            if (this.check(TokenKind.As)) {
323✔
743
                if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
5✔
744
                    // as <EOL>
745
                    // `as` is the field name
746
                } else if (this.checkNext(TokenKind.As)) {
4✔
747
                    //  as as ____
748
                    // first `as` is the field name
749
                } else if (optionalKeyword) {
2!
750
                    // optional as ____
751
                    // optional is the field name, `as` starts type
752
                    // rewind current token
753
                    optionalKeyword = null;
2✔
754
                    this.current--;
2✔
755
                }
756
            }
757
        } else {
758
            // no name after `optional` ... optional is the name
759
            // rewind current token
760
            optionalKeyword = null;
1✔
761
            this.current--;
1✔
762
        }
763

764
        let name = this.consume(
324✔
765
            DiagnosticMessages.expectedClassFieldIdentifier(),
766
            TokenKind.Identifier,
767
            ...AllowedProperties
768
        ) as Identifier;
769

770
        let asToken: Token;
771
        let fieldTypeExpression: TypeExpression;
772
        //look for `as SOME_TYPE`
773
        if (this.check(TokenKind.As)) {
324✔
774
            [asToken, fieldTypeExpression] = this.consumeAsTokenAndTypeExpression();
217✔
775
        }
776

777
        let initialValue: Expression;
778
        let equal: Token;
779
        //if there is a field initializer
780
        if (this.check(TokenKind.Equal)) {
324✔
781
            equal = this.advance();
79✔
782
            initialValue = this.expression();
79✔
783
        }
784

785
        return new FieldStatement({
323✔
786
            accessModifier: accessModifier,
787
            name: name,
788
            as: asToken,
789
            typeExpression: fieldTypeExpression,
790
            equals: equal,
791
            initialValue: initialValue,
792
            optional: optionalKeyword
793
        });
794
    }
795

796
    /**
797
     * An array of CallExpression for the current function body
798
     */
799
    private callExpressions = [];
3,470✔
800

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

833
            if (isAnonymous) {
3,567✔
834
                leftParen = this.consume(
85✔
835
                    DiagnosticMessages.expectedLeftParenAfterCallable(functionTypeText),
836
                    TokenKind.LeftParen
837
                );
838
            } else {
839
                name = this.consume(
3,482✔
840
                    DiagnosticMessages.expectedNameAfterCallableKeyword(functionTypeText),
841
                    TokenKind.Identifier,
842
                    ...AllowedProperties
843
                ) as Identifier;
844
                leftParen = this.consume(
3,480✔
845
                    DiagnosticMessages.expectedLeftParenAfterCallableName(functionTypeText),
846
                    TokenKind.LeftParen
847
                );
848

849
                //prevent functions from ending with type designators
850
                let lastChar = name.text[name.text.length - 1];
3,479✔
851
                if (['$', '%', '!', '#', '&'].includes(lastChar)) {
3,479✔
852
                    //don't throw this error; let the parser continue
853
                    this.diagnostics.push({
8✔
854
                        ...DiagnosticMessages.functionNameCannotEndWithTypeDesignator(functionTypeText, name.text, lastChar),
855
                        location: name.location
856
                    });
857
                }
858

859
                //flag functions with keywords for names (only for standard functions)
860
                if (checkIdentifier && DisallowedFunctionIdentifiersText.has(name.text.toLowerCase())) {
3,479✔
861
                    this.diagnostics.push({
1✔
862
                        ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
863
                        location: name.location
864
                    });
865
                }
866
            }
867

868
            let params = [] as FunctionParameterExpression[];
3,564✔
869
            let asToken: Token;
870
            let typeExpression: TypeExpression;
871
            if (!this.check(TokenKind.RightParen)) {
3,564✔
872
                do {
1,624✔
873
                    params.push(this.functionParameter());
2,881✔
874
                } while (this.match(TokenKind.Comma));
875
            }
876
            let rightParen = this.advance();
3,564✔
877

878
            if (this.check(TokenKind.As)) {
3,564✔
879
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
279✔
880
            }
881

882
            params.reduce((haveFoundOptional: boolean, param: FunctionParameterExpression) => {
3,564✔
883
                if (haveFoundOptional && !param.defaultValue) {
2,881!
UNCOV
884
                    this.diagnostics.push({
×
885
                        ...DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter(param.tokens.name.text),
886
                        location: param.location
887
                    });
888
                }
889

890
                return haveFoundOptional || !!param.defaultValue;
2,881✔
891
            }, false);
892

893
            this.consumeStatementSeparators(true);
3,564✔
894

895

896
            //support ending the function with `end sub` OR `end function`
897
            let body = this.block();
3,564✔
898
            //if the parser was unable to produce a block, make an empty one so the AST makes some sense...
899

900
            // consume 'end sub' or 'end function'
901
            const endFunctionType = this.advance();
3,564✔
902
            let expectedEndKind = isSub ? TokenKind.EndSub : TokenKind.EndFunction;
3,564✔
903

904
            //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
905
            //add an error but don't hard-fail so the AST can continue more gracefully
906
            if (endFunctionType.kind !== expectedEndKind) {
3,564✔
907
                this.diagnostics.push({
9✔
908
                    ...DiagnosticMessages.mismatchedEndCallableKeyword(functionTypeText, endFunctionType.text),
909
                    location: endFunctionType.location
910
                });
911
            }
912

913
            if (!body) {
3,564✔
914
                body = new Block({ statements: [] });
3✔
915
            }
916

917
            let func = new FunctionExpression({
3,564✔
918
                parameters: params,
919
                body: body,
920
                functionType: functionType,
921
                endFunctionType: endFunctionType,
922
                leftParen: leftParen,
923
                rightParen: rightParen,
924
                as: asToken,
925
                returnTypeExpression: typeExpression
926
            });
927

928
            if (isAnonymous) {
3,564✔
929
                return func;
85✔
930
            } else {
931
                let result = new FunctionStatement({ name: name, func: func });
3,479✔
932
                return result;
3,479✔
933
            }
934
        } finally {
935
            this.namespaceAndFunctionDepth--;
3,567✔
936
            //restore the previous CallExpression list
937
            this.callExpressions = previousCallExpressions;
3,567✔
938
        }
939
    }
940

941
    private functionParameter(): FunctionParameterExpression {
942
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
2,892!
UNCOV
943
            this.diagnostics.push({
×
944
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
945
                location: this.peek().location
946
            });
UNCOV
947
            throw this.lastDiagnosticAsError();
×
948
        }
949

950
        let name = this.advance() as Identifier;
2,892✔
951
        // force the name into an identifier so the AST makes some sense
952
        name.kind = TokenKind.Identifier;
2,892✔
953

954
        let typeExpression: TypeExpression;
955
        let defaultValue;
956
        let equalToken: Token;
957
        // parse argument default value
958
        if ((equalToken = this.consumeTokenIf(TokenKind.Equal))) {
2,892✔
959
            // it seems any expression is allowed here -- including ones that operate on other arguments!
960
            defaultValue = this.expression(false);
355✔
961
        }
962

963
        let asToken: Token = null;
2,892✔
964
        if (this.check(TokenKind.As)) {
2,892✔
965
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
588✔
966

967
        }
968
        return new FunctionParameterExpression({
2,892✔
969
            name: name,
970
            equals: equalToken,
971
            defaultValue: defaultValue,
972
            as: asToken,
973
            typeExpression: typeExpression
974
        });
975
    }
976

977
    private assignment(allowTypedAssignment = false): AssignmentStatement {
1,439✔
978
        let name = this.advance() as Identifier;
1,448✔
979
        //add diagnostic if name is a reserved word that cannot be used as an identifier
980
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
1,448✔
981
            this.diagnostics.push({
12✔
982
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
983
                location: name.location
984
            });
985
        }
986
        let asToken: Token;
987
        let typeExpression: TypeExpression;
988

989
        if (allowTypedAssignment) {
1,448✔
990
            //look for `as SOME_TYPE`
991
            if (this.check(TokenKind.As)) {
9!
992
                this.warnIfNotBrighterScriptMode('typed assignment');
9✔
993

994
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
9✔
995
            }
996
        }
997

998
        let operator = this.consume(
1,448✔
999
            DiagnosticMessages.expectedOperatorAfterIdentifier([TokenKind.Equal], name.text),
1000
            ...[TokenKind.Equal]
1001
        );
1002
        let value = this.expression();
1,445✔
1003

1004
        let result = new AssignmentStatement({ equals: operator, name: name, value: value, as: asToken, typeExpression: typeExpression });
1,438✔
1005

1006
        return result;
1,438✔
1007
    }
1008

1009
    private augmentedAssignment(): AugmentedAssignmentStatement {
1010
        let item = this.expression();
60✔
1011

1012
        let operator = this.consume(
60✔
1013
            DiagnosticMessages.expectedToken(...CompoundAssignmentOperators),
1014
            ...CompoundAssignmentOperators
1015
        );
1016
        let value = this.expression();
60✔
1017

1018
        let result = new AugmentedAssignmentStatement({
60✔
1019
            item: item,
1020
            operator: operator,
1021
            value: value
1022
        });
1023

1024
        return result;
60✔
1025
    }
1026

1027
    private checkLibrary() {
1028
        let isLibraryToken = this.check(TokenKind.Library);
20,353✔
1029

1030
        //if we are at the top level, any line that starts with "library" should be considered a library statement
1031
        if (this.isAtRootLevel() && isLibraryToken) {
20,353✔
1032
            return true;
12✔
1033

1034
            //not at root level, library statements are all invalid here, but try to detect if the tokens look
1035
            //like a library statement (and let the libraryStatement function handle emitting the diagnostics)
1036
        } else if (isLibraryToken && this.checkNext(TokenKind.StringLiteral)) {
20,341✔
1037
            return true;
1✔
1038

1039
            //definitely not a library statement
1040
        } else {
1041
            return false;
20,340✔
1042
        }
1043
    }
1044

1045
    private checkAlias() {
1046
        let isAliasToken = this.check(TokenKind.Alias);
20,113✔
1047

1048
        //if we are at the top level, any line that starts with "alias" should be considered a alias statement
1049
        if (this.isAtRootLevel() && isAliasToken) {
20,113✔
1050
            return true;
31✔
1051

1052
            //not at root level, alias statements are all invalid here, but try to detect if the tokens look
1053
            //like a alias statement (and let the alias function handle emitting the diagnostics)
1054
        } else if (isAliasToken && this.checkNext(TokenKind.Identifier)) {
20,082✔
1055
            return true;
2✔
1056

1057
            //definitely not a alias statement
1058
        } else {
1059
            return false;
20,080✔
1060
        }
1061
    }
1062

1063
    private statement(): Statement | undefined {
1064
        if (this.checkLibrary()) {
10,090!
UNCOV
1065
            return this.libraryStatement();
×
1066
        }
1067

1068
        if (this.check(TokenKind.Import)) {
10,090✔
1069
            return this.importStatement();
203✔
1070
        }
1071

1072
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
9,887✔
1073
            return this.typecastStatement();
24✔
1074
        }
1075

1076
        if (this.checkAlias()) {
9,863!
UNCOV
1077
            return this.aliasStatement();
×
1078
        }
1079

1080
        if (this.check(TokenKind.Stop)) {
9,863✔
1081
            return this.stopStatement();
16✔
1082
        }
1083

1084
        if (this.check(TokenKind.If)) {
9,847✔
1085
            return this.ifStatement();
1,067✔
1086
        }
1087

1088
        //`try` must be followed by a block, otherwise it could be a local variable
1089
        if (this.check(TokenKind.Try) && this.checkAnyNext(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
8,780✔
1090
            return this.tryCatchStatement();
35✔
1091
        }
1092

1093
        if (this.check(TokenKind.Throw)) {
8,745✔
1094
            return this.throwStatement();
12✔
1095
        }
1096

1097
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
8,733✔
1098
            return this.printStatement();
1,149✔
1099
        }
1100
        if (this.check(TokenKind.Dim)) {
7,584✔
1101
            return this.dimStatement();
43✔
1102
        }
1103

1104
        if (this.check(TokenKind.While)) {
7,541✔
1105
            return this.whileStatement();
32✔
1106
        }
1107

1108
        if (this.checkAny(TokenKind.Exit, TokenKind.ExitWhile)) {
7,509✔
1109
            return this.exitStatement();
22✔
1110
        }
1111

1112
        if (this.check(TokenKind.For)) {
7,487✔
1113
            return this.forStatement();
39✔
1114
        }
1115

1116
        if (this.check(TokenKind.ForEach)) {
7,448✔
1117
            return this.forEachStatement();
36✔
1118
        }
1119

1120
        if (this.check(TokenKind.End)) {
7,412✔
1121
            return this.endStatement();
8✔
1122
        }
1123

1124
        if (this.match(TokenKind.Return)) {
7,404✔
1125
            return this.returnStatement();
3,135✔
1126
        }
1127

1128
        if (this.check(TokenKind.Goto)) {
4,269✔
1129
            return this.gotoStatement();
12✔
1130
        }
1131

1132
        //the continue keyword (followed by `for`, `while`, or a statement separator)
1133
        if (this.check(TokenKind.Continue) && this.checkAnyNext(TokenKind.While, TokenKind.For, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
4,257✔
1134
            return this.continueStatement();
12✔
1135
        }
1136

1137
        //does this line look like a label? (i.e.  `someIdentifier:` )
1138
        if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
4,245✔
1139
            try {
12✔
1140
                return this.labelStatement();
12✔
1141
            } catch (err) {
1142
                if (!(err instanceof CancelStatementError)) {
2!
UNCOV
1143
                    throw err;
×
1144
                }
1145
                //not a label, try something else
1146
            }
1147
        }
1148

1149
        // BrightScript is like python, in that variables can be declared without a `var`,
1150
        // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
1151
        // out what to do with it.
1152
        if (
4,235✔
1153
            this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)
1154
        ) {
1155
            if (this.checkAnyNext(...AssignmentOperators)) {
4,006✔
1156
                if (this.checkAnyNext(...CompoundAssignmentOperators)) {
1,440✔
1157
                    return this.augmentedAssignment();
60✔
1158
                }
1159
                return this.assignment();
1,380✔
1160
            } else if (this.checkNext(TokenKind.As)) {
2,566✔
1161
                // may be a typed assignment
1162
                const backtrack = this.current;
10✔
1163
                let validTypeExpression = false;
10✔
1164

1165
                try {
10✔
1166
                    // skip the identifier, and check for valid type expression
1167
                    this.advance();
10✔
1168
                    const parts = this.consumeAsTokenAndTypeExpression(true);
10✔
1169
                    validTypeExpression = !!(parts?.[0] && parts?.[1]);
10!
1170
                } catch (e) {
1171
                    // ignore any errors
1172
                } finally {
1173
                    this.current = backtrack;
10✔
1174
                }
1175
                if (validTypeExpression) {
10✔
1176
                    // there is a valid 'as' and type expression
1177
                    return this.assignment(true);
9✔
1178
                }
1179
            }
1180
        }
1181

1182
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1183
        if (this.check(TokenKind.Interface)) {
2,786✔
1184
            return this.interfaceDeclaration();
155✔
1185
        }
1186

1187
        if (this.check(TokenKind.Class)) {
2,631✔
1188
            return this.classDeclaration();
675✔
1189
        }
1190

1191
        if (this.check(TokenKind.Namespace)) {
1,956✔
1192
            return this.namespaceStatement();
622✔
1193
        }
1194

1195
        if (this.check(TokenKind.Enum)) {
1,334✔
1196
            return this.enumDeclaration();
165✔
1197
        }
1198

1199
        // TODO: support multi-statements
1200
        return this.setStatement();
1,169✔
1201
    }
1202

1203
    private whileStatement(): WhileStatement {
1204
        const whileKeyword = this.advance();
32✔
1205
        const condition = this.expression();
32✔
1206

1207
        this.consumeStatementSeparators();
31✔
1208

1209
        const whileBlock = this.block(TokenKind.EndWhile);
31✔
1210
        let endWhile: Token;
1211
        if (!whileBlock || this.peek().kind !== TokenKind.EndWhile) {
31✔
1212
            this.diagnostics.push({
1✔
1213
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('while'),
1214
                location: this.peek().location
1215
            });
1216
            if (!whileBlock) {
1!
UNCOV
1217
                throw this.lastDiagnosticAsError();
×
1218
            }
1219
        } else {
1220
            endWhile = this.advance();
30✔
1221
        }
1222

1223
        return new WhileStatement({
31✔
1224
            while: whileKeyword,
1225
            endWhile: endWhile,
1226
            condition: condition,
1227
            body: whileBlock
1228
        });
1229
    }
1230

1231
    private exitStatement(): ExitStatement {
1232
        let exitToken = this.advance();
22✔
1233
        if (exitToken.kind === TokenKind.ExitWhile) {
22✔
1234
            // `exitwhile` is allowed in code, and means `exit while`
1235
            // use an ExitStatement that is nicer to work with by breaking the `exit` and `while` tokens apart
1236

1237
            const exitText = exitToken.text.substring(0, 4);
5✔
1238
            const whileText = exitToken.text.substring(4);
5✔
1239
            const originalRange = exitToken.location.range;
5✔
1240
            const originalStart = originalRange.start;
4✔
1241

1242
            const exitRange = util.createRange(
4✔
1243
                originalStart.line,
1244
                originalStart.character,
1245
                originalStart.line,
1246
                originalStart.character + 4);
1247
            const whileRange = util.createRange(
4✔
1248
                originalStart.line,
1249
                originalStart.character + 4,
1250
                originalStart.line,
1251
                originalStart.character + exitToken.text.length);
1252

1253
            exitToken = createToken(TokenKind.Exit, exitText, util.createLocationFromRange(exitToken.location.uri, exitRange));
4✔
1254
            this.tokens[this.current - 1] = exitToken;
4✔
1255
            const newLoopToken = createToken(TokenKind.While, whileText, util.createLocationFromRange(exitToken.location.uri, whileRange));
4✔
1256
            this.tokens.splice(this.current, 0, newLoopToken);
4✔
1257
        }
1258

1259
        const loopTypeToken = this.tryConsume(
21✔
1260
            DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
1261
            TokenKind.While, TokenKind.For
1262
        );
1263

1264
        return new ExitStatement({
21✔
1265
            exit: exitToken,
1266
            loopType: loopTypeToken
1267
        });
1268
    }
1269

1270
    private forStatement(): ForStatement {
1271
        const forToken = this.advance();
39✔
1272
        const initializer = this.assignment();
39✔
1273

1274
        //TODO: newline allowed?
1275

1276
        const toToken = this.advance();
38✔
1277
        const finalValue = this.expression();
38✔
1278
        let incrementExpression: Expression | undefined;
1279
        let stepToken: Token | undefined;
1280

1281
        if (this.check(TokenKind.Step)) {
38✔
1282
            stepToken = this.advance();
10✔
1283
            incrementExpression = this.expression();
10✔
1284
        } else {
1285
            // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
1286
        }
1287

1288
        this.consumeStatementSeparators();
38✔
1289

1290
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
38✔
1291
        let endForToken: Token;
1292
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
38✔
1293
            this.diagnostics.push({
1✔
1294
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1295
                location: this.peek().location
1296
            });
1297
            if (!body) {
1!
UNCOV
1298
                throw this.lastDiagnosticAsError();
×
1299
            }
1300
        } else {
1301
            endForToken = this.advance();
37✔
1302
        }
1303

1304
        // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
1305
        // stays around in scope with whatever value it was when the loop exited.
1306
        return new ForStatement({
38✔
1307
            for: forToken,
1308
            counterDeclaration: initializer,
1309
            to: toToken,
1310
            finalValue: finalValue,
1311
            body: body,
1312
            endFor: endForToken,
1313
            step: stepToken,
1314
            increment: incrementExpression
1315
        });
1316
    }
1317

1318
    private forEachStatement(): ForEachStatement {
1319
        let forEach = this.advance();
36✔
1320
        let name = this.advance();
36✔
1321

1322
        let maybeIn = this.peek();
36✔
1323
        if (this.check(TokenKind.Identifier) && maybeIn.text.toLowerCase() === 'in') {
36!
1324
            this.advance();
36✔
1325
        } else {
UNCOV
1326
            this.diagnostics.push({
×
1327
                ...DiagnosticMessages.expectedInAfterForEach(name.text),
1328
                location: this.peek().location
1329
            });
UNCOV
1330
            throw this.lastDiagnosticAsError();
×
1331
        }
1332
        maybeIn.kind = TokenKind.In;
36✔
1333

1334
        let target = this.expression();
36✔
1335
        if (!target) {
36!
UNCOV
1336
            this.diagnostics.push({
×
1337
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1338
                location: this.peek().location
1339
            });
UNCOV
1340
            throw this.lastDiagnosticAsError();
×
1341
        }
1342

1343
        this.consumeStatementSeparators();
36✔
1344

1345
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
36✔
1346
        if (!body) {
36!
UNCOV
1347
            this.diagnostics.push({
×
1348
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1349
                location: this.peek().location
1350
            });
UNCOV
1351
            throw this.lastDiagnosticAsError();
×
1352
        }
1353

1354
        let endFor = this.advance();
36✔
1355

1356
        return new ForEachStatement({
36✔
1357
            forEach: forEach,
1358
            in: maybeIn,
1359
            endFor: endFor,
1360
            item: name,
1361
            target: target,
1362
            body: body
1363
        });
1364
    }
1365

1366
    private namespaceStatement(): NamespaceStatement | undefined {
1367
        this.warnIfNotBrighterScriptMode('namespace');
622✔
1368
        let keyword = this.advance();
622✔
1369

1370
        this.namespaceAndFunctionDepth++;
622✔
1371

1372
        let name = this.identifyingExpression();
622✔
1373
        //set the current namespace name
1374

1375
        this.globalTerminators.push([TokenKind.EndNamespace]);
621✔
1376
        let body = this.body();
621✔
1377
        this.globalTerminators.pop();
621✔
1378

1379
        let endKeyword: Token;
1380
        if (this.check(TokenKind.EndNamespace)) {
621✔
1381
            endKeyword = this.advance();
619✔
1382
        } else {
1383
            //the `end namespace` keyword is missing. add a diagnostic, but keep parsing
1384
            this.diagnostics.push({
2✔
1385
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'),
1386
                location: keyword.location
1387
            });
1388
        }
1389

1390
        this.namespaceAndFunctionDepth--;
621✔
1391

1392
        let result = new NamespaceStatement({
621✔
1393
            namespace: keyword,
1394
            nameExpression: name,
1395
            body: body,
1396
            endNamespace: endKeyword
1397
        });
1398

1399
        //cache the range property so that plugins can't affect it
1400
        result.cacheLocation();
621✔
1401
        result.body.symbolTable.name += `: namespace '${result.name}'`;
621✔
1402
        return result;
621✔
1403
    }
1404

1405
    /**
1406
     * Get an expression with identifiers separated by periods. Useful for namespaces and class extends
1407
     */
1408
    private identifyingExpression(allowedTokenKinds?: TokenKind[]): DottedGetExpression | VariableExpression {
1409
        allowedTokenKinds = allowedTokenKinds ?? this.allowedLocalIdentifiers;
1,274✔
1410
        let firstIdentifier = this.consume(
1,274✔
1411
            DiagnosticMessages.expectedIdentifierAfterKeyword(this.previous().text),
1412
            TokenKind.Identifier,
1413
            ...allowedTokenKinds
1414
        ) as Identifier;
1415

1416
        let expr: DottedGetExpression | VariableExpression;
1417

1418
        if (firstIdentifier) {
1,273!
1419
            // force it into an identifier so the AST makes some sense
1420
            firstIdentifier.kind = TokenKind.Identifier;
1,273✔
1421
            const varExpr = new VariableExpression({ name: firstIdentifier });
1,273✔
1422
            expr = varExpr;
1,273✔
1423

1424
            //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
1425
            while (this.check(TokenKind.Dot)) {
1,273✔
1426
                let dot = this.tryConsume(
454✔
1427
                    DiagnosticMessages.unexpectedToken(this.peek().text),
1428
                    TokenKind.Dot
1429
                );
1430
                if (!dot) {
454!
UNCOV
1431
                    break;
×
1432
                }
1433
                let identifier = this.tryConsume(
454✔
1434
                    DiagnosticMessages.expectedIdentifier(),
1435
                    TokenKind.Identifier,
1436
                    ...allowedTokenKinds,
1437
                    ...AllowedProperties
1438
                ) as Identifier;
1439

1440
                if (!identifier) {
454✔
1441
                    break;
3✔
1442
                }
1443
                // force it into an identifier so the AST makes some sense
1444
                identifier.kind = TokenKind.Identifier;
451✔
1445
                expr = new DottedGetExpression({ obj: expr, name: identifier, dot: dot });
451✔
1446
            }
1447
        }
1448
        return expr;
1,273✔
1449
    }
1450
    /**
1451
     * Add an 'unexpected token' diagnostic for any token found between current and the first stopToken found.
1452
     */
1453
    private flagUntil(...stopTokens: TokenKind[]) {
1454
        while (!this.checkAny(...stopTokens) && !this.isAtEnd()) {
4!
UNCOV
1455
            let token = this.advance();
×
UNCOV
1456
            this.diagnostics.push({
×
1457
                ...DiagnosticMessages.unexpectedToken(token.text),
1458
                location: token.location
1459
            });
1460
        }
1461
    }
1462

1463
    /**
1464
     * Consume tokens until one of the `stopTokenKinds` is encountered
1465
     * @param stopTokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
1466
     * @returns - the list of tokens consumed, EXCLUDING the `stopTokenKind` (you can use `this.peek()` to see which one it was)
1467
     */
1468
    private consumeUntil(...stopTokenKinds: TokenKind[]) {
1469
        let result = [] as Token[];
61✔
1470
        //take tokens until we encounter one of the stopTokenKinds
1471
        while (!stopTokenKinds.includes(this.peek().kind)) {
61✔
1472
            result.push(this.advance());
136✔
1473
        }
1474
        return result;
61✔
1475
    }
1476

1477
    private constDeclaration(): ConstStatement | undefined {
1478
        this.warnIfNotBrighterScriptMode('const declaration');
155✔
1479
        const constToken = this.advance();
155✔
1480
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
155✔
1481
        const equalToken = this.consumeToken(TokenKind.Equal);
155✔
1482
        const expression = this.expression();
155✔
1483
        const statement = new ConstStatement({
155✔
1484
            const: constToken,
1485
            name: nameToken,
1486
            equals: equalToken,
1487
            value: expression
1488
        });
1489
        return statement;
155✔
1490
    }
1491

1492
    private libraryStatement(): LibraryStatement | undefined {
1493
        let libStatement = new LibraryStatement({
13✔
1494
            library: this.advance(),
1495
            //grab the next token only if it's a string
1496
            filePath: this.tryConsume(
1497
                DiagnosticMessages.expectedStringLiteralAfterKeyword('library'),
1498
                TokenKind.StringLiteral
1499
            )
1500
        });
1501

1502
        return libStatement;
13✔
1503
    }
1504

1505
    private importStatement() {
1506
        this.warnIfNotBrighterScriptMode('import statements');
203✔
1507
        let importStatement = new ImportStatement({
203✔
1508
            import: this.advance(),
1509
            //grab the next token only if it's a string
1510
            path: this.tryConsume(
1511
                DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
1512
                TokenKind.StringLiteral
1513
            )
1514
        });
1515

1516
        return importStatement;
203✔
1517
    }
1518

1519
    private typecastStatement() {
1520
        this.warnIfNotBrighterScriptMode('typecast statements');
24✔
1521
        const typecastToken = this.advance();
24✔
1522
        const typecastExpr = this.expression();
24✔
1523
        if (isTypecastExpression(typecastExpr)) {
24!
1524
            return new TypecastStatement({
24✔
1525
                typecast: typecastToken,
1526
                typecastExpression: typecastExpr
1527
            });
1528
        }
UNCOV
1529
        this.diagnostics.push({
×
1530
            ...DiagnosticMessages.expectedIdentifierAfterKeyword('typecast'),
1531
            location: {
1532
                uri: typecastToken.location.uri,
1533
                range: util.createBoundingRange(typecastToken, this.peek())
1534
            }
1535
        });
UNCOV
1536
        throw this.lastDiagnosticAsError();
×
1537
    }
1538

1539
    private aliasStatement(): AliasStatement | undefined {
1540
        this.warnIfNotBrighterScriptMode('alias statements');
33✔
1541
        const aliasToken = this.advance();
33✔
1542
        const name = this.tryConsume(
33✔
1543
            DiagnosticMessages.expectedIdentifierAfterKeyword('alias'),
1544
            TokenKind.Identifier
1545
        );
1546
        const equals = this.tryConsume(
33✔
1547
            DiagnosticMessages.expectedToken(TokenKind.Equal),
1548
            TokenKind.Equal
1549
        );
1550
        let value = this.identifyingExpression();
33✔
1551

1552
        let aliasStmt = new AliasStatement({
33✔
1553
            alias: aliasToken,
1554
            name: name,
1555
            equals: equals,
1556
            value: value
1557

1558
        });
1559

1560
        return aliasStmt;
33✔
1561
    }
1562

1563
    private annotationExpression() {
1564
        const atToken = this.advance();
75✔
1565
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
75✔
1566
        if (identifier) {
75✔
1567
            identifier.kind = TokenKind.Identifier;
74✔
1568
        }
1569
        let annotation = new AnnotationExpression({ at: atToken, name: identifier });
75✔
1570
        this.pendingAnnotations.push(annotation);
74✔
1571

1572
        //optional arguments
1573
        if (this.check(TokenKind.LeftParen)) {
74✔
1574
            let leftParen = this.advance();
30✔
1575
            annotation.call = this.finishCall(leftParen, annotation, false);
30✔
1576
        }
1577
        return annotation;
74✔
1578
    }
1579

1580
    private ternaryExpression(test?: Expression): TernaryExpression {
1581
        this.warnIfNotBrighterScriptMode('ternary operator');
80✔
1582
        if (!test) {
80!
UNCOV
1583
            test = this.expression();
×
1584
        }
1585
        const questionMarkToken = this.advance();
80✔
1586

1587
        //consume newlines or comments
1588
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
80✔
1589
            this.advance();
7✔
1590
        }
1591

1592
        let consequent: Expression;
1593
        try {
80✔
1594
            consequent = this.expression();
80✔
1595
        } catch { }
1596

1597
        //consume newlines or comments
1598
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
80✔
1599
            this.advance();
5✔
1600
        }
1601

1602
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
80✔
1603

1604
        //consume newlines
1605
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
80✔
1606
            this.advance();
11✔
1607
        }
1608
        let alternate: Expression;
1609
        try {
80✔
1610
            alternate = this.expression();
80✔
1611
        } catch { }
1612

1613
        return new TernaryExpression({
80✔
1614
            test: test,
1615
            questionMark: questionMarkToken,
1616
            consequent: consequent,
1617
            colon: colonToken,
1618
            alternate: alternate
1619
        });
1620
    }
1621

1622
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1623
        this.warnIfNotBrighterScriptMode('null coalescing operator');
34✔
1624
        const questionQuestionToken = this.advance();
34✔
1625
        const alternate = this.expression();
34✔
1626
        return new NullCoalescingExpression({
34✔
1627
            consequent: test,
1628
            questionQuestion: questionQuestionToken,
1629
            alternate: alternate
1630
        });
1631
    }
1632

1633
    private regexLiteralExpression() {
1634
        this.warnIfNotBrighterScriptMode('regular expression literal');
45✔
1635
        return new RegexLiteralExpression({
45✔
1636
            regexLiteral: this.advance()
1637
        });
1638
    }
1639

1640
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1641
        this.warnIfNotBrighterScriptMode('template string');
51✔
1642

1643
        //get the tag name
1644
        let tagName: Identifier;
1645
        if (isTagged) {
51✔
1646
            tagName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties) as Identifier;
8✔
1647
            // force it into an identifier so the AST makes some sense
1648
            tagName.kind = TokenKind.Identifier;
8✔
1649
        }
1650

1651
        let quasis = [] as TemplateStringQuasiExpression[];
51✔
1652
        let expressions = [];
51✔
1653
        let openingBacktick = this.peek();
51✔
1654
        this.advance();
51✔
1655
        let currentQuasiExpressionParts = [];
51✔
1656
        while (!this.isAtEnd() && !this.check(TokenKind.BackTick)) {
51✔
1657
            let next = this.peek();
194✔
1658
            if (next.kind === TokenKind.TemplateStringQuasi) {
194✔
1659
                //a quasi can actually be made up of multiple quasis when it includes char literals
1660
                currentQuasiExpressionParts.push(
122✔
1661
                    new LiteralExpression({ value: next })
1662
                );
1663
                this.advance();
122✔
1664
            } else if (next.kind === TokenKind.EscapedCharCodeLiteral) {
72✔
1665
                currentQuasiExpressionParts.push(
31✔
1666
                    new EscapedCharCodeLiteralExpression({ value: next as Token & { charCode: number } })
1667
                );
1668
                this.advance();
31✔
1669
            } else {
1670
                //finish up the current quasi
1671
                quasis.push(
41✔
1672
                    new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1673
                );
1674
                currentQuasiExpressionParts = [];
41✔
1675

1676
                if (next.kind === TokenKind.TemplateStringExpressionBegin) {
41!
1677
                    this.advance();
41✔
1678
                }
1679
                //now keep this expression
1680
                expressions.push(this.expression());
41✔
1681
                if (!this.isAtEnd() && this.check(TokenKind.TemplateStringExpressionEnd)) {
41!
1682
                    //TODO is it an error if this is not present?
1683
                    this.advance();
41✔
1684
                } else {
UNCOV
1685
                    this.diagnostics.push({
×
1686
                        ...DiagnosticMessages.unterminatedTemplateExpression(),
1687
                        location: {
1688
                            uri: openingBacktick.location.uri,
1689
                            range: util.createBoundingRange(openingBacktick, this.peek())
1690
                        }
1691
                    });
UNCOV
1692
                    throw this.lastDiagnosticAsError();
×
1693
                }
1694
            }
1695
        }
1696

1697
        //store the final set of quasis
1698
        quasis.push(
51✔
1699
            new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1700
        );
1701

1702
        if (this.isAtEnd()) {
51✔
1703
            //error - missing backtick
1704
            this.diagnostics.push({
2✔
1705
                ...DiagnosticMessages.unterminatedTemplateStringAtEndOfFile(),
1706
                location: {
1707
                    uri: openingBacktick.location.uri,
1708
                    range: util.createBoundingRange(openingBacktick, this.peek())
1709
                }
1710
            });
1711
            throw this.lastDiagnosticAsError();
2✔
1712

1713
        } else {
1714
            let closingBacktick = this.advance();
49✔
1715
            if (isTagged) {
49✔
1716
                return new TaggedTemplateStringExpression({
8✔
1717
                    tagName: tagName,
1718
                    openingBacktick: openingBacktick,
1719
                    quasis: quasis,
1720
                    expressions: expressions,
1721
                    closingBacktick: closingBacktick
1722
                });
1723
            } else {
1724
                return new TemplateStringExpression({
41✔
1725
                    openingBacktick: openingBacktick,
1726
                    quasis: quasis,
1727
                    expressions: expressions,
1728
                    closingBacktick: closingBacktick
1729
                });
1730
            }
1731
        }
1732
    }
1733

1734
    private tryCatchStatement(): TryCatchStatement {
1735
        const tryToken = this.advance();
35✔
1736
        let endTryToken: Token;
1737
        let catchStmt: CatchStatement;
1738
        //ensure statement separator
1739
        this.consumeStatementSeparators();
35✔
1740

1741
        let tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
35✔
1742

1743
        const peek = this.peek();
35✔
1744
        if (peek.kind !== TokenKind.Catch) {
35✔
1745
            this.diagnostics.push({
2✔
1746
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1747
                location: this.peek()?.location
6!
1748
            });
1749
        } else {
1750
            const catchToken = this.advance();
33✔
1751

1752
            //get the exception variable as an expression
1753
            let exceptionVariableExpression: Expression;
1754
            //if we consumed any statement separators, that means we don't have an exception variable
1755
            if (this.consumeStatementSeparators(true)) {
33✔
1756
                //no exception variable. That's fine in BrighterScript but not in brightscript. But that'll get caught by the validator later...
1757
            } else {
1758
                exceptionVariableExpression = this.expression(true);
27✔
1759
                this.consumeStatementSeparators();
27✔
1760
            }
1761

1762
            const catchBranch = this.block(TokenKind.EndTry);
33✔
1763
            catchStmt = new CatchStatement({
33✔
1764
                catch: catchToken,
1765
                exceptionVariableExpression: exceptionVariableExpression,
1766
                catchBranch: catchBranch
1767
            });
1768
        }
1769
        if (this.peek().kind !== TokenKind.EndTry) {
35✔
1770
            this.diagnostics.push({
2✔
1771
                ...DiagnosticMessages.expectedEndTryToTerminateTryCatch(),
1772
                location: this.peek().location
1773
            });
1774
        } else {
1775
            endTryToken = this.advance();
33✔
1776
        }
1777

1778
        const statement = new TryCatchStatement({
35✔
1779
            try: tryToken,
1780
            tryBranch: tryBranch,
1781
            catchStatement: catchStmt,
1782
            endTry: endTryToken
1783
        }
1784
        );
1785
        return statement;
35✔
1786
    }
1787

1788
    private throwStatement() {
1789
        const throwToken = this.advance();
12✔
1790
        let expression: Expression;
1791
        if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
12✔
1792
            this.diagnostics.push({
2✔
1793
                ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(),
1794
                location: throwToken.location
1795
            });
1796
        } else {
1797
            expression = this.expression();
10✔
1798
        }
1799
        return new ThrowStatement({ throw: throwToken, expression: expression });
10✔
1800
    }
1801

1802
    private dimStatement() {
1803
        const dim = this.advance();
43✔
1804

1805
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
43✔
1806
        // force to an identifier so the AST makes some sense
1807
        if (identifier) {
43✔
1808
            identifier.kind = TokenKind.Identifier;
41✔
1809
        }
1810

1811
        let leftSquareBracket = this.tryConsume(DiagnosticMessages.missingLeftSquareBracketAfterDimIdentifier(), TokenKind.LeftSquareBracket);
43✔
1812

1813
        let expressions: Expression[] = [];
43✔
1814
        let expression: Expression;
1815
        do {
43✔
1816
            try {
82✔
1817
                expression = this.expression();
82✔
1818
                expressions.push(expression);
77✔
1819
                if (this.check(TokenKind.Comma)) {
77✔
1820
                    this.advance();
39✔
1821
                } else {
1822
                    // will also exit for right square braces
1823
                    break;
38✔
1824
                }
1825
            } catch (error) {
1826
            }
1827
        } while (expression);
1828

1829
        if (expressions.length === 0) {
43✔
1830
            this.diagnostics.push({
5✔
1831
                ...DiagnosticMessages.missingExpressionsInDimStatement(),
1832
                location: this.peek().location
1833
            });
1834
        }
1835
        let rightSquareBracket = this.tryConsume(DiagnosticMessages.missingRightSquareBracketAfterDimIdentifier(), TokenKind.RightSquareBracket);
43✔
1836
        return new DimStatement({
43✔
1837
            dim: dim,
1838
            name: identifier,
1839
            openingSquare: leftSquareBracket,
1840
            dimensions: expressions,
1841
            closingSquare: rightSquareBracket
1842
        });
1843
    }
1844

1845
    private nestedInlineConditionalCount = 0;
3,470✔
1846

1847
    private ifStatement(incrementNestedCount = true): IfStatement {
2,001✔
1848
        // colon before `if` is usually not allowed, unless it's after `then`
1849
        if (this.current > 0) {
2,011✔
1850
            const prev = this.previous();
2,006✔
1851
            if (prev.kind === TokenKind.Colon) {
2,006✔
1852
                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then && this.nestedInlineConditionalCount === 0) {
4✔
1853
                    this.diagnostics.push({
1✔
1854
                        ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
1855
                        location: prev.location
1856
                    });
1857
                }
1858
            }
1859
        }
1860

1861
        const ifToken = this.advance();
2,011✔
1862

1863
        const condition = this.expression();
2,011✔
1864
        let thenBranch: Block;
1865
        let elseBranch: IfStatement | Block | undefined;
1866

1867
        let thenToken: Token | undefined;
1868
        let endIfToken: Token | undefined;
1869
        let elseToken: Token | undefined;
1870

1871
        //optional `then`
1872
        if (this.check(TokenKind.Then)) {
2,009✔
1873
            thenToken = this.advance();
1,603✔
1874
        }
1875

1876
        //is it inline or multi-line if?
1877
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
2,009✔
1878

1879
        if (isInlineIfThen) {
2,009✔
1880
            /*** PARSE INLINE IF STATEMENT ***/
1881
            if (!incrementNestedCount) {
48✔
1882
                this.nestedInlineConditionalCount++;
5✔
1883
            }
1884

1885
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
48✔
1886

1887
            if (!thenBranch) {
48!
UNCOV
1888
                this.diagnostics.push({
×
1889
                    ...DiagnosticMessages.expectedStatementToFollowConditionalCondition(ifToken.text),
1890
                    location: this.peek().location
1891
                });
UNCOV
1892
                throw this.lastDiagnosticAsError();
×
1893
            } else {
1894
                this.ensureInline(thenBranch.statements);
48✔
1895
            }
1896

1897
            //else branch
1898
            if (this.check(TokenKind.Else)) {
48✔
1899
                elseToken = this.advance();
33✔
1900

1901
                if (this.check(TokenKind.If)) {
33✔
1902
                    // recurse-read `else if`
1903
                    elseBranch = this.ifStatement(false);
10✔
1904

1905
                    //no multi-line if chained with an inline if
1906
                    if (!elseBranch.isInline) {
9✔
1907
                        this.diagnostics.push({
4✔
1908
                            ...DiagnosticMessages.expectedInlineIfStatement(),
1909
                            location: elseBranch.location
1910
                        });
1911
                    }
1912

1913
                } else if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
23✔
1914
                    //expecting inline else branch
1915
                    this.diagnostics.push({
3✔
1916
                        ...DiagnosticMessages.expectedInlineIfStatement(),
1917
                        location: this.peek().location
1918
                    });
1919
                    throw this.lastDiagnosticAsError();
3✔
1920
                } else {
1921
                    elseBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
20✔
1922

1923
                    if (elseBranch) {
20!
1924
                        this.ensureInline(elseBranch.statements);
20✔
1925
                    }
1926
                }
1927

1928
                if (!elseBranch) {
29!
1929
                    //missing `else` branch
UNCOV
1930
                    this.diagnostics.push({
×
1931
                        ...DiagnosticMessages.expectedStatementToFollowElse(),
1932
                        location: this.peek().location
1933
                    });
UNCOV
1934
                    throw this.lastDiagnosticAsError();
×
1935
                }
1936
            }
1937

1938
            if (!elseBranch || !isIfStatement(elseBranch)) {
44✔
1939
                //enforce newline at the end of the inline if statement
1940
                const peek = this.peek();
35✔
1941
                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && peek.kind !== TokenKind.Else && !this.isAtEnd()) {
35✔
1942
                    //ignore last error if it was about a colon
1943
                    if (this.previous().kind === TokenKind.Colon) {
3!
1944
                        this.diagnostics.pop();
3✔
1945
                        this.current--;
3✔
1946
                    }
1947
                    //newline is required
1948
                    this.diagnostics.push({
3✔
1949
                        ...DiagnosticMessages.expectedFinalNewline(),
1950
                        location: this.peek().location
1951
                    });
1952
                }
1953
            }
1954
            this.nestedInlineConditionalCount--;
44✔
1955
        } else {
1956
            /*** PARSE MULTI-LINE IF STATEMENT ***/
1957

1958
            thenBranch = this.blockConditionalBranch(ifToken);
1,961✔
1959

1960
            //ensure newline/colon before next keyword
1961
            this.ensureNewLineOrColon();
1,958✔
1962

1963
            //else branch
1964
            if (this.check(TokenKind.Else)) {
1,958✔
1965
                elseToken = this.advance();
1,571✔
1966

1967
                if (this.check(TokenKind.If)) {
1,571✔
1968
                    // recurse-read `else if`
1969
                    elseBranch = this.ifStatement();
934✔
1970

1971
                } else {
1972
                    elseBranch = this.blockConditionalBranch(ifToken);
637✔
1973

1974
                    //ensure newline/colon before next keyword
1975
                    this.ensureNewLineOrColon();
637✔
1976
                }
1977
            }
1978

1979
            if (!isIfStatement(elseBranch)) {
1,958✔
1980
                if (this.check(TokenKind.EndIf)) {
1,024✔
1981
                    endIfToken = this.advance();
1,021✔
1982

1983
                } else {
1984
                    //missing endif
1985
                    this.diagnostics.push({
3✔
1986
                        ...DiagnosticMessages.expectedEndIfToCloseIfStatement(ifToken.location?.range.start),
9!
1987
                        location: ifToken.location
1988
                    });
1989
                }
1990
            }
1991
        }
1992

1993
        return new IfStatement({
2,002✔
1994
            if: ifToken,
1995
            then: thenToken,
1996
            endIf: endIfToken,
1997
            else: elseToken,
1998
            condition: condition,
1999
            thenBranch: thenBranch,
2000
            elseBranch: elseBranch
2001
        });
2002
    }
2003

2004
    //consume a `then` or `else` branch block of an `if` statement
2005
    private blockConditionalBranch(ifToken: Token) {
2006
        //keep track of the current error count, because if the then branch fails,
2007
        //we will trash them in favor of a single error on if
2008
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
2,598✔
2009

2010
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
2011
        // a trailing "end if" or "else if"
2012
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
2,598✔
2013

2014
        if (!branch) {
2,598✔
2015
            //throw out any new diagnostics created as a result of a `then` block parse failure.
2016
            //the block() function will discard the current line, so any discarded diagnostics will
2017
            //resurface if they are legitimate, and not a result of a malformed if statement
2018
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
3✔
2019

2020
            //this whole if statement is bogus...add error to the if token and hard-fail
2021
            this.diagnostics.push({
3✔
2022
                ...DiagnosticMessages.expectedEndIfElseIfOrElseToTerminateThenBlock(),
2023
                location: ifToken.location
2024
            });
2025
            throw this.lastDiagnosticAsError();
3✔
2026
        }
2027
        return branch;
2,595✔
2028
    }
2029

2030
    private conditionalCompileStatement(): ConditionalCompileStatement {
2031
        const hashIfToken = this.advance();
56✔
2032
        let notToken: Token | undefined;
2033

2034
        if (this.check(TokenKind.Not)) {
56✔
2035
            notToken = this.advance();
7✔
2036
        }
2037

2038
        if (!this.checkAny(TokenKind.True, TokenKind.False, TokenKind.Identifier)) {
56✔
2039
            this.diagnostics.push({
1✔
2040
                ...DiagnosticMessages.invalidHashIfValue(),
2041
                location: this.peek()?.location
3!
2042
            });
2043
        }
2044

2045

2046
        const condition = this.advance();
56✔
2047

2048
        let thenBranch: Block;
2049
        let elseBranch: ConditionalCompileStatement | Block | undefined;
2050

2051
        let hashEndIfToken: Token | undefined;
2052
        let hashElseToken: Token | undefined;
2053

2054
        //keep track of the current error count
2055
        //if this is `#if false` remove all diagnostics.
2056
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
56✔
2057

2058
        thenBranch = this.blockConditionalCompileBranch(hashIfToken);
56✔
2059
        const conditionTextLower = condition.text.toLowerCase();
55✔
2060
        if (!this.options.bsConsts?.get(conditionTextLower) || conditionTextLower === 'false') {
55✔
2061
            //throw out any new diagnostics created as a result of a false block
2062
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
45✔
2063
        }
2064

2065
        this.ensureNewLine();
55✔
2066
        this.advance();
55✔
2067

2068
        //else branch
2069
        if (this.check(TokenKind.HashElseIf)) {
55✔
2070
            // recurse-read `#else if`
2071
            elseBranch = this.conditionalCompileStatement();
15✔
2072
            this.ensureNewLine();
15✔
2073

2074
        } else if (this.check(TokenKind.HashElse)) {
40✔
2075
            hashElseToken = this.advance();
10✔
2076
            let diagnosticsLengthBeforeBlock = this.diagnostics.length;
10✔
2077
            elseBranch = this.blockConditionalCompileBranch(hashIfToken);
10✔
2078

2079
            if (condition.text.toLowerCase() === 'true') {
10✔
2080
                //throw out any new diagnostics created as a result of a false block
2081
                this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
1✔
2082
            }
2083
            this.ensureNewLine();
10✔
2084
            this.advance();
10✔
2085
        }
2086

2087
        if (!isConditionalCompileStatement(elseBranch)) {
55✔
2088

2089
            if (this.check(TokenKind.HashEndIf)) {
40!
2090
                hashEndIfToken = this.advance();
40✔
2091

2092
            } else {
2093
                //missing #endif
UNCOV
2094
                this.diagnostics.push({
×
2095
                    ...DiagnosticMessages.expectedHashEndIfToCloseHashIf(hashIfToken.location?.range.start.line),
×
2096
                    location: hashIfToken.location
2097
                });
2098
            }
2099
        }
2100

2101
        return new ConditionalCompileStatement({
55✔
2102
            hashIf: hashIfToken,
2103
            hashElse: hashElseToken,
2104
            hashEndIf: hashEndIfToken,
2105
            not: notToken,
2106
            condition: condition,
2107
            thenBranch: thenBranch,
2108
            elseBranch: elseBranch
2109
        });
2110
    }
2111

2112
    //consume a conditional compile branch block of an `#if` statement
2113
    private blockConditionalCompileBranch(hashIfToken: Token) {
2114
        //keep track of the current error count, because if the then branch fails,
2115
        //we will trash them in favor of a single error on if
2116
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
66✔
2117

2118
        //parsing until trailing "#end if", "#else", "#else if"
2119
        let branch = this.conditionalCompileBlock();
66✔
2120

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

2127
            //this whole if statement is bogus...add error to the if token and hard-fail
UNCOV
2128
            this.diagnostics.push({
×
2129
                ...DiagnosticMessages.expectedTerminatorOnConditionalCompileBlock(),
2130
                location: hashIfToken.location
2131
            });
UNCOV
2132
            throw this.lastDiagnosticAsError();
×
2133
        }
2134
        return branch;
65✔
2135
    }
2136

2137
    /**
2138
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2139
     * Always looks for `#end if` or `#else`
2140
     */
2141
    private conditionalCompileBlock(): Block | undefined {
2142
        const parentAnnotations = this.enterAnnotationBlock();
66✔
2143

2144
        this.consumeStatementSeparators(true);
66✔
2145
        const unsafeTerminators = BlockTerminators;
66✔
2146
        const conditionalEndTokens = [TokenKind.HashElse, TokenKind.HashElseIf, TokenKind.HashEndIf];
66✔
2147
        const terminators = [...conditionalEndTokens, ...unsafeTerminators];
66✔
2148
        this.globalTerminators.push(conditionalEndTokens);
66✔
2149
        const statements: Statement[] = [];
66✔
2150
        while (!this.isAtEnd() && !this.checkAny(...terminators)) {
66✔
2151
            //grab the location of the current token
2152
            let loopCurrent = this.current;
69✔
2153
            let dec = this.declaration();
69✔
2154
            if (dec) {
69✔
2155
                if (!isAnnotationExpression(dec)) {
68!
2156
                    this.consumePendingAnnotations(dec);
68✔
2157
                    statements.push(dec);
68✔
2158
                }
2159

2160
                const peekKind = this.peek().kind;
68✔
2161
                if (conditionalEndTokens.includes(peekKind)) {
68✔
2162
                    // current conditional compile branch was closed by other statement, rewind to preceding newline
2163
                    this.current--;
1✔
2164
                }
2165
                //ensure statement separator
2166
                this.consumeStatementSeparators();
68✔
2167

2168
            } else {
2169
                //something went wrong. reset to the top of the loop
2170
                this.current = loopCurrent;
1✔
2171

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

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

2179
                //consume potential separators
2180
                this.consumeStatementSeparators(true);
1✔
2181
            }
2182
        }
2183
        this.globalTerminators.pop();
66✔
2184

2185

2186
        if (this.isAtEnd()) {
66!
UNCOV
2187
            return undefined;
×
2188
            // TODO: Figure out how to handle unterminated blocks well
2189
        } else {
2190
            //did we  hit an unsafe terminator?
2191
            //if so, we need to restore the statement separator
2192
            let prev = this.previous();
66✔
2193
            let prevKind = prev.kind;
66✔
2194
            let peek = this.peek();
66✔
2195
            let peekKind = this.peek().kind;
66✔
2196
            if (
66✔
2197
                (peekKind === TokenKind.HashEndIf || peekKind === TokenKind.HashElse || peekKind === TokenKind.HashElseIf) &&
173✔
2198
                (prevKind === TokenKind.Newline)
2199
            ) {
2200
                this.current--;
65✔
2201
            } else if (unsafeTerminators.includes(peekKind) &&
1!
2202
                (prevKind === TokenKind.Newline || prevKind === TokenKind.Colon)
2203
            ) {
2204
                this.diagnostics.push({
1✔
2205
                    ...DiagnosticMessages.unsafeUnmatchedTerminatorInConditionalCompileBlock(peek.text),
2206
                    location: peek.location
2207
                });
2208
                throw this.lastDiagnosticAsError();
1✔
2209
            } else {
UNCOV
2210
                return undefined;
×
2211
            }
2212
        }
2213
        this.exitAnnotationBlock(parentAnnotations);
65✔
2214
        return new Block({ statements: statements });
65✔
2215
    }
2216

2217
    private conditionalCompileConstStatement() {
2218
        const hashConstToken = this.advance();
21✔
2219

2220
        const constName = this.peek();
21✔
2221
        //disallow using keywords for const names
2222
        if (ReservedWords.has(constName?.text.toLowerCase())) {
21!
2223
            this.diagnostics.push({
1✔
2224
                ...DiagnosticMessages.constNameCannotBeReservedWord(),
2225
                location: constName?.location
3!
2226
            });
2227

2228
            this.lastDiagnosticAsError();
1✔
2229
            return;
1✔
2230
        }
2231
        const assignment = this.assignment();
20✔
2232
        if (assignment) {
18!
2233
            // check for something other than #const <name> = <otherName|true|false>
2234
            if (assignment.tokens.as || assignment.typeExpression) {
18!
UNCOV
2235
                this.diagnostics.push({
×
2236
                    ...DiagnosticMessages.unexpectedToken(assignment.tokens.as?.text || assignment.typeExpression?.getName(ParseMode.BrighterScript)),
×
2237
                    location: assignment.tokens.as?.location ?? assignment.typeExpression?.location
×
2238
                });
UNCOV
2239
                this.lastDiagnosticAsError();
×
2240
            }
2241

2242
            if (isVariableExpression(assignment.value) || isLiteralBoolean(assignment.value)) {
18✔
2243
                //value is an identifier or a boolean
2244
                //check for valid identifiers will happen in program validation
2245
            } else {
2246
                this.diagnostics.push({
2✔
2247
                    ...DiagnosticMessages.invalidHashConstValue(),
2248
                    location: assignment.value.location
2249
                });
2250
                this.lastDiagnosticAsError();
2✔
2251
            }
2252
        } else {
UNCOV
2253
            return undefined;
×
2254
        }
2255

2256
        if (!this.check(TokenKind.Newline)) {
18!
UNCOV
2257
            this.diagnostics.push({
×
2258
                ...DiagnosticMessages.expectedNewlineInConditionalCompile(),
2259
                location: this.peek().location
2260
            });
UNCOV
2261
            throw this.lastDiagnosticAsError();
×
2262
        }
2263

2264
        return new ConditionalCompileConstStatement({ hashConst: hashConstToken, assignment: assignment });
18✔
2265
    }
2266

2267
    private conditionalCompileErrorStatement() {
2268
        const hashErrorToken = this.advance();
10✔
2269
        const tokensUntilEndOfLine = this.consumeUntil(TokenKind.Newline);
10✔
2270
        const message = createToken(TokenKind.HashErrorMessage, tokensUntilEndOfLine.map(t => t.text).join(' '));
10✔
2271
        return new ConditionalCompileErrorStatement({ hashError: hashErrorToken, message: message });
10✔
2272
    }
2273

2274
    private ensureNewLine() {
2275
        //ensure newline before next keyword
2276
        if (!this.check(TokenKind.Newline)) {
80!
UNCOV
2277
            this.diagnostics.push({
×
2278
                ...DiagnosticMessages.expectedNewlineInConditionalCompile(),
2279
                location: this.peek().location
2280
            });
UNCOV
2281
            throw this.lastDiagnosticAsError();
×
2282
        }
2283
    }
2284

2285
    private ensureNewLineOrColon(silent = false) {
2,595✔
2286
        const prev = this.previous().kind;
2,770✔
2287
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
2,770✔
2288
            if (!silent) {
123✔
2289
                this.diagnostics.push({
6✔
2290
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2291
                    location: this.peek().location
2292
                });
2293
            }
2294
            return false;
123✔
2295
        }
2296
        return true;
2,647✔
2297
    }
2298

2299
    //ensure each statement of an inline block is single-line
2300
    private ensureInline(statements: Statement[]) {
2301
        for (const stat of statements) {
68✔
2302
            if (isIfStatement(stat) && !stat.isInline) {
86✔
2303
                this.diagnostics.push({
2✔
2304
                    ...DiagnosticMessages.expectedInlineIfStatement(),
2305
                    location: stat.location
2306
                });
2307
            }
2308
        }
2309
    }
2310

2311
    //consume inline branch of an `if` statement
2312
    private inlineConditionalBranch(...additionalTerminators: BlockTerminator[]): Block | undefined {
2313
        let statements = [];
86✔
2314
        //attempt to get the next statement without using `this.declaration`
2315
        //which seems a bit hackish to get to work properly
2316
        let statement = this.statement();
86✔
2317
        if (!statement) {
86!
UNCOV
2318
            return undefined;
×
2319
        }
2320
        statements.push(statement);
86✔
2321

2322
        //look for colon statement separator
2323
        let foundColon = false;
86✔
2324
        while (this.match(TokenKind.Colon)) {
86✔
2325
            foundColon = true;
23✔
2326
        }
2327

2328
        //if a colon was found, add the next statement or err if unexpected
2329
        if (foundColon) {
86✔
2330
            if (!this.checkAny(TokenKind.Newline, ...additionalTerminators)) {
23✔
2331
                //if not an ending keyword, add next statement
2332
                let extra = this.inlineConditionalBranch(...additionalTerminators);
18✔
2333
                if (!extra) {
18!
UNCOV
2334
                    return undefined;
×
2335
                }
2336
                statements.push(...extra.statements);
18✔
2337
            } else {
2338
                //error: colon before next keyword
2339
                const colon = this.previous();
5✔
2340
                this.diagnostics.push({
5✔
2341
                    ...DiagnosticMessages.unexpectedToken(colon.text),
2342
                    location: colon.location
2343
                });
2344
            }
2345
        }
2346
        return new Block({ statements: statements });
86✔
2347
    }
2348

2349
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2350
        let expressionStart = this.peek();
778✔
2351

2352
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
778✔
2353
            let operator = this.advance();
25✔
2354

2355
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
25✔
2356
                this.diagnostics.push({
1✔
2357
                    ...DiagnosticMessages.consecutiveIncrementDecrementOperatorsAreNotAllowed(),
2358
                    location: this.peek().location
2359
                });
2360
                throw this.lastDiagnosticAsError();
1✔
2361
            } else if (isCallExpression(expr)) {
24✔
2362
                this.diagnostics.push({
1✔
2363
                    ...DiagnosticMessages.incrementDecrementOperatorsAreNotAllowedAsResultOfFunctionCall(),
2364
                    location: expressionStart.location
2365
                });
2366
                throw this.lastDiagnosticAsError();
1✔
2367
            }
2368

2369
            const result = new IncrementStatement({ value: expr, operator: operator });
23✔
2370
            return result;
23✔
2371
        }
2372

2373
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
753✔
2374
            return new ExpressionStatement({ expression: expr });
451✔
2375
        }
2376

2377
        if (this.checkAny(...BinaryExpressionOperatorTokens)) {
302✔
2378
            expr = new BinaryExpression({ left: expr, operator: this.advance(), right: this.expression() });
6✔
2379
        }
2380

2381
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an inclosing ExpressionStatement
2382
        this.diagnostics.push({
302✔
2383
            ...DiagnosticMessages.expectedStatementOrFunctionCallButReceivedExpression(),
2384
            location: expressionStart.location
2385
        });
2386
        return new ExpressionStatement({ expression: expr });
302✔
2387
    }
2388

2389
    private setStatement(): DottedSetStatement | IndexedSetStatement | ExpressionStatement | IncrementStatement | AssignmentStatement | AugmentedAssignmentStatement {
2390
        /**
2391
         * Attempts to find an expression-statement or an increment statement.
2392
         * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
2393
         * statements aren't valid expressions. They _do_ however fall under the same parsing
2394
         * priority as standalone function calls though, so we can parse them in the same way.
2395
         */
2396
        let expr = this.call();
1,169✔
2397
        if (this.check(TokenKind.Equal) && !(isCallExpression(expr))) {
1,122✔
2398
            let left = expr;
327✔
2399
            let operator = this.advance();
327✔
2400
            let right = this.expression();
327✔
2401

2402
            // Create a dotted or indexed "set" based on the left-hand side's type
2403
            if (isIndexedGetExpression(left)) {
327✔
2404
                return new IndexedSetStatement({
29✔
2405
                    obj: left.obj,
2406
                    indexes: left.indexes,
2407
                    value: right,
2408
                    openingSquare: left.tokens.openingSquare,
2409
                    closingSquare: left.tokens.closingSquare,
2410
                    equals: operator
2411
                });
2412
            } else if (isDottedGetExpression(left)) {
298✔
2413
                return new DottedSetStatement({
295✔
2414
                    obj: left.obj,
2415
                    name: left.tokens.name,
2416
                    value: right,
2417
                    dot: left.tokens.dot,
2418
                    equals: operator
2419
                });
2420
            }
2421
        } else if (this.checkAny(...CompoundAssignmentOperators) && !(isCallExpression(expr))) {
795✔
2422
            let left = expr;
20✔
2423
            let operator = this.advance();
20✔
2424
            let right = this.expression();
20✔
2425
            return new AugmentedAssignmentStatement({
20✔
2426
                item: left,
2427
                operator: operator,
2428
                value: right
2429
            });
2430
        }
2431
        return this.expressionStatement(expr);
778✔
2432
    }
2433

2434
    private printStatement(): PrintStatement {
2435
        let printKeyword = this.advance();
1,149✔
2436

2437
        let values: Expression[] = [];
1,149✔
2438

2439
        while (!this.checkEndOfStatement()) {
1,149✔
2440
            if (this.checkAny(TokenKind.Semicolon, TokenKind.Comma)) {
1,258✔
2441
                values.push(new PrintSeparatorExpression({ separator: this.advance() as PrintSeparatorToken }));
42✔
2442
            } else if (this.check(TokenKind.Else)) {
1,216✔
2443
                break; // inline branch
22✔
2444
            } else {
2445
                values.push(this.expression());
1,194✔
2446
            }
2447
        }
2448

2449
        //print statements can be empty, so look for empty print conditions
2450
        if (!values.length) {
1,146✔
2451
            const endOfStatementLocation = util.createBoundingLocation(printKeyword, this.peek());
12✔
2452
            let emptyStringLiteral = createStringLiteral('', endOfStatementLocation);
12✔
2453
            values.push(emptyStringLiteral);
12✔
2454
        }
2455

2456
        let last = values[values.length - 1];
1,146✔
2457
        if (isToken(last)) {
1,146!
2458
            // TODO: error, expected value
2459
        }
2460

2461
        return new PrintStatement({ print: printKeyword, expressions: values });
1,146✔
2462
    }
2463

2464
    /**
2465
     * Parses a return statement with an optional return value.
2466
     * @returns an AST representation of a return statement.
2467
     */
2468
    private returnStatement(): ReturnStatement {
2469
        let options = { return: this.previous() };
3,135✔
2470

2471
        if (this.checkEndOfStatement()) {
3,135✔
2472
            return new ReturnStatement(options);
16✔
2473
        }
2474

2475
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
3,119✔
2476
        return new ReturnStatement({ ...options, value: toReturn });
3,118✔
2477
    }
2478

2479
    /**
2480
     * Parses a `label` statement
2481
     * @returns an AST representation of an `label` statement.
2482
     */
2483
    private labelStatement() {
2484
        let options = {
12✔
2485
            name: this.advance(),
2486
            colon: this.advance()
2487
        };
2488

2489
        //label must be alone on its line, this is probably not a label
2490
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
12✔
2491
            //rewind and cancel
2492
            this.current -= 2;
2✔
2493
            throw new CancelStatementError();
2✔
2494
        }
2495

2496
        return new LabelStatement(options);
10✔
2497
    }
2498

2499
    /**
2500
     * Parses a `continue` statement
2501
     */
2502
    private continueStatement() {
2503
        return new ContinueStatement({
12✔
2504
            continue: this.advance(),
2505
            loopType: this.tryConsume(
2506
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2507
                TokenKind.While, TokenKind.For
2508
            )
2509
        });
2510
    }
2511

2512
    /**
2513
     * Parses a `goto` statement
2514
     * @returns an AST representation of an `goto` statement.
2515
     */
2516
    private gotoStatement() {
2517
        let tokens = {
12✔
2518
            goto: this.advance(),
2519
            label: this.consume(
2520
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2521
                TokenKind.Identifier
2522
            )
2523
        };
2524

2525
        return new GotoStatement(tokens);
10✔
2526
    }
2527

2528
    /**
2529
     * Parses an `end` statement
2530
     * @returns an AST representation of an `end` statement.
2531
     */
2532
    private endStatement() {
2533
        let options = { end: this.advance() };
8✔
2534

2535
        return new EndStatement(options);
8✔
2536
    }
2537
    /**
2538
     * Parses a `stop` statement
2539
     * @returns an AST representation of a `stop` statement
2540
     */
2541
    private stopStatement() {
2542
        let options = { stop: this.advance() };
16✔
2543

2544
        return new StopStatement(options);
16✔
2545
    }
2546

2547
    /**
2548
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2549
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2550
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2551
     *                    ignored.
2552
     */
2553
    private block(...terminators: BlockTerminator[]): Block | undefined {
2554
        const parentAnnotations = this.enterAnnotationBlock();
6,335✔
2555

2556
        this.consumeStatementSeparators(true);
6,335✔
2557
        const statements: Statement[] = [];
6,335✔
2558
        const flatGlobalTerminators = this.globalTerminators.flat().flat();
6,335✔
2559
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators, ...flatGlobalTerminators)) {
6,335✔
2560
            //grab the location of the current token
2561
            let loopCurrent = this.current;
7,460✔
2562
            let dec = this.declaration();
7,460✔
2563
            if (dec) {
7,460✔
2564
                if (!isAnnotationExpression(dec)) {
7,410✔
2565
                    this.consumePendingAnnotations(dec);
7,403✔
2566
                    statements.push(dec);
7,403✔
2567
                }
2568

2569
                //ensure statement separator
2570
                this.consumeStatementSeparators();
7,410✔
2571

2572
            } else {
2573
                //something went wrong. reset to the top of the loop
2574
                this.current = loopCurrent;
50✔
2575

2576
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2577
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
50✔
2578

2579
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2580
                //but there's already an error in the file being parsed, so just leave this line here
2581
                this.advance();
50✔
2582

2583
                //consume potential separators
2584
                this.consumeStatementSeparators(true);
50✔
2585
            }
2586
        }
2587

2588
        if (this.isAtEnd()) {
6,335✔
2589
            return undefined;
6✔
2590
            // TODO: Figure out how to handle unterminated blocks well
2591
        } else if (terminators.length > 0) {
6,329✔
2592
            //did we hit end-sub / end-function while looking for some other terminator?
2593
            //if so, we need to restore the statement separator
2594
            let prev = this.previous().kind;
2,768✔
2595
            let peek = this.peek().kind;
2,768✔
2596
            if (
2,768✔
2597
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
5,540!
2598
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2599
            ) {
2600
                this.current--;
6✔
2601
            }
2602
        }
2603

2604
        this.exitAnnotationBlock(parentAnnotations);
6,329✔
2605
        return new Block({ statements: statements });
6,329✔
2606
    }
2607

2608
    /**
2609
     * Attach pending annotations to the provided statement,
2610
     * and then reset the annotations array
2611
     */
2612
    consumePendingAnnotations(statement: Statement) {
2613
        if (this.pendingAnnotations.length) {
14,543✔
2614
            statement.annotations = this.pendingAnnotations;
51✔
2615
            this.pendingAnnotations = [];
51✔
2616
        }
2617
    }
2618

2619
    enterAnnotationBlock() {
2620
        const pending = this.pendingAnnotations;
11,475✔
2621
        this.pendingAnnotations = [];
11,475✔
2622
        return pending;
11,475✔
2623
    }
2624

2625
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2626
        // non consumed annotations are an error
2627
        if (this.pendingAnnotations.length) {
11,467✔
2628
            for (const annotation of this.pendingAnnotations) {
5✔
2629
                this.diagnostics.push({
7✔
2630
                    ...DiagnosticMessages.unusedAnnotation(),
2631
                    location: annotation.location
2632
                });
2633
            }
2634
        }
2635
        this.pendingAnnotations = parentAnnotations;
11,467✔
2636
    }
2637

2638
    private expression(findTypecast = true): Expression {
11,508✔
2639
        let expression = this.anonymousFunction();
11,890✔
2640
        let asToken: Token;
2641
        let typeExpression: TypeExpression;
2642
        if (findTypecast) {
11,851✔
2643
            do {
11,496✔
2644
                if (this.check(TokenKind.As)) {
11,562✔
2645
                    this.warnIfNotBrighterScriptMode('type cast');
67✔
2646
                    // Check if this expression is wrapped in any type casts
2647
                    // allows for multiple casts:
2648
                    // myVal = foo() as dynamic as string
2649
                    [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
67✔
2650
                    if (asToken && typeExpression) {
67✔
2651
                        expression = new TypecastExpression({ obj: expression, as: asToken, typeExpression: typeExpression });
66✔
2652
                    }
2653
                } else {
2654
                    break;
11,495✔
2655
                }
2656

2657
            } while (asToken && typeExpression);
134✔
2658
        }
2659
        return expression;
11,851✔
2660
    }
2661

2662
    private anonymousFunction(): Expression {
2663
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
11,890✔
2664
            const func = this.functionDeclaration(true);
85✔
2665
            //if there's an open paren after this, this is an IIFE
2666
            if (this.check(TokenKind.LeftParen)) {
85✔
2667
                return this.finishCall(this.advance(), func);
3✔
2668
            } else {
2669
                return func;
82✔
2670
            }
2671
        }
2672

2673
        let expr = this.boolean();
11,805✔
2674

2675
        if (this.check(TokenKind.Question)) {
11,766✔
2676
            return this.ternaryExpression(expr);
80✔
2677
        } else if (this.check(TokenKind.QuestionQuestion)) {
11,686✔
2678
            return this.nullCoalescingExpression(expr);
34✔
2679
        } else {
2680
            return expr;
11,652✔
2681
        }
2682
    }
2683

2684
    private boolean(): Expression {
2685
        let expr = this.relational();
11,805✔
2686

2687
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
11,766✔
2688
            let operator = this.previous();
32✔
2689
            let right = this.relational();
32✔
2690
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
32✔
2691
        }
2692

2693
        return expr;
11,766✔
2694
    }
2695

2696
    private relational(): Expression {
2697
        let expr = this.additive();
11,864✔
2698

2699
        while (
11,825✔
2700
            this.matchAny(
2701
                TokenKind.Equal,
2702
                TokenKind.LessGreater,
2703
                TokenKind.Greater,
2704
                TokenKind.GreaterEqual,
2705
                TokenKind.Less,
2706
                TokenKind.LessEqual
2707
            )
2708
        ) {
2709
            let operator = this.previous();
1,666✔
2710
            let right = this.additive();
1,666✔
2711
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,666✔
2712
        }
2713

2714
        return expr;
11,825✔
2715
    }
2716

2717
    // TODO: bitshift
2718

2719
    private additive(): Expression {
2720
        let expr = this.multiplicative();
13,530✔
2721

2722
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
13,491✔
2723
            let operator = this.previous();
1,338✔
2724
            let right = this.multiplicative();
1,338✔
2725
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,338✔
2726
        }
2727

2728
        return expr;
13,491✔
2729
    }
2730

2731
    private multiplicative(): Expression {
2732
        let expr = this.exponential();
14,868✔
2733

2734
        while (this.matchAny(
14,829✔
2735
            TokenKind.Forwardslash,
2736
            TokenKind.Backslash,
2737
            TokenKind.Star,
2738
            TokenKind.Mod,
2739
            TokenKind.LeftShift,
2740
            TokenKind.RightShift
2741
        )) {
2742
            let operator = this.previous();
53✔
2743
            let right = this.exponential();
53✔
2744
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
53✔
2745
        }
2746

2747
        return expr;
14,829✔
2748
    }
2749

2750
    private exponential(): Expression {
2751
        let expr = this.prefixUnary();
14,921✔
2752

2753
        while (this.match(TokenKind.Caret)) {
14,882✔
2754
            let operator = this.previous();
8✔
2755
            let right = this.prefixUnary();
8✔
2756
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
8✔
2757
        }
2758

2759
        return expr;
14,882✔
2760
    }
2761

2762
    private prefixUnary(): Expression {
2763
        const nextKind = this.peek().kind;
14,965✔
2764
        if (nextKind === TokenKind.Not) {
14,965✔
2765
            this.current++; //advance
27✔
2766
            let operator = this.previous();
27✔
2767
            let right = this.relational();
27✔
2768
            return new UnaryExpression({ operator: operator, right: right });
27✔
2769
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
14,938✔
2770
            this.current++; //advance
36✔
2771
            let operator = this.previous();
36✔
2772
            let right = (nextKind as any) === TokenKind.Not
36✔
2773
                ? this.boolean()
36!
2774
                : this.prefixUnary();
2775
            return new UnaryExpression({ operator: operator, right: right });
36✔
2776
        }
2777
        return this.call();
14,902✔
2778
    }
2779

2780
    private indexedGet(expr: Expression) {
2781
        let openingSquare = this.previous();
153✔
2782
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
153✔
2783
        let indexes: Expression[] = [];
153✔
2784

2785

2786
        //consume leading newlines
2787
        while (this.match(TokenKind.Newline)) { }
153✔
2788

2789
        try {
153✔
2790
            indexes.push(
153✔
2791
                this.expression()
2792
            );
2793
            //consume additional indexes separated by commas
2794
            while (this.check(TokenKind.Comma)) {
151✔
2795
                //discard the comma
2796
                this.advance();
17✔
2797
                indexes.push(
17✔
2798
                    this.expression()
2799
                );
2800
            }
2801
        } catch (error) {
2802
            this.rethrowNonDiagnosticError(error);
2✔
2803
        }
2804
        //consume trailing newlines
2805
        while (this.match(TokenKind.Newline)) { }
153✔
2806

2807
        const closingSquare = this.tryConsume(
153✔
2808
            DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
2809
            TokenKind.RightSquareBracket
2810
        );
2811

2812
        return new IndexedGetExpression({
153✔
2813
            obj: expr,
2814
            indexes: indexes,
2815
            openingSquare: openingSquare,
2816
            closingSquare: closingSquare,
2817
            questionDot: questionDotToken
2818
        });
2819
    }
2820

2821
    private newExpression() {
2822
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
134✔
2823
        let newToken = this.advance();
134✔
2824

2825
        let nameExpr = this.identifyingExpression();
134✔
2826
        let leftParen = this.tryConsume(
134✔
2827
            DiagnosticMessages.unexpectedToken(this.peek().text),
2828
            TokenKind.LeftParen,
2829
            TokenKind.QuestionLeftParen
2830
        );
2831

2832
        if (!leftParen) {
134✔
2833
            // new expression without a following call expression
2834
            // wrap the name in an expression
2835
            const endOfStatementLocation = util.createBoundingLocation(newToken, this.peek());
4✔
2836
            const exprStmt = nameExpr ?? createStringLiteral('', endOfStatementLocation);
4!
2837
            return new ExpressionStatement({ expression: exprStmt });
4✔
2838
        }
2839

2840
        let call = this.finishCall(leftParen, nameExpr);
130✔
2841
        //pop the call from the  callExpressions list because this is technically something else
2842
        this.callExpressions.pop();
130✔
2843
        let result = new NewExpression({ new: newToken, call: call });
130✔
2844
        return result;
130✔
2845
    }
2846

2847
    /**
2848
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2849
     */
2850
    private callfunc(callee: Expression): Expression {
2851
        this.warnIfNotBrighterScriptMode('callfunc operator');
31✔
2852
        let operator = this.previous();
31✔
2853
        let methodName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
31✔
2854
        // force it into an identifier so the AST makes some sense
2855
        methodName.kind = TokenKind.Identifier;
28✔
2856
        let openParen = this.consume(DiagnosticMessages.expectedOpenParenToFollowCallfuncIdentifier(), TokenKind.LeftParen);
28✔
2857
        let call = this.finishCall(openParen, callee, false);
28✔
2858

2859
        return new CallfuncExpression({
28✔
2860
            callee: callee,
2861
            operator: operator,
2862
            methodName: methodName as Identifier,
2863
            openingParen: openParen,
2864
            args: call.args,
2865
            closingParen: call.tokens.closingParen
2866
        });
2867
    }
2868

2869
    private call(): Expression {
2870
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
16,071✔
2871
            return this.newExpression();
134✔
2872
        }
2873
        let expr = this.primary();
15,937✔
2874

2875
        while (true) {
15,854✔
2876
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
20,439✔
2877
                expr = this.finishCall(this.previous(), expr);
2,127✔
2878
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
18,312✔
2879
                expr = this.indexedGet(expr);
151✔
2880
            } else if (this.match(TokenKind.Callfunc)) {
18,161✔
2881
                expr = this.callfunc(expr);
31✔
2882
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
18,130✔
2883
                if (this.match(TokenKind.LeftSquareBracket)) {
2,318✔
2884
                    expr = this.indexedGet(expr);
2✔
2885
                } else {
2886
                    let dot = this.previous();
2,316✔
2887
                    let name = this.tryConsume(
2,316✔
2888
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2889
                        TokenKind.Identifier,
2890
                        ...AllowedProperties
2891
                    );
2892
                    if (!name) {
2,316✔
2893
                        break;
39✔
2894
                    }
2895

2896
                    // force it into an identifier so the AST makes some sense
2897
                    name.kind = TokenKind.Identifier;
2,277✔
2898
                    expr = new DottedGetExpression({ obj: expr, name: name as Identifier, dot: dot });
2,277✔
2899
                }
2900

2901
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
15,812✔
2902
                let dot = this.advance();
11✔
2903
                let name = this.tryConsume(
11✔
2904
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2905
                    TokenKind.Identifier,
2906
                    ...AllowedProperties
2907
                );
2908

2909
                // force it into an identifier so the AST makes some sense
2910
                name.kind = TokenKind.Identifier;
11✔
2911
                if (!name) {
11!
UNCOV
2912
                    break;
×
2913
                }
2914
                expr = new XmlAttributeGetExpression({ obj: expr, name: name as Identifier, at: dot });
11✔
2915
                //only allow a single `@` expression
2916
                break;
11✔
2917

2918
            } else {
2919
                break;
15,801✔
2920
            }
2921
        }
2922

2923
        return expr;
15,851✔
2924
    }
2925

2926
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
2,260✔
2927
        let args = [] as Expression[];
2,318✔
2928
        while (this.match(TokenKind.Newline)) { }
2,318✔
2929

2930
        if (!this.check(TokenKind.RightParen)) {
2,318✔
2931
            do {
1,177✔
2932
                while (this.match(TokenKind.Newline)) { }
1,685✔
2933

2934
                if (args.length >= CallExpression.MaximumArguments) {
1,685!
UNCOV
2935
                    this.diagnostics.push({
×
2936
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
2937
                        location: this.peek()?.location
×
2938
                    });
UNCOV
2939
                    throw this.lastDiagnosticAsError();
×
2940
                }
2941
                try {
1,685✔
2942
                    args.push(this.expression());
1,685✔
2943
                } catch (error) {
2944
                    this.rethrowNonDiagnosticError(error);
5✔
2945
                    // we were unable to get an expression, so don't continue
2946
                    break;
5✔
2947
                }
2948
            } while (this.match(TokenKind.Comma));
2949
        }
2950

2951
        while (this.match(TokenKind.Newline)) { }
2,318✔
2952

2953
        const closingParen = this.tryConsume(
2,318✔
2954
            DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
2955
            TokenKind.RightParen
2956
        );
2957

2958
        let expression = new CallExpression({
2,318✔
2959
            callee: callee,
2960
            openingParen: openingParen,
2961
            args: args,
2962
            closingParen: closingParen
2963
        });
2964
        if (addToCallExpressionList) {
2,318✔
2965
            this.callExpressions.push(expression);
2,260✔
2966
        }
2967
        return expression;
2,318✔
2968
    }
2969

2970
    /**
2971
     * Creates a TypeExpression, which wraps standard ASTNodes that represent a BscType
2972
     */
2973
    private typeExpression(): TypeExpression {
2974
        const changedTokens: { token: Token; oldKind: TokenKind }[] = [];
1,467✔
2975
        try {
1,467✔
2976
            let expr: Expression = this.getTypeExpressionPart(changedTokens);
1,467✔
2977
            while (this.options.mode === ParseMode.BrighterScript && this.matchAny(TokenKind.Or)) {
1,467✔
2978
                // If we're in Brighterscript mode, allow union types with "or" between types
2979
                // TODO: Handle Union types in parens? eg. "(string or integer)"
2980
                let operator = this.previous();
32✔
2981
                let right = this.getTypeExpressionPart(changedTokens);
32✔
2982
                if (right) {
32!
2983
                    expr = new BinaryExpression({ left: expr, operator: operator, right: right });
32✔
2984
                } else {
UNCOV
2985
                    break;
×
2986
                }
2987
            }
2988
            if (expr) {
1,467!
2989
                return new TypeExpression({ expression: expr });
1,467✔
2990
            }
2991

2992
        } catch (error) {
2993
            // Something went wrong - reset the kind to what it was previously
UNCOV
2994
            for (const changedToken of changedTokens) {
×
UNCOV
2995
                changedToken.token.kind = changedToken.oldKind;
×
2996
            }
UNCOV
2997
            throw error;
×
2998
        }
2999
    }
3000

3001
    /**
3002
     * Gets a single "part" of a type of a potential Union type
3003
     * Note: this does not NEED to be part of a union type, but the logic is the same
3004
     *
3005
     * @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
3006
     * @returns an expression that was successfully parsed
3007
     */
3008
    private getTypeExpressionPart(changedTokens: { token: Token; oldKind: TokenKind }[]) {
3009
        let expr: VariableExpression | DottedGetExpression | TypedArrayExpression;
3010
        if (this.checkAny(...DeclarableTypes)) {
1,499✔
3011
            // if this is just a type, just use directly
3012
            expr = new VariableExpression({ name: this.advance() as Identifier });
1,014✔
3013
        } else {
3014
            if (this.checkAny(...AllowedTypeIdentifiers)) {
485✔
3015
                // Since the next token is allowed as a type identifier, change the kind
3016
                let nextToken = this.peek();
1✔
3017
                changedTokens.push({ token: nextToken, oldKind: nextToken.kind });
1✔
3018
                nextToken.kind = TokenKind.Identifier;
1✔
3019
            }
3020
            expr = this.identifyingExpression(AllowedTypeIdentifiers);
485✔
3021
        }
3022

3023
        //Check if it has square brackets, thus making it an array
3024
        if (expr && this.check(TokenKind.LeftSquareBracket)) {
1,499✔
3025
            if (this.options.mode === ParseMode.BrightScript) {
28✔
3026
                // typed arrays not allowed in Brightscript
3027
                this.warnIfNotBrighterScriptMode('typed arrays');
1✔
3028
                return expr;
1✔
3029
            }
3030

3031
            // Check if it is an array - that is, if it has `[]` after the type
3032
            // eg. `string[]` or `SomeKlass[]`
3033
            // This is while loop, so it supports multidimensional arrays (eg. integer[][])
3034
            while (this.check(TokenKind.LeftSquareBracket)) {
27✔
3035
                const leftBracket = this.advance();
29✔
3036
                if (this.check(TokenKind.RightSquareBracket)) {
29!
3037
                    const rightBracket = this.advance();
29✔
3038
                    expr = new TypedArrayExpression({ innerType: expr, leftBracket: leftBracket, rightBracket: rightBracket });
29✔
3039
                }
3040
            }
3041
        }
3042

3043
        return expr;
1,498✔
3044
    }
3045

3046
    private primary(): Expression {
3047
        switch (true) {
15,937✔
3048
            case this.matchAny(
15,937!
3049
                TokenKind.False,
3050
                TokenKind.True,
3051
                TokenKind.Invalid,
3052
                TokenKind.IntegerLiteral,
3053
                TokenKind.LongIntegerLiteral,
3054
                TokenKind.FloatLiteral,
3055
                TokenKind.DoubleLiteral,
3056
                TokenKind.StringLiteral
3057
            ):
3058
                return new LiteralExpression({ value: this.previous() });
7,124✔
3059

3060
            //capture source literals (LINE_NUM if brightscript, or a bunch of them if brighterscript)
3061
            case this.matchAny(TokenKind.LineNumLiteral, ...(this.options.mode === ParseMode.BrightScript ? [] : BrighterScriptSourceLiterals)):
8,813✔
3062
                return new SourceLiteralExpression({ value: this.previous() });
35✔
3063

3064
            //template string
3065
            case this.check(TokenKind.BackTick):
3066
                return this.templateString(false);
43✔
3067

3068
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
3069
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
16,887✔
3070
                return this.templateString(true);
8✔
3071

3072
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3073
                return new VariableExpression({ name: this.previous() as Identifier });
8,151✔
3074

3075
            case this.match(TokenKind.LeftParen):
3076
                let left = this.previous();
48✔
3077
                let expr = this.expression();
48✔
3078
                let right = this.consume(
47✔
3079
                    DiagnosticMessages.unmatchedLeftParenAfterExpression(),
3080
                    TokenKind.RightParen
3081
                );
3082
                return new GroupingExpression({ leftParen: left, rightParen: right, expression: expr });
47✔
3083

3084
            case this.matchAny(TokenKind.LeftSquareBracket):
3085
                return this.arrayLiteral();
139✔
3086

3087
            case this.match(TokenKind.LeftCurlyBrace):
3088
                return this.aaLiteral();
264✔
3089

3090
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
UNCOV
3091
                let token = Object.assign(this.previous(), {
×
3092
                    kind: TokenKind.Identifier
3093
                }) as Identifier;
UNCOV
3094
                return new VariableExpression({ name: token });
×
3095

3096
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
UNCOV
3097
                return this.anonymousFunction();
×
3098

3099
            case this.check(TokenKind.RegexLiteral):
3100
                return this.regexLiteralExpression();
45✔
3101

3102
            default:
3103
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
3104
                if (this.checkAny(...this.peekGlobalTerminators())) {
80!
3105
                    //don't throw a diagnostic, just return undefined
3106

3107
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
3108
                } else {
3109
                    this.diagnostics.push({
80✔
3110
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
3111
                        location: this.peek()?.location
240!
3112
                    });
3113
                    throw this.lastDiagnosticAsError();
80✔
3114
                }
3115
        }
3116
    }
3117

3118
    private arrayLiteral() {
3119
        let elements: Array<Expression> = [];
139✔
3120
        let openingSquare = this.previous();
139✔
3121

3122
        while (this.match(TokenKind.Newline)) {
139✔
3123
        }
3124
        let closingSquare: Token;
3125

3126
        if (!this.match(TokenKind.RightSquareBracket)) {
139✔
3127
            try {
102✔
3128
                elements.push(this.expression());
102✔
3129

3130
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
101✔
3131

3132
                    while (this.match(TokenKind.Newline)) {
138✔
3133

3134
                    }
3135

3136
                    if (this.check(TokenKind.RightSquareBracket)) {
138✔
3137
                        break;
24✔
3138
                    }
3139

3140
                    elements.push(this.expression());
114✔
3141
                }
3142
            } catch (error: any) {
3143
                this.rethrowNonDiagnosticError(error);
2✔
3144
            }
3145

3146
            closingSquare = this.tryConsume(
102✔
3147
                DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral(),
3148
                TokenKind.RightSquareBracket
3149
            );
3150
        } else {
3151
            closingSquare = this.previous();
37✔
3152
        }
3153

3154
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3155
        return new ArrayLiteralExpression({ elements: elements, open: openingSquare, close: closingSquare });
139✔
3156
    }
3157

3158
    private aaLiteral() {
3159
        let openingBrace = this.previous();
264✔
3160
        let members: Array<AAMemberExpression> = [];
264✔
3161

3162
        let key = () => {
264✔
3163
            let result = {
273✔
3164
                colonToken: null as Token,
3165
                keyToken: null as Token,
3166
                range: null as Range
3167
            };
3168
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
273✔
3169
                result.keyToken = this.identifier(...AllowedProperties);
242✔
3170
            } else if (this.check(TokenKind.StringLiteral)) {
31!
3171
                result.keyToken = this.advance();
31✔
3172
            } else {
UNCOV
3173
                this.diagnostics.push({
×
3174
                    ...DiagnosticMessages.unexpectedAAKey(),
3175
                    location: this.peek().location
3176
                });
UNCOV
3177
                throw this.lastDiagnosticAsError();
×
3178
            }
3179

3180
            result.colonToken = this.consume(
273✔
3181
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3182
                TokenKind.Colon
3183
            );
3184
            result.range = util.createBoundingRange(result.keyToken, result.colonToken);
272✔
3185
            return result;
272✔
3186
        };
3187

3188
        while (this.match(TokenKind.Newline)) { }
264✔
3189
        let closingBrace: Token;
3190
        if (!this.match(TokenKind.RightCurlyBrace)) {
264✔
3191
            let lastAAMember: AAMemberExpression;
3192
            try {
188✔
3193
                let k = key();
188✔
3194
                let expr = this.expression();
188✔
3195
                lastAAMember = new AAMemberExpression({
187✔
3196
                    key: k.keyToken,
3197
                    colon: k.colonToken,
3198
                    value: expr
3199
                });
3200
                members.push(lastAAMember);
187✔
3201

3202
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
187✔
3203
                    // collect comma at end of expression
3204
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
209✔
3205
                        (lastAAMember as DeepWriteable<AAMemberExpression>).tokens.comma = this.previous();
60✔
3206
                    }
3207

3208
                    this.consumeStatementSeparators(true);
209✔
3209

3210
                    if (this.check(TokenKind.RightCurlyBrace)) {
209✔
3211
                        break;
124✔
3212
                    }
3213
                    let k = key();
85✔
3214
                    let expr = this.expression();
84✔
3215
                    lastAAMember = new AAMemberExpression({
84✔
3216
                        key: k.keyToken,
3217
                        colon: k.colonToken,
3218
                        value: expr
3219
                    });
3220
                    members.push(lastAAMember);
84✔
3221

3222
                }
3223
            } catch (error: any) {
3224
                this.rethrowNonDiagnosticError(error);
2✔
3225
            }
3226

3227
            closingBrace = this.tryConsume(
188✔
3228
                DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral(),
3229
                TokenKind.RightCurlyBrace
3230
            );
3231
        } else {
3232
            closingBrace = this.previous();
76✔
3233
        }
3234

3235
        const aaExpr = new AALiteralExpression({ elements: members, open: openingBrace, close: closingBrace });
264✔
3236
        return aaExpr;
264✔
3237
    }
3238

3239
    /**
3240
     * Pop token if we encounter specified token
3241
     */
3242
    private match(tokenKind: TokenKind) {
3243
        if (this.check(tokenKind)) {
59,659✔
3244
            this.current++; //advance
6,009✔
3245
            return true;
6,009✔
3246
        }
3247
        return false;
53,650✔
3248
    }
3249

3250
    /**
3251
     * Pop token if we encounter a token in the specified list
3252
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3253
     */
3254
    private matchAny(...tokenKinds: TokenKind[]) {
3255
        for (let tokenKind of tokenKinds) {
209,340✔
3256
            if (this.check(tokenKind)) {
585,502✔
3257
                this.current++; //advance
55,500✔
3258
                return true;
55,500✔
3259
            }
3260
        }
3261
        return false;
153,840✔
3262
    }
3263

3264
    /**
3265
     * If the next series of tokens matches the given set of tokens, pop them all
3266
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
3267
     */
3268
    private matchSequence(...tokenKinds: TokenKind[]) {
3269
        const endIndex = this.current + tokenKinds.length;
18,164✔
3270
        for (let i = 0; i < tokenKinds.length; i++) {
18,164✔
3271
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
18,188!
3272
                return false;
18,161✔
3273
            }
3274
        }
3275
        this.current = endIndex;
3✔
3276
        return true;
3✔
3277
    }
3278

3279
    /**
3280
     * Get next token matching a specified list, or fail with an error
3281
     */
3282
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
3283
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
14,570✔
3284
        if (token) {
14,570✔
3285
            return token;
14,556✔
3286
        } else {
3287
            let error = new Error(diagnosticInfo.message);
14✔
3288
            (error as any).isDiagnostic = true;
14✔
3289
            throw error;
14✔
3290
        }
3291
    }
3292

3293
    /**
3294
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3295
     */
3296
    private consumeTokenIf(tokenKind: TokenKind) {
3297
        if (this.match(tokenKind)) {
3,445✔
3298
            return this.previous();
394✔
3299
        }
3300
    }
3301

3302
    private consumeToken(tokenKind: TokenKind) {
3303
        return this.consume(
1,884✔
3304
            DiagnosticMessages.expectedToken(tokenKind),
3305
            tokenKind
3306
        );
3307
    }
3308

3309
    /**
3310
     * Consume, or add a message if not found. But then continue and return undefined
3311
     */
3312
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3313
        const nextKind = this.peek().kind;
22,139✔
3314
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
47,260✔
3315

3316
        if (foundTokenKind) {
22,139✔
3317
            return this.advance();
22,032✔
3318
        }
3319
        this.diagnostics.push({
107✔
3320
            ...diagnostic,
3321
            location: this.peek()?.location
321!
3322
        });
3323
    }
3324

3325
    private tryConsumeToken(tokenKind: TokenKind) {
3326
        return this.tryConsume(
80✔
3327
            DiagnosticMessages.expectedToken(tokenKind),
3328
            tokenKind
3329
        );
3330
    }
3331

3332
    private consumeStatementSeparators(optional = false) {
9,884✔
3333
        //a comment or EOF mark the end of the statement
3334
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
30,119✔
3335
            return true;
607✔
3336
        }
3337
        let consumed = false;
29,512✔
3338
        //consume any newlines and colons
3339
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
29,512✔
3340
            consumed = true;
31,990✔
3341
        }
3342
        if (!optional && !consumed) {
29,512✔
3343
            this.diagnostics.push({
68✔
3344
                ...DiagnosticMessages.expectedNewlineOrColon(),
3345
                location: this.peek()?.location
204!
3346
            });
3347
        }
3348
        return consumed;
29,512✔
3349
    }
3350

3351
    private advance(): Token {
3352
        if (!this.isAtEnd()) {
50,906✔
3353
            this.current++;
50,892✔
3354
        }
3355
        return this.previous();
50,906✔
3356
    }
3357

3358
    private checkEndOfStatement(): boolean {
3359
        const nextKind = this.peek().kind;
6,993✔
3360
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
6,993✔
3361
    }
3362

3363
    private checkPrevious(tokenKind: TokenKind): boolean {
3364
        return this.previous()?.kind === tokenKind;
222!
3365
    }
3366

3367
    /**
3368
     * Check that the next token kind is the expected kind
3369
     * @param tokenKind the expected next kind
3370
     * @returns true if the next tokenKind is the expected value
3371
     */
3372
    private check(tokenKind: TokenKind): boolean {
3373
        const nextKind = this.peek().kind;
944,875✔
3374
        if (nextKind === TokenKind.Eof) {
944,875✔
3375
            return false;
11,795✔
3376
        }
3377
        return nextKind === tokenKind;
933,080✔
3378
    }
3379

3380
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3381
        const nextKind = this.peek().kind;
161,675✔
3382
        if (nextKind === TokenKind.Eof) {
161,675✔
3383
            return false;
1,092✔
3384
        }
3385
        return tokenKinds.includes(nextKind);
160,583✔
3386
    }
3387

3388
    private checkNext(tokenKind: TokenKind): boolean {
3389
        if (this.isAtEnd()) {
13,681!
UNCOV
3390
            return false;
×
3391
        }
3392
        return this.peekNext().kind === tokenKind;
13,681✔
3393
    }
3394

3395
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3396
        if (this.isAtEnd()) {
6,084!
UNCOV
3397
            return false;
×
3398
        }
3399
        const nextKind = this.peekNext().kind;
6,084✔
3400
        return tokenKinds.includes(nextKind);
6,084✔
3401
    }
3402

3403
    private isAtEnd(): boolean {
3404
        const peekToken = this.peek();
151,436✔
3405
        return !peekToken || peekToken.kind === TokenKind.Eof;
151,436✔
3406
    }
3407

3408
    private peekNext(): Token {
3409
        if (this.isAtEnd()) {
19,765!
UNCOV
3410
            return this.peek();
×
3411
        }
3412
        return this.tokens[this.current + 1];
19,765✔
3413
    }
3414

3415
    private peek(): Token {
3416
        return this.tokens[this.current];
1,310,610✔
3417
    }
3418

3419
    private previous(): Token {
3420
        return this.tokens[this.current - 1];
87,302✔
3421
    }
3422

3423
    /**
3424
     * Sometimes we catch an error that is a diagnostic.
3425
     * If that's the case, we want to continue parsing.
3426
     * Otherwise, re-throw the error
3427
     *
3428
     * @param error error caught in a try/catch
3429
     */
3430
    private rethrowNonDiagnosticError(error) {
3431
        if (!error.isDiagnostic) {
11!
UNCOV
3432
            throw error;
×
3433
        }
3434
    }
3435

3436
    /**
3437
     * Get the token that is {offset} indexes away from {this.current}
3438
     * @param offset the number of index steps away from current index to fetch
3439
     * @param tokenKinds the desired token must match one of these
3440
     * @example
3441
     * getToken(-1); //returns the previous token.
3442
     * getToken(0);  //returns current token.
3443
     * getToken(1);  //returns next token
3444
     */
3445
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3446
        const token = this.tokens[this.current + offset];
153✔
3447
        if (tokenKinds.includes(token.kind)) {
153✔
3448
            return token;
3✔
3449
        }
3450
    }
3451

3452
    private synchronize() {
3453
        this.advance(); // skip the erroneous token
83✔
3454

3455
        while (!this.isAtEnd()) {
83✔
3456
            if (this.ensureNewLineOrColon(true)) {
175✔
3457
                // end of statement reached
3458
                return;
58✔
3459
            }
3460

3461
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
117✔
3462
                case TokenKind.Namespace:
2!
3463
                case TokenKind.Class:
3464
                case TokenKind.Function:
3465
                case TokenKind.Sub:
3466
                case TokenKind.If:
3467
                case TokenKind.For:
3468
                case TokenKind.ForEach:
3469
                case TokenKind.While:
3470
                case TokenKind.Print:
3471
                case TokenKind.Return:
3472
                    // start parsing again from the next block starter or obvious
3473
                    // expression start
3474
                    return;
1✔
3475
            }
3476

3477
            this.advance();
116✔
3478
        }
3479
    }
3480

3481

3482
    public dispose() {
3483
    }
3484
}
3485

3486
export enum ParseMode {
1✔
3487
    BrightScript = 'BrightScript',
1✔
3488
    BrighterScript = 'BrighterScript'
1✔
3489
}
3490

3491
export interface ParseOptions {
3492
    /**
3493
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3494
     */
3495
    mode?: ParseMode;
3496
    /**
3497
     * A logger that should be used for logging. If omitted, a default logger is used
3498
     */
3499
    logger?: Logger;
3500
    /**
3501
     * Path to the file where this source code originated
3502
     */
3503
    srcPath?: string;
3504
    /**
3505
     * Should locations be tracked. If false, the `range` property will be omitted
3506
     * @default true
3507
     */
3508
    trackLocations?: boolean;
3509
    /**
3510
     *
3511
     */
3512
    bsConsts?: Map<string, boolean>;
3513
}
3514

3515

3516
class CancelStatementError extends Error {
3517
    constructor() {
3518
        super('CancelStatement');
2✔
3519
    }
3520
}
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