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

rokucommunity / brighterscript / #13308

22 Nov 2024 02:25PM UTC coverage: 86.801%. Remained the same
#13308

push

web-flow
Merge 332332a1f into 2a6afd921

11833 of 14419 branches covered (82.07%)

Branch coverage included in aggregate %.

191 of 205 new or added lines in 26 files covered. (93.17%)

201 existing lines in 18 files now uncovered.

12868 of 14038 relevant lines covered (91.67%)

32022.22 hits per line

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

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

109

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

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

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

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

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

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

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

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

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

157
    private globalTerminators = [] as TokenKind[][];
3,574✔
158

159
    /**
160
     * A list of identifiers that are permitted to be used as local variables. We store this in a property because we augment the list in the constructor
161
     * based on the parse mode
162
     */
163
    private allowedLocalIdentifiers: TokenKind[];
164

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

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

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

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

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

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

221
    private logger: Logger;
222

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

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

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

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

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

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

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

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

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

309
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
13,638✔
310
                return this.functionDeclaration(false);
3,188✔
311
            }
312

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

317
            if (this.checkAlias()) {
10,437✔
318
                return this.aliasStatement();
33✔
319
            }
320

321
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
10,404✔
322
                return this.constDeclaration();
155✔
323
            }
324

325
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
10,249✔
326
                return this.annotationExpression();
58✔
327
            }
328

329
            //catch certain global terminators to prevent unnecessary lookahead (i.e. like `end namespace`, no need to continue)
330
            if (this.checkAny(...this.peekGlobalTerminators())) {
10,191!
331
                return;
×
332
            }
333

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

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

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

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

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

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

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

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

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

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

471
    private interfaceDeclaration(): InterfaceStatement {
472
        this.warnIfNotBrighterScriptMode('interface declarations');
162✔
473

474
        const parentAnnotations = this.enterAnnotationBlock();
162✔
475

476
        const interfaceToken = this.consume(
162✔
477
            DiagnosticMessages.expectedKeyword(TokenKind.Interface),
478
            TokenKind.Interface
479
        );
480
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
162✔
481

482
        let extendsToken: Token;
483
        let parentInterfaceName: TypeExpression;
484

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

506
                let decl: Statement;
507

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

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

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

539
            //ensure statement separator
540
            this.consumeStatementSeparators();
238✔
541
        }
542

543
        //consume the final `end interface` token
544
        const endInterfaceToken = this.consumeToken(TokenKind.EndInterface);
162✔
545

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

558
    private enumDeclaration(): EnumStatement {
559
        const enumToken = this.consume(
165✔
560
            DiagnosticMessages.expectedKeyword(TokenKind.Enum),
561
            TokenKind.Enum
562
        );
563
        const nameToken = this.tryIdentifier(...this.allowedLocalIdentifiers);
165✔
564

565
        this.warnIfNotBrighterScriptMode('enum declarations');
165✔
566

567
        const parentAnnotations = this.enterAnnotationBlock();
165✔
568

569
        this.consumeStatementSeparators();
165✔
570

571
        const body: Array<EnumMemberStatement> = [];
165✔
572
        //gather up all members
573
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
165✔
574
            try {
318✔
575
                let decl: EnumMemberStatement;
576

577
                //collect leading annotations
578
                if (this.check(TokenKind.At)) {
318!
579
                    this.annotationExpression();
×
580
                }
581

582
                //members
583
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
318!
584
                    decl = this.enumMemberStatement();
318✔
585
                }
586

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

599
            //ensure statement separator
600
            this.consumeStatementSeparators();
318✔
601
            //break out of this loop if we encountered the `EndEnum` token
602
            if (this.check(TokenKind.EndEnum)) {
318✔
603
                break;
156✔
604
            }
605
        }
606

607
        //consume the final `end interface` token
608
        const endEnumToken = this.consumeToken(TokenKind.EndEnum);
165✔
609

610
        const result = new EnumStatement({
164✔
611
            enum: enumToken,
612
            name: nameToken,
613
            body: body,
614
            endEnum: endEnumToken
615
        });
616

617
        this.exitAnnotationBlock(parentAnnotations);
164✔
618
        return result;
164✔
619
    }
620

621
    /**
622
     * A BrighterScript class declaration
623
     */
624
    private classDeclaration(): ClassStatement {
625
        this.warnIfNotBrighterScriptMode('class declarations');
675✔
626

627
        const parentAnnotations = this.enterAnnotationBlock();
675✔
628

629
        let classKeyword = this.consume(
675✔
630
            DiagnosticMessages.expectedKeyword(TokenKind.Class),
631
            TokenKind.Class
632
        );
633
        let extendsKeyword: Token;
634
        let parentClassName: TypeExpression;
635

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

639
        //see if the class inherits from parent
640
        if (this.peek().text.toLowerCase() === 'extends') {
675✔
641
            extendsKeyword = this.advance();
101✔
642
            if (this.checkEndOfStatement()) {
101✔
643
                this.diagnostics.push({
1✔
644
                    ...DiagnosticMessages.expectedIdentifierAfterKeyword(extendsKeyword.text),
645
                    location: extendsKeyword.location
646
                });
647
            } else {
648
                parentClassName = this.typeExpression();
100✔
649
            }
650
        }
651

652
        //ensure statement separator
653
        this.consumeStatementSeparators();
675✔
654

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

662
                if (this.check(TokenKind.At)) {
701✔
663
                    this.annotationExpression();
15✔
664
                }
665

666
                if (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private)) {
700✔
667
                    //use actual access modifier
668
                    accessModifier = this.advance();
96✔
669
                }
670

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

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

687
                    decl = new MethodStatement({
356✔
688
                        modifiers: accessModifier,
689
                        name: funcDeclaration.tokens.name,
690
                        func: funcDeclaration.func,
691
                        override: overrideKeyword
692
                    });
693

694
                    //fields
695
                } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
344✔
696

697
                    decl = this.fieldDeclaration(accessModifier);
330✔
698

699
                    //class fields cannot be overridden
700
                    if (overrideKeyword) {
329!
701
                        this.diagnostics.push({
×
702
                            ...DiagnosticMessages.classFieldCannotBeOverridden(),
703
                            location: overrideKeyword.location
704
                        });
705
                    }
706

707
                }
708

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

718
            //ensure statement separator
719
            this.consumeStatementSeparators();
701✔
720
        }
721

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

730
        const result = new ClassStatement({
675✔
731
            class: classKeyword,
732
            name: className,
733
            body: body,
734
            endClass: endingKeyword,
735
            extends: extendsKeyword,
736
            parentClassName: parentClassName
737
        });
738

739
        this.exitAnnotationBlock(parentAnnotations);
675✔
740
        return result;
675✔
741
    }
742

743
    private fieldDeclaration(accessModifier: Token | null) {
744

745
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
330✔
746

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

770
        let name = this.consume(
330✔
771
            DiagnosticMessages.expectedClassFieldIdentifier(),
772
            TokenKind.Identifier,
773
            ...AllowedProperties
774
        ) as Identifier;
775

776
        let asToken: Token;
777
        let fieldTypeExpression: TypeExpression;
778
        //look for `as SOME_TYPE`
779
        if (this.check(TokenKind.As)) {
330✔
780
            [asToken, fieldTypeExpression] = this.consumeAsTokenAndTypeExpression();
217✔
781
        }
782

783
        let initialValue: Expression;
784
        let equal: Token;
785
        //if there is a field initializer
786
        if (this.check(TokenKind.Equal)) {
330✔
787
            equal = this.advance();
79✔
788
            initialValue = this.expression();
79✔
789
        }
790

791
        return new FieldStatement({
329✔
792
            accessModifier: accessModifier,
793
            name: name,
794
            as: asToken,
795
            typeExpression: fieldTypeExpression,
796
            equals: equal,
797
            initialValue: initialValue,
798
            optional: optionalKeyword
799
        });
800
    }
801

802
    /**
803
     * An array of CallExpression for the current function body
804
     */
