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

rokucommunity / brighterscript / #13028

05 Sep 2024 06:50PM UTC coverage: 86.418% (-1.5%) from 87.933%
#13028

push

web-flow
Merge c5c56b09a into 43cbf8b72

10784 of 13272 branches covered (81.25%)

Branch coverage included in aggregate %.

153 of 158 new or added lines in 7 files covered. (96.84%)

811 existing lines in 44 files now uncovered.

12484 of 13653 relevant lines covered (91.44%)

27481.68 hits per line

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

90.87
/src/parser/Parser.ts
1
import type { Token, Identifier } from '../lexer/Token';
2
import { isToken } from '../lexer/Token';
1✔
3
import type { BlockTerminator } 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
} from '../lexer/TokenKind';
19
import type {
20
    PrintSeparatorSpace,
21
    PrintSeparatorTab
22
} from './Statement';
23
import {
1✔
24
    AliasStatement,
25
    AssignmentStatement,
26
    Block,
27
    Body,
28
    CatchStatement,
29
    ContinueStatement,
30
    ClassStatement,
31
    ConstStatement,
32
    ConditionalCompileStatement,
33
    DimStatement,
34
    DottedSetStatement,
35
    EndStatement,
36
    EnumMemberStatement,
37
    EnumStatement,
38
    ExitStatement,
39
    ExpressionStatement,
40
    ForEachStatement,
41
    FieldStatement,
42
    ForStatement,
43
    FunctionStatement,
44
    GotoStatement,
45
    IfStatement,
46
    ImportStatement,
47
    IncrementStatement,
48
    IndexedSetStatement,
49
    InterfaceFieldStatement,
50
    InterfaceMethodStatement,
51
    InterfaceStatement,
52
    LabelStatement,
53
    LibraryStatement,
54
    MethodStatement,
55
    NamespaceStatement,
56
    PrintStatement,
57
    ReturnStatement,
58
    StopStatement,
59
    ThrowStatement,
60
    TryCatchStatement,
61
    WhileStatement,
62
    TypecastStatement,
63
    ConditionalCompileConstStatement,
64
    ConditionalCompileErrorStatement,
65
    AugmentedAssignmentStatement
66
} from './Statement';
67
import type { DiagnosticInfo } from '../DiagnosticMessages';
68
import { DiagnosticMessages } from '../DiagnosticMessages';
1✔
69
import { util } from '../util';
1✔
70
import {
1✔
71
    AALiteralExpression,
72
    AAMemberExpression,
73
    AnnotationExpression,
74
    ArrayLiteralExpression,
75
    BinaryExpression,
76
    CallExpression,
77
    CallfuncExpression,
78
    DottedGetExpression,
79
    EscapedCharCodeLiteralExpression,
80
    FunctionExpression,
81
    FunctionParameterExpression,
82
    GroupingExpression,
83
    IndexedGetExpression,
84
    LiteralExpression,
85
    NewExpression,
86
    NullCoalescingExpression,
87
    RegexLiteralExpression,
88
    SourceLiteralExpression,
89
    TaggedTemplateStringExpression,
90
    TemplateStringExpression,
91
    TemplateStringQuasiExpression,
92
    TernaryExpression,
93
    TypecastExpression,
94
    TypeExpression,
95
    TypedArrayExpression,
96
    UnaryExpression,
97
    VariableExpression,
98
    XmlAttributeGetExpression
99
} from './Expression';
100
import type { Range } from 'vscode-languageserver';
101
import type { Logger } from '../logging';
102
import { createLogger } from '../logging';
1✔
103
import { isAnnotationExpression, isCallExpression, isCallfuncExpression, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression, isConditionalCompileStatement, isLiteralBoolean, isTypecastExpression } from '../astUtils/reflection';
1✔
104
import { createStringLiteral, createToken } from '../astUtils/creators';
1✔
105
import type { Expression, Statement } from './AstNode';
106
import type { BsDiagnostic, DeepWriteable } from '../interfaces';
107

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

114
    /**
115
     * The current token index
116
     */
117
    public current: number;
118

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

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

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

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

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

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

153
    private globalTerminators = [] as TokenKind[][];
3,070✔
154

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

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

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

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

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

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

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

217
    private logger: Logger;
218

