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

rokucommunity / brighterscript / #15427

24 Mar 2026 04:58PM UTC coverage: 88.856% (-0.1%) from 88.993%
#15427

push

web-flow
Merge 9221958c5 into 0c894b16d

7940 of 9425 branches covered (84.24%)

Branch coverage included in aggregate %.

50 of 67 new or added lines in 4 files covered. (74.63%)

1 existing line in 1 file now uncovered.

10183 of 10971 relevant lines covered (92.82%)

1946.79 hits per line

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

92.25
/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
    AllowedProperties,
8
    AssignmentOperators,
9
    BrighterScriptSourceLiterals,
10
    DeclarableTypes,
11
    DisallowedFunctionIdentifiersText,
12
    DisallowedLocalIdentifiersText,
13
    TokenKind
14
} from '../lexer/TokenKind';
15
import type {
16
    PrintSeparatorSpace,
17
    PrintSeparatorTab
18
} from './Statement';
19
import {
1✔
20
    AssignmentStatement,
21
    Block,
22
    Body,
23
    CatchStatement,
24
    ContinueStatement,
25
    ClassStatement,
26
    ConstStatement,
27
    CommentStatement,
28
    DimStatement,
29
    DottedSetStatement,
30
    EndStatement,
31
    EnumMemberStatement,
32
    EnumStatement,
33
    ExitForStatement,
34
    ExitWhileStatement,
35
    ExpressionStatement,
36
    FieldStatement,
37
    ForEachStatement,
38
    ForStatement,
39
    FunctionStatement,
40
    GotoStatement,
41
    IfStatement,
42
    ImportStatement,
43
    IncrementStatement,
44
    IndexedSetStatement,
45
    InterfaceFieldStatement,
46
    InterfaceMethodStatement,
47
    InterfaceStatement,
48
    LabelStatement,
49
    LibraryStatement,
50
    MethodStatement,
51
    NamespaceStatement,
52
    PrintStatement,
53
    ReturnStatement,
54
    StopStatement,
55
    ThrowStatement,
56
    TryCatchStatement,
57
    WhileStatement,
58
    TypecastStatement,
59
    AliasStatement,
60
    TypeStatement
61
} from './Statement';
62
import type { DiagnosticInfo } from '../DiagnosticMessages';
63
import { DiagnosticMessages } from '../DiagnosticMessages';
1✔
64
import { util } from '../util';
1✔
65
import {
1✔
66
    AALiteralExpression,
67
    AAMemberExpression,
68
    AnnotationExpression,
69
    ArrayLiteralExpression,
70
    BinaryExpression,
71
    CallExpression,
72
    CallfuncExpression,
73
    DottedGetExpression,
74
    EscapedCharCodeLiteralExpression,
75
    FunctionExpression,
76
    FunctionParameterExpression,
77
    GroupingExpression,
78
    IndexedGetExpression,
79
    LiteralExpression,
80
    NamespacedVariableNameExpression,
81
    NewExpression,
82
    NullCoalescingExpression,
83
    RegexLiteralExpression,
84
    SourceLiteralExpression,
85
    TaggedTemplateStringExpression,
86
    TemplateStringExpression,
87
    TemplateStringQuasiExpression,
88
    TernaryExpression,
89
    TypeCastExpression,
90
    UnaryExpression,
91
    VariableExpression,
92
    XmlAttributeGetExpression
93
} from './Expression';
94
import type { Diagnostic, Range } from 'vscode-languageserver';
95
import type { Logger } from '../logging';
96
import { createLogger } from '../logging';
1✔
97
import { isAAMemberExpression, isAnnotationExpression, isBinaryExpression, isCallExpression, isCallfuncExpression, isMethodStatement, isCommentStatement, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression, isXmlAttributeGetExpression } from '../astUtils/reflection';
1✔
98
import { createVisitor, WalkMode } from '../astUtils/visitors';
1✔
99
import { createStringLiteral, createToken } from '../astUtils/creators';
1✔
100
import { Cache } from '../Cache';
1✔
101
import type { Expression, Statement } from './AstNode';
102
import { SymbolTable } from '../SymbolTable';
1✔
103
import type { BscType } from '../types/BscType';
104

105
export class Parser {
1✔
106
    /**
107
     * The array of tokens passed to `parse()`
108
     */
109
    public tokens = [] as Token[];
2,288✔
110

111
    /**
112
     * The current token index
113
     */
114
    public current: number;
115

116
    /**
117
     * The list of statements for the parsed file
118
     */
119
    public ast = new Body([]);
2,288✔
120

121
    public get statements() {
122
        return this.ast.statements;
501✔
123
    }
124

125
    /**
126
     * The top-level symbol table for the body of this file.
127
     */
128
    public get symbolTable() {
129
        return this.ast.symbolTable;
8,828✔
130
    }
131

132
    /**
133
     * References for significant statements/expressions in the parser.
134
     * These are initially extracted during parse-time to improve performance, but will also be dynamically regenerated if need be.
135
     *
136
     * If a plugin modifies the AST, then the plugin should call Parser#invalidateReferences() to force this object to refresh
137
     */
138
    public get references() {
139
        //build the references object if it's missing.
140
        if (!this._references) {
49,157✔
141
            this.findReferences();
7✔
142
        }
143
        return this._references;
49,157✔
144
    }
145

146
    private _references = new References();
2,288✔
147

148
    /**
149
     * Invalidates (clears) the references collection. This should be called anytime the AST has been manipulated.
150
     */
151
    invalidateReferences() {
152
        this._references = undefined;
7✔
153
    }
154

155
    private addPropertyHints(item: Token | AALiteralExpression) {
156
        if (isToken(item)) {
1,257✔
157
            const name = item.text;
1,026✔
158
            this._references.propertyHints[name.toLowerCase()] = name;
1,026✔
159
        } else {
160
            for (const member of item.elements) {
231✔
161
                if (!isCommentStatement(member) && member.keyToken) {
269✔
162
                    const name = member.keyToken.text;
230✔
163
                    if (!name.startsWith('"')) {
230✔
164
                        this._references.propertyHints[name.toLowerCase()] = name;
198✔
165
                    }
166
                }
167
            }
168
        }
169
    }
170

171
    /**
172
     * The list of diagnostics found during the parse process
173
     */
174
    public diagnostics: Diagnostic[];
175

176
    /**
177
     * The depth of the calls to function declarations. Helps some checks know if they are at the root or not.
178
     */
179
    private namespaceAndFunctionDepth: number;
180

181
    /**
182
     * The options used to parse the file
183
     */
184
    public options: ParseOptions;
185

186
    private globalTerminators = [] as TokenKind[][];
2,288✔
187

188
    /**
189
     * 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
190
     * based on the parse mode
191
     */
192
    private allowedLocalIdentifiers: TokenKind[];
193

194
    /**
195
     * Annotations collected which should be attached to the next statement
196
     */
197
    private pendingAnnotations: AnnotationExpression[];
198

199
    /**
200
     * Get the currently active global terminators
201
     */
202
    private peekGlobalTerminators() {
203
        return this.globalTerminators[this.globalTerminators.length - 1] ?? [];
7,423✔
204
    }
205

206
    /**
207
     * Static wrapper around creating a new parser and parsing a list of tokens
208
     */
209
    public static parse(toParse: Token[] | string, options?: ParseOptions): Parser {
210
        return new Parser().parse(toParse, options);
2,261✔
211
    }
212

213
    /**
214
     * Parses an array of `Token`s into an abstract syntax tree
215
     * @param toParse the array of tokens to parse. May not contain any whitespace tokens
216
     * @returns the same instance of the parser which contains the diagnostics and statements
217
     */
218
    public parse(toParse: Token[] | string, options?: ParseOptions) {
219
        this.logger = options?.logger ?? createLogger();
2,262✔
220
        options = this.sanitizeParseOptions(options);
2,262✔
221
        this.options = options;
2,262✔
222

223
        let tokens: Token[];
224
        if (typeof toParse === 'string') {
2,262✔
225
            tokens = Lexer.scan(toParse, { trackLocations: options.trackLocations }).tokens;
298✔
226
        } else {
227
            tokens = toParse;
1,964✔
228
        }
229
        this.tokens = tokens;
2,262✔
230
        this.allowedLocalIdentifiers = [
2,262✔
231
            ...AllowedLocalIdentifiers,
232
            //when in plain brightscript mode, the BrighterScript source literals can be used as regular variables
233
            ...(this.options.mode === ParseMode.BrightScript ? BrighterScriptSourceLiterals : [])
2,262✔
234
        ];
235
        this.current = 0;
2,262✔
236
        this.diagnostics = [];
2,262✔
237
        this.namespaceAndFunctionDepth = 0;
2,262✔
238
        this.pendingAnnotations = [];
2,262✔
239

240
        this.ast = this.body();
2,262✔
241

242
        //now that we've built the AST, link every node to its parent
243
        this.ast.link();
2,262✔
244
        return this;
2,262✔
245
    }
246

247
    private logger: Logger;
248

249
    private body() {
250
        const parentAnnotations = this.enterAnnotationBlock();
2,527✔
251

252
        let body = new Body([]);
2,527✔
253
        if (this.tokens.length > 0) {
2,527✔
254
            this.consumeStatementSeparators(true);
2,526✔
255

256
            try {
2,526✔
257
                while (
2,526✔
258
                    //not at end of tokens
259
                    !this.isAtEnd() &&
9,176✔
260
                    //the next token is not one of the end terminators
261
                    !this.checkAny(...this.peekGlobalTerminators())
262
                ) {
263
                    let dec = this.declaration();
3,193✔
264
                    if (dec) {
3,193✔
265
                        if (!isAnnotationExpression(dec)) {
3,146✔
266
                            this.consumePendingAnnotations(dec);
3,109✔
267
                            body.statements.push(dec);
3,109✔
268
                            //ensure statement separator
269
                            this.consumeStatementSeparators(false);
3,109✔
270
                        } else {
271
                            this.consumeStatementSeparators(true);
37✔
272
                        }
273
                    }
274
                }
275
            } catch (parseError) {
276
                //do nothing with the parse error for now. perhaps we can remove this?
277
                console.error(parseError);
×
278
            }
279
        }
280

281
        this.exitAnnotationBlock(parentAnnotations);
2,527✔
282
        return body;
2,527✔
283
    }
284

285
    private sanitizeParseOptions(options: ParseOptions) {
286
        options ??= {};
2,262✔
287
        options.mode ??= ParseMode.BrightScript;
2,262✔
288
        options.trackLocations ??= true;
2,262✔
289
        return options;
2,262✔
290
    }
291

292
    /**
293
     * Determine if the parser is currently parsing tokens at the root level.
294
     */
295
    private isAtRootLevel() {
296
        return this.namespaceAndFunctionDepth === 0;
16,008✔
297
    }
298

299
    /**
300
     * Throws an error if the input file type is not BrighterScript
301
     */
302
    private warnIfNotBrighterScriptMode(featureName: string) {
303
        if (this.options.mode !== ParseMode.BrighterScript) {
1,406✔
304
            let diagnostic = {
170✔
305
                ...DiagnosticMessages.bsFeatureNotSupportedInBrsFiles(featureName),
306
                range: this.peek().range
307
            } as Diagnostic;
308
            this.diagnostics.push(diagnostic);
170✔
309
        }
310
    }
311

312
    /**
313
     * Throws an exception using the last diagnostic message
314
     */
315
    private lastDiagnosticAsError() {
316
        let error = new Error(this.diagnostics[this.diagnostics.length - 1]?.message ?? 'Unknown error');
122!
317
        (error as any).isDiagnostic = true;
122✔
318
        return error;
122✔
319
    }
320

321
    private declaration(): Statement | AnnotationExpression | undefined {
322
        try {
5,773✔
323
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
5,773✔
324
                return this.functionDeclaration(false);
1,518✔
325
            }
326

327
            if (this.checkLibrary()) {
4,255✔
328
                return this.libraryStatement();
14✔
329
            }
330

331
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
4,241✔
332
                return this.constDeclaration();
84✔
333
            }
334

335
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
4,157✔
336
                return this.annotationExpression();
44✔
337
            }
338

339
            if (this.check(TokenKind.Comment)) {
4,113✔
340
                return this.commentStatement();
216✔
341
            }
342

343
            //catch certain global terminators to prevent unnecessary lookahead (i.e. like `end namespace`, no need to continue)
344
            if (this.checkAny(...this.peekGlobalTerminators())) {
3,897!
345
                return;
×
346
            }
347

348
            return this.statement();
3,897✔
349
        } catch (error: any) {
350
            //if the error is not a diagnostic, then log the error for debugging purposes
351
            if (!error.isDiagnostic) {
117!
352
                this.logger.error(error);
×
353
            }
354
            this.synchronize();
117✔
355
        }
356
    }
357

358
    /**
359
     * Try to get an identifier. If not found, add diagnostic and return undefined
360
     */
361
    private tryIdentifier(...additionalTokenKinds: TokenKind[]): Identifier | undefined {
362
        const identifier = this.tryConsume(
124✔
363
            DiagnosticMessages.expectedIdentifier(),
364
            TokenKind.Identifier,
365
            ...additionalTokenKinds
366
        ) as Identifier;
367
        if (identifier) {
124✔
368
            // force the name into an identifier so the AST makes some sense
369
            identifier.kind = TokenKind.Identifier;
123✔
370
            return identifier;
123✔
371
        }
372
    }
373

374
    private identifier(...additionalTokenKinds: TokenKind[]) {
375
        const identifier = this.consume(
453✔
376
            DiagnosticMessages.expectedIdentifier(),
377
            TokenKind.Identifier,
378
            ...additionalTokenKinds
379
        ) as Identifier;
380
        // force the name into an identifier so the AST makes some sense
381
        identifier.kind = TokenKind.Identifier;
453✔
382
        return identifier;
453✔
383
    }
384

385
    private enumMemberStatement() {
386
        const statement = new EnumMemberStatement({} as any);
213✔
387
        statement.tokens.name = this.consume(
213✔
388
            DiagnosticMessages.expectedClassFieldIdentifier(),
389
            TokenKind.Identifier,
390
            ...AllowedProperties
391
        ) as Identifier;
392
        //look for `= SOME_EXPRESSION`
393
        if (this.check(TokenKind.Equal)) {
213✔
394
            statement.tokens.equal = this.advance();
121✔
395
            statement.value = this.expression();
121✔
396
        }
397
        return statement;
213✔
398
    }
399

400
    /**
401
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration`
402
     */
403
    private interfaceFieldStatement(optionalKeyword?: Token) {
404
        const name = this.identifier(...AllowedProperties);
75✔
405
        let asToken: Token;
406
        let typeToken: Token;
407
        let type: BscType;
408
        if (this.check(TokenKind.As)) {
75!
409
            asToken = this.consumeToken(TokenKind.As);
75✔
410
            typeToken = this.typeToken();
75✔
411
            type = util.tokenToBscType(typeToken);
75✔
412
        }
413

414
        if (!type) {
75!
415
            this.diagnostics.push({
×
416
                ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, typeToken.text),
417
                range: typeToken.range
418
            });
419
            throw this.lastDiagnosticAsError();
×
420
        }
421

422
        return new InterfaceFieldStatement(name, asToken, typeToken, type, optionalKeyword);
75✔
423
    }
424

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

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

443
                params.push(this.functionParameter());
7✔
444
            } while (this.match(TokenKind.Comma));
445
        }
446
        const rightParen = this.consumeToken(TokenKind.RightParen);
23✔
447
        let asToken = null as Token;
23✔
448
        let returnTypeToken = null as Token;
23✔
449
        if (this.check(TokenKind.As)) {
23✔
450
            asToken = this.advance();
20✔
451
            returnTypeToken = this.typeToken();
20✔
452
            const returnType = util.tokenToBscType(returnTypeToken);
20✔
453
            if (!returnType) {
20!
454
                this.diagnostics.push({
×
455
                    ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, returnTypeToken.text),
456
                    range: returnTypeToken.range
457
                });
458
                throw this.lastDiagnosticAsError();
×
459
            }
460
        }
461

