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

rokucommunity / brighterscript / #13074

25 Sep 2024 04:16PM UTC coverage: 86.525% (-1.4%) from 87.933%
#13074

push

web-flow
Merge c610b9e4e into 56dcaaa63

10903 of 13389 branches covered (81.43%)

Branch coverage included in aggregate %.

6936 of 7533 new or added lines in 100 files covered. (92.07%)

83 existing lines in 18 files now uncovered.

12548 of 13714 relevant lines covered (91.5%)

27593.28 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

218
    private logger: Logger;
219

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

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

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

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

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

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

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

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

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

306
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
12,850✔
307
                return this.functionDeclaration(false);
2,948✔
308
            }
309

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

503
                let decl: Statement;
504

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

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

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

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

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

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

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

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

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

566
        this.consumeStatementSeparators();
159✔
567

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

704
                }
705

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

898

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1009
        return result;
1,406✔
1010
    }
1011

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

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

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

1027
        return result;
58✔
1028
    }
1029

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

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

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

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

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

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

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

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

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

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

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

1079
        if (this.checkAlias()) {
9,517!
NEW
1080
            return this.aliasStatement();
×
1081
        }
1082

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

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

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

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

1100
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
8,425✔
1101
            return this.printStatement();
1,080✔
1102
        }
1103
        if (this.check(TokenKind.Dim)) {
7,345✔
1104
            return this.dimStatement();
40✔
1105
        }
1106

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

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

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

1119
        if (this.check(TokenKind.ForEach)) {
7,223✔
1120
            return this.forEachStatement();
34✔
1121
        }
1122

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

1127
        if (this.match(TokenKind.Return)) {
7,182✔
1128
            return this.returnStatement();
3,009✔
1129
        }
1130

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

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

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

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

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

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

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

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

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

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

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

1210
        this.consumeStatementSeparators();
27✔
1211

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

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

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

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

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

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

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

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

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

1277
        //TODO: newline allowed?
1278

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

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

1291
        this.consumeStatementSeparators();
33✔
1292

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

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

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

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

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

1346
        this.consumeStatementSeparators();
34✔
1347

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

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

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

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

1373
        this.namespaceAndFunctionDepth++;
606✔
1374

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

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

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

1393
        this.namespaceAndFunctionDepth--;
605✔
1394

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

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

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

1419
        let expr: DottedGetExpression | VariableExpression;
1420

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

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

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

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

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

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

1505
        return libStatement;
12✔
1506
    }
1507

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

1519
        return importStatement;
202✔
1520
    }
1521

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

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

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

1561
        });
1562

1563
        return aliasStmt;
32✔
1564
    }
1565

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1844
    private nestedInlineConditionalCount = 0;
3,295✔
1845

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2044

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2184

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2436
        let values: (
2437
            | Expression
2438
            | PrintSeparatorTab
2439
            | PrintSeparatorSpace)[] = [];
1,080✔
2440

2441
        while (!this.checkEndOfStatement()) {
1,080✔
2442
            if (this.check(TokenKind.Semicolon)) {
1,189✔
2443
                values.push(this.advance() as PrintSeparatorSpace);
29✔
2444
            } else if (this.check(TokenKind.Comma)) {
1,160✔
2445
                values.push(this.advance() as PrintSeparatorTab);
13✔
2446
            } else if (this.check(TokenKind.Else)) {
1,147✔
2447
                break; // inline branch
22✔
2448
            } else {
2449
                values.push(this.expression());
1,125✔
2450
            }
2451
        }
2452

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2573
                //ensure statement separator
2574
                this.consumeStatementSeparators();
7,102✔
2575

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

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

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

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

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

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

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

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

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

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

2661
            } while (asToken && typeExpression);
128✔
2662
        }
2663
        return expression;
11,411✔
2664
    }
2665

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

2677
        let expr = this.boolean();
11,367✔
2678

2679
        if (this.check(TokenKind.Question)) {
11,328✔
2680
            return this.ternaryExpression(expr);
76✔
2681
        } else if (this.check(TokenKind.QuestionQuestion)) {
11,252✔
2682
            return this.nullCoalescingExpression(expr);
32✔
2683
        } else {
2684
            return expr;
11,220✔
2685
        }
