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

rokucommunity / brighterscript / #15439

24 Mar 2026 07:33PM UTC coverage: 88.983% (-0.01%) from 88.993%
#15439

push

web-flow
Merge 5b04f945e into 0c894b16d

7957 of 9428 branches covered (84.4%)

Branch coverage included in aggregate %.

72 of 78 new or added lines in 5 files covered. (92.31%)

2 existing lines in 2 files now uncovered.

10199 of 10976 relevant lines covered (92.92%)

1961.03 hits per line

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

92.14
/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,300✔
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,300✔
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;
8,912✔
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) {
49,600✔
142
            this.findReferences();
7✔
143
        }
144
        return this._references;
49,600✔
145
    }
146

147
    private _references = new References();
2,300✔
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,274✔
158
            const name = item.text;
1,031✔
159
            this._references.propertyHints[name.toLowerCase()] = name;
1,031✔
160
        } else {
161
            for (const member of item.elements) {
243✔
162
                if (!isCommentStatement(member) && isAAMemberExpression(member) && member.keyToken) {
281✔
163
                    const name = member.keyToken.text;
230✔
164
                    if (!name.startsWith('"')) {
230✔
165
                        this._references.propertyHints[name.toLowerCase()] = name;
198✔
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
    private globalTerminators = [] as TokenKind[][];
2,300✔
188

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

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

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

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

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

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

241
        this.ast = this.body();
2,274✔
242

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

248
    private logger: Logger;
249

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

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

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

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

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

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

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

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

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

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

332
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
4,265✔
333
                return this.constDeclaration();
92✔
334
            }
335

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

490
        if (this.peek().text.toLowerCase() === 'extends') {
66✔
491
            extendsToken = this.advance();
2✔
492
            parentInterfaceName = this.getNamespacedVariableNameExpression();
2✔
493
        }
494
        this.consumeStatementSeparators();
66✔
495
        //gather up all interface members (Fields, Methods)
496
        let body = [] as Statement[];
66✔
497
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
66✔
498
            try {
167✔
499
                //break out of this loop if we encountered the `EndInterface` token not followed by `as`
500
                if (this.check(TokenKind.EndInterface) && !this.checkNext(TokenKind.As)) {
167✔
501
                    break;
66✔
502
                }
503

504
                let decl: Statement;
505

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

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

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

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

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

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

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

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

565
        const parentAnnotations = this.enterAnnotationBlock();
126✔
566

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

572
        result.tokens.name = this.tryIdentifier(...this.allowedLocalIdentifiers);
126✔
573

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

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

585
                //members
586
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
221✔
587
                    decl = this.enumMemberStatement();
215✔
588

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

898
            if (this.check(TokenKind.As)) {
1,872✔
899
                asToken = this.advance();
119✔
900

901
                typeToken = this.typeToken();
119✔
902

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

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

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

922
            this.consumeStatementSeparators(true);
1,872✔
923

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

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

941
            this._references.functionExpressions.push(func);
1,872✔
942

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1058
        let operator = this.consume(
967✔
1059
            DiagnosticMessages.expectedOperatorAfterIdentifier(AssignmentOperators, name.text),
1060
            ...AssignmentOperators
1061
        );
1062
        let value = this.expression();
965✔
1063

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

1082
        this._references.assignmentStatements.push(result);
958✔
1083
        return result;
958✔
1084
    }
1085

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1209
        if (this.match(TokenKind.Return)) {
2,835✔
1210
            return this.returnStatement();
231✔
1211
        }
1212

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

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

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

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

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

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

1276
        if (this.check(TokenKind.Namespace)) {
1,084✔
1277
            return this.namespaceStatement();
267✔
1278
        }
1279

1280
        if (this.check(TokenKind.Enum)) {
817✔
1281
            return this.enumDeclaration();
126✔
1282
        }
1283

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

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

1292
        this.consumeStatementSeparators();
22✔
1293

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

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

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

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

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

1325
        //TODO: newline allowed?
1326

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

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

1339
        this.consumeStatementSeparators();
33✔
1340

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

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

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

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

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

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

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

1400
        this.consumeStatementSeparators();
23✔
1401

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

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

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

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

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

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

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

1451
        this.namespaceAndFunctionDepth++;
267✔
1452

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

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

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

1472
        this.namespaceAndFunctionDepth--;
266✔
1473

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2178

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

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

2206

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2401
                //ensure statement separator
2402
                this.consumeStatementSeparators();
2,523✔
2403

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

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

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

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

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

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

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

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

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

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

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

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

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

2508
        let expr = this.boolean();
4,504✔
2509

2510
        if (this.check(TokenKind.Question)) {
4,468✔
2511
            return this.ternaryExpression(expr);
94✔
2512
        } else if (this.check(TokenKind.QuestionQuestion)) {
4,374✔
2513
            return this.nullCoalescingExpression(expr);
30✔
2514
        } else {
2515
            return expr;
4,344✔
2516
        }
2517
    }
2518

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

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

2529
        return expr;
4,468✔
2530
    }
2531

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

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

2551
        return expr;
4,512✔
2552
    }
2553

2554
    private addExpressionsToReferences(...expressions: Expression[]) {
2555
        for (const expression of expressions) {
339✔
2556
            if (!isBinaryExpression(expression)) {
632✔
2557
                this.references.expressions.add(expression);
590✔
2558
            }
2559
        }
2560
    }
2561

2562
    // TODO: bitshift
2563

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

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

2574
        return expr;
4,662✔
2575
    }
