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

rokucommunity / brighterscript / #14044

20 Mar 2025 07:09PM UTC coverage: 87.163% (-2.0%) from 89.117%
#14044

push

web-flow
Merge e33b1f944 into 0eceb0830

13257 of 16072 branches covered (82.49%)

Branch coverage included in aggregate %.

1163 of 1279 new or added lines in 24 files covered. (90.93%)

802 existing lines in 52 files now uncovered.

14323 of 15570 relevant lines covered (91.99%)

21312.85 hits per line

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

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

106

107
const declarableTypesLower = DeclarableTypes.map(tokenKind => tokenKind.toLowerCase());
10✔
108

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

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

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

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

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

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

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

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

154
    private globalTerminators = [] as TokenKind[][];
3,832✔
155

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

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

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

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

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

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

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

218
    private logger: Logger;
219

220
    private body() {
221
        const parentAnnotations = this.enterAnnotationBlock();
4,462✔
222

223
        let body = new Body({ statements: [] });
4,462✔
224
        if (this.tokens.length > 0) {
4,462✔
225
            this.consumeStatementSeparators(true);
4,461✔
226

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

252
        this.exitAnnotationBlock(parentAnnotations);
4,462✔
253
        return body;
4,462✔
254
    }
255

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

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

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

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

294
    private declaration(): Statement | AnnotationExpression | undefined {
295
        try {
14,597✔
296
            if (this.checkAny(TokenKind.HashConst)) {
14,597✔
297
                return this.conditionalCompileConstStatement();
21✔
298
            }
299
            if (this.checkAny(TokenKind.HashIf)) {
14,576✔
300
                return this.conditionalCompileStatement();
41✔
301
            }
302
            if (this.checkAny(TokenKind.HashError)) {
14,535✔
303
                return this.conditionalCompileErrorStatement();
10✔
304
            }
305

306
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
14,525✔
307
                return this.functionDeclaration(false);
3,437✔
308
            }
309

310
            if (this.checkLibrary()) {
11,088✔
311
                return this.libraryStatement();
13✔
312
            }
313

314
            if (this.checkAlias()) {
11,075✔
315
                return this.aliasStatement();
33✔
316
            }
317

318
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
11,042✔
319
                return this.constDeclaration();
160✔
320
            }
321

322
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
10,882✔
323
                return this.annotationExpression();
58✔
324
            }
325

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

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

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

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

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

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

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

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

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

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

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

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

471
        const parentAnnotations = this.enterAnnotationBlock();
173✔
472

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

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

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

503
                let decl: Statement;
504

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

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

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

536
            //ensure statement separator
537
            this.consumeStatementSeparators();
252✔
538
        }
539

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

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

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

562
        this.warnIfNotBrighterScriptMode('enum declarations');
179✔
563

564
        const parentAnnotations = this.enterAnnotationBlock();
179✔
565

566
        this.consumeStatementSeparators();
179✔
567

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

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

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

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

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

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

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

614
        this.exitAnnotationBlock(parentAnnotations);
178✔
615
        return result;
178✔
616
    }
617

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

624
        const parentAnnotations = this.enterAnnotationBlock();
695✔
625

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

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

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

649
        //ensure statement separator
650
        this.consumeStatementSeparators();
695✔
651

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

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

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

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

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

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

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

694
                    decl = this.fieldDeclaration(accessModifier);
340✔
695

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

704
                }
705

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

715
            //ensure statement separator
716
            this.consumeStatementSeparators();
723✔
717
        }
718

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

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

736
        this.exitAnnotationBlock(parentAnnotations);
695✔
737
        return result;
695✔
738
    }
739

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

742
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
340✔
743

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

767
        let name = this.consume(
340✔
768
            DiagnosticMessages.expectedIdentifier(),
769
            TokenKind.Identifier,
770
            ...AllowedProperties
771
        ) as Identifier;
772

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

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

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

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

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

836
            if (isAnonymous) {
3,893✔
837
                leftParen = this.consume(
88✔
838
                    DiagnosticMessages.expectedToken('('),
839
                    TokenKind.LeftParen
840
                );
841
            } else {
842
                name = this.consume(
3,805✔
843
                    DiagnosticMessages.expectedIdentifier(functionTypeText),
844
                    TokenKind.Identifier,
845
                    ...AllowedProperties
846
                ) as Identifier;
847
                leftParen = this.consume(
3,803✔
848
                    DiagnosticMessages.expectedToken('('),
849
                    TokenKind.LeftParen
850
                );
851

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

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

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

881
            if (this.check(TokenKind.As)) {
3,887✔
882
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
318✔
883
            }
884

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

893
                return haveFoundOptional || !!param.defaultValue;
3,092✔
894
            }, false);
895

896
            this.consumeStatementSeparators(true);
3,887✔
897

898

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

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

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

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

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

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

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

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

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

966
        let asToken: Token = null;
3,103✔
967
        if (this.check(TokenKind.As)) {
3,103✔
968
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
658✔
969

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

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

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

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

1001
        let operator = this.consume(
1,572✔
1002
            DiagnosticMessages.expectedOperator([TokenKind.Equal], name.text),
1003
            ...[TokenKind.Equal]
1004
        );
1005
        let value = this.expression();
1,569✔
1006

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

1009
        return result;
1,562✔
1010
    }
1011

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

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

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

1027
        return result;
73✔
1028
    }
1029

1030
    private checkLibrary() {
1031
        let isLibraryToken = this.check(TokenKind.Library);
21,998✔
1032

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

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

1042
            //definitely not a library statement
1043
        } else {
1044
            return false;
21,985✔
1045
        }
1046
    }
1047

1048
    private checkAlias() {
1049
        let isAliasToken = this.check(TokenKind.Alias);
21,748✔
1050

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

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

1060
            //definitely not a alias statement
1061
        } else {
1062
            return false;
21,715✔
1063
        }
1064
    }
1065

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

1071
        if (this.check(TokenKind.Import)) {
10,910✔
1072
            return this.importStatement();
211✔
1073
        }
1074

1075
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
10,699✔
1076
            return this.typecastStatement();
26✔
1077
        }
1078

1079
        if (this.checkAlias()) {
10,673!
UNCOV
1080
            return this.aliasStatement();
×
1081
        }
1082

1083
        if (this.check(TokenKind.Stop)) {
10,673✔
1084
            return this.stopStatement();
16✔
1085
        }
1086

1087
        if (this.check(TokenKind.If)) {
10,657✔
1088
            return this.ifStatement();
1,154✔
1089
        }
1090

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

1096
        if (this.check(TokenKind.Throw)) {
9,468✔
1097
            return this.throwStatement();
12✔
1098
        }
1099

1100
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
9,456✔
1101
            return this.printStatement();
1,252✔
1102
        }
1103
        if (this.check(TokenKind.Dim)) {
8,204✔
1104
            return this.dimStatement();
43✔
1105
        }
1106

1107
        if (this.check(TokenKind.While)) {
8,161✔
1108
            return this.whileStatement();
32✔
1109
        }
1110

1111
        if (this.checkAny(TokenKind.Exit, TokenKind.ExitWhile)) {
8,129✔
1112
            return this.exitStatement();
22✔
1113
        }
1114

1115
        if (this.check(TokenKind.For)) {
8,107✔
1116
            return this.forStatement();
41✔
1117
        }
1118

1119
        if (this.check(TokenKind.ForEach)) {
8,066✔
1120
            return this.forEachStatement();
40✔
1121
        }
1122

