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

rokucommunity / brighterscript / #12716

14 Jun 2024 08:20PM UTC coverage: 85.629% (-2.3%) from 87.936%
#12716

push

web-flow
Merge 94311dc0a into 42db50190

10808 of 13500 branches covered (80.06%)

Branch coverage included in aggregate %.

6557 of 7163 new or added lines in 96 files covered. (91.54%)

83 existing lines in 17 files now uncovered.

12270 of 13451 relevant lines covered (91.22%)

26529.43 hits per line

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

86.62
/src/parser/Parser.ts
1
import type { Token, Identifier } from '../lexer/Token';
2
import { isToken } from '../lexer/Token';
1✔
3
import type { BlockTerminator } from '../lexer/TokenKind';
4
import { Lexer } from '../lexer/Lexer';
1✔
5
import {
1✔
6
    AllowedLocalIdentifiers,
7
    AllowedTypeIdentifiers,
8
    DeclarableTypes,
9
    AllowedProperties,
10
    AssignmentOperators,
11
    BrighterScriptSourceLiterals,
12
    DisallowedFunctionIdentifiersText,
13
    DisallowedLocalIdentifiersText,
14
    TokenKind,
15
    BlockTerminators,
16
    ReservedWords,
17
    CompoundAssignmentOperators
18
} from '../lexer/TokenKind';
19
import type {
20
    PrintSeparatorSpace,
21
    PrintSeparatorTab
22
} from './Statement';
23
import {
1✔
24
    AliasStatement,
25
    AssignmentStatement,
26
    Block,
27
    Body,
28
    CatchStatement,
29
    ContinueStatement,
30
    ClassStatement,
31
    ConstStatement,
32
    ConditionalCompileStatement,
33
    DimStatement,
34
    DottedSetStatement,
35
    EndStatement,
36
    EnumMemberStatement,
37
    EnumStatement,
38
    ExitForStatement,
39
    ExitWhileStatement,
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 { Diagnostic, 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 { DeepWriteable } from '../interfaces';
108

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

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

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

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

132
    public get statements() {
133
        return this.ast.statements;
545✔
134
    }
135

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

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

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

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

158
    private globalTerminators = [] as TokenKind[][];
2,970✔
159

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

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

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

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

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

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

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

222
    private logger: Logger;
223

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

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

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

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

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

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

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

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

298
    private declaration(): Statement | AnnotationExpression | undefined {
299
        try {
12,006✔
300
            if (this.checkAny(TokenKind.HashConst)) {
12,006✔
301
                return this.conditionalCompileConstStatement();
20✔
302
            }
303
            if (this.checkAny(TokenKind.HashIf)) {
11,986✔
304
                return this.conditionalCompileStatement();
35✔
305
            }
306
            if (this.checkAny(TokenKind.HashError)) {
11,951✔
307
                return this.conditionalCompileErrorStatement();
9✔
308
            }
309

310
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
11,942✔
311
                return this.functionDeclaration(false);
2,786✔
312
            }
313

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

318
            if (this.checkAlias()) {
9,144✔
319
                return this.aliasStatement();
32✔
320
            }
321

322
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
9,112✔
323
                return this.constDeclaration();
146✔
324
            }
325

326
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
8,966✔
327
                return this.annotationExpression();
31✔
328
            }
329

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

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

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

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

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

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

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

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

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

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

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

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

475
        const parentAnnotations = this.enterAnnotationBlock();
145✔
476

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

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

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

507
                let decl: Statement;
508

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

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

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

540
            //ensure statement separator
541
            this.consumeStatementSeparators();
219✔
542
        }
543

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

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

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

566
        this.warnIfNotBrighterScriptMode('enum declarations');
157✔
567

568
        const parentAnnotations = this.enterAnnotationBlock();
157✔
569

570
        this.consumeStatementSeparators();
157✔
571

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

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

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

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

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

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

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

618
        this.exitAnnotationBlock(parentAnnotations);
156✔
619
        return result;
156✔
620
    }
621

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

628
        const parentAnnotations = this.enterAnnotationBlock();
652✔
629

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

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

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

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

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

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

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

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

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

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

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

698
                    decl = this.fieldDeclaration(accessModifier);
310✔
699

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

708
                }
709

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

719
            //ensure statement separator
720
            this.consumeStatementSeparators();
669✔
721
        }
722

723
        let endingKeyword = this.advance();
652✔
724
        if (endingKeyword.kind !== TokenKind.EndClass) {
652✔
725
            this.diagnostics.push({
4✔
726
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('class'),
727
                range: endingKeyword.location?.range
12!
728
            });
729
        }
730

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

740
        this.exitAnnotationBlock(parentAnnotations);
652✔
741
        return result;
652✔
742
    }
743

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

746
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
310✔
747

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

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

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

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

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

803
    /**
804
     * An array of CallExpression for the current function body
805
     */
806
    private callExpressions = [];
2,970✔
807

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

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

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

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

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

885
            if (this.check(TokenKind.As)) {
3,205✔
886
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
255✔
887
            }
888

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

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

900
            this.consumeStatementSeparators(true);
3,205✔
901

902

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1013
        return result;
1,355✔
1014
    }
1015

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

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

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

1031
        return result;
58✔
1032
    }
1033

1034
    private checkLibrary() {
1035
        let isLibraryToken = this.check(TokenKind.Library);
18,177✔
1036

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

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

1046
            //definitely not a library statement
1047
        } else {
1048
            return false;
18,165✔
1049
        }
1050
    }
1051

1052
    private checkAlias() {
1053
        let isAliasToken = this.check(TokenKind.Alias);
17,942✔
1054

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

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

1064
            //definitely not a alias statement
1065
        } else {
1066
            return false;
17,910✔
1067
        }
1068
    }
1069

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

1075
        if (this.check(TokenKind.Import)) {
9,021✔
1076
            return this.importStatement();
200✔
1077
        }
1078

1079
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
8,821✔
1080
            return this.typecastStatement();
23✔
1081
        }
1082

1083
        if (this.checkAlias()) {
8,798!
NEW
1084
            return this.aliasStatement();
×
1085
        }
1086

1087
        if (this.check(TokenKind.Stop)) {
8,798✔
1088
            return this.stopStatement();
15✔
1089
        }
1090

1091
        if (this.check(TokenKind.If)) {
8,783✔
1092
            return this.ifStatement();
963✔
1093
        }
1094

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

1100
        if (this.check(TokenKind.Throw)) {
7,794✔
1101
            return this.throwStatement();
10✔
1102
        }
1103

1104
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
7,784✔
1105
            return this.printStatement();
1,054✔
1106
        }
1107
        if (this.check(TokenKind.Dim)) {
6,730✔
1108
            return this.dimStatement();
40✔
1109
        }
1110

1111
        if (this.check(TokenKind.While)) {
6,690✔
1112
            return this.whileStatement();
22✔
1113
        }
1114

1115
        if (this.check(TokenKind.ExitWhile)) {
6,668✔
1116
            return this.exitWhile();
6✔
1117
        }
1118

1119
        if (this.check(TokenKind.For)) {
6,662✔
1120
            return this.forStatement();
30✔
1121
        }
1122

1123
        if (this.check(TokenKind.ForEach)) {
6,632✔
1124
            return this.forEachStatement();
32✔
1125
        }
1126

1127
        if (this.check(TokenKind.ExitFor)) {
6,600✔
1128
            return this.exitFor();
3✔
1129
        }
1130

1131
        if (this.check(TokenKind.End)) {
6,597✔
1132
            return this.endStatement();
7✔
1133
        }
1134

1135
        if (this.match(TokenKind.Return)) {
6,590✔
1136
            return this.returnStatement();
2,751✔
1137
        }
1138

1139
        if (this.check(TokenKind.Goto)) {
3,839✔
1140
            return this.gotoStatement();
11✔
1141
        }
1142

1143
        //the continue keyword (followed by `for`, `while`, or a statement separator)
1144
        if (this.check(TokenKind.Continue) && this.checkAnyNext(TokenKind.While, TokenKind.For, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
3,828✔
1145
            return this.continueStatement();
11✔
1146
        }
1147

1148
        //does this line look like a label? (i.e.  `someIdentifier:` )
1149
        if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
3,817✔
1150
            try {
11✔
1151
                return this.labelStatement();
11✔
1152
            } catch (err) {
1153
                if (!(err instanceof CancelStatementError)) {
2!
1154
                    throw err;
×
1155
                }
1156
                //not a label, try something else
1157
            }
1158
        }
1159

1160
        // BrightScript is like python, in that variables can be declared without a `var`,
1161
        // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
1162
        // out what to do with it.
1163
        if (
3,808✔
1164
            this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)
1165
        ) {
1166
            if (this.checkAnyNext(...AssignmentOperators)) {
3,588✔
1167
                if (this.checkAnyNext(...CompoundAssignmentOperators)) {
1,366✔
1168
                    return this.augmentedAssignment();
58✔
1169
                }
1170
                return this.assignment();
1,308✔
1171
            } else if (this.checkNext(TokenKind.As)) {
2,222✔
1172
                // may be a typed assignment
1173
                const backtrack = this.current;
9✔
1174
                let validTypeExpression = false;
9✔
1175

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

1193
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1194
        if (this.check(TokenKind.Interface)) {
2,434✔
1195
            return this.interfaceDeclaration();
145✔
1196
        }
1197

1198
        if (this.check(TokenKind.Class)) {
2,289✔
1199
            return this.classDeclaration();
652✔
1200
        }
1201

1202
        if (this.check(TokenKind.Namespace)) {
1,637✔
1203
            return this.namespaceStatement();
576✔
1204
        }
1205

1206
        if (this.check(TokenKind.Enum)) {
1,061✔
1207
            return this.enumDeclaration();
157✔
1208
        }
1209

1210
        // TODO: support multi-statements
1211
        return this.setStatement();
904✔
1212
    }
1213

1214
    private whileStatement(): WhileStatement {
1215
        const whileKeyword = this.advance();
22✔
1216
        const condition = this.expression();
22✔
1217

1218
        this.consumeStatementSeparators();
21✔
1219

1220
        const whileBlock = this.block(TokenKind.EndWhile);
21✔
1221
        let endWhile: Token;
1222
        if (!whileBlock || this.peek().kind !== TokenKind.EndWhile) {
21✔
1223
            this.diagnostics.push({
1✔
1224
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('while'),
1225
                range: this.peek().location?.range
3!
1226
            });
1227
            if (!whileBlock) {
1!
1228
                throw this.lastDiagnosticAsError();
×
1229
            }
1230
        } else {
1231
            endWhile = this.advance();
20✔
1232
        }
1233

1234
        return new WhileStatement({
21✔
1235
            while: whileKeyword,
1236
            endWhile: endWhile,
1237
            condition: condition,
1238
            body: whileBlock
1239
        });
1240
    }
1241

1242
    private exitWhile(): ExitWhileStatement {
1243
        let keyword = this.advance();
6✔
1244

1245
        return new ExitWhileStatement({ exitWhile: keyword });
6✔
1246
    }
1247

1248
    private forStatement(): ForStatement {
1249
        const forToken = this.advance();
30✔
1250
        const initializer = this.assignment();
30✔
1251

1252
        //TODO: newline allowed?
1253

1254
        const toToken = this.advance();
29✔
1255
        const finalValue = this.expression();
29✔
1256
        let incrementExpression: Expression | undefined;
1257
        let stepToken: Token | undefined;
1258

1259
        if (this.check(TokenKind.Step)) {
29✔
1260
            stepToken = this.advance();
8✔
1261
            incrementExpression = this.expression();
8✔
1262
        } else {
1263
            // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
1264
        }
1265

1266
        this.consumeStatementSeparators();
29✔
1267

1268
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
29✔
1269
        let endForToken: Token;
1270
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
29✔
1271
            this.diagnostics.push({
1✔
1272
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1273
                range: this.peek().location?.range
3!
1274
            });
1275
            if (!body) {
1!
1276
                throw this.lastDiagnosticAsError();
×
1277
            }
1278
        } else {
1279
            endForToken = this.advance();
28✔
1280
        }
1281

1282
        // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
1283
        // stays around in scope with whatever value it was when the loop exited.
1284
        return new ForStatement({
29✔
1285
            for: forToken,
1286
            counterDeclaration: initializer,
1287
            to: toToken,
1288
            finalValue: finalValue,
1289
            body: body,
1290
            endFor: endForToken,
1291
            step: stepToken,
1292
            increment: incrementExpression
1293
        });
1294
    }