219
    private body() {
220
        const parentAnnotations = this.enterAnnotationBlock();
3,660✔
221

222
        let body = new Body({ statements: [] });
3,660✔
223
        if (this.tokens.length > 0) {
3,660✔
224
            this.consumeStatementSeparators(true);
3,659✔
225

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

251
        this.exitAnnotationBlock(parentAnnotations);
3,660✔
252
        return body;
3,660✔
253
    }
254

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

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

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

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

293
    private declaration(): Statement | AnnotationExpression | undefined {
294
        try {
12,675✔
295
            if (this.checkAny(TokenKind.HashConst)) {
12,675✔
296
                return this.conditionalCompileConstStatement();
20✔
297
            }
298
            if (this.checkAny(TokenKind.HashIf)) {
12,655✔
299
                return this.conditionalCompileStatement();
40✔
300
            }
301
            if (this.checkAny(TokenKind.HashError)) {
12,615✔
302
                return this.conditionalCompileErrorStatement();
9✔
303
            }
304

305
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
12,606✔
306
                return this.functionDeclaration(false);
2,938✔
307
            }
308

309
            if (this.checkLibrary()) {
9,668✔
310
                return this.libraryStatement();
12✔
311
            }
312

313
            if (this.checkAlias()) {
9,656✔
314
                return this.aliasStatement();
32✔
315
            }
316

317
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
9,624✔
318
                return this.constDeclaration();
152✔
319
            }
320

321
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
9,472✔
322
                return this.annotationExpression();
50✔
323
            }
324

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

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

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

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

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

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

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

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

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

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

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

467
    private interfaceDeclaration(): InterfaceStatement {
468
        this.warnIfNotBrighterScriptMode('interface declarations');
146✔
469

470
        const parentAnnotations = this.enterAnnotationBlock();
146✔
471

472
        const interfaceToken = this.consume(
146✔
473
            DiagnosticMessages.expectedKeyword(TokenKind.Interface),
474
            TokenKind.Interface
475
        );
476
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
146✔
477

478
        let extendsToken: Token;
479
        let parentInterfaceName: TypeExpression;
480

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

502
                let decl: Statement;
503

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

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

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

535
            //ensure statement separator
536
            this.consumeStatementSeparators();
220✔
537
        }
538

539
        //consume the final `end interface` token
540
        const endInterfaceToken = this.consumeToken(TokenKind.EndInterface);
146✔
541

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

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

561
        this.warnIfNotBrighterScriptMode('enum declarations');
159✔
562

563
        const parentAnnotations = this.enterAnnotationBlock();
159✔
564

565
        this.consumeStatementSeparators();
159✔
566

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

573
                //collect leading annotations
574
                if (this.check(TokenKind.At)) {
314!
UNCOV
575
                    this.annotationExpression();
×
576
                }
577

578
                //members
579
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
314!
580
                    decl = this.enumMemberStatement();
314✔
581
                }
582

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

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

603
        //consume the final `end interface` token
604
        const endEnumToken = this.consumeToken(TokenKind.EndEnum);
159✔
605

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

613
        this.exitAnnotationBlock(parentAnnotations);
158✔
614
        return result;
158✔
615
    }
616

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

623
        const parentAnnotations = this.enterAnnotationBlock();
663✔
624

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

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

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

648
        //ensure statement separator
649
        this.consumeStatementSeparators();
663✔
650

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

658
                if (this.check(TokenKind.At)) {
684✔
659
                    this.annotationExpression();
15✔
660
                }
661

662
                if (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private)) {
683✔
663
                    //use actual access modifier
664
                    accessModifier = this.advance();
93✔
665
                }
666

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

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

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

690
                    //fields
691
                } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
332✔
692

693
                    decl = this.fieldDeclaration(accessModifier);
318✔
694

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

703
                }
704

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

714
            //ensure statement separator
715
            this.consumeStatementSeparators();
684✔
716
        }
717

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

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

735
        this.exitAnnotationBlock(parentAnnotations);
663✔
736
        return result;
663✔
737
    }
738

739
    private fieldDeclaration(accessModifier: Token | null) {
740

741
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
318✔
742

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

766
        let name = this.consume(
318✔
767
            DiagnosticMessages.expectedClassFieldIdentifier(),
768
            TokenKind.Identifier,
769
            ...AllowedProperties
770
        ) as Identifier;
771

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

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

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

798
    /**
799
     * An array of CallExpression for the current function body
800
     */
801
    private callExpressions = [];
3,070✔
802

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

835
            if (isAnonymous) {
3,371✔
836
                leftParen = this.consume(
82✔
837
                    DiagnosticMessages.expectedLeftParenAfterCallable(functionTypeText),
838
                    TokenKind.LeftParen
839
                );
840
            } else {
841
                name = this.consume(
3,289✔
842
                    DiagnosticMessages.expectedNameAfterCallableKeyword(functionTypeText),
843
                    TokenKind.Identifier,
844
                    ...AllowedProperties
845
                ) as Identifier;
846
                leftParen = this.consume(
3,287✔
847
                    DiagnosticMessages.expectedLeftParenAfterCallableName(functionTypeText),
848
                    TokenKind.LeftParen
849
                );
850

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

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

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

880
            if (this.check(TokenKind.As)) {
3,368✔
881
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
256✔
882
            }
883

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

892
                return haveFoundOptional || !!param.defaultValue;
2,809✔
893
            }, false);