462
        return new InterfaceMethodStatement(
23✔
463
            functionType,
464
            name,
465
            leftParen,
466
            params,
467
            rightParen,
468
            asToken,
469
            returnTypeToken,
470
            util.tokenToBscType(returnTypeToken),
471
            optionalKeyword
472
        );
473
    }
474

475
    private interfaceDeclaration(): InterfaceStatement {
476
        this.warnIfNotBrighterScriptMode('interface declarations');
66✔
477

478
        const parentAnnotations = this.enterAnnotationBlock();
66✔
479

480
        const interfaceToken = this.consume(
66✔
481
            DiagnosticMessages.expectedKeyword(TokenKind.Interface),
482
            TokenKind.Interface
483
        );
484
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
66✔
485

486
        let extendsToken: Token;
487
        let parentInterfaceName: NamespacedVariableNameExpression;
488

489
        if (this.peek().text.toLowerCase() === 'extends') {
66✔
490
            extendsToken = this.advance();
2✔
491
            parentInterfaceName = this.getNamespacedVariableNameExpression();
2✔
492
        }
493
        this.consumeStatementSeparators();
66✔
494
        //gather up all interface members (Fields, Methods)
495
        let body = [] as Statement[];
66✔
496
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
66✔
497
            try {
167✔
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)) {
167✔
500
                    break;
66✔
501
                }
502

503
                let decl: Statement;
504

505
                //collect leading annotations
506
                if (this.check(TokenKind.At)) {
101✔
507
                    this.annotationExpression();
2✔
508
                }
509

510
                const optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
101✔
511
                //fields
512
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkAnyNext(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
101✔
513
                    decl = this.interfaceFieldStatement(optionalKeyword);
75✔
514
                    //field with name = 'optional'
515
                } else if (optionalKeyword && this.checkAny(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
26!
516
                    //rewind one place, so that 'optional' is the field name
517
                    this.current--;
×
518
                    decl = this.interfaceFieldStatement();
×
519

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

524
                    //comments
525
                } else if (this.check(TokenKind.Comment)) {
3✔
526
                    decl = this.commentStatement();
1✔
527
                }
528
                if (decl) {
98✔
529
                    this.consumePendingAnnotations(decl);
96✔
530
                    body.push(decl);
96✔
531
                } else {
532
                    //we didn't find a declaration...flag tokens until next line
533
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
534
                }
535
            } catch (e) {
536
                //throw out any failed members and move on to the next line
537
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3✔
538
            }
539

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

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

547
        const statement = new InterfaceStatement(
66✔
548
            interfaceToken,
549
            nameToken,
550
            extendsToken,
551
            parentInterfaceName,
552
            body,
553
            endInterfaceToken
554
        );
555
        this._references.interfaceStatements.push(statement);
66✔
556
        this.exitAnnotationBlock(parentAnnotations);
66✔
557
        return statement;
66✔
558
    }
559

560
    private enumDeclaration(): EnumStatement {
561
        const result = new EnumStatement({} as any, []);
124✔
562
        this.warnIfNotBrighterScriptMode('enum declarations');
124✔
563

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

566
        result.tokens.enum = this.consume(
124✔
567
            DiagnosticMessages.expectedKeyword(TokenKind.Enum),
568
            TokenKind.Enum
569
        );
570

571
        result.tokens.name = this.tryIdentifier(...this.allowedLocalIdentifiers);
124✔
572

573
        this.consumeStatementSeparators();
124✔
574
        //gather up all members
575
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
124✔
576
            try {
219✔
577
                let decl: EnumMemberStatement | CommentStatement;
578

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

584
                //members
585
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
219✔
586
                    decl = this.enumMemberStatement();
213✔
587

588
                    //comments
589
                } else if (this.check(TokenKind.Comment)) {
6!
590
                    decl = this.commentStatement();
6✔
591
                }
592

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

605
            //ensure statement separator
606
            this.consumeStatementSeparators();
219✔
607
            //break out of this loop if we encountered the `EndEnum` token
608
            if (this.check(TokenKind.EndEnum)) {
219✔
609
                break;
116✔
610
            }
611
        }
612

613
        //consume the final `end interface` token
614
        result.tokens.endEnum = this.consumeToken(TokenKind.EndEnum);
124✔
615

616
        this._references.enumStatements.push(result);
124✔
617
        this.exitAnnotationBlock(parentAnnotations);
124✔
618
        return result;
124✔
619
    }
620

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

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

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

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

639
        //see if the class inherits from parent
640
        if (this.peek().text.toLowerCase() === 'extends') {
487✔
641
            extendsKeyword = this.advance();
76✔
642
            parentClassName = this.getNamespacedVariableNameExpression();
76✔
643
        }
644

645
        //ensure statement separator
646
        this.consumeStatementSeparators();
486✔
647

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

655
                if (this.check(TokenKind.At)) {
473✔
656
                    this.annotationExpression();
15✔
657
                }
658

659
                if (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private)) {
472✔
660
                    //use actual access modifier
661
                    accessModifier = this.advance();
65✔
662
                }
663

664
                let overrideKeyword: Token;
665
                if (this.peek().text.toLowerCase() === 'override') {
472✔
666
                    overrideKeyword = this.advance();
17✔
667
                }
668

669
                //methods (function/sub keyword OR identifier followed by opening paren)
670
                if (this.checkAny(TokenKind.Function, TokenKind.Sub) || (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.LeftParen))) {
472✔
671
                    const funcDeclaration = this.functionDeclaration(false, false);
266✔
672

673
                    //remove this function from the lists because it's not a callable
674
                    const functionStatement = this._references.functionStatements.pop();
266✔
675

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

684
                    decl = new MethodStatement(
266✔
685
                        accessModifier,
686
                        funcDeclaration.name,
687
                        funcDeclaration.func,
688
                        overrideKeyword
689
                    );
690

691
                    //refer to this statement as parent of the expression
692
                    functionStatement.func.functionStatement = decl as MethodStatement;
266✔
693

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

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

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

707
                    //comments
708
                } else if (this.check(TokenKind.Comment)) {
22✔
709
                    decl = this.commentStatement();
8✔
710
                }
711

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

721
            //ensure statement separator
722
            this.consumeStatementSeparators();
473✔
723
        }
724

725
        let endingKeyword = this.advance();
486✔
726
        if (endingKeyword.kind !== TokenKind.EndClass) {
486✔
727
            this.diagnostics.push({
3✔
728
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('class'),
729
                range: endingKeyword.range
730
            });
731
        }
732

733
        const result = new ClassStatement(
486✔
734
            classKeyword,
735
            className,
736
            body,
737
            endingKeyword,
738
            extendsKeyword,
739
            parentClassName
740
        );
741

742
        this._references.classStatements.push(result);
486✔
743
        this.exitAnnotationBlock(parentAnnotations);
486✔
744
        return result;
486✔
745
    }
746

747
    private fieldDeclaration(accessModifier: Token | null) {
748

749
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
184✔
750

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

774
        let name = this.consume(
184✔
775
            DiagnosticMessages.expectedClassFieldIdentifier(),
776
            TokenKind.Identifier,
777
            ...AllowedProperties
778
        ) as Identifier;
779
        let asToken: Token;
780
        let fieldType: Token;
781
        //look for `as SOME_TYPE`
782
        if (this.check(TokenKind.As)) {
184✔
783
            asToken = this.advance();
129✔
784
            fieldType = this.typeToken();
129✔
785

786
            //no field type specified
787
            if (!util.tokenToBscType(fieldType)) {
129✔
788
                this.diagnostics.push({
1✔
789
                    ...DiagnosticMessages.expectedValidTypeToFollowAsKeyword(),
790
                    range: this.peek().range
791
                });
792
            }
793
        }
794

795
        let initialValue: Expression;
796
        let equal: Token;
797
        //if there is a field initializer
798
        if (this.check(TokenKind.Equal)) {
184✔
799
            equal = this.advance();
40✔
800
            initialValue = this.expression();
40✔
801
        }
802

803
        return new FieldStatement(
183✔
804
            accessModifier,
805
            name,
806
            asToken,
807
            fieldType,
808
            equal,
809
            initialValue,
810
            optionalKeyword
811
        );
812
    }
813

814
    /**
815
     * An array of CallExpression for the current function body
816
     */
817
    private callExpressions = [];
2,288✔
818

819
    private functionDeclaration(isAnonymous: true, checkIdentifier?: boolean, onlyCallableAsMember?: boolean): FunctionExpression;
820
    private functionDeclaration(isAnonymous: false, checkIdentifier?: boolean, onlyCallableAsMember?: boolean): FunctionStatement;
821
    private functionDeclaration(isAnonymous: boolean, checkIdentifier = true, onlyCallableAsMember = false) {
3,464✔
822
        let previousCallExpressions = this.callExpressions;
1,865✔
823
        this.callExpressions = [];
1,865✔
824
        try {
1,865✔
825
            //track depth to help certain statements need to know if they are contained within a function body
826
            this.namespaceAndFunctionDepth++;
1,865✔
827
            let functionType: Token;
828
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
1,865✔
829
                functionType = this.advance();
1,863✔
830
            } else {
831
                this.diagnostics.push({
2✔
832
                    ...DiagnosticMessages.missingCallableKeyword(),
833
                    range: this.peek().range
834
                });
835
                functionType = {
2✔
836
                    isReserved: true,
837
                    kind: TokenKind.Function,
838
                    text: 'function',
839
                    //zero-length location means derived
840
                    range: {
841
                        start: this.peek().range.start,
842
                        end: this.peek().range.start
843
                    },
844
                    leadingWhitespace: ''
845
                };
846
            }
847
            let isSub = functionType?.kind === TokenKind.Sub;
1,865!
848
            let functionTypeText = isSub ? 'sub' : 'function';
1,865✔
849
            let name: Identifier;
850
            let leftParen: Token;
851

852
            if (isAnonymous) {
1,865✔
853
                leftParen = this.consume(
81✔
854
                    DiagnosticMessages.expectedLeftParenAfterCallable(functionTypeText),
855
                    TokenKind.LeftParen
856
                );
857
            } else {
858
                name = this.consume(
1,784✔
859
                    DiagnosticMessages.expectedNameAfterCallableKeyword(functionTypeText),
860
                    TokenKind.Identifier,
861
                    ...AllowedProperties
862
                ) as Identifier;
863
                leftParen = this.consume(
1,782✔
864
                    DiagnosticMessages.expectedLeftParenAfterCallableName(functionTypeText),
865
                    TokenKind.LeftParen
866
                );
867

868
                //prevent functions from ending with type designators
869
                let lastChar = name.text[name.text.length - 1];
1,779✔
870
                if (['$', '%', '!', '#', '&'].includes(lastChar)) {
1,779✔
871
                    //don't throw this error; let the parser continue
872
                    this.diagnostics.push({
8✔
873
                        ...DiagnosticMessages.functionNameCannotEndWithTypeDesignator(functionTypeText, name.text, lastChar),
874
                        range: name.range
875
                    });
876
                }
877

878
                //flag functions with keywords for names (only for standard functions)
879
                if (checkIdentifier && DisallowedFunctionIdentifiersText.has(name.text.toLowerCase())) {
1,779✔
880
                    this.diagnostics.push({
2✔
881
                        ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
882
                        range: name.range
883
                    });
884
                }
885
            }
886

887
            let params = [] as FunctionParameterExpression[];
1,860✔
888
            let asToken: Token;
889
            let typeToken: Token;
890
            if (!this.check(TokenKind.RightParen)) {
1,860✔
891
                do {
329✔
892
                    params.push(this.functionParameter());
680✔
893
                } while (this.match(TokenKind.Comma));
894
            }
895
            let rightParen = this.advance();
1,859✔
896

897
            if (this.check(TokenKind.As)) {
1,859✔
898
                asToken = this.advance();
118✔
899

900
                typeToken = this.typeToken();
118✔
901

902
                if (!util.tokenToBscType(typeToken, this.options.mode === ParseMode.BrighterScript)) {
118✔
903
                    this.diagnostics.push({
2✔
904
                        ...DiagnosticMessages.invalidFunctionReturnType(typeToken.text ?? ''),
6!
905
                        range: typeToken.range
906
                    });
907
                }
908
            }
909

910
            params.reduce((haveFoundOptional: boolean, param: FunctionParameterExpression) => {
1,859✔
911
                if (haveFoundOptional && !param.defaultValue) {
678!
912
                    this.diagnostics.push({
×
913
                        ...DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter(param.name.text),
914
                        range: param.range
915
                    });
916
                }
917

918
                return haveFoundOptional || !!param.defaultValue;
678✔
919
            }, false);
920

921
            this.consumeStatementSeparators(true);
1,859✔
922

923
            let func = new FunctionExpression(
1,859✔
924
                params,
925
                undefined, //body
926
                functionType,
927
                undefined, //ending keyword
928
                leftParen,
929
                rightParen,
930
                asToken,
931
                typeToken
932
            );
933

934
            // add the function to the relevant symbol tables
935
            if (!onlyCallableAsMember && name) {
1,859✔
936
                const funcType = func.getFunctionType();
1,778✔
937
                funcType.setName(name.text);
1,778✔
938
            }
939

940
            this._references.functionExpressions.push(func);
1,859✔
941

942
            //support ending the function with `end sub` OR `end function`
943
            func.body = this.block();
1,859✔
944
            //if the parser was unable to produce a block, make an empty one so the AST makes some sense...
945
            if (!func.body) {
1,859✔
946
                func.body = new Block([], util.createRangeFromPositions(func.range.start, func.range.start));
3✔
947
            }
948
            func.body.symbolTable = new SymbolTable(`Block: Function '${name?.text ?? ''}'`, () => func.getSymbolTable());
1,859✔
949

950
            if (!func.body) {
1,859!
951
                this.diagnostics.push({
×
952
                    ...DiagnosticMessages.callableBlockMissingEndKeyword(functionTypeText),
953
                    range: this.peek().range
954
                });
955
                throw this.lastDiagnosticAsError();
×
956
            }
957

958
            // consume 'end sub' or 'end function'
959
            func.end = this.advance();
1,859✔
960
            let expectedEndKind = isSub ? TokenKind.EndSub : TokenKind.EndFunction;
1,859✔
961

962
            //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
963
            //add an error but don't hard-fail so the AST can continue more gracefully
964
            if (func.end.kind !== expectedEndKind) {
1,859✔
965
                this.diagnostics.push({
9✔
966
                    ...DiagnosticMessages.mismatchedEndCallableKeyword(functionTypeText, func.end.text),
967
                    range: func.end.range
968
                });
969
            }
970
            func.callExpressions = this.callExpressions;
1,859✔
971

972
            if (isAnonymous) {
1,859✔
973
                return func;
81✔
974
            } else {
975
                let result = new FunctionStatement(name, func);
1,778✔
976
                func.symbolTable.name += `: '${name?.text}'`;
1,778!
977
                func.functionStatement = result;
1,778✔
978
                this._references.functionStatements.push(result);
1,778✔
979

980
                return result;
1,778✔
981
            }
982
        } finally {
983
            this.namespaceAndFunctionDepth--;
1,865✔
984
            //restore the previous CallExpression list
985
            this.callExpressions = previousCallExpressions;
1,865✔
986
        }
987
    }
988

989
    private functionParameter(): FunctionParameterExpression {
990
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
694✔
991
            this.diagnostics.push({
1✔
992
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
993
                range: this.peek().range
994
            });
995
            throw this.lastDiagnosticAsError();
1✔
996
        }
997

998
        let name = this.advance() as Identifier;
693✔
999
        // force the name into an identifier so the AST makes some sense
1000
        name.kind = TokenKind.Identifier;
693✔
1001

1002
        //add diagnostic if name is a reserved word that cannot be used as an identifier
1003
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
693✔
1004
            this.diagnostics.push({
3✔
1005
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
1006
                range: name.range
1007
            });
1008
        }
1009

1010
        let typeToken: Token | undefined;
1011
        let defaultValue;
1012

1013
        // parse argument default value