1295

1296
    private forEachStatement(): ForEachStatement {
1297
        let forEach = this.advance();
32✔
1298
        let name = this.advance();
32✔
1299

1300
        let maybeIn = this.peek();
32✔
1301
        if (this.check(TokenKind.Identifier) && maybeIn.text.toLowerCase() === 'in') {
32!
1302
            this.advance();
32✔
1303
        } else {
1304
            this.diagnostics.push({
×
1305
                ...DiagnosticMessages.expectedInAfterForEach(name.text),
1306
                range: this.peek().location?.range
×
1307
            });
1308
            throw this.lastDiagnosticAsError();
×
1309
        }
1310
        maybeIn.kind = TokenKind.In;
32✔
1311

1312
        let target = this.expression();
32✔
1313
        if (!target) {
32!
1314
            this.diagnostics.push({
×
1315
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1316
                range: this.peek().location?.range
×
1317
            });
1318
            throw this.lastDiagnosticAsError();
×
1319
        }
1320

1321
        this.consumeStatementSeparators();
32✔
1322

1323
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
32✔
1324
        if (!body) {
32!
1325
            this.diagnostics.push({
×
1326
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1327
                range: this.peek().location?.range
×
1328
            });
1329
            throw this.lastDiagnosticAsError();
×
1330
        }
1331

1332
        let endFor = this.advance();
32✔
1333

1334
        return new ForEachStatement({
32✔
1335
            forEach: forEach,
1336
            in: maybeIn,
1337
            endFor: endFor,
1338
            item: name,
1339
            target: target,
1340
            body: body
1341
        });
1342
    }
1343

1344
    private exitFor(): ExitForStatement {
1345
        let keyword = this.advance();
3✔
1346

1347
        return new ExitForStatement({ exitFor: keyword });
3✔
1348
    }
1349

1350
    private namespaceStatement(): NamespaceStatement | undefined {
1351
        this.warnIfNotBrighterScriptMode('namespace');
576✔
1352
        let keyword = this.advance();
576✔
1353

1354
        this.namespaceAndFunctionDepth++;
576✔
1355

1356
        let name = this.identifyingExpression();
576✔
1357
        //set the current namespace name
1358

1359
        this.globalTerminators.push([TokenKind.EndNamespace]);
575✔
1360
        let body = this.body();
575✔
1361
        this.globalTerminators.pop();
575✔
1362

1363
        let endKeyword: Token;
1364
        if (this.check(TokenKind.EndNamespace)) {
575✔
1365
            endKeyword = this.advance();
573✔
1366
        } else {
1367
            //the `end namespace` keyword is missing. add a diagnostic, but keep parsing
1368
            this.diagnostics.push({
2✔
1369
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'),
1370
                range: keyword.location?.range
6!
1371
            });
1372
        }
1373

1374
        this.namespaceAndFunctionDepth--;
575✔
1375

1376
        let result = new NamespaceStatement({
575✔
1377
            namespace: keyword,
1378
            nameExpression: name,
1379
            body: body,
1380
            endNamespace: endKeyword
1381
        });
1382

1383
        //cache the range property so that plugins can't affect it
1384
        result.cacheLocation();
575✔
1385
        result.body.symbolTable.name += `: namespace '${result.name}'`;
575✔
1386
        return result;
575✔
1387
    }
1388

1389
    /**
1390
     * Get an expression with identifiers separated by periods. Useful for namespaces and class extends
1391
     */
1392
    private identifyingExpression(allowedTokenKinds?: TokenKind[]): DottedGetExpression | VariableExpression {
1393
        allowedTokenKinds = allowedTokenKinds ?? this.allowedLocalIdentifiers;
1,209✔
1394
        let firstIdentifier = this.consume(
1,209✔
1395
            DiagnosticMessages.expectedIdentifierAfterKeyword(this.previous().text),
1396
            TokenKind.Identifier,
1397
            ...allowedTokenKinds
1398
        ) as Identifier;
1399

1400
        let expr: DottedGetExpression | VariableExpression;
1401

1402
        if (firstIdentifier) {
1,208!
1403
            // force it into an identifier so the AST makes some sense
1404
            firstIdentifier.kind = TokenKind.Identifier;
1,208✔
1405
            const varExpr = new VariableExpression({ name: firstIdentifier });
1,208✔
1406
            expr = varExpr;
1,208✔
1407

1408
            //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
1409
            while (this.check(TokenKind.Dot)) {
1,208✔
1410
                let dot = this.tryConsume(
446✔
1411
                    DiagnosticMessages.unexpectedToken(this.peek().text),
1412
                    TokenKind.Dot
1413
                );
1414
                if (!dot) {
446!
1415
                    break;
×
1416
                }
1417
                let identifier = this.tryConsume(
446✔
1418
                    DiagnosticMessages.expectedIdentifier(),
1419
                    TokenKind.Identifier,
1420
                    ...allowedTokenKinds,
1421
                    ...AllowedProperties
1422
                ) as Identifier;
1423

1424
                if (!identifier) {
446✔
1425
                    break;
3✔
1426
                }
1427
                // force it into an identifier so the AST makes some sense
1428
                identifier.kind = TokenKind.Identifier;
443✔
1429
                expr = new DottedGetExpression({ obj: expr, name: identifier, dot: dot });
443✔
1430
            }
1431
        }
1432
        return expr;
1,208✔
1433
    }
1434
    /**
1435
     * Add an 'unexpected token' diagnostic for any token found between current and the first stopToken found.
1436
     */
1437
    private flagUntil(...stopTokens: TokenKind[]) {
1438
        while (!this.checkAny(...stopTokens) && !this.isAtEnd()) {
4!
1439
            let token = this.advance();
×
1440
            this.diagnostics.push({
×
1441
                ...DiagnosticMessages.unexpectedToken(token.text),
1442
                range: token.location?.range
×
1443
            });
1444
        }
1445
    }
1446

1447
    /**
1448
     * Consume tokens until one of the `stopTokenKinds` is encountered
1449
     * @param stopTokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
1450
     * @returns - the list of tokens consumed, EXCLUDING the `stopTokenKind` (you can use `this.peek()` to see which one it was)
1451
     */
1452
    private consumeUntil(...stopTokenKinds: TokenKind[]) {
1453
        let result = [] as Token[];
59✔
1454
        //take tokens until we encounter one of the stopTokenKinds
1455
        while (!stopTokenKinds.includes(this.peek().kind)) {
59✔
1456
            result.push(this.advance());
140✔
1457
        }
1458
        return result;
59✔
1459
    }
1460

1461
    private constDeclaration(): ConstStatement | undefined {
1462
        this.warnIfNotBrighterScriptMode('const declaration');
146✔
1463
        const constToken = this.advance();
146✔
1464
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
146✔
1465
        const equalToken = this.consumeToken(TokenKind.Equal);
146✔
1466
        const expression = this.expression();
146✔
1467
        const statement = new ConstStatement({
146✔
1468
            const: constToken,
1469
            name: nameToken,
1470
            equals: equalToken,
1471
            value: expression
1472
        });
1473
        return statement;
146✔
1474
    }
1475

1476
    private libraryStatement(): LibraryStatement | undefined {
1477
        let libStatement = new LibraryStatement({
12✔
1478
            library: this.advance(),
1479
            //grab the next token only if it's a string
1480
            filePath: this.tryConsume(
1481
                DiagnosticMessages.expectedStringLiteralAfterKeyword('library'),
1482
                TokenKind.StringLiteral
1483
            )
1484
        });
1485

1486
        return libStatement;
12✔
1487
    }
1488

1489
    private importStatement() {
1490
        this.warnIfNotBrighterScriptMode('import statements');
200✔
1491
        let importStatement = new ImportStatement({
200✔
1492
            import: this.advance(),
1493
            //grab the next token only if it's a string
1494
            path: this.tryConsume(
1495
                DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
1496
                TokenKind.StringLiteral
1497
            )
1498
        });
1499

1500
        return importStatement;
200✔
1501
    }
1502

1503
    private typecastStatement() {
1504
        this.warnIfNotBrighterScriptMode('typecast statements');
23✔
1505
        const typecastToken = this.advance();
23✔
1506
        const typecastExpr = this.expression();
23✔
1507
        if (isTypecastExpression(typecastExpr)) {
23!
1508
            return new TypecastStatement({
23✔
1509
                typecast: typecastToken,
1510
                typecastExpression: typecastExpr
1511
            });
1512
        }
NEW
1513
        this.diagnostics.push({
×
1514
            ...DiagnosticMessages.expectedIdentifierAfterKeyword('typecast'),
1515
            range: util.createBoundingRange(typecastToken, this.peek())
1516
        });
NEW
1517
        throw this.lastDiagnosticAsError();
×
1518
    }
1519

1520
    private aliasStatement(): AliasStatement | undefined {
1521
        this.warnIfNotBrighterScriptMode('alias statements');
32✔
1522
        const aliasToken = this.advance();
32✔
1523
        const name = this.tryConsume(
32✔
1524
            DiagnosticMessages.expectedIdentifierAfterKeyword('alias'),
1525
            TokenKind.Identifier
1526
        );
1527
        const equals = this.tryConsume(
32✔
1528
            DiagnosticMessages.expectedToken(TokenKind.Equal),
1529
            TokenKind.Equal
1530
        );
1531
        let value = this.identifyingExpression();
32✔
1532

1533
        let aliasStmt = new AliasStatement({
32✔
1534
            alias: aliasToken,
1535
            name: name,
1536
            equals: equals,
1537
            value: value
1538

1539
        });
1540

1541
        return aliasStmt;
32✔
1542
    }
1543

1544
    private annotationExpression() {
1545
        const atToken = this.advance();
48✔
1546
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
48✔
1547
        if (identifier) {
48✔
1548
            identifier.kind = TokenKind.Identifier;
47✔
1549
        }
1550
        let annotation = new AnnotationExpression({ at: atToken, name: identifier });
48✔
1551
        this.pendingAnnotations.push(annotation);
47✔
1552

1553
        //optional arguments
1554
        if (this.check(TokenKind.LeftParen)) {
47✔
1555
            let leftParen = this.advance();
9✔
1556
            annotation.call = this.finishCall(leftParen, annotation, false);
9✔
1557
        }
1558
        return annotation;
47✔
1559
    }
1560

1561
    private ternaryExpression(test?: Expression): TernaryExpression {
1562
        this.warnIfNotBrighterScriptMode('ternary operator');
76✔
1563
        if (!test) {
76!
1564
            test = this.expression();
×
1565
        }
1566
        const questionMarkToken = this.advance();
76✔
1567

1568
        //consume newlines or comments
1569
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
76✔
1570
            this.advance();
7✔
1571
        }
1572

1573
        let consequent: Expression;
1574
        try {
76✔
1575
            consequent = this.expression();
76✔
1576
        } catch { }
1577

1578
        //consume newlines or comments
1579
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
76✔
1580
            this.advance();
5✔
1581
        }
1582

1583
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
76✔
1584

1585
        //consume newlines
1586
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
76✔
1587
            this.advance();
11✔
1588
        }
1589
        let alternate: Expression;
1590
        try {
76✔
1591
            alternate = this.expression();
76✔
1592
        } catch { }
1593