894

895
            this.consumeStatementSeparators(true);
3,368✔
896

897

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

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

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

915
            if (!body) {
3,368✔
916
                body = new Block({ statements: [] });
3✔
917
            }
918

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

930
            if (isAnonymous) {
3,368✔
931
                return func;
82✔
932
            } else {
933
                let result = new FunctionStatement({ name: name, func: func });
3,286✔
934
                return result;
3,286✔
935
            }
936
        } finally {
937
            this.namespaceAndFunctionDepth--;
3,371✔
938
            //restore the previous CallExpression list
939
            this.callExpressions = previousCallExpressions;
3,371✔
940
        }
941
    }
942

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

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

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

965
        let asToken: Token = null;
2,817✔
966
        if (this.check(TokenKind.As)) {
2,817✔
967
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
582✔
968

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

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

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

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

1000
        let operator = this.consume(
1,414✔
1001
            DiagnosticMessages.expectedOperatorAfterIdentifier([TokenKind.Equal], name.text),
1002
            ...[TokenKind.Equal]
1003
        );
1004
        let value = this.expression();
1,411✔
1005

1006
        let result = new AssignmentStatement({ equals: operator, name: name, value: value, as: asToken, typeExpression: typeExpression });
1,404✔
1007

1008
        return result;
1,404✔
1009
    }
1010

1011
    private augmentedAssignment(): AugmentedAssignmentStatement {
1012
        let item = this.expression();
58✔
1013

1014
        let operator = this.consume(
58✔
1015
            DiagnosticMessages.expectedToken(...CompoundAssignmentOperators),
1016
            ...CompoundAssignmentOperators
1017
        );
1018
        let value = this.expression();
58✔
1019

1020
        let result = new AugmentedAssignmentStatement({
58✔
1021
            item: item,
1022
            operator: operator,
1023
            value: value
1024
        });
1025

1026
        return result;
58✔
1027
    }
1028

1029
    private checkLibrary() {
1030
        let isLibraryToken = this.check(TokenKind.Library);
19,176✔
1031

1032
        //if we are at the top level, any line that starts with "library" should be considered a library statement
1033
        if (this.isAtRootLevel() && isLibraryToken) {
19,176✔
1034
            return true;
11✔
1035

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

1041
            //definitely not a library statement
1042
        } else {
1043
            return false;
19,164✔
1044
        }
1045
    }
1046

1047
    private checkAlias() {
1048
        let isAliasToken = this.check(TokenKind.Alias);
18,939✔
1049

1050
        //if we are at the top level, any line that starts with "alias" should be considered a alias statement
1051
        if (this.isAtRootLevel() && isAliasToken) {
18,939✔
1052
            return true;
30✔
1053

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

1059
            //definitely not a alias statement
1060
        } else {
1061
            return false;
18,907✔
1062
        }
1063
    }
1064

1065
    private statement(): Statement | undefined {
1066
        if (this.checkLibrary()) {
9,508!
UNCOV
1067
            return this.libraryStatement();
×
1068
        }
1069

1070
        if (this.check(TokenKind.Import)) {
9,508✔
1071
            return this.importStatement();
202✔
1072
        }
1073

1074
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
9,306✔
1075
            return this.typecastStatement();
23✔
1076
        }
1077

1078
        if (this.checkAlias()) {
9,283!
UNCOV
1079
            return this.aliasStatement();
×
1080
        }
1081

1082
        if (this.check(TokenKind.Stop)) {
9,283✔
1083
            return this.stopStatement();
15✔
1084
        }
1085

1086
        if (this.check(TokenKind.If)) {
9,268✔
1087
            return this.ifStatement();
1,041✔
1088
        }
1089

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

1095
        if (this.check(TokenKind.Throw)) {
8,201✔
1096
            return this.throwStatement();
10✔
1097
        }
1098

1099
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
8,191✔
1100
            return this.printStatement();
1,079✔
1101
        }
1102
        if (this.check(TokenKind.Dim)) {
7,112✔
1103
            return this.dimStatement();
40✔
1104
        }
1105

1106
        if (this.check(TokenKind.While)) {
7,072✔
1107
            return this.whileStatement();
28✔
1108
        }
1109