2686
    }
2687

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

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

2697
        return expr;
11,328✔
2698
    }
2699

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

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

2718
        return expr;
11,382✔
2719
    }
2720

2721
    // TODO: bitshift
2722

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

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

2732
        return expr;
13,009✔
2733
    }
2734

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

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

2751
        return expr;
14,315✔
2752
    }
2753

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

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

2763
        return expr;
14,368✔
2764
    }
2765

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

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

2789

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

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

2811
        const closingSquare = this.tryConsume(
142✔
2812
            DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
2813
            TokenKind.RightSquareBracket
2814
        );
2815

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

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

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

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

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

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

2863
        return new CallfuncExpression({
25✔
2864
            callee: callee,
2865
            operator: operator,
2866
            methodName: methodName as Identifier,
2867
            openingParen: openParen,
2868
            args: call.args,
2869
            closingParen: call.tokens.closingParen
2870
        });
2871
    }
2872

2873
    private call(): Expression {
2874
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
15,536✔
2875
            return this.newExpression();
131✔
2876
        }
2877
        let expr = this.primary();
15,405✔
2878

2879
        while (true) {
15,322✔
2880
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
19,808✔
2881
                expr = this.finishCall(this.previous(), expr);
2,074✔
2882
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
17,734✔
2883
                expr = this.indexedGet(expr);
140✔
2884
            } else if (this.match(TokenKind.Callfunc)) {
17,594✔
2885
                expr = this.callfunc(expr);
28✔
2886
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
17,566✔
2887
                if (this.match(TokenKind.LeftSquareBracket)) {
2,286✔
2888
                    expr = this.indexedGet(expr);
2✔
2889
                } else {
2890
                    let dot = this.previous();
2,284✔
2891
                    let name = this.tryConsume(
2,284✔
2892
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2893
                        TokenKind.Identifier,
2894
                        ...AllowedProperties
2895
                    );
2896
                    if (!name) {
2,284✔
2897
                        break;
39✔
2898
                    }
2899

2900
                    // force it into an identifier so the AST makes some sense
2901
                    name.kind = TokenKind.Identifier;
2,245✔
2902
                    expr = new DottedGetExpression({ obj: expr, name: name as Identifier, dot: dot });
2,245✔
2903
                }
2904

2905
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
15,280✔
2906
                let dot = this.advance();
9✔
2907
                let name = this.tryConsume(
9✔
2908
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2909
                    TokenKind.Identifier,
2910
                    ...AllowedProperties
2911
                );
2912

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

2922
            } else {
2923
                break;
15,271✔
2924
            }
2925
        }
2926

2927
        return expr;
15,319✔
2928
    }
2929

2930
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
2,204✔
2931
        let args = [] as Expression[];
2,251✔
2932
        while (this.match(TokenKind.Newline)) { }
2,251✔
2933

2934
        if (!this.check(TokenKind.RightParen)) {
2,251✔
2935
            do {
1,148✔
2936
                while (this.match(TokenKind.Newline)) { }
1,639✔
2937

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

2955
        while (this.match(TokenKind.Newline)) { }
2,251✔
2956

2957
        const closingParen = this.tryConsume(
2,251✔
2958
            DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
2959
            TokenKind.RightParen
2960
        );
2961

2962
        let expression = new CallExpression({
2,251✔
2963
            callee: callee,
2964
            openingParen: openingParen,
2965
            args: args,
2966
            closingParen: closingParen
2967
        });
2968
        if (addToCallExpressionList) {
2,251✔
2969
            this.callExpressions.push(expression);
2,204✔
2970
        }
2971
        return expression;
2,251✔
2972
    }
2973

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

2996
        } catch (error) {
2997
            // Something went wrong - reset the kind to what it was previously
NEW
2998
            for (const changedToken of changedTokens) {
×
NEW
2999
                changedToken.token.kind = changedToken.oldKind;
×
3000
            }
NEW
3001
            throw error;
×
3002
        }
3003
    }
3004

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

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

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

3047
        return expr;
1,453✔
3048
    }
3049