1123
        if (this.check(TokenKind.End)) {
8,026✔
1124
            return this.endStatement();
8✔
1125
        }
1126

1127
        if (this.match(TokenKind.Return)) {
8,018✔
1128
            return this.returnStatement();
3,363✔
1129
        }
1130

1131
        if (this.check(TokenKind.Goto)) {
4,655✔
1132
            return this.gotoStatement();
12✔
1133
        }
1134

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

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

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

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

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

1190
        if (this.check(TokenKind.Class)) {
2,864✔
1191
            return this.classDeclaration();
695✔
1192
        }
1193

1194
        if (this.check(TokenKind.Namespace)) {
2,169✔
1195
            return this.namespaceStatement();
643✔
1196
        }
1197

1198
        if (this.check(TokenKind.Enum)) {
1,526✔
1199
            return this.enumDeclaration();
179✔
1200
        }
1201

1202
        // TODO: support multi-statements
1203
        return this.setStatement();
1,347✔
1204
    }
1205

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

1210
        this.consumeStatementSeparators();
31✔
1211

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

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

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

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

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

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

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

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

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

1277
        //TODO: newline allowed?
1278

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

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

1291
        this.consumeStatementSeparators();
40✔
1292

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

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

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

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

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

1346
        this.consumeStatementSeparators();
40✔
1347

1348
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
40✔
1349
        let endForToken: Token;
1350
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
40✔
1351

1352
            this.diagnostics.push({
1✔
1353
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(forEach.text),
1354
                location: this.peek().location
1355
            });
1356
            throw this.lastDiagnosticAsError();
1✔
1357
        }
1358
        endForToken = this.advance();
39✔
1359

1360
        return new ForEachStatement({
39✔
1361
            forEach: forEach,
1362
            in: maybeIn,
1363
            endFor: endForToken,
1364
            item: name,
1365
            target: target,
1366
            body: body
1367
        });
1368
    }
1369

1370
    private namespaceStatement(): NamespaceStatement | undefined {
1371
        this.warnIfNotBrighterScriptMode('namespace');
643✔
1372
        let keyword = this.advance();
643✔
1373

1374
        this.namespaceAndFunctionDepth++;
643✔
1375

1376
        let name = this.identifyingExpression();
643✔
1377
        //set the current namespace name
1378

1379
        this.globalTerminators.push([TokenKind.EndNamespace]);
642✔
1380
        let body = this.body();
642✔
1381
        this.globalTerminators.pop();
642✔
1382

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

1394
        this.namespaceAndFunctionDepth--;
642✔
1395

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

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

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

1420
        let expr: DottedGetExpression | VariableExpression;
1421

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

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

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

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

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

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

1506
        return libStatement;
13✔
1507
    }
1508

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

1520
        return importStatement;
211✔
1521
    }
1522

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

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

1556
        let aliasStmt = new AliasStatement({
33✔
1557
            alias: aliasToken,
1558
            name: name,
1559
            equals: equals,
1560
            value: value
1561

1562
        });
1563

1564
        return aliasStmt;
33✔
1565
    }
1566

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

1576
        //optional arguments
1577
        if (this.check(TokenKind.LeftParen)) {
74✔
1578
            let leftParen = this.advance();
30✔
1579
            annotation.call = this.finishCall(leftParen, annotation, false);
30✔
1580
        }
1581
        return annotation;
74✔
1582
    }
1583

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

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

1596
        let consequent: Expression;
1597
        try {
97✔
1598
            consequent = this.expression();
97✔
1599
        } catch { }
1600

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

1606
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
97✔
1607

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

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

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

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

1644
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1645
        this.warnIfNotBrighterScriptMode('template string');
51✔
1646

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

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

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

1701
        //store the final set of quasis
1702
        quasis.push(
51✔
1703
            new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1704
        );
1705

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

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

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

1745
        let tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
35✔
1746

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

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

1766
            const catchBranch = this.block(TokenKind.EndTry);
33✔
1767
            catchStmt = new CatchStatement({
33✔
1768
                catch: catchToken,
1769
                exceptionVariableExpression: exceptionVariableExpression,
1770
                catchBranch: catchBranch
1771
            });
1772
        }
1773
        if (this.peek().kind !== TokenKind.EndTry) {
35✔
1774
            this.diagnostics.push({
2✔
1775
                ...DiagnosticMessages.expectedTerminator('end try', 'try-catch'),
1776
                location: this.peek().location
1777
            });
1778
        } else {
1779
            endTryToken = this.advance();
33✔
1780
        }
1781

1782
        const statement = new TryCatchStatement({
35✔
1783
            try: tryToken,
1784
            tryBranch: tryBranch,
1785
            catchStatement: catchStmt,
1786
            endTry: endTryToken
1787
        }
1788
        );
1789
        return statement;
35✔
1790
    }
1791

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

1806
    private dimStatement() {
1807
        const dim = this.advance();
43✔
1808

1809
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
43✔
1810
        // force to an identifier so the AST makes some sense
1811
        if (identifier) {
43✔
1812
            identifier.kind = TokenKind.Identifier;
41✔
1813
        }
1814

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

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

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

1849
    private nestedInlineConditionalCount = 0;
3,832✔
1850

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

1865
        const ifToken = this.advance();
2,149✔
1866

1867
        const condition = this.expression();
2,149✔
1868
        let thenBranch: Block;
1869
        let elseBranch: IfStatement | Block | undefined;
1870

1871
        let thenToken: Token | undefined;
1872
        let endIfToken: Token | undefined;
1873
        let elseToken: Token | undefined;
1874

1875
        //optional `then`
1876
        if (this.check(TokenKind.Then)) {
2,147✔
1877
            thenToken = this.advance();
1,712✔
1878
        }
1879

1880
        //is it inline or multi-line if?
1881
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
2,147✔
1882

1883
        if (isInlineIfThen) {
2,147✔
1884
            /*** PARSE INLINE IF STATEMENT ***/
1885
            if (!incrementNestedCount) {
48✔
1886
                this.nestedInlineConditionalCount++;
5✔
1887
            }
1888

1889
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
48✔
1890

1891
            if (!thenBranch) {
48!
UNCOV
1892
                this.diagnostics.push({
×
1893
                    ...DiagnosticMessages.expectedStatement(ifToken.text, 'statement'),
1894
                    location: this.peek().location
1895
                });
UNCOV
1896
                throw this.lastDiagnosticAsError();
×
1897
            } else {
1898
                this.ensureInline(thenBranch.statements);
48✔
1899
            }
1900

1901
            //else branch
1902
            if (this.check(TokenKind.Else)) {
48✔
1903
                elseToken = this.advance();
33✔
1904

1905
                if (this.check(TokenKind.If)) {
33✔
1906
                    // recurse-read `else if`
1907
                    elseBranch = this.ifStatement(false);
10✔
1908

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

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

1927
                    if (elseBranch) {
20!
1928
                        this.ensureInline(elseBranch.statements);
20✔
1929
                    }
1930
                }
1931

1932
                if (!elseBranch) {
29!
1933
                    //missing `else` branch
UNCOV
1934
                    this.diagnostics.push({
×
1935
                        ...DiagnosticMessages.expectedStatement('else', 'statement'),
1936
                        location: this.peek().location
1937
                    });
UNCOV
1938
                    throw this.lastDiagnosticAsError();
×
1939
                }
1940
            }
1941

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

1962
            thenBranch = this.blockConditionalBranch(ifToken);
2,099✔
1963

1964
            //ensure newline/colon before next keyword
1965
            this.ensureNewLineOrColon();
2,096✔
1966

1967
            //else branch
1968
            if (this.check(TokenKind.Else)) {
2,096✔
1969
                elseToken = this.advance();
1,664✔
1970

1971
                if (this.check(TokenKind.If)) {
1,664✔
1972
                    // recurse-read `else if`
1973
                    elseBranch = this.ifStatement();
985✔
1974

1975
                } else {
1976
                    elseBranch = this.blockConditionalBranch(ifToken);
679✔
1977

1978
                    //ensure newline/colon before next keyword
1979
                    this.ensureNewLineOrColon();
679✔
1980
                }
1981
            }
1982

1983
            if (!isIfStatement(elseBranch)) {
2,096✔
1984
                if (this.check(TokenKind.EndIf)) {
1,111✔
1985
                    endIfToken = this.advance();
1,108✔
1986

1987
                } else {
1988
                    //missing endif
1989
                    this.diagnostics.push({
3✔
1990
                        ...DiagnosticMessages.expectedTerminator('end if', 'if'),
1991
                        location: ifToken.location
1992
                    });
1993
                }
1994
            }
1995
        }
1996

1997
        return new IfStatement({
2,140✔
1998
            if: ifToken,
1999
            then: thenToken,
2000
            endIf: endIfToken,
2001
            else: elseToken,
2002
            condition: condition,
2003
            thenBranch: thenBranch,
2004
            elseBranch: elseBranch
2005
        });
2006
    }