1014
        if (this.match(TokenKind.Equal)) {
693✔
1015
            // it seems any expression is allowed here -- including ones that operate on other arguments!
1016
            defaultValue = this.expression(false);
255✔
1017
        }
1018

1019
        let asToken = null;
693✔
1020
        if (this.check(TokenKind.As)) {
693✔
1021
            asToken = this.advance();
333✔
1022

1023
            typeToken = this.typeToken();
333✔
1024

1025
            if (!util.tokenToBscType(typeToken, this.options.mode === ParseMode.BrighterScript)) {
333✔
1026
                this.diagnostics.push({
5✔
1027
                    ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, typeToken.text),
1028
                    range: typeToken.range
1029
                });
1030
            }
1031
        }
1032
        return new FunctionParameterExpression(
693✔
1033
            name,
1034
            typeToken,
1035
            defaultValue,
1036
            asToken
1037
        );
1038
    }
1039

1040
    private assignment(): AssignmentStatement {
1041
        let name = this.advance() as Identifier;
955✔
1042
        //add diagnostic if name is a reserved word that cannot be used as an identifier
1043
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
955✔
1044
            this.diagnostics.push({
13✔
1045
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
1046
                range: name.range
1047
            });
1048
        }
1049
        if (this.check(TokenKind.As)) {
955✔
1050
            // v1 syntax allows type declaration on lhs of assignment
1051
            this.warnIfNotBrighterScriptMode('typed assignment');
4✔
1052

1053
            this.advance(); // skip 'as'
4✔
1054
            this.typeToken(); // skip typeToken;
4✔
1055
        }
1056

1057
        let operator = this.consume(
955✔
1058
            DiagnosticMessages.expectedOperatorAfterIdentifier(AssignmentOperators, name.text),
1059
            ...AssignmentOperators
1060
        );
1061
        let value = this.expression();
953✔
1062

1063
        let result: AssignmentStatement;
1064
        if (operator.kind === TokenKind.Equal) {
946✔
1065
            result = new AssignmentStatement(operator, name, value);
900✔
1066
        } else {
1067
            const nameExpression = new VariableExpression(name);
46✔
1068
            result = new AssignmentStatement(
46✔
1069
                { kind: TokenKind.Equal, text: '=', range: operator.range },
1070
                name,
1071
                new BinaryExpression(nameExpression, operator, value)
1072
            );
1073
            this.addExpressionsToReferences(nameExpression);
46✔
1074
            if (isBinaryExpression(value)) {
46✔
1075
                //remove the right-hand-side expression from this assignment operator, and replace with the full assignment expression
1076
                this._references.expressions.delete(value);
3✔
1077
            }
1078
            this._references.expressions.add(result);
46✔
1079
        }
1080

1081
        this._references.assignmentStatements.push(result);
946✔
1082
        return result;
946✔
1083
    }
1084

1085
    private checkLibrary() {
1086
        let isLibraryToken = this.check(TokenKind.Library);
8,206✔
1087

1088
        //if we are at the top level, any line that starts with "library" should be considered a library statement
1089
        if (this.isAtRootLevel() && isLibraryToken) {
8,206✔
1090
            return true;
13✔
1091

1092
            //not at root level, library statements are all invalid here, but try to detect if the tokens look
1093
            //like a library statement (and let the libraryStatement function handle emitting the diagnostics)
1094
        } else if (isLibraryToken && this.checkNext(TokenKind.StringLiteral)) {
8,193✔
1095
            return true;
1✔
1096

1097
            //definitely not a library statement
1098
        } else {
1099
            return false;
8,192✔
1100
        }
1101
    }
1102

1103
    private checkAlias() {
1104
        let isAliasToken = this.check(TokenKind.Alias);
3,902✔
1105

1106
        //if we are at the top level, any line that starts with "alias" should be considered a alias statement
1107
        if (this.isAtRootLevel() && isAliasToken) {
3,902✔
1108
            return true;
2✔
1109

1110
            //not at root level, alias statements are all invalid here, but try to detect if the tokens look
1111
            //like a alias statement (and let the alias function handle emitting the diagnostics)
1112
        } else if (isAliasToken && this.checkNext(TokenKind.Identifier)) {
3,900!
1113
            return true;
×
1114

1115
            //definitely not a alias statement
1116
        } else {
1117
            return false;
3,900✔
1118
        }
1119
    }
1120

1121
    private checkTypeStatement() {
1122
        let isTypeToken = this.check(TokenKind.Type);
3,900✔
1123

1124
        //if we are at the top level, any line that starts with "type" should be considered a type statement
1125
        if (this.isAtRootLevel() && isTypeToken) {
3,900✔
1126
            return true;
8✔
1127

1128
            //not at root level, type statements are all invalid here, but try to detect if the tokens look
1129
            //like a type statement (and let the type function handle emitting the diagnostics)
1130
        } else if (isTypeToken && this.checkNext(TokenKind.Identifier)) {
3,892✔
1131
            return true;
3✔
1132

1133
            //definitely not a type statement
1134
        } else {
1135
            return false;
3,889✔
1136
        }
1137
    }
1138

1139
    private statement(): Statement | undefined {
1140
        if (this.checkLibrary()) {
3,951!
1141
            return this.libraryStatement();
×
1142
        }
1143

1144
        if (this.check(TokenKind.Import)) {
3,951✔
1145
            return this.importStatement();
44✔
1146
        }
1147

1148
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
3,907✔
1149
            return this.typecastStatement();
5✔
1150
        }
1151

1152
        if (this.checkAlias()) {
3,902✔
1153
            return this.aliasStatement();
2✔
1154
        }
1155

1156
        if (this.checkTypeStatement()) {
3,900✔
1157
            return this.typeStatement();
11✔
1158
        }
1159

1160
        if (this.check(TokenKind.Stop)) {
3,889✔
1161
            return this.stopStatement();
16✔
1162
        }
1163

1164
        if (this.check(TokenKind.If)) {
3,873✔
1165
            return this.ifStatement();
171✔
1166
        }
1167

1168
        //`try` must be followed by a block, otherwise it could be a local variable
1169
        if (this.check(TokenKind.Try) && this.checkAnyNext(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
3,702✔
1170
            return this.tryCatchStatement();
27✔
1171
        }
1172

1173
        if (this.check(TokenKind.Throw)) {
3,675✔
1174
            return this.throwStatement();
11✔
1175
        }
1176

1177
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
3,664✔
1178
            return this.printStatement();
703✔
1179
        }
1180
        if (this.check(TokenKind.Dim)) {
2,961✔
1181
            return this.dimStatement();
43✔
1182
        }
1183

1184
        if (this.check(TokenKind.While)) {
2,918✔
1185
            return this.whileStatement();
23✔
1186
        }
1187

1188
        if (this.check(TokenKind.ExitWhile)) {
2,895✔
1189
            return this.exitWhile();
7✔
1190
        }
1191

1192
        if (this.check(TokenKind.For)) {
2,888✔
1193
            return this.forStatement();
34✔
1194
        }
1195

1196
        if (this.check(TokenKind.ForEach)) {
2,854✔
1197
            return this.forEachStatement();
23✔
1198
        }
1199

1200
        if (this.check(TokenKind.ExitFor)) {
2,831✔
1201
            return this.exitFor();
4✔
1202
        }
1203

1204
        if (this.check(TokenKind.End)) {
2,827✔
1205
            return this.endStatement();
8✔
1206
        }
1207

1208
        if (this.match(TokenKind.Return)) {
2,819✔
1209
            return this.returnStatement();
230✔
1210
        }
1211

1212
        if (this.check(TokenKind.Goto)) {
2,589✔
1213
            return this.gotoStatement();
12✔
1214
        }
1215

1216
        //the continue keyword (followed by `for`, `while`, or a statement separator)
1217
        if (this.check(TokenKind.Continue) && this.checkAnyNext(TokenKind.While, TokenKind.For, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
2,577✔
1218
            return this.continueStatement();
12✔
1219
        }
1220

1221
        //does this line look like a label? (i.e.  `someIdentifier:` )
1222
        if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
2,565✔
1223
            try {
12✔
1224
                return this.labelStatement();
12✔
1225
            } catch (err) {
1226
                if (!(err instanceof CancelStatementError)) {
2!
1227
                    throw err;
×
1228
                }
1229
                //not a label, try something else
1230
            }
1231
        }
1232

1233
        // BrightScript is like python, in that variables can be declared without a `var`,
1234
        // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
1235
        // out what to do with it.
1236
        if (
2,555✔
1237
            this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)
1238
        ) {
1239
            if (this.checkAnyNext(...AssignmentOperators)) {
2,370✔
1240
                return this.assignment();
917✔
1241
            } else if (this.checkNext(TokenKind.As)) {
1,453✔
1242
                // may be a typed assignment - this is v1 syntax
1243
                const backtrack = this.current;
5✔
1244
                let validTypeExpression = false;
5✔
1245
                try {
5✔
1246
                    // skip the identifier, and check for valid type expression
1247
                    this.advance();
5✔
1248
                    // skip the 'as'
1249
                    this.advance();
5✔
1250
                    // check if there is a valid type
1251
                    const typeToken = this.typeToken(true);
5✔
1252
                    const allowedNameKinds = [TokenKind.Identifier, ...DeclarableTypes, ...this.allowedLocalIdentifiers];
5✔
1253
                    validTypeExpression = allowedNameKinds.includes(typeToken.kind);
5✔
1254
                } catch (e) {
1255
                    // ignore any errors
1256
                } finally {
1257
                    this.current = backtrack;
5✔
1258
                }
1259
                if (validTypeExpression) {
5✔
1260
                    // there is a valid 'as' and type expression
1261
                    return this.assignment();
4✔
1262
                }
1263
            }
1264
        }
1265

1266
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1267
        if (this.check(TokenKind.Interface)) {
1,634✔
1268
            return this.interfaceDeclaration();
66✔
1269
        }
1270

1271
        if (this.check(TokenKind.Class)) {
1,568✔
1272
            return this.classDeclaration();
487✔
1273
        }
1274

1275
        if (this.check(TokenKind.Namespace)) {
1,081✔
1276
            return this.namespaceStatement();
266✔
1277
        }
1278

1279
        if (this.check(TokenKind.Enum)) {
815✔
1280
            return this.enumDeclaration();
124✔
1281
        }
1282

1283
        // TODO: support multi-statements
1284
        return this.setStatement();
691✔
1285
    }
1286

1287
    private whileStatement(): WhileStatement {
1288
        const whileKeyword = this.advance();
23✔
1289
        const condition = this.expression();
23✔
1290

1291
        this.consumeStatementSeparators();
22✔
1292

1293
        const whileBlock = this.block(TokenKind.EndWhile);
22✔
1294
        let endWhile: Token;
1295
        if (!whileBlock || this.peek().kind !== TokenKind.EndWhile) {
22✔
1296
            this.diagnostics.push({
1✔
1297
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('while'),
1298
                range: this.peek().range
1299
            });
1300
            if (!whileBlock) {
1!
1301
                throw this.lastDiagnosticAsError();
×
1302
            }
1303
        } else {
1304
            endWhile = this.advance();
21✔
1305
        }
1306

1307
        return new WhileStatement(
22✔
1308
            { while: whileKeyword, endWhile: endWhile },
1309
            condition,
1310
            whileBlock
1311
        );
1312
    }
1313

1314
    private exitWhile(): ExitWhileStatement {
1315
        let keyword = this.advance();
7✔
1316

1317
        return new ExitWhileStatement({ exitWhile: keyword });
7✔
1318
    }
1319

1320
    private forStatement(): ForStatement {
1321
        const forToken = this.advance();
34✔
1322
        const initializer = this.assignment();
34✔
1323

1324
        //TODO: newline allowed?
1325

1326
        const toToken = this.advance();
33✔
1327
        const finalValue = this.expression();
33✔
1328
        let incrementExpression: Expression | undefined;
1329
        let stepToken: Token | undefined;
1330

1331
        if (this.check(TokenKind.Step)) {
33✔
1332
            stepToken = this.advance();
10✔
1333
            incrementExpression = this.expression();
10✔
1334
        } else {
1335
            // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
1336
        }
1337

1338
        this.consumeStatementSeparators();
33✔
1339

1340
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
33✔
1341
        let endForToken: Token;
1342
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
33✔
1343
            this.diagnostics.push({
1✔
1344
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1345
                range: this.peek().range
1346
            });
1347
            if (!body) {
1!
1348
                throw this.lastDiagnosticAsError();
×
1349
            }
1350
        } else {
1351
            endForToken = this.advance();
32✔
1352
        }
1353

1354
        // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
1355
        // stays around in scope with whatever value it was when the loop exited.
1356
        return new ForStatement(
33✔
1357
            forToken,
1358
            initializer,
1359
            toToken,
1360
            finalValue,
1361
            body,
1362
            endForToken,
1363
            stepToken,
1364
            incrementExpression
1365
        );
1366
    }
1367

1368
    private forEachStatement(): ForEachStatement {
1369
        let forEach = this.advance();
23✔
1370
        let name = this.advance();
23✔
1371

1372
        if (this.check(TokenKind.As)) {
23✔
1373
            this.warnIfNotBrighterScriptMode('typed for each item');
3✔
1374

1375
            this.advance(); // get 'as'
3✔
1376
            this.typeToken(); // get type
3✔
1377
        }
1378

1379
        let maybeIn = this.peek();
23✔
1380
        if (this.check(TokenKind.Identifier) && maybeIn.text.toLowerCase() === 'in') {
23!
1381
            this.advance();
23✔
1382
        } else {
1383
            this.diagnostics.push({
×
1384
                ...DiagnosticMessages.expectedInAfterForEach(name.text),
1385
                range: this.peek().range
1386
            });
1387
            throw this.lastDiagnosticAsError();
×
1388
        }
1389

1390
        let target = this.expression();
23✔
1391
        if (!target) {
23!
1392
            this.diagnostics.push({
×
1393
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1394
                range: this.peek().range
1395
            });
1396
            throw this.lastDiagnosticAsError();
×
1397
        }
1398

1399
        this.consumeStatementSeparators();
23✔
1400

1401
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
23✔
1402
        if (!body) {
23!
1403
            this.diagnostics.push({
×
1404
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1405
                range: this.peek().range
1406
            });
1407
            throw this.lastDiagnosticAsError();
×
1408
        }
1409

1410
        let endFor = this.advance();
23✔
1411

1412
        return new ForEachStatement(
23✔
1413
            {
1414
                forEach: forEach,
1415
                in: maybeIn,
1416
                endFor: endFor
1417
            },
1418
            name,
1419
            target,
1420
            body
1421
        );
1422
    }
1423

1424
    private exitFor(): ExitForStatement {
1425
        let keyword = this.advance();
4✔
1426

1427
        return new ExitForStatement({ exitFor: keyword });
4✔
1428
    }
1429

1430
    private commentStatement() {
1431
        //if this comment is on the same line as the previous statement,
1432
        //then this comment should be treated as a single-line comment
1433
        let prev = this.previous();
231✔
1434
        if (prev?.range?.end.line === this.peek().range?.start.line) {
231✔
1435
            return new CommentStatement([this.advance()]);
128✔
1436
        } else {
1437
            let comments = [this.advance()];
103✔
1438
            while (this.check(TokenKind.Newline) && this.checkNext(TokenKind.Comment)) {
103✔
1439
                this.advance();
20✔
1440
                comments.push(this.advance());
20✔
1441
            }
1442
            return new CommentStatement(comments);
103✔
1443
        }
1444
    }
1445