1594
        return new TernaryExpression({
76✔
1595
            test: test,
1596
            questionMark: questionMarkToken,
1597
            consequent: consequent,
1598
            colon: colonToken,
1599
            alternate: alternate
1600
        });
1601
    }
1602

1603
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1604
        this.warnIfNotBrighterScriptMode('null coalescing operator');
32✔
1605
        const questionQuestionToken = this.advance();
32✔
1606
        const alternate = this.expression();
32✔
1607
        return new NullCoalescingExpression({
32✔
1608
            consequent: test,
1609
            questionQuestion: questionQuestionToken,
1610
            alternate: alternate
1611
        });
1612
    }
1613

1614
    private regexLiteralExpression() {
1615
        this.warnIfNotBrighterScriptMode('regular expression literal');
44✔
1616
        return new RegexLiteralExpression({
44✔
1617
            regexLiteral: this.advance()
1618
        });
1619
    }
1620

1621
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1622
        this.warnIfNotBrighterScriptMode('template string');
39✔
1623

1624
        //get the tag name
1625
        let tagName: Identifier;
1626
        if (isTagged) {
39✔
1627
            tagName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties) as Identifier;
5✔
1628
            // force it into an identifier so the AST makes some sense
1629
            tagName.kind = TokenKind.Identifier;
5✔
1630
        }
1631

1632
        let quasis = [] as TemplateStringQuasiExpression[];
39✔
1633
        let expressions = [];
39✔
1634
        let openingBacktick = this.peek();
39✔
1635
        this.advance();
39✔
1636
        let currentQuasiExpressionParts = [];
39✔
1637
        while (!this.isAtEnd() && !this.check(TokenKind.BackTick)) {
39✔
1638
            let next = this.peek();
150✔
1639
            if (next.kind === TokenKind.TemplateStringQuasi) {
150✔
1640
                //a quasi can actually be made up of multiple quasis when it includes char literals
1641
                currentQuasiExpressionParts.push(
94✔
1642
                    new LiteralExpression({ value: next })
1643
                );
1644
                this.advance();
94✔
1645
            } else if (next.kind === TokenKind.EscapedCharCodeLiteral) {
56✔
1646
                currentQuasiExpressionParts.push(
24✔
1647
                    new EscapedCharCodeLiteralExpression({ value: next as Token & { charCode: number } })
1648
                );
1649
                this.advance();
24✔
1650
            } else {
1651
                //finish up the current quasi
1652
                quasis.push(
32✔
1653
                    new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1654
                );
1655
                currentQuasiExpressionParts = [];
32✔
1656

1657
                if (next.kind === TokenKind.TemplateStringExpressionBegin) {
32!
1658
                    this.advance();
32✔
1659
                }
1660
                //now keep this expression
1661
                expressions.push(this.expression());
32✔
1662
                if (!this.isAtEnd() && this.check(TokenKind.TemplateStringExpressionEnd)) {
32!
1663
                    //TODO is it an error if this is not present?
1664
                    this.advance();
32✔
1665
                } else {
1666
                    this.diagnostics.push({
×
1667
                        ...DiagnosticMessages.unterminatedTemplateExpression(),
1668
                        range: util.createBoundingRange(openingBacktick, this.peek())
1669
                    });
1670
                    throw this.lastDiagnosticAsError();
×
1671
                }
1672
            }
1673
        }
1674

1675
        //store the final set of quasis
1676
        quasis.push(
39✔
1677
            new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1678
        );
1679

1680
        if (this.isAtEnd()) {
39✔
1681
            //error - missing backtick
1682
            this.diagnostics.push({
2✔
1683
                ...DiagnosticMessages.unterminatedTemplateStringAtEndOfFile(),
1684
                range: util.createBoundingRange(openingBacktick, this.peek())
1685
            });
1686
            throw this.lastDiagnosticAsError();
2✔
1687

1688
        } else {
1689
            let closingBacktick = this.advance();
37✔
1690
            if (isTagged) {
37✔
1691
                return new TaggedTemplateStringExpression({
5✔
1692
                    tagName: tagName,
1693
                    openingBacktick: openingBacktick,
1694
                    quasis: quasis,
1695
                    expressions: expressions,
1696
                    closingBacktick: closingBacktick
1697
                });
1698
            } else {
1699
                return new TemplateStringExpression({
32✔
1700
                    openingBacktick: openingBacktick,
1701
                    quasis: quasis,
1702
                    expressions: expressions,
1703
                    closingBacktick: closingBacktick
1704
                });
1705
            }
1706
        }
1707
    }
1708

1709
    private tryCatchStatement(): TryCatchStatement {
1710
        const tryToken = this.advance();
26✔
1711
        let endTryToken: Token;
1712
        let catchStmt: CatchStatement;
1713
        //ensure statement separator
1714
        this.consumeStatementSeparators();
26✔
1715

1716
        let tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
26✔
1717

1718
        const peek = this.peek();
26✔
1719
        if (peek.kind !== TokenKind.Catch) {
26✔
1720
            this.diagnostics.push({
2✔
1721
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1722
                range: this.peek()?.location?.range
12!
1723
            });
1724
        } else {
1725
            const catchToken = this.advance();
24✔
1726
            const exceptionVarToken = this.tryConsume(DiagnosticMessages.missingExceptionVarToFollowCatch(), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
24✔
1727
            if (exceptionVarToken) {
24✔
1728
                // force it into an identifier so the AST makes some sense
1729
                exceptionVarToken.kind = TokenKind.Identifier;
22✔
1730
            }
1731
            //ensure statement sepatator
1732
            this.consumeStatementSeparators();
24✔
1733
            const catchBranch = this.block(TokenKind.EndTry);
24✔
1734
            catchStmt = new CatchStatement({
24✔
1735
                catch: catchToken,
1736
                exceptionVariable: exceptionVarToken,
1737
                catchBranch: catchBranch
1738
            });
1739
        }
1740
        if (this.peek().kind !== TokenKind.EndTry) {
26✔
1741
            this.diagnostics.push({
2✔
1742
                ...DiagnosticMessages.expectedEndTryToTerminateTryCatch(),
1743
                range: this.peek().location?.range
6!
1744
            });
1745
        } else {
1746
            endTryToken = this.advance();
24✔
1747
        }
1748

1749
        const statement = new TryCatchStatement({
26✔
1750
            try: tryToken,
1751
            tryBranch: tryBranch,
1752
            catchStatement: catchStmt,
1753
            endTry: endTryToken
1754
        }
1755
        );
1756
        return statement;
26✔
1757
    }
1758

1759
    private throwStatement() {
1760
        const throwToken = this.advance();
10✔
1761
        let expression: Expression;
1762
        if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
10✔
1763
            this.diagnostics.push({
2✔
1764
                ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(),
1765
                range: throwToken.location?.range
6!
1766
            });
1767
        } else {
1768
            expression = this.expression();
8✔
1769
        }
1770
        return new ThrowStatement({ throw: throwToken, expression: expression });
8✔
1771
    }
1772

1773
    private dimStatement() {
1774
        const dim = this.advance();
40✔
1775

1776
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
40✔
1777
        // force to an identifier so the AST makes some sense
1778
        if (identifier) {
40✔
1779
            identifier.kind = TokenKind.Identifier;
38✔
1780
        }
1781

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

1784
        let expressions: Expression[] = [];
40✔
1785
        let expression: Expression;
1786
        do {
40✔
1787
            try {
76✔
1788
                expression = this.expression();
76✔
1789
                expressions.push(expression);
71✔
1790
                if (this.check(TokenKind.Comma)) {
71✔
1791
                    this.advance();
36✔
1792
                } else {
1793
                    // will also exit for right square braces
1794
                    break;
35✔
1795
                }
1796
            } catch (error) {
1797
            }
1798
        } while (expression);
1799

1800
        if (expressions.length === 0) {
40✔
1801
            this.diagnostics.push({
5✔
1802
                ...DiagnosticMessages.missingExpressionsInDimStatement(),
1803
                range: this.peek().location?.range
15!
1804
            });
1805
        }
1806
        let rightSquareBracket = this.tryConsume(DiagnosticMessages.missingRightSquareBracketAfterDimIdentifier(), TokenKind.RightSquareBracket);
40✔
1807
        return new DimStatement({
40✔
1808
            dim: dim,
1809
            name: identifier,
1810
            openingSquare: leftSquareBracket,
1811
            dimensions: expressions,
1812
            closingSquare: rightSquareBracket
1813
        });
1814
    }
1815

1816
    private nestedInlineConditionalCount = 0;
2,970✔
1817

1818
    private ifStatement(incrementNestedCount = true): IfStatement {
1,797✔
1819
        // colon before `if` is usually not allowed, unless it's after `then`
1820
        if (this.current > 0) {
1,807✔
1821
            const prev = this.previous();
1,802✔
1822
            if (prev.kind === TokenKind.Colon) {
1,802✔
1823
                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then && this.nestedInlineConditionalCount === 0) {
4✔
1824
                    this.diagnostics.push({
1✔
1825
                        ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
1826
                        range: prev.location?.range
3!
1827
                    });
1828
                }
1829
            }
1830
        }
1831

1832
        const ifToken = this.advance();
1,807✔
1833

1834
        const condition = this.expression();
1,807✔
1835
        let thenBranch: Block;
1836
        let elseBranch: IfStatement | Block | undefined;
1837

1838
        let thenToken: Token | undefined;
1839
        let endIfToken: Token | undefined;
1840
        let elseToken: Token | undefined;
1841

1842
        //optional `then`
1843
        if (this.check(TokenKind.Then)) {
1,805✔
1844
            thenToken = this.advance();
1,437✔
1845
        }
1846

1847
        //is it inline or multi-line if?
1848
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
1,805✔
1849

1850
        if (isInlineIfThen) {
1,805✔
1851
            /*** PARSE INLINE IF STATEMENT ***/
1852
            if (!incrementNestedCount) {
48✔
1853
                this.nestedInlineConditionalCount++;
5✔
1854
            }
1855

1856
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
48✔
1857

1858
            if (!thenBranch) {
48!
1859
                this.diagnostics.push({
×
1860
                    ...DiagnosticMessages.expectedStatementToFollowConditionalCondition(ifToken.text),
1861
                    range: this.peek().location?.range
×
1862
                });
1863
                throw this.lastDiagnosticAsError();
×
1864
            } else {
1865
                this.ensureInline(thenBranch.statements);
48✔
1866
            }
1867

1868
            //else branch
1869
            if (this.check(TokenKind.Else)) {
48✔
1870
                elseToken = this.advance();
33✔
1871

1872
                if (this.check(TokenKind.If)) {
33✔
1873
                    // recurse-read `else if`
1874
                    elseBranch = this.ifStatement(false);
10✔
1875

1876
                    //no multi-line if chained with an inline if
1877
                    if (!elseBranch.isInline) {
9✔
1878
                        this.diagnostics.push({
4✔
1879
                            ...DiagnosticMessages.expectedInlineIfStatement(),
1880
                            range: elseBranch.location?.range
12!
1881
                        });
1882
                    }
1883

1884
                } else if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
23✔
1885
                    //expecting inline else branch
1886
                    this.diagnostics.push({
3✔
1887
                        ...DiagnosticMessages.expectedInlineIfStatement(),
1888
                        range: this.peek().location?.range
9!
1889
                    });
1890
                    throw this.lastDiagnosticAsError();
3✔
1891
                } else {
1892
                    elseBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
20✔
1893

1894
                    if (elseBranch) {
20!
1895
                        this.ensureInline(elseBranch.statements);
20✔
1896
                    }
1897
                }
1898

1899
                if (!elseBranch) {
29!
1900
                    //missing `else` branch
1901
                    this.diagnostics.push({
×
1902
                        ...DiagnosticMessages.expectedStatementToFollowElse(),
1903
                        range: this.peek().location?.range
×
1904
                    });
1905
                    throw this.lastDiagnosticAsError();
×
1906
                }
1907
            }