1110
        if (this.checkAny(TokenKind.Exit, TokenKind.ExitWhile)) {
7,044✔
1111
            return this.exitStatement();
20✔
1112
        }
1113

1114
        if (this.check(TokenKind.For)) {
7,024✔
1115
            return this.forStatement();
34✔
1116
        }
1117

1118
        if (this.check(TokenKind.ForEach)) {
6,990✔
1119
            return this.forEachStatement();
34✔
1120
        }
1121

1122
        if (this.check(TokenKind.End)) {
6,956✔
1123
            return this.endStatement();
7✔
1124
        }
1125

1126
        if (this.match(TokenKind.Return)) {
6,949✔
1127
            return this.returnStatement();
3,002✔
1128
        }
1129

1130
        if (this.check(TokenKind.Goto)) {
3,947✔
1131
            return this.gotoStatement();
11✔
1132
        }
1133

1134
        //the continue keyword (followed by `for`, `while`, or a statement separator)
1135
        if (this.check(TokenKind.Continue) && this.checkAnyNext(TokenKind.While, TokenKind.For, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
3,936✔
1136
            return this.continueStatement();
11✔
1137
        }
1138

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

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

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

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

1189
        if (this.check(TokenKind.Class)) {
2,351✔
1190
            return this.classDeclaration();
663✔
1191
        }
1192

1193
        if (this.check(TokenKind.Namespace)) {
1,688✔
1194
            return this.namespaceStatement();
606✔
1195
        }
1196

1197
        if (this.check(TokenKind.Enum)) {
1,082✔
1198
            return this.enumDeclaration();
159✔
1199
        }
1200

1201
        // TODO: support multi-statements
1202
        return this.setStatement();
923✔
1203
    }
1204

1205
    private whileStatement(): WhileStatement {
1206
        const whileKeyword = this.advance();
28✔
1207
        const condition = this.expression();
28✔
1208

1209
        this.consumeStatementSeparators();
27✔
1210

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

1225
        return new WhileStatement({
27✔
1226
            while: whileKeyword,
1227
            endWhile: endWhile,
1228
            condition: condition,
1229
            body: whileBlock
1230
        });
1231
    }
1232

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

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

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

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

1261
        const loopTypeToken = this.tryConsume(
19✔
1262
            DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
1263
            TokenKind.While, TokenKind.For
1264
        );
1265

1266
        return new ExitStatement({
19✔
1267
            exit: exitToken,
1268
            loopType: loopTypeToken
1269
        });
1270
    }
1271

1272
    private forStatement(): ForStatement {
1273
        const forToken = this.advance();
34✔
1274
        const initializer = this.assignment();
34✔
1275

1276
        //TODO: newline allowed?
1277

1278
        const toToken = this.advance();
33✔
1279
        const finalValue = this.expression();
33✔
1280
        let incrementExpression: Expression | undefined;
1281
        let stepToken: Token | undefined;
1282

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

1290
        this.consumeStatementSeparators();
33✔
1291

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

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

1320
    private forEachStatement(): ForEachStatement {
1321
        let forEach = this.advance();
34✔
1322
        let name = this.advance();
34✔
1323

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

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

1345
        this.consumeStatementSeparators();
34✔
1346

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

1356
        let endFor = this.advance();
34✔
1357

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

1368
    private namespaceStatement(): NamespaceStatement | undefined {
1369
        this.warnIfNotBrighterScriptMode('namespace');
606✔
1370
        let keyword = this.advance();
606✔
1371

1372
        this.namespaceAndFunctionDepth++;
606✔
1373

1374
        let name = this.identifyingExpression();
606✔
1375
        //set the current namespace name
1376

1377
        this.globalTerminators.push([TokenKind.EndNamespace]);
605✔
1378
        let body = this.body();
605✔
1379
        this.globalTerminators.pop();
605✔
1380

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

1392
        this.namespaceAndFunctionDepth--;
605✔
1393

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

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

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

1418
        let expr: DottedGetExpression | VariableExpression;
1419

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

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

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

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

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

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

1504
        return libStatement;
12✔
1505
    }
1506

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

1518
        return importStatement;
202✔
1519
    }
1520

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

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

1554
        let aliasStmt = new AliasStatement({
32✔
1555
            alias: aliasToken,
1556
            name: name,
1557
            equals: equals,
1558
            value: value
1559

1560
        });
1561

1562
        return aliasStmt;
32✔
1563
    }
1564

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

1574
        //optional arguments
1575
        if (this.check(TokenKind.LeftParen)) {
66✔
1576
            let leftParen = this.advance();
22✔
1577
            annotation.call = this.finishCall(leftParen, annotation, false);
22✔
1578
        }
1579
        return annotation;
66✔
1580
    }