1446
    private namespaceStatement(): NamespaceStatement | undefined {
1447
        this.warnIfNotBrighterScriptMode('namespace');
266✔
1448
        let keyword = this.advance();
266✔
1449

1450
        this.namespaceAndFunctionDepth++;
266✔
1451

1452
        let name = this.getNamespacedVariableNameExpression();
266✔
1453
        //set the current namespace name
1454
        let result = new NamespaceStatement(keyword, name, null, null);
265✔
1455

1456
        this.globalTerminators.push([TokenKind.EndNamespace]);
265✔
1457
        let body = this.body();
265✔
1458
        this.globalTerminators.pop();
265✔
1459

1460
        let endKeyword: Token;
1461
        if (this.check(TokenKind.EndNamespace)) {
265✔
1462
            endKeyword = this.advance();
264✔
1463
        } else {
1464
            //the `end namespace` keyword is missing. add a diagnostic, but keep parsing
1465
            this.diagnostics.push({
1✔
1466
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'),
1467
                range: keyword.range
1468
            });
1469
        }
1470

1471
        this.namespaceAndFunctionDepth--;
265✔
1472

1473
        result.body = body;
265✔
1474
        result.endKeyword = endKeyword;
265✔
1475
        this._references.namespaceStatements.push(result);
265✔
1476
        //cache the range property so that plugins can't affect it
1477
        result.cacheRange();
265✔
1478
        result.body.symbolTable.name += `: namespace '${result.name}'`;
265✔
1479
        return result;
265✔
1480
    }
1481

1482
    /**
1483
     * Get an expression with identifiers separated by periods. Useful for namespaces and class extends
1484
     */
1485
    private getNamespacedVariableNameExpression(ignoreDiagnostics = false) {
388✔
1486
        let firstIdentifier: Identifier;
1487
        if (ignoreDiagnostics) {
505✔
1488
            if (this.checkAny(...this.allowedLocalIdentifiers)) {
2!
1489
                firstIdentifier = this.advance() as Identifier;
×
1490
            } else {
1491
                throw new Error();
2✔
1492
            }
1493
        } else {
1494
            firstIdentifier = this.consume(
503✔
1495
                DiagnosticMessages.expectedIdentifierAfterKeyword(this.previous().text),
1496
                TokenKind.Identifier,
1497
                ...this.allowedLocalIdentifiers
1498
            ) as Identifier;
1499
        }
1500
        let expr: DottedGetExpression | VariableExpression;
1501

1502
        if (firstIdentifier) {
497!
1503
            // force it into an identifier so the AST makes some sense
1504
            firstIdentifier.kind = TokenKind.Identifier;
497✔
1505
            const varExpr = new VariableExpression(firstIdentifier);
497✔
1506
            expr = varExpr;
497✔
1507

1508
            //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
1509
            while (this.check(TokenKind.Dot)) {
497✔
1510
                let dot = this.tryConsume(
168✔
1511
                    DiagnosticMessages.unexpectedToken(this.peek().text),
1512
                    TokenKind.Dot
1513
                );
1514
                if (!dot) {
168!
1515
                    break;
×
1516
                }
1517
                let identifier = this.tryConsume(
168✔
1518
                    DiagnosticMessages.expectedIdentifier(),
1519
                    TokenKind.Identifier,
1520
                    ...this.allowedLocalIdentifiers,
1521
                    ...AllowedProperties
1522
                ) as Identifier;
1523

1524
                if (!identifier) {
168✔
1525
                    break;
3✔
1526
                }
1527
                // force it into an identifier so the AST makes some sense
1528
                identifier.kind = TokenKind.Identifier;
165✔
1529
                expr = new DottedGetExpression(expr, identifier, dot);
165✔
1530
            }
1531
        }
1532
        return new NamespacedVariableNameExpression(expr);
497✔
1533
    }
1534

1535
    /**
1536
     * Add an 'unexpected token' diagnostic for any token found between current and the first stopToken found.
1537
     */
1538
    private flagUntil(...stopTokens: TokenKind[]) {
1539
        while (!this.checkAny(...stopTokens) && !this.isAtEnd()) {
7!
1540
            let token = this.advance();
×
1541
            this.diagnostics.push({
×
1542
                ...DiagnosticMessages.unexpectedToken(token.text),
1543
                range: token.range
1544
            });
1545
        }
1546
    }
1547

1548
    /**
1549
     * Consume tokens until one of the `stopTokenKinds` is encountered
1550
     * @param stopTokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
1551
     * @returns - the list of tokens consumed, EXCLUDING the `stopTokenKind` (you can use `this.peek()` to see which one it was)
1552
     */
1553
    private consumeUntil(...stopTokenKinds: TokenKind[]) {
1554
        let result = [] as Token[];
70✔
1555
        //take tokens until we encounter one of the stopTokenKinds
1556
        while (!stopTokenKinds.includes(this.peek().kind)) {
70✔
1557
            result.push(this.advance());
181✔
1558
        }
1559
        return result;
70✔
1560
    }
1561

1562
    private constDeclaration(): ConstStatement | undefined {
1563
        this.warnIfNotBrighterScriptMode('const declaration');
84✔
1564
        const constToken = this.advance();
84✔
1565
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
84✔
1566
        const equalToken = this.consumeToken(TokenKind.Equal);
84✔
1567
        const expression = this.expression();
84✔
1568
        const statement = new ConstStatement({
84✔
1569
            const: constToken,
1570
            name: nameToken,
1571
            equals: equalToken
1572
        }, expression);
1573
        this._references.constStatements.push(statement);
84✔
1574
        return statement;
84✔
1575
    }
1576

1577
    private libraryStatement(): LibraryStatement | undefined {
1578
        let libStatement = new LibraryStatement({
14✔
1579
            library: this.advance(),
1580
            //grab the next token only if it's a string
1581
            filePath: this.tryConsume(
1582
                DiagnosticMessages.expectedStringLiteralAfterKeyword('library'),
1583
                TokenKind.StringLiteral
1584
            )
1585
        });
1586

1587
        this._references.libraryStatements.push(libStatement);
14✔
1588
        return libStatement;
14✔
1589
    }
1590

1591
    private importStatement() {
1592
        this.warnIfNotBrighterScriptMode('import statements');
44✔
1593
        let importStatement = new ImportStatement(
44✔
1594
            this.advance(),
1595
            //grab the next token only if it's a string
1596
            this.tryConsume(
1597
                DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
1598
                TokenKind.StringLiteral
1599
            )
1600
        );
1601

1602
        this._references.importStatements.push(importStatement);
44✔
1603
        return importStatement;
44✔
1604
    }
1605

1606
    private typecastStatement() {
1607
        this.warnIfNotBrighterScriptMode('typecast statements');
5✔
1608
        const typecastToken = this.advance();
5✔
1609
        const obj = this.identifier(...this.allowedLocalIdentifiers);
5✔
1610
        const asToken = this.advance();
5✔
1611
        const typeToken = this.typeToken();
5✔
1612
        return new TypecastStatement({
5✔
1613
            typecast: typecastToken,
1614
            obj: obj,
1615
            as: asToken,
1616
            type: typeToken
1617
        });
1618
    }
1619

1620
    private aliasStatement() {
1621
        this.warnIfNotBrighterScriptMode('alias statements');
2✔
1622
        const aliasToken = this.advance();
2✔
1623
        const name = this.identifier(...this.allowedLocalIdentifiers);
2✔
1624
        const equals = this.consumeToken(TokenKind.Equal);
2✔
1625
        const value = this.identifier(...this.allowedLocalIdentifiers);
2✔
1626
        return new AliasStatement({
2✔
1627
            alias: aliasToken,
1628
            name: name,
1629
            equals: equals,
1630
            value: value
1631
        });
1632
    }
1633

1634
    private annotationExpression() {
1635
        const atToken = this.advance();
61✔
1636
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
61✔
1637
        if (identifier) {
61✔
1638
            identifier.kind = TokenKind.Identifier;
60✔
1639
        }
1640
        let annotation = new AnnotationExpression(atToken, identifier);
61✔
1641
        this.pendingAnnotations.push(annotation);
60✔
1642

1643
        //optional arguments
1644
        if (this.check(TokenKind.LeftParen)) {
60✔
1645
            let leftParen = this.advance();
24✔
1646
            annotation.call = this.finishCall(leftParen, annotation, false);
24✔
1647
        }
1648
        return annotation;
60✔
1649
    }
1650

1651
    private typeStatement(): TypeStatement | undefined {
1652
        this.warnIfNotBrighterScriptMode('type statements');
11✔
1653
        const typeToken = this.advance();
11✔
1654
        const name = this.identifier(...this.allowedLocalIdentifiers);
11✔
1655
        const equals = this.tryConsume(
11✔
1656
            DiagnosticMessages.expectedToken(TokenKind.Equal),
1657
            TokenKind.Equal
1658
        );
1659
        let value = this.typeToken();
11✔
1660

1661
        let typeStmt = new TypeStatement({
11✔
1662
            type: typeToken,
1663
            name: name,
1664
            equals: equals,
1665
            value: value
1666

1667
        });
1668
        this._references.typeStatements.push(typeStmt);
11✔
1669
        return typeStmt;
11✔
1670
    }
1671

1672
    private ternaryExpression(test?: Expression): TernaryExpression {
1673
        this.warnIfNotBrighterScriptMode('ternary operator');
94✔
1674
        if (!test) {
94!
1675
            test = this.expression();
×
1676
        }
1677
        const questionMarkToken = this.advance();
94✔
1678

1679
        //consume newlines or comments
1680
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
94✔
1681
            this.advance();
8✔
1682
        }
1683

1684
        let consequent: Expression;
1685
        try {
94✔
1686
            consequent = this.expression();
94✔
1687
        } catch { }
1688

1689
        //consume newlines or comments
1690
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
94✔
1691
            this.advance();
6✔
1692
        }
1693

1694
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
94✔
1695

1696
        //consume newlines
1697
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
94✔
1698
            this.advance();
12✔
1699
        }
1700
        let alternate: Expression;
1701
        try {
94✔
1702
            alternate = this.expression();
94✔
1703
        } catch { }
1704

1705
        return new TernaryExpression(test, questionMarkToken, consequent, colonToken, alternate);
94✔
1706
    }
1707

1708
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1709
        this.warnIfNotBrighterScriptMode('null coalescing operator');
30✔
1710
        const questionQuestionToken = this.advance();
30✔
1711
        const alternate = this.expression();
30✔
1712
        return new NullCoalescingExpression(test, questionQuestionToken, alternate);
30✔
1713
    }
1714

1715
    private regexLiteralExpression() {
1716
        this.warnIfNotBrighterScriptMode('regular expression literal');
45✔
1717
        return new RegexLiteralExpression({
45✔
1718
            regexLiteral: this.advance()
1719
        });
1720
    }
1721

1722
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1723
        this.warnIfNotBrighterScriptMode('template string');
55✔
1724

1725
        //get the tag name
1726
        let tagName: Identifier;
1727
        if (isTagged) {
55✔
1728
            tagName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties) as Identifier;
8✔
1729
            // force it into an identifier so the AST makes some sense
1730
            tagName.kind = TokenKind.Identifier;
8✔
1731
        }
1732

1733
        let quasis = [] as TemplateStringQuasiExpression[];
55✔
1734
        let expressions = [];
55✔
1735
        let openingBacktick = this.peek();
55✔
1736
        this.advance();
55✔
1737
        let currentQuasiExpressionParts = [];
55✔
1738
        while (!this.isAtEnd() && !this.check(TokenKind.BackTick)) {
55✔
1739
            let next = this.peek();
206✔
1740
            if (next.kind === TokenKind.TemplateStringQuasi) {
206✔
1741
                //a quasi can actually be made up of multiple quasis when it includes char literals
1742
                currentQuasiExpressionParts.push(
130✔
1743
                    new LiteralExpression(next)
1744
                );
1745
                this.advance();
130✔
1746
            } else if (next.kind === TokenKind.EscapedCharCodeLiteral) {
76✔
1747
                currentQuasiExpressionParts.push(
33✔
1748
                    new EscapedCharCodeLiteralExpression(<any>next)
1749
                );
1750
                this.advance();
33✔
1751
            } else {
1752
                //finish up the current quasi
1753
                quasis.push(
43✔
1754
                    new TemplateStringQuasiExpression(currentQuasiExpressionParts)
1755
                );
1756
                currentQuasiExpressionParts = [];
43✔
1757

1758
                if (next.kind === TokenKind.TemplateStringExpressionBegin) {
43!
1759
                    this.advance();
43✔
1760
                }
1761
                //now keep this expression
1762
                expressions.push(this.expression());
43✔
1763
                if (!this.isAtEnd() && this.check(TokenKind.TemplateStringExpressionEnd)) {
43!
1764
                    //TODO is it an error if this is not present?
1765
                    this.advance();
43✔
1766
                } else {
1767
                    this.diagnostics.push({
×
1768
                        ...DiagnosticMessages.unterminatedTemplateExpression(),
1769
                        range: util.getRange(openingBacktick, this.peek())
1770
                    });
1771
                    throw this.lastDiagnosticAsError();
×
1772
                }
1773
            }
1774
        }
1775

1776
        //store the final set of quasis
1777
        quasis.push(
55✔
1778
            new TemplateStringQuasiExpression(currentQuasiExpressionParts)
1779
        );
1780

1781
        if (this.isAtEnd()) {
55✔
1782
            //error - missing backtick
1783
            this.diagnostics.push({
2✔
1784
                ...DiagnosticMessages.unterminatedTemplateStringAtEndOfFile(),
1785
                range: util.getRange(openingBacktick, this.peek())
1786
            });
1787
            throw this.lastDiagnosticAsError();
2✔
1788

1789
        } else {
1790
            let closingBacktick = this.advance();
53✔
1791
            if (isTagged) {
53✔
1792
                return new TaggedTemplateStringExpression(tagName, openingBacktick, quasis, expressions, closingBacktick);
8✔
1793
            } else {
1794
                return new TemplateStringExpression(openingBacktick, quasis, expressions, closingBacktick);
45✔
1795
            }
1796
        }
1797
    }
1798

1799
    private tryCatchStatement(): TryCatchStatement {
1800
        const tryToken = this.advance();
27✔
1801
        const statement = new TryCatchStatement(
27✔
1802
            { try: tryToken }
1803
        );
1804

1805
        //ensure statement separator
1806
        this.consumeStatementSeparators();
27✔
1807

1808
        statement.tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
27✔
1809

1810
        const peek = this.peek();
27✔
1811
        if (peek.kind !== TokenKind.Catch) {
27✔
1812
            this.diagnostics.push({
2✔
1813
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1814
                range: this.peek().range
1815
            });
1816
            //gracefully handle end-try
1817
            if (peek.kind === TokenKind.EndTry) {
2✔
1818
                statement.tokens.endTry = this.advance();
1✔
1819
            }
1820
            return statement;
2✔
1821
        }
1822
        const catchStmt = new CatchStatement({ catch: this.advance() });
25✔
1823
        statement.catchStatement = catchStmt;
25✔
1824

1825
        const exceptionVarToken = this.tryConsume(DiagnosticMessages.missingExceptionVarToFollowCatch(), TokenKind.Identifier, ...this.allowedLocalIdentifiers);
25✔
1826
        if (exceptionVarToken) {
25✔
1827
            // force it into an identifier so the AST makes some sense
1828
            exceptionVarToken.kind = TokenKind.Identifier;
23✔
1829
            catchStmt.exceptionVariable = exceptionVarToken as Identifier;
23✔
1830
        }
1831

1832
        //ensure statement sepatator
1833
        this.consumeStatementSeparators();
25✔
1834

1835
        catchStmt.catchBranch = this.block(TokenKind.EndTry);
25✔
1836

1837
        if (this.peek().kind !== TokenKind.EndTry) {
25✔
1838
            this.diagnostics.push({
1✔
1839
                ...DiagnosticMessages.expectedEndTryToTerminateTryCatch(),
1840
                range: this.peek().range
1841
            });
1842
        } else {
1843
            statement.tokens.endTry = this.advance();
24✔
1844
        }
1845
        return statement;
25✔
1846
    }
1847

1848
    private throwStatement() {
1849
        const throwToken = this.advance();
11✔
1850
        let expression: Expression;
1851
        if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
11✔
1852
            this.diagnostics.push({
1✔
1853
                ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(),
1854
                range: throwToken.range
1855
            });
1856
        } else {
1857
            expression = this.expression();
10✔
1858
        }
1859
        return new ThrowStatement(throwToken, expression);
9✔
1860
    }
1861