1908

1909
            if (!elseBranch || !isIfStatement(elseBranch)) {
44✔
1910
                //enforce newline at the end of the inline if statement
1911
                const peek = this.peek();
35✔
1912
                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && peek.kind !== TokenKind.Else && !this.isAtEnd()) {
35✔
1913
                    //ignore last error if it was about a colon
1914
                    if (this.previous().kind === TokenKind.Colon) {
3!
1915
                        this.diagnostics.pop();
3✔
1916
                        this.current--;
3✔
1917
                    }
1918
                    //newline is required
1919
                    this.diagnostics.push({
3✔
1920
                        ...DiagnosticMessages.expectedFinalNewline(),
1921
                        range: this.peek().location?.range
9!
1922
                    });
1923
                }
1924
            }
1925
            this.nestedInlineConditionalCount--;
44✔
1926
        } else {
1927
            /*** PARSE MULTI-LINE IF STATEMENT ***/
1928

1929
            thenBranch = this.blockConditionalBranch(ifToken);
1,757✔
1930

1931
            //ensure newline/colon before next keyword
1932
            this.ensureNewLineOrColon();
1,754✔
1933

1934
            //else branch
1935
            if (this.check(TokenKind.Else)) {
1,754✔
1936
                elseToken = this.advance();
1,403✔
1937

1938
                if (this.check(TokenKind.If)) {
1,403✔
1939
                    // recurse-read `else if`
1940
                    elseBranch = this.ifStatement();
834✔
1941

1942
                } else {
1943
                    elseBranch = this.blockConditionalBranch(ifToken);
569✔
1944

1945
                    //ensure newline/colon before next keyword
1946
                    this.ensureNewLineOrColon();
569✔
1947
                }
1948
            }
1949

1950
            if (!isIfStatement(elseBranch)) {
1,754✔
1951
                if (this.check(TokenKind.EndIf)) {
920✔
1952
                    endIfToken = this.advance();
917✔
1953

1954
                } else {
1955
                    //missing endif
1956
                    this.diagnostics.push({
3✔
1957
                        ...DiagnosticMessages.expectedEndIfToCloseIfStatement(ifToken.location?.range.start),
9!
1958
                        range: ifToken.location?.range
9!
1959
                    });
1960
                }
1961
            }
1962
        }
1963

1964
        return new IfStatement({
1,798✔
1965
            if: ifToken,
1966
            then: thenToken,
1967
            endIf: endIfToken,
1968
            else: elseToken,
1969
            condition: condition,
1970
            thenBranch: thenBranch,
1971
            elseBranch: elseBranch
1972
        });
1973
    }
1974

1975
    //consume a `then` or `else` branch block of an `if` statement
1976
    private blockConditionalBranch(ifToken: Token) {
1977
        //keep track of the current error count, because if the then branch fails,
1978
        //we will trash them in favor of a single error on if
1979
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
2,326✔
1980

1981
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
1982
        // a trailing "end if" or "else if"
1983
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
2,326✔
1984

1985
        if (!branch) {
2,326✔
1986
            //throw out any new diagnostics created as a result of a `then` block parse failure.
1987
            //the block() function will discard the current line, so any discarded diagnostics will
1988
            //resurface if they are legitimate, and not a result of a malformed if statement
1989
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
3✔
1990

1991
            //this whole if statement is bogus...add error to the if token and hard-fail
1992
            this.diagnostics.push({
3✔
1993
                ...DiagnosticMessages.expectedEndIfElseIfOrElseToTerminateThenBlock(),
1994
                range: ifToken.location?.range
9!
1995
            });
1996
            throw this.lastDiagnosticAsError();
3✔
1997
        }
1998
        return branch;
2,323✔
1999
    }
2000

2001
    private conditionalCompileStatement(): ConditionalCompileStatement {
2002
        const hashIfToken = this.advance();
50✔
2003
        let notToken: Token | undefined;
2004

2005
        if (this.check(TokenKind.Not)) {
50✔
2006
            notToken = this.advance();
7✔
2007
        }
2008

2009
        if (!this.checkAny(TokenKind.True, TokenKind.False, TokenKind.Identifier)) {
50✔
2010
            this.diagnostics.push({
1✔
2011
                ...DiagnosticMessages.invalidHashIfValue(),
2012
                range: this.peek()?.location?.range
6!
2013
            });
2014
        }
2015

2016

2017
        const condition = this.advance();
50✔
2018

2019
        let thenBranch: Block;
2020
        let elseBranch: ConditionalCompileStatement | Block | undefined;
2021

2022
        let hashEndIfToken: Token | undefined;
2023
        let hashElseToken: Token | undefined;
2024

2025
        //keep track of the current error count
2026
        //if this is `#if false` remove all diagnostics.
2027
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
50✔
2028

2029
        thenBranch = this.blockConditionalCompileBranch(hashIfToken);
50✔
2030
        const conditionTextLower = condition.text.toLowerCase();
49✔
2031
        if (!this.options.bsConsts?.get(conditionTextLower) || conditionTextLower === 'false') {
49!
2032
            //throw out any new diagnostics created as a result of a false block
2033
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
44✔
2034
        }
2035

2036
        this.ensureNewLine();
49✔
2037
        this.advance();
49✔
2038

2039
        //else branch
2040
        if (this.check(TokenKind.HashElseIf)) {
49✔
2041
            // recurse-read `#else if`
2042
            elseBranch = this.conditionalCompileStatement();
15✔
2043
            this.ensureNewLine();
15✔
2044

2045
        } else if (this.check(TokenKind.HashElse)) {
34✔
2046
            hashElseToken = this.advance();
9✔
2047
            let diagnosticsLengthBeforeBlock = this.diagnostics.length;
9✔
2048
            elseBranch = this.blockConditionalCompileBranch(hashIfToken);
9✔
2049

2050
            if (condition.text.toLowerCase() === 'true') {
9!
2051
                //throw out any new diagnostics created as a result of a false block
NEW
2052
                this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
×
2053
            }
2054
            this.ensureNewLine();
9✔
2055
            this.advance();
9✔
2056
        }
2057

2058
        if (!isConditionalCompileStatement(elseBranch)) {
49✔
2059

2060
            if (this.check(TokenKind.HashEndIf)) {
34!
2061
                hashEndIfToken = this.advance();
34✔
2062

2063
            } else {
2064
                //missing #endif
NEW
2065
                this.diagnostics.push({
×
2066
                    ...DiagnosticMessages.expectedHashEndIfToCloseHashIf(hashIfToken.location?.range.start.line),
×
2067
                    range: hashIfToken.location?.range
×
2068
                });
2069
            }
2070
        }
2071

2072
        return new ConditionalCompileStatement({
49✔
2073
            hashIf: hashIfToken,
2074
            hashElse: hashElseToken,
2075
            hashEndIf: hashEndIfToken,
2076
            not: notToken,
2077
            condition: condition,
2078
            thenBranch: thenBranch,
2079
            elseBranch: elseBranch
2080
        });
2081
    }
2082

2083
    //consume a conditional compile branch block of an `#if` statement
2084
    private blockConditionalCompileBranch(hashIfToken: Token) {
2085
        //keep track of the current error count, because if the then branch fails,
2086
        //we will trash them in favor of a single error on if
2087
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
59✔
2088

2089
        //parsing until trailing "#end if", "#else", "#else if"
2090
        let branch = this.conditionalCompileBlock();
59✔
2091

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

2098
            //this whole if statement is bogus...add error to the if token and hard-fail
NEW
2099
            this.diagnostics.push({
×
2100
                ...DiagnosticMessages.expectedTerminatorOnConditionalCompileBlock(),
2101
                range: hashIfToken.location?.range
×
2102
            });
UNCOV
2103
            throw this.lastDiagnosticAsError();
×
2104
        }
2105
        return branch;
58✔
2106
    }
2107

2108
    /**
2109
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2110
     * Always looks for `#end if` or `#else`
2111
     */
2112
    private conditionalCompileBlock(): Block | undefined {
2113
        const parentAnnotations = this.enterAnnotationBlock();
59✔
2114

2115
        this.consumeStatementSeparators(true);
59✔
2116
        const unsafeTerminators = BlockTerminators;
59✔
2117
        const conditionalEndTokens = [TokenKind.HashElse, TokenKind.HashElseIf, TokenKind.HashEndIf];
59✔
2118
        const terminators = [...conditionalEndTokens, ...unsafeTerminators];
59✔
2119
        this.globalTerminators.push(conditionalEndTokens);
59✔
2120
        const statements: Statement[] = [];
59✔
2121
        while (!this.isAtEnd() && !this.checkAny(...terminators)) {
59✔
2122
            //grab the location of the current token
2123
            let loopCurrent = this.current;
57✔
2124
            let dec = this.declaration();
57✔
2125
            if (dec) {
57✔
2126
                if (!isAnnotationExpression(dec)) {
56!
2127
                    this.consumePendingAnnotations(dec);
56✔
2128
                    statements.push(dec);
56✔
2129
                }
2130

2131
                const peekKind = this.peek().kind;
56✔
2132
                if (conditionalEndTokens.includes(peekKind)) {
56✔
2133
                    // current conditional compile branch was closed by other statement, rewind to preceding newline
2134
                    this.current--;
1✔
2135
                }
2136
                //ensure statement separator
2137
                this.consumeStatementSeparators();
56✔
2138

2139
            } else {
2140
                //something went wrong. reset to the top of the loop
2141
                this.current = loopCurrent;
1✔
2142

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

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

2150
                //consume potential separators
2151
                this.consumeStatementSeparators(true);
1✔
2152
            }
2153
        }
2154
        this.globalTerminators.pop();
59✔
2155

2156

2157
        if (this.isAtEnd()) {
59!
NEW
2158
            return undefined;
×
2159
            // TODO: Figure out how to handle unterminated blocks well
2160
        } else {
2161
            //did we  hit an unsafe terminator?
2162
            //if so, we need to restore the statement separator
2163
            let prev = this.previous();
59✔
2164
            let prevKind = prev.kind;
59✔
2165
            let peek = this.peek();
59✔
2166
            let peekKind = this.peek().kind;
59✔
2167
            if (
59✔
2168
                (peekKind === TokenKind.HashEndIf || peekKind === TokenKind.HashElse || peekKind === TokenKind.HashElseIf) &&
158✔
2169
                (prevKind === TokenKind.Newline)
2170
            ) {
2171
                this.current--;
58✔
2172
            } else if (unsafeTerminators.includes(peekKind) &&
1!
2173
                (prevKind === TokenKind.Newline || prevKind === TokenKind.Colon)
2174
            ) {
2175
                this.diagnostics.push({
1✔
2176
                    ...DiagnosticMessages.unsafeUnmatchedTerminatorInConditionalCompileBlock(peek.text),
2177
                    range: peek.location?.range
3!
2178
                });
2179
                throw this.lastDiagnosticAsError();
1✔
2180
            } else {
NEW
2181
                return undefined;
×
2182
            }
2183
        }
2184
        this.exitAnnotationBlock(parentAnnotations);
58✔
2185
        return new Block({ statements: statements });
58✔
2186
    }
2187