2007

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

2014
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
2015
        // a trailing "end if" or "else if"
2016
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
2,778✔
2017

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

2024
            //this whole if statement is bogus...add error to the if token and hard-fail
2025
            this.diagnostics.push({
3✔
2026
                ...DiagnosticMessages.expectedTerminator(['end if', 'else if', 'else'], 'then', 'block'),
2027
                location: ifToken.location
2028
            });
2029
            throw this.lastDiagnosticAsError();
3✔
2030
        }
2031
        return branch;
2,775✔
2032
    }
2033

2034
    private conditionalCompileStatement(): ConditionalCompileStatement {
2035
        const hashIfToken = this.advance();
56✔
2036
        let notToken: Token | undefined;
2037

2038
        if (this.check(TokenKind.Not)) {
56✔
2039
            notToken = this.advance();
7✔
2040
        }
2041

2042
        if (!this.checkAny(TokenKind.True, TokenKind.False, TokenKind.Identifier)) {
56✔
2043
            this.diagnostics.push({
1✔
2044
                ...DiagnosticMessages.invalidHashIfValue(),
2045
                location: this.peek()?.location
3!
2046
            });
2047
        }
2048

2049

2050
        const condition = this.advance();
56✔
2051

2052
        let thenBranch: Block;
2053
        let elseBranch: ConditionalCompileStatement | Block | undefined;
2054

2055
        let hashEndIfToken: Token | undefined;
2056
        let hashElseToken: Token | undefined;
2057

2058
        //keep track of the current error count
2059
        //if this is `#if false` remove all diagnostics.
2060
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
56✔
2061

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

2069
        this.ensureNewLine();
55✔
2070
        this.advance();
55✔
2071

2072
        //else branch
2073
        if (this.check(TokenKind.HashElseIf)) {
55✔
2074
            // recurse-read `#else if`
2075
            elseBranch = this.conditionalCompileStatement();
15✔
2076
            this.ensureNewLine();
15✔
2077

2078
        } else if (this.check(TokenKind.HashElse)) {
40✔
2079
            hashElseToken = this.advance();
10✔
2080
            let diagnosticsLengthBeforeBlock = this.diagnostics.length;
10✔
2081
            elseBranch = this.blockConditionalCompileBranch(hashIfToken);
10✔
2082

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

2091
        if (!isConditionalCompileStatement(elseBranch)) {
55✔
2092

2093
            if (this.check(TokenKind.HashEndIf)) {
40!
2094
                hashEndIfToken = this.advance();
40✔
2095

2096
            } else {
2097
                //missing #endif
UNCOV
2098
                this.diagnostics.push({
×
2099
                    ...DiagnosticMessages.expectedTerminator('#end if', '#if'),
2100
                    location: hashIfToken.location
2101
                });
2102
            }
2103
        }
2104

2105
        return new ConditionalCompileStatement({
55✔
2106
            hashIf: hashIfToken,
2107
            hashElse: hashElseToken,
2108
            hashEndIf: hashEndIfToken,
2109
            not: notToken,
2110
            condition: condition,
2111
            thenBranch: thenBranch,
2112
            elseBranch: elseBranch
2113
        });
2114
    }
2115

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

2122
        //parsing until trailing "#end if", "#else", "#else if"
2123
        let branch = this.conditionalCompileBlock();
66✔
2124

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

2131
            //this whole if statement is bogus...add error to the if token and hard-fail
UNCOV
2132
            this.diagnostics.push({
×
2133
                ...DiagnosticMessages.expectedTerminator(['#end if', '#else if', '#else'], 'conditional compilation', 'block'),
2134
                location: hashIfToken.location
2135
            });
UNCOV
2136
            throw this.lastDiagnosticAsError();
×
2137
        }
2138
        return branch;
65✔
2139
    }
2140

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

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

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

2172
            } else {
2173
                //something went wrong. reset to the top of the loop
2174
                this.current = loopCurrent;
1✔
2175

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

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

2183
                //consume potential separators
2184
                this.consumeStatementSeparators(true);
1✔
2185
            }
2186
        }
2187
        this.globalTerminators.pop();
66✔
2188

2189

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

2221
    private conditionalCompileConstStatement() {
2222
        const hashConstToken = this.advance();
21✔
2223

2224
        const constName = this.peek();
21✔
2225
        //disallow using keywords for const names
2226
        if (ReservedWords.has(constName?.text.toLowerCase())) {
21!
2227
            this.diagnostics.push({
1✔
2228
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(constName?.text),
3!
2229
                location: constName?.location
3!
2230
            });
2231

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

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

2260
        if (!this.check(TokenKind.Newline)) {
18!
UNCOV
2261
            this.diagnostics.push({
×
2262
                ...DiagnosticMessages.unexpectedToken(this.peek().text),
2263
                location: this.peek().location
2264
            });
UNCOV
2265
            throw this.lastDiagnosticAsError();
×
2266
        }
2267

2268
        return new ConditionalCompileConstStatement({ hashConst: hashConstToken, assignment: assignment });
18✔
2269
    }
2270

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

2278
    private ensureNewLine() {
2279
        //ensure newline before next keyword
2280
        if (!this.check(TokenKind.Newline)) {
80!
UNCOV
2281
            this.diagnostics.push({
×
2282
                ...DiagnosticMessages.unexpectedToken(this.peek().text),
2283
                location: this.peek().location
2284
            });
UNCOV
2285
            throw this.lastDiagnosticAsError();
×
2286
        }
2287
    }
2288

2289
    private ensureNewLineOrColon(silent = false) {
2,775✔
2290
        const prev = this.previous().kind;
2,969✔
2291
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
2,969✔
2292
            if (!silent) {
134✔
2293
                this.diagnostics.push({
6✔
2294
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2295
                    location: this.peek().location
2296
                });
2297
            }
2298
            return false;
134✔
2299
        }
2300
        return true;
2,835✔
2301
    }
2302

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

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

2326
        //look for colon statement separator
2327
        let foundColon = false;
86✔
2328
        while (this.match(TokenKind.Colon)) {
86✔
2329
            foundColon = true;
23✔
2330
        }