2576

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

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

2594
        return expr;
4,750✔
2595
    }
2596

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

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

2607
        return expr;
4,771✔
2608
    }
2609

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

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

2631

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

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

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

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

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

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

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

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

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

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

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

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

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

2733
                    this.addPropertyHints(name);
1,004✔
2734
                }
2735

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

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

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

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

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

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

2789
        while (this.match(TokenKind.Newline)) { }
679✔
2790

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

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

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

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

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

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

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

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

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

2937
        return completeInlineInterfaceToken;
20✔
2938
    }
2939

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

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

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

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

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

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

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

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

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

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

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

3013
            case this.match(TokenKind.LeftCurlyBrace):
3014
                return this.aaLiteral();
235✔
3015

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

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

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

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

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

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

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

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

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

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

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

3071
                    }
3072

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

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

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

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

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

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

3125
            result.colon = this.consume(
245✔
3126
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3127
                TokenKind.Colon
3128
            );
3129
            result.range = util.getRange(result.keyToken ?? result.leftBracket, result.colon);
244✔
3130
            return result;
244✔
3131
        };
3132

3133
        while (this.match(TokenKind.Newline)) { }
235✔
3134
        let closingBrace: Token;
3135
        if (!this.match(TokenKind.RightCurlyBrace)) {
235✔
3136
            let lastAAMember: AAMemberExpression | AAIndexedMemberExpression;
3137
            try {
187✔
3138
                if (this.check(TokenKind.Comment)) {
187✔
3139
                    lastAAMember = null;
7✔
3140
                    members.push(new CommentStatement([this.advance()]));
7✔
3141
                } else {
3142
                    let k = key();
180✔
3143
                    let expr = this.expression();
180✔
3144
                    lastAAMember = k.key
179✔
3145
                        ? new AAIndexedMemberExpression({ leftBracket: k.leftBracket, key: k.key, rightBracket: k.rightBracket, colon: k.colon, value: expr })
179✔
3146
                        : new AAMemberExpression(k.keyToken, k.colon, expr);
3147
                    members.push(lastAAMember);
179✔
3148
                }
3149

3150
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
186✔
3151
                    // collect comma at end of expression
3152
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
227✔
3153
                        lastAAMember.commaToken = this.previous();
42✔
3154
                    }
3155

3156
                    //check for comment at the end of the current line
3157
                    if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
227✔
3158
                        let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
14✔
3159
                        members.push(new CommentStatement([token]));
14✔
3160
                    } else {
3161
                        this.consumeStatementSeparators(true);
213✔
3162

3163
                        //check for a comment on its own line
3164
                        if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
213✔
3165
                            let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
1!
3166
                            lastAAMember = null;
1✔
3167
                            members.push(new CommentStatement([token]));
1✔
3168
                            continue;
1✔
3169
                        }
3170

3171
                        if (this.check(TokenKind.RightCurlyBrace)) {
212✔
3172
                            break;
147✔
3173
                        }
3174
                        let k = key();
65✔
3175
                        let expr = this.expression();
64✔
3176
                        lastAAMember = k.key
64✔
3177
                            ? new AAIndexedMemberExpression({ leftBracket: k.leftBracket, key: k.key, rightBracket: k.rightBracket, colon: k.colon, value: expr })
64✔
3178
                            : new AAMemberExpression(k.keyToken, k.colon, expr);
3179
                        members.push(lastAAMember);
64✔
3180
                    }
3181
                }
3182
            } catch (error: any) {
3183
                this.rethrowNonDiagnosticError(error);
2✔
3184
            }
3185

3186
            closingBrace = this.tryConsume(
187✔
3187
                DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral(),
3188
                TokenKind.RightCurlyBrace
3189
            );
3190
        } else {
3191
            closingBrace = this.previous();
48✔
3192
        }
3193

3194
        const aaExpr = new AALiteralExpression(members, openingBrace, closingBrace);
235✔
3195
        this.addPropertyHints(aaExpr);
235✔
3196
        return aaExpr;
235✔
3197
    }
3198

3199
    /**
3200
     * Pop token if we encounter specified token
3201
     */
3202
    private match(tokenKind: TokenKind) {
3203
        if (this.check(tokenKind)) {
21,485✔
3204
            this.current++; //advance
1,665✔
3205
            return true;
1,665✔
3206
        }
3207
        return false;
19,820✔
3208
    }
3209

3210
    /**
3211
     * Pop token if we encounter a token in the specified list
3212
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3213
     */
3214
    private matchAny(...tokenKinds: TokenKind[]) {
3215
        for (let tokenKind of tokenKinds) {
74,814✔
3216
            if (this.check(tokenKind)) {
221,927✔
3217
                this.current++; //advance
18,884✔
3218
                return true;
18,884✔
3219
            }
3220
        }
3221
        return false;
55,930✔
3222
    }
3223

3224
    /**
3225
     * If the next series of tokens matches the given set of tokens, pop them all
3226
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
3227
     */
3228
    private matchSequence(...tokenKinds: TokenKind[]) {
3229
        const endIndex = this.current + tokenKinds.length;
6,405✔
3230
        for (let i = 0; i < tokenKinds.length; i++) {
6,405✔
3231
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
6,429!
3232
                return false;
6,402✔
3233
            }
3234
        }
3235
        this.current = endIndex;
3✔
3236
        return true;
3✔
3237
    }
3238

3239
    /**
3240
     * Get next token matching a specified list, or fail with an error
3241
     */
3242
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
3243
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
7,497✔
3244
        if (token) {
7,497✔
3245
            return token;
7,476✔
3246
        } else {
3247
            let error = new Error(diagnosticInfo.message);
21✔
3248
            (error as any).isDiagnostic = true;
21✔
3249
            throw error;
21✔
3250
        }
3251
    }
3252

3253
    /**
3254
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3255
     */
3256
    private consumeTokenIf(tokenKind: TokenKind) {
3257
        if (this.match(tokenKind)) {
311✔
3258
            return this.previous();
12✔
3259
        }
3260
    }
3261

3262
    private consumeToken(tokenKind: TokenKind) {
3263
        return this.consume(
384✔
3264
            DiagnosticMessages.expectedToken(tokenKind),
3265
            tokenKind
3266
        );
3267
    }
3268

3269
    /**
3270
     * Consume, or add a message if not found. But then continue and return undefined
3271
     */
3272
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3273
        const nextKind = this.peek().kind;
11,015✔
3274
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
33,809✔
3275

3276
        if (foundTokenKind) {
11,015✔
3277
            return this.advance();
10,922✔
3278
        }
3279
        this.diagnostics.push({
93✔
3280
            ...diagnostic,
3281
            range: this.peek().range
3282
        });
3283
    }
3284

3285
    private tryConsumeToken(tokenKind: TokenKind) {
3286
        return this.tryConsume(
123✔
3287
            DiagnosticMessages.expectedToken(tokenKind),
3288
            tokenKind
3289
        );
3290
    }
3291

3292
    private consumeStatementSeparators(optional = false) {
4,126✔
3293
        //a comment or EOF mark the end of the statement
3294
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
14,225✔
3295
            return true;
596✔
3296
        }
3297
        let consumed = false;
13,629✔
3298
        //consume any newlines and colons
3299
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
13,629✔
3300
            consumed = true;
11,454✔
3301
        }
3302
        if (!optional && !consumed) {
13,629✔
3303
            this.diagnostics.push({
38✔
3304
                ...DiagnosticMessages.expectedNewlineOrColon(),
3305
                range: this.peek().range
3306
            });
3307
        }
3308
        return consumed;
13,629✔
3309
    }
3310

3311
    private advance(): Token {
3312
        if (!this.isAtEnd()) {
24,964✔
3313
            this.current++;
24,946✔
3314
        }
3315
        return this.previous();
24,964✔
3316
    }
3317

3318
    private checkEndOfStatement(): boolean {
3319
        const nextKind = this.peek().kind;
1,713✔
3320
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
1,713✔
3321
    }
3322

3323
    private checkPrevious(tokenKind: TokenKind): boolean {
3324
        return this.previous()?.kind === tokenKind;
824!
3325
    }
3326

3327
    /**
3328
     * Check that the next token kind is the expected kind
3329
     * @param tokenKind the expected next kind
3330
     * @returns true if the next tokenKind is the expected value
3331
     */
3332
    private check(tokenKind: TokenKind): boolean {
3333
        const nextKind = this.peek().kind;
379,018✔
3334
        if (nextKind === TokenKind.Eof) {
379,018✔
3335
            return false;
7,823✔
3336
        }
3337
        return nextKind === tokenKind;
371,195✔
3338
    }
3339

3340
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3341
        const nextKind = this.peek().kind;
46,710✔
3342
        if (nextKind === TokenKind.Eof) {
46,710✔
3343
            return false;
215✔
3344
        }
3345
        return tokenKinds.includes(nextKind);
46,495✔
3346
    }
3347

3348
    private checkNext(tokenKind: TokenKind): boolean {
3349
        if (this.isAtEnd()) {
5,081!
3350
            return false;
×
3351
        }
3352
        return this.peekNext().kind === tokenKind;
5,081✔
3353
    }
3354

3355
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3356
        if (this.isAtEnd()) {
2,694!
3357
            return false;
×
3358
        }
3359
        const nextKind = this.peekNext().kind;
2,694✔
3360
        return tokenKinds.includes(nextKind);
2,694✔
3361
    }
3362

3363
    private isAtEnd(): boolean {
3364
        return this.peek().kind === TokenKind.Eof;
68,181✔
3365
    }
3366

3367
    private peekNext(): Token {
3368
        if (this.isAtEnd()) {
7,791!
3369
            return this.peek();
×
3370
        }
3371
        return this.tokens[this.current + 1];
7,791✔
3372
    }
3373

3374
    private peek(): Token {
3375
        return this.tokens[this.current];
517,142✔
3376
    }
3377

3378
    private previous(): Token {
3379
        return this.tokens[this.current - 1];
35,306✔
3380
    }
3381

3382
    /**
3383
     * Sometimes we catch an error that is a diagnostic.
3384
     * If that's the case, we want to continue parsing.
3385
     * Otherwise, re-throw the error
3386
     *
3387
     * @param error error caught in a try/catch
3388
     */
3389
    private rethrowNonDiagnosticError(error) {
3390
        if (!error.isDiagnostic) {
10!
3391
            throw error;
×
3392
        }
3393
    }
3394

3395
    /**
3396
     * Get the token that is {offset} indexes away from {this.current}
3397
     * @param offset the number of index steps away from current index to fetch
3398
     * @param tokenKinds the desired token must match one of these
3399
     * @example
3400
     * getToken(-1); //returns the previous token.
3401
     * getToken(0);  //returns current token.
3402
     * getToken(1);  //returns next token
3403
     */
3404
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3405
        const token = this.tokens[this.current + offset];
146✔
3406
        if (tokenKinds.includes(token.kind)) {
146✔
3407
            return token;
3✔
3408
        }
3409
    }
3410

3411
    private synchronize() {
3412
        this.advance(); // skip the erroneous token
117✔
3413

3414
        while (!this.isAtEnd()) {
117✔
3415
            if (this.ensureNewLineOrColon(true)) {
208✔
3416
                // end of statement reached
3417
                return;
80✔
3418
            }
3419

3420
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
128✔
3421
                case TokenKind.Namespace:
8!
3422
                case TokenKind.Class:
3423
                case TokenKind.Function:
3424
                case TokenKind.Sub:
3425
                case TokenKind.If:
3426
                case TokenKind.For:
3427
                case TokenKind.ForEach:
3428
                case TokenKind.While:
3429
                case TokenKind.Print:
3430
                case TokenKind.Return:
3431
                    // start parsing again from the next block starter or obvious
3432
                    // expression start
3433
                    return;
1✔
3434
            }
3435

3436
            this.advance();
127✔
3437
        }
3438
    }
3439

3440
    /**
3441
     * References are found during the initial parse.
3442
     * However, sometimes plugins can modify the AST, requiring a full walk to re-compute all references.
3443
     * This does that walk.
3444
     */
3445
    private findReferences() {
3446
        this._references = new References();
7✔
3447
        const excludedExpressions = new Set<Expression>();
7✔
3448

3449
        const visitCallExpression = (e: CallExpression | CallfuncExpression) => {
7✔
3450
            for (const p of e.args) {
14✔
3451
                this._references.expressions.add(p);
7✔
3452
            }
3453
            //add calls that were not excluded (from loop below)
3454
            if (!excludedExpressions.has(e)) {
14✔
3455
                this._references.expressions.add(e);
12✔
3456
            }
3457

3458
            //if this call is part of a longer expression that includes a call higher up, find that higher one and remove it
3459
            if (e.callee) {
14!
3460
                let node: Expression = e.callee;
14✔
3461
                while (node) {
14✔
3462
                    //the primary goal for this loop. If we found a parent call expression, remove it from `references`
3463
                    if (isCallExpression(node)) {
22✔
3464
                        this.references.expressions.delete(node);
2✔
3465
                        excludedExpressions.add(node);
2✔
3466
                        //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.
3467
                        break;
2✔
3468

3469
                        //when we hit a variable expression, we're definitely at the leftmost expression so stop
3470
                    } else if (isVariableExpression(node)) {
20✔
3471
                        break;
12✔
3472
                        //if
3473

3474
                    } else if (isDottedGetExpression(node) || isIndexedGetExpression(node)) {
8!
3475
                        node = node.obj;
8✔
3476
                    } else {
3477
                        //some expression we don't understand. log it and quit the loop
3478
                        this.logger.info('Encountered unknown expression while calculating function expression chain', node);
×
3479
                        break;
×
3480
                    }
3481
                }
3482
            }
3483
        };
3484

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

3588
    public dispose() {
3589
    }
3590
}
3591

3592
export enum ParseMode {
1✔
3593
    BrightScript = 'BrightScript',
1✔
3594
    BrighterScript = 'BrighterScript'
1✔
3595
}
3596

3597
export interface ParseOptions {
3598
    /**
3599
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3600
     */
3601
    mode?: ParseMode;
3602
    /**
3603
     * A logger that should be used for logging. If omitted, a default logger is used
3604
     */
3605
    logger?: Logger;
3606
    /**
3607
     * Should locations be tracked. If false, the `range` property will be omitted
3608
     * @default true
3609
     */
3610
    trackLocations?: boolean;
3611
}
3612

3613
export class References {
1✔
3614
    private cache = new Cache();
2,307✔
3615
    public assignmentStatements = [] as AssignmentStatement[];
2,307✔
3616
    public classStatements = [] as ClassStatement[];
2,307✔
3617

3618
    public get classStatementLookup() {
3619
        if (!this._classStatementLookup) {
17✔
3620
            this._classStatementLookup = new Map();
15✔
3621
            for (const stmt of this.classStatements) {
15✔
3622
                this._classStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
2✔
3623
            }
3624
        }
3625
        return this._classStatementLookup;
17✔
3626
    }
3627
    private _classStatementLookup: Map<string, ClassStatement>;
3628

3629
    public functionExpressions = [] as FunctionExpression[];
2,307✔
3630
    public functionStatements = [] as FunctionStatement[];
2,307✔
3631
    /**
3632
     * A map of function statements, indexed by fully-namespaced lower function name.
3633
     */
3634
    public get functionStatementLookup() {
3635
        if (!this._functionStatementLookup) {
17✔
3636
            this._functionStatementLookup = new Map();
15✔
3637
            for (const stmt of this.functionStatements) {
15✔
3638
                this._functionStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
13✔
3639
            }
3640
        }
3641
        return this._functionStatementLookup;
17✔
3642
    }
3643
    private _functionStatementLookup: Map<string, FunctionStatement>;
3644

3645
    public interfaceStatements = [] as InterfaceStatement[];
2,307✔
3646

3647
    public get interfaceStatementLookup() {
3648
        if (!this._interfaceStatementLookup) {
×
3649
            this._interfaceStatementLookup = new Map();
×
3650
            for (const stmt of this.interfaceStatements) {
×
3651
                this._interfaceStatementLookup.set(stmt.fullName.toLowerCase(), stmt);
×
3652
            }
3653
        }
3654
        return this._interfaceStatementLookup;
×
3655
    }
3656
    private _interfaceStatementLookup: Map<string, InterfaceStatement>;
3657

3658
    public enumStatements = [] as EnumStatement[];
2,307✔
3659

3660
    public get enumStatementLookup() {
3661
        return this.cache.getOrAdd('enums', () => {
18✔
3662
            const result = new Map<string, EnumStatement>();
16✔
3663
            for (const stmt of this.enumStatements) {
16✔
3664
                result.set(stmt.fullName.toLowerCase(), stmt);
1✔
3665
            }
3666
            return result;
16✔
3667
        });
3668
    }
3669

3670
    public constStatements = [] as ConstStatement[];
2,307✔
3671

3672
    public get constStatementLookup() {
3673
        return this.cache.getOrAdd('consts', () => {
×
3674
            const result = new Map<string, ConstStatement>();
×
3675
            for (const stmt of this.constStatements) {
×
3676
                result.set(stmt.fullName.toLowerCase(), stmt);
×
3677
            }
3678
            return result;
×
3679
        });
3680
    }
3681

3682
    /**
3683
     * A collection of full expressions. This excludes intermediary expressions.
3684
     *
3685
     * Example 1:
3686
     * `a.b.c` is composed of `a` (variableExpression)  `.b` (DottedGetExpression) `.c` (DottedGetExpression)
3687
     * This will only contain the final `.c` DottedGetExpression because `.b` and `a` can both be derived by walking back from the `.c` DottedGetExpression.
3688
     *
3689
     * Example 2:
3690
     * `name.space.doSomething(a.b.c)` will result in 2 entries in this list. the `CallExpression` for `doSomething`, and the `.c` DottedGetExpression.
3691
     *
3692
     * Example 3:
3693
     * `value = SomeEnum.value > 2 or SomeEnum.otherValue < 10` will result in 4 entries. `SomeEnum.value`, `2`, `SomeEnum.otherValue`, `10`
3694
     */
3695
    public expressions = new Set<Expression>();
2,307✔
3696

3697
    public importStatements = [] as ImportStatement[];
2,307✔
3698
    public libraryStatements = [] as LibraryStatement[];
2,307✔
3699
    public namespaceStatements = [] as NamespaceStatement[];
2,307✔
3700
    public typeStatements = [] as TypeStatement[];
2,307✔
3701
    public newExpressions = [] as NewExpression[];
2,307✔
3702
    public propertyHints = {} as Record<string, string>;
2,307✔
3703
}
3704

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

© 2026 Coveralls, Inc