2188
    private conditionalCompileConstStatement() {
2189
        const hashConstToken = this.advance();
20✔
2190

2191
        const constName = this.peek();
20✔
2192
        //disallow using keywords for const names
2193
        if (ReservedWords.has(constName?.text.toLowerCase())) {
20!
2194
            this.diagnostics.push({
1✔
2195
                ...DiagnosticMessages.constNameCannotBeReservedWord(),
2196
                range: constName?.location?.range
6!
2197
            });
2198

2199
            this.lastDiagnosticAsError();
1✔
2200
            return;
1✔
2201
        }
2202
        const assignment = this.assignment();
19✔
2203
        if (assignment) {
17!
2204
            // check for something other than #const <name> = <otherName|true|false>
2205
            if (assignment.tokens.as || assignment.typeExpression) {
17!
NEW
2206
                this.diagnostics.push({
×
2207
                    ...DiagnosticMessages.unexpectedToken(assignment.tokens.as?.text || assignment.typeExpression?.getName(ParseMode.BrighterScript)),
×
2208
                    range: assignment.tokens.as?.location?.range ?? assignment.typeExpression?.location?.range
×
2209
                });
NEW
2210
                this.lastDiagnosticAsError();
×
2211
            }
2212

2213
            if (isVariableExpression(assignment.value) || isLiteralBoolean(assignment.value)) {
17✔
2214
                //value is an identifier or a boolean
2215
                //check for valid identifiers will happen in program validation
2216
            } else {
2217
                this.diagnostics.push({
2✔
2218
                    ...DiagnosticMessages.invalidHashConstValue(),
2219
                    range: assignment.value.location?.range
6!
2220
                });
2221
                this.lastDiagnosticAsError();
2✔
2222
            }
2223
        } else {
NEW
2224
            return undefined;
×
2225
        }
2226

2227
        if (!this.check(TokenKind.Newline)) {
17!
NEW
2228
            this.diagnostics.push({
×
2229
                ...DiagnosticMessages.expectedNewlineInConditionalCompile(),
2230
                range: this.peek().location?.range
×
2231
            });
NEW
2232
            throw this.lastDiagnosticAsError();
×
2233
        }
2234

2235
        return new ConditionalCompileConstStatement({ hashConst: hashConstToken, assignment: assignment });
17✔
2236
    }
2237

2238
    private conditionalCompileErrorStatement() {
2239
        const hashErrorToken = this.advance();
9✔
2240
        const tokensUntilEndOfLine = this.consumeUntil(TokenKind.Newline);
9✔
2241
        const message = createToken(TokenKind.HashErrorMessage, tokensUntilEndOfLine.map(t => t.text).join(' '));
9✔
2242
        return new ConditionalCompileErrorStatement({ hashError: hashErrorToken, message: message });
9✔
2243
    }
2244

2245
    private ensureNewLine() {
2246
        //ensure newline before next keyword
2247
        if (!this.check(TokenKind.Newline)) {
73!
NEW
2248
            this.diagnostics.push({
×
2249
                ...DiagnosticMessages.expectedNewlineInConditionalCompile(),
2250
                range: this.peek().location?.range
×
2251
            });
NEW
2252
            throw this.lastDiagnosticAsError();
×
2253
        }
2254
    }
2255

2256
    private ensureNewLineOrColon(silent = false) {
2,323✔
2257
        const prev = this.previous().kind;
2,503✔
2258
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
2,503✔
2259
            if (!silent) {
129✔
2260
                this.diagnostics.push({
6✔
2261
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2262
                    range: this.peek().location?.range
18!
2263
                });
2264
            }
2265
            return false;
129✔
2266
        }
2267
        return true;
2,374✔
2268
    }
2269

2270
    //ensure each statement of an inline block is single-line
2271
    private ensureInline(statements: Statement[]) {
2272
        for (const stat of statements) {
68✔
2273
            if (isIfStatement(stat) && !stat.isInline) {
86✔
2274
                this.diagnostics.push({
2✔
2275
                    ...DiagnosticMessages.expectedInlineIfStatement(),
2276
                    range: stat.location?.range
6!
2277
                });
2278
            }
2279
        }
2280
    }
2281

2282
    //consume inline branch of an `if` statement
2283
    private inlineConditionalBranch(...additionalTerminators: BlockTerminator[]): Block | undefined {
2284
        let statements = [];
86✔
2285
        //attempt to get the next statement without using `this.declaration`
2286
        //which seems a bit hackish to get to work properly
2287
        let statement = this.statement();
86✔
2288
        if (!statement) {
86!
2289
            return undefined;
×
2290
        }
2291
        statements.push(statement);
86✔
2292

2293
        //look for colon statement separator
2294
        let foundColon = false;
86✔
2295
        while (this.match(TokenKind.Colon)) {
86✔
2296
            foundColon = true;
23✔
2297
        }
2298

2299
        //if a colon was found, add the next statement or err if unexpected
2300
        if (foundColon) {
86✔
2301
            if (!this.checkAny(TokenKind.Newline, ...additionalTerminators)) {
23✔
2302
                //if not an ending keyword, add next statement
2303
                let extra = this.inlineConditionalBranch(...additionalTerminators);
18✔
2304
                if (!extra) {
18!
2305
                    return undefined;
×
2306
                }
2307
                statements.push(...extra.statements);
18✔
2308
            } else {
2309
                //error: colon before next keyword
2310
                const colon = this.previous();
5✔
2311
                this.diagnostics.push({
5✔
2312
                    ...DiagnosticMessages.unexpectedToken(colon.text),
2313
                    range: colon.location?.range
15!
2314
                });
2315
            }
2316
        }
2317
        return new Block({ statements: statements });
86✔
2318
    }
2319

2320
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2321
        let expressionStart = this.peek();
531✔
2322

2323
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
531✔
2324
            let operator = this.advance();
21✔
2325

2326
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
21✔
2327
                this.diagnostics.push({
1✔
2328
                    ...DiagnosticMessages.consecutiveIncrementDecrementOperatorsAreNotAllowed(),
2329
                    range: this.peek().location?.range
2!
2330
                });
2331
                throw this.lastDiagnosticAsError();
1✔
2332
            } else if (isCallExpression(expr)) {
20✔
2333
                this.diagnostics.push({
1✔
2334
                    ...DiagnosticMessages.incrementDecrementOperatorsAreNotAllowedAsResultOfFunctionCall(),
2335
                    range: expressionStart.location?.range
2!
2336
                });
2337
                throw this.lastDiagnosticAsError();
1✔
2338
            }
2339

2340
            const result = new IncrementStatement({ value: expr, operator: operator });
19✔
2341
            return result;
19✔
2342
        }
2343

2344
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
510✔
2345
            return new ExpressionStatement({ expression: expr });
431✔
2346
        }
2347

2348
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an inclosing ExpressionStatement
2349
        this.diagnostics.push({
79✔
2350
            ...DiagnosticMessages.expectedStatementOrFunctionCallButReceivedExpression(),
2351
            range: expressionStart.location?.range
235✔
2352
        });
2353
        return new ExpressionStatement({ expression: expr });
79✔
2354
    }
2355

2356
    private setStatement(): DottedSetStatement | IndexedSetStatement | ExpressionStatement | IncrementStatement | AssignmentStatement | AugmentedAssignmentStatement {
2357
        /**
2358
         * Attempts to find an expression-statement or an increment statement.
2359
         * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
2360
         * statements aren't valid expressions. They _do_ however fall under the same parsing
2361
         * priority as standalone function calls though, so we can parse them in the same way.
2362
         */
2363
        let expr = this.call();
904✔
2364
        if (this.check(TokenKind.Equal) && !(isCallExpression(expr))) {
857✔
2365
            let left = expr;
309✔
2366
            let operator = this.advance();
309✔
2367
            let right = this.expression();
309✔
2368

2369
            // Create a dotted or indexed "set" based on the left-hand side's type
2370
            if (isIndexedGetExpression(left)) {
309✔
2371
                return new IndexedSetStatement({
23✔
2372
                    obj: left.obj,
2373
                    indexes: left.indexes,
2374
                    value: right,
2375
                    openingSquare: left.tokens.openingSquare,
2376
                    closingSquare: left.tokens.closingSquare,
2377
                    equals: operator
2378
                });
2379
            } else if (isDottedGetExpression(left)) {
286✔
2380
                return new DottedSetStatement({
283✔
2381
                    obj: left.obj,
2382
                    name: left.tokens.name,
2383
                    value: right,
2384
                    dot: left.tokens.dot,
2385
                    equals: operator
2386
                });
2387
            }
2388
        } else if (this.checkAny(...CompoundAssignmentOperators) && !(isCallExpression(expr))) {
548✔
2389
            let left = expr;
20✔
2390
            let operator = this.advance();
20✔
2391
            let right = this.expression();
20✔
2392
            return new AugmentedAssignmentStatement({
20✔
2393
                item: left,
2394
                operator: operator,
2395
                value: right
2396
            });
2397
        }
2398
        return this.expressionStatement(expr);
531✔
2399
    }
2400

2401
    private printStatement(): PrintStatement {
2402
        let printKeyword = this.advance();
1,054✔
2403

2404
        let values: (
2405
            | Expression
2406
            | PrintSeparatorTab
2407
            | PrintSeparatorSpace)[] = [];
1,054✔
2408

2409
        while (!this.checkEndOfStatement()) {
1,054✔
2410
            if (this.check(TokenKind.Semicolon)) {
1,163✔
2411
                values.push(this.advance() as PrintSeparatorSpace);
29✔
2412
            } else if (this.check(TokenKind.Comma)) {
1,134✔
2413
                values.push(this.advance() as PrintSeparatorTab);
13✔
2414
            } else if (this.check(TokenKind.Else)) {
1,121✔
2415
                break; // inline branch
22✔
2416
            } else {
2417
                values.push(this.expression());
1,099✔
2418
            }
2419
        }
2420

2421
        //print statements can be empty, so look for empty print conditions
2422
        if (!values.length) {
1,051✔
2423
            const endOfStatementLocation = util.createBoundingLocation(printKeyword, this.peek());
12✔
2424
            let emptyStringLiteral = createStringLiteral('', endOfStatementLocation);
12✔
2425
            values.push(emptyStringLiteral);
12✔
2426
        }
2427

2428
        let last = values[values.length - 1];
1,051✔
2429
        if (isToken(last)) {
1,051✔
2430
            // TODO: error, expected value
2431
        }
2432

2433
        return new PrintStatement({ print: printKeyword, expressions: values });
1,051✔
2434
    }
2435

2436
    /**
2437
     * Parses a return statement with an optional return value.
2438
     * @returns an AST representation of a return statement.
2439
     */
2440
    private returnStatement(): ReturnStatement {
2441
        let options = { return: this.previous() };
2,751✔
2442

2443
        if (this.checkEndOfStatement()) {
2,751✔
2444
            return new ReturnStatement(options);
9✔
2445
        }
2446

2447
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
2,742✔
2448
        return new ReturnStatement({ ...options, value: toReturn });
2,741✔
2449
    }
2450

2451
    /**
2452
     * Parses a `label` statement
2453
     * @returns an AST representation of an `label` statement.
2454
     */
2455
    private labelStatement() {
2456
        let options = {
11✔
2457
            name: this.advance(),
2458
            colon: this.advance()
2459
        };
2460

2461
        //label must be alone on its line, this is probably not a label
2462
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
11✔
2463
            //rewind and cancel
2464
            this.current -= 2;
2✔
2465
            throw new CancelStatementError();
2✔
2466
        }
2467

2468
        return new LabelStatement(options);
9✔
2469
    }
2470

2471
    /**
2472
     * Parses a `continue` statement
2473
     */
2474
    private continueStatement() {
2475
        return new ContinueStatement({
11✔
2476
            continue: this.advance(),
2477
            loopType: this.tryConsume(
2478
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2479
                TokenKind.While, TokenKind.For
2480
            )
2481
        });
2482
    }
2483

2484
    /**
2485
     * Parses a `goto` statement
2486
     * @returns an AST representation of an `goto` statement.
2487
     */
2488
    private gotoStatement() {
2489
        let tokens = {
11✔
2490
            goto: this.advance(),
2491
            label: this.consume(
2492
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2493
                TokenKind.Identifier
2494
            )
2495
        };
2496

2497
        return new GotoStatement(tokens);
9✔
2498
    }
2499

2500
    /**
2501
     * Parses an `end` statement
2502
     * @returns an AST representation of an `end` statement.
2503
     */