2331

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

2353
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2354
        let expressionStart = this.peek();
928✔
2355

2356
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
928✔
2357
            let operator = this.advance();
27✔
2358

2359
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
27✔
2360
                this.diagnostics.push({
1✔
2361
                    ...DiagnosticMessages.unexpectedOperator(),
2362
                    location: this.peek().location
2363
                });
2364
                throw this.lastDiagnosticAsError();
1✔
2365
            } else if (isCallExpression(expr)) {
26✔
2366
                this.diagnostics.push({
1✔
2367
                    ...DiagnosticMessages.unexpectedOperator(),
2368
                    location: expressionStart.location
2369
                });
2370
                throw this.lastDiagnosticAsError();
1✔
2371
            }
2372

2373
            const result = new IncrementStatement({ value: expr, operator: operator });
25✔
2374
            return result;
25✔
2375
        }
2376

2377
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
901✔
2378
            return new ExpressionStatement({ expression: expr });
525✔
2379
        }
2380

2381
        if (this.checkAny(...BinaryExpressionOperatorTokens)) {
376✔
2382
            expr = new BinaryExpression({ left: expr, operator: this.advance(), right: this.expression() });
6✔
2383
        }
2384

2385
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an inclosing ExpressionStatement
2386
        this.diagnostics.push({
376✔
2387
            ...DiagnosticMessages.expectedStatement(),
2388
            location: expressionStart.location
2389
        });
2390
        return new ExpressionStatement({ expression: expr });
376✔
2391
    }
2392

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

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

2438
    private printStatement(): PrintStatement {
2439
        let printKeyword = this.advance();
1,252✔
2440

2441
        let values: Expression[] = [];
1,252✔
2442

2443
        while (!this.checkEndOfStatement()) {
1,252✔
2444
            if (this.checkAny(TokenKind.Semicolon, TokenKind.Comma)) {
1,368✔
2445
                values.push(new PrintSeparatorExpression({ separator: this.advance() as PrintSeparatorToken }));
42✔
2446
            } else if (this.check(TokenKind.Else)) {
1,326✔
2447
                break; // inline branch
22✔
2448
            } else {
2449
                values.push(this.expression());
1,304✔
2450
            }
2451
        }
2452

2453
        //print statements can be empty, so look for empty print conditions
2454
        if (!values.length) {
1,250✔
2455
            const endOfStatementLocation = util.createBoundingLocation(printKeyword, this.peek());
9✔
2456
            let emptyStringLiteral = createStringLiteral('', endOfStatementLocation);
9✔
2457
            values.push(emptyStringLiteral);
9✔
2458
        }
2459

2460
        let last = values[values.length - 1];
1,250✔
2461
        if (isToken(last)) {
1,250!
2462
            // TODO: error, expected value
2463
        }
2464

2465
        return new PrintStatement({ print: printKeyword, expressions: values });
1,250✔
2466
    }
2467

2468
    /**
2469
     * Parses a return statement with an optional return value.
2470
     * @returns an AST representation of a return statement.
2471
     */
2472
    private returnStatement(): ReturnStatement {
2473
        let options = { return: this.previous() };
3,363✔
2474

2475
        if (this.checkEndOfStatement()) {
3,363✔
2476
            return new ReturnStatement(options);
16✔
2477
        }
2478

2479
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
3,347✔
2480
        return new ReturnStatement({ ...options, value: toReturn });
3,346✔
2481
    }
2482

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

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

2500
        return new LabelStatement(options);
10✔
2501
    }
2502

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

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

2529
        return new GotoStatement(tokens);
10✔
2530
    }
2531

2532
    /**
2533
     * Parses an `end` statement
2534
     * @returns an AST representation of an `end` statement.
2535
     */
2536
    private endStatement() {
2537
        let options = { end: this.advance() };
8✔
2538

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

2548
        return new StopStatement(options);
16✔
2549
    }
2550

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

2560
        this.consumeStatementSeparators(true);
6,844✔
2561
        const statements: Statement[] = [];
6,844✔
2562
        const flatGlobalTerminators = this.globalTerminators.flat().flat();
6,844✔
2563
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators, ...flatGlobalTerminators)) {
6,844✔
2564
            //grab the location of the current token
2565
            let loopCurrent = this.current;
8,124✔
2566
            let dec = this.declaration();
8,124✔
2567
            if (dec) {
8,124✔
2568
                if (!isAnnotationExpression(dec)) {
8,068✔
2569
                    this.consumePendingAnnotations(dec);
8,061✔
2570
                    statements.push(dec);
8,061✔
2571
                }
2572

2573
                //ensure statement separator
2574
                this.consumeStatementSeparators();
8,068✔
2575

2576
            } else {
2577
                //something went wrong. reset to the top of the loop
2578
                this.current = loopCurrent;
56✔
2579

2580
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2581
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
56✔
2582

2583
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2584
                //but there's already an error in the file being parsed, so just leave this line here
2585
                this.advance();
56✔
2586

2587
                //consume potential separators
2588
                this.consumeStatementSeparators(true);
56✔
2589
            }
2590
        }
2591

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

2608
        this.exitAnnotationBlock(parentAnnotations);
6,838✔
2609
        return new Block({ statements: statements });
6,838✔
2610
    }
2611

2612
    /**
2613
     * Attach pending annotations to the provided statement,
2614
     * and then reset the annotations array
2615
     */
2616
    consumePendingAnnotations(statement: Statement) {
2617
        if (this.pendingAnnotations.length) {
15,757✔
2618
            statement.annotations = this.pendingAnnotations;
51✔
2619
            this.pendingAnnotations = [];
51✔
2620
        }
2621
    }
2622

2623
    enterAnnotationBlock() {
2624
        const pending = this.pendingAnnotations;
12,419✔
2625
        this.pendingAnnotations = [];
12,419✔
2626
        return pending;
12,419✔
2627
    }
2628

2629
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2630
        // non consumed annotations are an error
2631
        if (this.pendingAnnotations.length) {
12,411✔
2632
            for (const annotation of this.pendingAnnotations) {
5✔
2633
                this.diagnostics.push({
7✔
2634
                    ...DiagnosticMessages.unusedAnnotation(),
2635
                    location: annotation.location
2636
                });
2637
            }
2638
        }
2639
        this.pendingAnnotations = parentAnnotations;
12,411✔
2640
    }
2641

2642
    private expression(findTypecast = true): Expression {
12,454✔
2643
        let expression = this.anonymousFunction();
12,847✔
2644
        let asToken: Token;
2645
        let typeExpression: TypeExpression;
2646
        if (findTypecast) {
12,809✔
2647
            do {
12,443✔
2648
                if (this.check(TokenKind.As)) {
12,519✔
2649
                    this.warnIfNotBrighterScriptMode('type cast');
78✔
2650
                    // Check if this expression is wrapped in any type casts
2651
                    // allows for multiple casts:
2652
                    // myVal = foo() as dynamic as string
2653
                    [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
78✔
2654
                    if (asToken && typeExpression) {
78✔
2655
                        expression = new TypecastExpression({ obj: expression, as: asToken, typeExpression: typeExpression });
76✔
2656
                    }
2657
                } else {
2658
                    break;
12,441✔
2659
                }
2660

2661
            } while (asToken && typeExpression);
156✔
2662
        }
2663
        return expression;
12,809✔
2664
    }
2665

2666
    private anonymousFunction(): Expression {
2667
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
12,847✔
2668
            const func = this.functionDeclaration(true);
88✔
2669
            //if there's an open paren after this, this is an IIFE
2670
            if (this.check(TokenKind.LeftParen)) {
87✔
2671
                return this.finishCall(this.advance(), func);
3✔
2672
            } else {
2673
                return func;
84✔
2674
            }
2675
        }
2676

2677
        let expr = this.boolean();
12,759✔
2678

2679
        if (this.check(TokenKind.Question)) {
12,722✔
2680
            return this.ternaryExpression(expr);
97✔
2681
        } else if (this.check(TokenKind.QuestionQuestion)) {
12,625✔
2682
            return this.nullCoalescingExpression(expr);
34✔
2683
        } else {
2684
            return expr;
12,591✔
2685
        }
2686
    }
2687

2688
    private boolean(): Expression {
2689
        let expr = this.relational();
12,759✔
2690

2691
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
12,722✔
2692
            let operator = this.previous();
34✔
2693
            let right = this.relational();
34✔
2694
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
34✔
2695
        }
2696

2697
        return expr;
12,722✔
2698
    }
2699

2700
    private relational(): Expression {
2701
        let expr = this.additive();
12,820✔
2702

2703
        while (
12,783✔
2704
            this.matchAny(
2705
                TokenKind.Equal,
2706
                TokenKind.LessGreater,
2707
                TokenKind.Greater,
2708
                TokenKind.GreaterEqual,
2709
                TokenKind.Less,
2710
                TokenKind.LessEqual
2711
            )
2712
        ) {
2713
            let operator = this.previous();
1,770✔
2714
            let right = this.additive();
1,770✔
2715
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,770✔
2716
        }
2717

2718
        return expr;
12,783✔
2719
    }
2720

2721
    // TODO: bitshift
2722

2723
    private additive(): Expression {
2724
        let expr = this.multiplicative();
14,590✔
2725

2726
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
14,553✔
2727
            let operator = this.previous();
1,420✔
2728
            let right = this.multiplicative();
1,420✔
2729
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,420✔
2730
        }
2731

2732
        return expr;
14,553✔
2733
    }
2734

2735
    private multiplicative(): Expression {
2736
        let expr = this.exponential();
16,010✔
2737

2738
        while (this.matchAny(
15,973✔
2739
            TokenKind.Forwardslash,
2740
            TokenKind.Backslash,
2741
            TokenKind.Star,
2742
            TokenKind.Mod,
2743
            TokenKind.LeftShift,
2744
            TokenKind.RightShift
2745
        )) {
2746
            let operator = this.previous();
60✔
2747
            let right = this.exponential();
60✔
2748
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
60✔
2749
        }
2750

2751
        return expr;
15,973✔
2752
    }
2753

2754
    private exponential(): Expression {
2755
        let expr = this.prefixUnary();
16,070✔
2756

2757
        while (this.match(TokenKind.Caret)) {
16,033✔
2758
            let operator = this.previous();
9✔
2759
            let right = this.prefixUnary();
9✔
2760
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
9✔
2761
        }
2762

2763
        return expr;
16,033✔
2764
    }
2765

2766
    private prefixUnary(): Expression {
2767
        const nextKind = this.peek().kind;
16,115✔
2768
        if (nextKind === TokenKind.Not) {
16,115✔
2769
            this.current++; //advance
27✔
2770
            let operator = this.previous();
27✔
2771
            let right = this.relational();
27✔
2772
            return new UnaryExpression({ operator: operator, right: right });
27✔
2773
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
16,088✔
2774
            this.current++; //advance
36✔
2775
            let operator = this.previous();
36✔
2776
            let right = (nextKind as any) === TokenKind.Not
36✔
2777
                ? this.boolean()
36!
2778
                : this.prefixUnary();
2779
            return new UnaryExpression({ operator: operator, right: right });
36✔
2780
        }
2781
        return this.call();
16,052✔
2782
    }
2783

2784
    private indexedGet(expr: Expression) {
2785
        let openingSquare = this.previous();
158✔
2786
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
158✔
2787
        let indexes: Expression[] = [];
158✔
2788

2789

2790
        //consume leading newlines
2791
        while (this.match(TokenKind.Newline)) { }
158✔
2792

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

2811
        const closingSquare = this.tryConsume(
158✔
2812
            DiagnosticMessages.unmatchedLeftToken(openingSquare.text, 'array or object index'),
2813
            TokenKind.RightSquareBracket
2814
        );
2815

2816
        return new IndexedGetExpression({
158✔
2817
            obj: expr,
2818
            indexes: indexes,
2819
            openingSquare: openingSquare,
2820
            closingSquare: closingSquare,
2821
            questionDot: questionDotToken
2822
        });
2823
    }
2824

2825
    private newExpression() {
2826
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
141✔
2827
        let newToken = this.advance();
141✔
2828

2829
        let nameExpr = this.identifyingExpression();
141✔
2830
        let leftParen = this.tryConsume(
141✔
2831
            DiagnosticMessages.unexpectedToken(this.peek().text),
2832
            TokenKind.LeftParen,
2833
            TokenKind.QuestionLeftParen
2834
        );
2835

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

2844
        let call = this.finishCall(leftParen, nameExpr);
137✔
2845
        //pop the call from the  callExpressions list because this is technically something else
2846
        this.callExpressions.pop();
137✔
2847
        let result = new NewExpression({ new: newToken, call: call });
137✔
2848
        return result;
137✔
2849
    }
2850

2851
    /**
2852
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2853
     */
2854
    private callfunc(callee: Expression): Expression {
2855
        this.warnIfNotBrighterScriptMode('callfunc operator');
57✔
2856
        let operator = this.previous();
57✔
2857
        let methodName = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
57✔
2858
        let openParen: Token;
2859
        let call: CallExpression;
2860
        if (methodName) {
57✔
2861
            // force it into an identifier so the AST makes some sense
2862
            methodName.kind = TokenKind.Identifier;
49✔
2863
            openParen = this.tryConsume(DiagnosticMessages.expectedToken(TokenKind.LeftParen), TokenKind.LeftParen);
49✔
2864
            if (openParen) {
49!
2865
                call = this.finishCall(openParen, callee, false);
49✔
2866
            }
2867
        }
2868
        return new CallfuncExpression({
57✔
2869
            callee: callee,
2870
            operator: operator,
2871
            methodName: methodName as Identifier,
2872
            openingParen: openParen,
2873
            args: call?.args,
171✔
2874
            closingParen: call?.tokens?.closingParen
342✔
2875
        });
2876
    }
2877

2878
    private call(): Expression {
2879
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
17,399✔
2880
            return this.newExpression();
141✔
2881
        }
2882
        let expr = this.primary();
17,258✔
2883