3050
    private primary(): Expression {
3051
        switch (true) {
15,405✔
3052
            case this.matchAny(
15,405!
3053
                TokenKind.False,
3054
                TokenKind.True,
3055
                TokenKind.Invalid,
3056
                TokenKind.IntegerLiteral,
3057
                TokenKind.LongIntegerLiteral,
3058
                TokenKind.FloatLiteral,
3059
                TokenKind.DoubleLiteral,
3060
                TokenKind.StringLiteral
3061
            ):
3062
                return new LiteralExpression({ value: this.previous() });
6,861✔
3063

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

3068
            //template string
3069
            case this.check(TokenKind.BackTick):
3070
                return this.templateString(false);
36✔
3071

3072
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
3073
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
16,384✔
3074
                return this.templateString(true);
5✔
3075

3076
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3077
                return new VariableExpression({ name: this.previous() as Identifier });
7,912✔
3078

3079
            case this.match(TokenKind.LeftParen):
3080
                let left = this.previous();
46✔
3081
                let expr = this.expression();
46✔
3082
                let right = this.consume(
45✔
3083
                    DiagnosticMessages.unmatchedLeftParenAfterExpression(),
3084
                    TokenKind.RightParen
3085
                );
3086
                return new GroupingExpression({ leftParen: left, rightParen: right, expression: expr });
45✔
3087

3088
            case this.matchAny(TokenKind.LeftSquareBracket):
3089
                return this.arrayLiteral();
133✔
3090

3091
            case this.match(TokenKind.LeftCurlyBrace):
3092
                return this.aaLiteral();
254✔
3093

3094
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
3095
                let token = Object.assign(this.previous(), {
×
3096
                    kind: TokenKind.Identifier
3097
                }) as Identifier;
NEW
3098
                return new VariableExpression({ name: token });
×
3099

3100
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
3101
                return this.anonymousFunction();
×
3102

3103
            case this.check(TokenKind.RegexLiteral):
3104
                return this.regexLiteralExpression();
44✔
3105

3106
            default:
3107
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
3108
                if (this.checkAny(...this.peekGlobalTerminators())) {
80!
3109
                    //don't throw a diagnostic, just return undefined
3110

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

3122
    private arrayLiteral() {
3123
        let elements: Array<Expression> = [];
133✔
3124
        let openingSquare = this.previous();
133✔
3125

3126
        while (this.match(TokenKind.Newline)) {
133✔
3127
        }
3128
        let closingSquare: Token;
3129

3130
        if (!this.match(TokenKind.RightSquareBracket)) {
133✔
3131
            try {
98✔
3132
                elements.push(this.expression());
98✔
3133

3134
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
97✔
3135

3136
                    while (this.match(TokenKind.Newline)) {
130✔
3137

3138
                    }
3139

3140
                    if (this.check(TokenKind.RightSquareBracket)) {
130✔
3141
                        break;
23✔
3142
                    }
3143

3144
                    elements.push(this.expression());
107✔
3145
                }
3146
            } catch (error: any) {
3147
                this.rethrowNonDiagnosticError(error);
2✔
3148
            }
3149

3150
            closingSquare = this.tryConsume(
98✔
3151
                DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral(),
3152
                TokenKind.RightSquareBracket
3153
            );
3154
        } else {
3155
            closingSquare = this.previous();
35✔
3156
        }
3157

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

3162
    private aaLiteral() {
3163
        let openingBrace = this.previous();
254✔
3164
        let members: Array<AAMemberExpression> = [];
254✔
3165

3166
        let key = () => {
254✔
3167
            let result = {
268✔
3168
                colonToken: null as Token,
3169
                keyToken: null as Token,
3170
                range: null as Range
3171
            };
3172
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
268✔
3173
                result.keyToken = this.identifier(...AllowedProperties);
237✔
3174
            } else if (this.check(TokenKind.StringLiteral)) {
31!
3175
                result.keyToken = this.advance();
31✔
3176
            } else {
3177
                this.diagnostics.push({
×
3178
                    ...DiagnosticMessages.unexpectedAAKey(),
3179
                    location: this.peek().location
3180
                });
3181
                throw this.lastDiagnosticAsError();
×
3182
            }
3183

3184
            result.colonToken = this.consume(
268✔
3185
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3186
                TokenKind.Colon
3187
            );
3188
            result.range = util.createBoundingRange(result.keyToken, result.colonToken);
267✔
3189
            return result;
267✔
3190
        };
3191

3192
        while (this.match(TokenKind.Newline)) { }
254✔
3193
        let closingBrace: Token;
3194
        if (!this.match(TokenKind.RightCurlyBrace)) {
254✔
3195
            let lastAAMember: AAMemberExpression;
3196
            try {
183✔
3197
                let k = key();
183✔
3198
                let expr = this.expression();
183✔
3199
                lastAAMember = new AAMemberExpression({
182✔
3200
                    key: k.keyToken,
3201
                    colon: k.colonToken,
3202
                    value: expr
3203
                });
3204
                members.push(lastAAMember);
182✔
3205

3206
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
182✔
3207
                    // collect comma at end of expression
3208
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
204✔
3209
                        (lastAAMember as DeepWriteable<AAMemberExpression>).tokens.comma = this.previous();
60✔
3210
                    }
3211

3212
                    this.consumeStatementSeparators(true);
204✔
3213

3214
                    if (this.check(TokenKind.RightCurlyBrace)) {
204✔
3215
                        break;
119✔
3216
                    }
3217
                    let k = key();
85✔
3218
                    let expr = this.expression();
84✔
3219
                    lastAAMember = new AAMemberExpression({
84✔
3220
                        key: k.keyToken,
3221
                        colon: k.colonToken,
3222
                        value: expr
3223
                    });
3224
                    members.push(lastAAMember);
84✔
3225

3226
                }
3227
            } catch (error: any) {
3228
                this.rethrowNonDiagnosticError(error);
2✔
3229
            }
3230

3231
            closingBrace = this.tryConsume(
183✔
3232
                DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral(),
3233
                TokenKind.RightCurlyBrace
3234
            );
3235
        } else {
3236
            closingBrace = this.previous();
71✔
3237
        }
3238

3239
        const aaExpr = new AALiteralExpression({ elements: members, open: openingBrace, close: closingBrace });
254✔
3240
        return aaExpr;
254✔
3241
    }
3242

3243
    /**
3244
     * Pop token if we encounter specified token
3245
     */
3246
    private match(tokenKind: TokenKind) {
3247
        if (this.check(tokenKind)) {
57,862✔
3248
            this.current++; //advance
5,812✔
3249
            return true;
5,812✔
3250
        }
3251
        return false;
52,050✔
3252
    }
3253

3254
    /**
3255
     * Pop token if we encounter a token in the specified list
3256
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3257
     */
3258
    private matchAny(...tokenKinds: TokenKind[]) {
3259
        for (let tokenKind of tokenKinds) {
201,971✔
3260
            if (this.check(tokenKind)) {
565,314✔
3261
                this.current++; //advance
53,596✔
3262
                return true;
53,596✔
3263
            }
3264
        }
3265
        return false;
148,375✔
3266
    }
3267

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

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

3297
    /**
3298
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3299
     */
3300
    private consumeTokenIf(tokenKind: TokenKind) {
3301
        if (this.match(tokenKind)) {
3,370✔
3302
            return this.previous();
392✔
3303
        }
3304
    }
3305

3306
    private consumeToken(tokenKind: TokenKind) {
3307
        return this.consume(
1,820✔
3308
            DiagnosticMessages.expectedToken(tokenKind),
3309
            tokenKind
3310
        );
3311
    }
3312

3313
    /**
3314
     * Consume, or add a message if not found. But then continue and return undefined
3315
     */
3316
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3317
        const nextKind = this.peek().kind;
21,408✔
3318
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
46,267✔
3319

3320
        if (foundTokenKind) {
21,408✔
3321
            return this.advance();
21,299✔
3322
        }
3323
        this.diagnostics.push({
109✔
3324
            ...diagnostic,
3325
            location: this.peek()?.location
327!
3326
        });
3327
    }
3328

3329
    private tryConsumeToken(tokenKind: TokenKind) {
3330
        return this.tryConsume(
76✔
3331
            DiagnosticMessages.expectedToken(tokenKind),
3332
            tokenKind
3333
        );
3334
    }