2504
    private endStatement() {
2505
        let options = { end: this.advance() };
7✔
2506

2507
        return new EndStatement(options);
7✔
2508
    }
2509
    /**
2510
     * Parses a `stop` statement
2511
     * @returns an AST representation of a `stop` statement
2512
     */
2513
    private stopStatement() {
2514
        let options = { stop: this.advance() };
15✔
2515

2516
        return new StopStatement(options);
15✔
2517
    }
2518

2519
    /**
2520
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2521
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2522
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2523
     *                    ignored.
2524
     */
2525
    private block(...terminators: BlockTerminator[]): Block | undefined {
2526
        const parentAnnotations = this.enterAnnotationBlock();
5,663✔
2527

2528
        this.consumeStatementSeparators(true);
5,663✔
2529
        const statements: Statement[] = [];
5,663✔
2530
        const flatGlobalTerminators = this.globalTerminators.flat().flat();
5,663✔
2531
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators, ...flatGlobalTerminators)) {
5,663✔
2532
            //grab the location of the current token
2533
            let loopCurrent = this.current;
6,695✔
2534
            let dec = this.declaration();
6,695✔
2535
            if (dec) {
6,695✔
2536
                if (!isAnnotationExpression(dec)) {
6,646✔
2537
                    this.consumePendingAnnotations(dec);
6,642✔
2538
                    statements.push(dec);
6,642✔
2539
                }
2540

2541
                //ensure statement separator
2542
                this.consumeStatementSeparators();
6,646✔
2543

2544
            } else {
2545
                //something went wrong. reset to the top of the loop
2546
                this.current = loopCurrent;
49✔
2547

2548
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2549
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
49✔
2550

2551
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2552
                //but there's already an error in the file being parsed, so just leave this line here
2553
                this.advance();
49✔
2554

2555
                //consume potential separators
2556
                this.consumeStatementSeparators(true);
49✔
2557
            }
2558
        }
2559

2560
        if (this.isAtEnd()) {
5,663✔
2561
            return undefined;
6✔
2562
            // TODO: Figure out how to handle unterminated blocks well
2563
        } else if (terminators.length > 0) {
5,657✔
2564
            //did we hit end-sub / end-function while looking for some other terminator?
2565
            //if so, we need to restore the statement separator
2566
            let prev = this.previous().kind;
2,455✔
2567
            let peek = this.peek().kind;
2,455✔
2568
            if (
2,455✔
2569
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
4,914!
2570
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2571
            ) {
2572
                this.current--;
6✔
2573
            }
2574
        }
2575

2576
        this.exitAnnotationBlock(parentAnnotations);
5,657✔
2577
        return new Block({ statements: statements });
5,657✔
2578
    }
2579

2580
    /**
2581
     * Attach pending annotations to the provided statement,
2582
     * and then reset the annotations array
2583
     */
2584
    consumePendingAnnotations(statement: Statement) {
2585
        if (this.pendingAnnotations.length) {
13,073✔
2586
            statement.annotations = this.pendingAnnotations;
31✔
2587
            this.pendingAnnotations = [];
31✔
2588
        }
2589
    }
2590

2591
    enterAnnotationBlock() {
2592
        const pending = this.pendingAnnotations;
10,205✔
2593
        this.pendingAnnotations = [];
10,205✔
2594
        return pending;
10,205✔
2595
    }
2596

2597
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2598
        // non consumed annotations are an error
2599
        if (this.pendingAnnotations.length) {
10,197✔
2600
            for (const annotation of this.pendingAnnotations) {
4✔
2601
                this.diagnostics.push({
6✔
2602
                    ...DiagnosticMessages.unusedAnnotation(),
2603
                    range: annotation.location?.range
18!
2604
                });
2605
            }
2606
        }
2607
        this.pendingAnnotations = parentAnnotations;
10,197✔
2608
    }
2609

2610
    private expression(findTypecast = true): Expression {
10,472✔
2611
        let expression = this.anonymousFunction();
10,824✔
2612
        let asToken: Token;
2613
        let typeExpression: TypeExpression;
2614
        if (findTypecast) {
10,785✔
2615
            do {
10,433✔
2616
                if (this.check(TokenKind.As)) {
10,496✔
2617
                    this.warnIfNotBrighterScriptMode('type cast');
64✔
2618
                    // Check if this expression is wrapped in any type casts
2619
                    // allows for multiple casts:
2620
                    // myVal = foo() as dynamic as string
2621
                    [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
64✔
2622
                    if (asToken && typeExpression) {
64✔
2623
                        expression = new TypecastExpression({ obj: expression, as: asToken, typeExpression: typeExpression });
63✔
2624
                    }
2625
                } else {
2626
                    break;
10,432✔
2627
                }
2628

2629
            } while (asToken && typeExpression);
128✔
2630
        }
2631
        return expression;
10,785✔
2632
    }
2633

2634
    private anonymousFunction(): Expression {
2635
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
10,824✔
2636
            const func = this.functionDeclaration(true);
78✔
2637
            //if there's an open paren after this, this is an IIFE
2638
            if (this.check(TokenKind.LeftParen)) {
78✔
2639
                return this.finishCall(this.advance(), func);
3✔
2640
            } else {
2641
                return func;
75✔
2642
            }
2643
        }
2644

2645
        let expr = this.boolean();
10,746✔
2646

2647
        if (this.check(TokenKind.Question)) {
10,707✔
2648
            return this.ternaryExpression(expr);
76✔
2649
        } else if (this.check(TokenKind.QuestionQuestion)) {
10,631✔
2650
            return this.nullCoalescingExpression(expr);
32✔
2651
        } else {
2652
            return expr;
10,599✔
2653
        }
2654
    }
2655

2656
    private boolean(): Expression {
2657
        let expr = this.relational();
10,746✔
2658

2659
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
10,707✔
2660
            let operator = this.previous();
29✔
2661
            let right = this.relational();
29✔
2662
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
29✔
2663
        }
2664

2665
        return expr;
10,707✔
2666
    }
2667

2668
    private relational(): Expression {
2669
        let expr = this.additive();
10,800✔
2670

2671
        while (
10,761✔
2672
            this.matchAny(
2673
                TokenKind.Equal,
2674
                TokenKind.LessGreater,
2675
                TokenKind.Greater,
2676
                TokenKind.GreaterEqual,
2677
                TokenKind.Less,
2678
                TokenKind.LessEqual
2679
            )
2680
        ) {
2681
            let operator = this.previous();
1,495✔
2682
            let right = this.additive();
1,495✔
2683
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,495✔
2684
        }
2685

2686
        return expr;
10,761✔
2687
    }
2688

2689
    // TODO: bitshift
2690

2691
    private additive(): Expression {
2692
        let expr = this.multiplicative();
12,295✔
2693

2694
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
12,256✔
2695
            let operator = this.previous();
1,190✔
2696
            let right = this.multiplicative();
1,190✔
2697
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,190✔
2698
        }
2699

2700
        return expr;
12,256✔
2701
    }
2702

2703
    private multiplicative(): Expression {
2704
        let expr = this.exponential();
13,485✔
2705

2706
        while (this.matchAny(
13,446✔
2707
            TokenKind.Forwardslash,
2708
            TokenKind.Backslash,
2709
            TokenKind.Star,
2710
            TokenKind.Mod,
2711
            TokenKind.LeftShift,
2712
            TokenKind.RightShift
2713
        )) {
2714
            let operator = this.previous();
53✔
2715
            let right = this.exponential();
53✔
2716
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
53✔
2717
        }
2718

2719
        return expr;
13,446✔
2720
    }
2721

2722
    private exponential(): Expression {
2723
        let expr = this.prefixUnary();
13,538✔
2724

2725
        while (this.match(TokenKind.Caret)) {
13,499✔
2726
            let operator = this.previous();
8✔
2727
            let right = this.prefixUnary();
8✔
2728
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
8✔
2729
        }
2730

2731
        return expr;
13,499✔
2732
    }
2733

2734
    private prefixUnary(): Expression {
2735
        const nextKind = this.peek().kind;
13,582✔
2736
        if (nextKind === TokenKind.Not) {
13,582✔
2737
            this.current++; //advance
25✔
2738
            let operator = this.previous();
25✔
2739
            let right = this.relational();
25✔
2740
            return new UnaryExpression({ operator: operator, right: right });
25✔
2741
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
13,557✔
2742
            this.current++; //advance
36✔
2743
            let operator = this.previous();
36✔
2744
            let right = (nextKind as any) === TokenKind.Not
36✔
2745
                ? this.boolean()
36!
2746
                : this.prefixUnary();
2747
            return new UnaryExpression({ operator: operator, right: right });
36✔
2748
        }
2749
        return this.call();
13,521✔
2750
    }
2751

2752
    private indexedGet(expr: Expression) {
2753
        let openingSquare = this.previous();
142✔
2754
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
142✔
2755
        let indexes: Expression[] = [];
142✔
2756

2757

2758
        //consume leading newlines
2759
        while (this.match(TokenKind.Newline)) { }
142✔
2760

2761
        try {
142✔
2762
            indexes.push(
142✔
2763
                this.expression()
2764
            );
2765
            //consume additional indexes separated by commas
2766
            while (this.check(TokenKind.Comma)) {
140✔
2767
                //discard the comma
2768
                this.advance();
13✔
2769
                indexes.push(
13✔
2770
                    this.expression()
2771
                );
2772
            }
2773
        } catch (error) {
2774
            this.rethrowNonDiagnosticError(error);
2✔
2775
        }
2776
        //consume trailing newlines
2777
        while (this.match(TokenKind.Newline)) { }
142✔
2778

2779
        const closingSquare = this.tryConsume(
142✔
2780
            DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
2781
            TokenKind.RightSquareBracket
2782
        );
2783

2784
        return new IndexedGetExpression({
142✔
2785
            obj: expr,
2786
            indexes: indexes,
2787
            openingSquare: openingSquare,
2788
            closingSquare: closingSquare,
2789
            questionDot: questionDotToken
2790
        });
2791
    }
2792

2793
    private newExpression() {
2794
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
130✔
2795
        let newToken = this.advance();
130✔
2796

2797
        let nameExpr = this.identifyingExpression();
130✔
2798
        let leftParen = this.tryConsume(
130✔
2799
            DiagnosticMessages.unexpectedToken(this.peek().text),
2800
            TokenKind.LeftParen,
2801
            TokenKind.QuestionLeftParen
2802
        );
2803

2804
        if (!leftParen) {
130✔
2805
            // new expression without a following call expression
2806
            // wrap the name in an expression
2807
            const endOfStatementLocation = util.createBoundingLocation(newToken, this.peek());
4✔
2808
            const exprStmt = nameExpr ?? createStringLiteral('', endOfStatementLocation);
4!
2809
            return new ExpressionStatement({ expression: exprStmt });
4✔
2810
        }
2811

2812
        let call = this.finishCall(leftParen, nameExpr);
126✔
2813
        //pop the call from the  callExpressions list because this is technically something else
2814
        this.callExpressions.pop();
126✔
2815
        let result = new NewExpression({ new: newToken, call: call });
126✔
2816
        return result;
126✔
2817
    }
2818

2819
    /**
2820
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2821
     */
2822
    private callfunc(callee: Expression): Expression {
2823
        this.warnIfNotBrighterScriptMode('callfunc operator');
28✔
2824
        let operator = this.previous();
28✔
2825
        let methodName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
28✔
2826
        // force it into an identifier so the AST makes some sense
2827
        methodName.kind = TokenKind.Identifier;
25✔
2828
        let openParen = this.consume(DiagnosticMessages.expectedOpenParenToFollowCallfuncIdentifier(), TokenKind.LeftParen);
25✔
2829
        let call = this.finishCall(openParen, callee, false);
25✔
2830

2831
        return new CallfuncExpression({
25✔
2832
            callee: callee,
2833
            operator: operator,
2834
            methodName: methodName as Identifier,
2835
            openingParen: openParen,
2836
            args: call.args,
2837
            closingParen: call.tokens.closingParen
2838
        });
2839
    }