2884
        while (true) {
17,165✔
2885
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
22,153✔
2886
                expr = this.finishCall(this.previous(), expr);
2,302✔
2887
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
19,851✔
2888
                expr = this.indexedGet(expr);
156✔
2889
            } else if (this.match(TokenKind.Callfunc)) {
19,695✔
2890
                expr = this.callfunc(expr);
57✔
2891
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
19,638✔
2892
                if (this.match(TokenKind.LeftSquareBracket)) {
2,514✔
2893
                    expr = this.indexedGet(expr);
2✔
2894
                } else {
2895
                    let dot = this.previous();
2,512✔
2896
                    let name = this.tryConsume(
2,512✔
2897
                        DiagnosticMessages.expectedIdentifier(),
2898
                        TokenKind.Identifier,
2899
                        ...AllowedProperties
2900
                    );
2901
                    if (!name) {
2,512✔
2902
                        break;
41✔
2903
                    }
2904

2905
                    // force it into an identifier so the AST makes some sense
2906
                    name.kind = TokenKind.Identifier;
2,471✔
2907
                    expr = new DottedGetExpression({ obj: expr, name: name as Identifier, dot: dot });
2,471✔
2908
                }
2909

2910
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
17,124✔
2911
                let dot = this.advance();
11✔
2912
                let name = this.tryConsume(
11✔
2913
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2914
                    TokenKind.Identifier,
2915
                    ...AllowedProperties
2916
                );
2917

2918
                // force it into an identifier so the AST makes some sense
2919
                name.kind = TokenKind.Identifier;
11✔
2920
                if (!name) {
11!
UNCOV
2921
                    break;
×
2922
                }
2923
                expr = new XmlAttributeGetExpression({ obj: expr, name: name as Identifier, at: dot });
11✔
2924
                //only allow a single `@` expression
2925
                break;
11✔
2926

2927
            } else {
2928
                break;
17,113✔
2929
            }
2930
        }
2931

2932
        return expr;
17,165✔
2933
    }
2934

2935
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
2,442✔
2936
        let args = [] as Expression[];
2,521✔
2937
        while (this.match(TokenKind.Newline)) { }
2,521✔
2938

2939
        if (!this.check(TokenKind.RightParen)) {
2,521✔
2940
            do {
1,278✔
2941
                while (this.match(TokenKind.Newline)) { }
1,832✔
2942

2943
                if (args.length >= CallExpression.MaximumArguments) {
1,832!
UNCOV
2944
                    this.diagnostics.push({
×
2945
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
2946
                        location: this.peek()?.location
×
2947
                    });
UNCOV
2948
                    throw this.lastDiagnosticAsError();
×
2949
                }
2950
                try {
1,832✔
2951
                    args.push(this.expression());
1,832✔
2952
                } catch (error) {
2953
                    this.rethrowNonDiagnosticError(error);
5✔
2954
                    // we were unable to get an expression, so don't continue
2955
                    break;
5✔
2956
                }
2957
            } while (this.match(TokenKind.Comma));
2958
        }
2959

2960
        while (this.match(TokenKind.Newline)) { }
2,521✔
2961

2962
        const closingParen = this.tryConsume(
2,521✔
2963
            DiagnosticMessages.unmatchedLeftToken(openingParen.text, 'function call arguments'),
2964
            TokenKind.RightParen
2965
        );
2966

2967
        let expression = new CallExpression({
2,521✔
2968
            callee: callee,
2969
            openingParen: openingParen,
2970
            args: args,
2971
            closingParen: closingParen
2972
        });
2973
        if (addToCallExpressionList) {
2,521✔
2974
            this.callExpressions.push(expression);
2,442✔
2975
        }
2976
        return expression;
2,521✔
2977
    }
2978

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

3001
        } catch (error) {
3002
            // Something went wrong - reset the kind to what it was previously
UNCOV
3003
            for (const changedToken of changedTokens) {
×
UNCOV
3004
                changedToken.token.kind = changedToken.oldKind;
×
3005
            }
UNCOV
3006
            throw error;
×
3007
        }
3008
    }
3009

3010
    /**
3011
     * Gets a single "part" of a type of a potential Union type
3012
     * Note: this does not NEED to be part of a union type, but the logic is the same
3013
     *
3014
     * @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
3015
     * @returns an expression that was successfully parsed
3016
     */
3017
    private getTypeExpressionPart(changedTokens: { token: Token; oldKind: TokenKind }[]) {
3018
        let expr: VariableExpression | DottedGetExpression | TypedArrayExpression;
3019

3020
        if (this.checkAny(...DeclarableTypes)) {
1,650✔
3021
            // if this is just a type, just use directly
3022
            expr = new VariableExpression({ name: this.advance() as Identifier });
1,092✔
3023
        } else {
3024
            if (this.options.mode === ParseMode.BrightScript && !declarableTypesLower.includes(this.peek()?.text?.toLowerCase())) {
558!
3025
                // custom types arrays not allowed in Brightscript
3026
                this.warnIfNotBrighterScriptMode('custom types');
14✔
3027
                return expr;
14✔
3028
            }
3029

3030
            if (this.checkAny(...AllowedTypeIdentifiers)) {
544✔
3031
                // Since the next token is allowed as a type identifier, change the kind
3032
                let nextToken = this.peek();
1✔
3033
                changedTokens.push({ token: nextToken, oldKind: nextToken.kind });
1✔
3034
                nextToken.kind = TokenKind.Identifier;
1✔
3035
            }
3036
            expr = this.identifyingExpression(AllowedTypeIdentifiers);
544✔
3037
        }
3038

3039
        //Check if it has square brackets, thus making it an array
3040
        if (expr && this.check(TokenKind.LeftSquareBracket)) {
1,636✔
3041
            if (this.options.mode === ParseMode.BrightScript) {
28✔
3042
                // typed arrays not allowed in Brightscript
3043
                this.warnIfNotBrighterScriptMode('typed arrays');
1✔
3044
                return expr;
1✔
3045
            }
3046

3047
            // Check if it is an array - that is, if it has `[]` after the type
3048
            // eg. `string[]` or `SomeKlass[]`
3049
            // This is while loop, so it supports multidimensional arrays (eg. integer[][])
3050
            while (this.check(TokenKind.LeftSquareBracket)) {
27✔
3051
                const leftBracket = this.advance();
29✔
3052
                if (this.check(TokenKind.RightSquareBracket)) {
29!
3053
                    const rightBracket = this.advance();
29✔
3054
                    expr = new TypedArrayExpression({ innerType: expr, leftBracket: leftBracket, rightBracket: rightBracket });
29✔
3055
                }
3056
            }
3057
        }
3058

3059
        return expr;
1,635✔
3060
    }
3061

3062
    private primary(): Expression {
3063
        switch (true) {
17,258✔
3064
            case this.matchAny(
17,258!
3065
                TokenKind.False,
3066
                TokenKind.True,
3067
                TokenKind.Invalid,
3068
                TokenKind.IntegerLiteral,
3069
                TokenKind.LongIntegerLiteral,
3070
                TokenKind.FloatLiteral,
3071
                TokenKind.DoubleLiteral,
3072
                TokenKind.StringLiteral
3073
            ):
3074
                return new LiteralExpression({ value: this.previous() });
7,623✔
3075

3076
            //capture source literals (LINE_NUM if brightscript, or a bunch of them if brighterscript)
3077
            case this.matchAny(TokenKind.LineNumLiteral, ...(this.options.mode === ParseMode.BrightScript ? [] : BrighterScriptSourceLiterals)):
9,635✔
3078
                return new SourceLiteralExpression({ value: this.previous() });
35✔
3079

3080
            //template string
3081
            case this.check(TokenKind.BackTick):
3082
                return this.templateString(false);
43✔
3083

3084
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
3085
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
18,464✔
3086
                return this.templateString(true);
8✔
3087

3088
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3089
                return new VariableExpression({ name: this.previous() as Identifier });
8,906✔
3090

3091
            case this.match(TokenKind.LeftParen):
3092
                let left = this.previous();
57✔
3093
                let expr = this.expression();
57✔
3094
                let right = this.consume(
56✔
3095
                    DiagnosticMessages.unmatchedLeftToken(left.text, 'expression'),
3096
                    TokenKind.RightParen
3097
                );
3098
                return new GroupingExpression({ leftParen: left, rightParen: right, expression: expr });
56✔
3099

3100
            case this.matchAny(TokenKind.LeftSquareBracket):
3101
                return this.arrayLiteral();
164✔
3102

3103
            case this.match(TokenKind.LeftCurlyBrace):
3104
                return this.aaLiteral();
287✔
3105

3106
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
UNCOV
3107
                let token = Object.assign(this.previous(), {
×
3108
                    kind: TokenKind.Identifier
3109
                }) as Identifier;
UNCOV
3110
                return new VariableExpression({ name: token });
×
3111

3112
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
UNCOV
3113
                return this.anonymousFunction();
×
3114

3115
            case this.check(TokenKind.RegexLiteral):
3116
                return this.regexLiteralExpression();
45✔
3117

3118
            default:
3119
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
3120
                if (this.checkAny(...this.peekGlobalTerminators())) {
90!
3121
                    //don't throw a diagnostic, just return undefined
3122

3123
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
3124
                } else {
3125
                    this.diagnostics.push({
90✔
3126
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
3127
                        location: this.peek()?.location
270!
3128
                    });
3129
                    throw this.lastDiagnosticAsError();
90✔
3130
                }
3131
        }
3132
    }