3335

3336
    private consumeStatementSeparators(optional = false) {
9,502✔
3337
        //a comment or EOF mark the end of the statement
3338
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
28,812✔
3339
            return true;
603✔
3340
        }
3341
        let consumed = false;
28,209✔
3342
        //consume any newlines and colons
3343
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
28,209✔
3344
            consumed = true;
30,778✔
3345
        }
3346
        if (!optional && !consumed) {
28,209✔
3347
            this.diagnostics.push({
68✔
3348
                ...DiagnosticMessages.expectedNewlineOrColon(),
3349
                location: this.peek()?.location
204!
3350
            });
3351
        }
3352
        return consumed;
28,209✔
3353
    }
3354

3355
    private advance(): Token {
3356
        if (!this.isAtEnd()) {
49,011✔
3357
            this.current++;
48,997✔
3358
        }
3359
        return this.previous();
49,011✔
3360
    }
3361

3362
    private checkEndOfStatement(): boolean {
3363
        const nextKind = this.peek().kind;
6,684✔
3364
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
6,684✔
3365
    }
3366

3367
    private checkPrevious(tokenKind: TokenKind): boolean {
3368
        return this.previous()?.kind === tokenKind;
216!
3369
    }
3370

3371
    /**
3372
     * Check that the next token kind is the expected kind
3373
     * @param tokenKind the expected next kind
3374
     * @returns true if the next tokenKind is the expected value
3375
     */
3376
    private check(tokenKind: TokenKind): boolean {
3377
        const nextKind = this.peek().kind;
914,893✔
3378
        if (nextKind === TokenKind.Eof) {
914,893✔
3379
            return false;
11,453✔
3380
        }
3381
        return nextKind === tokenKind;
903,440✔
3382
    }
3383

3384
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3385
        const nextKind = this.peek().kind;
154,672✔
3386
        if (nextKind === TokenKind.Eof) {
154,672✔
3387
            return false;
1,092✔
3388
        }
3389
        return tokenKinds.includes(nextKind);
153,580✔
3390
    }
3391

3392
    private checkNext(tokenKind: TokenKind): boolean {
3393
        if (this.isAtEnd()) {
13,307!
3394
            return false;
×
3395
        }
3396
        return this.peekNext().kind === tokenKind;
13,307✔
3397
    }
3398

3399
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3400
        if (this.isAtEnd()) {
5,939!
3401
            return false;
×
3402
        }
3403
        const nextKind = this.peekNext().kind;
5,939✔
3404
        return tokenKinds.includes(nextKind);
5,939✔
3405
    }
3406

3407
    private isAtEnd(): boolean {
3408
        const peekToken = this.peek();
145,840✔
3409
        return !peekToken || peekToken.kind === TokenKind.Eof;
145,840✔
3410
    }
3411

3412
    private peekNext(): Token {
3413
        if (this.isAtEnd()) {
19,246!
3414
            return this.peek();
×
3415
        }
3416
        return this.tokens[this.current + 1];
19,246✔
3417
    }
3418

3419
    private peek(): Token {
3420
        return this.tokens[this.current];
1,266,166✔
3421
    }
3422

3423
    private previous(): Token {
3424
        return this.tokens[this.current - 1];
84,336✔
3425
    }
3426

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

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

3456
    private synchronize() {
3457
        this.advance(); // skip the erroneous token
83✔
3458

3459
        while (!this.isAtEnd()) {
83✔
3460
            if (this.ensureNewLineOrColon(true)) {
175✔
3461
                // end of statement reached
3462
                return;
58✔
3463
            }
3464

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

3481
            this.advance();
116✔
3482
        }
3483
    }
3484

3485

3486
    public dispose() {
3487
    }
3488
}
3489

3490
export enum ParseMode {
1✔
3491
    BrightScript = 'BrightScript',
1✔
3492
    BrighterScript = 'BrighterScript'
1✔
3493
}
3494

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

3519

3520
class CancelStatementError extends Error {
3521
    constructor() {
3522
        super('CancelStatement');
2✔
3523
    }
3524
}
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