2840

2841
    private call(): Expression {
2842
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
14,425✔
2843
            return this.newExpression();
130✔
2844
        }
2845
        let expr = this.primary();
14,295✔
2846

2847
        while (true) {
14,212✔
2848
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
18,458✔
2849
                expr = this.finishCall(this.previous(), expr);
1,949✔
2850
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
16,509✔
2851
                expr = this.indexedGet(expr);
140✔
2852
            } else if (this.match(TokenKind.Callfunc)) {
16,369✔
2853
                expr = this.callfunc(expr);
28✔
2854
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
16,341✔
2855
                if (this.match(TokenKind.LeftSquareBracket)) {
2,171✔
2856
                    expr = this.indexedGet(expr);
2✔
2857
                } else {
2858
                    let dot = this.previous();
2,169✔
2859
                    let name = this.tryConsume(
2,169✔
2860
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2861
                        TokenKind.Identifier,
2862
                        ...AllowedProperties
2863
                    );
2864
                    if (!name) {
2,169✔
2865
                        break;
39✔
2866
                    }
2867

2868
                    // force it into an identifier so the AST makes some sense
2869
                    name.kind = TokenKind.Identifier;
2,130✔
2870
                    expr = new DottedGetExpression({ obj: expr, name: name as Identifier, dot: dot });
2,130✔
2871
                }
2872

2873
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
14,170✔
2874
                let dot = this.advance();
9✔
2875
                let name = this.tryConsume(
9✔
2876
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2877
                    TokenKind.Identifier,
2878
                    ...AllowedProperties
2879
                );
2880

2881
                // force it into an identifier so the AST makes some sense
2882
                name.kind = TokenKind.Identifier;
9✔
2883
                if (!name) {
9!
2884
                    break;
×
2885
                }
2886
                expr = new XmlAttributeGetExpression({ obj: expr, name: name as Identifier, at: dot });
9✔
2887
                //only allow a single `@` expression
2888
                break;
9✔
2889

2890
            } else {
2891
                break;
14,161✔
2892
            }
2893
        }
2894

2895
        return expr;
14,209✔
2896
    }
2897

2898
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
2,078✔
2899
        let args = [] as Expression[];
2,112✔
2900
        while (this.match(TokenKind.Newline)) { }
2,112✔
2901

2902
        if (!this.check(TokenKind.RightParen)) {
2,112✔
2903
            do {
1,082✔
2904
                while (this.match(TokenKind.Newline)) { }
1,547✔
2905

2906
                if (args.length >= CallExpression.MaximumArguments) {
1,547!
2907
                    this.diagnostics.push({
×
2908
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
2909
                        range: this.peek()?.location?.range
×
2910
                    });
2911
                    throw this.lastDiagnosticAsError();
×
2912
                }
2913
                try {
1,547✔
2914
                    args.push(this.expression());
1,547✔
2915
                } catch (error) {
2916
                    this.rethrowNonDiagnosticError(error);
5✔
2917
                    // we were unable to get an expression, so don't continue
2918
                    break;
5✔
2919
                }
2920
            } while (this.match(TokenKind.Comma));
2921
        }
2922

2923
        while (this.match(TokenKind.Newline)) { }
2,112✔
2924

2925
        const closingParen = this.tryConsume(
2,112✔
2926
            DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
2927
            TokenKind.RightParen
2928
        );
2929

2930
        let expression = new CallExpression({
2,112✔
2931
            callee: callee,
2932
            openingParen: openingParen,
2933
            args: args,
2934
            closingParen: closingParen
2935
        });
2936
        if (addToCallExpressionList) {
2,112✔
2937
            this.callExpressions.push(expression);
2,078✔
2938
        }
2939
        return expression;
2,112✔
2940
    }
2941

2942
    /**
2943
     * Creates a TypeExpression, which wraps standard ASTNodes that represent a BscType
2944
     */
2945
    private typeExpression(): TypeExpression {
2946
        const changedTokens: { token: Token; oldKind: TokenKind }[] = [];
1,404✔
2947
        try {
1,404✔
2948
            let expr: Expression = this.getTypeExpressionPart(changedTokens);
1,404✔
2949
            while (this.options.mode === ParseMode.BrighterScript && this.matchAny(TokenKind.Or)) {
1,404✔
2950
                // If we're in Brighterscript mode, allow union types with "or" between types
2951
                // TODO: Handle Union types in parens? eg. "(string or integer)"
2952
                let operator = this.previous();
32✔
2953
                let right = this.getTypeExpressionPart(changedTokens);
32✔
2954
                if (right) {
32!
2955
                    expr = new BinaryExpression({ left: expr, operator: operator, right: right });
32✔
2956
                } else {
NEW
2957
                    break;
×
2958
                }
2959
            }
2960
            if (expr) {
1,404!
2961
                return new TypeExpression({ expression: expr });
1,404✔
2962
            }
2963

2964
        } catch (error) {
2965
            // Something went wrong - reset the kind to what it was previously
NEW
2966
            for (const changedToken of changedTokens) {
×
NEW
2967
                changedToken.token.kind = changedToken.oldKind;
×
2968
            }
NEW
2969
            throw error;
×
2970
        }
2971
    }
2972

2973
    /**
2974
     * Gets a single "part" of a type of a potential Union type
2975
     * Note: this does not NEED to be part of a union type, but the logic is the same
2976
     *
2977
     * @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
2978
     * @returns an expression that was successfully parsed
2979
     */
2980
    private getTypeExpressionPart(changedTokens: { token: Token; oldKind: TokenKind }[]) {
2981
        let expr: VariableExpression | DottedGetExpression | TypedArrayExpression;
2982
        if (this.checkAny(...DeclarableTypes)) {
1,436✔
2983
            // if this is just a type, just use directly
2984
            expr = new VariableExpression({ name: this.advance() as Identifier });
965✔
2985
        } else {
2986
            if (this.checkAny(...AllowedTypeIdentifiers)) {
471✔
2987
                // Since the next token is allowed as a type identifier, change the kind
2988
                let nextToken = this.peek();
1✔
2989
                changedTokens.push({ token: nextToken, oldKind: nextToken.kind });
1✔
2990
                nextToken.kind = TokenKind.Identifier;
1✔
2991
            }
2992
            expr = this.identifyingExpression(AllowedTypeIdentifiers);
471✔
2993
        }
2994

2995
        //Check if it has square brackets, thus making it an array
2996
        if (expr && this.check(TokenKind.LeftSquareBracket)) {
1,436✔
2997
            if (this.options.mode === ParseMode.BrightScript) {
26✔
2998
                // typed arrays not allowed in Brightscript
2999
                this.warnIfNotBrighterScriptMode('typed arrays');
1✔
3000
                return expr;
1✔
3001
            }
3002

3003
            // Check if it is an array - that is, if it has `[]` after the type
3004
            // eg. `string[]` or `SomeKlass[]`
3005
            // This is while loop, so it supports multidimensional arrays (eg. integer[][])
3006
            while (this.check(TokenKind.LeftSquareBracket)) {
25✔
3007
                const leftBracket = this.advance();
27✔
3008
                if (this.check(TokenKind.RightSquareBracket)) {
27!
3009
                    const rightBracket = this.advance();
27✔
3010
                    expr = new TypedArrayExpression({ innerType: expr, leftBracket: leftBracket, rightBracket: rightBracket });
27✔
3011
                }
3012
            }
3013
        }
3014

3015
        return expr;
1,435✔
3016
    }
3017

3018
    private primary(): Expression {
3019
        switch (true) {
14,295✔
3020
            case this.matchAny(
14,295!
3021
                TokenKind.False,
3022
                TokenKind.True,
3023
                TokenKind.Invalid,
3024
                TokenKind.IntegerLiteral,
3025
                TokenKind.LongIntegerLiteral,
3026
                TokenKind.FloatLiteral,
3027
                TokenKind.DoubleLiteral,
3028
                TokenKind.StringLiteral
3029
            ):
3030
                return new LiteralExpression({ value: this.previous() });
6,509✔
3031

3032
            //capture source literals (LINE_NUM if brightscript, or a bunch of them if brighterscript)
3033
            case this.matchAny(TokenKind.LineNumLiteral, ...(this.options.mode === ParseMode.BrightScript ? [] : BrighterScriptSourceLiterals)):
7,786✔
3034
                return new SourceLiteralExpression({ value: this.previous() });
34✔
3035

3036
            //template string
3037
            case this.check(TokenKind.BackTick):
3038
                return this.templateString(false);
34✔
3039

3040
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
3041
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
14,885✔
3042
                return this.templateString(true);
5✔
3043

3044
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3045
                return new VariableExpression({ name: this.previous() as Identifier });
7,169✔
3046

3047
            case this.match(TokenKind.LeftParen):
3048
                let left = this.previous();
46✔
3049
                let expr = this.expression();
46✔
3050
                let right = this.consume(
45✔
3051
                    DiagnosticMessages.unmatchedLeftParenAfterExpression(),
3052
                    TokenKind.RightParen
3053
                );
3054
                return new GroupingExpression({ leftParen: left, rightParen: right, expression: expr });
45✔
3055

3056
            case this.matchAny(TokenKind.LeftSquareBracket):
3057
                return this.arrayLiteral();
127✔
3058

3059
            case this.match(TokenKind.LeftCurlyBrace):
3060
                return this.aaLiteral();
247✔
3061

3062
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
3063
                let token = Object.assign(this.previous(), {
×
3064
                    kind: TokenKind.Identifier
3065
                }) as Identifier;
NEW
3066
                return new VariableExpression({ name: token });
×
3067

3068
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
3069
                return this.anonymousFunction();
×
3070

3071
            case this.check(TokenKind.RegexLiteral):
3072
                return this.regexLiteralExpression();
44✔
3073

3074
            default:
3075
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
3076
                if (this.checkAny(...this.peekGlobalTerminators())) {
80!
3077
                    //don't throw a diagnostic, just return undefined
3078

3079
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
3080
                } else {
3081
                    this.diagnostics.push({
80✔
3082
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
3083
                        range: this.peek()?.location?.range
478!
3084
                    });
3085
                    throw this.lastDiagnosticAsError();
80✔
3086
                }
3087
        }
3088
    }
3089

3090
    private arrayLiteral() {
3091
        let elements: Array<Expression> = [];
127✔
3092
        let openingSquare = this.previous();
127✔
3093

3094
        while (this.match(TokenKind.Newline)) {
127✔
3095
        }
3096
        let closingSquare: Token;
3097

3098
        if (!this.match(TokenKind.RightSquareBracket)) {
127✔
3099
            try {
97✔
3100
                elements.push(this.expression());
97✔
3101

3102
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
96✔
3103

3104
                    while (this.match(TokenKind.Newline)) {
128✔
3105

3106
                    }
3107

3108
                    if (this.check(TokenKind.RightSquareBracket)) {
128✔
3109
                        break;
23✔
3110
                    }
3111

3112
                    elements.push(this.expression());
105✔
3113
                }
3114
            } catch (error: any) {
3115
                this.rethrowNonDiagnosticError(error);
2✔
3116
            }
3117

3118
            closingSquare = this.tryConsume(
97✔
3119
                DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral(),
3120
                TokenKind.RightSquareBracket
3121
            );
3122
        } else {
3123
            closingSquare = this.previous();
30✔
3124
        }
3125

3126
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3127
        return new ArrayLiteralExpression({ elements: elements, open: openingSquare, close: closingSquare });
127✔
3128
    }
3129