1862
    private dimStatement() {
1863
        const dim = this.advance();
43✔
1864

1865
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
43✔
1866
        // force to an identifier so the AST makes some sense
1867
        if (identifier) {
43✔
1868
            identifier.kind = TokenKind.Identifier;
41✔
1869
        }
1870

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

1873
        let expressions: Expression[] = [];
43✔
1874
        let expression: Expression;
1875
        do {
43✔
1876
            try {
82✔
1877
                expression = this.expression();
82✔
1878
                expressions.push(expression);
77✔
1879
                if (this.check(TokenKind.Comma)) {
77✔
1880
                    this.advance();
39✔
1881
                } else {
1882
                    // will also exit for right square braces
1883
                    break;
38✔
1884
                }
1885
            } catch (error) {
1886
            }
1887
        } while (expression);
1888

1889
        if (expressions.length === 0) {
43✔
1890
            this.diagnostics.push({
5✔
1891
                ...DiagnosticMessages.missingExpressionsInDimStatement(),
1892
                range: this.peek().range
1893
            });
1894
        }
1895
        let rightSquareBracket = this.tryConsume(DiagnosticMessages.missingRightSquareBracketAfterDimIdentifier(), TokenKind.RightSquareBracket);
43✔
1896
        return new DimStatement(dim, identifier, leftSquareBracket, expressions, rightSquareBracket);
43✔
1897
    }
1898

1899
    private ifStatement(): IfStatement {
1900
        // colon before `if` is usually not allowed, unless it's after `then`
1901
        if (this.current > 0) {
215✔
1902
            const prev = this.previous();
210✔
1903
            if (prev.kind === TokenKind.Colon) {
210✔
1904
                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then) {
3✔
1905
                    this.diagnostics.push({
1✔
1906
                        ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
1907
                        range: prev.range
1908
                    });
1909
                }
1910
            }
1911
        }
1912

1913
        const ifToken = this.advance();
215✔
1914
        const startingRange = ifToken.range;
215✔
1915

1916
        const condition = this.expression();
215✔
1917
        let thenBranch: Block;
1918
        let elseBranch: IfStatement | Block | undefined;
1919

1920
        let thenToken: Token | undefined;
1921
        let endIfToken: Token | undefined;
1922
        let elseToken: Token | undefined;
1923

1924
        //optional `then`
1925
        if (this.check(TokenKind.Then)) {
213✔
1926
            thenToken = this.advance();
148✔
1927
        }
1928

1929
        //is it inline or multi-line if?
1930
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
213✔
1931

1932
        if (isInlineIfThen) {
213✔
1933
            /*** PARSE INLINE IF STATEMENT ***/
1934

1935
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
32✔
1936

1937
            if (!thenBranch) {
32!
1938
                this.diagnostics.push({
×
1939
                    ...DiagnosticMessages.expectedStatementToFollowConditionalCondition(ifToken.text),
1940
                    range: this.peek().range
1941
                });
1942
                throw this.lastDiagnosticAsError();
×
1943
            } else {
1944
                this.ensureInline(thenBranch.statements);
32✔
1945
            }
1946

1947
            //else branch
1948
            if (this.check(TokenKind.Else)) {
32✔
1949
                elseToken = this.advance();
19✔
1950

1951
                if (this.check(TokenKind.If)) {
19✔
1952
                    // recurse-read `else if`
1953
                    elseBranch = this.ifStatement();
4✔
1954

1955
                    //no multi-line if chained with an inline if
1956
                    if (!elseBranch.isInline) {
4✔
1957
                        this.diagnostics.push({
2✔
1958
                            ...DiagnosticMessages.expectedInlineIfStatement(),
1959
                            range: elseBranch.range
1960
                        });
1961
                    }
1962

1963
                } else if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
15!
1964
                    //expecting inline else branch
1965
                    this.diagnostics.push({
×
1966
                        ...DiagnosticMessages.expectedInlineIfStatement(),
1967
                        range: this.peek().range
1968
                    });
1969
                    throw this.lastDiagnosticAsError();
×
1970
                } else {
1971
                    elseBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
15✔
1972

1973
                    if (elseBranch) {
15!
1974
                        this.ensureInline(elseBranch.statements);
15✔
1975
                    }
1976
                }
1977

1978
                if (!elseBranch) {
19!
1979
                    //missing `else` branch
1980
                    this.diagnostics.push({
×
1981
                        ...DiagnosticMessages.expectedStatementToFollowElse(),
1982
                        range: this.peek().range
1983
                    });
1984
                    throw this.lastDiagnosticAsError();
×
1985
                }
1986
            }
1987

1988
            if (!elseBranch || !isIfStatement(elseBranch)) {
32✔
1989
                //enforce newline at the end of the inline if statement
1990
                const peek = this.peek();
28✔
1991
                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && !this.isAtEnd()) {
28✔
1992
                    //ignore last error if it was about a colon
1993
                    if (this.previous().kind === TokenKind.Colon) {
3!
1994
                        this.diagnostics.pop();
3✔
1995
                        this.current--;
3✔
1996
                    }
1997
                    //newline is required
1998
                    this.diagnostics.push({
3✔
1999
                        ...DiagnosticMessages.expectedFinalNewline(),
2000
                        range: this.peek().range
2001
                    });
2002
                }
2003
            }
2004

2005
        } else {
2006
            /*** PARSE MULTI-LINE IF STATEMENT ***/
2007

2008
            thenBranch = this.blockConditionalBranch(ifToken);
181✔
2009

2010
            //ensure newline/colon before next keyword
2011
            this.ensureNewLineOrColon();
179✔
2012

2013
            //else branch
2014
            if (this.check(TokenKind.Else)) {
179✔
2015
                elseToken = this.advance();
92✔
2016

2017
                if (this.check(TokenKind.If)) {
92✔
2018
                    // recurse-read `else if`
2019
                    elseBranch = this.ifStatement();
40✔
2020

2021
                } else {
2022
                    elseBranch = this.blockConditionalBranch(ifToken);
52✔
2023

2024
                    //ensure newline/colon before next keyword
2025
                    this.ensureNewLineOrColon();
52✔
2026
                }
2027
            }
2028

2029
            if (!isIfStatement(elseBranch)) {
179✔
2030
                if (this.check(TokenKind.EndIf)) {
139✔
2031
                    endIfToken = this.advance();
137✔
2032

2033
                } else {
2034
                    //missing endif
2035
                    this.diagnostics.push({
2✔
2036
                        ...DiagnosticMessages.expectedEndIfToCloseIfStatement(startingRange.start),
2037
                        range: ifToken.range
2038
                    });
2039
                }
2040
            }
2041
        }
2042

2043
        return new IfStatement(
211✔
2044
            {
2045
                if: ifToken,
2046
                then: thenToken,
2047
                endIf: endIfToken,
2048
                else: elseToken
2049
            },
2050
            condition,
2051
            thenBranch,
2052
            elseBranch,
2053
            isInlineIfThen
2054
        );
2055
    }
2056

2057
    //consume a `then` or `else` branch block of an `if` statement
2058
    private blockConditionalBranch(ifToken: Token) {
2059
        //keep track of the current error count, because if the then branch fails,
2060
        //we will trash them in favor of a single error on if
2061
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
233✔
2062

2063
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
2064
        // a trailing "end if" or "else if"
2065
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
233✔
2066

2067
        if (!branch) {
233✔
2068
            //throw out any new diagnostics created as a result of a `then` block parse failure.
2069
            //the block() function will discard the current line, so any discarded diagnostics will
2070
            //resurface if they are legitimate, and not a result of a malformed if statement
2071
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
2✔
2072

2073
            //this whole if statement is bogus...add error to the if token and hard-fail
2074
            this.diagnostics.push({
2✔
2075
                ...DiagnosticMessages.expectedEndIfElseIfOrElseToTerminateThenBlock(),
2076
                range: ifToken.range
2077
            });
2078
            throw this.lastDiagnosticAsError();
2✔
2079
        }
2080
        return branch;
231✔
2081
    }
2082

2083
    private ensureNewLineOrColon(silent = false) {
231✔
2084
        const prev = this.previous().kind;
439✔
2085
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
439✔
2086
            if (!silent) {
134✔
2087
                this.diagnostics.push({
6✔
2088
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2089
                    range: this.peek().range
2090
                });
2091
            }
2092
            return false;
134✔
2093
        }
2094
        return true;
305✔
2095
    }
2096

2097
    //ensure each statement of an inline block is single-line
2098
    private ensureInline(statements: Statement[]) {
2099
        for (const stat of statements) {
47✔
2100
            if (isIfStatement(stat) && !stat.isInline) {
54✔
2101
                this.diagnostics.push({
2✔
2102
                    ...DiagnosticMessages.expectedInlineIfStatement(),
2103
                    range: stat.range
2104
                });
2105
            }
2106
        }
2107
    }
2108

2109
    //consume inline branch of an `if` statement
2110
    private inlineConditionalBranch(...additionalTerminators: BlockTerminator[]): Block | undefined {
2111
        let statements = [];
54✔
2112
        //attempt to get the next statement without using `this.declaration`
2113
        //which seems a bit hackish to get to work properly
2114
        let statement = this.statement();
54✔
2115
        if (!statement) {
54!
2116
            return undefined;
×
2117
        }
2118
        statements.push(statement);
54✔
2119
        const startingRange = statement.range;
54✔
2120

2121
        //look for colon statement separator
2122
        let foundColon = false;
54✔
2123
        while (this.match(TokenKind.Colon)) {
54✔
2124
            foundColon = true;
12✔
2125
        }
2126

2127
        //if a colon was found, add the next statement or err if unexpected
2128
        if (foundColon) {
54✔
2129
            if (!this.checkAny(TokenKind.Newline, ...additionalTerminators)) {
12✔
2130
                //if not an ending keyword, add next statement
2131
                let extra = this.inlineConditionalBranch(...additionalTerminators);
7✔
2132
                if (!extra) {
7!
2133
                    return undefined;
×
2134
                }
2135
                statements.push(...extra.statements);
7✔
2136
            } else {
2137
                //error: colon before next keyword
2138
                const colon = this.previous();
5✔
2139
                this.diagnostics.push({
5✔
2140
                    ...DiagnosticMessages.unexpectedToken(colon.text),
2141
                    range: colon.range
2142
                });
2143
            }
2144
        }
2145
        return new Block(statements, startingRange);
54✔
2146
    }
2147

2148
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2149
        let expressionStart = this.peek();
371✔
2150

2151
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
371✔
2152
            let operator = this.advance();
20✔
2153

2154
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
20✔
2155
                this.diagnostics.push({
1✔
2156
                    ...DiagnosticMessages.consecutiveIncrementDecrementOperatorsAreNotAllowed(),
2157
                    range: this.peek().range
2158
                });
2159
                throw this.lastDiagnosticAsError();
1✔
2160
            } else if (isCallExpression(expr)) {
19✔
2161
                this.diagnostics.push({
1✔
2162
                    ...DiagnosticMessages.incrementDecrementOperatorsAreNotAllowedAsResultOfFunctionCall(),
2163
                    range: expressionStart.range
2164
                });
2165
                throw this.lastDiagnosticAsError();
1✔
2166
            }
2167

2168
            const result = new IncrementStatement(expr, operator);
18✔
2169
            this._references.expressions.add(result);
18✔
2170
            return result;
18✔
2171
        }
2172

2173
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
351✔
2174
            return new ExpressionStatement(expr);
282✔
2175
        }
2176

2177

2178
        //you're not allowed to do dottedGet or XmlAttrGet after a function call
2179
        if (isDottedGetExpression(expr)) {
69✔
2180
            this.diagnostics.push({
21✔
2181
                ...DiagnosticMessages.propAccessNotPermittedAfterFunctionCallInExpressionStatement('Property'),
2182
                range: util.createBoundingRange(expr.dot, expr.name)
2183
            });
2184
            //we can recover gracefully here even though it's invalid syntax
2185
            return new ExpressionStatement(expr);
21✔
2186

2187
            //you're not allowed to do indexedGet expressions after a function call
2188
        } else if (isIndexedGetExpression(expr)) {
48✔
2189
            this.diagnostics.push({
1✔
2190
                ...DiagnosticMessages.propAccessNotPermittedAfterFunctionCallInExpressionStatement('Index'),
2191
                range: util.createBoundingRange(expr.openingSquare, expr.index, expr.closingSquare)
2192
            });
2193
            //we can recover gracefully here even though it's invalid syntax
2194
            return new ExpressionStatement(expr);
1✔
2195
            //you're not allowed to do XmlAttrGet after a function call
2196
        } else if (isXmlAttributeGetExpression(expr)) {
47✔
2197
            this.diagnostics.push({
1✔
2198
                ...DiagnosticMessages.propAccessNotPermittedAfterFunctionCallInExpressionStatement('XML attribute'),
2199
                range: util.createBoundingRange(expr.at, expr.name)
2200
            });
2201
            //we can recover gracefully here even though it's invalid syntax
2202
            return new ExpressionStatement(expr);
1✔
2203
        }
2204

2205

2206
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an assignment
2207
        this.diagnostics.push({
46✔
2208
            ...DiagnosticMessages.expectedStatementOrFunctionCallButReceivedExpression(),
2209
            range: expressionStart.range
2210
        });
2211

2212
        throw this.lastDiagnosticAsError();
46✔
2213
    }
2214

2215
    private setStatement(): DottedSetStatement | IndexedSetStatement | ExpressionStatement | IncrementStatement | AssignmentStatement {
2216
        /**
2217
         * Attempts to find an expression-statement or an increment statement.
2218
         * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
2219
         * statements aren't valid expressions. They _do_ however fall under the same parsing
2220
         * priority as standalone function calls though, so we can parse them in the same way.
2221
         */
2222
        let expr = this.call();
691✔
2223
        if (this.checkAny(...AssignmentOperators) && !(isCallExpression(expr))) {
650✔
2224
            let left = expr;
282✔
2225
            let operator = this.advance();
282✔
2226
            let right = this.expression();
282✔
2227

2228
            // Create a dotted or indexed "set" based on the left-hand side's type
2229
            if (isIndexedGetExpression(left)) {
282✔
2230
                return new IndexedSetStatement(
40✔
2231
                    left.obj,
2232
                    left.index,
2233
                    operator.kind === TokenKind.Equal
2234
                        ? right
40✔
2235
                        : new BinaryExpression(left, operator, right),
2236
                    left.openingSquare,
2237
                    left.closingSquare,
2238
                    left.additionalIndexes,
2239
                    operator.kind === TokenKind.Equal
2240
                        ? operator
40✔
2241
                        : { kind: TokenKind.Equal, text: '=', range: operator.range }
2242
                );
2243
            } else if (isDottedGetExpression(left)) {
242✔
2244
                return new DottedSetStatement(
239✔
2245
                    left.obj,
2246
                    left.name,
2247
                    operator.kind === TokenKind.Equal
2248
                        ? right
239✔
2249
                        : new BinaryExpression(left, operator, right),
2250
                    left.dot,
2251
                    operator.kind === TokenKind.Equal
2252
                        ? operator
239✔
2253
                        : { kind: TokenKind.Equal, text: '=', range: operator.range }
2254
                );
2255
            }
2256
        }
2257
        return this.expressionStatement(expr);
371✔
2258
    }
2259

2260
    private printStatement(): PrintStatement {
2261
        let printKeyword = this.advance();
703✔
2262

2263
        let values: (
2264
            | Expression
2265
            | PrintSeparatorTab
2266
            | PrintSeparatorSpace)[] = [];
703✔
2267

2268
        while (!this.checkEndOfStatement()) {
703✔
2269
            if (this.check(TokenKind.Semicolon)) {
787✔
2270
                values.push(this.advance() as PrintSeparatorSpace);
20✔
2271
            } else if (this.check(TokenKind.Comma)) {
767✔
2272
                values.push(this.advance() as PrintSeparatorTab);
13✔
2273
            } else if (this.check(TokenKind.Else)) {
754✔
2274
                break; // inline branch
7✔
2275
            } else {
2276
                values.push(this.expression());
747✔
2277
            }
2278
        }
2279

2280
        //print statements can be empty, so look for empty print conditions
2281
        if (!values.length) {
702✔
2282
            let emptyStringLiteral = createStringLiteral('');
4✔
2283
            values.push(emptyStringLiteral);
4✔
2284
        }
2285

2286
        let last = values[values.length - 1];
702✔
2287
        if (isToken(last)) {
702✔
2288
            // TODO: error, expected value
2289
        }
2290

2291
        return new PrintStatement({ print: printKeyword }, values);
702✔
2292
    }
