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

rokucommunity / brighterscript / #15757

03 May 2026 12:36PM UTC coverage: 88.945% (+0.03%) from 88.911%
#15757

push

web-flow
Merge aaf94efc6 into 10050636c

8534 of 10102 branches covered (84.48%)

Branch coverage included in aggregate %.

32 of 32 new or added lines in 3 files covered. (100.0%)

1 existing line in 1 file now uncovered.

10808 of 11644 relevant lines covered (92.82%)

2060.02 hits per line

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

92.43
/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
    AAIndexedMemberExpression,
67
    AALiteralExpression,
68
    AAMemberExpression,
69
    AnnotationExpression,
70
    ArrayLiteralExpression,
71
    BinaryExpression,
72
    CallExpression,
73
    CallfuncExpression,
74
    DottedGetExpression,
75
    EscapedCharCodeLiteralExpression,
76
    FunctionExpression,
77
    FunctionParameterExpression,
78
    GroupingExpression,
79
    IndexedGetExpression,
80
    LiteralExpression,
81
    NamespacedVariableNameExpression,
82
    NewExpression,
83
    NullCoalescingExpression,
84
    RegexLiteralExpression,
85
    SourceLiteralExpression,
86
    TaggedTemplateStringExpression,
87
    TemplateStringExpression,
88
    TemplateStringQuasiExpression,
89
    TernaryExpression,
90
    TypeCastExpression,
91
    UnaryExpression,
92
    VariableExpression,
93
    XmlAttributeGetExpression
94
} from './Expression';
95
import type { Diagnostic, Range } from 'vscode-languageserver';
96
import type { Logger } from '../logging';
97
import { createLogger } from '../logging';
1✔
98
import { isAAIndexedMemberExpression, isAAMemberExpression, isAnnotationExpression, isBinaryExpression, isCallExpression, isCallfuncExpression, isMethodStatement, isCommentStatement, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression, isXmlAttributeGetExpression } from '../astUtils/reflection';
1✔
99
import { createVisitor, WalkMode } from '../astUtils/visitors';
1✔
100
import { createStringLiteral, createToken } from '../astUtils/creators';
1✔
101
import { Cache } from '../Cache';
1✔
102
import type { Expression, Statement } from './AstNode';
103
import { SymbolTable } from '../SymbolTable';
1✔
104
import type { BscType } from '../types/BscType';
105

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

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

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

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

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

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

147
    private _references = new References();
2,518✔
148

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

156
    private addPropertyHints(item: Token | AALiteralExpression) {
157
        if (isToken(item)) {
1,344✔
158
            const name = item.text;
1,074✔
159
            this._references.propertyHints[name.toLowerCase()] = name;
1,074✔
160
        } else {
161
            for (const member of item.elements) {
270✔
162
                if (!isCommentStatement(member) && isAAMemberExpression(member) && member.keyToken) {
305✔
163
                    const name = member.keyToken.text;
252✔
164
                    if (!name.startsWith('"')) {
252✔
165
                        this._references.propertyHints[name.toLowerCase()] = name;
201✔
166
                    }
167
                }
168
            }
169
        }
170
    }
171

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

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

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

187
    /**
188
     * Whether line continuation after binary operators is allowed.
189
     * Enabled only in BrighterScript mode.
190
     */
191
    private allowLineContinuation: boolean;
192

193
    /**
194
     * If line continuation is enabled, consumes all immediately following Newline tokens.
195
     * Call this after matching a binary operator to allow the right-hand operand on the next line.
196
     */
197
    private consumeNewlinesIfAllowed() {
198
        if (this.allowLineContinuation) {
2,269✔
199
            while (this.match(TokenKind.Newline)) { }
1,289✔
200
        }
201
    }
202

203
    private globalTerminators = [] as TokenKind[][];
2,518✔
204

205
    /**
206
     * 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
207
     * based on the parse mode
208
     */
209
    private allowedLocalIdentifiers: TokenKind[];
210

211
    /**
212
     * Annotations collected which should be attached to the next statement
213
     */
214
    private pendingAnnotations: AnnotationExpression[];
215

216
    /**
217
     * Get the currently active global terminators
218
     */
219
    private peekGlobalTerminators() {
220
        return this.globalTerminators[this.globalTerminators.length - 1] ?? [];
8,110✔
221
    }
222

223
    /**
224
     * Static wrapper around creating a new parser and parsing a list of tokens
225
     */
226
    public static parse(toParse: Token[] | string, options?: ParseOptions): Parser {
227
        return new Parser().parse(toParse, options);
2,482✔
228
    }
229

230
    /**
231
     * Parses an array of `Token`s into an abstract syntax tree
232
     * @param toParse the array of tokens to parse. May not contain any whitespace tokens
233
     * @returns the same instance of the parser which contains the diagnostics and statements
234
     */
235
    public parse(toParse: Token[] | string, options?: ParseOptions) {
236
        this.logger = options?.logger ?? createLogger();
2,483✔
237
        options = this.sanitizeParseOptions(options);
2,483✔
238
        this.options = options;
2,483✔
239
        this.allowLineContinuation = options.mode === ParseMode.BrighterScript;
2,483✔
240

241
        let tokens: Token[];
242
        if (typeof toParse === 'string') {
2,483✔
243
            tokens = Lexer.scan(toParse, { trackLocations: options.trackLocations }).tokens;
311✔
244
        } else {
245
            tokens = toParse;
2,172✔
246
        }
247
        this.tokens = tokens;
2,483✔
248
        this.allowedLocalIdentifiers = [
2,483✔
249
            ...AllowedLocalIdentifiers,
250
            //when in plain brightscript mode, the BrighterScript source literals can be used as regular variables
251
            ...(this.options.mode === ParseMode.BrightScript ? BrighterScriptSourceLiterals : [])
2,483✔
252
        ];
253
        this.current = 0;
2,483✔
254
        this.diagnostics = [];
2,483✔
255
        this.namespaceAndFunctionDepth = 0;
2,483✔
256
        this.pendingAnnotations = [];
2,483✔
257

258
        this.ast = this.body();
2,483✔
259

260
        //now that we've built the AST, link every node to its parent
261
        this.ast.link();
2,483✔
262
        return this;
2,483✔
263
    }
264

265
    private logger: Logger;
266

267
    private body() {
268
        const parentAnnotations = this.enterAnnotationBlock();
2,819✔
269

270
        let body = new Body([]);
2,819✔
271
        if (this.tokens.length > 0) {
2,819✔
272
            this.consumeStatementSeparators(true);
2,818✔
273

274
            try {
2,818✔
275
                while (
2,818✔
276
                    //not at end of tokens
277
                    !this.isAtEnd() &&
10,199✔
278
                    //the next token is not one of the end terminators
279
                    !this.checkAny(...this.peekGlobalTerminators())
280
                ) {
281
                    let dec = this.declaration();
3,523✔
282
                    if (dec) {
3,523✔
283
                        if (!isAnnotationExpression(dec)) {
3,475✔
284
                            this.consumePendingAnnotations(dec);
3,438✔
285
                            body.statements.push(dec);
3,438✔
286
                            //ensure statement separator
287
                            this.consumeStatementSeparators(false);
3,438✔
288
                        } else {
289
                            this.consumeStatementSeparators(true);
37✔
290
                        }
291
                    }
292
                }
293
            } catch (parseError) {
294
                //do nothing with the parse error for now. perhaps we can remove this?
295
                console.error(parseError);
×
296
            }
297
        }
298

299
        this.exitAnnotationBlock(parentAnnotations);
2,819✔
300
        return body;
2,819✔
301
    }
302

303
    private sanitizeParseOptions(options: ParseOptions) {
304
        options ??= {};
2,483✔
305
        options.mode ??= ParseMode.BrightScript;
2,483✔
306
        options.trackLocations ??= true;
2,483✔
307
        return options;
2,483✔
308
    }
309

310
    /**
311
     * Determine if the parser is currently parsing tokens at the root level.
312
     */
313
    private isAtRootLevel() {
314
        return this.namespaceAndFunctionDepth === 0;
17,148✔
315
    }
316

317
    /**
318
     * Throws an error if the input file type is not BrighterScript
319
     */
320
    private warnIfNotBrighterScriptMode(featureName: string) {
321
        if (this.options.mode !== ParseMode.BrighterScript) {
1,543✔
322
            let diagnostic = {
173✔
323
                ...DiagnosticMessages.bsFeatureNotSupportedInBrsFiles(featureName),
324
                range: this.peek().range
325
            } as Diagnostic;
326
            this.diagnostics.push(diagnostic);
173✔
327
        }
328
    }
329

330
    /**
331
     * Throws an exception using the last diagnostic message
332
     */
333
    private lastDiagnosticAsError() {
334
        let error = new Error(this.diagnostics[this.diagnostics.length - 1]?.message ?? 'Unknown error');
135!
335
        (error as any).isDiagnostic = true;
135✔
336
        return error;
135✔
337
    }
338

339
    private declaration(): Statement | AnnotationExpression | undefined {
340
        try {
6,276✔
341
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
6,276✔
342
                return this.functionDeclaration(false);
1,707✔
343
            }
344

345
            if (this.checkLibrary()) {
4,569✔
346
                return this.libraryStatement();
14✔
347
            }
348

349
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
4,555✔
350
                return this.constDeclaration();
114✔
351
            }
352

353
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
4,441✔
354
                return this.annotationExpression();
44✔
355
            }
356

357
            if (this.check(TokenKind.Comment)) {
4,397✔
358
                return this.commentStatement();
220✔
359
            }
360

361
            //catch certain global terminators to prevent unnecessary lookahead (i.e. like `end namespace`, no need to continue)
362
            if (this.checkAny(...this.peekGlobalTerminators())) {
4,177!
363
                return;
×
364
            }
365

366
            return this.statement();
4,177✔
367
        } catch (error: any) {
368
            //if the error is not a diagnostic, then log the error for debugging purposes
369
            if (!error.isDiagnostic) {
128!
370
                this.logger.error(error);
×
371
            }
372
            this.synchronize();
128✔
373
        }
374
    }
375

376
    /**
377
     * Try to get an identifier. If not found, add diagnostic and return undefined
378
     */
379
    private tryIdentifier(...additionalTokenKinds: TokenKind[]): Identifier | undefined {
380
        const identifier = this.tryConsume(
134✔
381
            DiagnosticMessages.expectedIdentifier(),
382
            TokenKind.Identifier,
383
            ...additionalTokenKinds
384
        ) as Identifier;
385
        if (identifier) {
134✔
386
            // force the name into an identifier so the AST makes some sense
387
            identifier.kind = TokenKind.Identifier;
133✔
388
            return identifier;
133✔
389
        }
390
    }
391

392
    private identifier(...additionalTokenKinds: TokenKind[]) {
393
        const identifier = this.consume(
486✔
394
            DiagnosticMessages.expectedIdentifier(),
395
            TokenKind.Identifier,
396
            ...additionalTokenKinds
397
        ) as Identifier;
398
        // force the name into an identifier so the AST makes some sense
399
        identifier.kind = TokenKind.Identifier;
486✔
400
        return identifier;
486✔
401
    }
402

403
    private enumMemberStatement() {
404
        const statement = new EnumMemberStatement({} as any);
228✔
405
        statement.tokens.name = this.consume(
228✔
406
            DiagnosticMessages.expectedClassFieldIdentifier(),
407
            TokenKind.Identifier,
408
            ...AllowedProperties
409
        ) as Identifier;
410
        //look for `= SOME_EXPRESSION`
411
        if (this.check(TokenKind.Equal)) {
228✔
412
            statement.tokens.equal = this.advance();
135✔
413
            statement.value = this.expression();
135✔
414
        }
415
        return statement;
228✔
416
    }
417

418
    /**
419
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration`
420
     */
421
    private interfaceFieldStatement(optionalKeyword?: Token) {
422
        const name = this.identifier(...AllowedProperties);
75✔
423
        let asToken: Token;
424
        let typeToken: Token;
425
        let type: BscType;
426
        if (this.check(TokenKind.As)) {
75!
427
            asToken = this.consumeToken(TokenKind.As);
75✔
428
            typeToken = this.typeToken();
75✔
429
            type = util.tokenToBscType(typeToken);
75✔
430
        }
431

432
        if (!type) {
75!
433
            this.diagnostics.push({
×
434
                ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, typeToken.text),
435
                range: typeToken.range
436
            });
437
            throw this.lastDiagnosticAsError();
×
438
        }
439

440
        return new InterfaceFieldStatement(name, asToken, typeToken, type, optionalKeyword);
75✔
441
    }
442

443
    /**
444
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration()`
445
     */
446
    private interfaceMethodStatement(optionalKeyword?: Token) {
447
        const functionType = this.advance();
23✔
448
        const name = this.identifier(...AllowedProperties);
23✔
449
        const leftParen = this.consume(DiagnosticMessages.expectedToken(TokenKind.LeftParen), TokenKind.LeftParen);
23✔
450

451
        let params = [] as FunctionParameterExpression[];
23✔
452
        if (!this.check(TokenKind.RightParen)) {
23✔
453
            do {
5✔
454
                if (params.length >= CallExpression.MaximumArguments) {
7!
455
                    this.diagnostics.push({
×
456
                        ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments),
457
                        range: this.peek().range
458
                    });
459
                }
460

461
                params.push(this.functionParameter());
7✔
462
            } while (this.match(TokenKind.Comma));
463
        }
464
        const rightParen = this.consumeToken(TokenKind.RightParen);
23✔
465
        let asToken = null as Token;
23✔
466
        let returnTypeToken = null as Token;
23✔
467
        if (this.check(TokenKind.As)) {
23✔
468
            asToken = this.advance();
20✔
469
            returnTypeToken = this.typeToken();
20✔
470
            const returnType = util.tokenToBscType(returnTypeToken);
20✔
471
            if (!returnType) {
20!
472
                this.diagnostics.push({
×
473
                    ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, returnTypeToken.text),
474
                    range: returnTypeToken.range
475
                });
476
                throw this.lastDiagnosticAsError();
×
477
            }
478
        }
479

480
        return new InterfaceMethodStatement(
23✔
481
            functionType,
482
            name,
483
            leftParen,
484
            params,
485
            rightParen,
486
            asToken,
487
            returnTypeToken,
488
            util.tokenToBscType(returnTypeToken),
489
            optionalKeyword
490
        );
491
    }
492

493
    private interfaceDeclaration(): InterfaceStatement {
494
        this.warnIfNotBrighterScriptMode('interface declarations');
66✔
495

496
        const parentAnnotations = this.enterAnnotationBlock();
66✔
497

498
        const interfaceToken = this.consume(
66✔
499
            DiagnosticMessages.expectedKeyword(TokenKind.Interface),
500
            TokenKind.Interface
501
        );
502
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
66✔
503

504
        let extendsToken: Token;
505
        let parentInterfaceName: NamespacedVariableNameExpression;
506

507
        if (this.peek().text.toLowerCase() === 'extends') {
66✔
508
            extendsToken = this.advance();
2✔
509
            parentInterfaceName = this.getNamespacedVariableNameExpression();
2✔
510
        }
511
        this.consumeStatementSeparators();
66✔
512
        //gather up all interface members (Fields, Methods)
513
        let body = [] as Statement[];
66✔
514
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
66✔
515
            try {
167✔
516
                //break out of this loop if we encountered the `EndInterface` token not followed by `as`
517
                if (this.check(TokenKind.EndInterface) && !this.checkNext(TokenKind.As)) {
167✔
518
                    break;
66✔
519
                }
520

521
                let decl: Statement;
522

523
                //collect leading annotations
524
                if (this.check(TokenKind.At)) {
101✔
525
                    this.annotationExpression();
2✔
526
                }
527

528
                const optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
101✔
529
                //fields
530
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkAnyNext(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
101✔
531
                    decl = this.interfaceFieldStatement(optionalKeyword);
75✔
532
                    //field with name = 'optional'
533
                } else if (optionalKeyword && this.checkAny(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
26!
534
                    //rewind one place, so that 'optional' is the field name
535
                    this.current--;
×
536
                    decl = this.interfaceFieldStatement();
×
537

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

542
                    //comments
543
                } else if (this.check(TokenKind.Comment)) {
3✔
544
                    decl = this.commentStatement();
1✔
545
                }
546
                if (decl) {
98✔
547
                    this.consumePendingAnnotations(decl);
96✔
548
                    body.push(decl);
96✔
549
                } else {
550
                    //we didn't find a declaration...flag tokens until next line
551
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
552
                }
553
            } catch (e) {
554
                //throw out any failed members and move on to the next line
555
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3✔
556
            }
557

558
            //ensure statement separator
559
            this.consumeStatementSeparators();
101✔
560
        }
561

562
        //consume the final `end interface` token
563
        const endInterfaceToken = this.consumeToken(TokenKind.EndInterface);
66✔
564

565
        const statement = new InterfaceStatement(
66✔
566
            interfaceToken,
567
            nameToken,
568
            extendsToken,
569
            parentInterfaceName,
570
            body,
571
            endInterfaceToken
572
        );
573
        this._references.interfaceStatements.push(statement);
66✔
574
        this.exitAnnotationBlock(parentAnnotations);
66✔
575
        return statement;
66✔
576
    }
577

578
    private enumDeclaration(): EnumStatement {
579
        const result = new EnumStatement({} as any, []);
134✔
580
        this.warnIfNotBrighterScriptMode('enum declarations');
134✔
581

582
        const parentAnnotations = this.enterAnnotationBlock();
134✔
583

584
        result.tokens.enum = this.consume(
134✔
585
            DiagnosticMessages.expectedKeyword(TokenKind.Enum),
586
            TokenKind.Enum
587
        );
588

589
        result.tokens.name = this.tryIdentifier(...this.allowedLocalIdentifiers);
134✔
590

591
        this.consumeStatementSeparators();
134✔
592
        //gather up all members
593
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
134✔
594
            try {
234✔
595
                let decl: EnumMemberStatement | CommentStatement;
596

597
                //collect leading annotations
598
                if (this.check(TokenKind.At)) {
234!
599
                    this.annotationExpression();
×
600
                }
601

602
                //members
603
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
234✔
604
                    decl = this.enumMemberStatement();
228✔
605

606
                    //comments
607
                } else if (this.check(TokenKind.Comment)) {
6!
608
                    decl = this.commentStatement();
6✔
609
                }
610

611
                if (decl) {
234!
612
                    this.consumePendingAnnotations(decl);
234✔
613
                    result.body.push(decl);
234✔
614
                } else {
615
                    //we didn't find a declaration...flag tokens until next line
616
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
617
                }
618
            } catch (e) {
619
                //throw out any failed members and move on to the next line
620
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
621
            }
622

623
            //ensure statement separator
624
            this.consumeStatementSeparators();
234✔
625
            //break out of this loop if we encountered the `EndEnum` token
626
            if (this.check(TokenKind.EndEnum)) {
234✔
627
                break;
126✔
628
            }
629
        }
630

631
        //consume the final `end interface` token
632
        result.tokens.endEnum = this.consumeToken(TokenKind.EndEnum);
134✔
633

634
        this._references.enumStatements.push(result);
134✔
635
        this.exitAnnotationBlock(parentAnnotations);
134✔
636
        return result;
134✔
637
    }
638

639
    /**
640
     * A BrighterScript class declaration
641
     */
642
    private classDeclaration(): ClassStatement {
643
        this.warnIfNotBrighterScriptMode('class declarations');
505✔
644

645
        const parentAnnotations = this.enterAnnotationBlock();
505✔
646

647
        let classKeyword = this.consume(
505✔
648
            DiagnosticMessages.expectedKeyword(TokenKind.Class),
649
            TokenKind.Class
650
        );
651
        let extendsKeyword: Token;
652
        let parentClassName: NamespacedVariableNameExpression;
653

654
        //get the class name
655
        let className = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('class'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
505✔
656

657
        //see if the class inherits from parent
658
        if (this.peek().text.toLowerCase() === 'extends') {
505✔
659
            extendsKeyword = this.advance();
79✔
660
            parentClassName = this.getNamespacedVariableNameExpression();
79✔
661
        }
662

663
        //ensure statement separator
664
        this.consumeStatementSeparators();
504✔
665

666
        //gather up all class members (Fields, Methods)
667
        let body = [] as Statement[];
504✔
668
        while (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private, TokenKind.Function, TokenKind.Sub, TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
504✔
669
            try {
485✔
670
                let decl: Statement;
671
                let accessModifier: Token;
672

673
                if (this.check(TokenKind.At)) {
485✔
674
                    this.annotationExpression();
15✔
675
                }
676

677
                if (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private)) {
484✔
678
                    //use actual access modifier
679
                    accessModifier = this.advance();
66✔
680
                }
681

682
                let overrideKeyword: Token;
683
                if (this.peek().text.toLowerCase() === 'override') {
484✔
684
                    overrideKeyword = this.advance();
19✔
685
                }
686

687
                //methods (function/sub keyword OR identifier followed by opening paren)
688
                if (this.checkAny(TokenKind.Function, TokenKind.Sub) || (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.LeftParen))) {
484✔
689
                    const funcDeclaration = this.functionDeclaration(false, false);
278✔
690

691
                    //remove this function from the lists because it's not a callable
692
                    const functionStatement = this._references.functionStatements.pop();
278✔
693

694
                    //if we have an overrides keyword AND this method is called 'new', that's not allowed
695
                    if (overrideKeyword && funcDeclaration.name.text.toLowerCase() === 'new') {
278✔
696
                        this.diagnostics.push({
2✔
697
                            ...DiagnosticMessages.cannotUseOverrideKeywordOnConstructorFunction(),
698
                            range: overrideKeyword.range
699
                        });
700
                    }
701

702
                    decl = new MethodStatement(
278✔
703
                        accessModifier,
704
                        funcDeclaration.name,
705
                        funcDeclaration.func,
706
                        overrideKeyword
707
                    );
708

709
                    //refer to this statement as parent of the expression
710
                    functionStatement.func.functionStatement = decl as MethodStatement;
278✔
711

712
                    //fields
713
                } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
206✔
714

715
                    decl = this.fieldDeclaration(accessModifier);
184✔
716

717
                    //class fields cannot be overridden
718
                    if (overrideKeyword) {
183!
719
                        this.diagnostics.push({
×
720
                            ...DiagnosticMessages.classFieldCannotBeOverridden(),
721
                            range: overrideKeyword.range
722
                        });
723
                    }
724

725
                    //comments
726
                } else if (this.check(TokenKind.Comment)) {
22✔
727
                    decl = this.commentStatement();
8✔
728
                }
729

730
                if (decl) {
483✔
731
                    this.consumePendingAnnotations(decl);
469✔
732
                    body.push(decl);
469✔
733
                }
734
            } catch (e) {
735
                //throw out any failed members and move on to the next line
736
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
737
            }
738

739
            //ensure statement separator
740
            this.consumeStatementSeparators();
485✔
741
        }
742

743
        let endingKeyword = this.advance();
504✔
744
        if (endingKeyword.kind !== TokenKind.EndClass) {
504✔
745
            this.diagnostics.push({
3✔
746
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('class'),
747
                range: endingKeyword.range
748
            });
749
        }
750

751
        const result = new ClassStatement(
504✔
752
            classKeyword,
753
            className,
754
            body,
755
            endingKeyword,
756
            extendsKeyword,
757
            parentClassName
758
        );
759

760
        this._references.classStatements.push(result);
504✔
761
        this.exitAnnotationBlock(parentAnnotations);
504✔
762
        return result;
504✔
763
    }
764

765
    private fieldDeclaration(accessModifier: Token | null) {
766

767
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
184✔
768

769
        if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
184✔
770
            if (this.check(TokenKind.As)) {
183✔
771
                if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
5✔
772
                    // as <EOL>
773
                    // `as` is the field name
774
                } else if (this.checkNext(TokenKind.As)) {
4✔
775
                    //  as as ____
776
                    // first `as` is the field name
777
                } else if (optionalKeyword) {
2!
778
                    // optional as ____
779
                    // optional is the field name, `as` starts type
780
                    // rewind current token
781
                    optionalKeyword = null;
2✔
782
                    this.current--;
2✔
783
                }
784
            }
785
        } else {
786
            // no name after `optional` ... optional is the name
787
            // rewind current token
788
            optionalKeyword = null;
1✔
789
            this.current--;
1✔
790
        }
791

792
        let name = this.consume(
184✔
793
            DiagnosticMessages.expectedClassFieldIdentifier(),
794
            TokenKind.Identifier,
795
            ...AllowedProperties
796
        ) as Identifier;
797
        let asToken: Token;
798
        let fieldType: Token;
799
        //look for `as SOME_TYPE`
800
        if (this.check(TokenKind.As)) {
184✔
801
            asToken = this.advance();
129✔
802
            fieldType = this.typeToken();
129✔
803

804
            //no field type specified
805
            if (!util.tokenToBscType(fieldType)) {
129✔
806
                this.diagnostics.push({
1✔
807
                    ...DiagnosticMessages.expectedValidTypeToFollowAsKeyword(),
808
                    range: this.peek().range
809
                });
810
            }
811
        }
812

813
        let initialValue: Expression;
814
        let equal: Token;
815
        //if there is a field initializer
816
        if (this.check(TokenKind.Equal)) {
184✔
817
            equal = this.advance();
40✔
818
            initialValue = this.expression();
40✔
819
        }
820

821
        return new FieldStatement(
183✔
822
            accessModifier,
823
            name,
824
            asToken,
825
            fieldType,
826
            equal,
827
            initialValue,
828
            optionalKeyword
829
        );
830
    }
831

832
    /**
833
     * An array of CallExpression for the current function body
834
     */
835
    private callExpressions = [];
2,518✔
836

837
    private functionDeclaration(isAnonymous: true, checkIdentifier?: boolean, onlyCallableAsMember?: boolean): FunctionExpression;
838
    private functionDeclaration(isAnonymous: false, checkIdentifier?: boolean, onlyCallableAsMember?: boolean): FunctionStatement;
839
    private functionDeclaration(isAnonymous: boolean, checkIdentifier = true, onlyCallableAsMember = false) {
3,856✔
840
        let previousCallExpressions = this.callExpressions;
2,067✔
841
        this.callExpressions = [];
2,067✔
842
        try {
2,067✔
843
            //track depth to help certain statements need to know if they are contained within a function body
844
            this.namespaceAndFunctionDepth++;
2,067✔
845
            let functionType: Token;
846
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
2,067✔
847
                functionType = this.advance();
2,065✔
848
            } else {
849
                this.diagnostics.push({
2✔
850
                    ...DiagnosticMessages.missingCallableKeyword(),
851
                    range: this.peek().range
852
                });
853
                functionType = {
2✔
854
                    isReserved: true,
855
                    kind: TokenKind.Function,
856
                    text: 'function',
857
                    //zero-length location means derived
858
                    range: {
859
                        start: this.peek().range.start,
860
                        end: this.peek().range.start
861
                    },
862
                    leadingWhitespace: ''
863
                };
864
            }
865
            let isSub = functionType?.kind === TokenKind.Sub;
2,067!
866
            let functionTypeText = isSub ? 'sub' : 'function';
2,067✔
867
            let name: Identifier;
868
            let leftParen: Token;
869

870
            if (isAnonymous) {
2,067✔
871
                leftParen = this.consume(
82✔
872
                    DiagnosticMessages.expectedLeftParenAfterCallable(functionTypeText),
873
                    TokenKind.LeftParen
874
                );
875
            } else {
876
                name = this.consume(
1,985✔
877
                    DiagnosticMessages.expectedNameAfterCallableKeyword(functionTypeText),
878
                    TokenKind.Identifier,
879
                    ...AllowedProperties
880
                ) as Identifier;
881
                leftParen = this.consume(
1,983✔
882
                    DiagnosticMessages.expectedLeftParenAfterCallableName(functionTypeText),
883
                    TokenKind.LeftParen
884
                );
885

886
                //prevent functions from ending with type designators
887
                let lastChar = name.text[name.text.length - 1];
1,980✔
888
                if (['$', '%', '!', '#', '&'].includes(lastChar)) {
1,980✔
889
                    //don't throw this error; let the parser continue
890
                    this.diagnostics.push({
8✔
891
                        ...DiagnosticMessages.functionNameCannotEndWithTypeDesignator(functionTypeText, name.text, lastChar),
892
                        range: name.range
893
                    });
894
                }
895

896
                //flag functions with keywords for names (only for standard functions)
897
                if (checkIdentifier && DisallowedFunctionIdentifiersText.has(name.text.toLowerCase())) {
1,980✔
898
                    this.diagnostics.push({
2✔
899
                        ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
900
                        range: name.range
901
                    });
902
                }
903
            }
904

905
            let params = [] as FunctionParameterExpression[];
2,062✔
906
            let asToken: Token;
907
            let typeToken: Token;
908
            if (!this.check(TokenKind.RightParen)) {
2,062✔
909
                do {
340✔
910
                    params.push(this.functionParameter());
697✔
911
                } while (this.match(TokenKind.Comma));
912
            }
913
            let rightParen = this.advance();
2,061✔
914

915
            if (this.check(TokenKind.As)) {
2,061✔
916
                asToken = this.advance();
129✔
917

918
                typeToken = this.typeToken();
129✔
919

920
                if (!util.tokenToBscType(typeToken, this.options.mode === ParseMode.BrighterScript)) {
129✔
921
                    this.diagnostics.push({
2✔
922
                        ...DiagnosticMessages.invalidFunctionReturnType(typeToken.text ?? ''),
6!
923
                        range: typeToken.range
924
                    });
925
                }
926
            }
927

928
            params.reduce((haveFoundOptional: boolean, param: FunctionParameterExpression) => {
2,061✔
929
                if (haveFoundOptional && !param.defaultValue) {
695!
930
                    this.diagnostics.push({
×
931
                        ...DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter(param.name.text),
932
                        range: param.range
933
                    });
934
                }
935

936
                return haveFoundOptional || !!param.defaultValue;
695✔
937
            }, false);
938

939
            this.consumeStatementSeparators(true);
2,061✔
940

941
            let func = new FunctionExpression(
2,061✔
942
                params,
943
                undefined, //body
944
                functionType,
945
                undefined, //ending keyword
946
                leftParen,
947
                rightParen,
948
                asToken,
949
                typeToken
950
            );
951

952
            // add the function to the relevant symbol tables
953
            if (!onlyCallableAsMember && name) {
2,061✔
954
                const funcType = func.getFunctionType();
1,979✔
955
                funcType.setName(name.text);
1,979✔
956
            }
957

958
            this._references.functionExpressions.push(func);
2,061✔
959

960
            //support ending the function with `end sub` OR `end function`
961
            func.body = this.block();
2,061✔
962
            //if the parser was unable to produce a block, make an empty one so the AST makes some sense...
963
            if (!func.body) {
2,061✔
964
                func.body = new Block([], util.createRangeFromPositions(func.range.start, func.range.start));
3✔
965
            }
966
            func.body.symbolTable = new SymbolTable(`Block: Function '${name?.text ?? ''}'`, () => func.getSymbolTable());
2,061✔
967

968
            if (!func.body) {
2,061!
969
                this.diagnostics.push({
×
970
                    ...DiagnosticMessages.callableBlockMissingEndKeyword(functionTypeText),
971
                    range: this.peek().range
972
                });
973
                throw this.lastDiagnosticAsError();
×
974
            }
975

976
            // consume 'end sub' or 'end function'
977
            func.end = this.advance();
2,061✔
978
            let expectedEndKind = isSub ? TokenKind.EndSub : TokenKind.EndFunction;
2,061✔
979

980
            //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
981
            //add an error but don't hard-fail so the AST can continue more gracefully
982
            if (func.end.kind !== expectedEndKind) {
2,061✔
983
                this.diagnostics.push({
9✔
984
                    ...DiagnosticMessages.mismatchedEndCallableKeyword(functionTypeText, func.end.text),
985
                    range: func.end.range
986
                });
987
            }
988
            func.callExpressions = this.callExpressions;
2,061✔
989

990
            if (isAnonymous) {
2,061✔
991
                return func;
82✔
992
            } else {
993
                let result = new FunctionStatement(name, func);
1,979✔
994
                func.symbolTable.name += `: '${name?.text}'`;
1,979!
995
                func.functionStatement = result;
1,979✔
996
                this._references.functionStatements.push(result);
1,979✔
997

998
                return result;
1,979✔
999
            }
1000
        } finally {
1001
            this.namespaceAndFunctionDepth--;
2,067✔
1002
            //restore the previous CallExpression list
1003
            this.callExpressions = previousCallExpressions;
2,067✔
1004
        }
1005
    }
1006

1007
    private functionParameter(): FunctionParameterExpression {
1008
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
711✔
1009
            this.diagnostics.push({
1✔
1010
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
1011
                range: this.peek().range
1012
            });
1013
            throw this.lastDiagnosticAsError();
1✔
1014
        }
1015

1016
        let name = this.advance() as Identifier;
710✔
1017
        // force the name into an identifier so the AST makes some sense
1018
        name.kind = TokenKind.Identifier;
710✔
1019

1020
        //add diagnostic if name is a reserved word that cannot be used as an identifier
1021
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
710✔
1022
            this.diagnostics.push({
3✔
1023
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
1024
                range: name.range
1025
            });
1026
        }
1027

1028
        let typeToken: Token | undefined;
1029
        let defaultValue;
1030

1031
        // parse argument default value
1032
        if (this.match(TokenKind.Equal)) {
710✔
1033
            // it seems any expression is allowed here -- including ones that operate on other arguments!
1034
            defaultValue = this.expression(false);
255✔
1035
        }
1036

1037
        let asToken = null;
710✔
1038
        if (this.check(TokenKind.As)) {
710✔
1039
            asToken = this.advance();
335✔
1040

1041
            typeToken = this.typeToken();
335✔
1042

1043
            if (!util.tokenToBscType(typeToken, this.options.mode === ParseMode.BrighterScript)) {
335✔
1044
                this.diagnostics.push({
5✔
1045
                    ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, typeToken.text),
1046
                    range: typeToken.range
1047
                });
1048
            }
1049
        }
1050
        return new FunctionParameterExpression(
710✔
1051
            name,
1052
            typeToken,
1053
            defaultValue,
1054
            asToken
1055
        );
1056
    }
1057

1058
    private assignment(): AssignmentStatement {
1059
        let name = this.advance() as Identifier;
1,040✔
1060
        //add diagnostic if name is a reserved word that cannot be used as an identifier
1061
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
1,040✔
1062
            this.diagnostics.push({
13✔
1063
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
1064
                range: name.range
1065
            });
1066
        }
1067
        if (this.check(TokenKind.As)) {
1,040✔
1068
            // v1 syntax allows type declaration on lhs of assignment
1069
            this.warnIfNotBrighterScriptMode('typed assignment');
4✔
1070

1071
            this.advance(); // skip 'as'
4✔
1072
            this.typeToken(); // skip typeToken;
4✔
1073
        }
1074

1075
        let operator = this.consume(
1,040✔
1076
            DiagnosticMessages.expectedOperatorAfterIdentifier(AssignmentOperators, name.text),
1077
            ...AssignmentOperators
1078
        );
1079
        let value = this.expression();
1,038✔
1080

1081
        let result: AssignmentStatement;
1082
        if (operator.kind === TokenKind.Equal) {
1,029✔
1083
            result = new AssignmentStatement(operator, name, value);
983✔
1084
        } else {
1085
            const nameExpression = new VariableExpression(name);
46✔
1086
            result = new AssignmentStatement(
46✔
1087
                { kind: TokenKind.Equal, text: '=', range: operator.range },
1088
                name,
1089
                new BinaryExpression(nameExpression, operator, value)
1090
            );
1091
            this.addExpressionsToReferences(nameExpression);
46✔
1092
            if (isBinaryExpression(value)) {
46✔
1093
                //remove the right-hand-side expression from this assignment operator, and replace with the full assignment expression
1094
                this._references.expressions.delete(value);
3✔
1095
            }
1096
            this._references.expressions.add(result);
46✔
1097
        }
1098

1099
        this._references.assignmentStatements.push(result);
1,029✔
1100
        return result;
1,029✔
1101
    }
1102

1103
    private checkLibrary() {
1104
        let isLibraryToken = this.check(TokenKind.Library);
8,800✔
1105

1106
        //if we are at the top level, any line that starts with "library" should be considered a library statement
1107
        if (this.isAtRootLevel() && isLibraryToken) {
8,800✔
1108
            return true;
13✔
1109

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

1115
            //definitely not a library statement
1116
        } else {
1117
            return false;
8,786✔
1118
        }
1119
    }
1120

1121
    private checkAlias() {
1122
        let isAliasToken = this.check(TokenKind.Alias);
4,175✔
1123

1124
        //if we are at the top level, any line that starts with "alias" should be considered a alias statement
1125
        if (this.isAtRootLevel() && isAliasToken) {
4,175✔
1126
            return true;
2✔
1127

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

1133
            //definitely not a alias statement
1134
        } else {
1135
            return false;
4,173✔
1136
        }
1137
    }
1138

1139
    private checkTypeStatement() {
1140
        let isTypeToken = this.check(TokenKind.Type);
4,173✔
1141

1142
        //if we are at the top level, any line that starts with "type" should be considered a type statement
1143
        if (this.isAtRootLevel() && isTypeToken) {
4,173✔
1144
            return true;
8✔
1145

1146
            //not at root level, type statements are all invalid here, but try to detect if the tokens look
1147
            //like a type statement (and let the type function handle emitting the diagnostics)
1148
        } else if (isTypeToken && this.checkNext(TokenKind.Identifier)) {
4,165✔
1149
            return true;
3✔
1150

1151
            //definitely not a type statement
1152
        } else {
1153
            return false;
4,162✔
1154
        }
1155
    }
1156

1157
    private statement(): Statement | undefined {
1158
        if (this.checkLibrary()) {
4,231!
1159
            return this.libraryStatement();
×
1160
        }
1161

1162
        if (this.check(TokenKind.Import)) {
4,231✔
1163
            return this.importStatement();
51✔
1164
        }
1165

1166
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
4,180✔
1167
            return this.typecastStatement();
5✔
1168
        }
1169

1170
        if (this.checkAlias()) {
4,175✔
1171
            return this.aliasStatement();
2✔
1172
        }
1173

1174
        if (this.checkTypeStatement()) {
4,173✔
1175
            return this.typeStatement();
11✔
1176
        }
1177

1178
        if (this.check(TokenKind.Stop)) {
4,162✔
1179
            return this.stopStatement();
16✔
1180
        }
1181

1182
        if (this.check(TokenKind.If)) {
4,146✔
1183
            return this.ifStatement();
172✔
1184
        }
1185

1186
        //`try` must be followed by a block, otherwise it could be a local variable
1187
        if (this.check(TokenKind.Try) && this.checkAnyNext(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
3,974✔
1188
            return this.tryCatchStatement();
27✔
1189
        }
1190

1191
        if (this.check(TokenKind.Throw)) {
3,947✔
1192
            return this.throwStatement();
11✔
1193
        }
1194

1195
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
3,936✔
1196
            return this.printStatement();
730✔
1197
        }
1198
        if (this.check(TokenKind.Dim)) {
3,206✔
1199
            return this.dimStatement();
43✔
1200
        }
1201

1202
        if (this.check(TokenKind.While)) {
3,163✔
1203
            return this.whileStatement();
32✔
1204
        }
1205

1206
        if (this.check(TokenKind.ExitWhile)) {
3,131✔
1207
            return this.exitWhile();
7✔
1208
        }
1209

1210
        if (this.check(TokenKind.For)) {
3,124✔
1211
            return this.forStatement();
45✔
1212
        }
1213

1214
        if (this.check(TokenKind.ForEach)) {
3,079✔
1215
            return this.forEachStatement();
27✔
1216
        }
1217

1218
        if (this.check(TokenKind.ExitFor)) {
3,052✔
1219
            return this.exitFor();
4✔
1220
        }
1221

1222
        if (this.check(TokenKind.End)) {
3,048✔
1223
            return this.endStatement();
8✔
1224
        }
1225

1226
        if (this.match(TokenKind.Return)) {
3,040✔
1227
            return this.returnStatement();
251✔
1228
        }
1229

1230
        if (this.check(TokenKind.Goto)) {
2,789✔
1231
            return this.gotoStatement();
12✔
1232
        }
1233

1234
        //the continue keyword (followed by `for`, `while`, or a statement separator)
1235
        if (this.check(TokenKind.Continue) && this.checkAnyNext(TokenKind.While, TokenKind.For, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
2,777✔
1236
            return this.continueStatement();
12✔
1237
        }
1238

1239
        //does this line look like a label? (i.e.  `someIdentifier:` )
1240
        if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
2,765✔
1241
            try {
12✔
1242
                return this.labelStatement();
12✔
1243
            } catch (err) {
1244
                if (!(err instanceof CancelStatementError)) {
2!
1245
                    throw err;
×
1246
                }
1247
                //not a label, try something else
1248
            }
1249
        }
1250

1251
        // BrightScript is like python, in that variables can be declared without a `var`,
1252
        // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
1253
        // out what to do with it.
1254
        if (
2,755✔
1255
            this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)
1256
        ) {
1257
            if (this.checkAnyNext(...AssignmentOperators)) {
2,555✔
1258
                return this.assignment();
991✔
1259
            } else if (this.checkNext(TokenKind.As)) {
1,564✔
1260
                // may be a typed assignment - this is v1 syntax
1261
                const backtrack = this.current;
5✔
1262
                let validTypeExpression = false;
5✔
1263
                try {
5✔
1264
                    // skip the identifier, and check for valid type expression
1265
                    this.advance();
5✔
1266
                    // skip the 'as'
1267
                    this.advance();
5✔
1268
                    // check if there is a valid type
1269
                    const typeToken = this.typeToken(true);
5✔
1270
                    const allowedNameKinds = [TokenKind.Identifier, ...DeclarableTypes, ...this.allowedLocalIdentifiers];
5✔
1271
                    validTypeExpression = allowedNameKinds.includes(typeToken.kind);
5✔
1272
                } catch (e) {
1273
                    // ignore any errors
1274
                } finally {
1275
                    this.current = backtrack;
5✔
1276
                }
1277
                if (validTypeExpression) {
5✔
1278
                    // there is a valid 'as' and type expression
1279
                    return this.assignment();
4✔
1280
                }
1281
            }
1282
        }
1283

1284
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1285
        if (this.check(TokenKind.Interface)) {
1,760✔
1286
            return this.interfaceDeclaration();
66✔
1287
        }
1288

1289
        if (this.check(TokenKind.Class)) {
1,694✔
1290
            return this.classDeclaration();
505✔
1291
        }
1292

1293
        if (this.check(TokenKind.Namespace)) {
1,189✔
1294
            return this.namespaceStatement();
337✔
1295
        }
1296

1297
        if (this.check(TokenKind.Enum)) {
852✔
1298
            return this.enumDeclaration();
134✔
1299
        }
1300

1301
        // TODO: support multi-statements
1302
        return this.setStatement();
718✔
1303
    }
1304

1305
    private whileStatement(): WhileStatement {
1306
        const whileKeyword = this.advance();
32✔
1307
        const condition = this.expression();
32✔
1308

1309
        this.consumeStatementSeparators();
31✔
1310

1311
        const whileBlock = this.block(TokenKind.EndWhile, TokenKind.Next);
31✔
1312
        let endWhile: Token;
1313
        if (whileBlock && this.peek().kind === TokenKind.EndWhile) {
31✔
1314
            endWhile = this.advance();
22✔
1315
        } else if (whileBlock && this.peek().kind === TokenKind.Next) {
9✔
1316
            //recover: a stray `next` is a common mistake when the user means `end while`.
1317
            //emit a targeted diagnostic and consume the `next` so the rest of the file parses cleanly.
1318
            this.diagnostics.push({
7✔
1319
                ...DiagnosticMessages.whileLoopTerminatedWithNext(),
1320
                range: this.peek().range
1321
            });
1322
            endWhile = this.advance();
7✔
1323
        } else {
1324
            this.diagnostics.push({
2✔
1325
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('while'),
1326
                range: this.peek().range
1327
            });
1328
            if (!whileBlock) {
2✔
1329
                throw this.lastDiagnosticAsError();
1✔
1330
            }
1331
        }
1332

1333
        return new WhileStatement(
30✔
1334
            { while: whileKeyword, endWhile: endWhile },
1335
            condition,
1336
            whileBlock
1337
        );
1338
    }
1339

1340
    private exitWhile(): ExitWhileStatement {
1341
        let keyword = this.advance();
7✔
1342

1343
        return new ExitWhileStatement({ exitWhile: keyword });
7✔
1344
    }
1345

1346
    private forStatement(): ForStatement {
1347
        const forToken = this.advance();
45✔
1348
        const initializer = this.assignment();
45✔
1349

1350
        //TODO: newline allowed?
1351

1352
        const toToken = this.advance();
44✔
1353
        const finalValue = this.expression();
44✔
1354
        let incrementExpression: Expression | undefined;
1355
        let stepToken: Token | undefined;
1356

1357
        if (this.check(TokenKind.Step)) {
44✔
1358
            stepToken = this.advance();
10✔
1359
            incrementExpression = this.expression();
10✔
1360
        } else {
1361
            // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
1362
        }
1363

1364
        this.consumeStatementSeparators();
44✔
1365

1366
        let body = this.block(TokenKind.EndFor, TokenKind.Next, TokenKind.EndWhile);
44✔
1367
        let endForToken: Token;
1368
        if (body && this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
44✔
1369
            endForToken = this.advance();
37✔
1370
        } else if (body && this.peek().kind === TokenKind.EndWhile) {
7✔
1371
            //recover: a stray `end while` is a common mistake when the user means `end for`.
1372
            this.diagnostics.push({
6✔
1373
                ...DiagnosticMessages.forLoopTerminatedWithEndWhile(),
1374
                range: this.peek().range
1375
            });
1376
            endForToken = this.advance();
6✔
1377
        } else {
1378
            this.diagnostics.push({
1✔
1379
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1380
                range: this.peek().range
1381
            });
1382
            if (!body) {
1!
1383
                throw this.lastDiagnosticAsError();
×
1384
            }
1385
        }
1386

1387
        // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
1388
        // stays around in scope with whatever value it was when the loop exited.
1389
        return new ForStatement(
44✔
1390
            forToken,
1391
            initializer,
1392
            toToken,
1393
            finalValue,
1394
            body,
1395
            endForToken,
1396
            stepToken,
1397
            incrementExpression
1398
        );
1399
    }
1400

1401
    private forEachStatement(): ForEachStatement {
1402
        let forEach = this.advance();
27✔
1403
        let name = this.advance();
27✔
1404

1405
        if (this.check(TokenKind.As)) {
27✔
1406
            this.warnIfNotBrighterScriptMode('typed for each item');
3✔
1407

1408
            this.advance(); // get 'as'
3✔
1409
            this.typeToken(); // get type
3✔
1410
        }
1411

1412
        let maybeIn = this.peek();
27✔
1413
        if (this.check(TokenKind.Identifier) && maybeIn.text.toLowerCase() === 'in') {
27!
1414
            this.advance();
27✔
1415
        } else {
1416
            this.diagnostics.push({
×
1417
                ...DiagnosticMessages.expectedInAfterForEach(name.text),
1418
                range: this.peek().range
1419
            });
1420
            throw this.lastDiagnosticAsError();
×
1421
        }
1422

1423
        let target = this.expression();
27✔
1424
        if (!target) {
27!
1425
            this.diagnostics.push({
×
1426
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1427
                range: this.peek().range
1428
            });
1429
            throw this.lastDiagnosticAsError();
×
1430
        }
1431

1432
        this.consumeStatementSeparators();
27✔
1433

1434
        let body = this.block(TokenKind.EndFor, TokenKind.Next, TokenKind.EndWhile);
27✔
1435
        let endFor: Token;
1436
        if (body && this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
27✔
1437
            endFor = this.advance();
24✔
1438
        } else if (body && this.peek().kind === TokenKind.EndWhile) {
3!
1439
            //recover: a stray `end while` is a common mistake when the user means `end for`.
1440
            this.diagnostics.push({
3✔
1441
                ...DiagnosticMessages.forLoopTerminatedWithEndWhile(),
1442
                range: this.peek().range
1443
            });
1444
            endFor = this.advance();
3✔
1445
        } else {
UNCOV
1446
            this.diagnostics.push({
×
1447
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1448
                range: this.peek().range
1449
            });
1450
            throw this.lastDiagnosticAsError();
×
1451
        }
1452

1453
        return new ForEachStatement(
27✔
1454
            {
1455
                forEach: forEach,
1456
                in: maybeIn,
1457
                endFor: endFor
1458
            },
1459
            name,
1460
            target,
1461
            body
1462
        );
1463
    }
1464

1465
    private exitFor(): ExitForStatement {
1466
        let keyword = this.advance();
4✔
1467

1468
        return new ExitForStatement({ exitFor: keyword });
4✔
1469
    }
1470

1471
    private commentStatement() {
1472
        //if this comment is on the same line as the previous statement,
1473
        //then this comment should be treated as a single-line comment
1474
        let prev = this.previous();
235✔
1475
        if (prev?.range?.end.line === this.peek().range?.start.line) {
235✔
1476
            return new CommentStatement([this.advance()]);
128✔
1477
        } else {
1478
            let comments = [this.advance()];
107✔
1479
            while (this.check(TokenKind.Newline) && this.checkNext(TokenKind.Comment)) {
107✔
1480
                this.advance();
20✔
1481
                comments.push(this.advance());
20✔
1482
            }
1483
            return new CommentStatement(comments);
107✔
1484
        }
1485
    }
1486

1487
    private namespaceStatement(): NamespaceStatement | undefined {
1488
        this.warnIfNotBrighterScriptMode('namespace');
337✔
1489
        let keyword = this.advance();
337✔
1490

1491
        this.namespaceAndFunctionDepth++;
337✔
1492

1493
        let name = this.getNamespacedVariableNameExpression();
337✔
1494
        //set the current namespace name
1495
        let result = new NamespaceStatement(keyword, name, null, null);
336✔
1496

1497
        this.globalTerminators.push([TokenKind.EndNamespace]);
336✔
1498
        let body = this.body();
336✔
1499
        this.globalTerminators.pop();
336✔
1500

1501
        let endKeyword: Token;
1502
        if (this.check(TokenKind.EndNamespace)) {
336✔
1503
            endKeyword = this.advance();
335✔
1504
        } else {
1505
            //the `end namespace` keyword is missing. add a diagnostic, but keep parsing
1506
            this.diagnostics.push({
1✔
1507
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'),
1508
                range: keyword.range
1509
            });
1510
        }
1511

1512
        this.namespaceAndFunctionDepth--;
336✔
1513

1514
        result.body = body;
336✔
1515
        result.endKeyword = endKeyword;
336✔
1516
        this._references.namespaceStatements.push(result);
336✔
1517
        //cache the range property so that plugins can't affect it
1518
        result.cacheRange();
336✔
1519
        result.body.symbolTable.name += `: namespace '${result.name}'`;
336✔
1520
        return result;
336✔
1521
    }
1522

1523
    /**
1524
     * Get an expression with identifiers separated by periods. Useful for namespaces and class extends
1525
     */
1526
    private getNamespacedVariableNameExpression(ignoreDiagnostics = false) {
463✔
1527
        let firstIdentifier: Identifier;
1528
        if (ignoreDiagnostics) {
580✔
1529
            if (this.checkAny(...this.allowedLocalIdentifiers)) {
2!
1530
                firstIdentifier = this.advance() as Identifier;
×
1531
            } else {
1532
                throw new Error();
2✔
1533
            }
1534
        } else {
1535
            firstIdentifier = this.consume(
578✔
1536
                DiagnosticMessages.expectedIdentifierAfterKeyword(this.previous().text),
1537
                TokenKind.Identifier,
1538
                ...this.allowedLocalIdentifiers
1539
            ) as Identifier;
1540
        }
1541
        let expr: DottedGetExpression | VariableExpression;
1542

1543
        if (firstIdentifier) {
572!
1544
            // force it into an identifier so the AST makes some sense
1545
            firstIdentifier.kind = TokenKind.Identifier;
572✔
1546
            const varExpr = new VariableExpression(firstIdentifier);
572✔
1547
            expr = varExpr;
572✔
1548

1549
            //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
1550
            while (this.check(TokenKind.Dot)) {
572✔
1551
                let dot = this.tryConsume(
191✔
1552
                    DiagnosticMessages.unexpectedToken(this.peek().text),
1553
                    TokenKind.Dot
1554
                );
1555
                if (!dot) {
191!
1556
                    break;
×
1557
                }
1558
                let identifier = this.tryConsume(
191✔
1559
                    DiagnosticMessages.expectedIdentifier(),
1560
                    TokenKind.Identifier,
1561
                    ...this.allowedLocalIdentifiers,
1562
                    ...AllowedProperties
1563
                ) as Identifier;
1564

1565
                if (!identifier) {
191✔
1566
                    break;
3✔
1567
                }
1568
                // force it into an identifier so the AST makes some sense
1569
                identifier.kind = TokenKind.Identifier;
188✔
1570
                expr = new DottedGetExpression(expr, identifier, dot);
188✔
1571
            }
1572
        }
1573
        return new NamespacedVariableNameExpression(expr);
572✔
1574
    }
1575

1576
    /**
1577
     * Add an 'unexpected token' diagnostic for any token found between current and the first stopToken found.
1578
     */
1579
    private flagUntil(...stopTokens: TokenKind[]) {
1580
        while (!this.checkAny(...stopTokens) && !this.isAtEnd()) {
7!
1581
            let token = this.advance();
×
1582
            this.diagnostics.push({
×
1583
                ...DiagnosticMessages.unexpectedToken(token.text),
1584
                range: token.range
1585
            });
1586
        }
1587
    }
1588

1589
    /**
1590
     * Consume tokens until one of the `stopTokenKinds` is encountered
1591
     * @param stopTokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
1592
     * @returns - the list of tokens consumed, EXCLUDING the `stopTokenKind` (you can use `this.peek()` to see which one it was)
1593
     */
1594
    private consumeUntil(...stopTokenKinds: TokenKind[]) {
1595
        let result = [] as Token[];
80✔
1596
        //take tokens until we encounter one of the stopTokenKinds
1597
        while (!stopTokenKinds.includes(this.peek().kind)) {
80✔
1598
            result.push(this.advance());
199✔
1599
        }
1600
        return result;
80✔
1601
    }
1602

1603
    private constDeclaration(): ConstStatement | undefined {
1604
        this.warnIfNotBrighterScriptMode('const declaration');
114✔
1605
        const constToken = this.advance();
114✔
1606
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
114✔
1607
        const equalToken = this.consumeToken(TokenKind.Equal);
114✔
1608
        const expression = this.expression();
114✔
1609
        const statement = new ConstStatement({
114✔
1610
            const: constToken,
1611
            name: nameToken,
1612
            equals: equalToken
1613
        }, expression);
1614
        this._references.constStatements.push(statement);
114✔
1615
        return statement;
114✔
1616
    }
1617

1618
    private libraryStatement(): LibraryStatement | undefined {
1619
        let libStatement = new LibraryStatement({
14✔
1620
            library: this.advance(),
1621
            //grab the next token only if it's a string
1622
            filePath: this.tryConsume(
1623
                DiagnosticMessages.expectedStringLiteralAfterKeyword('library'),
1624
                TokenKind.StringLiteral
1625
            )
1626
        });
1627

1628
        this._references.libraryStatements.push(libStatement);
14✔
1629
        return libStatement;
14✔
1630
    }
1631

1632
    private importStatement() {
1633
        this.warnIfNotBrighterScriptMode('import statements');
51✔
1634
        let importStatement = new ImportStatement(
51✔
1635
            this.advance(),
1636
            //grab the next token only if it's a string
1637
            this.tryConsume(
1638
                DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
1639
                TokenKind.StringLiteral
1640
            )
1641
        );
1642

1643
        this._references.importStatements.push(importStatement);
51✔
1644
        return importStatement;
51✔
1645
    }
1646

1647
    private typecastStatement() {
1648
        this.warnIfNotBrighterScriptMode('typecast statements');
5✔
1649
        const typecastToken = this.advance();
5✔
1650
        const obj = this.identifier(...this.allowedLocalIdentifiers);
5✔
1651
        const asToken = this.advance();
5✔
1652
        const typeToken = this.typeToken();
5✔
1653
        return new TypecastStatement({
5✔
1654
            typecast: typecastToken,
1655
            obj: obj,
1656
            as: asToken,
1657
            type: typeToken
1658
        });
1659
    }
1660

1661
    private aliasStatement() {
1662
        this.warnIfNotBrighterScriptMode('alias statements');
2✔
1663
        const aliasToken = this.advance();
2✔
1664
        const name = this.identifier(...this.allowedLocalIdentifiers);
2✔
1665
        const equals = this.consumeToken(TokenKind.Equal);
2✔
1666
        const value = this.identifier(...this.allowedLocalIdentifiers);
2✔
1667
        return new AliasStatement({
2✔
1668
            alias: aliasToken,
1669
            name: name,
1670
            equals: equals,
1671
            value: value
1672
        });
1673
    }
1674

1675
    private annotationExpression() {
1676
        const atToken = this.advance();
61✔
1677
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
61✔
1678
        if (identifier) {
61✔
1679
            identifier.kind = TokenKind.Identifier;
60✔
1680
        }
1681
        let annotation = new AnnotationExpression(atToken, identifier);
61✔
1682
        this.pendingAnnotations.push(annotation);
60✔
1683

1684
        //optional arguments
1685
        if (this.check(TokenKind.LeftParen)) {
60✔
1686
            let leftParen = this.advance();
24✔
1687
            annotation.call = this.finishCall(leftParen, annotation, false);
24✔
1688
        }
1689
        return annotation;
60✔
1690
    }
1691

1692
    private typeStatement(): TypeStatement | undefined {
1693
        this.warnIfNotBrighterScriptMode('type statements');
11✔
1694
        const typeToken = this.advance();
11✔
1695
        const name = this.identifier(...this.allowedLocalIdentifiers);
11✔
1696
        const equals = this.tryConsume(
11✔
1697
            DiagnosticMessages.expectedToken(TokenKind.Equal),
1698
            TokenKind.Equal
1699
        );
1700
        let value = this.typeToken();
11✔
1701

1702
        let typeStmt = new TypeStatement({
11✔
1703
            type: typeToken,
1704
            name: name,
1705
            equals: equals,
1706
            value: value
1707

1708
        });
1709
        this._references.typeStatements.push(typeStmt);
11✔
1710
        return typeStmt;
11✔
1711
    }
1712

1713
    private ternaryExpression(test?: Expression): TernaryExpression {
1714
        this.warnIfNotBrighterScriptMode('ternary operator');
94✔
1715
        if (!test) {
94!
1716
            test = this.expression();
×
1717
        }
1718
        const questionMarkToken = this.advance();
94✔
1719

1720
        //consume newlines or comments
1721
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
94✔
1722
            this.advance();
8✔
1723
        }
1724

1725
        let consequent: Expression;
1726
        try {
94✔
1727
            consequent = this.expression();
94✔
1728
        } catch { }
1729

1730
        //consume newlines or comments
1731
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
94✔
1732
            this.advance();
6✔
1733
        }
1734

1735
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
94✔
1736

1737
        //consume newlines
1738
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
94✔
1739
            this.advance();
12✔
1740
        }
1741
        let alternate: Expression;
1742
        try {
94✔
1743
            alternate = this.expression();
94✔
1744
        } catch { }
1745

1746
        return new TernaryExpression(test, questionMarkToken, consequent, colonToken, alternate);
94✔
1747
    }
1748

1749
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1750
        this.warnIfNotBrighterScriptMode('null coalescing operator');
30✔
1751
        const questionQuestionToken = this.advance();
30✔
1752
        const alternate = this.expression();
30✔
1753
        return new NullCoalescingExpression(test, questionQuestionToken, alternate);
30✔
1754
    }
1755

1756
    private regexLiteralExpression() {
1757
        this.warnIfNotBrighterScriptMode('regular expression literal');
45✔
1758
        return new RegexLiteralExpression({
45✔
1759
            regexLiteral: this.advance()
1760
        });
1761
    }
1762

1763
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1764
        this.warnIfNotBrighterScriptMode('template string');
55✔
1765

1766
        //get the tag name
1767
        let tagName: Identifier;
1768
        if (isTagged) {
55✔
1769
            tagName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties) as Identifier;
8✔
1770
            // force it into an identifier so the AST makes some sense
1771
            tagName.kind = TokenKind.Identifier;
8✔
1772
        }
1773

1774
        let quasis = [] as TemplateStringQuasiExpression[];
55✔
1775
        let expressions = [];
55✔
1776
        let openingBacktick = this.peek();
55✔
1777
        this.advance();
55✔
1778
        let currentQuasiExpressionParts = [];
55✔
1779
        while (!this.isAtEnd() && !this.check(TokenKind.BackTick)) {
55✔
1780
            let next = this.peek();
206✔
1781
            if (next.kind === TokenKind.TemplateStringQuasi) {
206✔
1782
                //a quasi can actually be made up of multiple quasis when it includes char literals
1783
                currentQuasiExpressionParts.push(
130✔
1784
                    new LiteralExpression(next)
1785
                );
1786
                this.advance();
130✔
1787
            } else if (next.kind === TokenKind.EscapedCharCodeLiteral) {
76✔
1788
                currentQuasiExpressionParts.push(
33✔
1789
                    new EscapedCharCodeLiteralExpression(<any>next)
1790
                );
1791
                this.advance();
33✔
1792
            } else {
1793
                //finish up the current quasi
1794
                quasis.push(
43✔
1795
                    new TemplateStringQuasiExpression(currentQuasiExpressionParts)
1796
                );
1797
                currentQuasiExpressionParts = [];
43✔
1798

1799
                if (next.kind === TokenKind.TemplateStringExpressionBegin) {
43!
1800
                    this.advance();
43✔
1801
                }
1802
                //now keep this expression
1803
                expressions.push(this.expression());
43✔
1804
                if (!this.isAtEnd() && this.check(TokenKind.TemplateStringExpressionEnd)) {
43!
1805
                    //TODO is it an error if this is not present?
1806
                    this.advance();
43✔
1807
                } else {
1808
                    this.diagnostics.push({
×
1809
                        ...DiagnosticMessages.unterminatedTemplateExpression(),
1810
                        range: util.getRange(openingBacktick, this.peek())
1811
                    });
1812
                    throw this.lastDiagnosticAsError();
×
1813
                }
1814
            }
1815
        }
1816

1817
        //store the final set of quasis
1818
        quasis.push(
55✔
1819
            new TemplateStringQuasiExpression(currentQuasiExpressionParts)
1820
        );
1821

1822
        if (this.isAtEnd()) {
55✔
1823
            //error - missing backtick
1824
            this.diagnostics.push({
2✔
1825
                ...DiagnosticMessages.unterminatedTemplateStringAtEndOfFile(),
1826
                range: util.getRange(openingBacktick, this.peek())
1827
            });
1828
            throw this.lastDiagnosticAsError();
2✔
1829

1830
        } else {
1831
            let closingBacktick = this.advance();
53✔
1832
            if (isTagged) {
53✔
1833
                return new TaggedTemplateStringExpression(tagName, openingBacktick, quasis, expressions, closingBacktick);
8✔
1834
            } else {
1835
                return new TemplateStringExpression(openingBacktick, quasis, expressions, closingBacktick);
45✔
1836
            }
1837
        }
1838
    }
1839

1840
    private tryCatchStatement(): TryCatchStatement {
1841
        const tryToken = this.advance();
27✔
1842
        const statement = new TryCatchStatement(
27✔
1843
            { try: tryToken }
1844
        );
1845

1846
        //ensure statement separator
1847
        this.consumeStatementSeparators();
27✔
1848

1849
        statement.tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
27✔
1850

1851
        const peek = this.peek();
27✔
1852
        if (peek.kind !== TokenKind.Catch) {
27✔
1853
            this.diagnostics.push({
2✔
1854
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1855
                range: this.peek().range
1856
            });
1857
            //gracefully handle end-try
1858
            if (peek.kind === TokenKind.EndTry) {
2✔
1859
                statement.tokens.endTry = this.advance();
1✔
1860
            }
1861
            return statement;
2✔
1862
        }
1863
        const catchStmt = new CatchStatement({ catch: this.advance() });
25✔
1864
        statement.catchStatement = catchStmt;
25✔
1865

1866
        const exceptionVarToken = this.tryConsume(DiagnosticMessages.missingExceptionVarToFollowCatch(), TokenKind.Identifier, ...this.allowedLocalIdentifiers);
25✔
1867
        if (exceptionVarToken) {
25✔
1868
            // force it into an identifier so the AST makes some sense
1869
            exceptionVarToken.kind = TokenKind.Identifier;
23✔
1870
            catchStmt.exceptionVariable = exceptionVarToken as Identifier;
23✔
1871
        }
1872

1873
        //ensure statement sepatator
1874
        this.consumeStatementSeparators();
25✔
1875

1876
        catchStmt.catchBranch = this.block(TokenKind.EndTry);
25✔
1877

1878
        if (this.peek().kind !== TokenKind.EndTry) {
25✔
1879
            this.diagnostics.push({
1✔
1880
                ...DiagnosticMessages.expectedEndTryToTerminateTryCatch(),
1881
                range: this.peek().range
1882
            });
1883
        } else {
1884
            statement.tokens.endTry = this.advance();
24✔
1885
        }
1886
        return statement;
25✔
1887
    }
1888

1889
    private throwStatement() {
1890
        const throwToken = this.advance();
11✔
1891
        let expression: Expression;
1892
        if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
11✔
1893
            this.diagnostics.push({
1✔
1894
                ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(),
1895
                range: throwToken.range
1896
            });
1897
        } else {
1898
            expression = this.expression();
10✔
1899
        }
1900
        return new ThrowStatement(throwToken, expression);
9✔
1901
    }
1902

1903
    private dimStatement() {
1904
        const dim = this.advance();
43✔
1905

1906
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
43✔
1907
        // force to an identifier so the AST makes some sense
1908
        if (identifier) {
43✔
1909
            identifier.kind = TokenKind.Identifier;
41✔
1910
        }
1911

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

1914
        let expressions: Expression[] = [];
43✔
1915
        let expression: Expression;
1916
        do {
43✔
1917
            try {
82✔
1918
                expression = this.expression();
82✔
1919
                expressions.push(expression);
77✔
1920
                if (this.check(TokenKind.Comma)) {
77✔
1921
                    this.advance();
39✔
1922
                } else {
1923
                    // will also exit for right square braces
1924
                    break;
38✔
1925
                }
1926
            } catch (error) {
1927
            }
1928
        } while (expression);
1929

1930
        if (expressions.length === 0) {
43✔
1931
            this.diagnostics.push({
5✔
1932
                ...DiagnosticMessages.missingExpressionsInDimStatement(),
1933
                range: this.peek().range
1934
            });
1935
        }
1936
        let rightSquareBracket = this.tryConsume(DiagnosticMessages.missingRightSquareBracketAfterDimIdentifier(), TokenKind.RightSquareBracket);
43✔
1937
        return new DimStatement(dim, identifier, leftSquareBracket, expressions, rightSquareBracket);
43✔
1938
    }
1939

1940
    private ifStatement(): IfStatement {
1941
        // colon before `if` is usually not allowed, unless it's after `then`
1942
        if (this.current > 0) {
216✔
1943
            const prev = this.previous();
211✔
1944
            if (prev.kind === TokenKind.Colon) {
211✔
1945
                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then) {
3✔
1946
                    this.diagnostics.push({
1✔
1947
                        ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
1948
                        range: prev.range
1949
                    });
1950
                }
1951
            }
1952
        }
1953

1954
        const ifToken = this.advance();
216✔
1955
        const startingRange = ifToken.range;
216✔
1956

1957
        const condition = this.expression();
216✔
1958
        let thenBranch: Block;
1959
        let elseBranch: IfStatement | Block | undefined;
1960

1961
        let thenToken: Token | undefined;
1962
        let endIfToken: Token | undefined;
1963
        let elseToken: Token | undefined;
1964

1965
        //optional `then`
1966
        if (this.check(TokenKind.Then)) {
214✔
1967
            thenToken = this.advance();
149✔
1968
        }
1969

1970
        //is it inline or multi-line if?
1971
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
214✔
1972

1973
        if (isInlineIfThen) {
214✔
1974
            /*** PARSE INLINE IF STATEMENT ***/
1975

1976
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
32✔
1977

1978
            if (!thenBranch) {
32!
1979
                this.diagnostics.push({
×
1980
                    ...DiagnosticMessages.expectedStatementToFollowConditionalCondition(ifToken.text),
1981
                    range: this.peek().range
1982
                });
1983
                throw this.lastDiagnosticAsError();
×
1984
            } else {
1985
                this.ensureInline(thenBranch.statements);
32✔
1986
            }
1987

1988
            //else branch
1989
            if (this.check(TokenKind.Else)) {
32✔
1990
                elseToken = this.advance();
19✔
1991

1992
                if (this.check(TokenKind.If)) {
19✔
1993
                    // recurse-read `else if`
1994
                    elseBranch = this.ifStatement();
4✔
1995

1996
                    //no multi-line if chained with an inline if
1997
                    if (!elseBranch.isInline) {
4✔
1998
                        this.diagnostics.push({
2✔
1999
                            ...DiagnosticMessages.expectedInlineIfStatement(),
2000
                            range: elseBranch.range
2001
                        });
2002
                    }
2003

2004
                } else if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
15!
2005
                    //expecting inline else branch
2006
                    this.diagnostics.push({
×
2007
                        ...DiagnosticMessages.expectedInlineIfStatement(),
2008
                        range: this.peek().range
2009
                    });
2010
                    throw this.lastDiagnosticAsError();
×
2011
                } else {
2012
                    elseBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
15✔
2013

2014
                    if (elseBranch) {
15!
2015
                        this.ensureInline(elseBranch.statements);
15✔
2016
                    }
2017
                }
2018

2019
                if (!elseBranch) {
19!
2020
                    //missing `else` branch
2021
                    this.diagnostics.push({
×
2022
                        ...DiagnosticMessages.expectedStatementToFollowElse(),
2023
                        range: this.peek().range
2024
                    });
2025
                    throw this.lastDiagnosticAsError();
×
2026
                }
2027
            }
2028

2029
            if (!elseBranch || !isIfStatement(elseBranch)) {
32✔
2030
                //enforce newline at the end of the inline if statement
2031
                const peek = this.peek();
28✔
2032
                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && !this.isAtEnd()) {
28✔
2033
                    //ignore last error if it was about a colon
2034
                    if (this.previous().kind === TokenKind.Colon) {
3!
2035
                        this.diagnostics.pop();
3✔
2036
                        this.current--;
3✔
2037
                    }
2038
                    //newline is required
2039
                    this.diagnostics.push({
3✔
2040
                        ...DiagnosticMessages.expectedFinalNewline(),
2041
                        range: this.peek().range
2042
                    });
2043
                }
2044
            }
2045

2046
        } else {
2047
            /*** PARSE MULTI-LINE IF STATEMENT ***/
2048

2049
            thenBranch = this.blockConditionalBranch(ifToken);
182✔
2050

2051
            //ensure newline/colon before next keyword
2052
            this.ensureNewLineOrColon();
180✔
2053

2054
            //else branch
2055
            if (this.check(TokenKind.Else)) {
180✔
2056
                elseToken = this.advance();
92✔
2057

2058
                if (this.check(TokenKind.If)) {
92✔
2059
                    // recurse-read `else if`
2060
                    elseBranch = this.ifStatement();
40✔
2061

2062
                } else {
2063
                    elseBranch = this.blockConditionalBranch(ifToken);
52✔
2064

2065
                    //ensure newline/colon before next keyword
2066
                    this.ensureNewLineOrColon();
52✔
2067
                }
2068
            }
2069

2070
            if (!isIfStatement(elseBranch)) {
180✔
2071
                if (this.check(TokenKind.EndIf)) {
140✔
2072
                    endIfToken = this.advance();
138✔
2073

2074
                } else {
2075
                    //missing endif
2076
                    this.diagnostics.push({
2✔
2077
                        ...DiagnosticMessages.expectedEndIfToCloseIfStatement(startingRange.start),
2078
                        range: ifToken.range
2079
                    });
2080
                }
2081
            }
2082
        }
2083

2084
        return new IfStatement(
212✔
2085
            {
2086
                if: ifToken,
2087
                then: thenToken,
2088
                endIf: endIfToken,
2089
                else: elseToken
2090
            },
2091
            condition,
2092
            thenBranch,
2093
            elseBranch,
2094
            isInlineIfThen
2095
        );
2096
    }
2097

2098
    //consume a `then` or `else` branch block of an `if` statement
2099
    private blockConditionalBranch(ifToken: Token) {
2100
        //keep track of the current error count, because if the then branch fails,
2101
        //we will trash them in favor of a single error on if
2102
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
234✔
2103

2104
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
2105
        // a trailing "end if" or "else if"
2106
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
234✔
2107

2108
        if (!branch) {
234✔
2109
            //throw out any new diagnostics created as a result of a `then` block parse failure.
2110
            //the block() function will discard the current line, so any discarded diagnostics will
2111
            //resurface if they are legitimate, and not a result of a malformed if statement
2112
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
2✔
2113

2114
            //this whole if statement is bogus...add error to the if token and hard-fail
2115
            this.diagnostics.push({
2✔
2116
                ...DiagnosticMessages.expectedEndIfElseIfOrElseToTerminateThenBlock(),
2117
                range: ifToken.range
2118
            });
2119
            throw this.lastDiagnosticAsError();
2✔
2120
        }
2121
        return branch;
232✔
2122
    }
2123

2124
    private ensureNewLineOrColon(silent = false) {
232✔
2125
        const prev = this.previous().kind;
454✔
2126
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
454✔
2127
            if (!silent) {
138✔
2128
                this.diagnostics.push({
6✔
2129
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2130
                    range: this.peek().range
2131
                });
2132
            }
2133
            return false;
138✔
2134
        }
2135
        return true;
316✔
2136
    }
2137

2138
    //ensure each statement of an inline block is single-line
2139
    private ensureInline(statements: Statement[]) {
2140
        for (const stat of statements) {
47✔
2141
            if (isIfStatement(stat) && !stat.isInline) {
54✔
2142
                this.diagnostics.push({
2✔
2143
                    ...DiagnosticMessages.expectedInlineIfStatement(),
2144
                    range: stat.range
2145
                });
2146
            }
2147
        }
2148
    }
2149

2150
    //consume inline branch of an `if` statement
2151
    private inlineConditionalBranch(...additionalTerminators: BlockTerminator[]): Block | undefined {
2152
        let statements = [];
54✔
2153
        //attempt to get the next statement without using `this.declaration`
2154
        //which seems a bit hackish to get to work properly
2155
        let statement = this.statement();
54✔
2156
        if (!statement) {
54!
2157
            return undefined;
×
2158
        }
2159
        statements.push(statement);
54✔
2160
        const startingRange = statement.range;
54✔
2161

2162
        //look for colon statement separator
2163
        let foundColon = false;
54✔
2164
        while (this.match(TokenKind.Colon)) {
54✔
2165
            foundColon = true;
12✔
2166
        }
2167

2168
        //if a colon was found, add the next statement or err if unexpected
2169
        if (foundColon) {
54✔
2170
            if (!this.checkAny(TokenKind.Newline, ...additionalTerminators)) {
12✔
2171
                //if not an ending keyword, add next statement
2172
                let extra = this.inlineConditionalBranch(...additionalTerminators);
7✔
2173
                if (!extra) {
7!
2174
                    return undefined;
×
2175
                }
2176
                statements.push(...extra.statements);
7✔
2177
            } else {
2178
                //error: colon before next keyword
2179
                const colon = this.previous();
5✔
2180
                this.diagnostics.push({
5✔
2181
                    ...DiagnosticMessages.unexpectedToken(colon.text),
2182
                    range: colon.range
2183
                });
2184
            }
2185
        }
2186
        return new Block(statements, startingRange);
54✔
2187
    }
2188

2189
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2190
        let expressionStart = this.peek();
396✔
2191

2192
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
396✔
2193
            let operator = this.advance();
20✔
2194

2195
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
20✔
2196
                this.diagnostics.push({
1✔
2197
                    ...DiagnosticMessages.consecutiveIncrementDecrementOperatorsAreNotAllowed(),
2198
                    range: this.peek().range
2199
                });
2200
                throw this.lastDiagnosticAsError();
1✔
2201
            } else if (isCallExpression(expr)) {
19✔
2202
                this.diagnostics.push({
1✔
2203
                    ...DiagnosticMessages.incrementDecrementOperatorsAreNotAllowedAsResultOfFunctionCall(),
2204
                    range: expressionStart.range
2205
                });
2206
                throw this.lastDiagnosticAsError();
1✔
2207
            }
2208

2209
            const result = new IncrementStatement(expr, operator);
18✔
2210
            this._references.expressions.add(result);
18✔
2211
            return result;
18✔
2212
        }
2213

2214
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
376✔
2215
            return new ExpressionStatement(expr);
301✔
2216
        }
2217

2218

2219
        //you're not allowed to do dottedGet or XmlAttrGet after a function call
2220
        if (isDottedGetExpression(expr)) {
75✔
2221
            this.diagnostics.push({
21✔
2222
                ...DiagnosticMessages.propAccessNotPermittedAfterFunctionCallInExpressionStatement('Property'),
2223
                range: util.createBoundingRange(expr.dot, expr.name)
2224
            });
2225
            //we can recover gracefully here even though it's invalid syntax
2226
            return new ExpressionStatement(expr);
21✔
2227

2228
            //you're not allowed to do indexedGet expressions after a function call
2229
        } else if (isIndexedGetExpression(expr)) {
54✔
2230
            this.diagnostics.push({
1✔
2231
                ...DiagnosticMessages.propAccessNotPermittedAfterFunctionCallInExpressionStatement('Index'),
2232
                range: util.createBoundingRange(expr.openingSquare, expr.index, expr.closingSquare)
2233
            });
2234
            //we can recover gracefully here even though it's invalid syntax
2235
            return new ExpressionStatement(expr);
1✔
2236
            //you're not allowed to do XmlAttrGet after a function call
2237
        } else if (isXmlAttributeGetExpression(expr)) {
53✔
2238
            this.diagnostics.push({
1✔
2239
                ...DiagnosticMessages.propAccessNotPermittedAfterFunctionCallInExpressionStatement('XML attribute'),
2240
                range: util.createBoundingRange(expr.at, expr.name)
2241
            });
2242
            //we can recover gracefully here even though it's invalid syntax
2243
            return new ExpressionStatement(expr);
1✔
2244
        }
2245

2246

2247
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an assignment
2248
        this.diagnostics.push({
52✔
2249
            ...DiagnosticMessages.expectedStatementOrFunctionCallButReceivedExpression(),
2250
            range: expressionStart.range
2251
        });
2252

2253
        throw this.lastDiagnosticAsError();
52✔
2254
    }
2255

2256
    private setStatement(): DottedSetStatement | IndexedSetStatement | ExpressionStatement | IncrementStatement | AssignmentStatement {
2257
        /**
2258
         * Attempts to find an expression-statement or an increment statement.
2259
         * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
2260
         * statements aren't valid expressions. They _do_ however fall under the same parsing
2261
         * priority as standalone function calls though, so we can parse them in the same way.
2262
         */
2263
        let expr = this.call();
718✔
2264
        if (this.checkAny(...AssignmentOperators) && !(isCallExpression(expr))) {
675✔
2265
            let left = expr;
282✔
2266
            let operator = this.advance();
282✔
2267
            let right = this.expression();
282✔
2268

2269
            // Create a dotted or indexed "set" based on the left-hand side's type
2270
            if (isIndexedGetExpression(left)) {
282✔
2271
                return new IndexedSetStatement(
40✔
2272
                    left.obj,
2273
                    left.index,
2274
                    operator.kind === TokenKind.Equal
2275
                        ? right
40✔
2276
                        : new BinaryExpression(left, operator, right),
2277
                    left.openingSquare,
2278
                    left.closingSquare,
2279
                    left.additionalIndexes,
2280
                    operator.kind === TokenKind.Equal
2281
                        ? operator
40✔
2282
                        : { kind: TokenKind.Equal, text: '=', range: operator.range }
2283
                );
2284
            } else if (isDottedGetExpression(left)) {
242✔
2285
                return new DottedSetStatement(
239✔
2286
                    left.obj,
2287
                    left.name,
2288
                    operator.kind === TokenKind.Equal
2289
                        ? right
239✔
2290
                        : new BinaryExpression(left, operator, right),
2291
                    left.dot,
2292
                    operator.kind === TokenKind.Equal
2293
                        ? operator
239✔
2294
                        : { kind: TokenKind.Equal, text: '=', range: operator.range }
2295
                );
2296
            }
2297
        }
2298
        return this.expressionStatement(expr);
396✔
2299
    }
2300

2301
    private printStatement(): PrintStatement {
2302
        let printKeyword = this.advance();
730✔
2303

2304
        let values: (
2305
            | Expression
2306
            | PrintSeparatorTab
2307
            | PrintSeparatorSpace)[] = [];
730✔
2308

2309
        while (!this.checkEndOfStatement()) {
730✔
2310
            if (this.check(TokenKind.Semicolon)) {
814✔
2311
                values.push(this.advance() as PrintSeparatorSpace);
20✔
2312
            } else if (this.check(TokenKind.Comma)) {
794✔
2313
                values.push(this.advance() as PrintSeparatorTab);
13✔
2314
            } else if (this.check(TokenKind.Else)) {
781✔
2315
                break; // inline branch
7✔
2316
            } else {
2317
                values.push(this.expression());
774✔
2318
            }
2319
        }
2320

2321
        //print statements can be empty, so look for empty print conditions
2322
        if (!values.length) {
729✔
2323
            let emptyStringLiteral = createStringLiteral('');
4✔
2324
            values.push(emptyStringLiteral);
4✔
2325
        }
2326

2327
        let last = values[values.length - 1];
729✔
2328
        if (isToken(last)) {
729✔
2329
            // TODO: error, expected value
2330
        }
2331

2332
        return new PrintStatement({ print: printKeyword }, values);
729✔
2333
    }
2334

2335
    /**
2336
     * Parses a return statement with an optional return value.
2337
     * @returns an AST representation of a return statement.
2338
     */
2339
    private returnStatement(): ReturnStatement {
2340
        let tokens = { return: this.previous() };
251✔
2341

2342
        if (this.checkEndOfStatement()) {
251✔
2343
            return new ReturnStatement(tokens);
24✔
2344
        }
2345

2346
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
227✔
2347
        return new ReturnStatement(tokens, toReturn);
226✔
2348
    }
2349

2350
    /**
2351
     * Parses a `label` statement
2352
     * @returns an AST representation of an `label` statement.
2353
     */
2354
    private labelStatement() {
2355
        let tokens = {
12✔
2356
            identifier: this.advance(),
2357
            colon: this.advance()
2358
        };
2359

2360
        //label must be alone on its line, this is probably not a label
2361
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
12✔
2362
            //rewind and cancel
2363
            this.current -= 2;
2✔
2364
            throw new CancelStatementError();
2✔
2365
        }
2366

2367
        return new LabelStatement(tokens);
10✔
2368
    }
2369

2370
    /**
2371
     * Parses a `continue` statement
2372
     */
2373
    private continueStatement() {
2374
        return new ContinueStatement({
12✔
2375
            continue: this.advance(),
2376
            loopType: this.tryConsume(
2377
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2378
                TokenKind.While, TokenKind.For
2379
            )
2380
        });
2381
    }
2382

2383
    /**
2384
     * Parses a `goto` statement
2385
     * @returns an AST representation of an `goto` statement.
2386
     */
2387
    private gotoStatement() {
2388
        let tokens = {
12✔
2389
            goto: this.advance(),
2390
            label: this.consume(
2391
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2392
                TokenKind.Identifier
2393
            )
2394
        };
2395

2396
        return new GotoStatement(tokens);
10✔
2397
    }
2398

2399
    /**
2400
     * Parses an `end` statement
2401
     * @returns an AST representation of an `end` statement.
2402
     */
2403
    private endStatement() {
2404
        let endTokens = { end: this.advance() };
8✔
2405

2406
        return new EndStatement(endTokens);
8✔
2407
    }
2408
    /**
2409
     * Parses a `stop` statement
2410
     * @returns an AST representation of a `stop` statement
2411
     */
2412
    private stopStatement() {
2413
        let tokens = { stop: this.advance() };
16✔
2414

2415
        return new StopStatement(tokens);
16✔
2416
    }
2417

2418
    /**
2419
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2420
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2421
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2422
     *                    ignored.
2423
     */
2424
    private block(...terminators: BlockTerminator[]): Block | undefined {
2425
        const parentAnnotations = this.enterAnnotationBlock();
2,449✔
2426

2427
        this.consumeStatementSeparators(true);
2,449✔
2428
        let startingToken = this.peek();
2,449✔
2429

2430
        const statements: Statement[] = [];
2,449✔
2431
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators)) {
2,449✔
2432
            //grab the location of the current token
2433
            let loopCurrent = this.current;
2,753✔
2434
            let dec = this.declaration();
2,753✔
2435
            if (dec) {
2,753✔
2436
                if (!isAnnotationExpression(dec)) {
2,673✔
2437
                    this.consumePendingAnnotations(dec);
2,666✔
2438
                    statements.push(dec);
2,666✔
2439
                }
2440

2441
                //ensure statement separator
2442
                this.consumeStatementSeparators();
2,673✔
2443

2444
            } else {
2445
                //something went wrong. reset to the top of the loop
2446
                this.current = loopCurrent;
80✔
2447

2448
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2449
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
80✔
2450

2451
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2452
                //but there's already an error in the file being parsed, so just leave this line here
2453
                this.advance();
80✔
2454

2455
                //consume potential separators
2456
                this.consumeStatementSeparators(true);
80✔
2457
            }
2458
        }
2459

2460
        if (this.isAtEnd()) {
2,449✔
2461
            return undefined;
6✔
2462
            // TODO: Figure out how to handle unterminated blocks well
2463
        } else if (terminators.length > 0) {
2,443✔
2464
            //did we hit end-sub / end-function while looking for some other terminator?
2465
            //if so, we need to restore the statement separator
2466
            let prev = this.previous().kind;
385✔
2467
            let peek = this.peek().kind;
385✔
2468
            if (
385✔
2469
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
774!
2470
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2471
            ) {
2472
                this.current--;
6✔
2473
            }
2474
        }
2475

2476
        this.exitAnnotationBlock(parentAnnotations);
2,443✔
2477
        return new Block(statements, startingToken.range);
2,443✔
2478
    }
2479

2480
    /**
2481
     * Attach pending annotations to the provided statement,
2482
     * and then reset the annotations array
2483
     */
2484
    consumePendingAnnotations(statement: Statement) {
2485
        if (this.pendingAnnotations.length) {
6,903✔
2486
            statement.annotations = this.pendingAnnotations;
45✔
2487
            this.pendingAnnotations = [];
45✔
2488
        }
2489
    }
2490

2491
    enterAnnotationBlock() {
2492
        const pending = this.pendingAnnotations;
5,973✔
2493
        this.pendingAnnotations = [];
5,973✔
2494
        return pending;
5,973✔
2495
    }
2496

2497
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2498
        // non consumed annotations are an error
2499
        if (this.pendingAnnotations.length) {
5,966✔
2500
            for (const annotation of this.pendingAnnotations) {
4✔
2501
                this.diagnostics.push({
6✔
2502
                    ...DiagnosticMessages.unusedAnnotation(),
2503
                    range: annotation.range
2504
                });
2505
            }
2506
        }
2507
        this.pendingAnnotations = parentAnnotations;
5,966✔
2508
    }
2509

2510
    private expression(findTypeCast = true): Expression {
4,545✔
2511
        let expression = this.anonymousFunction();
4,800✔
2512
        let asToken: Token;
2513
        let typeToken: Token;
2514
        if (findTypeCast) {
4,760✔
2515
            do {
4,505✔
2516
                if (this.check(TokenKind.As)) {
4,522✔
2517
                    this.warnIfNotBrighterScriptMode('type cast');
17✔
2518
                    // Check if this expression is wrapped in any type casts
2519
                    // allows for multiple casts:
2520
                    // myVal = foo() as dynamic as string
2521

2522
                    asToken = this.advance();
17✔
2523
                    typeToken = this.typeToken();
17✔
2524
                    if (asToken && typeToken) {
17!
2525
                        expression = new TypeCastExpression(expression, asToken, typeToken);
17✔
2526
                    }
2527
                } else {
2528
                    break;
4,505✔
2529
                }
2530

2531
            } while (asToken && typeToken);
34✔
2532
        }
2533
        this._references.expressions.add(expression);
4,760✔
2534
        return expression;
4,760✔
2535
    }
2536

2537
    private anonymousFunction(): Expression {
2538
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
4,800✔
2539
            const func = this.functionDeclaration(true);
82✔
2540
            //if there's an open paren after this, this is an IIFE
2541
            if (this.check(TokenKind.LeftParen)) {
82✔
2542
                return this.finishCall(this.advance(), func);
3✔
2543
            } else {
2544
                return func;
79✔
2545
            }
2546
        }
2547

2548
        let expr = this.boolean();
4,718✔
2549

2550
        if (this.check(TokenKind.Question)) {
4,678✔
2551
            return this.ternaryExpression(expr);
94✔
2552
        } else if (this.check(TokenKind.QuestionQuestion)) {
4,584✔
2553
            return this.nullCoalescingExpression(expr);
30✔
2554
        } else {
2555
            return expr;
4,554✔
2556
        }
2557
    }
2558

2559
    private boolean(): Expression {
2560
        let expr = this.relational();
4,718✔
2561

2562
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
4,678✔
2563
            let operator = this.previous();
30✔
2564
            this.consumeNewlinesIfAllowed();
30✔
2565
            let right = this.relational();
30✔
2566
            this.addExpressionsToReferences(expr, right);
30✔
2567
            expr = new BinaryExpression(expr, operator, right);
30✔
2568
        }
2569

2570
        return expr;
4,678✔
2571
    }
2572

2573
    private relational(): Expression {
2574
        let expr = this.additive();
4,764✔
2575

2576
        while (
4,724✔
2577
            this.matchAny(
2578
                TokenKind.Equal,
2579
                TokenKind.LessGreater,
2580
                TokenKind.Greater,
2581
                TokenKind.GreaterEqual,
2582
                TokenKind.Less,
2583
                TokenKind.LessEqual
2584
            )
2585
        ) {
2586
            let operator = this.previous();
163✔
2587
            this.consumeNewlinesIfAllowed();
163✔
2588
            let right = this.additive();
163✔
2589
            this.addExpressionsToReferences(expr, right);
163✔
2590
            expr = new BinaryExpression(expr, operator, right);
163✔
2591
        }
2592

2593
        return expr;
4,724✔
2594
    }
2595

2596
    private addExpressionsToReferences(...expressions: Expression[]) {
2597
        for (const expression of expressions) {
371✔
2598
            if (!isBinaryExpression(expression)) {
696✔
2599
                this.references.expressions.add(expression);
654✔
2600
            }
2601
        }
2602
    }
2603

2604
    // TODO: bitshift
2605

2606
    private additive(): Expression {
2607
        let expr = this.multiplicative();
4,927✔
2608

2609
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
4,889✔
2610
            let operator = this.previous();
102✔
2611
            this.consumeNewlinesIfAllowed();
102✔
2612
            let right = this.multiplicative();
102✔
2613
            this.addExpressionsToReferences(expr, right);
100✔
2614
            expr = new BinaryExpression(expr, operator, right);
100✔
2615
        }
2616

2617
        return expr;
4,887✔
2618
    }
2619

2620
    private multiplicative(): Expression {
2621
        let expr = this.exponential();
5,029✔
2622

2623
        while (this.matchAny(
4,989✔
2624
            TokenKind.Forwardslash,
2625
            TokenKind.Backslash,
2626
            TokenKind.Star,
2627
            TokenKind.Mod,
2628
            TokenKind.LeftShift,
2629
            TokenKind.RightShift
2630
        )) {
2631
            let operator = this.previous();
25✔
2632
            this.consumeNewlinesIfAllowed();
25✔
2633
            let right = this.exponential();
25✔
2634
            this.addExpressionsToReferences(expr, right);
25✔
2635
            expr = new BinaryExpression(expr, operator, right);
25✔
2636
        }
2637

2638
        return expr;
4,989✔
2639
    }
2640

2641
    private exponential(): Expression {
2642
        let expr = this.prefixUnary();
5,054✔
2643

2644
        while (this.match(TokenKind.Caret)) {
5,014✔
2645
            let operator = this.previous();
7✔
2646
            this.consumeNewlinesIfAllowed();
7✔
2647
            let right = this.prefixUnary();
7✔
2648
            this.addExpressionsToReferences(expr, right);
7✔
2649
            expr = new BinaryExpression(expr, operator, right);
7✔
2650
        }
2651

2652
        return expr;
5,014✔
2653
    }
2654

2655
    private prefixUnary(): Expression {
2656
        const nextKind = this.peek().kind;
5,083✔
2657
        if (nextKind === TokenKind.Not) {
5,083✔
2658
            this.current++; //advance
16✔
2659
            let operator = this.previous();
16✔
2660
            let right = this.relational();
16✔
2661
            return new UnaryExpression(operator, right);
16✔
2662
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
5,067✔
2663
            this.current++; //advance
22✔
2664
            let operator = this.previous();
22✔
2665
            let right = this.prefixUnary();
22✔
2666
            return new UnaryExpression(operator, right);
22✔
2667
        }
2668
        return this.call();
5,045✔
2669
    }
2670

2671
    private indexedGet(expr: Expression) {
2672
        let openingSquare = this.previous();
149✔
2673
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
149✔
2674
        let indexes: Expression[] = [];
149✔
2675

2676

2677
        //consume leading newlines
2678
        while (this.match(TokenKind.Newline)) { }
149✔
2679

2680
        try {
149✔
2681
            indexes.push(
149✔
2682
                this.expression()
2683
            );
2684
            //consume additional indexes separated by commas
2685
            while (this.check(TokenKind.Comma)) {
148✔
2686
                //discard the comma
2687
                this.advance();
17✔
2688
                indexes.push(
17✔
2689
                    this.expression()
2690
                );
2691
            }
2692
        } catch (error) {
2693
            this.rethrowNonDiagnosticError(error);
1✔
2694
        }
2695
        //consume trailing newlines
2696
        while (this.match(TokenKind.Newline)) { }
149✔
2697

2698
        const closingSquare = this.tryConsume(
149✔
2699
            DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
2700
            TokenKind.RightSquareBracket
2701
        );
2702

2703
        return new IndexedGetExpression(expr, indexes.shift(), openingSquare, closingSquare, questionDotToken, indexes);
149✔
2704
    }
2705

2706
    private newExpression() {
2707
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
45✔
2708
        let newToken = this.advance();
45✔
2709

2710
        let nameExpr = this.getNamespacedVariableNameExpression();
45✔
2711
        let leftParen = this.consume(
45✔
2712
            DiagnosticMessages.unexpectedToken(this.peek().text),
2713
            TokenKind.LeftParen,
2714
            TokenKind.QuestionLeftParen
2715
        );
2716
        let call = this.finishCall(leftParen, nameExpr);
41✔
2717
        //pop the call from the  callExpressions list because this is technically something else
2718
        this.callExpressions.pop();
41✔
2719
        let result = new NewExpression(newToken, call);
41✔
2720
        this._references.newExpressions.push(result);
41✔
2721
        return result;
41✔
2722
    }
2723

2724
    /**
2725
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2726
     */
2727
    private callfunc(callee: Expression): Expression {
2728
        this.warnIfNotBrighterScriptMode('callfunc operator');
25✔
2729
        let operator = this.previous();
25✔
2730
        let methodName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
25✔
2731
        // force it into an identifier so the AST makes some sense
2732
        methodName.kind = TokenKind.Identifier;
24✔
2733
        let openParen = this.consume(DiagnosticMessages.expectedOpenParenToFollowCallfuncIdentifier(), TokenKind.LeftParen);
24✔
2734
        let call = this.finishCall(openParen, callee, false);
24✔
2735

2736
        return new CallfuncExpression(callee, operator, methodName as Identifier, openParen, call.args, call.closingParen);
24✔
2737
    }
2738

2739
    private call(): Expression {
2740
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
5,763✔
2741
            return this.newExpression();
45✔
2742
        }
2743
        let expr = this.primary();
5,718✔
2744
        //an expression to keep for _references
2745
        let referenceCallExpression: Expression;
2746
        while (true) {
5,640✔
2747
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
7,469✔
2748
                expr = this.finishCall(this.previous(), expr);
609✔
2749
                //store this call expression in references
2750
                referenceCallExpression = expr;
609✔
2751

2752
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
6,860✔
2753
                expr = this.indexedGet(expr);
147✔
2754

2755
            } else if (this.match(TokenKind.Callfunc)) {
6,713✔
2756
                expr = this.callfunc(expr);
25✔
2757
                //store this callfunc expression in references
2758
                referenceCallExpression = expr;
24✔
2759

2760
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
6,688✔
2761
                if (this.match(TokenKind.LeftSquareBracket)) {
1,073✔
2762
                    expr = this.indexedGet(expr);
2✔
2763
                } else {
2764
                    let dot = this.previous();
1,071✔
2765
                    let name = this.tryConsume(
1,071✔
2766
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2767
                        TokenKind.Identifier,
2768
                        ...AllowedProperties
2769
                    );
2770
                    if (!name) {
1,071✔
2771
                        break;
24✔
2772
                    }
2773

2774
                    // force it into an identifier so the AST makes some sense
2775
                    name.kind = TokenKind.Identifier;
1,047✔
2776
                    expr = new DottedGetExpression(expr, name as Identifier, dot);
1,047✔
2777

2778
                    this.addPropertyHints(name);
1,047✔
2779
                }
2780

2781
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
5,615✔
2782
                let dot = this.advance();
12✔
2783
                let name = this.tryConsume(
12✔
2784
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2785
                    TokenKind.Identifier,
2786
                    ...AllowedProperties
2787
                );
2788

2789
                // force it into an identifier so the AST makes some sense
2790
                name.kind = TokenKind.Identifier;
12✔
2791
                if (!name) {
12!
2792
                    break;
×
2793
                }
2794
                expr = new XmlAttributeGetExpression(expr, name as Identifier, dot);
12✔
2795
                //only allow a single `@` expression
2796
                break;
12✔
2797

2798
            } else {
2799
                break;
5,603✔
2800
            }
2801
        }
2802
        //if we found a callExpression, add it to `expressions` in references
2803
        if (referenceCallExpression) {
5,639✔
2804
            this._references.expressions.add(referenceCallExpression);
595✔
2805
        }
2806
        return expr;
5,639✔
2807
    }
2808

2809
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
653✔
2810
        let args = [] as Expression[];
701✔
2811
        this.consumeNewlinesIfAllowed();
701✔
2812

2813
        if (!this.check(TokenKind.RightParen)) {
701✔
2814
            do {
360✔
2815
                this.consumeNewlinesIfAllowed();
540✔
2816

2817
                if (args.length >= CallExpression.MaximumArguments) {
540!
2818
                    this.diagnostics.push({
×
2819
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
2820
                        range: this.peek().range
2821
                    });
2822
                    throw this.lastDiagnosticAsError();
×
2823
                }
2824
                try {
540✔
2825
                    args.push(this.expression());
540✔
2826
                } catch (error) {
2827
                    this.rethrowNonDiagnosticError(error);
7✔
2828
                    // we were unable to get an expression, so don't continue
2829
                    break;
7✔
2830
                }
2831
            } while (this.match(TokenKind.Comma));
2832
        }
2833

2834
        this.consumeNewlinesIfAllowed();
701✔
2835

2836
        const closingParen = this.tryConsume(
701✔
2837
            DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
2838
            TokenKind.RightParen
2839
        );
2840

2841
        let expression = new CallExpression(callee, openingParen, closingParen, args);
701✔
2842
        if (addToCallExpressionList) {
701✔
2843
            this.callExpressions.push(expression);
653✔
2844
        }
2845
        return expression;
701✔
2846
    }
2847

2848
    /**
2849
     * Tries to get the next token as a type
2850
     * Allows for built-in types (double, string, etc.) or namespaced custom types in Brighterscript mode
2851
     * Will return a token of whatever is next to be parsed
2852
     * Will allow v1 type syntax (typed arrays, union types), but there is no validation on types used this way
2853
     */
2854
    private typeToken(ignoreDiagnostics = false): Token {
773✔
2855
        let typeToken: Token;
2856
        let lookForCompounds = true;
778✔
2857
        let isACompound = false;
778✔
2858
        let resultToken;
2859
        while (lookForCompounds) {
778✔
2860
            lookForCompounds = false;
828✔
2861

2862
            const isTypedFunction = this.checkAny(TokenKind.Function, TokenKind.Sub) && this.checkNext(TokenKind.LeftParen);
828✔
2863

2864
            if (this.checkAny(...DeclarableTypes) && !isTypedFunction) {
828✔
2865
                // Token is a built in type
2866
                typeToken = this.advance();
641✔
2867
            } else if (this.options.mode === ParseMode.BrighterScript) {
187✔
2868
                try {
159✔
2869
                    if (this.check(TokenKind.LeftCurlyBrace)) {
159✔
2870
                        // could be an inline interface
2871
                        typeToken = this.inlineInterface();
20✔
2872
                    } else if (this.check(TokenKind.LeftParen)) {
139✔
2873
                        // could be an inline interface
2874
                        typeToken = this.groupedTypeExpression();
12✔
2875
                    } else if (isTypedFunction) {
127✔
2876
                        //typed function type
2877
                        typeToken = this.typedFunctionType();
10✔
2878
                    } else {
2879
                        // see if we can get a namespaced identifer
2880
                        const qualifiedType = this.getNamespacedVariableNameExpression(ignoreDiagnostics);
117✔
2881
                        typeToken = createToken(TokenKind.Identifier, qualifiedType.getName(this.options.mode), qualifiedType.range);
111✔
2882
                    }
2883
                } catch {
2884
                    //could not get an identifier - just get whatever's next
2885
                    typeToken = this.advance();
6✔
2886
                }
2887
            } else {
2888
                // just get whatever's next
2889
                typeToken = this.advance();
28✔
2890
            }
2891
            resultToken = resultToken ?? typeToken;
828✔
2892
            if (resultToken && this.options.mode === ParseMode.BrighterScript) {
828✔
2893
                // check for brackets for typed arrays
2894
                while (this.check(TokenKind.LeftSquareBracket) && this.peekNext().kind === TokenKind.RightSquareBracket) {
702✔
2895
                    const leftBracket = this.advance();
16✔
2896
                    const rightBracket = this.advance();
16✔
2897
                    typeToken = createToken(TokenKind.Identifier, typeToken.text + leftBracket.text + rightBracket.text, util.createBoundingRange(typeToken, leftBracket, rightBracket));
16✔
2898
                    resultToken = createToken(TokenKind.Dynamic, null, typeToken.range);
16✔
2899
                }
2900

2901
                if (this.checkAny(TokenKind.Or, TokenKind.And)) {
702✔
2902
                    lookForCompounds = true;
50✔
2903
                    let orToken = this.advance();
50✔
2904
                    resultToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(resultToken, typeToken, orToken));
50✔
2905
                    isACompound = true;
50✔
2906
                }
2907
            }
2908
        }
2909
        if (isACompound) {
778✔
2910
            resultToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(resultToken, typeToken));
43✔
2911
        }
2912
        return resultToken;
778✔
2913
    }
2914

2915
    private inlineInterface() {
2916
        const openToken = this.advance();
20✔
2917
        const memberTokens: Token[] = [];
20✔
2918
        memberTokens.push(openToken);
20✔
2919
        while (this.matchAny(TokenKind.Newline, TokenKind.Comment)) { }
20✔
2920
        while (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral, TokenKind.Optional)) {
20✔
2921
            let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
26✔
2922
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral)) {
26!
2923
                if (this.check(TokenKind.As)) {
26!
2924
                    if (this.checkAnyNext(TokenKind.Comment, TokenKind.Newline)) {
×
2925
                        // as <EOL>
2926
                        // `as` is the field name
2927
                    } else if (this.checkNext(TokenKind.As)) {
×
2928
                        //  as as ____
2929
                        // first `as` is the field name
2930
                    } else if (optionalKeyword) {
×
2931
                        // optional as ____
2932
                        // optional is the field name, `as` starts type
2933
                        // rewind current token
2934
                        optionalKeyword = null;
×
2935
                        this.current--;
×
2936
                    }
2937
                }
2938
            } else {
2939
                // no name after `optional` ... optional is the name
2940
                // rewind current token
2941
                optionalKeyword = null;
×
2942
                this.current--;
×
2943
            }
2944
            if (optionalKeyword) {
26✔
2945
                memberTokens.push(optionalKeyword);
1✔
2946
            }
2947
            if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers, TokenKind.StringLiteral)) {
26!
2948
                this.diagnostics.push({
×
2949
                    ...DiagnosticMessages.unexpectedToken(this.peek().text),
2950
                    range: this.peek().range
2951
                });
2952
                throw this.lastDiagnosticAsError();
×
2953
            }
2954
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties, TokenKind.StringLiteral)) {
26!
2955
                this.advance();
26✔
2956
            } else {
2957
                this.diagnostics.push({
×
2958
                    ...DiagnosticMessages.unexpectedToken(this.peek().text),
2959
                    range: this.peek().range
2960
                });
2961
                throw this.lastDiagnosticAsError();
×
2962
            }
2963

2964
            if (this.check(TokenKind.As)) {
26✔
2965
                memberTokens.push(this.advance()); // as
24✔
2966
                memberTokens.push(this.typeToken()); // type
24✔
2967
            }
2968
            while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) { }
26✔
2969
        }
2970
        if (!this.check(TokenKind.RightCurlyBrace)) {
20!
2971
            this.diagnostics.push({
×
2972
                ...DiagnosticMessages.unexpectedToken(this.peek().text),
2973
                range: this.peek().range
2974
            });
2975
            throw this.lastDiagnosticAsError();
×
2976
        }
2977
        const closeToken = this.advance();
20✔
2978
        memberTokens.push(closeToken);
20✔
2979

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

2982
        return completeInlineInterfaceToken;
20✔
2983
    }
2984

2985
    private groupedTypeExpression() {
2986
        const leftParen = this.advance();
12✔
2987
        const typeToken = this.typeToken();
12✔
2988
        const rightParen = this.consume(
12✔
2989
            DiagnosticMessages.expectedToken(TokenKind.RightParen),
2990
            TokenKind.RightParen
2991
        );
2992
        return createToken(TokenKind.Dynamic, null, util.createBoundingRange(leftParen, typeToken, rightParen));
12✔
2993
    }
2994

2995
    private typedFunctionType() {
2996
        const funcOrSub = this.advance();
10✔
2997
        const leftParen = this.advance();
10✔
2998

2999
        let params = [] as FunctionParameterExpression[];
10✔
3000
        if (!this.check(TokenKind.RightParen)) {
10✔
3001
            do {
4✔
3002
                params.push(this.functionParameter());
7✔
3003
            } while (this.match(TokenKind.Comma));
3004
        }
3005
        const rightParen = this.advance();
10✔
3006
        let asToken: Token;
3007
        let returnType: Token;
3008
        if ((this.check(TokenKind.As))) {
10✔
3009
            // this is a function type with a return type, e.g. `function(string) as void`
3010
            asToken = this.advance();
9✔
3011
            returnType = this.typeToken();
9✔
3012
        }
3013

3014
        return createToken(TokenKind.Function, null, util.createBoundingRange(funcOrSub, leftParen, rightParen, asToken, returnType));
10✔
3015
    }
3016

3017
    private primary(): Expression {
3018
        switch (true) {
5,718✔
3019
            case this.matchAny(
5,718!
3020
                TokenKind.False,
3021
                TokenKind.True,
3022
                TokenKind.Invalid,
3023
                TokenKind.IntegerLiteral,
3024
                TokenKind.LongIntegerLiteral,
3025
                TokenKind.FloatLiteral,
3026
                TokenKind.DoubleLiteral,
3027
                TokenKind.StringLiteral
3028
            ):
3029
                return new LiteralExpression(this.previous());
3,296✔
3030

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

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

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

3043
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3044
                return new VariableExpression(this.previous() as Identifier);
1,778✔
3045

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

3055
            case this.matchAny(TokenKind.LeftSquareBracket):
3056
                return this.arrayLiteral();
131✔
3057

3058
            case this.match(TokenKind.LeftCurlyBrace):
3059
                return this.aaLiteral();
262✔
3060

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

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

3070
            case this.check(TokenKind.RegexLiteral):
3071
                return this.regexLiteralExpression();
45✔
3072

3073
            case this.check(TokenKind.Comment):
3074
                return new CommentStatement([this.advance()]);
3✔
3075

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

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

3092
    private arrayLiteral() {
3093
        let elements: Array<Expression | CommentStatement> = [];
131✔
3094
        let openingSquare = this.previous();
131✔
3095

3096
        //add any comment found right after the opening square
3097
        if (this.check(TokenKind.Comment)) {
131✔
3098
            elements.push(new CommentStatement([this.advance()]));
1✔
3099
        }
3100

3101
        while (this.match(TokenKind.Newline)) {
131✔
3102
        }
3103
        let closingSquare: Token;
3104

3105
        if (!this.match(TokenKind.RightSquareBracket)) {
131✔
3106
            try {
99✔
3107
                elements.push(this.expression());
99✔
3108

3109
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
98✔
3110
                    if (this.checkPrevious(TokenKind.Comment) || this.check(TokenKind.Comment)) {
146✔
3111
                        let comment = this.check(TokenKind.Comment) ? this.advance() : this.previous();
4✔
3112
                        elements.push(new CommentStatement([comment]));
4✔
3113
                    }
3114
                    while (this.match(TokenKind.Newline)) {
146✔
3115

3116
                    }
3117

3118
                    if (this.check(TokenKind.RightSquareBracket)) {
146✔
3119
                        break;
33✔
3120
                    }
3121

3122
                    elements.push(this.expression());
113✔
3123
                }
3124
            } catch (error: any) {
3125
                this.rethrowNonDiagnosticError(error);
2✔
3126
            }
3127

3128
            closingSquare = this.tryConsume(
99✔
3129
                DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral(),
3130
                TokenKind.RightSquareBracket
3131
            );
3132
        } else {
3133
            closingSquare = this.previous();
32✔
3134
        }
3135

3136
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3137
        return new ArrayLiteralExpression(elements, openingSquare, closingSquare);
131✔
3138
    }
3139

3140
    private aaLiteral() {
3141
        let openingBrace = this.previous();
262✔
3142
        let members: Array<AAMemberExpression | AAIndexedMemberExpression | CommentStatement> = [];
262✔
3143

3144
        let key = () => {
262✔
3145
            let result = {
269✔
3146
                colon: null as Token,
3147
                keyToken: null as Token,
3148
                key: null as Expression,
3149
                leftBracket: null as Token,
3150
                rightBracket: null as Token,
3151
                range: null as Range
3152
            };
3153
            if (this.check(TokenKind.LeftSquareBracket)) {
269✔
3154
                // Computed key: [expr]
3155
                result.leftBracket = this.advance();
31✔
3156
                result.key = this.expression();
31✔
3157
                result.rightBracket = this.tryConsumeToken(TokenKind.RightSquareBracket);
31✔
3158
            } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
238✔
3159
                result.keyToken = this.identifier(...AllowedProperties);
188✔
3160
            } else if (this.check(TokenKind.StringLiteral)) {
50!
3161
                result.keyToken = this.advance();
50✔
3162
            } else {
3163
                this.diagnostics.push({
×
3164
                    ...DiagnosticMessages.unexpectedAAKey(),
3165
                    range: this.peek().range
3166
                });
3167
                throw this.lastDiagnosticAsError();
×
3168
            }
3169

3170
            result.colon = this.consume(
269✔
3171
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3172
                TokenKind.Colon
3173
            );
3174
            result.range = util.getRange(result.keyToken ?? result.leftBracket, result.colon);
268✔
3175
            return result;
268✔
3176
        };
3177

3178
        while (this.match(TokenKind.Newline)) { }
262✔
3179
        let closingBrace: Token;
3180
        if (!this.match(TokenKind.RightCurlyBrace)) {
262✔
3181
            let lastAAMember: AAMemberExpression | AAIndexedMemberExpression;
3182
            try {
206✔
3183
                if (this.check(TokenKind.Comment)) {
206✔
3184
                    lastAAMember = null;
7✔
3185
                    members.push(new CommentStatement([this.advance()]));
7✔
3186
                } else {
3187
                    let k = key();
199✔
3188
                    let expr = this.expression();
199✔
3189
                    lastAAMember = k.key
198✔
3190
                        ? new AAIndexedMemberExpression({ leftBracket: k.leftBracket, key: k.key, rightBracket: k.rightBracket, colon: k.colon, value: expr })
198✔
3191
                        : new AAMemberExpression(k.keyToken, k.colon, expr);
3192
                    members.push(lastAAMember);
198✔
3193
                }
3194

3195
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
205✔
3196
                    // collect comma at end of expression
3197
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
236✔
3198
                        lastAAMember.commaToken = this.previous();
44✔
3199
                    }
3200

3201
                    //check for comment at the end of the current line
3202
                    if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
236✔
3203
                        let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
14✔
3204
                        members.push(new CommentStatement([token]));
14✔
3205
                    } else {
3206
                        this.consumeStatementSeparators(true);
222✔
3207

3208
                        //check for a comment on its own line
3209
                        if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
222✔
3210
                            let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
1!
3211
                            lastAAMember = null;
1✔
3212
                            members.push(new CommentStatement([token]));
1✔
3213
                            continue;
1✔
3214
                        }
3215

3216
                        if (this.check(TokenKind.RightCurlyBrace)) {
221✔
3217
                            break;
151✔
3218
                        }
3219
                        let k = key();
70✔
3220
                        let expr = this.expression();
69✔
3221
                        lastAAMember = k.key
69✔
3222
                            ? new AAIndexedMemberExpression({ leftBracket: k.leftBracket, key: k.key, rightBracket: k.rightBracket, colon: k.colon, value: expr })
69✔
3223
                            : new AAMemberExpression(k.keyToken, k.colon, expr);
3224
                        members.push(lastAAMember);
69✔
3225
                    }