805
    private callExpressions = [];
3,574✔
806

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

839
            if (isAnonymous) {
3,629✔
840
                leftParen = this.consume(
85✔
841
                    DiagnosticMessages.expectedLeftParenAfterCallable(functionTypeText),
842
                    TokenKind.LeftParen
843
                );
844
            } else {
845
                name = this.consume(
3,544✔
846
                    DiagnosticMessages.expectedNameAfterCallableKeyword(functionTypeText),
847
                    TokenKind.Identifier,
848
                    ...AllowedProperties
849
                ) as Identifier;
850
                leftParen = this.consume(
3,542✔
851
                    DiagnosticMessages.expectedLeftParenAfterCallableName(functionTypeText),
852
                    TokenKind.LeftParen
853
                );
854

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

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

874
            let params = [] as FunctionParameterExpression[];
3,626✔
875
            let asToken: Token;
876
            let typeExpression: TypeExpression;
877
            if (!this.check(TokenKind.RightParen)) {
3,626✔
878
                do {
1,675✔
879
                    params.push(this.functionParameter());
2,946✔
880
                } while (this.match(TokenKind.Comma));
881
            }
882
            let rightParen = this.advance();
3,626✔
883

884
            if (this.check(TokenKind.As)) {
3,626✔
885
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
297✔
886
            }
887

888
            params.reduce((haveFoundOptional: boolean, param: FunctionParameterExpression) => {
3,626✔
889
                if (haveFoundOptional && !param.defaultValue) {
2,946!
890
                    this.diagnostics.push({
×
891
                        ...DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter(param.tokens.name.text),
892
                        location: param.location
893
                    });
894
                }
895

896
                return haveFoundOptional || !!param.defaultValue;
2,946✔
897
            }, false);
898

899
            this.consumeStatementSeparators(true);
3,626✔
900

901

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

906
            // consume 'end sub' or 'end function'
907
            const endFunctionType = this.advance();
3,626✔
908
            let expectedEndKind = isSub ? TokenKind.EndSub : TokenKind.EndFunction;
3,626✔
909

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

919
            if (!body) {
3,626✔
920
                body = new Block({ statements: [] });
3✔
921
            }
922

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

934
            if (isAnonymous) {
3,626✔
935
                return func;
85✔
936
            } else {
937
                let result = new FunctionStatement({ name: name, func: func });
3,541✔
938
                return result;
3,541✔
939
            }
940
        } finally {
941
            this.namespaceAndFunctionDepth--;
3,629✔
942
            //restore the previous CallExpression list
943
            this.callExpressions = previousCallExpressions;
3,629✔
944
        }
945
    }
946

947
    private functionParameter(): FunctionParameterExpression {
948
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
2,957!
949
            this.diagnostics.push({
×
950
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
951
                location: this.peek().location
952
            });
953
            throw this.lastDiagnosticAsError();
×
954
        }
955

956
        let name = this.advance() as Identifier;
2,957✔
957
        // force the name into an identifier so the AST makes some sense
958
        name.kind = TokenKind.Identifier;
2,957✔
959

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

969
        let asToken: Token = null;
2,957✔
970
        if (this.check(TokenKind.As)) {
2,957✔
971
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
619✔
972

973
        }
974
        return new FunctionParameterExpression({
2,957✔
975
            name: name,
976
            equals: equalToken,
977
            defaultValue: defaultValue,
978
            as: asToken,
979
            typeExpression: typeExpression
980
        });
981
    }
982

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

995
        if (allowTypedAssignment) {
1,470✔
996
            //look for `as SOME_TYPE`
997
            if (this.check(TokenKind.As)) {
9!
998
                this.warnIfNotBrighterScriptMode('typed assignment');
9✔
999

1000
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
9✔
1001
            }
1002
        }
1003

1004
        let operator = this.consume(
1,470✔
1005
            DiagnosticMessages.expectedOperatorAfterIdentifier([TokenKind.Equal], name.text),
1006
            ...[TokenKind.Equal]
1007
        );
1008
        let value = this.expression();
1,467✔
1009

1010
        let result = new AssignmentStatement({ equals: operator, name: name, value: value, as: asToken, typeExpression: typeExpression });
1,460✔
1011

1012
        return result;
1,460✔
1013
    }
1014

1015
    private augmentedAssignment(): AugmentedAssignmentStatement {
1016
        let item = this.expression();
60✔
1017

1018
        let operator = this.consume(
60✔
1019
            DiagnosticMessages.expectedToken(...CompoundAssignmentOperators),
1020
            ...CompoundAssignmentOperators
1021
        );
1022
        let value = this.expression();
60✔
1023

1024
        let result = new AugmentedAssignmentStatement({
60✔
1025
            item: item,
1026
            operator: operator,
1027
            value: value
1028
        });
1029

1030
        return result;
60✔
1031
    }
1032

1033
    private checkLibrary() {
1034
        let isLibraryToken = this.check(TokenKind.Library);
20,727✔
1035

1036
        //if we are at the top level, any line that starts with "library" should be considered a library statement
1037
        if (this.isAtRootLevel() && isLibraryToken) {
20,727✔
1038
            return true;
12✔
1039

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

1045
            //definitely not a library statement
1046
        } else {
1047
            return false;
20,714✔
1048
        }
1049
    }
1050

1051
    private checkAlias() {
1052
        let isAliasToken = this.check(TokenKind.Alias);
20,483✔
1053

1054
        //if we are at the top level, any line that starts with "alias" should be considered a alias statement
1055
        if (this.isAtRootLevel() && isAliasToken) {
20,483✔
1056
            return true;
31✔
1057

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

1063
            //definitely not a alias statement
1064
        } else {
1065
            return false;
20,450✔
1066
        }
1067
    }
1068

1069
    private statement(): Statement | undefined {
1070
        if (this.checkLibrary()) {
10,277!
1071
            return this.libraryStatement();
×
1072
        }
1073

1074
        if (this.check(TokenKind.Import)) {
10,277✔
1075
            return this.importStatement();
207✔
1076
        }
1077

1078
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
10,070✔
1079
            return this.typecastStatement();
24✔
1080
        }
1081

1082
        if (this.checkAlias()) {
10,046!
1083
            return this.aliasStatement();
×
1084
        }
1085

1086
        if (this.check(TokenKind.Stop)) {
10,046✔
1087
            return this.stopStatement();
16✔
1088
        }
1089

1090
        if (this.check(TokenKind.If)) {
10,030✔
1091
            return this.ifStatement();
1,079✔
1092
        }
1093

1094
        //`try` must be followed by a block, otherwise it could be a local variable
1095
        if (this.check(TokenKind.Try) && this.checkAnyNext(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
8,951✔
1096
            return this.tryCatchStatement();
35✔
1097
        }
1098

1099
        if (this.check(TokenKind.Throw)) {
8,916✔
1100
            return this.throwStatement();
12✔
1101
        }
1102

1103
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
8,904✔
1104
            return this.printStatement();
1,169✔
1105
        }
1106
        if (this.check(TokenKind.Dim)) {
7,735✔
1107
            return this.dimStatement();
43✔
1108
        }
1109

1110
        if (this.check(TokenKind.While)) {
7,692✔
1111
            return this.whileStatement();
32✔
1112
        }
1113

1114
        if (this.checkAny(TokenKind.Exit, TokenKind.ExitWhile)) {
7,660✔
1115
            return this.exitStatement();
22✔
1116
        }
1117

1118
        if (this.check(TokenKind.For)) {
7,638✔
1119
            return this.forStatement();
39✔
1120
        }
1121

1122
        if (this.check(TokenKind.ForEach)) {
7,599✔
1123
            return this.forEachStatement();
36✔
1124
        }
1125

1126
        if (this.check(TokenKind.End)) {
7,563✔
1127
            return this.endStatement();
8✔
1128
        }
1129

1130
        if (this.match(TokenKind.Return)) {
7,555✔
1131
            return this.returnStatement();
3,193✔
1132
        }
1133

1134
        if (this.check(TokenKind.Goto)) {
4,362✔
1135
            return this.gotoStatement();
12✔
1136
        }
1137

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

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

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

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

1188
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1189
        if (this.check(TokenKind.Interface)) {
2,857✔
1190
            return this.interfaceDeclaration();
162✔
1191
        }
1192

1193
        if (this.check(TokenKind.Class)) {
2,695✔
1194
            return this.classDeclaration();
675✔
1195
        }
1196

1197
        if (this.check(TokenKind.Namespace)) {
2,020✔
1198
            return this.namespaceStatement();
623✔
1199
        }
1200

1201
        if (this.check(TokenKind.Enum)) {
1,397✔
1202
            return this.enumDeclaration();
165✔
1203
        }
1204

1205
        // TODO: support multi-statements
1206
        return this.setStatement();
1,232✔
1207
    }
1208

1209
    private whileStatement(): WhileStatement {
1210
        const whileKeyword = this.advance();
32✔
1211
        const condition = this.expression();
32✔
1212

1213
        this.consumeStatementSeparators();
31✔
1214

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

1229
        return new WhileStatement({
31✔
1230
            while: whileKeyword,
1231
            endWhile: endWhile,
1232
            condition: condition,
1233
            body: whileBlock
1234
        });
1235
    }
1236

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

1243
            const exitText = exitToken.text.substring(0, 4);
5✔
1244
            const whileText = exitToken.text.substring(4);
5✔
1245
            const originalRange = exitToken.location.range;
5✔
1246
            const originalStart = originalRange.start;
4✔
1247

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

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

1265
        const loopTypeToken = this.tryConsume(
21✔
1266
            DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
1267
            TokenKind.While, TokenKind.For
1268
        );
1269

1270
        return new ExitStatement({
21✔
1271
            exit: exitToken,
1272
            loopType: loopTypeToken
1273
        });
1274
    }
1275

1276
    private forStatement(): ForStatement {
1277
        const forToken = this.advance();
39✔
1278
        const initializer = this.assignment();
39✔
1279

1280
        //TODO: newline allowed?
1281

1282
        const toToken = this.advance();
38✔
1283
        const finalValue = this.expression();
38✔
1284
        let incrementExpression: Expression | undefined;
1285
        let stepToken: Token | undefined;
1286

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

1294
        this.consumeStatementSeparators();
38✔
1295

1296
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
38✔
1297
        let endForToken: Token;
1298
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
38✔
1299
            this.diagnostics.push({
1✔
1300
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1301
                location: this.peek().location
1302
            });
1303
            if (!body) {
1!
1304
                throw this.lastDiagnosticAsError();
×
1305
            }
1306
        } else {
1307
            endForToken = this.advance();
37✔
1308
        }
1309

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

1324
    private forEachStatement(): ForEachStatement {
1325
        let forEach = this.advance();
36✔
1326
        let name = this.advance();
36✔
1327

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

1340
        let target = this.expression();
36✔
1341
        if (!target) {
36!
1342
            this.diagnostics.push({
×
1343
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1344
                location: this.peek().location
1345
            });
1346
            throw this.lastDiagnosticAsError();
×
1347
        }
1348

1349
        this.consumeStatementSeparators();
36✔
1350

1351
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
36✔
1352
        if (!body) {
36!
1353
            this.diagnostics.push({
×
1354
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1355
                location: this.peek().location
1356
            });
1357
            throw this.lastDiagnosticAsError();
×
1358
        }
1359

1360
        let endFor = this.advance();
36✔
1361

1362
        return new ForEachStatement({
36✔
1363
            forEach: forEach,
1364
            in: maybeIn,
1365
            endFor: endFor,
1366
            item: name,
1367
            target: target,
1368
            body: body
1369
        });
1370
    }
1371

1372
    private namespaceStatement(): NamespaceStatement | undefined {
1373
        this.warnIfNotBrighterScriptMode('namespace');
623✔
1374
        let keyword = this.advance();
623✔
1375

1376
        this.namespaceAndFunctionDepth++;
623✔
1377

1378
        let name = this.identifyingExpression();
623✔
1379
        //set the current namespace name
1380

1381
        this.globalTerminators.push([TokenKind.EndNamespace]);
622✔
1382
        let body = this.body();
622✔
1383
        this.globalTerminators.pop();
622✔
1384

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

1396
        this.namespaceAndFunctionDepth--;
622✔
1397

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

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

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

1422
        let expr: DottedGetExpression | VariableExpression;
1423

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

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

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

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

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

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

1508
        return libStatement;
13✔
1509
    }
1510

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

1522
        return importStatement;
207✔
1523
    }
1524

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

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

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

1564
        });
1565

1566
        return aliasStmt;
33✔
1567
    }
1568

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

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

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

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

1598
        let consequent: Expression;
1599
        try {
80✔
1600
            consequent = this.expression();
80✔
1601
        } catch { }
1602

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

1608
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
80✔
1609

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

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

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

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

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

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

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

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

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

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

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

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

1747
        let tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
35✔
1748

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

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

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

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

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

1808
    private dimStatement() {
1809
        const dim = this.advance();
43✔
1810

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

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

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

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

1851
    private nestedInlineConditionalCount = 0;
3,574✔
1852

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

1867
        const ifToken = this.advance();
2,035✔
1868

1869
        const condition = this.expression();
2,035✔
1870
        let thenBranch: Block;
1871
        let elseBranch: IfStatement | Block | undefined;
1872

1873
        let thenToken: Token | undefined;
1874
        let endIfToken: Token | undefined;
1875
        let elseToken: Token | undefined;
1876

1877
        //optional `then`
1878
        if (this.check(TokenKind.Then)) {
2,033✔
1879
            thenToken = this.advance();
1,623✔
1880
        }
1881

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

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

1891
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
48✔
1892

1893
            if (!thenBranch) {
48!
1894
                this.diagnostics.push({
×
1895
                    ...DiagnosticMessages.expectedStatementToFollowConditionalCondition(ifToken.text),
1896
                    location: this.peek().location
1897
                });
1898
                throw this.lastDiagnosticAsError();
×
1899
            } else {
1900
                this.ensureInline(thenBranch.statements);
48✔
1901
            }
1902

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

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

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

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

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

1934
                if (!elseBranch) {
29!
1935
                    //missing `else` branch
1936
                    this.diagnostics.push({
×
1937
                        ...DiagnosticMessages.expectedStatementToFollowElse(),
1938
                        location: this.peek().location
1939
                    });
1940
                    throw this.lastDiagnosticAsError();
×
1941
                }
1942
            }
1943

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

1964
            thenBranch = this.blockConditionalBranch(ifToken);
1,985✔
1965

1966
            //ensure newline/colon before next keyword
1967
            this.ensureNewLineOrColon();
1,982✔
1968

1969
            //else branch
1970
            if (this.check(TokenKind.Else)) {
1,982✔
1971
                elseToken = this.advance();
1,591✔
1972

1973
                if (this.check(TokenKind.If)) {
1,591✔
1974
                    // recurse-read `else if`
1975
                    elseBranch = this.ifStatement();
946✔
1976

1977
                } else {
1978
                    elseBranch = this.blockConditionalBranch(ifToken);
645✔
1979

1980
                    //ensure newline/colon before next keyword
1981
                    this.ensureNewLineOrColon();
645✔
1982
                }
1983
            }
1984

1985
            if (!isIfStatement(elseBranch)) {
1,982✔
1986
                if (this.check(TokenKind.EndIf)) {
1,036✔
1987
                    endIfToken = this.advance();
1,033✔
1988

1989
                } else {
1990
                    //missing endif
1991
                    this.diagnostics.push({
3✔
1992
                        ...DiagnosticMessages.expectedEndIfToCloseIfStatement(ifToken.location?.range.start),
9!
1993
                        location: ifToken.location
1994
                    });
1995
                }
1996
            }
1997
        }
1998

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

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

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

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

2026
            //this whole if statement is bogus...add error to the if token and hard-fail
2027
            this.diagnostics.push({
3✔
2028
                ...DiagnosticMessages.expectedEndIfElseIfOrElseToTerminateThenBlock(),
2029
                location: ifToken.location
2030
            });
2031
            throw this.lastDiagnosticAsError();
3✔
2032
        }
2033
        return branch;
2,627✔
2034
    }
2035

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

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

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

2051

2052
        const condition = this.advance();
56✔
2053

2054
        let thenBranch: Block;
2055
        let elseBranch: ConditionalCompileStatement | Block | undefined;
2056

2057
        let hashEndIfToken: Token | undefined;
2058
        let hashElseToken: Token | undefined;
2059

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

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

2071
        this.ensureNewLine();
55✔
2072
        this.advance();
55✔
2073

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

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

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

2093
        if (!isConditionalCompileStatement(elseBranch)) {
55✔
2094

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

2098
            } else {
2099
                //missing #endif
2100
                this.diagnostics.push({
×
2101
                    ...DiagnosticMessages.expectedHashEndIfToCloseHashIf(hashIfToken.location?.range.start.line),
×
2102
                    location: hashIfToken.location
2103
                });
2104
            }
2105
        }
2106

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

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

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

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

2133
            //this whole if statement is bogus...add error to the if token and hard-fail
2134
            this.diagnostics.push({
×
2135
                ...DiagnosticMessages.expectedTerminatorOnConditionalCompileBlock(),
2136
                location: hashIfToken.location
2137
            });
2138
            throw this.lastDiagnosticAsError();
×
2139
        }
2140
        return branch;
65✔
2141
    }
2142

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

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

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

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

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

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

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

2191

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

2223
    private conditionalCompileConstStatement() {
2224
        const hashConstToken = this.advance();
21✔
2225

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

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

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

2262
        if (!this.check(TokenKind.Newline)) {
18!
2263
            this.diagnostics.push({
×
2264
                ...DiagnosticMessages.expectedNewlineInConditionalCompile(),
2265
                location: this.peek().location
2266
            });
2267
            throw this.lastDiagnosticAsError();
×
2268
        }
2269

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

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

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

2291
    private ensureNewLineOrColon(silent = false) {
2,627✔
2292
        const prev = this.previous().kind;
2,815✔
2293
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
2,815✔
2294
            if (!silent) {
133✔
2295
                this.diagnostics.push({
6✔
2296
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2297
                    location: this.peek().location
2298
                });
2299
            }
2300
            return false;
133✔
2301
        }
2302
        return true;
2,682✔
2303
    }
2304

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

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

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

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

2355
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2356
        let expressionStart = this.peek();
832✔
2357

2358
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
832✔
2359
            let operator = this.advance();
25✔
2360

2361
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
25✔
2362
                this.diagnostics.push({
1✔
2363
                    ...DiagnosticMessages.consecutiveIncrementDecrementOperatorsAreNotAllowed(),
2364
                    location: this.peek().location
2365
                });
2366
                throw this.lastDiagnosticAsError();
1✔
2367
            } else if (isCallExpression(expr)) {
24✔
2368
                this.diagnostics.push({
1✔
2369
                    ...DiagnosticMessages.incrementDecrementOperatorsAreNotAllowedAsResultOfFunctionCall(),
2370
                    location: expressionStart.location
2371
                });
2372
                throw this.lastDiagnosticAsError();
1✔
2373
            }
2374

2375
            const result = new IncrementStatement({ value: expr, operator: operator });
23✔
2376
            return result;
23✔
2377
        }
2378

2379
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
807✔
2380
            return new ExpressionStatement({ expression: expr });
454✔
2381
        }
2382

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

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

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

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

2440
    private printStatement(): PrintStatement {
2441
        let printKeyword = this.advance();
1,169✔
2442

2443
        let values: (
2444
            | Expression
2445
            | PrintSeparatorTab
2446
            | PrintSeparatorSpace)[] = [];
1,169✔
2447

2448
        while (!this.checkEndOfStatement()) {
1,169✔
2449
            if (this.check(TokenKind.Semicolon)) {
1,278✔
2450
                values.push(this.advance() as PrintSeparatorSpace);
29✔
2451
            } else if (this.check(TokenKind.Comma)) {
1,249✔
2452
                values.push(this.advance() as PrintSeparatorTab);
13✔
2453
            } else if (this.check(TokenKind.Else)) {
1,236✔
2454
                break; // inline branch
22✔
2455
            } else {
2456
                values.push(this.expression());
1,214✔
2457
            }
2458
        }
2459

2460
        //print statements can be empty, so look for empty print conditions
2461
        if (!values.length) {
1,168✔
2462
            const endOfStatementLocation = util.createBoundingLocation(printKeyword, this.peek());
12✔
2463
            let emptyStringLiteral = createStringLiteral('', endOfStatementLocation);
12✔
2464
            values.push(emptyStringLiteral);
12✔
2465
        }
2466

2467
        let last = values[values.length - 1];
1,168✔
2468
        if (isToken(last)) {
1,168✔
2469
            // TODO: error, expected value
2470
        }
2471

2472
        return new PrintStatement({ print: printKeyword, expressions: values });
1,168✔
2473
    }
2474

2475
    /**
2476
     * Parses a return statement with an optional return value.
2477
     * @returns an AST representation of a return statement.
2478
     */
2479
    private returnStatement(): ReturnStatement {
2480
        let options = { return: this.previous() };
3,193✔
2481

2482
        if (this.checkEndOfStatement()) {
3,193✔
2483
            return new ReturnStatement(options);
16✔
2484
        }
2485

2486
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
3,177✔
2487
        return new ReturnStatement({ ...options, value: toReturn });
3,176✔
2488
    }
2489

2490
    /**
2491
     * Parses a `label` statement
2492
     * @returns an AST representation of an `label` statement.
2493
     */
2494
    private labelStatement() {
2495
        let options = {
12✔
2496
            name: this.advance(),
2497
            colon: this.advance()
2498
        };
2499

2500
        //label must be alone on its line, this is probably not a label
2501
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
12✔
2502
            //rewind and cancel
2503
            this.current -= 2;
2✔
2504
            throw new CancelStatementError();
2✔
2505
        }
2506

2507
        return new LabelStatement(options);
10✔
2508
    }
2509

2510
    /**
2511
     * Parses a `continue` statement
2512
     */
2513
    private continueStatement() {
2514
        return new ContinueStatement({
12✔
2515
            continue: this.advance(),
2516
            loopType: this.tryConsume(
2517
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2518
                TokenKind.While, TokenKind.For
2519
            )
2520
        });
2521
    }
2522

2523
    /**
2524
     * Parses a `goto` statement
2525
     * @returns an AST representation of an `goto` statement.
2526
     */
2527
    private gotoStatement() {
2528
        let tokens = {
12✔
2529
            goto: this.advance(),
2530
            label: this.consume(
2531
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2532
                TokenKind.Identifier
2533
            )
2534
        };
2535

2536
        return new GotoStatement(tokens);
10✔
2537
    }
2538

2539
    /**
2540
     * Parses an `end` statement
2541
     * @returns an AST representation of an `end` statement.
2542
     */
2543
    private endStatement() {
2544
        let options = { end: this.advance() };
8✔
2545

2546
        return new EndStatement(options);
8✔
2547
    }
2548
    /**
2549
     * Parses a `stop` statement
2550
     * @returns an AST representation of a `stop` statement
2551
     */
2552
    private stopStatement() {
2553
        let options = { stop: this.advance() };
16✔
2554

2555
        return new StopStatement(options);
16✔
2556
    }
2557

2558
    /**
2559
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2560
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2561
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2562
     *                    ignored.
2563
     */
2564
    private block(...terminators: BlockTerminator[]): Block | undefined {
2565
        const parentAnnotations = this.enterAnnotationBlock();
6,429✔
2566

2567
        this.consumeStatementSeparators(true);
6,429✔
2568
        const statements: Statement[] = [];
6,429✔
2569
        const flatGlobalTerminators = this.globalTerminators.flat().flat();
6,429✔
2570
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators, ...flatGlobalTerminators)) {
6,429✔
2571
            //grab the location of the current token
2572
            let loopCurrent = this.current;
7,586✔
2573
            let dec = this.declaration();
7,586✔
2574
            if (dec) {
7,586✔
2575
                if (!isAnnotationExpression(dec)) {
7,533✔
2576
                    this.consumePendingAnnotations(dec);
7,526✔
2577
                    statements.push(dec);
7,526✔
2578
                }
2579

2580
                //ensure statement separator
2581
                this.consumeStatementSeparators();
7,533✔
2582

2583
            } else {
2584
                //something went wrong. reset to the top of the loop
2585
                this.current = loopCurrent;
53✔
2586

2587
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2588
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
53✔
2589

2590
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2591
                //but there's already an error in the file being parsed, so just leave this line here
2592
                this.advance();
53✔
2593

2594
                //consume potential separators
2595
                this.consumeStatementSeparators(true);
53✔
2596
            }
2597
        }
2598

2599
        if (this.isAtEnd()) {
6,429✔
2600
            return undefined;
6✔
2601
            // TODO: Figure out how to handle unterminated blocks well
2602
        } else if (terminators.length > 0) {
6,423✔
2603
            //did we hit end-sub / end-function while looking for some other terminator?
2604
            //if so, we need to restore the statement separator
2605
            let prev = this.previous().kind;
2,800✔
2606
            let peek = this.peek().kind;
2,800✔
2607
            if (
2,800✔
2608
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
5,604!
2609
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2610
            ) {
2611
                this.current--;
6✔
2612
            }
2613
        }
2614

2615
        this.exitAnnotationBlock(parentAnnotations);
6,423✔
2616
        return new Block({ statements: statements });
6,423✔
2617
    }
2618

2619
    /**
2620
     * Attach pending annotations to the provided statement,
2621
     * and then reset the annotations array
2622
     */
2623
    consumePendingAnnotations(statement: Statement) {
2624
        if (this.pendingAnnotations.length) {
14,802✔
2625
            statement.annotations = this.pendingAnnotations;
51✔
2626
            this.pendingAnnotations = [];
51✔
2627
        }
2628
    }
2629

2630
    enterAnnotationBlock() {
2631
        const pending = this.pendingAnnotations;
11,681✔
2632
        this.pendingAnnotations = [];
11,681✔
2633
        return pending;
11,681✔
2634
    }
2635

2636
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2637
        // non consumed annotations are an error
2638
        if (this.pendingAnnotations.length) {
11,673✔
2639
            for (const annotation of this.pendingAnnotations) {
5✔
2640
                this.diagnostics.push({
7✔
2641
                    ...DiagnosticMessages.unusedAnnotation(),
2642
                    location: annotation.location
2643
                });
2644
            }
2645
        }
2646
        this.pendingAnnotations = parentAnnotations;
11,673✔
2647
    }
2648

2649
    private expression(findTypecast = true): Expression {
11,691✔
2650
        let expression = this.anonymousFunction();
12,073✔
2651
        let asToken: Token;
2652
        let typeExpression: TypeExpression;
2653
        if (findTypecast) {
12,036✔
2654
            do {
11,681✔
2655
                if (this.check(TokenKind.As)) {
11,751✔
2656
                    this.warnIfNotBrighterScriptMode('type cast');
72✔
2657
                    // Check if this expression is wrapped in any type casts
2658
                    // allows for multiple casts:
2659
                    // myVal = foo() as dynamic as string
2660
                    [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
72✔
2661
                    if (asToken && typeExpression) {
72✔
2662
                        expression = new TypecastExpression({ obj: expression, as: asToken, typeExpression: typeExpression });
70✔
2663
                    }
2664
                } else {
2665
                    break;
11,679✔
2666
                }
2667

2668
            } while (asToken && typeExpression);
144✔
2669
        }
2670
        return expression;
12,036✔
2671
    }
2672

2673
    private anonymousFunction(): Expression {
2674
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
12,073✔
2675
            const func = this.functionDeclaration(true);
85✔
2676
            //if there's an open paren after this, this is an IIFE
2677
            if (this.check(TokenKind.LeftParen)) {
85✔
2678
                return this.finishCall(this.advance(), func);
3✔
2679
            } else {
2680
                return func;
82✔
2681
            }
2682
        }
2683

2684
        let expr = this.boolean();
11,988✔
2685

2686
        if (this.check(TokenKind.Question)) {
11,951✔
2687
            return this.ternaryExpression(expr);
80✔
2688
        } else if (this.check(TokenKind.QuestionQuestion)) {
11,871✔
2689
            return this.nullCoalescingExpression(expr);
34✔
2690
        } else {
2691
            return expr;
11,837✔
2692
        }
2693
    }
2694

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

2698
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
11,951✔
2699
            let operator = this.previous();
32✔
2700
            let right = this.relational();
32✔
2701
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
32✔
2702
        }
2703

2704
        return expr;
11,951✔
2705
    }
2706

2707
    private relational(): Expression {
2708
        let expr = this.additive();
12,047✔
2709

2710
        while (
12,010✔
2711
            this.matchAny(
2712
                TokenKind.Equal,
2713
                TokenKind.LessGreater,
2714
                TokenKind.Greater,
2715
                TokenKind.GreaterEqual,
2716
                TokenKind.Less,
2717
                TokenKind.LessEqual
2718
            )
2719
        ) {
2720
            let operator = this.previous();
1,686✔
2721
            let right = this.additive();
1,686✔
2722
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,686✔
2723
        }
2724

2725
        return expr;
12,010✔
2726
    }
2727

2728
    // TODO: bitshift
2729

2730
    private additive(): Expression {
2731
        let expr = this.multiplicative();
13,733✔
2732

2733
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
13,696✔
2734
            let operator = this.previous();
1,356✔
2735
            let right = this.multiplicative();
1,356✔
2736
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,356✔
2737
        }
2738

2739
        return expr;
13,696✔
2740
    }
2741

2742
    private multiplicative(): Expression {
2743
        let expr = this.exponential();
15,089✔
2744

2745
        while (this.matchAny(
15,052✔
2746
            TokenKind.Forwardslash,
2747
            TokenKind.Backslash,
2748
            TokenKind.Star,
2749
            TokenKind.Mod,
2750
            TokenKind.LeftShift,
2751
            TokenKind.RightShift
2752
        )) {
2753
            let operator = this.previous();
54✔
2754
            let right = this.exponential();
54✔
2755
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
54✔
2756
        }
2757

2758
        return expr;
15,052✔
2759
    }
2760

2761
    private exponential(): Expression {
2762
        let expr = this.prefixUnary();
15,143✔
2763

2764
        while (this.match(TokenKind.Caret)) {
15,106✔
2765
            let operator = this.previous();
8✔
2766
            let right = this.prefixUnary();
8✔
2767
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
8✔
2768
        }
2769

2770
        return expr;
15,106✔
2771
    }
2772

2773
    private prefixUnary(): Expression {
2774
        const nextKind = this.peek().kind;
15,187✔
2775
        if (nextKind === TokenKind.Not) {
15,187✔
2776
            this.current++; //advance
27✔
2777
            let operator = this.previous();
27✔
2778
            let right = this.relational();
27✔
2779
            return new UnaryExpression({ operator: operator, right: right });
27✔
2780
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
15,160✔
2781
            this.current++; //advance
36✔
2782
            let operator = this.previous();
36✔
2783
            let right = (nextKind as any) === TokenKind.Not
36✔
2784
                ? this.boolean()
36!
2785
                : this.prefixUnary();
2786
            return new UnaryExpression({ operator: operator, right: right });
36✔
2787
        }
2788
        return this.call();
15,124✔
2789
    }
2790

2791
    private indexedGet(expr: Expression) {
2792
        let openingSquare = this.previous();
153✔
2793
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
153✔
2794
        let indexes: Expression[] = [];
153✔
2795

2796

2797
        //consume leading newlines
2798
        while (this.match(TokenKind.Newline)) { }
153✔
2799

2800
        try {
153✔
2801
            indexes.push(
153✔
2802
                this.expression()
2803
            );
2804
            //consume additional indexes separated by commas
2805
            while (this.check(TokenKind.Comma)) {
151✔
2806
                //discard the comma
2807
                this.advance();
17✔
2808
                indexes.push(
17✔
2809
                    this.expression()
2810
                );
2811
            }
2812
        } catch (error) {
2813
            this.rethrowNonDiagnosticError(error);
2✔
2814
        }
2815
        //consume trailing newlines
2816
        while (this.match(TokenKind.Newline)) { }
153✔
2817

2818
        const closingSquare = this.tryConsume(
153✔
2819
            DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
2820
            TokenKind.RightSquareBracket
2821
        );
2822

2823
        return new IndexedGetExpression({
153✔
2824
            obj: expr,
2825
            indexes: indexes,
2826
            openingSquare: openingSquare,
2827
            closingSquare: closingSquare,
2828
            questionDot: questionDotToken
2829
        });
2830
    }
2831

2832
    private newExpression() {
2833
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
134✔
2834
        let newToken = this.advance();
134✔
2835

2836
        let nameExpr = this.identifyingExpression();
134✔
2837
        let leftParen = this.tryConsume(
134✔
2838
            DiagnosticMessages.unexpectedToken(this.peek().text),
2839
            TokenKind.LeftParen,
2840
            TokenKind.QuestionLeftParen
2841
        );
2842

2843
        if (!leftParen) {
134✔
2844
            // new expression without a following call expression
2845
            // wrap the name in an expression
2846
            const endOfStatementLocation = util.createBoundingLocation(newToken, this.peek());
4✔
2847
            const exprStmt = nameExpr ?? createStringLiteral('', endOfStatementLocation);
4!
2848
            return new ExpressionStatement({ expression: exprStmt });
4✔
2849
        }
2850

2851
        let call = this.finishCall(leftParen, nameExpr);
130✔
2852
        //pop the call from the  callExpressions list because this is technically something else
2853
        this.callExpressions.pop();
130✔
2854
        let result = new NewExpression({ new: newToken, call: call });
130✔
2855
        return result;
130✔
2856
    }
2857

2858
    /**
2859
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2860
     */
2861
    private callfunc(callee: Expression): Expression {
2862
        this.warnIfNotBrighterScriptMode('callfunc operator');
40✔
2863
        let operator = this.previous();
40✔
2864
        let methodName = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
40✔
2865
        let openParen: Token;
2866
        let call: CallExpression;
2867
        if (methodName) {
40✔
2868
            // force it into an identifier so the AST makes some sense
2869
            methodName.kind = TokenKind.Identifier;
35✔
2870
            openParen = this.tryConsume(DiagnosticMessages.expectedOpenParenToFollowCallfuncIdentifier(), TokenKind.LeftParen);
35✔
2871
            if (openParen) {
35!
2872
                call = this.finishCall(openParen, callee, false);
35✔
2873
            }
2874
        }
2875
        return new CallfuncExpression({
40✔
2876
            callee: callee,
2877
            operator: operator,
2878
            methodName: methodName as Identifier,
2879
            openingParen: openParen,
2880
            args: call?.args,
120✔
2881
            closingParen: call?.tokens?.closingParen
240✔
2882
        });
2883
    }
2884

2885
    private call(): Expression {
2886
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
16,356✔
2887
            return this.newExpression();
134✔
2888
        }
2889
        let expr = this.primary();
16,222✔
2890

2891
        while (true) {
16,132✔
2892
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
20,808✔
2893
                expr = this.finishCall(this.previous(), expr);
2,160✔
2894
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
18,648✔
2895
                expr = this.indexedGet(expr);
151✔
2896
            } else if (this.match(TokenKind.Callfunc)) {
18,497✔
2897
                expr = this.callfunc(expr);
40✔
2898
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
18,457✔
2899
                if (this.match(TokenKind.LeftSquareBracket)) {
2,364✔
2900
                    expr = this.indexedGet(expr);
2✔
2901
                } else {
2902
                    let dot = this.previous();
2,362✔
2903
                    let name = this.tryConsume(
2,362✔
2904
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2905
                        TokenKind.Identifier,
2906
                        ...AllowedProperties
2907
                    );
2908
                    if (!name) {
2,362✔
2909
                        break;
39✔
2910
                    }
2911

2912
                    // force it into an identifier so the AST makes some sense
2913
                    name.kind = TokenKind.Identifier;
2,323✔
2914
                    expr = new DottedGetExpression({ obj: expr, name: name as Identifier, dot: dot });
2,323✔
2915
                }
2916

2917
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
16,093✔
2918
                let dot = this.advance();
11✔
2919
                let name = this.tryConsume(
11✔
2920
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2921
                    TokenKind.Identifier,
2922
                    ...AllowedProperties
2923
                );
2924

2925
                // force it into an identifier so the AST makes some sense
2926
                name.kind = TokenKind.Identifier;
11✔
2927
                if (!name) {
11!
UNCOV
2928
                    break;
×
2929
                }
2930
                expr = new XmlAttributeGetExpression({ obj: expr, name: name as Identifier, at: dot });
11✔
2931
                //only allow a single `@` expression
2932
                break;
11✔
2933

2934
            } else {
2935
                break;
16,082✔
2936
            }
2937
        }
2938

2939
        return expr;
16,132✔
2940
    }
2941

2942
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
2,293✔
2943
        let args = [] as Expression[];
2,358✔
2944
        while (this.match(TokenKind.Newline)) { }
2,358✔
2945

2946
        if (!this.check(TokenKind.RightParen)) {
2,358✔
2947
            do {
1,201✔
2948
                while (this.match(TokenKind.Newline)) { }
1,726✔
2949

2950
                if (args.length >= CallExpression.MaximumArguments) {
1,726!
UNCOV
2951
                    this.diagnostics.push({
×
2952
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
2953
                        location: this.peek()?.location
×
2954
                    });
UNCOV
2955
                    throw this.lastDiagnosticAsError();
×
2956
                }
2957
                try {
1,726✔
2958
                    args.push(this.expression());
1,726✔
2959
                } catch (error) {
2960
                    this.rethrowNonDiagnosticError(error);
5✔
2961
                    // we were unable to get an expression, so don't continue
2962
                    break;
5✔
2963
                }
2964
            } while (this.match(TokenKind.Comma));
2965
        }
2966

2967
        while (this.match(TokenKind.Newline)) { }
2,358✔
2968

2969
        const closingParen = this.tryConsume(
2,358✔
2970
            DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
2971
            TokenKind.RightParen
2972
        );
2973

2974
        let expression = new CallExpression({
2,358✔
2975
            callee: callee,
2976
            openingParen: openingParen,
2977
            args: args,
2978
            closingParen: closingParen
2979
        });
2980
        if (addToCallExpressionList) {
2,358✔
2981
            this.callExpressions.push(expression);
2,293✔
2982
        }
2983
        return expression;
2,358✔
2984
    }
2985

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

3008
        } catch (error) {
3009
            // Something went wrong - reset the kind to what it was previously
UNCOV
3010
            for (const changedToken of changedTokens) {
×
UNCOV
3011
                changedToken.token.kind = changedToken.oldKind;
×
3012
            }
UNCOV
3013
            throw error;
×
3014
        }
3015
    }
3016

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

3027
        if (this.checkAny(...DeclarableTypes)) {
1,562✔
3028
            // if this is just a type, just use directly
3029
            expr = new VariableExpression({ name: this.advance() as Identifier });
1,040✔
3030
        } else {
3031
            if (this.options.mode === ParseMode.BrightScript && !declarableTypesLower.includes(this.peek()?.text?.toLowerCase())) {
522!
3032
                // custom types arrays not allowed in Brightscript
3033
                this.warnIfNotBrighterScriptMode('custom types');
14✔
3034
                return expr;
14✔
3035
            }
3036

3037
            if (this.checkAny(...AllowedTypeIdentifiers)) {
508✔
3038
                // Since the next token is allowed as a type identifier, change the kind
3039
                let nextToken = this.peek();
1✔
3040
                changedTokens.push({ token: nextToken, oldKind: nextToken.kind });
1✔
3041
                nextToken.kind = TokenKind.Identifier;
1✔
3042
            }
3043
            expr = this.identifyingExpression(AllowedTypeIdentifiers);
508✔
3044
        }
3045

3046
        //Check if it has square brackets, thus making it an array
3047
        if (expr && this.check(TokenKind.LeftSquareBracket)) {
1,548✔
3048
            if (this.options.mode === ParseMode.BrightScript) {
28✔
3049
                // typed arrays not allowed in Brightscript
3050
                this.warnIfNotBrighterScriptMode('typed arrays');
1✔
3051
                return expr;
1✔
3052
            }
3053

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

3066
        return expr;
1,547✔
3067
    }
3068

3069
    private primary(): Expression {
3070
        switch (true) {
16,222✔
3071
            case this.matchAny(
16,222!
3072
                TokenKind.False,
3073
                TokenKind.True,
3074
                TokenKind.Invalid,
3075
                TokenKind.IntegerLiteral,
3076
                TokenKind.LongIntegerLiteral,
3077
                TokenKind.FloatLiteral,
3078
                TokenKind.DoubleLiteral,
3079
                TokenKind.StringLiteral
3080
            ):
3081
                return new LiteralExpression({ value: this.previous() });
7,204✔
3082

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

3087
            //template string
3088
            case this.check(TokenKind.BackTick):
3089
                return this.templateString(false);
43✔
3090

3091
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
3092
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
17,277✔
3093
                return this.templateString(true);
8✔
3094

3095
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3096
                return new VariableExpression({ name: this.previous() as Identifier });
8,336✔
3097

3098
            case this.match(TokenKind.LeftParen):
3099
                let left = this.previous();
50✔
3100
                let expr = this.expression();
50✔
3101
                let right = this.consume(
49✔
3102
                    DiagnosticMessages.unmatchedLeftParenAfterExpression(),
3103
                    TokenKind.RightParen
3104
                );
3105
                return new GroupingExpression({ leftParen: left, rightParen: right, expression: expr });
49✔
3106

3107
            case this.matchAny(TokenKind.LeftSquareBracket):
3108
                return this.arrayLiteral();
140✔
3109

3110
            case this.match(TokenKind.LeftCurlyBrace):
3111
                return this.aaLiteral();
274✔
3112

3113
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
UNCOV
3114
                let token = Object.assign(this.previous(), {
×
3115
                    kind: TokenKind.Identifier
3116
                }) as Identifier;
UNCOV
3117
                return new VariableExpression({ name: token });
×
3118

3119
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
UNCOV
3120
                return this.anonymousFunction();
×
3121

3122
            case this.check(TokenKind.RegexLiteral):
3123
                return this.regexLiteralExpression();
45✔
3124

3125
            default:
3126
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
3127
                if (this.checkAny(...this.peekGlobalTerminators())) {
87!
3128
                    //don't throw a diagnostic, just return undefined
3129

3130
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
3131
                } else {
3132
                    this.diagnostics.push({
87✔
3133
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
3134
                        location: this.peek()?.location
261!
3135
                    });
3136
                    throw this.lastDiagnosticAsError();
87✔
3137
                }
3138
        }
3139
    }
3140

3141
    private arrayLiteral() {
3142
        let elements: Array<Expression> = [];
140✔
3143
        let openingSquare = this.previous();
140✔
3144

3145
        while (this.match(TokenKind.Newline)) {
140✔
3146
        }
3147
        let closingSquare: Token;
3148

3149
        if (!this.match(TokenKind.RightSquareBracket)) {
140✔
3150
            try {
103✔
3151
                elements.push(this.expression());
103✔
3152

3153
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
102✔
3154

3155
                    while (this.match(TokenKind.Newline)) {
140✔
3156

3157
                    }
3158

3159
                    if (this.check(TokenKind.RightSquareBracket)) {
140✔
3160
                        break;
25✔
3161
                    }
3162

3163
                    elements.push(this.expression());
115✔
3164
                }
3165
            } catch (error: any) {
3166
                this.rethrowNonDiagnosticError(error);
2✔
3167
            }
3168

3169
            closingSquare = this.tryConsume(
103✔
3170
                DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral(),
3171
                TokenKind.RightSquareBracket
3172
            );
3173
        } else {
3174
            closingSquare = this.previous();
37✔
3175
        }
3176

3177
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3178
        return new ArrayLiteralExpression({ elements: elements, open: openingSquare, close: closingSquare });
140✔
3179
    }
3180

3181
    private aaLiteral() {
3182
        let openingBrace = this.previous();
274✔
3183
        let members: Array<AAMemberExpression> = [];
274✔
3184

3185
        let key = () => {
274✔
3186
            let result = {
284✔
3187
                colonToken: null as Token,
3188
                keyToken: null as Token,
3189
                range: null as Range
3190
            };
3191
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
284✔
3192
                result.keyToken = this.identifier(...AllowedProperties);
253✔
3193
            } else if (this.check(TokenKind.StringLiteral)) {
31!
3194
                result.keyToken = this.advance();
31✔
3195
            } else {
UNCOV
3196
                this.diagnostics.push({
×
3197
                    ...DiagnosticMessages.unexpectedAAKey(),
3198
                    location: this.peek().location
3199
                });
UNCOV
3200
                throw this.lastDiagnosticAsError();
×
3201
            }
3202

3203
            result.colonToken = this.consume(
284✔
3204
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3205
                TokenKind.Colon
3206
            );
3207
            result.range = util.createBoundingRange(result.keyToken, result.colonToken);
283✔
3208
            return result;
283✔
3209
        };
3210

3211
        while (this.match(TokenKind.Newline)) { }
274✔
3212
        let closingBrace: Token;
3213
        if (!this.match(TokenKind.RightCurlyBrace)) {
274✔
3214
            let lastAAMember: AAMemberExpression;
3215
            try {
197✔
3216
                let k = key();
197✔
3217
                let expr = this.expression();
197✔
3218
                lastAAMember = new AAMemberExpression({
196✔
3219
                    key: k.keyToken,
3220
                    colon: k.colonToken,
3221
                    value: expr
3222
                });
3223
                members.push(lastAAMember);
196✔
3224

3225
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
196✔
3226
                    // collect comma at end of expression
3227
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
214✔
3228
                        (lastAAMember as DeepWriteable<AAMemberExpression>).tokens.comma = this.previous();
62✔
3229
                    }
3230

3231
                    this.consumeStatementSeparators(true);
214✔
3232

3233
                    if (this.check(TokenKind.RightCurlyBrace)) {
214✔
3234
                        break;
127✔
3235
                    }
3236
                    let k = key();
87✔
3237
                    let expr = this.expression();
86✔
3238
                    lastAAMember = new AAMemberExpression({
86✔
3239
                        key: k.keyToken,
3240
                        colon: k.colonToken,
3241
                        value: expr
3242
                    });
3243
                    members.push(lastAAMember);
86✔
3244

3245
                }
3246
            } catch (error: any) {
3247
                this.rethrowNonDiagnosticError(error);
2✔
3248
            }
3249

3250
            closingBrace = this.tryConsume(
197✔
3251
                DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral(),
3252
                TokenKind.RightCurlyBrace
3253
            );
3254
        } else {
3255
            closingBrace = this.previous();
77✔
3256
        }
3257

3258
        const aaExpr = new AALiteralExpression({ elements: members, open: openingBrace, close: closingBrace });
274✔
3259
        return aaExpr;
274✔
3260
    }
3261

3262
    /**
3263
     * Pop token if we encounter specified token
3264
     */