1581

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

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

1594
        let consequent: Expression;
1595
        try {
76✔
1596
            consequent = this.expression();
76✔
1597
        } catch { }
1598

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

1604
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
76✔
1605

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

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

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

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

1642
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1643
        this.warnIfNotBrighterScriptMode('template string');
41✔
1644

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

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

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

1699
        //store the final set of quasis
1700
        quasis.push(
41✔
1701
            new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1702
        );
1703

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

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

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

1743
        let tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
26✔
1744

1745
        const peek = this.peek();
26✔
1746
        if (peek.kind !== TokenKind.Catch) {
26✔
1747
            this.diagnostics.push({
2✔
1748
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1749
                location: this.peek()?.location
6!
1750
            });
1751
        } else {
1752
            const catchToken = this.advance();
24✔
1753
            const exceptionVarToken = this.tryConsume(DiagnosticMessages.missingExceptionVarToFollowCatch(), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
24✔
1754
            if (exceptionVarToken) {
24✔
1755
                // force it into an identifier so the AST makes some sense
1756
                exceptionVarToken.kind = TokenKind.Identifier;
22✔
1757
            }
1758
            //ensure statement sepatator
1759
            this.consumeStatementSeparators();
24✔
1760
            const catchBranch = this.block(TokenKind.EndTry);
24✔
1761
            catchStmt = new CatchStatement({
24✔
1762
                catch: catchToken,
1763
                exceptionVariable: exceptionVarToken,
1764
                catchBranch: catchBranch
1765
            });
1766
        }
1767
        if (this.peek().kind !== TokenKind.EndTry) {
26✔
1768
            this.diagnostics.push({
2✔
1769
                ...DiagnosticMessages.expectedEndTryToTerminateTryCatch(),
1770
                location: this.peek().location
1771
            });
1772
        } else {
1773
            endTryToken = this.advance();
24✔
1774
        }
1775

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

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

1800
    private dimStatement() {
1801
        const dim = this.advance();
40✔
1802

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

1809
        let leftSquareBracket = this.tryConsume(DiagnosticMessages.missingLeftSquareBracketAfterDimIdentifier(), TokenKind.LeftSquareBracket);
40✔
1810

1811
        let expressions: Expression[] = [];
40✔
1812
        let expression: Expression;
1813
        do {
40✔
1814
            try {
76✔
1815
                expression = this.expression();
76✔
1816
                expressions.push(expression);
71✔
1817
                if (this.check(TokenKind.Comma)) {
71✔
1818
                    this.advance();
36✔
1819
                } else {
1820
                    // will also exit for right square braces
1821
                    break;
35✔
1822
                }
1823
            } catch (error) {
1824
            }
1825
        } while (expression);
1826

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

1843
    private nestedInlineConditionalCount = 0;
3,070✔
1844

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

1859
        const ifToken = this.advance();
1,963✔
1860

1861
        const condition = this.expression();
1,963✔
1862
        let thenBranch: Block;
1863
        let elseBranch: IfStatement | Block | undefined;
1864

1865
        let thenToken: Token | undefined;
1866
        let endIfToken: Token | undefined;
1867
        let elseToken: Token | undefined;
1868

1869
        //optional `then`
1870
        if (this.check(TokenKind.Then)) {
1,961✔
1871
            thenToken = this.advance();
1,567✔
1872
        }
1873

1874
        //is it inline or multi-line if?
1875
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
1,961✔
1876

1877
        if (isInlineIfThen) {
1,961✔
1878
            /*** PARSE INLINE IF STATEMENT ***/
1879
            if (!incrementNestedCount) {
48✔
1880
                this.nestedInlineConditionalCount++;
5✔
1881
            }
1882

1883
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
48✔
1884

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

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

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

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

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

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

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

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

1956
            thenBranch = this.blockConditionalBranch(ifToken);
1,913✔
1957

1958
            //ensure newline/colon before next keyword
1959
            this.ensureNewLineOrColon();
1,910✔
1960

1961
            //else branch
1962
            if (this.check(TokenKind.Else)) {
1,910✔
1963
                elseToken = this.advance();
1,533✔
1964

1965
                if (this.check(TokenKind.If)) {
1,533✔
1966
                    // recurse-read `else if`
1967
                    elseBranch = this.ifStatement();
912✔
1968

1969
                } else {
1970
                    elseBranch = this.blockConditionalBranch(ifToken);
621✔
1971

1972
                    //ensure newline/colon before next keyword
1973
                    this.ensureNewLineOrColon();
621✔
1974
                }
1975
            }
1976

1977
            if (!isIfStatement(elseBranch)) {
1,910✔
1978
                if (this.check(TokenKind.EndIf)) {
998✔
1979
                    endIfToken = this.advance();
995✔
1980

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

1991
        return new IfStatement({
1,954✔
1992
            if: ifToken,
1993
            then: thenToken,
1994
            endIf: endIfToken,
1995
            else: elseToken,
1996
            condition: condition,
1997
            thenBranch: thenBranch,
1998
            elseBranch: elseBranch
1999
        });
2000
    }
2001

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

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

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

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

2028
    private conditionalCompileStatement(): ConditionalCompileStatement {
2029
        const hashIfToken = this.advance();
55✔
2030
        let notToken: Token | undefined;
2031

2032
        if (this.check(TokenKind.Not)) {
55✔
2033
            notToken = this.advance();
7✔
2034
        }
2035

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

2043

2044
        const condition = this.advance();
55✔
2045

2046
        let thenBranch: Block;
2047
        let elseBranch: ConditionalCompileStatement | Block | undefined;
2048

2049
        let hashEndIfToken: Token | undefined;
2050
        let hashElseToken: Token | undefined;
2051

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

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

2063
        this.ensureNewLine();
54✔
2064
        this.advance();
54✔
2065

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

2072
        } else if (this.check(TokenKind.HashElse)) {
39✔
2073
            hashElseToken = this.advance();
9✔
2074
            let diagnosticsLengthBeforeBlock = this.diagnostics.length;
9✔
2075
            elseBranch = this.blockConditionalCompileBranch(hashIfToken);
9✔
2076

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

2085
        if (!isConditionalCompileStatement(elseBranch)) {
54✔
2086

2087
            if (this.check(TokenKind.HashEndIf)) {
39!
2088
                hashEndIfToken = this.advance();
39✔
2089

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

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

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

2116
        //parsing until trailing "#end if", "#else", "#else if"
2117
        let branch = this.conditionalCompileBlock();
64✔
2118

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

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

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

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

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

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

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

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

2177
                //consume potential separators
2178
                this.consumeStatementSeparators(true);
1✔
2179
            }
2180
        }
2181
        this.globalTerminators.pop();
64✔
2182

2183

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

2215
    private conditionalCompileConstStatement() {
2216
        const hashConstToken = this.advance();
20✔
2217

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

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

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

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

2262
        return new ConditionalCompileConstStatement({ hashConst: hashConstToken, assignment: assignment });
17✔
2263
    }
2264

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

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

2283
    private ensureNewLineOrColon(silent = false) {
2,531✔
2284
        const prev = this.previous().kind;
2,711✔
2285
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
2,711✔
2286
            if (!silent) {
128✔
2287
                this.diagnostics.push({
6✔
2288
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2289
                    location: this.peek().location
2290
                });
2291
            }
2292
            return false;
128✔
2293
        }
2294
        return true;
2,583✔
2295
    }
2296

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

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

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

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

2347
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2348
        let expressionStart = this.peek();
542✔
2349

2350
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
542✔
2351
            let operator = this.advance();
23✔
2352

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

2367
            const result = new IncrementStatement({ value: expr, operator: operator });
21✔
2368
            return result;
21✔
2369
        }
2370

2371
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
519✔
2372
            return new ExpressionStatement({ expression: expr });
440✔
2373
        }
2374

2375
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an inclosing ExpressionStatement
2376
        this.diagnostics.push({
79✔
2377
            ...DiagnosticMessages.expectedStatementOrFunctionCallButReceivedExpression(),
2378
            location: expressionStart.location
2379
        });
2380
        return new ExpressionStatement({ expression: expr });
79✔
2381
    }
2382

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

2396
            // Create a dotted or indexed "set" based on the left-hand side's type
2397
            if (isIndexedGetExpression(left)) {
317✔
2398
                return new IndexedSetStatement({
23✔
2399
                    obj: left.obj,
2400
                    indexes: left.indexes,
2401
                    value: right,
2402
                    openingSquare: left.tokens.openingSquare,
2403
                    closingSquare: left.tokens.closingSquare,
2404
                    equals: operator
2405
                });
2406
            } else if (isDottedGetExpression(left)) {
294✔
2407
                return new DottedSetStatement({
291✔
2408
                    obj: left.obj,
2409
                    name: left.tokens.name,
2410
                    value: right,
2411
                    dot: left.tokens.dot,
2412
                    equals: operator
2413
                });
2414
            }
2415
        } else if (this.checkAny(...CompoundAssignmentOperators) && !(isCallExpression(expr))) {
559✔
2416
            let left = expr;
20✔
2417
            let operator = this.advance();
20✔
2418
            let right = this.expression();
20✔
2419
            return new AugmentedAssignmentStatement({
20✔
2420
                item: left,
2421
                operator: operator,
2422
                value: right
2423
            });
2424
        }