3133

3134
    private arrayLiteral() {
3135
        let elements: Array<Expression> = [];
164✔
3136
        let openingSquare = this.previous();
164✔
3137

3138
        while (this.match(TokenKind.Newline)) {
164✔
3139
        }
3140
        let closingSquare: Token;
3141

3142
        if (!this.match(TokenKind.RightSquareBracket)) {
164✔
3143
            try {
125✔
3144
                elements.push(this.expression());
125✔
3145

3146
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
124✔
3147

3148
                    while (this.match(TokenKind.Newline)) {
165✔
3149

3150
                    }
3151

3152
                    if (this.check(TokenKind.RightSquareBracket)) {
165✔
3153
                        break;
34✔
3154
                    }
3155

3156
                    elements.push(this.expression());
131✔
3157
                }
3158
            } catch (error: any) {
3159
                this.rethrowNonDiagnosticError(error);
2✔
3160
            }
3161

3162
            closingSquare = this.tryConsume(
125✔
3163
                DiagnosticMessages.unmatchedLeftToken(openingSquare.text, 'array literal'),
3164
                TokenKind.RightSquareBracket
3165
            );
3166
        } else {
3167
            closingSquare = this.previous();
39✔
3168
        }
3169

3170
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3171
        return new ArrayLiteralExpression({ elements: elements, open: openingSquare, close: closingSquare });
164✔
3172
    }
3173

3174
    private aaLiteral() {
3175
        let openingBrace = this.previous();
287✔
3176
        let members: Array<AAMemberExpression> = [];
287✔
3177

3178
        let key = () => {
287✔
3179
            let result = {
303✔
3180
                colonToken: null as Token,
3181
                keyToken: null as Token,
3182
                range: null as Range
3183
            };
3184
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
303✔
3185
                result.keyToken = this.identifier(...AllowedProperties);
270✔
3186
            } else if (this.check(TokenKind.StringLiteral)) {
33!
3187
                result.keyToken = this.advance();
33✔
3188
            } else {
UNCOV
3189
                this.diagnostics.push({
×
3190
                    ...DiagnosticMessages.unexpectedAAKey(),
3191
                    location: this.peek().location
3192
                });
UNCOV
3193
                throw this.lastDiagnosticAsError();
×
3194
            }
3195

3196
            result.colonToken = this.consume(
303✔
3197
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3198
                TokenKind.Colon
3199
            );
3200
            result.range = util.createBoundingRange(result.keyToken, result.colonToken);
302✔
3201
            return result;
302✔
3202
        };
3203

3204
        while (this.match(TokenKind.Newline)) { }
287✔
3205
        let closingBrace: Token;
3206
        if (!this.match(TokenKind.RightCurlyBrace)) {
287✔
3207
            let lastAAMember: AAMemberExpression;
3208
            try {
208✔
3209
                let k = key();
208✔
3210
                let expr = this.expression();
208✔
3211
                lastAAMember = new AAMemberExpression({
207✔
3212
                    key: k.keyToken,
3213
                    colon: k.colonToken,
3214
                    value: expr
3215
                });
3216
                members.push(lastAAMember);
207✔
3217

3218
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
207✔
3219
                    // collect comma at end of expression
3220
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
227✔
3221
                        (lastAAMember as DeepWriteable<AAMemberExpression>).tokens.comma = this.previous();
70✔
3222
                    }
3223

3224
                    this.consumeStatementSeparators(true);
227✔
3225

3226
                    if (this.check(TokenKind.RightCurlyBrace)) {
227✔
3227
                        break;
132✔
3228
                    }
3229
                    let k = key();
95✔
3230
                    let expr = this.expression();
94✔
3231
                    lastAAMember = new AAMemberExpression({
94✔
3232
                        key: k.keyToken,
3233
                        colon: k.colonToken,
3234
                        value: expr
3235
                    });
3236
                    members.push(lastAAMember);
94✔
3237

3238
                }
3239
            } catch (error: any) {
3240
                this.rethrowNonDiagnosticError(error);
2✔
3241
            }
3242

3243
            closingBrace = this.tryConsume(
208✔
3244
                DiagnosticMessages.unmatchedLeftToken(openingBrace.text, 'associative array literal'),
3245
                TokenKind.RightCurlyBrace
3246
            );
3247
        } else {
3248
            closingBrace = this.previous();
79✔
3249
        }
3250

3251
        const aaExpr = new AALiteralExpression({ elements: members, open: openingBrace, close: closingBrace });
287✔
3252
        return aaExpr;
287✔
3253
    }
3254

3255
    /**
3256
     * Pop token if we encounter specified token
3257
     */
3258
    private match(tokenKind: TokenKind) {
3259
        if (this.check(tokenKind)) {
64,571✔
3260
            this.current++; //advance
6,445✔
3261
            return true;
6,445✔
3262
        }
3263
        return false;
58,126✔
3264
    }
3265

3266
    /**
3267
     * Pop token if we encounter a token in the specified list
3268
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3269
     */
3270
    private matchAny(...tokenKinds: TokenKind[]) {
3271
        for (let tokenKind of tokenKinds) {
226,454✔
3272
            if (this.check(tokenKind)) {
641,239✔
3273
                this.current++; //advance
59,783✔
3274
                return true;
59,783✔
3275
            }
3276
        }
3277
        return false;
166,671✔
3278
    }
3279

3280
    /**
3281
     * If the next series of tokens matches the given set of tokens, pop them all
3282
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
3283
     */
3284
    private matchSequence(...tokenKinds: TokenKind[]) {
3285
        const endIndex = this.current + tokenKinds.length;
19,698✔
3286
        for (let i = 0; i < tokenKinds.length; i++) {
19,698✔
3287
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
19,722!
3288
                return false;
19,695✔
3289
            }
3290
        }
3291
        this.current = endIndex;
3✔
3292
        return true;
3✔
3293
    }
3294

3295
    /**
3296
     * Get next token matching a specified list, or fail with an error
3297
     */
3298
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
3299
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
15,791✔
3300
        if (token) {
15,791✔
3301
            return token;
15,777✔
3302
        } else {
3303
            let error = new Error(diagnosticInfo.message);
14✔
3304
            (error as any).isDiagnostic = true;
14✔
3305
            throw error;
14✔
3306
        }