2293

2294
    /**
2295
     * Parses a return statement with an optional return value.
2296
     * @returns an AST representation of a return statement.
2297
     */
2298
    private returnStatement(): ReturnStatement {
2299
        let tokens = { return: this.previous() };
230✔
2300

2301
        if (this.checkEndOfStatement()) {
230✔
2302
            return new ReturnStatement(tokens);
14✔
2303
        }
2304

2305
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
216✔
2306
        return new ReturnStatement(tokens, toReturn);
215✔
2307
    }
2308

2309
    /**
2310
     * Parses a `label` statement
2311
     * @returns an AST representation of an `label` statement.
2312
     */
2313
    private labelStatement() {
2314
        let tokens = {
12✔
2315
            identifier: this.advance(),
2316
            colon: this.advance()
2317
        };
2318

2319
        //label must be alone on its line, this is probably not a label
2320
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
12✔
2321
            //rewind and cancel
2322
            this.current -= 2;
2✔
2323
            throw new CancelStatementError();
2✔
2324
        }
2325

2326
        return new LabelStatement(tokens);
10✔
2327
    }
2328

2329
    /**
2330
     * Parses a `continue` statement
2331
     */
2332
    private continueStatement() {
2333
        return new ContinueStatement({
12✔
2334
            continue: this.advance(),
2335
            loopType: this.tryConsume(
2336
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2337
                TokenKind.While, TokenKind.For
2338
            )
2339
        });
2340
    }
2341

2342
    /**
2343
     * Parses a `goto` statement
2344
     * @returns an AST representation of an `goto` statement.
2345
     */
2346
    private gotoStatement() {
2347
        let tokens = {
12✔
2348
            goto: this.advance(),
2349
            label: this.consume(
2350
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2351
                TokenKind.Identifier
2352
            )
2353
        };
2354

2355
        return new GotoStatement(tokens);
10✔
2356
    }
2357

2358
    /**
2359
     * Parses an `end` statement
2360
     * @returns an AST representation of an `end` statement.
2361
     */
2362
    private endStatement() {
2363
        let endTokens = { end: this.advance() };
8✔
2364

2365
        return new EndStatement(endTokens);
8✔
2366
    }
2367
    /**
2368
     * Parses a `stop` statement
2369
     * @returns an AST representation of a `stop` statement
2370
     */
2371
    private stopStatement() {
2372
        let tokens = { stop: this.advance() };
16✔
2373

2374
        return new StopStatement(tokens);
16✔
2375
    }
2376

2377
    /**
2378
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2379
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2380
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2381
     *                    ignored.
2382
     */
2383
    private block(...terminators: BlockTerminator[]): Block | undefined {
2384
        const parentAnnotations = this.enterAnnotationBlock();
2,222✔
2385

2386
        this.consumeStatementSeparators(true);
2,222✔
2387
        let startingToken = this.peek();
2,222✔
2388

2389
        const statements: Statement[] = [];
2,222✔
2390
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators)) {
2,222✔
2391
            //grab the location of the current token
2392
            let loopCurrent = this.current;
2,580✔
2393
            let dec = this.declaration();
2,580✔
2394
            if (dec) {
2,580✔
2395
                if (!isAnnotationExpression(dec)) {
2,510✔
2396
                    this.consumePendingAnnotations(dec);
2,503✔
2397
                    statements.push(dec);
2,503✔
2398
                }
2399

2400
                //ensure statement separator
2401
                this.consumeStatementSeparators();
2,510✔
2402

2403
            } else {
2404
                //something went wrong. reset to the top of the loop
2405
                this.current = loopCurrent;
70✔
2406

2407
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2408
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
70✔
2409

2410
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2411
                //but there's already an error in the file being parsed, so just leave this line here
2412
                this.advance();
70✔
2413

2414
                //consume potential separators
2415
                this.consumeStatementSeparators(true);
70✔
2416
            }
2417
        }
2418

2419
        if (this.isAtEnd()) {
2,222✔
2420
            return undefined;
5✔
2421
            // TODO: Figure out how to handle unterminated blocks well
2422
        } else if (terminators.length > 0) {
2,217✔
2423
            //did we hit end-sub / end-function while looking for some other terminator?
2424
            //if so, we need to restore the statement separator
2425
            let prev = this.previous().kind;
361✔
2426
            let peek = this.peek().kind;
361✔
2427
            if (
361✔
2428
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
726!
2429
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2430
            ) {
2431
                this.current--;
6✔
2432
            }
2433
        }
2434

2435
        this.exitAnnotationBlock(parentAnnotations);
2,217✔
2436
        return new Block(statements, startingToken.range);
2,217✔
2437
    }
2438

2439
    /**
2440
     * Attach pending annotations to the provided statement,
2441
     * and then reset the annotations array
2442
     */
2443
    consumePendingAnnotations(statement: Statement) {
2444
        if (this.pendingAnnotations.length) {
6,384✔
2445
            statement.annotations = this.pendingAnnotations;
45✔
2446
            this.pendingAnnotations = [];
45✔
2447
        }
2448
    }
2449

2450
    enterAnnotationBlock() {
2451
        const pending = this.pendingAnnotations;
5,426✔
2452
        this.pendingAnnotations = [];
5,426✔
2453
        return pending;
5,426✔
2454
    }
2455

2456
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2457
        // non consumed annotations are an error
2458
        if (this.pendingAnnotations.length) {
5,420✔
2459
            for (const annotation of this.pendingAnnotations) {
4✔
2460
                this.diagnostics.push({
6✔
2461
                    ...DiagnosticMessages.unusedAnnotation(),
2462
                    range: annotation.range
2463
                });
2464
            }
2465
        }
2466
        this.pendingAnnotations = parentAnnotations;
5,420✔
2467
    }
2468

2469
    private expression(findTypeCast = true): Expression {
4,284✔
2470
        let expression = this.anonymousFunction();
4,539✔
2471
        let asToken: Token;
2472
        let typeToken: Token;
2473
        if (findTypeCast) {
4,503✔
2474
            do {
4,248✔
2475
                if (this.check(TokenKind.As)) {
4,265✔
2476
                    this.warnIfNotBrighterScriptMode('type cast');
17✔
2477
                    // Check if this expression is wrapped in any type casts
2478
                    // allows for multiple casts:
2479
                    // myVal = foo() as dynamic as string
2480

2481
                    asToken = this.advance();
17✔
2482
                    typeToken = this.typeToken();
17✔
2483
                    if (asToken && typeToken) {
17!
2484
                        expression = new TypeCastExpression(expression, asToken, typeToken);
17✔
2485
                    }
2486
                } else {
2487
                    break;
4,248✔
2488
                }
2489

2490
            } while (asToken && typeToken);
34✔
2491
        }
2492
        this._references.expressions.add(expression);
4,503✔
2493
        return expression;
4,503✔
2494
    }
2495

2496
    private anonymousFunction(): Expression {
2497
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
4,539✔
2498
            const func = this.functionDeclaration(true);
81✔
2499
            //if there's an open paren after this, this is an IIFE
2500
            if (this.check(TokenKind.LeftParen)) {
81✔
2501
                return this.finishCall(this.advance(), func);
3✔
2502
            } else {
2503
                return func;
78✔
2504
            }
2505
        }
2506

2507
        let expr = this.boolean();
4,458✔
2508

2509
        if (this.check(TokenKind.Question)) {
4,422✔
2510
            return this.ternaryExpression(expr);
94✔
2511
        } else if (this.check(TokenKind.QuestionQuestion)) {
4,328✔
2512
            return this.nullCoalescingExpression(expr);
30✔
2513
        } else {
2514
            return expr;
4,298✔
2515
        }
2516
    }
2517

2518
    private boolean(): Expression {
2519
        let expr = this.relational();
4,458✔
2520

2521
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
4,422✔
2522
            let operator = this.previous();
28✔
2523
            let right = this.relational();
28✔
2524
            this.addExpressionsToReferences(expr, right);
28✔
2525
            expr = new BinaryExpression(expr, operator, right);
28✔
2526
        }
2527

2528
        return expr;
4,422✔
2529
    }
2530

2531
    private relational(): Expression {
2532
        let expr = this.additive();
4,502✔
2533

2534
        while (
4,466✔
2535
            this.matchAny(
2536
                TokenKind.Equal,
2537
                TokenKind.LessGreater,
2538
                TokenKind.Greater,
2539
                TokenKind.GreaterEqual,
2540
                TokenKind.Less,
2541
                TokenKind.LessEqual
2542
            )
2543
        ) {
2544
            let operator = this.previous();
150✔
2545
            let right = this.additive();
150✔
2546
            this.addExpressionsToReferences(expr, right);
150✔
2547
            expr = new BinaryExpression(expr, operator, right);
150✔
2548
        }
2549

2550
        return expr;
4,466✔
2551
    }
2552

2553
    private addExpressionsToReferences(...expressions: Expression[]) {
2554
        for (const expression of expressions) {
338✔
2555
            if (!isBinaryExpression(expression)) {
630✔
2556
                this.references.expressions.add(expression);
588✔
2557
            }
2558
        }
2559
    }
2560

2561
    // TODO: bitshift
2562

2563
    private additive(): Expression {
2564
        let expr = this.multiplicative();
4,652✔
2565

2566
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
4,616✔
2567
            let operator = this.previous();
87✔
2568
            let right = this.multiplicative();
87✔
2569
            this.addExpressionsToReferences(expr, right);
87✔
2570
            expr = new BinaryExpression(expr, operator, right);
87✔
2571
        }
2572

2573
        return expr;
4,616✔
2574
    }
2575

2576
    private multiplicative(): Expression {
2577
        let expr = this.exponential();
4,739✔
2578

2579
        while (this.matchAny(
4,703✔
2580
            TokenKind.Forwardslash,
2581
            TokenKind.Backslash,
2582
            TokenKind.Star,
2583
            TokenKind.Mod,
2584
            TokenKind.LeftShift,
2585
            TokenKind.RightShift
2586
        )) {
2587
            let operator = this.previous();
21✔
2588
            let right = this.exponential();
21✔
2589
            this.addExpressionsToReferences(expr, right);
21✔
2590
            expr = new BinaryExpression(expr, operator, right);
21✔
2591
        }
2592

2593
        return expr;
4,703✔
2594
    }
2595

2596
    private exponential(): Expression {
2597
        let expr = this.prefixUnary();
4,760✔
2598

2599
        while (this.match(TokenKind.Caret)) {
4,724✔
2600
            let operator = this.previous();
6✔
2601
            let right = this.prefixUnary();
6✔
2602
            this.addExpressionsToReferences(expr, right);
6✔
2603
            expr = new BinaryExpression(expr, operator, right);
6✔
2604
        }
2605

2606
        return expr;
4,724✔
2607
    }
2608

2609
    private prefixUnary(): Expression {
2610
        const nextKind = this.peek().kind;
4,788✔
2611
        if (nextKind === TokenKind.Not) {
4,788✔
2612
            this.current++; //advance
16✔
2613
            let operator = this.previous();
16✔
2614
            let right = this.relational();
16✔
2615
            return new UnaryExpression(operator, right);
16✔
2616
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
4,772✔
2617
            this.current++; //advance
22✔
2618
            let operator = this.previous();
22✔
2619
            let right = this.prefixUnary();
22✔
2620
            return new UnaryExpression(operator, right);
22✔
2621
        }
2622
        return this.call();
4,750✔
2623
    }
2624

2625
    private indexedGet(expr: Expression) {
2626
        let openingSquare = this.previous();
146✔
2627
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
146✔
2628
        let indexes: Expression[] = [];
146✔
2629

2630

2631
        //consume leading newlines
2632
        while (this.match(TokenKind.Newline)) { }
146✔
2633

2634
        try {
146✔
2635
            indexes.push(
146✔
2636
                this.expression()
2637
            );
2638
            //consume additional indexes separated by commas
2639
            while (this.check(TokenKind.Comma)) {
145✔
2640
                //discard the comma
2641
                this.advance();
17✔
2642
                indexes.push(
17✔
2643
                    this.expression()
2644
                );
2645
            }
2646
        } catch (error) {
2647
            this.rethrowNonDiagnosticError(error);
1✔
2648
        }
2649
        //consume trailing newlines
2650
        while (this.match(TokenKind.Newline)) { }
146✔
2651

2652
        const closingSquare = this.tryConsume(
146✔
2653
            DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
2654
            TokenKind.RightSquareBracket
2655
        );
2656

2657
        return new IndexedGetExpression(expr, indexes.shift(), openingSquare, closingSquare, questionDotToken, indexes);
146✔
2658
    }
2659

2660
    private newExpression() {
2661
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
44✔
2662
        let newToken = this.advance();
44✔
2663

2664
        let nameExpr = this.getNamespacedVariableNameExpression();
44✔
2665
        let leftParen = this.consume(
44✔
2666
            DiagnosticMessages.unexpectedToken(this.peek().text),
2667
            TokenKind.LeftParen,
2668
            TokenKind.QuestionLeftParen
2669
        );
2670
        let call = this.finishCall(leftParen, nameExpr);
40✔
2671
        //pop the call from the  callExpressions list because this is technically something else
2672
        this.callExpressions.pop();
40✔
2673
        let result = new NewExpression(newToken, call);
40✔
2674
        this._references.newExpressions.push(result);
40✔
2675
        return result;
40✔
2676
    }
2677

2678
    /**
2679
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2680
     */
2681
    private callfunc(callee: Expression): Expression {
2682
        this.warnIfNotBrighterScriptMode('callfunc operator');
25✔
2683
        let operator = this.previous();
25✔
2684
        let methodName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
25✔
2685
        // force it into an identifier so the AST makes some sense
2686
        methodName.kind = TokenKind.Identifier;
24✔
2687
        let openParen = this.consume(DiagnosticMessages.expectedOpenParenToFollowCallfuncIdentifier(), TokenKind.LeftParen);
24✔
2688
        let call = this.finishCall(openParen, callee, false);
24✔
2689

2690
        return new CallfuncExpression(callee, operator, methodName as Identifier, openParen, call.args, call.closingParen);
24✔
2691
    }
2692

2693
    private call(): Expression {
2694
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
5,441✔
2695
            return this.newExpression();
44✔
2696
        }
2697
        let expr = this.primary();
5,397✔
2698
        //an expression to keep for _references
2699
        let referenceCallExpression: Expression;
2700
        while (true) {
5,325✔
2701
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
7,081✔
2702
                expr = this.finishCall(this.previous(), expr);
587✔
2703
                //store this call expression in references
2704
                referenceCallExpression = expr;
587✔
2705

2706
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
6,494✔
2707
                expr = this.indexedGet(expr);
144✔
2708

2709
            } else if (this.match(TokenKind.Callfunc)) {
6,350✔
2710
                expr = this.callfunc(expr);
25✔
2711
                //store this callfunc expression in references
2712
                referenceCallExpression = expr;
24✔
2713

2714
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
6,325✔
2715
                if (this.match(TokenKind.LeftSquareBracket)) {
1,025✔
2716
                    expr = this.indexedGet(expr);
2✔
2717
                } else {
2718
                    let dot = this.previous();
1,023✔
2719
                    let name = this.tryConsume(
1,023✔
2720
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2721
                        TokenKind.Identifier,
2722
                        ...AllowedProperties
2723
                    );
2724
                    if (!name) {
1,023✔
2725
                        break;
24✔
2726
                    }
2727

2728
                    // force it into an identifier so the AST makes some sense
2729
                    name.kind = TokenKind.Identifier;
999✔
2730
                    expr = new DottedGetExpression(expr, name as Identifier, dot);
999✔
2731

2732
                    this.addPropertyHints(name);
999✔
2733
                }
2734

2735
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
5,300✔
2736
                let dot = this.advance();
12✔
2737
                let name = this.tryConsume(
12✔
2738
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2739
                    TokenKind.Identifier,
2740
                    ...AllowedProperties
2741
                );
2742

2743
                // force it into an identifier so the AST makes some sense
2744
                name.kind = TokenKind.Identifier;
12✔
2745
                if (!name) {
12!
2746
                    break;
×
2747
                }
2748
                expr = new XmlAttributeGetExpression(expr, name as Identifier, dot);
12✔
2749
                //only allow a single `@` expression
2750
                break;
12✔
2751

2752
            } else {
2753
                break;
5,288✔
2754
            }