2425
        return this.expressionStatement(expr);
542✔
2426
    }
2427

2428
    private printStatement(): PrintStatement {
2429
        let printKeyword = this.advance();
1,079✔
2430

2431
        let values: (
2432
            | Expression
2433
            | PrintSeparatorTab
2434
            | PrintSeparatorSpace)[] = [];
1,079✔
2435

2436
        while (!this.checkEndOfStatement()) {
1,079✔
2437
            if (this.check(TokenKind.Semicolon)) {
1,188✔
2438
                values.push(this.advance() as PrintSeparatorSpace);
29✔
2439
            } else if (this.check(TokenKind.Comma)) {
1,159✔
2440
                values.push(this.advance() as PrintSeparatorTab);
13✔
2441
            } else if (this.check(TokenKind.Else)) {
1,146✔
2442
                break; // inline branch
22✔
2443
            } else {
2444
                values.push(this.expression());
1,124✔
2445
            }
2446
        }
2447

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

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

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

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

2470
        if (this.checkEndOfStatement()) {
3,002✔
2471
            return new ReturnStatement(options);
9✔
2472
        }
2473

2474
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
2,993✔
2475
        return new ReturnStatement({ ...options, value: toReturn });
2,992✔
2476
    }
2477

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

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

2495
        return new LabelStatement(options);