3265
    private match(tokenKind: TokenKind) {
3266
        if (this.check(tokenKind)) {
60,789✔
3267
            this.current++; //advance
6,127✔
3268
            return true;
6,127✔
3269
        }
3270
        return false;
54,662✔
3271
    }
3272

3273
    /**
3274
     * Pop token if we encounter a token in the specified list
3275
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3276
     */
3277
    private matchAny(...tokenKinds: TokenKind[]) {
3278
        for (let tokenKind of tokenKinds) {
212,957✔
3279
            if (this.check(tokenKind)) {
596,288✔
3280
                this.current++; //advance
56,343✔
3281
                return true;
56,343✔
3282
            }
3283
        }
3284
        return false;
156,614✔
3285
    }
3286

3287
    /**
3288
     * If the next series of tokens matches the given set of tokens, pop them all
3289
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
3290
     */
3291
    private matchSequence(...tokenKinds: TokenKind[]) {
3292
        const endIndex = this.current + tokenKinds.length;
18,500✔
3293
        for (let i = 0; i < tokenKinds.length; i++) {
18,500✔
3294
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
18,524!
3295
                return false;
18,497✔
3296
            }
3297
        }
3298
        this.current = endIndex;
3✔
3299
        return true;
3✔
3300
    }
3301

3302
    /**
3303
     * Get next token matching a specified list, or fail with an error
3304
     */
3305
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
3306
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
14,804✔
3307
        if (token) {
14,804✔
3308
            return token;
14,793✔
3309
        } else {
3310
            let error = new Error(diagnosticInfo.message);
11✔
3311
            (error as any).isDiagnostic = true;
11✔
3312
            throw error;
11✔
3313
        }
3314
    }
3315

3316
    /**
3317
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3318
     */
3319
    private consumeTokenIf(tokenKind: TokenKind) {
3320
        if (this.match(tokenKind)) {
3,525✔
3321
            return this.previous();
396✔
3322
        }
3323
    }
3324

3325
    private consumeToken(tokenKind: TokenKind) {
3326
        return this.consume(
1,954✔
3327
            DiagnosticMessages.expectedToken(tokenKind),
3328
            tokenKind
3329
        );
3330
    }
3331

3332
    /**
3333
     * Consume, or add a message if not found. But then continue and return undefined
3334
     */
3335
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3336
        const nextKind = this.peek().kind;
22,546✔
3337
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
47,987✔
3338

3339
        if (foundTokenKind) {
22,546✔
3340
            return this.advance();
22,437✔
3341
        }
3342
        this.diagnostics.push({
109✔
3343
            ...diagnostic,
3344
            location: this.peek()?.location
327!
3345
        });
3346
    }
3347

3348
    private tryConsumeToken(tokenKind: TokenKind) {
3349
        return this.tryConsume(
80✔
3350
            DiagnosticMessages.expectedToken(tokenKind),
3351
            tokenKind
3352
        );
3353
    }
3354

3355
    private consumeStatementSeparators(optional = false) {
10,029✔
3356
        //a comment or EOF mark the end of the statement
3357
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
30,654✔
3358
            return true;
655✔
3359
        }
3360
        let consumed = false;
29,999✔
3361
        //consume any newlines and colons
3362
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
29,999✔
3363
            consumed = true;
32,442✔
3364
        }
3365
        if (!optional && !consumed) {
29,999✔
3366
            this.diagnostics.push({
74✔
3367
                ...DiagnosticMessages.expectedNewlineOrColon(),
3368
                location: this.peek()?.location
222!
3369
            });
3370
        }
3371
        return consumed;
29,999✔
3372
    }
3373

3374
    private advance(): Token {
3375
        if (!this.isAtEnd()) {
51,731✔
3376
            this.current++;
51,717✔
3377
        }
3378
        return this.previous();
51,731✔
3379
    }
3380

3381
    private checkEndOfStatement(): boolean {
3382
        const nextKind = this.peek().kind;
7,156✔
3383
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
7,156✔
3384
    }
3385

3386
    private checkPrevious(tokenKind: TokenKind): boolean {
3387
        return this.previous()?.kind === tokenKind;
227!
3388
    }
3389

3390
    /**
3391
     * Check that the next token kind is the expected kind
3392
     * @param tokenKind the expected next kind
3393
     * @returns true if the next tokenKind is the expected value
3394
     */
3395
    private check(tokenKind: TokenKind): boolean {
3396
        const nextKind = this.peek().kind;
964,935✔
3397
        if (nextKind === TokenKind.Eof) {
964,935✔
3398
            return false;
12,279✔
3399
        }
3400
        return nextKind === tokenKind;
952,656✔
3401
    }
3402

3403
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3404
        const nextKind = this.peek().kind;
163,536✔
3405
        if (nextKind === TokenKind.Eof) {
163,536✔
3406
            return false;
1,280✔
3407
        }
3408
        return tokenKinds.includes(nextKind);
162,256✔
3409
    }
3410

3411
    private checkNext(tokenKind: TokenKind): boolean {
3412
        if (this.isAtEnd()) {
14,009!
UNCOV
3413
            return false;
×
3414
        }
3415
        return this.peekNext().kind === tokenKind;
14,009✔
3416
    }
3417

3418
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3419
        if (this.isAtEnd()) {
6,201!
UNCOV
3420
            return false;
×
3421
        }
3422
        const nextKind = this.peekNext().kind;
6,201✔
3423
        return tokenKinds.includes(nextKind);
6,201✔
3424
    }
3425

3426
    private isAtEnd(): boolean {
3427
        const peekToken = this.peek();
154,243✔
3428
        return !peekToken || peekToken.kind === TokenKind.Eof;
154,243✔
3429
    }
3430

3431
    private peekNext(): Token {
3432
        if (this.isAtEnd()) {
20,210!
UNCOV
3433
            return this.peek();
×
3434
        }
3435
        return this.tokens[this.current + 1];
20,210✔
3436
    }
3437

3438
    private peek(): Token {
3439
        return this.tokens[this.current];
1,336,349✔
3440
    }
3441

3442
    private previous(): Token {
3443
        return this.tokens[this.current - 1];
88,725✔
3444
    }
3445

3446
    /**
3447
     * Sometimes we catch an error that is a diagnostic.
3448
     * If that's the case, we want to continue parsing.
3449
     * Otherwise, re-throw the error
3450
     *
3451
     * @param error error caught in a try/catch
3452
     */
3453
    private rethrowNonDiagnosticError(error) {
3454
        if (!error.isDiagnostic) {
11!
UNCOV
3455
            throw error;
×
3456
        }
3457
    }
3458

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

3475
    private synchronize() {
3476
        this.advance(); // skip the erroneous token
88✔
3477

3478
        while (!this.isAtEnd()) {
88✔
3479
            if (this.ensureNewLineOrColon(true)) {
188✔
3480
                // end of statement reached
3481
                return;
61✔
3482
            }
3483

3484
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
127✔
3485
                case TokenKind.Namespace:
2!
3486
                case TokenKind.Class:
3487
                case TokenKind.Function:
3488
                case TokenKind.Sub:
3489
                case TokenKind.If:
3490
                case TokenKind.For:
3491
                case TokenKind.ForEach:
3492
                case TokenKind.While:
3493
                case TokenKind.Print:
3494
                case TokenKind.Return:
3495
                    // start parsing again from the next block starter or obvious
3496
                    // expression start
3497
                    return;
1✔
3498
            }
3499

3500
            this.advance();
126✔
3501
        }
3502
    }
3503

3504

3505
    public dispose() {
3506
    }
3507
}
3508

3509
export enum ParseMode {
1✔
3510
    BrightScript = 'BrightScript',
1✔
3511
    BrighterScript = 'BrighterScript'
1✔
3512
}
3513

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

3538

3539
class CancelStatementError extends Error {
3540
    constructor() {
3541
        super('CancelStatement');
2✔
3542
    }
3543
}
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

© 2025 Coveralls, Inc