2755
        }
2756
        //if we found a callExpression, add it to `expressions` in references
2757
        if (referenceCallExpression) {
5,324✔
2758
            this._references.expressions.add(referenceCallExpression);
573✔
2759
        }
2760
        return expr;
5,324✔
2761
    }
2762

2763
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
630✔
2764
        let args = [] as Expression[];
678✔
2765
        while (this.match(TokenKind.Newline)) { }
678✔
2766

2767
        if (!this.check(TokenKind.RightParen)) {
678✔
2768
            do {
350✔
2769
                while (this.match(TokenKind.Newline)) { }
526✔
2770

2771
                if (args.length >= CallExpression.MaximumArguments) {
526!
2772
                    this.diagnostics.push({
×
2773
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
2774
                        range: this.peek().range
2775
                    });
2776
                    throw this.lastDiagnosticAsError();
×
2777
                }
2778
                try {
526✔
2779
                    args.push(this.expression());
526✔
2780
                } catch (error) {
2781
                    this.rethrowNonDiagnosticError(error);
5✔
2782
                    // we were unable to get an expression, so don't continue
2783
                    break;
5✔
2784
                }
2785
            } while (this.match(TokenKind.Comma));
2786
        }
2787

2788
        while (this.match(TokenKind.Newline)) { }
678✔
2789

2790
        const closingParen = this.tryConsume(
678✔
2791
            DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
2792
            TokenKind.RightParen
2793
        );
2794

2795
        let expression = new CallExpression(callee, openingParen, closingParen, args);
678✔
2796
        if (addToCallExpressionList) {
678✔
2797
            this.callExpressions.push(expression);
630✔
2798
        }
2799
        return expression;
678✔
2800
    }
2801

2802
    /**
2803
     * Tries to get the next token as a type
2804
     * Allows for built-in types (double, string, etc.) or namespaced custom types in Brighterscript mode
2805
     * Will return a token of whatever is next to be parsed
2806
     * Will allow v1 type syntax (typed arrays, union types), but there is no validation on types used this way
2807
     */
2808
    private typeToken(ignoreDiagnostics = false): Token {
760✔
2809
        let typeToken: Token;
2810
        let lookForCompounds = true;
765✔
2811
        let isACompound = false;
765✔
2812
        let resultToken;
2813
        while (lookForCompounds) {
765✔
2814
            lookForCompounds = false;
815✔
2815

2816
            const isTypedFunction = this.checkAny(TokenKind.Function, TokenKind.Sub) && this.checkNext(TokenKind.LeftParen);
815✔
2817

2818
            if (this.checkAny(...DeclarableTypes) && !isTypedFunction) {
815✔
2819
                // Token is a built in type
2820
                typeToken = this.advance();
628✔
2821
            } else if (this.options.mode === ParseMode.BrighterScript) {
187✔
2822
                try {
159✔
2823
                    if (this.check(TokenKind.LeftCurlyBrace)) {
159✔
2824
                        // could be an inline interface
2825
                        typeToken = this.inlineInterface();
20✔
2826
                    } else if (this.check(TokenKind.LeftParen)) {
139✔
2827
                        // could be an inline interface
2828
                        typeToken = this.groupedTypeExpression();
12✔
2829
                    } else if (isTypedFunction) {
127✔
2830
                        //typed function type
2831
                        typeToken = this.typedFunctionType();
10✔
2832
                    } else {
2833
                        // see if we can get a namespaced identifer
2834
                        const qualifiedType = this.getNamespacedVariableNameExpression(ignoreDiagnostics);
117✔
2835
                        typeToken = createToken(TokenKind.Identifier, qualifiedType.getName(this.options.mode), qualifiedType.range);
111✔
2836
                    }
2837
                } catch {
2838
                    //could not get an identifier - just get whatever's next
2839
                    typeToken = this.advance();
6✔
2840
                }
2841
            } else {
2842
                // just get whatever's next
2843
                typeToken = this.advance();
28✔
2844
            }
2845
            resultToken = resultToken ?? typeToken;
815✔
2846
            if (resultToken && this.options.mode === ParseMode.BrighterScript) {
815✔
2847
                // check for brackets for typed arrays
2848
                while (this.check(TokenKind.LeftSquareBracket) && this.peekNext().kind === TokenKind.RightSquareBracket) {
697✔
2849
                    const leftBracket = this.advance();
16✔
2850
                    const rightBracket = this.advance();
16✔
2851
                    typeToken = createToken(TokenKind.Identifier, typeToken.text + leftBracket.text + rightBracket.text, util.createBoundingRange(typeToken, leftBracket, rightBracket));
16✔
2852
                    resultToken = createToken(TokenKind.Dynamic, null, typeToken.range);
16✔
2853
                }
2854

2855
                if (this.checkAny(TokenKind.Or, TokenKind.And)) {
697✔
2856
                    lookForCompounds = true;
50✔
2857
                    let orToken = this.advance();
50✔
2858
                    resultToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(resultToken, typeToken, orToken));
50✔
2859
                    isACompound = true;
50✔
2860
                }
2861
            }
2862
        }
2863
        if (isACompound) {
765✔
2864
            resultToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(resultToken, typeToken));
43✔
2865
        }
2866
        return resultToken;
765✔
2867
    }
2868

2869
    private inlineInterface() {
2870
        const openToken = this.advance();
20✔
2871
        const memberTokens: Token[] = [];
20✔
2872
        memberTokens.push(openToken);
20✔
2873
        while (this.matchAny(TokenKind.Newline, TokenKind.Comment)) { }
20✔
2874
        while (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral, TokenKind.Optional)) {
20✔
2875
            let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
26✔
2876
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral)) {
26!
2877
                if (this.check(TokenKind.As)) {
26!
2878
                    if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
×
2879
                        // as <EOL>
2880
                        // `as` is the field name
2881
                    } else if (this.checkNext(TokenKind.As)) {
×
2882
                        //  as as ____
2883
                        // first `as` is the field name
2884
                    } else if (optionalKeyword) {
×
2885
                        // optional as ____
2886
                        // optional is the field name, `as` starts type
2887
                        // rewind current token
2888
                        optionalKeyword = null;
×
2889
                        this.current--;
×
2890
                    }
2891
                }
2892
            } else {
2893
                // no name after `optional` ... optional is the name
2894
                // rewind current token
2895
                optionalKeyword = null;
×
2896
                this.current--;
×
2897
            }
2898
            if (optionalKeyword) {
26✔
2899
                memberTokens.push(optionalKeyword);
1✔
2900
            }
2901
            if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers, TokenKind.StringLiteral)) {
26!
2902
                this.diagnostics.push({
×
2903
                    ...DiagnosticMessages.unexpectedToken(this.peek().text),
2904
                    range: this.peek().range
2905
                });
2906
                throw this.lastDiagnosticAsError();
×
2907
            }
2908
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral)) {
26!
2909
                this.advance();
26✔
2910
            } else {
2911
                this.diagnostics.push({
×
2912
                    ...DiagnosticMessages.unexpectedToken(this.peek().text),
2913
                    range: this.peek().range
2914
                });
2915
                throw this.lastDiagnosticAsError();
×
2916
            }
2917

2918
            if (this.check(TokenKind.As)) {
26✔
2919
                memberTokens.push(this.advance()); // as
24✔
2920
                memberTokens.push(this.typeToken()); // type
24✔
2921
            }
2922
            while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) { }
26✔
2923
        }
2924
        if (!this.check(TokenKind.RightCurlyBrace)) {
20!
2925
            this.diagnostics.push({
×
2926
                ...DiagnosticMessages.unexpectedToken(this.peek().text),
2927
                range: this.peek().range
2928
            });
2929
            throw this.lastDiagnosticAsError();
×
2930
        }
2931
        const closeToken = this.advance();
20✔
2932
        memberTokens.push(closeToken);
20✔
2933

2934
        const completeInlineInterfaceToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(...memberTokens));
20✔
2935

2936
        return completeInlineInterfaceToken;
20✔
2937
    }
2938

2939
    private groupedTypeExpression() {
2940
        const leftParen = this.advance();
12✔
2941
        const typeToken = this.typeToken();
12✔
2942
        const rightParen = this.consume(
12✔
2943
            DiagnosticMessages.expectedToken(TokenKind.RightParen),
2944
            TokenKind.RightParen
2945
        );
2946
        return createToken(TokenKind.Dynamic, null, util.createBoundingRange(leftParen, typeToken, rightParen));
12✔
2947
    }
2948

2949
    private typedFunctionType() {
2950
        const funcOrSub = this.advance();
10✔
2951
        const leftParen = this.advance();
10✔
2952

2953
        let params = [] as FunctionParameterExpression[];
10✔
2954
        if (!this.check(TokenKind.RightParen)) {
10✔
2955
            do {
4✔
2956
                params.push(this.functionParameter());
7✔
2957
            } while (this.match(TokenKind.Comma));
2958
        }
2959
        const rightParen = this.advance();
10✔
2960
        let asToken: Token;
2961
        let returnType: Token;
2962
        if ((this.check(TokenKind.As))) {
10✔
2963
            // this is a function type with a return type, e.g. `function(string) as void`
2964
            asToken = this.advance();
9✔
2965
            returnType = this.typeToken();
9✔
2966
        }
2967

2968
        return createToken(TokenKind.Function, null, util.createBoundingRange(funcOrSub, leftParen, rightParen, asToken, returnType));
10✔
2969
    }
2970

2971
    private primary(): Expression {
2972
        switch (true) {
5,397✔
2973
            case this.matchAny(
5,397!
2974
                TokenKind.False,
2975
                TokenKind.True,
2976
                TokenKind.Invalid,
2977
                TokenKind.IntegerLiteral,
2978
                TokenKind.LongIntegerLiteral,
2979
                TokenKind.FloatLiteral,
2980
                TokenKind.DoubleLiteral,
2981
                TokenKind.StringLiteral
2982
            ):
2983
                return new LiteralExpression(this.previous());
3,168✔
2984

2985
            //capture source literals (LINE_NUM if brightscript, or a bunch of them if brighterscript)
2986
            case this.matchAny(TokenKind.LineNumLiteral, ...(this.options.mode === ParseMode.BrightScript ? [] : BrighterScriptSourceLiterals)):
2,229✔
2987
                return new SourceLiteralExpression(this.previous());
35✔
2988

2989
            //template string
2990
            case this.check(TokenKind.BackTick):
2991
                return this.templateString(false);
47✔
2992

2993
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
2994
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
3,780✔
2995
                return this.templateString(true);
8✔
2996

2997
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
2998
                return new VariableExpression(this.previous() as Identifier);
1,632✔
2999

3000
            case this.match(TokenKind.LeftParen):
3001
                let left = this.previous();
38✔
3002
                let expr = this.expression();
38✔
3003
                let right = this.consume(
37✔
3004
                    DiagnosticMessages.unmatchedLeftParenAfterExpression(),
3005
                    TokenKind.RightParen
3006
                );
3007
                return new GroupingExpression({ left: left, right: right }, expr);
37✔
3008

3009
            case this.matchAny(TokenKind.LeftSquareBracket):
3010
                return this.arrayLiteral();
129✔
3011

3012
            case this.match(TokenKind.LeftCurlyBrace):
3013
                return this.aaLiteral();
223✔
3014

3015
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
3016
                let token = Object.assign(this.previous(), {
×
3017
                    kind: TokenKind.Identifier
3018
                }) as Identifier;
3019
                return new VariableExpression(token);
×
3020

3021
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
3022
                return this.anonymousFunction();
×
3023

3024
            case this.check(TokenKind.RegexLiteral):
3025
                return this.regexLiteralExpression();
45✔
3026

3027
            case this.check(TokenKind.Comment):
3028
                return new CommentStatement([this.advance()]);
3✔
3029

3030
            default:
3031
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
3032
                if (this.checkAny(...this.peekGlobalTerminators())) {
69!
3033
                    //don't throw a diagnostic, just return undefined
3034

3035
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
3036
                } else {
3037
                    this.diagnostics.push({
69✔
3038
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
3039
                        range: this.peek().range
3040
                    });
3041
                    throw this.lastDiagnosticAsError();
69✔
3042
                }
3043
        }
3044
    }
3045

3046
    private arrayLiteral() {
3047
        let elements: Array<Expression | CommentStatement> = [];
129✔
3048
        let openingSquare = this.previous();
129✔
3049

3050
        //add any comment found right after the opening square
3051
        if (this.check(TokenKind.Comment)) {
129✔
3052
            elements.push(new CommentStatement([this.advance()]));
1✔
3053
        }
3054

3055
        while (this.match(TokenKind.Newline)) {
129✔
3056
        }
3057
        let closingSquare: Token;
3058

3059
        if (!this.match(TokenKind.RightSquareBracket)) {
129✔
3060
            try {
98✔
3061
                elements.push(this.expression());
98✔
3062

3063
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
97✔
3064
                    if (this.checkPrevious(TokenKind.Comment) || this.check(TokenKind.Comment)) {
144✔
3065
                        let comment = this.check(TokenKind.Comment) ? this.advance() : this.previous();
4✔
3066
                        elements.push(new CommentStatement([comment]));
4✔
3067
                    }
3068
                    while (this.match(TokenKind.Newline)) {
144✔
3069

3070
                    }
3071

3072
                    if (this.check(TokenKind.RightSquareBracket)) {
144✔
3073
                        break;
32✔
3074
                    }
3075

3076
                    elements.push(this.expression());
112✔
3077
                }
3078
            } catch (error: any) {
3079
                this.rethrowNonDiagnosticError(error);
2✔
3080
            }
3081

3082
            closingSquare = this.tryConsume(
98✔
3083
                DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral(),
3084
                TokenKind.RightSquareBracket
3085
            );
3086
        } else {
3087
            closingSquare = this.previous();
31✔
3088
        }
3089

3090
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3091
        return new ArrayLiteralExpression(elements, openingSquare, closingSquare);
129✔
3092
    }
3093

3094
    private aaLiteral() {
3095
        let openingBrace = this.previous();
223✔
3096
        let members: Array<AAMemberExpression | CommentStatement> = [];
223✔
3097

3098
        let key = () => {
223✔
3099
            let result = {
233✔
3100
                colonToken: null as Token,
3101
                keyToken: null as Token,
3102
                keyExpr: null as Expression,
3103
                openBracketToken: null as Token,
3104
                closeBracketToken: null as Token,
3105
                range: null as Range
3106
            };
3107
            if (this.check(TokenKind.LeftSquareBracket)) {
233✔
3108
                // Computed key: [expr]
3109
                result.openBracketToken = this.advance();
17✔
3110
                result.keyExpr = this.expression();
17✔
3111
                result.closeBracketToken = this.tryConsume(
17✔
3112
                    DiagnosticMessages.expectedRightSquareBracketAfterAAComputedKey(),
3113
                    TokenKind.RightSquareBracket
3114
                );
3115
            } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
216✔
3116
                result.keyToken = this.identifier(...AllowedProperties);
185✔
3117
            } else if (this.check(TokenKind.StringLiteral)) {
31!
3118
                result.keyToken = this.advance();
31✔
3119
            } else {
3120
                this.diagnostics.push({
×
3121
                    ...DiagnosticMessages.unexpectedAAKey(),
3122
                    range: this.peek().range
3123
                });
3124
                throw this.lastDiagnosticAsError();
×
3125
            }
3126

3127
            result.colonToken = this.consume(
233✔
3128
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3129
                TokenKind.Colon
3130
            );
3131
            result.range = util.getRange(result.keyToken ?? result.openBracketToken, result.colonToken);
232✔
3132
            return result;
232✔
3133
        };