9✔
2496
    }
2497

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

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

2524
        return new GotoStatement(tokens);
9✔
2525
    }
2526

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

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

2543
        return new StopStatement(options);
15✔
2544
    }
2545

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

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

2568
                //ensure statement separator
2569
                this.consumeStatementSeparators();
7,088✔
2570

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

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

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

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

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

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

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

2618
    enterAnnotationBlock() {
2619
        const pending = this.pendingAnnotations;
10,738✔
2620
        this.pendingAnnotations = [];
10,738✔
2621
        return pending;
10,738✔
2622
    }
2623

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

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

2656
            } while (asToken && typeExpression);
128✔
2657
        }
2658
        return expression;
11,392✔
2659
    }
2660

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

2672
        let expr = this.boolean();
11,349✔
2673

2674
        if (this.check(TokenKind.Question)) {
11,310✔
2675
            return this.ternaryExpression(expr);
76✔
2676
        } else if (this.check(TokenKind.QuestionQuestion)) {
11,234✔
2677
            return this.nullCoalescingExpression(expr);
32✔
2678
        } else {
2679
            return expr;
11,202✔
2680
        }
2681
    }
2682

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

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

2692
        return expr;
11,310✔
2693
    }
2694

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

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

2713
        return expr;
11,364✔
2714
    }
2715

2716
    // TODO: bitshift
2717

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

2721
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
12,991✔
2722
            let operator = this.previous();
1,301✔
2723
            let right = this.multiplicative();
1,301✔
2724
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,301✔
2725
        }
2726

2727
        return expr;
12,991✔
2728
    }
2729

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

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

2746
        return expr;
14,292✔
2747
    }
2748

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

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

2758
        return expr;
14,345✔
2759
    }
2760

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

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

2784

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

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

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

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

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

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

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

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

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

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

2868
    private call(): Expression {
2869
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
15,290✔
2870
            return this.newExpression();
131✔
2871
        }
2872
        let expr = this.primary();
15,159✔
2873

2874
        while (true) {
15,076✔
2875
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
19,534✔
2876
                expr = this.finishCall(this.previous(), expr);
2,073✔
2877
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
17,461✔
2878
                expr = this.indexedGet(expr);
140✔
2879
            } else if (this.match(TokenKind.Callfunc)) {
17,321✔
2880
                expr = this.callfunc(expr);
28✔
2881
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
17,293✔
2882
                if (this.match(TokenKind.LeftSquareBracket)) {
2,259✔
2883
                    expr = this.indexedGet(expr);
2✔
2884
                } else {
2885
                    let dot = this.previous();
2,257✔
2886
                    let name = this.tryConsume(
2,257✔
2887
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2888
                        TokenKind.Identifier,
2889
                        ...AllowedProperties
2890
                    );
2891
                    if (!name) {
2,257✔
2892
                        break;
39✔
2893
                    }
2894

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

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

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

2917
            } else {
2918
                break;
15,025✔
2919
            }
2920
        }
2921

2922
        return expr;
15,073✔
2923
    }
2924

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