3307
    }
3308

3309
    /**
3310
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3311
     */
3312
    private consumeTokenIf(tokenKind: TokenKind) {
3313
        if (this.match(tokenKind)) {
3,695✔
3314
            return this.previous();
407✔
3315
        }
3316
    }
3317

3318
    private consumeToken(tokenKind: TokenKind) {
3319
        return this.consume(
2,071✔
3320
            DiagnosticMessages.expectedToken(tokenKind),
3321
            tokenKind
3322
        );
3323
    }
3324

3325
    /**
3326
     * Consume, or add a message if not found. But then continue and return undefined
3327
     */
3328
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3329
        const nextKind = this.peek().kind;
23,989✔
3330
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
50,355✔
3331

3332
        if (foundTokenKind) {
23,989✔
3333
            return this.advance();
23,872✔
3334
        }
3335
        this.diagnostics.push({
117✔
3336
            ...diagnostic,
3337
            location: this.peek()?.location
351!
3338
        });
3339
    }
3340

3341
    private tryConsumeToken(tokenKind: TokenKind) {
3342
        return this.tryConsume(
97✔
3343
            DiagnosticMessages.expectedToken(tokenKind),
3344
            tokenKind
3345
        );
3346
    }
3347

3348
    private consumeStatementSeparators(optional = false) {
10,690✔
3349
        //a comment or EOF mark the end of the statement
3350
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
32,630✔
3351
            return true;
733✔
3352
        }
3353
        let consumed = false;
31,897✔
3354
        //consume any newlines and colons
3355
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
31,897✔
3356
            consumed = true;
34,378✔
3357
        }
3358
        if (!optional && !consumed) {
31,897✔
3359
            this.diagnostics.push({
75✔
3360
                ...DiagnosticMessages.expectedNewlineOrColon(),
3361
                location: this.peek()?.location
225!
3362
            });
3363
        }
3364
        return consumed;
31,897✔
3365
    }
3366

3367
    private advance(): Token {
3368
        if (!this.isAtEnd()) {
54,866✔
3369
            this.current++;
54,852✔
3370
        }
3371
        return this.previous();
54,866✔
3372
    }
3373

3374
    private checkEndOfStatement(): boolean {
3375
        const nextKind = this.peek().kind;
7,588✔
3376
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
7,588✔
3377
    }
3378

3379
    private checkPrevious(tokenKind: TokenKind): boolean {
3380
        return this.previous()?.kind === tokenKind;
240!
3381
    }
3382

3383
    /**
3384
     * Check that the next token kind is the expected kind
3385
     * @param tokenKind the expected next kind
3386
     * @returns true if the next tokenKind is the expected value
3387
     */
3388
    private check(tokenKind: TokenKind): boolean {
3389
        const nextKind = this.peek().kind;
1,030,296✔
3390
        if (nextKind === TokenKind.Eof) {
1,030,296✔
3391
            return false;
12,809✔
3392
        }
3393
        return nextKind === tokenKind;
1,017,487✔
3394
    }
3395

3396
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3397
        const nextKind = this.peek().kind;
175,415✔
3398
        if (nextKind === TokenKind.Eof) {
175,415✔
3399
            return false;
1,364✔
3400
        }
3401
        return tokenKinds.includes(nextKind);
174,051✔
3402
    }
3403

3404
    private checkNext(tokenKind: TokenKind): boolean {
3405
        if (this.isAtEnd()) {
14,974!
UNCOV
3406
            return false;
×
3407
        }
3408
        return this.peekNext().kind === tokenKind;
14,974✔
3409
    }
3410

3411
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3412
        if (this.isAtEnd()) {
6,615!
UNCOV
3413
            return false;
×
3414
        }
3415
        const nextKind = this.peekNext().kind;
6,615✔
3416
        return tokenKinds.includes(nextKind);
6,615✔
3417
    }
3418

3419
    private isAtEnd(): boolean {
3420
        const peekToken = this.peek();
164,115✔
3421
        return !peekToken || peekToken.kind === TokenKind.Eof;
164,115✔
3422
    }
3423

3424
    private peekNext(): Token {
3425
        if (this.isAtEnd()) {
21,589!
UNCOV
3426
            return this.peek();
×
3427
        }
3428
        return this.tokens[this.current + 1];
21,589✔
3429
    }
3430

3431
    private peek(): Token {
3432
        return this.tokens[this.current];
1,426,697✔
3433
    }
3434

3435
    private previous(): Token {
3436
        return this.tokens[this.current - 1];
94,055✔
3437
    }
3438

3439
    /**
3440
     * Sometimes we catch an error that is a diagnostic.
3441
     * If that's the case, we want to continue parsing.
3442
     * Otherwise, re-throw the error
3443
     *
3444
     * @param error error caught in a try/catch
3445
     */
3446
    private rethrowNonDiagnosticError(error) {
3447
        if (!error.isDiagnostic) {
11!
UNCOV
3448
            throw error;
×
3449
        }
3450
    }
3451

3452
    /**
3453
     * Get the token that is {offset} indexes away from {this.current}
3454
     * @param offset the number of index steps away from current index to fetch
3455
     * @param tokenKinds the desired token must match one of these
3456
     * @example
3457
     * getToken(-1); //returns the previous token.
3458
     * getToken(0);  //returns current token.
3459
     * getToken(1);  //returns next token
3460
     */
3461
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3462
        const token = this.tokens[this.current + offset];
158✔
3463
        if (tokenKinds.includes(token.kind)) {
158✔
3464
            return token;
3✔
3465
        }
3466
    }
3467

3468
    private synchronize() {
3469
        this.advance(); // skip the erroneous token
95✔
3470

3471
        while (!this.isAtEnd()) {
95✔
3472
            if (this.ensureNewLineOrColon(true)) {
194✔
3473
                // end of statement reached
3474
                return;
66✔
3475
            }
3476

3477
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
128✔
3478
                case TokenKind.Namespace:
2!
3479
                case TokenKind.Class:
3480
                case TokenKind.Function:
3481
                case TokenKind.Sub:
3482
                case TokenKind.If:
3483
                case TokenKind.For:
3484
                case TokenKind.ForEach:
3485
                case TokenKind.While:
3486
                case TokenKind.Print:
3487
                case TokenKind.Return:
3488
                    // start parsing again from the next block starter or obvious
3489
                    // expression start
3490
                    return;
1✔
3491
            }
3492

3493
            this.advance();
127✔
3494
        }
3495
    }
3496

3497

3498
    public dispose() {
3499
    }
3500
}
3501

3502
export enum ParseMode {
1✔
3503
    BrightScript = 'BrightScript',
1✔
3504
    BrighterScript = 'BrighterScript'
1✔
3505
}
3506

3507
export interface ParseOptions {
3508
    /**
3509
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3510
     */
3511
    mode?: ParseMode;
3512
    /**
3513
     * A logger that should be used for logging. If omitted, a default logger is used
3514
     */
3515
    logger?: Logger;
3516
    /**
3517
     * Path to the file where this source code originated
3518
     */
3519
    srcPath?: string;
3520
    /**
3521
     * Should locations be tracked. If false, the `range` property will be omitted
3522
     * @default true
3523
     */
3524
    trackLocations?: boolean;
3525
    /**
3526
     *
3527
     */
3528
    bsConsts?: Map<string, boolean>;
3529
}
3530

3531

3532
class CancelStatementError extends Error {
3533
    constructor() {
3534
        super('CancelStatement');
2✔
3535
    }
3536
}
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