3134

3135
        while (this.match(TokenKind.Newline)) { }
223✔
3136
        let closingBrace: Token;
3137
        if (!this.match(TokenKind.RightCurlyBrace)) {
223✔
3138
            let lastAAMember: AAMemberExpression;
3139
            try {
175✔
3140
                if (this.check(TokenKind.Comment)) {
175✔
3141
                    lastAAMember = null;
7✔
3142
                    members.push(new CommentStatement([this.advance()]));
7✔
3143
                } else {
3144
                    let k = key();
168✔
3145
                    let expr = this.expression();
168✔
3146
                    lastAAMember = new AAMemberExpression(
167✔
3147
                        k.keyToken,
3148
                        k.colonToken,
3149
                        expr,
3150
                        k.keyExpr,
3151
                        k.openBracketToken,
3152
                        k.closeBracketToken
3153
                    );
3154
                    members.push(lastAAMember);
167✔
3155
                }
3156

3157
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
174✔
3158
                    // collect comma at end of expression
3159
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
215✔
3160
                        lastAAMember.commaToken = this.previous();
42✔
3161
                    }
3162

3163
                    //check for comment at the end of the current line
3164
                    if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
215✔
3165
                        let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
14✔
3166
                        members.push(new CommentStatement([token]));
14✔
3167
                    } else {
3168
                        this.consumeStatementSeparators(true);
201✔
3169

3170
                        //check for a comment on its own line
3171
                        if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
201✔
3172
                            let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
1!
3173
                            lastAAMember = null;
1✔
3174
                            members.push(new CommentStatement([token]));
1✔
3175
                            continue;
1✔
3176
                        }
3177

3178
                        if (this.check(TokenKind.RightCurlyBrace)) {
200✔
3179
                            break;
135✔
3180
                        }
3181
                        let k = key();
65✔
3182
                        let expr = this.expression();
64✔
3183
                        lastAAMember = new AAMemberExpression(
64✔
3184
                            k.keyToken,
3185
                            k.colonToken,
3186
                            expr,
3187
                            k.keyExpr,
3188
                            k.openBracketToken,
3189
                            k.closeBracketToken
3190
                        );
3191
                        members.push(lastAAMember);
64✔
3192
                    }
3193
                }
3194
            } catch (error: any) {
3195
                this.rethrowNonDiagnosticError(error);
2✔
3196
            }
3197

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

3206
        const aaExpr = new AALiteralExpression(members, openingBrace, closingBrace);
223✔
3207
        this.addPropertyHints(aaExpr);
223✔
3208
        return aaExpr;
223✔
3209
    }
3210

3211
    /**
3212
     * Pop token if we encounter specified token
3213
     */
3214
    private match(tokenKind: TokenKind) {
3215
        if (this.check(tokenKind)) {
21,303✔
3216
            this.current++; //advance
1,640✔
3217
            return true;
1,640✔
3218
        }
3219
        return false;
19,663✔
3220
    }
3221

3222
    /**
3223
     * Pop token if we encounter a token in the specified list
3224
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3225
     */
3226
    private matchAny(...tokenKinds: TokenKind[]) {
3227
        for (let tokenKind of tokenKinds) {
74,189✔
3228
            if (this.check(tokenKind)) {
219,573✔
3229
                this.current++; //advance
18,763✔
3230
                return true;
18,763✔
3231
            }
3232
        }
3233
        return false;
55,426✔
3234
    }
3235

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

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

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

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

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

3288
        if (foundTokenKind) {
10,910✔
3289
            return this.advance();
10,817✔
3290
        }
3291
        this.diagnostics.push({
93✔
3292
            ...diagnostic,
3293
            range: this.peek().range
3294
        });
3295
    }
3296

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

3304
    private consumeStatementSeparators(optional = false) {
4,109✔
3305
        //a comment or EOF mark the end of the statement
3306
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
14,133✔
3307
            return true;
596✔
3308
        }
3309
        let consumed = false;
13,537✔
3310
        //consume any newlines and colons
3311
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
13,537✔
3312
            consumed = true;
11,387✔
3313
        }
3314
        if (!optional && !consumed) {
13,537✔
3315
            this.diagnostics.push({
38✔
3316
                ...DiagnosticMessages.expectedNewlineOrColon(),
3317
                range: this.peek().range
3318
            });
3319
        }
3320
        return consumed;
13,537✔
3321
    }
3322

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

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

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

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

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

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

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

3375
    private isAtEnd(): boolean {
3376
        return this.peek().kind === TokenKind.Eof;
67,736✔
3377
    }
3378

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

3386
    private peek(): Token {
3387
        return this.tokens[this.current];
512,917✔
3388
    }
3389

3390
    private previous(): Token {
3391
        return this.tokens[this.current - 1];
35,033✔
3392
    }
3393

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

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

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

3426
        while (!this.isAtEnd()) {
117✔
3427
            if (this.ensureNewLineOrColon(true)) {
208✔
3428
                // end of statement reached
3429
                return;
80✔
3430
            }
3431

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

3448
            this.advance();
127✔
3449
        }
3450
    }
3451

3452
    /**
3453
     * References are found during the initial parse.
3454
     * However, sometimes plugins can modify the AST, requiring a full walk to re-compute all references.
3455
     * This does that walk.
3456
     */
3457
    private findReferences() {
3458
        this._references = new References();
7✔
3459
        const excludedExpressions = new Set<Expression>();
7✔
3460

3461
        const visitCallExpression = (e: CallExpression | CallfuncExpression) => {
7✔
3462
            for (const p of e.args) {
14✔
3463
                this._references.expressions.add(p);
7✔
3464
            }
3465
            //add calls that were not excluded (from loop below)
3466
            if (!excludedExpressions.has(e)) {
14✔
3467
                this._references.expressions.add(e);
12✔
3468
            }
3469

3470
            //if this call is part of a longer expression that includes a call higher up, find that higher one and remove it
3471
            if (e.callee) {
14!
3472
                let node: Expression = e.callee;
14✔
3473
                while (node) {
14✔
3474
                    //the primary goal for this loop. If we found a parent call expression, remove it from `references`
3475
                    if (isCallExpression(node)) {
22✔
3476
                        this.references.expressions.delete(node);
2✔
3477
                        excludedExpressions.add(node);
2✔
3478
                        //stop here. even if there are multiple calls in the chain, each child will find and remove its closest parent, so that reduces excess walking.
3479
                        break;
2✔
3480

3481
                        //when we hit a variable expression, we're definitely at the leftmost expression so stop
3482
                    } else if (isVariableExpression(node)) {
20✔
3483
                        break;
12✔
3484
                        //if
3485

3486
                    } else if (isDottedGetExpression(node) || isIndexedGetExpression(node)) {
8!
3487
                        node = node.obj;
8✔
3488
                    } else {
3489
                        //some expression we don't understand. log it and quit the loop
3490
                        this.logger.info('Encountered unknown expression while calculating function expression chain', node);
×
3491
                        break;
×
3492
                    }
3493
                }
3494
            }
3495
        };
3496

3497
        this.ast.walk(createVisitor({
7✔
3498
            AssignmentStatement: s => {
3499
                this._references.assignmentStatements.push(s);
11✔
3500
                this.references.expressions.add(s.value);
11✔
3501
            },
3502
            ClassStatement: s => {
3503
                this._references.classStatements.push(s);
1✔
3504
            },
3505
            ClassFieldStatement: s => {
3506
                if (s.initialValue) {
1!
3507
                    this._references.expressions.add(s.initialValue);
1✔
3508
                }
3509
            },
3510
            NamespaceStatement: s => {
3511
                this._references.namespaceStatements.push(s);
×
3512
            },
3513
            FunctionStatement: s => {
3514
                this._references.functionStatements.push(s);
4✔
3515
            },
3516
            ImportStatement: s => {
3517
                this._references.importStatements.push(s);
1✔
3518
            },
3519
            LibraryStatement: s => {
3520
                this._references.libraryStatements.push(s);
×
3521
            },
3522
            FunctionExpression: (expression, parent) => {
3523
                if (!isMethodStatement(parent)) {
4!
3524
                    this._references.functionExpressions.push(expression);
4✔
3525
                }
3526
            },
3527
            NewExpression: e => {
3528
                this._references.newExpressions.push(e);
×
3529
                for (const p of e.call.args) {
×
3530
                    this._references.expressions.add(p);
×
3531
                }
3532
            },
3533
            ExpressionStatement: s => {
3534
                this._references.expressions.add(s.expression);
7✔
3535
            },
3536
            CallfuncExpression: e => {
3537
                visitCallExpression(e);
1✔
3538
            },
3539
            CallExpression: e => {
3540
                visitCallExpression(e);
13✔
3541
            },
3542
            AALiteralExpression: e => {
3543
                this.addPropertyHints(e);
8✔
3544
                this._references.expressions.add(e);
8✔
3545
                for (const member of e.elements) {
8✔
3546
                    if (isAAMemberExpression(member)) {
16!
3547
                        this._references.expressions.add(member.value);
16✔
3548
                        if (member.keyExpr) {
16!
NEW
3549
                            this._references.expressions.add(member.keyExpr);
×
3550
                        }
3551
                    }
3552
                }
3553
            },
3554
            BinaryExpression: (e, parent) => {
3555
                //walk the chain of binary expressions and add each one to the list of expressions
3556
                const expressions: Expression[] = [e];
14✔
3557
                let expression: Expression;
3558
                while ((expression = expressions.pop())) {
14✔
3559
                    if (isBinaryExpression(expression)) {
64✔
3560
                        expressions.push(expression.left, expression.right);
25✔
3561
                    } else {
3562
                        this._references.expressions.add(expression);
39✔
3563
                    }
3564
                }
3565
            },
3566
            ArrayLiteralExpression: e => {
3567
                for (const element of e.elements) {
1✔
3568
                    //keep everything except comments
3569
                    if (!isCommentStatement(element)) {
1!
3570
                        this._references.expressions.add(element);
1✔
3571
                    }
3572
                }
3573
            },
3574
            DottedGetExpression: e => {
3575
                this.addPropertyHints(e.name);
23✔
3576
            },
3577
            DottedSetStatement: e => {
3578
                this.addPropertyHints(e.name);
4✔
3579
            },
3580
            EnumStatement: e => {
3581
                this._references.enumStatements.push(e);
×
3582
            },
3583
            ConstStatement: s => {
3584
                this._references.constStatements.push(s);
×
3585
            },
3586
            UnaryExpression: e => {
3587
                this._references.expressions.add(e);
×
3588
            },
3589
            IncrementStatement: e => {
3590
                this._references.expressions.add(e);
2✔
3591
            },
3592
            TypeStatement: (s) => {
3593
                this._references.typeStatements.push(s);
×
3594
            }
3595
        }), {
3596
            walkMode: WalkMode.visitAllRecursive
3597
        });
3598
    }
3599

3600
    public dispose() {
3601
    }
3602
}
3603

3604
export enum ParseMode {
1✔
3605
    BrightScript = 'BrightScript',
1✔
3606
    BrighterScript = 'BrighterScript'
1✔
3607
}
3608

3609
export interface ParseOptions {
3610
    /**
3611
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3612
     */
3613
    mode?: ParseMode;
3614
    /**
3615
     * A logger that should be used for logging. If omitted, a default logger is used
3616
     */
3617
    logger?: Logger;
3618
    /**
3619
     * Should locations be tracked. If false, the `range` property will be omitted
3620
     * @default true
3621
     */
3622
    trackLocations?: boolean;
3623
}
3624

3625
export class References {
1✔
3626
    private cache = new Cache();
2,295✔
3627
    public assignmentStatements = [] as AssignmentStatement[];
2,295✔
3628
    public classStatements = [] as ClassStatement[];
2,295✔
3629

3630
    public get classStatementLookup() {
3631
        if (!this._classStatementLookup) {
17✔
3632
            this._classStatementLookup = new Map();
15✔
3633
            for (const stmt of this.classStatements) {
15✔
3634
                this._classStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
2✔
3635
            }
3636
        }
3637
        return this._classStatementLookup;
17✔
3638
    }
3639
    private _classStatementLookup: Map<string, ClassStatement>;
3640

3641
    public functionExpressions = [] as FunctionExpression[];
2,295✔
3642
    public functionStatements = [] as FunctionStatement[];
2,295✔
3643
    /**
3644
     * A map of function statements, indexed by fully-namespaced lower function name.
3645
     */
3646
    public get functionStatementLookup() {
3647
        if (!this._functionStatementLookup) {
17✔
3648
            this._functionStatementLookup = new Map();
15✔
3649
            for (const stmt of this.functionStatements) {
15✔
3650
                this._functionStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
13✔
3651
            }
3652
        }
3653
        return this._functionStatementLookup;
17✔
3654
    }
3655
    private _functionStatementLookup: Map<string, FunctionStatement>;
3656

3657
    public interfaceStatements = [] as InterfaceStatement[];
2,295✔
3658

3659
    public get interfaceStatementLookup() {
3660
        if (!this._interfaceStatementLookup) {
×
3661
            this._interfaceStatementLookup = new Map();
×
3662
            for (const stmt of this.interfaceStatements) {
×
3663
                this._interfaceStatementLookup.set(stmt.fullName.toLowerCase(), stmt);
×
3664
            }
3665
        }
3666
        return this._interfaceStatementLookup;
×
3667
    }
3668
    private _interfaceStatementLookup: Map<string, InterfaceStatement>;
3669

3670
    public enumStatements = [] as EnumStatement[];
2,295✔
3671

3672
    public get enumStatementLookup() {
3673
        return this.cache.getOrAdd('enums', () => {
18✔
3674
            const result = new Map<string, EnumStatement>();
16✔
3675
            for (const stmt of this.enumStatements) {
16✔
3676
                result.set(stmt.fullName.toLowerCase(), stmt);
1✔
3677
            }
3678
            return result;
16✔
3679
        });
3680
    }
3681

3682
    public constStatements = [] as ConstStatement[];
2,295✔
3683

3684
    public get constStatementLookup() {
3685
        return this.cache.getOrAdd('consts', () => {
×
3686
            const result = new Map<string, ConstStatement>();
×
3687
            for (const stmt of this.constStatements) {
×
3688
                result.set(stmt.fullName.toLowerCase(), stmt);
×
3689
            }
3690
            return result;
×
3691
        });
3692
    }
3693

3694
    /**
3695
     * A collection of full expressions. This excludes intermediary expressions.
3696
     *
3697
     * Example 1:
3698
     * `a.b.c` is composed of `a` (variableExpression)  `.b` (DottedGetExpression) `.c` (DottedGetExpression)
3699
     * This will only contain the final `.c` DottedGetExpression because `.b` and `a` can both be derived by walking back from the `.c` DottedGetExpression.
3700
     *
3701
     * Example 2:
3702
     * `name.space.doSomething(a.b.c)` will result in 2 entries in this list. the `CallExpression` for `doSomething`, and the `.c` DottedGetExpression.
3703
     *
3704
     * Example 3:
3705
     * `value = SomeEnum.value > 2 or SomeEnum.otherValue < 10` will result in 4 entries. `SomeEnum.value`, `2`, `SomeEnum.otherValue`, `10`
3706
     */
3707
    public expressions = new Set<Expression>();
2,295✔
3708

3709
    public importStatements = [] as ImportStatement[];
2,295✔
3710
    public libraryStatements = [] as LibraryStatement[];
2,295✔
3711
    public namespaceStatements = [] as NamespaceStatement[];
2,295✔
3712
    public typeStatements = [] as TypeStatement[];
2,295✔
3713
    public newExpressions = [] as NewExpression[];
2,295✔
3714
    public propertyHints = {} as Record<string, string>;
2,295✔
3715
}
3716

3717
class CancelStatementError extends Error {
3718
    constructor() {
3719
        super('CancelStatement');
2✔
3720
    }
3721
}
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