2929
        if (!this.check(TokenKind.RightParen)) {
2,250✔
2930
            do {
1,147✔
2931
                while (this.match(TokenKind.Newline)) { }
1,638✔
2932

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

2950
        while (this.match(TokenKind.Newline)) { }
2,250✔
2951

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

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

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

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

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

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

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

3042
        return expr;
1,451✔
3043
    }
3044

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

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

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

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

3071
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3072
                return new VariableExpression({ name: this.previous() as Identifier });
7,673✔
3073

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

3083
            case this.matchAny(TokenKind.LeftSquareBracket):
3084
                return this.arrayLiteral();
133✔
3085

3086
            case this.match(TokenKind.LeftCurlyBrace):
3087
                return this.aaLiteral();
252✔
3088

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

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

3098
            case this.check(TokenKind.RegexLiteral):
3099
                return this.regexLiteralExpression();
44✔
3100

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

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

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

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

3125
        if (!this.match(TokenKind.RightSquareBracket)) {
133✔
3126
            try {
98✔
3127
                elements.push(this.expression());
98✔
3128

3129
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
97✔
3130

3131
                    while (this.match(TokenKind.Newline)) {
130✔
3132

3133
                    }
3134

3135
                    if (this.check(TokenKind.RightSquareBracket)) {
130✔
3136
                        break;
23✔
3137
                    }
3138

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

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

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

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

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

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

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

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

3207
                    this.consumeStatementSeparators(true);
203✔
3208

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

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

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

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

3238
    /**
3239
     * Pop token if we encounter specified token
3240
     */
3241
    private match(tokenKind: TokenKind) {
3242
        if (this.check(tokenKind)) {
57,265✔
3243
            this.current++; //advance
5,797✔
3244
            return true;
5,797✔
3245
        }
3246
        return false;
51,468✔
3247
    }
3248

3249
    /**
3250
     * Pop token if we encounter a token in the specified list
3251
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3252
     */
3253
    private matchAny(...tokenKinds: TokenKind[]) {
3254
        for (let tokenKind of tokenKinds) {
199,985✔
3255
            if (this.check(tokenKind)) {
559,327✔
3256
                this.current++; //advance
53,245✔
3257
                return true;
53,245✔
3258
            }
3259
        }
3260
        return false;
146,740✔
3261
    }
3262

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

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

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

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

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

3315
        if (foundTokenKind) {
21,344✔
3316
            return this.advance();
21,235✔
3317
        }
3318
        this.diagnostics.push({
109✔
3319
            ...diagnostic,
3320
            location: this.peek()?.location
327!
3321
        });
3322
    }
3323

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

3331
    private consumeStatementSeparators(optional = false) {
9,482✔
3332
        //a comment or EOF mark the end of the statement
3333
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
28,314✔
3334
            return true;
384✔
3335
        }
3336
        let consumed = false;
27,930✔
3337
        //consume any newlines and colons
3338
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
27,930✔
3339
            consumed = true;
30,705✔
3340
        }
3341
        if (!optional && !consumed) {
27,930✔
3342
            this.diagnostics.push({
65✔
3343
                ...DiagnosticMessages.expectedNewlineOrColon(),
3344
                location: this.peek()?.location
195!
3345
            });
3346
        }
3347
        return consumed;
27,930✔
3348
    }
3349

3350
    private advance(): Token {
3351
        if (!this.isAtEnd()) {
48,900✔
3352
            this.current++;
48,886✔
3353
        }
3354
        return this.previous();
48,900✔
3355
    }
3356

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

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

3366
    /**
3367
     * Check that the next token kind is the expected kind
3368
     * @param tokenKind the expected next kind
3369
     * @returns true if the next tokenKind is the expected value
3370
     */
3371
    private check(tokenKind: TokenKind): boolean {
3372
        const nextKind = this.peek().kind;
901,661✔
3373
        if (nextKind === TokenKind.Eof) {
901,661✔
3374
            return false;
9,688✔
3375
        }
3376
        return nextKind === tokenKind;
891,973✔
3377
    }
3378

3379
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3380
        const nextKind = this.peek().kind;
151,217✔
3381
        if (nextKind === TokenKind.Eof) {
151,217✔
3382
            return false;
225✔
3383
        }
3384
        return tokenKinds.includes(nextKind);
150,992✔
3385
    }
3386

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

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

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

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

3414
    private peek(): Token {
3415
        return this.tokens[this.current];
1,246,460✔
3416
    }
3417

3418
    private previous(): Token {
3419
        return this.tokens[this.current - 1];
83,942✔
3420
    }
3421

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

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

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

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

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

3476
            this.advance();
121✔
3477
        }
3478
    }
3479

3480

3481
    public dispose() {
3482
    }
3483
}
3484

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

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

3514

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