3226
                }
3227
            } catch (error: any) {
3228
                this.rethrowNonDiagnosticError(error);
2✔
3229
            }
3230

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

3239
        const aaExpr = new AALiteralExpression(members, openingBrace, closingBrace);
262✔
3240
        this.addPropertyHints(aaExpr);
262✔
3241
        return aaExpr;
262✔
3242
    }
3243

3244
    /**
3245
     * Pop token if we encounter specified token
3246
     */
3247
    private match(tokenKind: TokenKind) {
3248
        if (this.check(tokenKind)) {
21,903✔
3249
            this.current++; //advance
1,762✔
3250
            return true;
1,762✔
3251
        }
3252
        return false;
20,141✔
3253
    }
3254

3255
    /**
3256
     * Pop token if we encounter a token in the specified list
3257
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3258
     */
3259
    private matchAny(...tokenKinds: TokenKind[]) {
3260
        for (let tokenKind of tokenKinds) {
79,479✔
3261
            if (this.check(tokenKind)) {
235,472✔
3262
                this.current++; //advance
20,166✔
3263
                return true;
20,166✔
3264
            }
3265
        }
3266
        return false;
59,313✔
3267
    }
3268

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

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

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

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

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

3321
        if (foundTokenKind) {
11,827✔
3322
            return this.advance();
11,732✔
3323
        }
3324
        this.diagnostics.push({
95✔
3325
            ...diagnostic,
3326
            range: this.peek().range
3327
        });
3328
    }
3329

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

3337
    private consumeStatementSeparators(optional = false) {
4,351✔
3338
        //a comment or EOF mark the end of the statement
3339
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
15,456✔
3340
            return true;
646✔
3341
        }
3342
        let consumed = false;
14,810✔
3343
        //consume any newlines and colons
3344
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
14,810✔
3345
            consumed = true;
12,384✔
3346
        }
3347
        if (!optional && !consumed) {
14,810✔
3348
            this.diagnostics.push({
32✔
3349
                ...DiagnosticMessages.expectedNewlineOrColon(),
3350
                range: this.peek().range
3351
            });
3352
        }
3353
        return consumed;
14,810✔
3354
    }
3355

3356
    private advance(): Token {
3357
        if (!this.isAtEnd()) {
26,826✔
3358
            this.current++;
26,807✔
3359
        }
3360
        return this.previous();
26,826✔
3361
    }
3362

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

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

3372
    /**
3373
     * Check that the next token kind is the expected kind
3374
     * @param tokenKind the expected next kind
3375
     * @returns true if the next tokenKind is the expected value
3376
     */
3377
    private check(tokenKind: TokenKind): boolean {
3378
        const nextKind = this.peek().kind;
402,074✔
3379
        if (nextKind === TokenKind.Eof) {
402,074✔
3380
            return false;
8,144✔
3381
        }
3382
        return nextKind === tokenKind;
393,930✔
3383
    }
3384

3385
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3386
        const nextKind = this.peek().kind;
49,717✔
3387
        if (nextKind === TokenKind.Eof) {
49,717✔
3388
            return false;
215✔
3389
        }
3390
        return tokenKinds.includes(nextKind);
49,502✔
3391
    }
3392

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

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

3408
    private isAtEnd(): boolean {
3409
        return this.peek().kind === TokenKind.Eof;
73,504✔
3410
    }
3411

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

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

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

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

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

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

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

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

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

3485
    /**
3486
     * References are found during the initial parse.
3487
     * However, sometimes plugins can modify the AST, requiring a full walk to re-compute all references.
3488
     * This does that walk.
3489
     */
3490
    private findReferences() {
3491
        this._references = new References();
7✔
3492
        const excludedExpressions = new Set<Expression>();
7✔
3493

3494
        const visitCallExpression = (e: CallExpression | CallfuncExpression) => {
7✔
3495
            for (const p of e.args) {
14✔
3496
                this._references.expressions.add(p);
7✔
3497
            }
3498
            //add calls that were not excluded (from loop below)
3499
            if (!excludedExpressions.has(e)) {
14✔
3500
                this._references.expressions.add(e);
12✔
3501
            }
3502

3503
            //if this call is part of a longer expression that includes a call higher up, find that higher one and remove it
3504
            if (e.callee) {
14!
3505
                let node: Expression = e.callee;
14✔
3506
                while (node) {
14✔
3507
                    //the primary goal for this loop. If we found a parent call expression, remove it from `references`
3508
                    if (isCallExpression(node)) {
22✔
3509
                        this.references.expressions.delete(node);
2✔
3510
                        excludedExpressions.add(node);
2✔
3511
                        //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.
3512
                        break;
2✔
3513

3514
                        //when we hit a variable expression, we're definitely at the leftmost expression so stop
3515
                    } else if (isVariableExpression(node)) {
20✔
3516
                        break;
12✔
3517
                        //if
3518

3519
                    } else if (isDottedGetExpression(node) || isIndexedGetExpression(node)) {
8!
3520
                        node = node.obj;
8✔
3521
                    } else {
3522
                        //some expression we don't understand. log it and quit the loop
3523
                        this.logger.info('Encountered unknown expression while calculating function expression chain', node);
×
3524
                        break;
×
3525
                    }
3526
                }
3527
            }
3528
        };
3529

3530
        this.ast.walk(createVisitor({
7✔
3531
            AssignmentStatement: s => {
3532
                this._references.assignmentStatements.push(s);
11✔
3533
                this.references.expressions.add(s.value);
11✔
3534
            },
3535
            ClassStatement: s => {
3536
                this._references.classStatements.push(s);
1✔
3537
            },
3538
            ClassFieldStatement: s => {
3539
                if (s.initialValue) {
1!
3540
                    this._references.expressions.add(s.initialValue);
1✔
3541
                }
3542
            },
3543
            NamespaceStatement: s => {
3544
                this._references.namespaceStatements.push(s);
×
3545
            },
3546
            FunctionStatement: s => {
3547
                this._references.functionStatements.push(s);
4✔
3548
            },
3549
            ImportStatement: s => {
3550
                this._references.importStatements.push(s);
1✔
3551
            },
3552
            LibraryStatement: s => {
3553
                this._references.libraryStatements.push(s);
×
3554
            },
3555
            FunctionExpression: (expression, parent) => {
3556
                if (!isMethodStatement(parent)) {
4!
3557
                    this._references.functionExpressions.push(expression);
4✔
3558
                }
3559
            },
3560
            NewExpression: e => {
3561
                this._references.newExpressions.push(e);
×
3562
                for (const p of e.call.args) {
×
3563
                    this._references.expressions.add(p);
×
3564
                }
3565
            },
3566
            ExpressionStatement: s => {
3567
                this._references.expressions.add(s.expression);
7✔
3568
            },
3569
            CallfuncExpression: e => {
3570
                visitCallExpression(e);
1✔
3571
            },
3572
            CallExpression: e => {
3573
                visitCallExpression(e);
13✔
3574
            },
3575
            AALiteralExpression: e => {
3576
                this.addPropertyHints(e);
8✔
3577
                this._references.expressions.add(e);
8✔
3578
                for (const member of e.elements) {
8✔
3579
                    if (isAAMemberExpression(member)) {
16!
3580
                        this._references.expressions.add(member.value);
16✔
3581
                    } else if (isAAIndexedMemberExpression(member)) {
×
3582
                        this._references.expressions.add(member.value);
×
3583
                        this._references.expressions.add(member.key);
×
3584
                    }
3585
                }
3586
            },
3587
            BinaryExpression: (e, parent) => {
3588
                //walk the chain of binary expressions and add each one to the list of expressions
3589
                const expressions: Expression[] = [e];
14✔
3590
                let expression: Expression;
3591
                while ((expression = expressions.pop())) {
14✔
3592
                    if (isBinaryExpression(expression)) {
64✔
3593
                        expressions.push(expression.left, expression.right);
25✔
3594
                    } else {
3595
                        this._references.expressions.add(expression);
39✔
3596
                    }
3597
                }
3598
            },
3599
            ArrayLiteralExpression: e => {
3600
                for (const element of e.elements) {
1✔
3601
                    //keep everything except comments
3602
                    if (!isCommentStatement(element)) {
1!
3603
                        this._references.expressions.add(element);
1✔
3604
                    }
3605
                }
3606
            },
3607
            DottedGetExpression: e => {
3608
                this.addPropertyHints(e.name);
23✔
3609
            },
3610
            DottedSetStatement: e => {
3611
                this.addPropertyHints(e.name);
4✔
3612
            },
3613
            EnumStatement: e => {
3614
                this._references.enumStatements.push(e);
×
3615
            },
3616
            ConstStatement: s => {
3617
                this._references.constStatements.push(s);
×
3618
            },
3619
            UnaryExpression: e => {
3620
                this._references.expressions.add(e);
×
3621
            },
3622
            IncrementStatement: e => {
3623
                this._references.expressions.add(e);
2✔
3624
            },
3625
            TypeStatement: (s) => {
3626
                this._references.typeStatements.push(s);
×
3627
            }
3628
        }), {
3629
            walkMode: WalkMode.visitAllRecursive
3630
        });
3631
    }
3632

3633
    public dispose() {
3634
    }
3635
}
3636

3637
export enum ParseMode {
1✔
3638
    BrightScript = 'BrightScript',
1✔
3639
    BrighterScript = 'BrighterScript'
1✔
3640
}
3641

3642
export interface ParseOptions {
3643
    /**
3644
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3645
     */
3646
    mode?: ParseMode;
3647
    /**
3648
     * A logger that should be used for logging. If omitted, a default logger is used
3649
     */
3650
    logger?: Logger;
3651
    /**
3652
     * Should locations be tracked. If false, the `range` property will be omitted
3653
     * @default true
3654
     */
3655
    trackLocations?: boolean;
3656
}
3657

3658
export class References {
1✔
3659
    private cache = new Cache();
2,525✔
3660
    public assignmentStatements = [] as AssignmentStatement[];
2,525✔
3661
    public classStatements = [] as ClassStatement[];
2,525✔
3662

3663
    public get classStatementLookup() {
3664
        if (!this._classStatementLookup) {
88✔
3665
            this._classStatementLookup = new Map();
32✔
3666
            for (const stmt of this.classStatements) {
32✔
3667
                this._classStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
3✔
3668
            }
3669
        }
3670
        return this._classStatementLookup;
88✔
3671
    }
3672
    private _classStatementLookup: Map<string, ClassStatement>;
3673

3674
    public functionExpressions = [] as FunctionExpression[];
2,525✔
3675
    public functionStatements = [] as FunctionStatement[];
2,525✔
3676
    /**
3677
     * A map of function statements, indexed by fully-namespaced lower function name.
3678
     */
3679
    public get functionStatementLookup() {
3680
        if (!this._functionStatementLookup) {
88✔
3681
            this._functionStatementLookup = new Map();
32✔
3682
            for (const stmt of this.functionStatements) {
32✔
3683
                this._functionStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
30✔
3684
            }
3685
        }
3686
        return this._functionStatementLookup;
88✔
3687
    }
3688
    private _functionStatementLookup: Map<string, FunctionStatement>;
3689

3690
    public interfaceStatements = [] as InterfaceStatement[];
2,525✔
3691

3692
    public get interfaceStatementLookup() {
3693
        if (!this._interfaceStatementLookup) {
×
3694
            this._interfaceStatementLookup = new Map();
×
3695
            for (const stmt of this.interfaceStatements) {
×
3696
                this._interfaceStatementLookup.set(stmt.fullName.toLowerCase(), stmt);
×
3697
            }
3698
        }
3699
        return this._interfaceStatementLookup;
×
3700
    }
3701
    private _interfaceStatementLookup: Map<string, InterfaceStatement>;
3702

3703
    public enumStatements = [] as EnumStatement[];
2,525✔
3704

3705
    public get enumStatementLookup() {
3706
        return this.cache.getOrAdd('enums', () => {
89✔
3707
            const result = new Map<string, EnumStatement>();
33✔
3708
            for (const stmt of this.enumStatements) {
33✔
3709
                result.set(stmt.fullName.toLowerCase(), stmt);
1✔
3710
            }
3711
            return result;
33✔
3712
        });
3713
    }
3714

3715
    public constStatements = [] as ConstStatement[];
2,525✔
3716

3717
    public get constStatementLookup() {
3718
        return this.cache.getOrAdd('consts', () => {
×
3719
            const result = new Map<string, ConstStatement>();
×
3720
            for (const stmt of this.constStatements) {
×
3721
                result.set(stmt.fullName.toLowerCase(), stmt);
×
3722
            }
3723
            return result;
×
3724
        });
3725
    }
3726

3727
    /**
3728
     * A collection of full expressions. This excludes intermediary expressions.
3729
     *
3730
     * Example 1:
3731
     * `a.b.c` is composed of `a` (variableExpression)  `.b` (DottedGetExpression) `.c` (DottedGetExpression)
3732
     * This will only contain the final `.c` DottedGetExpression because `.b` and `a` can both be derived by walking back from the `.c` DottedGetExpression.
3733
     *
3734
     * Example 2:
3735
     * `name.space.doSomething(a.b.c)` will result in 2 entries in this list. the `CallExpression` for `doSomething`, and the `.c` DottedGetExpression.
3736
     *
3737
     * Example 3:
3738
     * `value = SomeEnum.value > 2 or SomeEnum.otherValue < 10` will result in 4 entries. `SomeEnum.value`, `2`, `SomeEnum.otherValue`, `10`
3739
     */
3740
    public expressions = new Set<Expression>();
2,525✔
3741

3742
    public importStatements = [] as ImportStatement[];
2,525✔
3743
    public libraryStatements = [] as LibraryStatement[];
2,525✔
3744
    public namespaceStatements = [] as NamespaceStatement[];
2,525✔
3745
    public typeStatements = [] as TypeStatement[];
2,525✔
3746
    public newExpressions = [] as NewExpression[];
2,525✔
3747
    public propertyHints = {} as Record<string, string>;
2,525✔
3748
}
3749

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

© 2026 Coveralls, Inc