3130
    private aaLiteral() {
3131
        let openingBrace = this.previous();
247✔
3132
        let members: Array<AAMemberExpression> = [];
247✔
3133

3134
        let key = () => {
247✔
3135
            let result = {
263✔
3136
                colonToken: null as Token,
3137
                keyToken: null as Token,
3138
                range: null as Range
3139
            };
3140
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
263✔
3141
                result.keyToken = this.identifier(...AllowedProperties);
232✔
3142
            } else if (this.check(TokenKind.StringLiteral)) {
31!
3143
                result.keyToken = this.advance();
31✔
3144
            } else {
3145
                this.diagnostics.push({
×
3146
                    ...DiagnosticMessages.unexpectedAAKey(),
3147
                    range: this.peek().location?.range
×
3148
                });
3149
                throw this.lastDiagnosticAsError();
×
3150
            }
3151

3152
            result.colonToken = this.consume(
263✔
3153
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3154
                TokenKind.Colon
3155
            );
3156
            result.range = util.createBoundingRange(result.keyToken, result.colonToken);
262✔
3157
            return result;
262✔
3158
        };
3159

3160
        while (this.match(TokenKind.Newline)) { }
247✔
3161
        let closingBrace: Token;
3162
        if (!this.match(TokenKind.RightCurlyBrace)) {
247✔
3163
            let lastAAMember: AAMemberExpression;
3164
            try {
178✔
3165
                let k = key();
178✔
3166
                let expr = this.expression();
178✔
3167
                lastAAMember = new AAMemberExpression({
177✔
3168
                    key: k.keyToken,
3169
                    colon: k.colonToken,
3170
                    value: expr
3171
                });
3172
                members.push(lastAAMember);
177✔
3173

3174
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
177✔
3175
                    // collect comma at end of expression
3176
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
203✔
3177
                        (lastAAMember as DeepWriteable<AAMemberExpression>).tokens.comma = this.previous();
60✔
3178
                    }
3179

3180
                    this.consumeStatementSeparators(true);
203✔
3181

3182
                    if (this.check(TokenKind.RightCurlyBrace)) {
203✔
3183
                        break;
118✔
3184
                    }
3185
                    let k = key();
85✔
3186
                    let expr = this.expression();
84✔
3187
                    lastAAMember = new AAMemberExpression({
84✔
3188
                        key: k.keyToken,
3189
                        colon: k.colonToken,
3190
                        value: expr
3191
                    });
3192
                    members.push(lastAAMember);
84✔
3193

3194
                }
3195
            } catch (error: any) {
3196
                this.rethrowNonDiagnosticError(error);
2✔
3197
            }
3198

3199
            closingBrace = this.tryConsume(
178✔
3200
                DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral(),
3201
                TokenKind.RightCurlyBrace
3202
            );
3203
        } else {
3204
            closingBrace = this.previous();
69✔
3205
        }
3206

3207
        const aaExpr = new AALiteralExpression({ elements: members, open: openingBrace, close: closingBrace });
247✔
3208
        return aaExpr;
247✔
3209
    }
3210

3211
    /**
3212
     * Pop token if we encounter specified token
3213
     */
3214
    private match(tokenKind: TokenKind) {
3215
        if (this.check(tokenKind)) {
54,159✔
3216
            this.current++; //advance
5,428✔
3217
            return true;
5,428✔
3218
        }
3219
        return false;
48,731✔
3220
    }
3221

3222
    /**
3223
     * Pop token if we encounter a token in the specified list
3224
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3225
     */
3226
    private matchAny(...tokenKinds: TokenKind[]) {
3227
        for (let tokenKind of tokenKinds) {
188,849✔
3228
            if (this.check(tokenKind)) {
529,682✔
3229
                this.current++; //advance
50,126✔
3230
                return true;
50,126✔
3231
            }
3232
        }
3233
        return false;
138,723✔
3234
    }
3235

3236
    /**
3237
     * If the next series of tokens matches the given set of tokens, pop them all
3238
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
3239
     */
3240
    private matchSequence(...tokenKinds: TokenKind[]) {
3241
        const endIndex = this.current + tokenKinds.length;
16,372✔
3242
        for (let i = 0; i < tokenKinds.length; i++) {
16,372✔
3243
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
16,396!
3244
                return false;
16,369✔
3245
            }
3246
        }
3247
        this.current = endIndex;
3✔
3248
        return true;
3✔
3249
    }
3250

3251
    /**
3252
     * Get next token matching a specified list, or fail with an error
3253
     */
3254
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
3255
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
13,492✔
3256
        if (token) {
13,492✔
3257
            return token;
13,478✔
3258
        } else {
3259
            let error = new Error(diagnosticInfo.message);
14✔
3260
            (error as any).isDiagnostic = true;
14✔
3261
            throw error;
14✔
3262
        }
3263
    }
3264

3265
    /**
3266
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3267
     */
3268
    private consumeTokenIf(tokenKind: TokenKind) {
3269
        if (this.match(tokenKind)) {
3,169✔
3270
            return this.previous();
391✔
3271
        }
3272
    }
3273

3274
    private consumeToken(tokenKind: TokenKind) {
3275
        return this.consume(
1,794✔
3276
            DiagnosticMessages.expectedToken(tokenKind),
3277
            tokenKind
3278
        );
3279
    }
3280

3281
    /**
3282
     * Consume, or add a message if not found. But then continue and return undefined
3283
     */
3284
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3285
        const nextKind = this.peek().kind;
20,585✔
3286
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
45,593✔
3287

3288
        if (foundTokenKind) {
20,585✔
3289
            return this.advance();
20,477✔
3290
        }
3291
        this.diagnostics.push({
108✔
3292
            ...diagnostic,
3293
            range: this.peek()?.location?.range
647!
3294
        });
3295
    }
3296

3297
    private tryConsumeToken(tokenKind: TokenKind) {
3298
        return this.tryConsume(
76✔
3299
            DiagnosticMessages.expectedToken(tokenKind),
3300
            tokenKind
3301
        );
3302
    }
3303

3304
    private consumeStatementSeparators(optional = false) {
8,989✔
3305
        //a comment or EOF mark the end of the statement
3306
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
26,918✔
3307
            return true;
381✔
3308
        }
3309
        let consumed = false;
26,537✔
3310
        //consume any newlines and colons
3311
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
26,537✔
3312
            consumed = true;
28,900✔
3313
        }
3314
        if (!optional && !consumed) {
26,537✔
3315
            this.diagnostics.push({
64✔
3316
                ...DiagnosticMessages.expectedNewlineOrColon(),
3317
                range: this.peek()?.location?.range
381!
3318
            });
3319
        }
3320
        return consumed;
26,537✔
3321
    }
3322

3323
    private advance(): Token {
3324
        if (!this.isAtEnd()) {
46,690✔
3325
            this.current++;
46,676✔
3326
        }
3327
        return this.previous();
46,690✔
3328
    }
3329

3330
    private checkEndOfStatement(): boolean {
3331
        const nextKind = this.peek().kind;
6,356✔
3332
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
6,356✔
3333
    }
3334

3335
    private checkPrevious(tokenKind: TokenKind): boolean {
3336
        return this.previous()?.kind === tokenKind;
215!
3337
    }
3338

3339
    /**
3340
     * Check that the next token kind is the expected kind
3341
     * @param tokenKind the expected next kind
3342
     * @returns true if the next tokenKind is the expected value
3343
     */
3344
    private check(tokenKind: TokenKind): boolean {
3345
        const nextKind = this.peek().kind;
867,739✔
3346
        if (nextKind === TokenKind.Eof) {
867,739✔
3347
            return false;
9,495✔
3348
        }
3349
        return nextKind === tokenKind;
858,244✔
3350
    }
3351

3352
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3353
        const nextKind = this.peek().kind;
136,779✔
3354
        if (nextKind === TokenKind.Eof) {
136,779✔
3355
            return false;
225✔
3356
        }
3357
        return tokenKinds.includes(nextKind);
136,554✔
3358
    }
3359

3360
    private checkNext(tokenKind: TokenKind): boolean {
3361
        if (this.isAtEnd()) {
12,077!
3362
            return false;
×
3363
        }
3364
        return this.peekNext().kind === tokenKind;
12,077✔
3365
    }
3366

3367
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3368
        if (this.isAtEnd()) {
5,554!
3369
            return false;
×
3370
        }
3371
        const nextKind = this.peekNext().kind;
5,554✔
3372
        return tokenKinds.includes(nextKind);
5,554✔
3373
    }
3374

3375
    private isAtEnd(): boolean {
3376
        const peekToken = this.peek();
136,316✔
3377
        return !peekToken || peekToken.kind === TokenKind.Eof;
136,316✔
3378
    }
3379

3380
    private peekNext(): Token {
3381
        if (this.isAtEnd()) {
17,631!
3382
            return this.peek();
×
3383
        }
3384
        return this.tokens[this.current + 1];
17,631✔
3385
    }
3386

3387
    private peek(): Token {
3388
        return this.tokens[this.current];
1,189,034✔
3389
    }
3390

3391
    private previous(): Token {
3392
        return this.tokens[this.current - 1];
79,530✔
3393
    }
3394

3395
    /**
3396
     * Sometimes we catch an error that is a diagnostic.
3397
     * If that's the case, we want to continue parsing.
3398
     * Otherwise, re-throw the error
3399
     *
3400
     * @param error error caught in a try/catch
3401
     */
3402
    private rethrowNonDiagnosticError(error) {
3403
        if (!error.isDiagnostic) {
11!
3404
            throw error;
×
3405
        }
3406
    }
3407

3408
    /**
3409
     * Get the token that is {offset} indexes away from {this.current}
3410
     * @param offset the number of index steps away from current index to fetch
3411
     * @param tokenKinds the desired token must match one of these
3412
     * @example
3413
     * getToken(-1); //returns the previous token.
3414
     * getToken(0);  //returns current token.
3415
     * getToken(1);  //returns next token
3416
     */
3417
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3418
        const token = this.tokens[this.current + offset];
142✔
3419
        if (tokenKinds.includes(token.kind)) {
142✔
3420
            return token;
3✔
3421
        }
3422
    }
3423

3424
    private synchronize() {
3425
        this.advance(); // skip the erroneous token
82✔
3426

3427
        while (!this.isAtEnd()) {
82✔
3428
            if (this.ensureNewLineOrColon(true)) {
180✔
3429
                // end of statement reached
3430
                return;
57✔
3431
            }
3432

3433
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
123✔
3434
                case TokenKind.Namespace:
2!
3435
                case TokenKind.Class:
3436
                case TokenKind.Function:
3437
                case TokenKind.Sub:
3438
                case TokenKind.If:
3439
                case TokenKind.For:
3440
                case TokenKind.ForEach:
3441
                case TokenKind.While:
3442
                case TokenKind.Print:
3443
                case TokenKind.Return:
3444
                    // start parsing again from the next block starter or obvious
3445
                    // expression start
3446
                    return;
1✔
3447
            }
3448

3449
            this.advance();
122✔
3450
        }
3451
    }
3452

3453

3454
    public dispose() {
3455
    }
3456
}
3457

3458
export enum ParseMode {
1✔
3459
    BrightScript = 'BrightScript',
1✔
3460
    BrighterScript = 'BrighterScript'
1✔
3461
}
3462

3463
export interface ParseOptions {
3464
    /**
3465
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3466
     */
3467
    mode?: ParseMode;
3468
    /**
3469
     * A logger that should be used for logging. If omitted, a default logger is used
3470
     */
3471
    logger?: Logger;
3472
    /**
3473
     * Path to the file where this source code originated
3474
     */
3475
    srcPath?: string;
3476
    /**
3477
     * Should locations be tracked. If false, the `range` property will be omitted
3478
     * @default true
3479
     */
3480
    trackLocations?: boolean;
3481
    /**
3482
     *
3483
     */
3484
    bsConsts?: Map<string, boolean>;
3485
}
3486

3487

3488
class CancelStatementError extends Error {
3489
    constructor() {
3490
        super('CancelStatement');
2✔
3491
    }
3492
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc