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

rokucommunity / brighterscript / #15046

03 Oct 2022 01:55PM UTC coverage: 87.532% (-0.3%) from 87.808%
#15046

push

TwitchBronBron
0.59.0

5452 of 6706 branches covered (81.3%)

Branch coverage included in aggregate %.

8259 of 8958 relevant lines covered (92.2%)

1521.92 hits per line

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

92.55
/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
} from './Statement';
59
import type { DiagnosticInfo } from '../DiagnosticMessages';
60
import { DiagnosticMessages } from '../DiagnosticMessages';
1✔
61
import { util } from '../util';
1✔
62
import {
1✔
63
    AALiteralExpression,
64
    AAMemberExpression,
65
    AnnotationExpression,
66
    ArrayLiteralExpression,
67
    BinaryExpression,
68
    CallExpression,
69
    CallfuncExpression,
70
    DottedGetExpression,
71
    EscapedCharCodeLiteralExpression,
72
    FunctionExpression,
73
    FunctionParameterExpression,
74
    GroupingExpression,
75
    IndexedGetExpression,
76
    LiteralExpression,
77
    NamespacedVariableNameExpression,
78
    NewExpression,
79
    NullCoalescingExpression,
80
    RegexLiteralExpression,
81
    SourceLiteralExpression,
82
    TaggedTemplateStringExpression,
83
    TemplateStringExpression,
84
    TemplateStringQuasiExpression,
85
    TernaryExpression,
86
    UnaryExpression,
87
    VariableExpression,
88
    XmlAttributeGetExpression
89
} from './Expression';
90
import type { Diagnostic, Range } from 'vscode-languageserver';
91
import { Logger } from '../Logger';
1✔
92
import { isAAMemberExpression, isAnnotationExpression, isBinaryExpression, isCallExpression, isCallfuncExpression, isMethodStatement, isCommentStatement, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression } from '../astUtils/reflection';
1✔
93
import { createVisitor, WalkMode } from '../astUtils/visitors';
1✔
94
import { createStringLiteral, createToken } from '../astUtils/creators';
1✔
95
import { Cache } from '../Cache';
1✔
96
import type { Expression, Statement } from './AstNode';
97

98
export class Parser {
1✔
99
    /**
100
     * The array of tokens passed to `parse()`
101
     */
102
    public tokens = [] as Token[];
1,626✔
103

104
    /**
105
     * The current token index
106
     */
107
    public current: number;
108

109
    /**
110
     * The list of statements for the parsed file
111
     */
112
    public ast = new Body([]);
1,626✔
113

114
    public get statements() {
115
        return this.ast.statements;
477✔
116
    }
117

118
    /**
119
     * The top-level symbol table for the body of this file.
120
     */
121
    public get symbolTable() {
122
        return this.ast.symbolTable;
7,055✔
123
    }
124

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

139
    private _references = new References();
1,626✔
140

141
    /**
142
     * Invalidates (clears) the references collection. This should be called anytime the AST has been manipulated.
143
     */
144
    invalidateReferences() {
145
        this._references = undefined;
7✔
146
    }
147

148
    private addPropertyHints(item: Token | AALiteralExpression) {
149
        if (isToken(item)) {
951✔
150
            const name = item.text;
763✔
151
            this._references.propertyHints[name.toLowerCase()] = name;
763✔
152
        } else {
153
            for (const member of item.elements) {
188✔
154
                if (!isCommentStatement(member)) {
218✔
155
                    const name = member.keyToken.text;
197✔
156
                    if (!name.startsWith('"')) {
197✔
157
                        this._references.propertyHints[name.toLowerCase()] = name;
166✔
158
                    }
159
                }
160
            }
161
        }
162
    }
163

164
    /**
165
     * The list of diagnostics found during the parse process
166
     */
167
    public diagnostics: Diagnostic[];
168

169
    /**
170
     * The depth of the calls to function declarations. Helps some checks know if they are at the root or not.
171
     */
172
    private namespaceAndFunctionDepth: number;
173

174
    /**
175
     * The options used to parse the file
176
     */
177
    public options: ParseOptions;
178

179
    private globalTerminators = [] as TokenKind[][];
1,626✔
180

181
    /**
182
     * When a FunctionExpression has been started, this gets set. When it's done, this gets unset.
183
     * It's useful for passing the function into statements and expressions that need to be located
184
     * by function later on.
185
     */
186
    private currentFunctionExpression: FunctionExpression;
187

188
    /**
189
     * A list of identifiers that are permitted to be used as local variables. We store this in a property because we augment the list in the constructor
190
     * based on the parse mode
191
     */
192
    private allowedLocalIdentifiers: TokenKind[];
193

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

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

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

213
    /**
214
     * Parses an array of `Token`s into an abstract syntax tree
215
     * @param toParse the array of tokens to parse. May not contain any whitespace tokens
216
     * @returns the same instance of the parser which contains the diagnostics and statements
217
     */
218
    public parse(toParse: Token[] | string, options?: ParseOptions) {
219
        let tokens: Token[];
220
        if (typeof toParse === 'string') {
1,614✔
221
            tokens = Lexer.scan(toParse).tokens;
133✔
222
        } else {
223
            tokens = toParse;
1,481✔
224
        }
225
        this.logger = options?.logger ?? new Logger();
1,614✔
226
        this.tokens = tokens;
1,614✔
227
        this.options = this.sanitizeParseOptions(options);
1,614✔
228
        this.allowedLocalIdentifiers = [
1,614✔
229
            ...AllowedLocalIdentifiers,
230
            //when in plain brightscript mode, the BrighterScript source literals can be used as regular variables
231
            ...(this.options.mode === ParseMode.BrightScript ? BrighterScriptSourceLiterals : [])
1,614✔
232
        ];
233
        this.current = 0;
1,614✔
234
        this.diagnostics = [];
1,614✔
235
        this.namespaceAndFunctionDepth = 0;
1,614✔
236
        this.pendingAnnotations = [];
1,614✔
237

238
        this.ast = this.body();
1,614✔
239

240
        return this;
1,614✔
241
    }
242

243
    private logger: Logger;
244

245
    private body() {
246
        const parentAnnotations = this.enterAnnotationBlock();
1,763✔
247

248
        let body = new Body([]);
1,763✔
249
        if (this.tokens.length > 0) {
1,763✔
250
            this.consumeStatementSeparators(true);
1,762✔
251

252
            try {
1,762✔
253
                while (
1,762✔
254
                    //not at end of tokens
255
                    !this.isAtEnd() &&
6,439✔
256
                    //the next token is not one of the end terminators
257
                    !this.checkAny(...this.peekGlobalTerminators())
258
                ) {
259
                    let dec = this.declaration();
2,265✔
260
                    if (dec) {
2,265✔
261
                        if (!isAnnotationExpression(dec)) {
2,221✔
262
                            this.consumePendingAnnotations(dec);
2,199✔
263
                            body.statements.push(dec);
2,199✔
264
                            //ensure statement separator
265
                            this.consumeStatementSeparators(false);
2,199✔
266
                        } else {
267
                            this.consumeStatementSeparators(true);
22✔
268
                        }
269
                    }
270
                }
271
            } catch (parseError) {
272
                //do nothing with the parse error for now. perhaps we can remove this?
273
                console.error(parseError);
×
274
            }
275
        }
276

277
        this.exitAnnotationBlock(parentAnnotations);
1,763✔
278
        return body;
1,763✔
279
    }
280

281
    private sanitizeParseOptions(options: ParseOptions) {
282
        return {
1,614✔
283
            mode: 'brightscript',
284
            ...(options || {})
1,997✔
285
        } as ParseOptions;
286
    }
287

288
    /**
289
     * Determine if the parser is currently parsing tokens at the root level.
290
     */
291
    private isAtRootLevel() {
292
        return this.namespaceAndFunctionDepth === 0;
6,155✔
293
    }
294

295
    /**
296
     * Throws an error if the input file type is not BrighterScript
297
     */
298
    private warnIfNotBrighterScriptMode(featureName: string) {
299
        if (this.options.mode !== ParseMode.BrighterScript) {
909✔
300
            let diagnostic = {
49✔
301
                ...DiagnosticMessages.bsFeatureNotSupportedInBrsFiles(featureName),
302
                range: this.peek().range
303
            } as Diagnostic;
304
            this.diagnostics.push(diagnostic);
49✔
305
        }
306
    }
307

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

317
    private declaration(): Statement | AnnotationExpression | undefined {
318
        try {
4,119✔
319
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
4,119✔
320
                return this.functionDeclaration(false);
1,015✔
321
            }
322

323
            if (this.checkLibrary()) {
3,104✔
324
                return this.libraryStatement();
10✔
325
            }
326

327
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
3,094✔
328
                return this.constDeclaration();
25✔
329
            }
330

331
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
3,069✔
332
                return this.annotationExpression();
26✔
333
            }
334

335
            if (this.check(TokenKind.Comment)) {
3,043✔
336
                return this.commentStatement();
196✔
337
            }
338

339
            //catch certain global terminators to prevent unnecessary lookahead (i.e. like `end namespace`, no need to continue)
340
            if (this.checkAny(...this.peekGlobalTerminators())) {
2,847!
341
                return;
×
342
            }
343

344
            return this.statement();
2,847✔
345
        } catch (error: any) {
346
            //if the error is not a diagnostic, then log the error for debugging purposes
347
            if (!error.isDiagnostic) {
126!
348
                this.logger.error(error);
×
349
            }
350
            this.synchronize();
126✔
351
        }
352
    }
353

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

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

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

396
    /**
397
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration`
398
     */
399
    private interfaceFieldStatement() {
400
        const name = this.identifier(...AllowedProperties);
18✔
401
        let asToken = this.consumeToken(TokenKind.As);
18✔
402
        let typeToken = this.typeToken();
18✔
403
        const type = util.tokenToBscType(typeToken);
18✔
404

405
        if (!type) {
18!
406
            this.diagnostics.push({
×
407
                ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, typeToken.text),
408
                range: typeToken.range
409
            });
410
            throw this.lastDiagnosticAsError();
×
411
        }
412

413
        return new InterfaceFieldStatement(name, asToken, typeToken, type);
18✔
414
    }
415

416
    /**
417
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration()`
418
     */
419
    private interfaceMethodStatement() {
420
        const functionType = this.advance();
9✔
421
        const name = this.identifier(...AllowedProperties);
9✔
422
        const leftParen = this.consumeToken(TokenKind.LeftParen);
9✔
423

424
        const params = [];
9✔
425
        const rightParen = this.consumeToken(TokenKind.RightParen);
9✔
426
        let asToken = null as Token;
9✔
427
        let returnTypeToken = null as Token;
9✔
428
        if (this.check(TokenKind.As)) {
9!
429
            asToken = this.advance();
9✔
430
            returnTypeToken = this.typeToken();
9✔
431
            const returnType = util.tokenToBscType(returnTypeToken);
9✔
432
            if (!returnType) {
9!
433
                this.diagnostics.push({
×
434
                    ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, returnTypeToken.text),
435
                    range: returnTypeToken.range
436
                });
437
                throw this.lastDiagnosticAsError();
×
438
            }
439
        }
440

441
        return new InterfaceMethodStatement(
9✔
442
            functionType,
443
            name,
444
            leftParen,
445
            params,
446
            rightParen,
447
            asToken,
448
            returnTypeToken,
449
            util.tokenToBscType(returnTypeToken)
450
        );
451
    }
452

453
    private interfaceDeclaration(): InterfaceStatement {
454
        this.warnIfNotBrighterScriptMode('interface declarations');
14✔
455

456
        const parentAnnotations = this.enterAnnotationBlock();
14✔
457

458
        const interfaceToken = this.consume(
14✔
459
            DiagnosticMessages.expectedKeyword(TokenKind.Interface),
460
            TokenKind.Interface
461
        );
462
        const nameToken = this.identifier();
14✔
463

464
        let extendsToken: Token;
465
        let parentInterfaceName: NamespacedVariableNameExpression;
466

467
        if (this.peek().text.toLowerCase() === 'extends') {
14!
468
            extendsToken = this.advance();
×
469
            parentInterfaceName = this.getNamespacedVariableNameExpression();
×
470
        }
471
        this.consumeStatementSeparators();
14✔
472
        //gather up all interface members (Fields, Methods)
473
        let body = [] as Statement[];
14✔
474
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
14✔
475
            try {
30✔
476
                let decl: Statement;
477

478
                //collect leading annotations
479
                if (this.check(TokenKind.At)) {
30✔
480
                    this.annotationExpression();
2✔
481
                }
482

483
                //fields
484
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.As)) {
30✔
485
                    decl = this.interfaceFieldStatement();
18✔
486

487
                    //methods (function/sub keyword followed by opening paren)
488
                } else if (this.checkAny(TokenKind.Function, TokenKind.Sub) && this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
12✔
489
                    decl = this.interfaceMethodStatement();
9✔
490

491
                    //comments
492
                } else if (this.check(TokenKind.Comment)) {
3✔
493
                    decl = this.commentStatement();
1✔
494
                }
495

496
                if (decl) {
30✔
497
                    this.consumePendingAnnotations(decl);
28✔
498
                    body.push(decl);
28✔
499
                } else {
500
                    //we didn't find a declaration...flag tokens until next line
501
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
502
                }
503
            } catch (e) {
504
                //throw out any failed members and move on to the next line
505
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
506
            }
507

508
            //ensure statement separator
509
            this.consumeStatementSeparators();
30✔
510
            //break out of this loop if we encountered the `EndInterface` token not followed by `as`
511
            if (this.check(TokenKind.EndInterface) && !this.checkNext(TokenKind.As)) {
30✔
512
                break;
14✔
513
            }
514
        }
515

516
        //consume the final `end interface` token
517
        const endInterfaceToken = this.consumeToken(TokenKind.EndInterface);
14✔
518

519
        const statement = new InterfaceStatement(
14✔
520
            interfaceToken,
521
            nameToken,
522
            extendsToken,
523
            parentInterfaceName,
524
            body,
525
            endInterfaceToken
526
        );
527
        this._references.interfaceStatements.push(statement);
14✔
528
        this.exitAnnotationBlock(parentAnnotations);
14✔
529
        return statement;
14✔
530
    }
531

532
    private enumDeclaration(): EnumStatement {
533
        const result = new EnumStatement({} as any, []);
70✔
534
        this.warnIfNotBrighterScriptMode('enum declarations');
70✔
535

536
        const parentAnnotations = this.enterAnnotationBlock();
70✔
537

538
        result.tokens.enum = this.consume(
70✔
539
            DiagnosticMessages.expectedKeyword(TokenKind.Enum),
540
            TokenKind.Enum
541
        );
542

543
        result.tokens.name = this.tryIdentifier();
70✔
544

545
        this.consumeStatementSeparators();
70✔
546
        //gather up all members
547
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
70✔
548
            try {
141✔
549
                let decl: EnumMemberStatement | CommentStatement;
550

551
                //collect leading annotations
552
                if (this.check(TokenKind.At)) {
141!
553
                    this.annotationExpression();
×
554
                }
555

556
                //members
557
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
141✔
558
                    decl = this.enumMemberStatement();
138✔
559

560
                    //comments
561
                } else if (this.check(TokenKind.Comment)) {
3!
562
                    decl = this.commentStatement();
3✔
563
                }
564

565
                if (decl) {
141!
566
                    this.consumePendingAnnotations(decl);
141✔
567
                    result.body.push(decl);
141✔
568
                } else {
569
                    //we didn't find a declaration...flag tokens until next line
570
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
571
                }
572
            } catch (e) {
573
                //throw out any failed members and move on to the next line
574
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
575
            }
576

577
            //ensure statement separator
578
            this.consumeStatementSeparators();
141✔
579
            //break out of this loop if we encountered the `EndEnum` token
580
            if (this.check(TokenKind.EndEnum)) {
141✔
581
                break;
66✔
582
            }
583
        }
584

585
        //consume the final `end interface` token
586
        result.tokens.endEnum = this.consumeToken(TokenKind.EndEnum);
70✔
587

588
        this._references.enumStatements.push(result);
70✔
589
        this.exitAnnotationBlock(parentAnnotations);
70✔
590
        return result;
70✔
591
    }
592

593
    /**
594
     * A BrighterScript class declaration
595
     */
596
    private classDeclaration(): ClassStatement {
597
        this.warnIfNotBrighterScriptMode('class declarations');
399✔
598

599
        const parentAnnotations = this.enterAnnotationBlock();
399✔
600

601
        let classKeyword = this.consume(
399✔
602
            DiagnosticMessages.expectedKeyword(TokenKind.Class),
603
            TokenKind.Class
604
        );
605
        let extendsKeyword: Token;
606
        let parentClassName: NamespacedVariableNameExpression;
607

608
        //get the class name
609
        let className = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('class'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
399✔
610

611
        //see if the class inherits from parent
612
        if (this.peek().text.toLowerCase() === 'extends') {
399✔
613
            extendsKeyword = this.advance();
60✔
614
            parentClassName = this.getNamespacedVariableNameExpression();
60✔
615
        }
616

617
        //ensure statement separator
618
        this.consumeStatementSeparators();
398✔
619

620
        //gather up all class members (Fields, Methods)
621
        let body = [] as Statement[];
398✔
622
        while (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private, TokenKind.Function, TokenKind.Sub, TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
398✔
623
            try {
385✔
624
                let decl: Statement;
625
                let accessModifier: Token;
626

627
                if (this.check(TokenKind.At)) {
385✔
628
                    this.annotationExpression();
15✔
629
                }
630

631
                if (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private)) {
384✔
632
                    //use actual access modifier
633
                    accessModifier = this.advance();
59✔
634
                }
635

636
                let overrideKeyword: Token;
637
                if (this.peek().text.toLowerCase() === 'override') {
384✔
638
                    overrideKeyword = this.advance();
13✔
639
                }
640

641
                //methods (function/sub keyword OR identifier followed by opening paren)
642
                if (this.checkAny(TokenKind.Function, TokenKind.Sub) || (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.LeftParen))) {
384✔
643
                    const funcDeclaration = this.functionDeclaration(false, false);
216✔
644

645
                    //remove this function from the lists because it's not a callable
646
                    const functionStatement = this._references.functionStatements.pop();
215✔
647

648
                    //if we have an overrides keyword AND this method is called 'new', that's not allowed
649
                    if (overrideKeyword && funcDeclaration.name.text.toLowerCase() === 'new') {
215!
650
                        this.diagnostics.push({
×
651
                            ...DiagnosticMessages.cannotUseOverrideKeywordOnConstructorFunction(),
652
                            range: overrideKeyword.range
653
                        });
654
                    }
655

656
                    decl = new MethodStatement(
215✔
657
                        accessModifier,
658
                        funcDeclaration.name,
659
                        funcDeclaration.func,
660
                        overrideKeyword
661
                    );
662

663
                    //refer to this statement as parent of the expression
664
                    functionStatement.func.functionStatement = decl as MethodStatement;
215✔
665

666
                    //fields
667
                } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
168✔
668

669
                    decl = this.fieldDeclaration(accessModifier);
146✔
670

671
                    //class fields cannot be overridden
672
                    if (overrideKeyword) {
145!
673
                        this.diagnostics.push({
×
674
                            ...DiagnosticMessages.classFieldCannotBeOverridden(),
675
                            range: overrideKeyword.range
676
                        });
677
                    }
678

679
                    //comments
680
                } else if (this.check(TokenKind.Comment)) {
22✔
681
                    decl = this.commentStatement();
8✔
682
                }
683

684
                if (decl) {
382✔
685
                    this.consumePendingAnnotations(decl);
368✔
686
                    body.push(decl);
368✔
687
                }
688
            } catch (e) {
689
                //throw out any failed members and move on to the next line
690
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3✔
691
            }
692

693
            //ensure statement separator
694
            this.consumeStatementSeparators();
385✔
695
        }
696

697
        let endingKeyword = this.advance();
398✔
698
        if (endingKeyword.kind !== TokenKind.EndClass) {
398✔
699
            this.diagnostics.push({
3✔
700
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('class'),
701
                range: endingKeyword.range
702
            });
703
        }
704

705
        const result = new ClassStatement(
398✔
706
            classKeyword,
707
            className,
708
            body,
709
            endingKeyword,
710
            extendsKeyword,
711
            parentClassName
712
        );
713

714
        this._references.classStatements.push(result);
398✔
715
        this.exitAnnotationBlock(parentAnnotations);
398✔
716
        return result;
398✔
717
    }
718

719
    private fieldDeclaration(accessModifier: Token | null) {
720
        let name = this.consume(
146✔
721
            DiagnosticMessages.expectedClassFieldIdentifier(),
722
            TokenKind.Identifier,
723
            ...AllowedProperties
724
        ) as Identifier;
725
        let asToken: Token;
726
        let fieldType: Token;
727
        //look for `as SOME_TYPE`
728
        if (this.check(TokenKind.As)) {
146✔
729
            asToken = this.advance();
104✔
730
            fieldType = this.typeToken();
104✔
731

732
            //no field type specified
733
            if (!util.tokenToBscType(fieldType)) {
104✔
734
                this.diagnostics.push({
1✔
735
                    ...DiagnosticMessages.expectedValidTypeToFollowAsKeyword(),
736
                    range: this.peek().range
737
                });
738
            }
739
        }
740

741
        let initialValue: Expression;
742
        let equal: Token;
743
        //if there is a field initializer
744
        if (this.check(TokenKind.Equal)) {
146✔
745
            equal = this.advance();
33✔
746
            initialValue = this.expression();
33✔
747
        }
748

749
        return new FieldStatement(
145✔
750
            accessModifier,
751
            name,
752
            asToken,
753
            fieldType,
754
            equal,
755
            initialValue
756
        );
757
    }
758

759
    /**
760
     * An array of CallExpression for the current function body
761
     */
762
    private callExpressions = [];
1,626✔
763

764
    private functionDeclaration(isAnonymous: true, checkIdentifier?: boolean, onlyCallableAsMember?: boolean): FunctionExpression;
765
    private functionDeclaration(isAnonymous: false, checkIdentifier?: boolean, onlyCallableAsMember?: boolean): FunctionStatement;
766
    private functionDeclaration(isAnonymous: boolean, checkIdentifier = true, onlyCallableAsMember = false) {
2,376✔
767
        let previousCallExpressions = this.callExpressions;
1,296✔
768
        this.callExpressions = [];
1,296✔
769
        try {
1,296✔
770
            //track depth to help certain statements need to know if they are contained within a function body
771
            this.namespaceAndFunctionDepth++;
1,296✔
772
            let functionType: Token;
773
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
1,296✔
774
                functionType = this.advance();
1,295✔
775
            } else {
776
                this.diagnostics.push({
1✔
777
                    ...DiagnosticMessages.missingCallableKeyword(),
778
                    range: this.peek().range
779
                });
780
                functionType = {
1✔
781
                    isReserved: true,
782
                    kind: TokenKind.Function,
783
                    text: 'function',
784
                    //zero-length location means derived
785
                    range: {
786
                        start: this.peek().range.start,
787
                        end: this.peek().range.start
788
                    },
789
                    leadingWhitespace: ''
790
                };
791
            }
792
            let isSub = functionType?.kind === TokenKind.Sub;
1,296!
793
            let functionTypeText = isSub ? 'sub' : 'function';
1,296✔
794
            let name: Identifier;
795
            let leftParen: Token;
796

797
            if (isAnonymous) {
1,296✔
798
                leftParen = this.consume(
65✔
799
                    DiagnosticMessages.expectedLeftParenAfterCallable(functionTypeText),
800
                    TokenKind.LeftParen
801
                );
802
            } else {
803
                name = this.consume(
1,231✔
804
                    DiagnosticMessages.expectedNameAfterCallableKeyword(functionTypeText),
805
                    TokenKind.Identifier,
806
                    ...AllowedProperties
807
                ) as Identifier;
808
                leftParen = this.consume(
1,229✔
809
                    DiagnosticMessages.expectedLeftParenAfterCallableName(functionTypeText),
810
                    TokenKind.LeftParen
811
                );
812

813
                //prevent functions from ending with type designators
814
                let lastChar = name.text[name.text.length - 1];
1,228✔
815
                if (['$', '%', '!', '#', '&'].includes(lastChar)) {
1,228✔
816
                    //don't throw this error; let the parser continue
817
                    this.diagnostics.push({
8✔
818
                        ...DiagnosticMessages.functionNameCannotEndWithTypeDesignator(functionTypeText, name.text, lastChar),
819
                        range: name.range
820
                    });
821
                }
822

823
                //flag functions with keywords for names (only for standard functions)
824
                if (checkIdentifier && DisallowedFunctionIdentifiersText.has(name.text.toLowerCase())) {
1,228✔
825
                    this.diagnostics.push({
1✔
826
                        ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
827
                        range: name.range
828
                    });
829
                }
830
            }
831

832
            let params = [] as FunctionParameterExpression[];
1,293✔
833
            let asToken: Token;
834
            let typeToken: Token;
835
            if (!this.check(TokenKind.RightParen)) {
1,293✔
836
                do {
197✔
837
                    if (params.length >= CallExpression.MaximumArguments) {
291!
838
                        this.diagnostics.push({
×
839
                            ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments),
840
                            range: this.peek().range
841
                        });
842
                    }
843

844
                    params.push(this.functionParameter());
291✔
845
                } while (this.match(TokenKind.Comma));
846
            }
847
            let rightParen = this.advance();
1,292✔
848

849
            if (this.check(TokenKind.As)) {
1,292✔
850
                asToken = this.advance();
64✔
851

852
                typeToken = this.typeToken();
64✔
853

854
                if (!util.tokenToBscType(typeToken, this.options.mode === ParseMode.BrighterScript)) {
64✔
855
                    this.diagnostics.push({
1✔
856
                        ...DiagnosticMessages.invalidFunctionReturnType(typeToken.text ?? ''),
3!
857
                        range: typeToken.range
858
                    });
859
                }
860
            }
861

862
            params.reduce((haveFoundOptional: boolean, param: FunctionParameterExpression) => {
1,292✔
863
                if (haveFoundOptional && !param.defaultValue) {
290!
864
                    this.diagnostics.push({
×
865
                        ...DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter(param.name.text),
866
                        range: param.range
867
                    });
868
                }
869

870
                return haveFoundOptional || !!param.defaultValue;
290✔
871
            }, false);
872

873
            this.consumeStatementSeparators(true);
1,292✔
874

875
            let func = new FunctionExpression(
1,292✔
876
                params,
877
                undefined, //body
878
                functionType,
879
                undefined, //ending keyword
880
                leftParen,
881
                rightParen,
882
                asToken,
883
                typeToken,
884
                this.currentFunctionExpression
885
            );
886
            //if there is a parent function, register this function with the parent
887
            if (this.currentFunctionExpression) {
1,292✔
888
                this.currentFunctionExpression.childFunctionExpressions.push(func);
46✔
889
            }
890

891
            // add the function to the relevant symbol tables
892
            if (!onlyCallableAsMember && name) {
1,292✔
893
                const funcType = func.getFunctionType();
1,227✔
894
                funcType.setName(name.text);
1,227✔
895
            }
896

897
            this._references.functionExpressions.push(func);
1,292✔
898

899
            let previousFunctionExpression = this.currentFunctionExpression;
1,292✔
900
            this.currentFunctionExpression = func;
1,292✔
901

902
            //make sure to restore the currentFunctionExpression even if the body block fails to parse
903
            try {
1,292✔
904
                //support ending the function with `end sub` OR `end function`
905
                func.body = this.block();
1,292✔
906
            } finally {
907
                this.currentFunctionExpression = previousFunctionExpression;
1,292✔
908
            }
909

910
            if (!func.body) {
1,292✔
911
                this.diagnostics.push({
2✔
912
                    ...DiagnosticMessages.callableBlockMissingEndKeyword(functionTypeText),
913
                    range: this.peek().range
914
                });
915
                throw this.lastDiagnosticAsError();
2✔
916
            }
917

918
            // consume 'end sub' or 'end function'
919
            func.end = this.advance();
1,290✔
920
            let expectedEndKind = isSub ? TokenKind.EndSub : TokenKind.EndFunction;
1,290✔
921

922
            //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
923
            //add an error but don't hard-fail so the AST can continue more gracefully
924
            if (func.end.kind !== expectedEndKind) {
1,290✔
925
                this.diagnostics.push({
4✔
926
                    ...DiagnosticMessages.mismatchedEndCallableKeyword(functionTypeText, func.end.text),
927
                    range: this.peek().range
928
                });
929
            }
930
            func.callExpressions = this.callExpressions;
1,290✔
931

932
            if (isAnonymous) {
1,290✔
933
                return func;
65✔
934
            } else {
935
                let result = new FunctionStatement(name, func);
1,225✔
936
                func.functionStatement = result;
1,225✔
937
                this._references.functionStatements.push(result);
1,225✔
938

939
                return result;
1,225✔
940
            }
941
        } finally {
942
            this.namespaceAndFunctionDepth--;
1,296✔
943
            //restore the previous CallExpression list
944
            this.callExpressions = previousCallExpressions;
1,296✔
945
        }
946
    }
947

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

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

961
        let typeToken: Token | undefined;
962
        let defaultValue;
963

964
        // parse argument default value
965
        if (this.match(TokenKind.Equal)) {
291✔
966
            // it seems any expression is allowed here -- including ones that operate on other arguments!
967
            defaultValue = this.expression();
40✔
968
        }
969

970
        let asToken = null;
291✔
971
        if (this.check(TokenKind.As)) {
291✔
972
            asToken = this.advance();
139✔
973

974
            typeToken = this.typeToken();
139✔
975

976
            if (!util.tokenToBscType(typeToken, this.options.mode === ParseMode.BrighterScript)) {
139✔
977
                this.diagnostics.push({
1✔
978
                    ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, typeToken.text),
979
                    range: typeToken.range
980
                });
981
                throw this.lastDiagnosticAsError();
1✔
982
            }
983
        }
984
        return new FunctionParameterExpression(
290✔
985
            name,
986
            typeToken,
987
            defaultValue,
988
            asToken
989
        );
990
    }
991

992
    private assignment(): AssignmentStatement {
993
        let name = this.advance() as Identifier;
811✔
994
        //add diagnostic if name is a reserved word that cannot be used as an identifier
995
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
811✔
996
            this.diagnostics.push({
12✔
997
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
998
                range: name.range
999
            });
1000
        }
1001
        let operator = this.consume(
811✔
1002
            DiagnosticMessages.expectedOperatorAfterIdentifier(AssignmentOperators, name.text),
1003
            ...AssignmentOperators
1004
        );
1005
        let value = this.expression();
810✔
1006

1007
        let result: AssignmentStatement;
1008
        if (operator.kind === TokenKind.Equal) {
803✔
1009
            result = new AssignmentStatement(operator, name, value, this.currentFunctionExpression);
760✔
1010
        } else {
1011
            const nameExpression = new VariableExpression(name);
43✔
1012
            result = new AssignmentStatement(
43✔
1013
                operator,
1014
                name,
1015
                new BinaryExpression(nameExpression, operator, value),
1016
                this.currentFunctionExpression
1017
            );
1018
            this.addExpressionsToReferences(nameExpression);
43✔
1019
            if (isBinaryExpression(value)) {
43✔
1020
                //remove the right-hand-side expression from this assignment operator, and replace with the full assignment expression
1021
                this._references.expressions.delete(value);
3✔
1022
            }
1023
            this._references.expressions.add(result);
43✔
1024
        }
1025

1026
        this._references.assignmentStatements.push(result);
803✔
1027
        return result;
803✔
1028
    }
1029

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

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

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

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

1048
    private statement(): Statement | undefined {
1049
        if (this.checkLibrary()) {
2,901!
1050
            return this.libraryStatement();
×
1051
        }
1052

1053
        if (this.check(TokenKind.Import)) {
2,901✔
1054
            return this.importStatement();
29✔
1055
        }
1056

1057
        if (this.check(TokenKind.Stop)) {
2,872✔
1058
            return this.stopStatement();
13✔
1059
        }
1060

1061
        if (this.check(TokenKind.If)) {
2,859✔
1062
            return this.ifStatement();
131✔
1063
        }
1064

1065
        //`try` must be followed by a block, otherwise it could be a local variable
1066
        if (this.check(TokenKind.Try) && this.checkAnyNext(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
2,728✔
1067
            return this.tryCatchStatement();
22✔
1068
        }
1069

1070
        if (this.check(TokenKind.Throw)) {
2,706✔
1071
            return this.throwStatement();
6✔
1072
        }
1073

1074
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
2,700✔
1075
            return this.printStatement();
479✔
1076
        }
1077
        if (this.check(TokenKind.Dim)) {
2,221✔
1078
            return this.dimStatement();
38✔
1079
        }
1080

1081
        if (this.check(TokenKind.While)) {
2,183✔
1082
            return this.whileStatement();
16✔
1083
        }
1084

1085
        if (this.check(TokenKind.ExitWhile)) {
2,167✔
1086
            return this.exitWhile();
4✔
1087
        }
1088

1089
        if (this.check(TokenKind.For)) {
2,163✔
1090
            return this.forStatement();
26✔
1091
        }
1092

1093
        if (this.check(TokenKind.ForEach)) {
2,137✔
1094
            return this.forEachStatement();
16✔
1095
        }
1096

1097
        if (this.check(TokenKind.ExitFor)) {
2,121✔
1098
            return this.exitFor();
1✔
1099
        }
1100

1101
        if (this.check(TokenKind.End)) {
2,120✔
1102
            return this.endStatement();
4✔
1103
        }
1104

1105
        if (this.match(TokenKind.Return)) {
2,116✔
1106
            return this.returnStatement();
129✔
1107
        }
1108

1109
        if (this.check(TokenKind.Goto)) {
1,987✔
1110
            return this.gotoStatement();
8✔
1111
        }
1112

1113
        if (this.check(TokenKind.Continue)) {
1,979✔
1114
            return this.continueStatement();
8✔
1115
        }
1116

1117
        //does this line look like a label? (i.e.  `someIdentifier:` )
1118
        if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
1,971✔
1119
            try {
8✔
1120
                return this.labelStatement();
8✔
1121
            } catch (err) {
1122
                if (!(err instanceof CancelStatementError)) {
2!
1123
                    throw err;
×
1124
                }
1125
                //not a label, try something else
1126
            }
1127
        }
1128

1129
        // BrightScript is like python, in that variables can be declared without a `var`,
1130
        // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
1131
        // out what to do with it.
1132
        if (
1,965✔
1133
            this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers) &&
3,805✔
1134
            this.checkAnyNext(...AssignmentOperators)
1135
        ) {
1136
            return this.assignment();
785✔
1137
        }
1138

1139
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1140
        if (this.check(TokenKind.Interface)) {
1,180✔
1141
            return this.interfaceDeclaration();
14✔
1142
        }
1143

1144
        if (this.check(TokenKind.Class)) {
1,166✔
1145
            return this.classDeclaration();
399✔
1146
        }
1147

1148
        if (this.check(TokenKind.Namespace)) {
767✔
1149
            return this.namespaceStatement();
150✔
1150
        }
1151

1152
        if (this.check(TokenKind.Enum)) {
617✔
1153
            return this.enumDeclaration();
70✔
1154
        }
1155

1156
        // TODO: support multi-statements
1157
        return this.setStatement();
547✔
1158
    }
1159

1160
    private whileStatement(): WhileStatement {
1161
        const whileKeyword = this.advance();
16✔
1162
        const condition = this.expression();
16✔
1163

1164
        this.consumeStatementSeparators();
15✔
1165

1166
        const whileBlock = this.block(TokenKind.EndWhile);
15✔
1167
        let endWhile: Token;
1168
        if (!whileBlock || this.peek().kind !== TokenKind.EndWhile) {
15✔
1169
            this.diagnostics.push({
1✔
1170
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('while'),
1171
                range: this.peek().range
1172
            });
1173
            if (!whileBlock) {
1!
1174
                throw this.lastDiagnosticAsError();
×
1175
            }
1176
        } else {
1177
            endWhile = this.advance();
14✔
1178
        }
1179

1180
        return new WhileStatement(
15✔
1181
            { while: whileKeyword, endWhile: endWhile },
1182
            condition,
1183
            whileBlock
1184
        );
1185
    }
1186

1187
    private exitWhile(): ExitWhileStatement {
1188
        let keyword = this.advance();
4✔
1189

1190
        return new ExitWhileStatement({ exitWhile: keyword });
4✔
1191
    }
1192

1193
    private forStatement(): ForStatement {
1194
        const forToken = this.advance();
26✔
1195
        const initializer = this.assignment();
26✔
1196

1197
        //TODO: newline allowed?
1198

1199
        const toToken = this.advance();
25✔
1200
        const finalValue = this.expression();
25✔
1201
        let incrementExpression: Expression | undefined;
1202
        let stepToken: Token | undefined;
1203

1204
        if (this.check(TokenKind.Step)) {
25✔
1205
            stepToken = this.advance();
6✔
1206
            incrementExpression = this.expression();
6✔
1207
        } else {
1208
            // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
1209
        }
1210

1211
        this.consumeStatementSeparators();
25✔
1212

1213
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
25✔
1214
        let endForToken: Token;
1215
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
25✔
1216
            this.diagnostics.push({
1✔
1217
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1218
                range: this.peek().range
1219
            });
1220
            if (!body) {
1!
1221
                throw this.lastDiagnosticAsError();
×
1222
            }
1223
        } else {
1224
            endForToken = this.advance();
24✔
1225
        }
1226

1227
        // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
1228
        // stays around in scope with whatever value it was when the loop exited.
1229
        return new ForStatement(
25✔
1230
            forToken,
1231
            initializer,
1232
            toToken,
1233
            finalValue,
1234
            body,
1235
            endForToken,
1236
            stepToken,
1237
            incrementExpression
1238
        );
1239
    }
1240

1241
    private forEachStatement(): ForEachStatement {
1242
        let forEach = this.advance();
16✔
1243
        let name = this.advance();
16✔
1244

1245
        let maybeIn = this.peek();
16✔
1246
        if (this.check(TokenKind.Identifier) && maybeIn.text.toLowerCase() === 'in') {
16!
1247
            this.advance();
16✔
1248
        } else {
1249
            this.diagnostics.push({
×
1250
                ...DiagnosticMessages.expectedInAfterForEach(name.text),
1251
                range: this.peek().range
1252
            });
1253
            throw this.lastDiagnosticAsError();
×
1254
        }
1255

1256
        let target = this.expression();
16✔
1257
        if (!target) {
16!
1258
            this.diagnostics.push({
×
1259
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1260
                range: this.peek().range
1261
            });
1262
            throw this.lastDiagnosticAsError();
×
1263
        }
1264

1265
        this.consumeStatementSeparators();
16✔
1266

1267
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
16✔
1268
        if (!body) {
16!
1269
            this.diagnostics.push({
×
1270
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1271
                range: this.peek().range
1272
            });
1273
            throw this.lastDiagnosticAsError();
×
1274
        }
1275

1276
        let endFor = this.advance();
16✔
1277

1278
        return new ForEachStatement(
16✔
1279
            {
1280
                forEach: forEach,
1281
                in: maybeIn,
1282
                endFor: endFor
1283
            },
1284
            name,
1285
            target,
1286
            body
1287
        );
1288
    }
1289

1290
    private exitFor(): ExitForStatement {
1291
        let keyword = this.advance();
1✔
1292

1293
        return new ExitForStatement({ exitFor: keyword });
1✔
1294
    }
1295

1296
    private commentStatement() {
1297
        //if this comment is on the same line as the previous statement,
1298
        //then this comment should be treated as a single-line comment
1299
        let prev = this.previous();
208✔
1300
        if (prev?.range.end.line === this.peek().range.start.line) {
208✔
1301
            return new CommentStatement([this.advance()]);
121✔
1302
        } else {
1303
            let comments = [this.advance()];
87✔
1304
            while (this.check(TokenKind.Newline) && this.checkNext(TokenKind.Comment)) {
87✔
1305
                this.advance();
19✔
1306
                comments.push(this.advance());
19✔
1307
            }
1308
            return new CommentStatement(comments);
87✔
1309
        }
1310
    }
1311

1312
    private namespaceStatement(): NamespaceStatement | undefined {
1313
        this.warnIfNotBrighterScriptMode('namespace');
150✔
1314
        let keyword = this.advance();
150✔
1315

1316
        if (!this.isAtRootLevel()) {
150✔
1317
            this.diagnostics.push({
6✔
1318
                ...DiagnosticMessages.keywordMustBeDeclaredAtRootLevel('namespace'),
1319
                range: keyword.range
1320
            });
1321
        }
1322
        this.namespaceAndFunctionDepth++;
150✔
1323

1324
        let name = this.getNamespacedVariableNameExpression();
150✔
1325
        //set the current namespace name
1326
        let result = new NamespaceStatement(keyword, name, null, null);
149✔
1327

1328
        this.globalTerminators.push([TokenKind.EndNamespace]);
149✔
1329
        let body = this.body();
149✔
1330
        this.globalTerminators.pop();
149✔
1331

1332
        let endKeyword: Token;
1333
        if (this.check(TokenKind.EndNamespace)) {
149✔
1334
            endKeyword = this.advance();
147✔
1335
        } else {
1336
            //the `end namespace` keyword is missing. add a diagnostic, but keep parsing
1337
            this.diagnostics.push({
2✔
1338
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'),
1339
                range: keyword.range
1340
            });
1341
        }
1342

1343
        this.namespaceAndFunctionDepth--;
149✔
1344
        result.body = body;
149✔
1345
        result.endKeyword = endKeyword;
149✔
1346
        this._references.namespaceStatements.push(result);
149✔
1347
        //cache the range property so that plugins can't affect it
1348
        result.cacheRange();
149✔
1349

1350
        return result;
149✔
1351
    }
1352

1353
    /**
1354
     * Get an expression with identifiers separated by periods. Useful for namespaces and class extends
1355
     */
1356
    private getNamespacedVariableNameExpression() {
1357
        let firstIdentifier = this.consume(
294✔
1358
            DiagnosticMessages.expectedIdentifierAfterKeyword(this.previous().text),
1359
            TokenKind.Identifier,
1360
            ...this.allowedLocalIdentifiers
1361
        ) as Identifier;
1362

1363
        let expr: DottedGetExpression | VariableExpression;
1364

1365
        if (firstIdentifier) {
291!
1366
            // force it into an identifier so the AST makes some sense
1367
            firstIdentifier.kind = TokenKind.Identifier;
291✔
1368
            const varExpr = new VariableExpression(firstIdentifier);
291✔
1369
            expr = varExpr;
291✔
1370

1371
            //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
1372
            while (this.check(TokenKind.Dot)) {
291✔
1373
                let dot = this.tryConsume(
114✔
1374
                    DiagnosticMessages.unexpectedToken(this.peek().text),
1375
                    TokenKind.Dot
1376
                );
1377
                if (!dot) {
114!
1378
                    break;
×
1379
                }
1380
                let identifier = this.tryConsume(
114✔
1381
                    DiagnosticMessages.expectedIdentifier(),
1382
                    TokenKind.Identifier,
1383
                    ...this.allowedLocalIdentifiers,
1384
                    ...AllowedProperties
1385
                ) as Identifier;
1386

1387
                if (!identifier) {
114✔
1388
                    break;
3✔
1389
                }
1390
                // force it into an identifier so the AST makes some sense
1391
                identifier.kind = TokenKind.Identifier;
111✔
1392
                expr = new DottedGetExpression(expr, identifier, dot);
111✔
1393
            }
1394
        }
1395
        return new NamespacedVariableNameExpression(expr);
291✔
1396
    }
1397

1398
    /**
1399
     * Add an 'unexpected token' diagnostic for any token found between current and the first stopToken found.
1400
     */
1401
    private flagUntil(...stopTokens: TokenKind[]) {
1402
        while (!this.checkAny(...stopTokens) && !this.isAtEnd()) {
5✔
1403
            let token = this.advance();
×
1404
            this.diagnostics.push({
×
1405
                ...DiagnosticMessages.unexpectedToken(token.text),
1406
                range: token.range
1407
            });
1408
        }
1409
    }
1410

1411
    /**
1412
     * Consume tokens until one of the `stopTokenKinds` is encountered
1413
     * @param tokenKinds
1414
     * @return - the list of tokens consumed, EXCLUDING the `stopTokenKind` (you can use `this.peek()` to see which one it was)
1415
     */
1416
    private consumeUntil(...stopTokenKinds: TokenKind[]) {
1417
        let result = [] as Token[];
82✔
1418
        //take tokens until we encounter one of the stopTokenKinds
1419
        while (!stopTokenKinds.includes(this.peek().kind)) {
82✔
1420
            result.push(this.advance());
242✔
1421
        }
1422
        return result;
82✔
1423
    }
1424

1425
    private constDeclaration(): ConstStatement | undefined {
1426
        this.warnIfNotBrighterScriptMode('const declaration');
25✔
1427
        const constToken = this.advance();
25✔
1428
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
25✔
1429
        const equalToken = this.consumeToken(TokenKind.Equal);
25✔
1430
        const expression = this.expression();
25✔
1431
        const statement = new ConstStatement({
25✔
1432
            const: constToken,
1433
            name: nameToken,
1434
            equals: equalToken
1435
        }, expression);
1436
        this._references.constStatements.push(statement);
25✔
1437
        return statement;
25✔
1438
    }
1439

1440
    private libraryStatement(): LibraryStatement | undefined {
1441
        let libStatement = new LibraryStatement({
10✔
1442
            library: this.advance(),
1443
            //grab the next token only if it's a string
1444
            filePath: this.tryConsume(
1445
                DiagnosticMessages.expectedStringLiteralAfterKeyword('library'),
1446
                TokenKind.StringLiteral
1447
            )
1448
        });
1449

1450
        this._references.libraryStatements.push(libStatement);
10✔
1451
        return libStatement;
10✔
1452
    }
1453

1454
    private importStatement() {
1455
        this.warnIfNotBrighterScriptMode('import statements');
29✔
1456
        let importStatement = new ImportStatement(
29✔
1457
            this.advance(),
1458
            //grab the next token only if it's a string
1459
            this.tryConsume(
1460
                DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
1461
                TokenKind.StringLiteral
1462
            )
1463
        );
1464

1465
        this._references.importStatements.push(importStatement);
29✔
1466
        return importStatement;
29✔
1467
    }
1468

1469
    private annotationExpression() {
1470
        const atToken = this.advance();
43✔
1471
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
43✔
1472
        if (identifier) {
43✔
1473
            identifier.kind = TokenKind.Identifier;
42✔
1474
        }
1475
        let annotation = new AnnotationExpression(atToken, identifier);
43✔
1476
        this.pendingAnnotations.push(annotation);
42✔
1477

1478
        //optional arguments
1479
        if (this.check(TokenKind.LeftParen)) {
42✔
1480
            let leftParen = this.advance();
6✔
1481
            annotation.call = this.finishCall(leftParen, annotation, false);
6✔
1482
        }
1483
        return annotation;
42✔
1484
    }
1485

1486
    private ternaryExpression(test?: Expression): TernaryExpression {
1487
        this.warnIfNotBrighterScriptMode('ternary operator');
69✔
1488
        if (!test) {
69!
1489
            test = this.expression();
×
1490
        }
1491
        const questionMarkToken = this.advance();
69✔
1492

1493
        //consume newlines or comments
1494
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
69✔
1495
            this.advance();
8✔
1496
        }
1497

1498
        let consequent: Expression;
1499
        try {
69✔
1500
            consequent = this.expression();
69✔
1501
        } catch { }
1502

1503
        //consume newlines or comments
1504
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
69✔
1505
            this.advance();
6✔
1506
        }
1507

1508
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
69✔
1509

1510
        //consume newlines
1511
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
69✔
1512
            this.advance();
12✔
1513
        }
1514
        let alternate: Expression;
1515
        try {
69✔
1516
            alternate = this.expression();
69✔
1517
        } catch { }
1518

1519
        return new TernaryExpression(test, questionMarkToken, consequent, colonToken, alternate);
69✔
1520
    }
1521

1522
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1523
        this.warnIfNotBrighterScriptMode('null coalescing operator');
22✔
1524
        const questionQuestionToken = this.advance();
22✔
1525
        const alternate = this.expression();
22✔
1526
        return new NullCoalescingExpression(test, questionQuestionToken, alternate);
22✔
1527
    }
1528

1529
    private regexLiteralExpression() {
1530
        this.warnIfNotBrighterScriptMode('regular expression literal');
42✔
1531
        return new RegexLiteralExpression({
42✔
1532
            regexLiteral: this.advance()
1533
        });
1534
    }
1535

1536
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1537
        this.warnIfNotBrighterScriptMode('template string');
32✔
1538

1539
        //get the tag name
1540
        let tagName: Identifier;
1541
        if (isTagged) {
32✔
1542
            tagName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties) as Identifier;
3✔
1543
            // force it into an identifier so the AST makes some sense
1544
            tagName.kind = TokenKind.Identifier;
3✔
1545
        }
1546

1547
        let quasis = [] as TemplateStringQuasiExpression[];
32✔
1548
        let expressions = [];
32✔
1549
        let openingBacktick = this.peek();
32✔
1550
        this.advance();
32✔
1551
        let currentQuasiExpressionParts = [];
32✔
1552
        while (!this.isAtEnd() && !this.check(TokenKind.BackTick)) {
32✔
1553
            let next = this.peek();
119✔
1554
            if (next.kind === TokenKind.TemplateStringQuasi) {
119✔
1555
                //a quasi can actually be made up of multiple quasis when it includes char literals
1556
                currentQuasiExpressionParts.push(
75✔
1557
                    new LiteralExpression(next)
1558
                );
1559
                this.advance();
75✔
1560
            } else if (next.kind === TokenKind.EscapedCharCodeLiteral) {
44✔
1561
                currentQuasiExpressionParts.push(
18✔
1562
                    new EscapedCharCodeLiteralExpression(<any>next)
1563
                );
1564
                this.advance();
18✔
1565
            } else {
1566
                //finish up the current quasi
1567
                quasis.push(
26✔
1568
                    new TemplateStringQuasiExpression(currentQuasiExpressionParts)
1569
                );
1570
                currentQuasiExpressionParts = [];
26✔
1571

1572
                if (next.kind === TokenKind.TemplateStringExpressionBegin) {
26!
1573
                    this.advance();
26✔
1574
                }
1575
                //now keep this expression
1576
                expressions.push(this.expression());
26✔
1577
                if (!this.isAtEnd() && this.check(TokenKind.TemplateStringExpressionEnd)) {
26!
1578
                    //TODO is it an error if this is not present?
1579
                    this.advance();
26✔
1580
                } else {
1581
                    this.diagnostics.push({
×
1582
                        ...DiagnosticMessages.unterminatedTemplateExpression(),
1583
                        range: util.getRange(openingBacktick, this.peek())
1584
                    });
1585
                    throw this.lastDiagnosticAsError();
×
1586
                }
1587
            }
1588
        }
1589

1590
        //store the final set of quasis
1591
        quasis.push(
32✔
1592
            new TemplateStringQuasiExpression(currentQuasiExpressionParts)
1593
        );
1594

1595
        if (this.isAtEnd()) {
32✔
1596
            //error - missing backtick
1597
            this.diagnostics.push({
2✔
1598
                ...DiagnosticMessages.unterminatedTemplateStringAtEndOfFile(),
1599
                range: util.getRange(openingBacktick, this.peek())
1600
            });
1601
            throw this.lastDiagnosticAsError();
2✔
1602

1603
        } else {
1604
            let closingBacktick = this.advance();
30✔
1605
            if (isTagged) {
30✔
1606
                return new TaggedTemplateStringExpression(tagName, openingBacktick, quasis, expressions, closingBacktick);
3✔
1607
            } else {
1608
                return new TemplateStringExpression(openingBacktick, quasis, expressions, closingBacktick);
27✔
1609
            }
1610
        }
1611
    }
1612

1613
    private tryCatchStatement(): TryCatchStatement {
1614
        const tryToken = this.advance();
22✔
1615
        const statement = new TryCatchStatement(
22✔
1616
            { try: tryToken }
1617
        );
1618

1619
        //ensure statement separator
1620
        this.consumeStatementSeparators();
22✔
1621

1622
        statement.tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
22✔
1623

1624
        const peek = this.peek();
22✔
1625
        if (peek.kind !== TokenKind.Catch) {
22✔
1626
            this.diagnostics.push({
2✔
1627
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1628
                range: this.peek().range
1629
            });
1630
            //gracefully handle end-try
1631
            if (peek.kind === TokenKind.EndTry) {
2✔
1632
                statement.tokens.endTry = this.advance();
1✔
1633
            }
1634
            return statement;
2✔
1635
        }
1636
        const catchStmt = new CatchStatement({ catch: this.advance() });
20✔
1637
        statement.catchStatement = catchStmt;
20✔
1638

1639
        const exceptionVarToken = this.tryConsume(DiagnosticMessages.missingExceptionVarToFollowCatch(), TokenKind.Identifier, ...this.allowedLocalIdentifiers);
20✔
1640
        if (exceptionVarToken) {
20✔
1641
            // force it into an identifier so the AST makes some sense
1642
            exceptionVarToken.kind = TokenKind.Identifier;
18✔
1643
            catchStmt.exceptionVariable = exceptionVarToken as Identifier;
18✔
1644
        }
1645

1646
        //ensure statement sepatator
1647
        this.consumeStatementSeparators();
20✔
1648

1649
        catchStmt.catchBranch = this.block(TokenKind.EndTry);
20✔
1650

1651
        if (this.peek().kind !== TokenKind.EndTry) {
20✔
1652
            this.diagnostics.push({
1✔
1653
                ...DiagnosticMessages.expectedEndTryToTerminateTryCatch(),
1654
                range: this.peek().range
1655
            });
1656
        } else {
1657
            statement.tokens.endTry = this.advance();
19✔
1658
        }
1659
        return statement;
20✔
1660
    }
1661

1662
    private throwStatement() {
1663
        const throwToken = this.advance();
6✔
1664
        let expression: Expression;
1665
        if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
6✔
1666
            this.diagnostics.push({
1✔
1667
                ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(),
1668
                range: throwToken.range
1669
            });
1670
        } else {
1671
            expression = this.expression();
5✔
1672
        }
1673
        return new ThrowStatement(throwToken, expression);
4✔
1674
    }
1675

1676
    private dimStatement() {
1677
        const dim = this.advance();
38✔
1678

1679
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
38✔
1680
        // force to an identifier so the AST makes some sense
1681
        if (identifier) {
38✔
1682
            identifier.kind = TokenKind.Identifier;
36✔
1683
        }
1684

1685
        let leftSquareBracket = this.tryConsume(DiagnosticMessages.missingLeftSquareBracketAfterDimIdentifier(), TokenKind.LeftSquareBracket);
38✔
1686

1687
        let expressions: Expression[] = [];
38✔
1688
        let expression: Expression;
1689
        do {
38✔
1690
            try {
72✔
1691
                expression = this.expression();
72✔
1692
                expressions.push(expression);
67✔
1693
                if (this.check(TokenKind.Comma)) {
67✔
1694
                    this.advance();
34✔
1695
                } else {
1696
                    // will also exit for right square braces
1697
                    break;
33✔
1698
                }
1699
            } catch (error) {
1700
            }
1701
        } while (expression);
1702

1703
        if (expressions.length === 0) {
38✔
1704
            this.diagnostics.push({
5✔
1705
                ...DiagnosticMessages.missingExpressionsInDimStatement(),
1706
                range: this.peek().range
1707
            });
1708
        }
1709
        let rightSquareBracket = this.tryConsume(DiagnosticMessages.missingRightSquareBracketAfterDimIdentifier(), TokenKind.RightSquareBracket);
38✔
1710
        return new DimStatement(dim, identifier, leftSquareBracket, expressions, rightSquareBracket);
38✔
1711
    }
1712

1713
    private ifStatement(): IfStatement {
1714
        // colon before `if` is usually not allowed, unless it's after `then`
1715
        if (this.current > 0) {
173✔
1716
            const prev = this.previous();
168✔
1717
            if (prev.kind === TokenKind.Colon) {
168✔
1718
                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then) {
3✔
1719
                    this.diagnostics.push({
1✔
1720
                        ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
1721
                        range: prev.range
1722
                    });
1723
                }
1724
            }
1725
        }
1726

1727
        const ifToken = this.advance();
173✔
1728
        const startingRange = ifToken.range;
173✔
1729

1730
        const condition = this.expression();
173✔
1731
        let thenBranch: Block;
1732
        let elseBranch: IfStatement | Block | undefined;
1733

1734
        let thenToken: Token | undefined;
1735
        let endIfToken: Token | undefined;
1736
        let elseToken: Token | undefined;
1737

1738
        //optional `then`
1739
        if (this.check(TokenKind.Then)) {
171✔
1740
            thenToken = this.advance();
115✔
1741
        }
1742

1743
        //is it inline or multi-line if?
1744
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
171✔
1745

1746
        if (isInlineIfThen) {
171✔
1747
            /*** PARSE INLINE IF STATEMENT ***/
1748

1749
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
32✔
1750

1751
            if (!thenBranch) {
32!
1752
                this.diagnostics.push({
×
1753
                    ...DiagnosticMessages.expectedStatementToFollowConditionalCondition(ifToken.text),
1754
                    range: this.peek().range
1755
                });
1756
                throw this.lastDiagnosticAsError();
×
1757
            } else {
1758
                this.ensureInline(thenBranch.statements);
32✔
1759
            }
1760

1761
            //else branch
1762
            if (this.check(TokenKind.Else)) {
32✔
1763
                elseToken = this.advance();
19✔
1764

1765
                if (this.check(TokenKind.If)) {
19✔
1766
                    // recurse-read `else if`
1767
                    elseBranch = this.ifStatement();
4✔
1768

1769
                    //no multi-line if chained with an inline if
1770
                    if (!elseBranch.isInline) {
4✔
1771
                        this.diagnostics.push({
2✔
1772
                            ...DiagnosticMessages.expectedInlineIfStatement(),
1773
                            range: elseBranch.range
1774
                        });
1775
                    }
1776

1777
                } else if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
15!
1778
                    //expecting inline else branch
1779
                    this.diagnostics.push({
×
1780
                        ...DiagnosticMessages.expectedInlineIfStatement(),
1781
                        range: this.peek().range
1782
                    });
1783
                    throw this.lastDiagnosticAsError();
×
1784
                } else {
1785
                    elseBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
15✔
1786

1787
                    if (elseBranch) {
15!
1788
                        this.ensureInline(elseBranch.statements);
15✔
1789
                    }
1790
                }
1791

1792
                if (!elseBranch) {
19!
1793
                    //missing `else` branch
1794
                    this.diagnostics.push({
×
1795
                        ...DiagnosticMessages.expectedStatementToFollowElse(),
1796
                        range: this.peek().range
1797
                    });
1798
                    throw this.lastDiagnosticAsError();
×
1799
                }
1800
            }
1801

1802
            if (!elseBranch || !isIfStatement(elseBranch)) {
32✔
1803
                //enforce newline at the end of the inline if statement
1804
                const peek = this.peek();
28✔
1805
                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && !this.isAtEnd()) {
28✔
1806
                    //ignore last error if it was about a colon
1807
                    if (this.previous().kind === TokenKind.Colon) {
3!
1808
                        this.diagnostics.pop();
3✔
1809
                        this.current--;
3✔
1810
                    }
1811
                    //newline is required
1812
                    this.diagnostics.push({
3✔
1813
                        ...DiagnosticMessages.expectedFinalNewline(),
1814
                        range: this.peek().range
1815
                    });
1816
                }
1817
            }
1818

1819
        } else {
1820
            /*** PARSE MULTI-LINE IF STATEMENT ***/
1821

1822
            thenBranch = this.blockConditionalBranch(ifToken);
139✔
1823

1824
            //ensure newline/colon before next keyword
1825
            this.ensureNewLineOrColon();
137✔
1826

1827
            //else branch
1828
            if (this.check(TokenKind.Else)) {
137✔
1829
                elseToken = this.advance();
76✔
1830

1831
                if (this.check(TokenKind.If)) {
76✔
1832
                    // recurse-read `else if`
1833
                    elseBranch = this.ifStatement();
38✔
1834

1835
                } else {
1836
                    elseBranch = this.blockConditionalBranch(ifToken);
38✔
1837

1838
                    //ensure newline/colon before next keyword
1839
                    this.ensureNewLineOrColon();
38✔
1840
                }
1841
            }
1842

1843
            if (!isIfStatement(elseBranch)) {
137✔
1844
                if (this.check(TokenKind.EndIf)) {
99✔
1845
                    endIfToken = this.advance();
97✔
1846

1847
                } else {
1848
                    //missing endif
1849
                    this.diagnostics.push({
2✔
1850
                        ...DiagnosticMessages.expectedEndIfToCloseIfStatement(startingRange.start),
1851
                        range: ifToken.range
1852
                    });
1853
                }
1854
            }
1855
        }
1856

1857
        return new IfStatement(
169✔
1858
            {
1859
                if: ifToken,
1860
                then: thenToken,
1861
                endIf: endIfToken,
1862
                else: elseToken
1863
            },
1864
            condition,
1865
            thenBranch,
1866
            elseBranch,
1867
            isInlineIfThen
1868
        );
1869
    }
1870

1871
    //consume a `then` or `else` branch block of an `if` statement
1872
    private blockConditionalBranch(ifToken: Token) {
1873
        //keep track of the current error count, because if the then branch fails,
1874
        //we will trash them in favor of a single error on if
1875
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
177✔
1876

1877
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
1878
        // a trailing "end if" or "else if"
1879
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
177✔
1880

1881
        if (!branch) {
177✔
1882
            //throw out any new diagnostics created as a result of a `then` block parse failure.
1883
            //the block() function will discard the current line, so any discarded diagnostics will
1884
            //resurface if they are legitimate, and not a result of a malformed if statement
1885
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
2✔
1886

1887
            //this whole if statement is bogus...add error to the if token and hard-fail
1888
            this.diagnostics.push({
2✔
1889
                ...DiagnosticMessages.expectedEndIfElseIfOrElseToTerminateThenBlock(),
1890
                range: ifToken.range
1891
            });
1892
            throw this.lastDiagnosticAsError();
2✔
1893
        }
1894
        return branch;
175✔
1895
    }
1896

1897
    private ensureNewLineOrColon(silent = false) {
175✔
1898
        const prev = this.previous().kind;
382✔
1899
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
382✔
1900
            if (!silent) {
122✔
1901
                this.diagnostics.push({
6✔
1902
                    ...DiagnosticMessages.expectedNewlineOrColon(),
1903
                    range: this.peek().range
1904
                });
1905
            }
1906
            return false;
122✔
1907
        }
1908
        return true;
260✔
1909
    }
1910

1911
    //ensure each statement of an inline block is single-line
1912
    private ensureInline(statements: Statement[]) {
1913
        for (const stat of statements) {
47✔
1914
            if (isIfStatement(stat) && !stat.isInline) {
54✔
1915
                this.diagnostics.push({
2✔
1916
                    ...DiagnosticMessages.expectedInlineIfStatement(),
1917
                    range: stat.range
1918
                });
1919
            }
1920
        }
1921
    }
1922

1923
    //consume inline branch of an `if` statement
1924
    private inlineConditionalBranch(...additionalTerminators: BlockTerminator[]): Block | undefined {
1925
        let statements = [];
54✔
1926
        //attempt to get the next statement without using `this.declaration`
1927
        //which seems a bit hackish to get to work properly
1928
        let statement = this.statement();
54✔
1929
        if (!statement) {
54!
1930
            return undefined;
×
1931
        }
1932
        statements.push(statement);
54✔
1933
        const startingRange = statement.range;
54✔
1934

1935
        //look for colon statement separator
1936
        let foundColon = false;
54✔
1937
        while (this.match(TokenKind.Colon)) {
54✔
1938
            foundColon = true;
12✔
1939
        }
1940

1941
        //if a colon was found, add the next statement or err if unexpected
1942
        if (foundColon) {
54✔
1943
            if (!this.checkAny(TokenKind.Newline, ...additionalTerminators)) {
12✔
1944
                //if not an ending keyword, add next statement
1945
                let extra = this.inlineConditionalBranch(...additionalTerminators);
7✔
1946
                if (!extra) {
7!
1947
                    return undefined;
×
1948
                }
1949
                statements.push(...extra.statements);
7✔
1950
            } else {
1951
                //error: colon before next keyword
1952
                const colon = this.previous();
5✔
1953
                this.diagnostics.push({
5✔
1954
                    ...DiagnosticMessages.unexpectedToken(colon.text),
1955
                    range: colon.range
1956
                });
1957
            }
1958
        }
1959
        return new Block(statements, startingRange);
54✔
1960
    }
1961

1962
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
1963
        let expressionStart = this.peek();
275✔
1964

1965
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
275✔
1966
            let operator = this.advance();
16✔
1967

1968
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
16✔
1969
                this.diagnostics.push({
1✔
1970
                    ...DiagnosticMessages.consecutiveIncrementDecrementOperatorsAreNotAllowed(),
1971
                    range: this.peek().range
1972
                });
1973
                throw this.lastDiagnosticAsError();
1✔
1974
            } else if (isCallExpression(expr)) {
15✔
1975
                this.diagnostics.push({
1✔
1976
                    ...DiagnosticMessages.incrementDecrementOperatorsAreNotAllowedAsResultOfFunctionCall(),
1977
                    range: expressionStart.range
1978
                });
1979
                throw this.lastDiagnosticAsError();
1✔
1980
            }
1981

1982
            const result = new IncrementStatement(expr, operator);
14✔
1983
            this._references.expressions.add(result);
14✔
1984
            return result;
14✔
1985
        }
1986

1987
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
259✔
1988
            return new ExpressionStatement(expr);
197✔
1989
        }
1990

1991
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an assignment
1992
        this.diagnostics.push({
62✔
1993
            ...DiagnosticMessages.expectedStatementOrFunctionCallButReceivedExpression(),
1994
            range: expressionStart.range
1995
        });
1996
        throw this.lastDiagnosticAsError();
62✔
1997
    }
1998

1999
    private setStatement(): DottedSetStatement | IndexedSetStatement | ExpressionStatement | IncrementStatement | AssignmentStatement {
2000
        /**
2001
         * Attempts to find an expression-statement or an increment statement.
2002
         * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
2003
         * statements aren't valid expressions. They _do_ however fall under the same parsing
2004
         * priority as standalone function calls though, so we can parse them in the same way.
2005
         */
2006
        let expr = this.call();
547✔
2007
        if (this.checkAny(...AssignmentOperators) && !(isCallExpression(expr))) {
511✔
2008
            let left = expr;
239✔
2009
            let operator = this.advance();
239✔
2010
            let right = this.expression();
239✔
2011

2012
            // Create a dotted or indexed "set" based on the left-hand side's type
2013
            if (isIndexedGetExpression(left)) {
239✔
2014
                return new IndexedSetStatement(
15✔
2015
                    left.obj,
2016
                    left.index,
2017
                    operator.kind === TokenKind.Equal
2018
                        ? right
15✔
2019
                        : new BinaryExpression(left, operator, right),
2020
                    left.openingSquare,
2021
                    left.closingSquare
2022
                );
2023
            } else if (isDottedGetExpression(left)) {
224✔
2024
                return new DottedSetStatement(
221✔
2025
                    left.obj,
2026
                    left.name,
2027
                    operator.kind === TokenKind.Equal
2028
                        ? right
221✔
2029
                        : new BinaryExpression(left, operator, right)
2030
                );
2031
            }
2032
        }
2033
        return this.expressionStatement(expr);
275✔
2034
    }
2035

2036
    private printStatement(): PrintStatement {
2037
        let printKeyword = this.advance();
479✔
2038

2039
        let values: (
2040
            | Expression
2041
            | PrintSeparatorTab
2042
            | PrintSeparatorSpace)[] = [];
479✔
2043

2044
        while (!this.checkEndOfStatement()) {
479✔
2045
            if (this.check(TokenKind.Semicolon)) {
563✔
2046
                values.push(this.advance() as PrintSeparatorSpace);
20✔
2047
            } else if (this.check(TokenKind.Comma)) {
543✔
2048
                values.push(this.advance() as PrintSeparatorTab);
13✔
2049
            } else if (this.check(TokenKind.Else)) {
530✔
2050
                break; // inline branch
7✔
2051
            } else {
2052
                values.push(this.expression());
523✔
2053
            }
2054
        }
2055

2056
        //print statements can be empty, so look for empty print conditions
2057
        if (!values.length) {
478✔
2058
            let emptyStringLiteral = createStringLiteral('');
4✔
2059
            values.push(emptyStringLiteral);
4✔
2060
        }
2061

2062
        let last = values[values.length - 1];
478✔
2063
        if (isToken(last)) {
478✔
2064
            // TODO: error, expected value
2065
        }
2066

2067
        return new PrintStatement({ print: printKeyword }, values);
478✔
2068
    }
2069

2070
    /**
2071
     * Parses a return statement with an optional return value.
2072
     * @returns an AST representation of a return statement.
2073
     */
2074
    private returnStatement(): ReturnStatement {
2075
        let tokens = { return: this.previous() };
129✔
2076

2077
        if (this.checkEndOfStatement()) {
129✔
2078
            return new ReturnStatement(tokens);
8✔
2079
        }
2080

2081
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
121✔
2082
        return new ReturnStatement(tokens, toReturn);
120✔
2083
    }
2084

2085
    /**
2086
     * Parses a `label` statement
2087
     * @returns an AST representation of an `label` statement.
2088
     */
2089
    private labelStatement() {
2090
        let tokens = {
8✔
2091
            identifier: this.advance(),
2092
            colon: this.advance()
2093
        };
2094

2095
        //label must be alone on its line, this is probably not a label
2096
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
8✔
2097
            //rewind and cancel
2098
            this.current -= 2;
2✔
2099
            throw new CancelStatementError();
2✔
2100
        }
2101

2102
        return new LabelStatement(tokens);
6✔
2103
    }
2104

2105
    /**
2106
     * Parses a `continue` statement
2107
     */
2108
    private continueStatement() {
2109
        return new ContinueStatement({
8✔
2110
            continue: this.advance(),
2111
            loopType: this.tryConsume(
2112
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2113
                TokenKind.While, TokenKind.For
2114
            )
2115
        });
2116
    }
2117

2118
    /**
2119
     * Parses a `goto` statement
2120
     * @returns an AST representation of an `goto` statement.
2121
     */
2122
    private gotoStatement() {
2123
        let tokens = {
8✔
2124
            goto: this.advance(),
2125
            label: this.consume(
2126
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2127
                TokenKind.Identifier
2128
            )
2129
        };
2130

2131
        return new GotoStatement(tokens);
6✔
2132
    }
2133

2134
    /**
2135
     * Parses an `end` statement
2136
     * @returns an AST representation of an `end` statement.
2137
     */
2138
    private endStatement() {
2139
        let endTokens = { end: this.advance() };
4✔
2140

2141
        return new EndStatement(endTokens);
4✔
2142
    }
2143
    /**
2144
     * Parses a `stop` statement
2145
     * @returns an AST representation of a `stop` statement
2146
     */
2147
    private stopStatement() {
2148
        let tokens = { stop: this.advance() };
13✔
2149

2150
        return new StopStatement(tokens);
13✔
2151
    }
2152

2153
    /**
2154
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2155
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2156
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2157
     *                    ignored.
2158
     */
2159
    private block(...terminators: BlockTerminator[]): Block | undefined {
2160
        const parentAnnotations = this.enterAnnotationBlock();
1,567✔
2161

2162
        this.consumeStatementSeparators(true);
1,567✔
2163
        let startingToken = this.peek();
1,567✔
2164

2165
        const statements: Statement[] = [];
1,567✔
2166
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators)) {
1,567✔
2167
            //grab the location of the current token
2168
            let loopCurrent = this.current;
1,854✔
2169
            let dec = this.declaration();
1,854✔
2170
            if (dec) {
1,854✔
2171
                if (!isAnnotationExpression(dec)) {
1,772✔
2172
                    this.consumePendingAnnotations(dec);
1,768✔
2173
                    statements.push(dec);
1,768✔
2174
                }
2175

2176
                //ensure statement separator
2177
                this.consumeStatementSeparators();
1,772✔
2178

2179
            } else {
2180
                //something went wrong. reset to the top of the loop
2181
                this.current = loopCurrent;
82✔
2182

2183
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2184
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
82✔
2185

2186
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2187
                //but there's already an error in the file being parsed, so just leave this line here
2188
                this.advance();
82✔
2189

2190
                //consume potential separators
2191
                this.consumeStatementSeparators(true);
82✔
2192
            }
2193
        }
2194

2195
        if (this.isAtEnd()) {
1,567✔
2196
            return undefined;
4✔
2197
            // TODO: Figure out how to handle unterminated blocks well
2198
        } else if (terminators.length > 0) {
1,563✔
2199
            //did we hit end-sub / end-function while looking for some other terminator?
2200
            //if so, we need to restore the statement separator
2201
            let prev = this.previous().kind;
273✔
2202
            let peek = this.peek().kind;
273✔
2203
            if (
273✔
2204
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
550!
2205
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2206
            ) {
2207
                this.current--;
6✔
2208
            }
2209
        }
2210

2211
        this.exitAnnotationBlock(parentAnnotations);
1,563✔
2212
        return new Block(statements, startingToken.range);
1,563✔
2213
    }
2214

2215
    /**
2216
     * Attach pending annotations to the provided statement,
2217
     * and then reset the annotations array
2218
     */
2219
    consumePendingAnnotations(statement: Statement) {
2220
        if (this.pendingAnnotations.length) {
4,504✔
2221
            statement.annotations = this.pendingAnnotations;
27✔
2222
            this.pendingAnnotations = [];
27✔
2223
        }
2224
    }
2225

2226
    enterAnnotationBlock() {
2227
        const pending = this.pendingAnnotations;
3,813✔
2228
        this.pendingAnnotations = [];
3,813✔
2229
        return pending;
3,813✔
2230
    }
2231

2232
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2233
        // non consumed annotations are an error
2234
        if (this.pendingAnnotations.length) {
3,808✔
2235
            for (const annotation of this.pendingAnnotations) {
4✔
2236
                this.diagnostics.push({
6✔
2237
                    ...DiagnosticMessages.unusedAnnotation(),
2238
                    range: annotation.range
2239
                });
2240
            }
2241
        }
2242
        this.pendingAnnotations = parentAnnotations;
3,808✔
2243
    }
2244

2245
    private expression(): Expression {
2246
        const expression = this.anonymousFunction();
3,139✔
2247
        this._references.expressions.add(expression);
3,103✔
2248
        return expression;
3,103✔
2249
    }
2250

2251
    private anonymousFunction(): Expression {
2252
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
3,139✔
2253
            const func = this.functionDeclaration(true);
65✔
2254
            //if there's an open paren after this, this is an IIFE
2255
            if (this.check(TokenKind.LeftParen)) {
65✔
2256
                return this.finishCall(this.advance(), func);
3✔
2257
            } else {
2258
                return func;
62✔
2259
            }
2260
        }
2261

2262
        let expr = this.boolean();
3,074✔
2263

2264
        if (this.check(TokenKind.Question)) {
3,038✔
2265
            return this.ternaryExpression(expr);
69✔
2266
        } else if (this.check(TokenKind.QuestionQuestion)) {
2,969✔
2267
            return this.nullCoalescingExpression(expr);
22✔
2268
        } else {
2269
            return expr;
2,947✔
2270
        }
2271
    }
2272

2273
    private boolean(): Expression {
2274
        let expr = this.relational();
3,074✔
2275

2276
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
3,038✔
2277
            let operator = this.previous();
26✔
2278
            let right = this.relational();
26✔
2279
            this.addExpressionsToReferences(expr, right);
26✔
2280
            expr = new BinaryExpression(expr, operator, right);
26✔
2281
        }
2282

2283
        return expr;
3,038✔
2284
    }
2285

2286
    private relational(): Expression {
2287
        let expr = this.additive();
3,100✔
2288

2289
        while (
3,064✔
2290
            this.matchAny(
2291
                TokenKind.Equal,
2292
                TokenKind.LessGreater,
2293
                TokenKind.Greater,
2294
                TokenKind.GreaterEqual,
2295
                TokenKind.Less,
2296
                TokenKind.LessEqual
2297
            )
2298
        ) {
2299
            let operator = this.previous();
140✔
2300
            let right = this.additive();
140✔
2301
            this.addExpressionsToReferences(expr, right);
140✔
2302
            expr = new BinaryExpression(expr, operator, right);
140✔
2303
        }
2304

2305
        return expr;
3,064✔
2306
    }
2307

2308
    private addExpressionsToReferences(...expressions: Expression[]) {
2309
        for (const expression of expressions) {
301✔
2310
            if (!isBinaryExpression(expression)) {
559✔
2311
                this.references.expressions.add(expression);
520✔
2312
            }
2313
        }
2314
    }
2315

2316
    // TODO: bitshift
2317

2318
    private additive(): Expression {
2319
        let expr = this.multiplicative();
3,240✔
2320

2321
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
3,204✔
2322
            let operator = this.previous();
65✔
2323
            let right = this.multiplicative();
65✔
2324
            this.addExpressionsToReferences(expr, right);
65✔
2325
            expr = new BinaryExpression(expr, operator, right);
65✔
2326
        }
2327

2328
        return expr;
3,204✔
2329
    }
2330

2331
    private multiplicative(): Expression {
2332
        let expr = this.exponential();
3,305✔
2333

2334
        while (this.matchAny(
3,269✔
2335
            TokenKind.Forwardslash,
2336
            TokenKind.Backslash,
2337
            TokenKind.Star,
2338
            TokenKind.Mod,
2339
            TokenKind.LeftShift,
2340
            TokenKind.RightShift
2341
        )) {
2342
            let operator = this.previous();
21✔
2343
            let right = this.exponential();
21✔
2344
            this.addExpressionsToReferences(expr, right);
21✔
2345
            expr = new BinaryExpression(expr, operator, right);
21✔
2346
        }
2347

2348
        return expr;
3,269✔
2349
    }
2350

2351
    private exponential(): Expression {
2352
        let expr = this.prefixUnary();
3,326✔
2353

2354
        while (this.match(TokenKind.Caret)) {
3,290✔
2355
            let operator = this.previous();
6✔
2356
            let right = this.prefixUnary();
6✔
2357
            this.addExpressionsToReferences(expr, right);
6✔
2358
            expr = new BinaryExpression(expr, operator, right);
6✔
2359
        }
2360

2361
        return expr;
3,290✔
2362
    }
2363

2364
    private prefixUnary(): Expression {
2365
        const nextKind = this.peek().kind;
3,353✔
2366
        if (nextKind === TokenKind.Not || nextKind === TokenKind.Minus) {
3,353✔
2367
            this.current++; //advance
21✔
2368
            let operator = this.previous();
21✔
2369
            let right = this.prefixUnary();
21✔
2370
            return new UnaryExpression(operator, right);
21✔
2371
        }
2372
        return this.call();
3,332✔
2373
    }
2374

2375
    private indexedGet(expr: Expression) {
2376
        let openingSquare = this.previous();
95✔
2377
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
95✔
2378
        let index: Expression;
2379
        let closingSquare: Token;
2380
        while (this.match(TokenKind.Newline)) { }
95✔
2381
        try {
95✔
2382
            index = this.expression();
95✔
2383
        } catch (error) {
2384
            this.rethrowNonDiagnosticError(error);
1✔
2385
        }
2386

2387
        while (this.match(TokenKind.Newline)) { }
95✔
2388
        closingSquare = this.tryConsume(
95✔
2389
            DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
2390
            TokenKind.RightSquareBracket
2391
        );
2392

2393
        return new IndexedGetExpression(expr, index, openingSquare, closingSquare, questionDotToken);
95✔
2394
    }
2395

2396
    private newExpression() {
2397
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
39✔
2398
        let newToken = this.advance();
39✔
2399

2400
        let nameExpr = this.getNamespacedVariableNameExpression();
39✔
2401
        let leftParen = this.consume(
39✔
2402
            DiagnosticMessages.unexpectedToken(this.peek().text),
2403
            TokenKind.LeftParen,
2404
            TokenKind.QuestionLeftParen
2405
        );
2406
        let call = this.finishCall(leftParen, nameExpr);
35✔
2407
        //pop the call from the  callExpressions list because this is technically something else
2408
        this.callExpressions.pop();
35✔
2409
        let result = new NewExpression(newToken, call);
35✔
2410
        this._references.newExpressions.push(result);
35✔
2411
        return result;
35✔
2412
    }
2413

2414
    /**
2415
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2416
     */
2417
    private callfunc(callee: Expression): Expression {
2418
        this.warnIfNotBrighterScriptMode('callfunc operator');
18✔
2419
        let operator = this.previous();
18✔
2420
        let methodName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
18✔
2421
        // force it into an identifier so the AST makes some sense
2422
        methodName.kind = TokenKind.Identifier;
16✔
2423
        let openParen = this.consume(DiagnosticMessages.expectedOpenParenToFollowCallfuncIdentifier(), TokenKind.LeftParen);
16✔
2424
        let call = this.finishCall(openParen, callee, false);
16✔
2425

2426
        return new CallfuncExpression(callee, operator, methodName as Identifier, openParen, call.args, call.closingParen);
16✔
2427
    }
2428

2429
    private call(): Expression {
2430
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
3,879✔
2431
            return this.newExpression();
39✔
2432
        }
2433
        let expr = this.primary();
3,840✔
2434
        //an expression to keep for _references
2435
        let referenceCallExpression: Expression;
2436
        while (true) {
3,774✔
2437
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
5,042✔
2438
                expr = this.finishCall(this.previous(), expr);
421✔
2439
                //store this call expression in references
2440
                referenceCallExpression = expr;
421✔
2441

2442
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
4,621✔
2443
                expr = this.indexedGet(expr);
93✔
2444

2445
            } else if (this.match(TokenKind.Callfunc)) {
4,528✔
2446
                expr = this.callfunc(expr);
18✔
2447
                //store this callfunc expression in references
2448
                referenceCallExpression = expr;
16✔
2449

2450
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
4,510✔
2451
                if (this.match(TokenKind.LeftSquareBracket)) {
760✔
2452
                    expr = this.indexedGet(expr);
2✔
2453
                } else {
2454
                    let dot = this.previous();
758✔
2455
                    let name = this.tryConsume(
758✔
2456
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2457
                        TokenKind.Identifier,
2458
                        ...AllowedProperties
2459
                    );
2460
                    if (!name) {
758✔
2461
                        break;
22✔
2462
                    }
2463

2464
                    // force it into an identifier so the AST makes some sense
2465
                    name.kind = TokenKind.Identifier;
736✔
2466
                    expr = new DottedGetExpression(expr, name as Identifier, dot);
736✔
2467

2468
                    this.addPropertyHints(name);
736✔
2469
                }
2470

2471
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
3,750✔
2472
                let dot = this.advance();
7✔
2473
                let name = this.tryConsume(
7✔
2474
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2475
                    TokenKind.Identifier,
2476
                    ...AllowedProperties
2477
                );
2478

2479
                // force it into an identifier so the AST makes some sense
2480
                name.kind = TokenKind.Identifier;
7✔
2481
                if (!name) {
7!
2482
                    break;
×
2483
                }
2484
                expr = new XmlAttributeGetExpression(expr, name as Identifier, dot);
7✔
2485
                //only allow a single `@` expression
2486
                break;
7✔
2487

2488
            } else {
2489
                break;
3,743✔
2490
            }
2491
        }
2492
        //if we found a callExpression, add it to `expressions` in references
2493
        if (referenceCallExpression) {
3,772✔
2494
            this._references.expressions.add(referenceCallExpression);
399✔
2495
        }
2496
        return expr;
3,772✔
2497
    }
2498

2499
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
459✔
2500
        let args = [] as Expression[];
481✔
2501
        while (this.match(TokenKind.Newline)) { }
481✔
2502

2503
        if (!this.check(TokenKind.RightParen)) {
481✔
2504
            do {
256✔
2505
                while (this.match(TokenKind.Newline)) { }
373✔
2506

2507
                if (args.length >= CallExpression.MaximumArguments) {
373!
2508
                    this.diagnostics.push({
×
2509
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
2510
                        range: this.peek().range
2511
                    });
2512
                    throw this.lastDiagnosticAsError();
×
2513
                }
2514
                try {
373✔
2515
                    args.push(this.expression());
373✔
2516
                } catch (error) {
2517
                    this.rethrowNonDiagnosticError(error);
5✔
2518
                    // we were unable to get an expression, so don't continue
2519
                    break;
5✔
2520
                }
2521
            } while (this.match(TokenKind.Comma));
2522
        }
2523

2524
        while (this.match(TokenKind.Newline)) { }
481✔
2525

2526
        const closingParen = this.tryConsume(
481✔
2527
            DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
2528
            TokenKind.RightParen
2529
        );
2530

2531
        let expression = new CallExpression(callee, openingParen, closingParen, args);
481✔
2532
        if (addToCallExpressionList) {
481✔
2533
            this.callExpressions.push(expression);
459✔
2534
        }
2535
        return expression;
481✔
2536
    }
2537

2538
    /**
2539
     * Tries to get the next token as a type
2540
     * Allows for built-in types (double, string, etc.) or namespaced custom types in Brighterscript mode
2541
     * Will  return a token of whatever is next to be parsed (unless `advanceIfUnknown` is false, in which case undefined will be returned instead
2542
     */
2543
    private typeToken(): Token {
2544
        let typeToken: Token;
2545

2546
        if (this.checkAny(...DeclarableTypes)) {
334✔
2547
            // Token is a built in type
2548
            typeToken = this.advance();
268✔
2549
        } else if (this.options.mode === ParseMode.BrighterScript) {
66✔
2550
            try {
45✔
2551
                // see if we can get a namespaced identifer
2552
                const qualifiedType = this.getNamespacedVariableNameExpression();
45✔
2553
                typeToken = createToken(TokenKind.Identifier, qualifiedType.getName(this.options.mode), qualifiedType.range);
44✔
2554
            } catch {
2555
                //could not get an identifier - just get whatever's next
2556
                typeToken = this.advance();
1✔
2557
            }
2558
        } else {
2559
            // just get whatever's next
2560
            typeToken = this.advance();
21✔
2561
        }
2562
        return typeToken;
334✔
2563
    }
2564

2565
    private primary(): Expression {
2566
        switch (true) {
3,840✔
2567
            case this.matchAny(
3,840!
2568
                TokenKind.False,
2569
                TokenKind.True,
2570
                TokenKind.Invalid,
2571
                TokenKind.IntegerLiteral,
2572
                TokenKind.LongIntegerLiteral,
2573
                TokenKind.FloatLiteral,
2574
                TokenKind.DoubleLiteral,
2575
                TokenKind.StringLiteral
2576
            ):
2577
                return new LiteralExpression(this.previous());
2,241✔
2578

2579
            //capture source literals (LINE_NUM if brightscript, or a bunch of them if brighterscript)
2580
            case this.matchAny(TokenKind.LineNumLiteral, ...(this.options.mode === ParseMode.BrightScript ? [] : BrighterScriptSourceLiterals)):
1,599✔
2581
                return new SourceLiteralExpression(this.previous());
23✔
2582

2583
            //template string
2584
            case this.check(TokenKind.BackTick):
2585
                return this.templateString(false);
29✔
2586

2587
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
2588
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
2,708✔
2589
                return this.templateString(true);
3✔
2590

2591
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
2592
                return new VariableExpression(this.previous() as Identifier);
1,165✔
2593

2594
            case this.match(TokenKind.LeftParen):
2595
                let left = this.previous();
23✔
2596
                let expr = this.expression();
23✔
2597
                let right = this.consume(
22✔
2598
                    DiagnosticMessages.unmatchedLeftParenAfterExpression(),
2599
                    TokenKind.RightParen
2600
                );
2601
                return new GroupingExpression({ left: left, right: right }, expr);
22✔
2602

2603
            case this.matchAny(TokenKind.LeftSquareBracket):
2604
                return this.arrayLiteral();
69✔
2605

2606
            case this.match(TokenKind.LeftCurlyBrace):
2607
                return this.aaLiteral();
180✔
2608

2609
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
2610
                let token = Object.assign(this.previous(), {
×
2611
                    kind: TokenKind.Identifier
2612
                }) as Identifier;
2613
                return new VariableExpression(token);
×
2614

2615
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
2616
                return this.anonymousFunction();
×
2617

2618
            case this.check(TokenKind.RegexLiteral):
2619
                return this.regexLiteralExpression();
42✔
2620

2621
            case this.check(TokenKind.Comment):
2622
                return new CommentStatement([this.advance()]);
2✔
2623

2624
            default:
2625
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
2626
                if (this.checkAny(...this.peekGlobalTerminators())) {
63!
2627
                    //don't throw a diagnostic, just return undefined
2628

2629
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
2630
                } else {
2631
                    this.diagnostics.push({
63✔
2632
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
2633
                        range: this.peek().range
2634
                    });
2635
                    throw this.lastDiagnosticAsError();
63✔
2636
                }
2637
        }
2638
    }
2639

2640
    private arrayLiteral() {
2641
        let elements: Array<Expression | CommentStatement> = [];
69✔
2642
        let openingSquare = this.previous();
69✔
2643

2644
        //add any comment found right after the opening square
2645
        if (this.check(TokenKind.Comment)) {
69✔
2646
            elements.push(new CommentStatement([this.advance()]));
1✔
2647
        }
2648

2649
        while (this.match(TokenKind.Newline)) {
69✔
2650
        }
2651
        let closingSquare: Token;
2652

2653
        if (!this.match(TokenKind.RightSquareBracket)) {
69✔
2654
            try {
45✔
2655
                elements.push(this.expression());
45✔
2656

2657
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
44✔
2658
                    if (this.checkPrevious(TokenKind.Comment) || this.check(TokenKind.Comment)) {
80✔
2659
                        let comment = this.check(TokenKind.Comment) ? this.advance() : this.previous();
3!
2660
                        elements.push(new CommentStatement([comment]));
3✔
2661
                    }
2662
                    while (this.match(TokenKind.Newline)) {
80✔
2663

2664
                    }
2665

2666
                    if (this.check(TokenKind.RightSquareBracket)) {
80✔
2667
                        break;
13✔
2668
                    }
2669

2670
                    elements.push(this.expression());
67✔
2671
                }
2672
            } catch (error: any) {
2673
                this.rethrowNonDiagnosticError(error);
2✔
2674
            }
2675

2676
            closingSquare = this.tryConsume(
45✔
2677
                DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral(),
2678
                TokenKind.RightSquareBracket
2679
            );
2680
        } else {
2681
            closingSquare = this.previous();
24✔
2682
        }
2683

2684
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2685
        return new ArrayLiteralExpression(elements, openingSquare, closingSquare);
69✔
2686
    }
2687

2688
    private aaLiteral() {
2689
        let openingBrace = this.previous();
180✔
2690
        let members: Array<AAMemberExpression | CommentStatement> = [];
180✔
2691

2692
        let key = () => {
180✔
2693
            let result = {
183✔
2694
                colonToken: null as Token,
2695
                keyToken: null as Token,
2696
                range: null as Range
2697
            };
2698
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
183✔
2699
                result.keyToken = this.identifier(...AllowedProperties);
153✔
2700
            } else if (this.check(TokenKind.StringLiteral)) {
30!
2701
                result.keyToken = this.advance();
30✔
2702
            } else {
2703
                this.diagnostics.push({
×
2704
                    ...DiagnosticMessages.unexpectedAAKey(),
2705
                    range: this.peek().range
2706
                });
2707
                throw this.lastDiagnosticAsError();
×
2708
            }
2709

2710
            result.colonToken = this.consume(
183✔
2711
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
2712
                TokenKind.Colon
2713
            );
2714
            result.range = util.getRange(result.keyToken, result.colonToken);
182✔
2715
            return result;
182✔
2716
        };
2717

2718
        while (this.match(TokenKind.Newline)) { }
180✔
2719
        let closingBrace: Token;
2720
        if (!this.match(TokenKind.RightCurlyBrace)) {
180✔
2721
            let lastAAMember: AAMemberExpression;
2722
            try {
137✔
2723
                if (this.check(TokenKind.Comment)) {
137✔
2724
                    lastAAMember = null;
6✔
2725
                    members.push(new CommentStatement([this.advance()]));
6✔
2726
                } else {
2727
                    let k = key();
131✔
2728
                    let expr = this.expression();
131✔
2729
                    lastAAMember = new AAMemberExpression(
130✔
2730
                        k.keyToken,
2731
                        k.colonToken,
2732
                        expr
2733
                    );
2734
                    members.push(lastAAMember);
130✔
2735
                }
2736

2737
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
136✔
2738
                    // collect comma at end of expression
2739
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
176✔
2740
                        lastAAMember.commaToken = this.previous();
32✔
2741
                    }
2742

2743
                    //check for comment at the end of the current line
2744
                    if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
176✔
2745
                        let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
14✔
2746
                        members.push(new CommentStatement([token]));
14✔
2747
                    } else {
2748
                        this.consumeStatementSeparators(true);
162✔
2749

2750
                        //check for a comment on its own line
2751
                        if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
162✔
2752
                            let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
1!
2753
                            lastAAMember = null;
1✔
2754
                            members.push(new CommentStatement([token]));
1✔
2755
                            continue;
1✔
2756
                        }
2757

2758
                        if (this.check(TokenKind.RightCurlyBrace)) {
161✔
2759
                            break;
109✔
2760
                        }
2761
                        let k = key();
52✔
2762
                        let expr = this.expression();
51✔
2763
                        lastAAMember = new AAMemberExpression(
51✔
2764
                            k.keyToken,
2765
                            k.colonToken,
2766
                            expr
2767
                        );
2768
                        members.push(lastAAMember);
51✔
2769
                    }
2770
                }
2771
            } catch (error: any) {
2772
                this.rethrowNonDiagnosticError(error);
2✔
2773
            }
2774

2775
            closingBrace = this.tryConsume(
137✔
2776
                DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral(),
2777
                TokenKind.RightCurlyBrace
2778
            );
2779
        } else {
2780
            closingBrace = this.previous();
43✔
2781
        }
2782

2783
        const aaExpr = new AALiteralExpression(members, openingBrace, closingBrace);
180✔
2784
        this.addPropertyHints(aaExpr);
180✔
2785
        return aaExpr;
180✔
2786
    }
2787

2788
    /**
2789
     * Pop token if we encounter specified token
2790
     */
2791
    private match(tokenKind: TokenKind) {
2792
        if (this.check(tokenKind)) {
14,654✔
2793
            this.current++; //advance
858✔
2794
            return true;
858✔
2795
        }
2796
        return false;
13,796✔
2797
    }
2798

2799
    /**
2800
     * Pop token if we encounter a token in the specified list
2801
     * @param tokenKinds
2802
     */
2803
    private matchAny(...tokenKinds: TokenKind[]) {
2804
        for (let tokenKind of tokenKinds) {
52,152✔
2805
            if (this.check(tokenKind)) {
153,407✔
2806
                this.current++; //advance
13,176✔
2807
                return true;
13,176✔
2808
            }
2809
        }
2810
        return false;
38,976✔
2811
    }
2812

2813
    /**
2814
     * If the next series of tokens matches the given set of tokens, pop them all
2815
     * @param tokenKinds
2816
     */
2817
    private matchSequence(...tokenKinds: TokenKind[]) {
2818
        const endIndex = this.current + tokenKinds.length;
4,531✔
2819
        for (let i = 0; i < tokenKinds.length; i++) {
4,531✔
2820
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
4,548!
2821
                return false;
4,528✔
2822
            }
2823
        }
2824
        this.current = endIndex;
3✔
2825
        return true;
3✔
2826
    }
2827

2828
    /**
2829
     * Get next token matching a specified list, or fail with an error
2830
     */
2831
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
2832
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
5,050✔
2833
        if (token) {
5,050✔
2834
            return token;
5,034✔
2835
        } else {
2836
            let error = new Error(diagnosticInfo.message);
16✔
2837
            (error as any).isDiagnostic = true;
16✔
2838
            throw error;
16✔
2839
        }
2840
    }
2841

2842
    private consumeToken(tokenKind: TokenKind) {
2843
        return this.consume(
145✔
2844
            DiagnosticMessages.expectedToken(tokenKind),
2845
            tokenKind
2846
        );
2847
    }
2848

2849
    /**
2850
     * Consume, or add a message if not found. But then continue and return undefined
2851
     */
2852
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
2853
        const nextKind = this.peek().kind;
7,563✔
2854
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
27,920✔
2855

2856
        if (foundTokenKind) {
7,563✔
2857
            return this.advance();
7,481✔
2858
        }
2859
        this.diagnostics.push({
82✔
2860
            ...diagnostic,
2861
            range: this.peek().range
2862
        });
2863
    }
2864

2865
    private tryConsumeToken(tokenKind: TokenKind) {
2866
        return this.tryConsume(
69✔
2867
            DiagnosticMessages.expectedToken(tokenKind),
2868
            tokenKind
2869
        );
2870
    }
2871

2872
    private consumeStatementSeparators(optional = false) {
2,908✔
2873
        //a comment or EOF mark the end of the statement
2874
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
9,994✔
2875
            return true;
499✔
2876
        }
2877
        let consumed = false;
9,495✔
2878
        //consume any newlines and colons
2879
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
9,495✔
2880
            consumed = true;
7,899✔
2881
        }
2882
        if (!optional && !consumed) {
9,495✔
2883
            this.diagnostics.push({
35✔
2884
                ...DiagnosticMessages.expectedNewlineOrColon(),
2885
                range: this.peek().range
2886
            });
2887
        }
2888
        return consumed;
9,495✔
2889
    }
2890

2891
    private advance(): Token {
2892
        if (!this.isAtEnd()) {
17,002✔
2893
            this.current++;
16,986✔
2894
        }
2895
        return this.previous();
17,002✔
2896
    }
2897

2898
    private checkEndOfStatement(): boolean {
2899
        const nextKind = this.peek().kind;
1,163✔
2900
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
1,163✔
2901
    }
2902

2903
    private checkPrevious(tokenKind: TokenKind): boolean {
2904
        return this.previous()?.kind === tokenKind;
604!
2905
    }
2906

2907
    private check(tokenKind: TokenKind): boolean {
2908
        const nextKind = this.peek().kind;
252,200✔
2909
        if (nextKind === TokenKind.Eof) {
252,200✔
2910
            return false;
6,350✔
2911
        }
2912
        return nextKind === tokenKind;
245,850✔
2913
    }
2914

2915
    private checkAny(...tokenKinds: TokenKind[]): boolean {
2916
        const nextKind = this.peek().kind;
31,702✔
2917
        if (nextKind === TokenKind.Eof) {
31,702✔
2918
            return false;
216✔
2919
        }
2920
        return tokenKinds.includes(nextKind);
31,486✔
2921
    }
2922

2923
    private checkNext(tokenKind: TokenKind): boolean {
2924
        if (this.isAtEnd()) {
2,731!
2925
            return false;
×
2926
        }
2927
        return this.peekNext().kind === tokenKind;
2,731✔
2928
    }
2929

2930
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
2931
        if (this.isAtEnd()) {
1,929!
2932
            return false;
×
2933
        }
2934
        const nextKind = this.peekNext().kind;
1,929✔
2935
        return tokenKinds.includes(nextKind);
1,929✔
2936
    }
2937

2938
    private isAtEnd(): boolean {
2939
        return this.peek().kind === TokenKind.Eof;
45,786✔
2940
    }
2941

2942
    private peekNext(): Token {
2943
        if (this.isAtEnd()) {
4,660!
2944
            return this.peek();
×
2945
        }
2946
        return this.tokens[this.current + 1];
4,660✔
2947
    }
2948

2949
    private peek(): Token {
2950
        return this.tokens[this.current];
346,056✔
2951
    }
2952

2953
    private previous(): Token {
2954
        return this.tokens[this.current - 1];
24,450✔
2955
    }
2956

2957
    /**
2958
     * Sometimes we catch an error that is a diagnostic.
2959
     * If that's the case, we want to continue parsing.
2960
     * Otherwise, re-throw the error
2961
     *
2962
     * @param error error caught in a try/catch
2963
     */
2964
    private rethrowNonDiagnosticError(error) {
2965
        if (!error.isDiagnostic) {
10!
2966
            throw error;
×
2967
        }
2968
    }
2969

2970
    /**
2971
     * Get the token that is {offset} indexes away from {this.current}
2972
     * @param offset the number of index steps away from current index to fetch
2973
     * @param tokenKinds the desired token must match one of these
2974
     * @example
2975
     * getToken(-1); //returns the previous token.
2976
     * getToken(0);  //returns current token.
2977
     * getToken(1);  //returns next token
2978
     */
2979
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
2980
        const token = this.tokens[this.current + offset];
95✔
2981
        if (tokenKinds.includes(token.kind)) {
95✔
2982
            return token;
3✔
2983
        }
2984
    }
2985

2986
    private synchronize() {
2987
        this.advance(); // skip the erroneous token
126✔
2988

2989
        while (!this.isAtEnd()) {
126✔
2990
            if (this.ensureNewLineOrColon(true)) {
207✔
2991
                // end of statement reached
2992
                return;
91✔
2993
            }
2994

2995
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
116✔
2996
                case TokenKind.Namespace:
8!
2997
                case TokenKind.Class:
2998
                case TokenKind.Function:
2999
                case TokenKind.Sub:
3000
                case TokenKind.If:
3001
                case TokenKind.For:
3002
                case TokenKind.ForEach:
3003
                case TokenKind.While:
3004
                case TokenKind.Print:
3005
                case TokenKind.Return:
3006
                    // start parsing again from the next block starter or obvious
3007
                    // expression start
3008
                    return;
1✔
3009
            }
3010

3011
            this.advance();
115✔
3012
        }
3013
    }
3014

3015
    /**
3016
     * References are found during the initial parse.
3017
     * However, sometimes plugins can modify the AST, requiring a full walk to re-compute all references.
3018
     * This does that walk.
3019
     */
3020
    private findReferences() {
3021
        this._references = new References();
7✔
3022
        const excludedExpressions = new Set<Expression>();
7✔
3023

3024
        const visitCallExpression = (e: CallExpression | CallfuncExpression) => {
7✔
3025
            for (const p of e.args) {
14✔
3026
                this._references.expressions.add(p);
7✔
3027
            }
3028
            //add calls that were not excluded (from loop below)
3029
            if (!excludedExpressions.has(e)) {
14✔
3030
                this._references.expressions.add(e);
12✔
3031
            }
3032

3033
            //if this call is part of a longer expression that includes a call higher up, find that higher one and remove it
3034
            if (e.callee) {
14!
3035
                let node: Expression = e.callee;
14✔
3036
                while (node) {
14✔
3037
                    //the primary goal for this loop. If we found a parent call expression, remove it from `references`
3038
                    if (isCallExpression(node)) {
22✔
3039
                        this.references.expressions.delete(node);
2✔
3040
                        excludedExpressions.add(node);
2✔
3041
                        //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.
3042
                        break;
2✔
3043

3044
                        //when we hit a variable expression, we're definitely at the leftmost expression so stop
3045
                    } else if (isVariableExpression(node)) {
20✔
3046
                        break;
12✔
3047
                        //if
3048

3049
                    } else if (isDottedGetExpression(node) || isIndexedGetExpression(node)) {
8!
3050
                        node = node.obj;
8✔
3051
                    } else {
3052
                        //some expression we don't understand. log it and quit the loop
3053
                        this.logger.info('Encountered unknown expression while calculating function expression chain', node);
×
3054
                        break;
×
3055
                    }
3056
                }
3057
            }
3058
        };
3059

3060
        this.ast.walk(createVisitor({
7✔
3061
            AssignmentStatement: s => {
3062
                this._references.assignmentStatements.push(s);
11✔
3063
                this.references.expressions.add(s.value);
11✔
3064
            },
3065
            ClassStatement: s => {
3066
                this._references.classStatements.push(s);
1✔
3067
            },
3068
            ClassFieldStatement: s => {
3069
                if (s.initialValue) {
1!
3070
                    this._references.expressions.add(s.initialValue);
1✔
3071
                }
3072
            },
3073
            NamespaceStatement: s => {
3074
                this._references.namespaceStatements.push(s);
×
3075
            },
3076
            FunctionStatement: s => {
3077
                this._references.functionStatements.push(s);
4✔
3078
            },
3079
            ImportStatement: s => {
3080
                this._references.importStatements.push(s);
1✔
3081
            },
3082
            LibraryStatement: s => {
3083
                this._references.libraryStatements.push(s);
×
3084
            },
3085
            FunctionExpression: (expression, parent) => {
3086
                if (!isMethodStatement(parent)) {
4!
3087
                    this._references.functionExpressions.push(expression);
4✔
3088
                }
3089
            },
3090
            NewExpression: e => {
3091
                this._references.newExpressions.push(e);
×
3092
                for (const p of e.call.args) {
×
3093
                    this._references.expressions.add(p);
×
3094
                }
3095
            },
3096
            ExpressionStatement: s => {
3097
                this._references.expressions.add(s.expression);
7✔
3098
            },
3099
            CallfuncExpression: e => {
3100
                visitCallExpression(e);
1✔
3101
            },
3102
            CallExpression: e => {
3103
                visitCallExpression(e);
13✔
3104
            },
3105
            AALiteralExpression: e => {
3106
                this.addPropertyHints(e);
8✔
3107
                this._references.expressions.add(e);
8✔
3108
                for (const member of e.elements) {
8✔
3109
                    if (isAAMemberExpression(member)) {
16!
3110
                        this._references.expressions.add(member.value);
16✔
3111
                    }
3112
                }
3113
            },
3114
            BinaryExpression: (e, parent) => {
3115
                //walk the chain of binary expressions and add each one to the list of expressions
3116
                const expressions: Expression[] = [e];
14✔
3117
                let expression: Expression;
3118
                while ((expression = expressions.pop())) {
14✔
3119
                    if (isBinaryExpression(expression)) {
64✔
3120
                        expressions.push(expression.left, expression.right);
25✔
3121
                    } else {
3122
                        this._references.expressions.add(expression);
39✔
3123
                    }
3124
                }
3125
            },
3126
            ArrayLiteralExpression: e => {
3127
                for (const element of e.elements) {
1✔
3128
                    //keep everything except comments
3129
                    if (!isCommentStatement(element)) {
1!
3130
                        this._references.expressions.add(element);
1✔
3131
                    }
3132
                }
3133
            },
3134
            DottedGetExpression: e => {
3135
                this.addPropertyHints(e.name);
23✔
3136
            },
3137
            DottedSetStatement: e => {
3138
                this.addPropertyHints(e.name);
4✔
3139
            },
3140
            EnumStatement: e => {
3141
                this._references.enumStatements.push(e);
×
3142
            },
3143
            ConstStatement: s => {
3144
                this._references.constStatements.push(s);
×
3145
            },
3146
            UnaryExpression: e => {
3147
                this._references.expressions.add(e);
×
3148
            },
3149
            IncrementStatement: e => {
3150
                this._references.expressions.add(e);
2✔
3151
            }
3152
        }), {
3153
            walkMode: WalkMode.visitAllRecursive
3154
        });
3155
    }
3156

3157
    public dispose() {
3158
    }
3159
}
3160

3161
export enum ParseMode {
1✔
3162
    BrightScript = 'BrightScript',
1✔
3163
    BrighterScript = 'BrighterScript'
1✔
3164
}
3165

3166
export interface ParseOptions {
3167
    /**
3168
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3169
     */
3170
    mode: ParseMode;
3171
    /**
3172
     * A logger that should be used for logging. If omitted, a default logger is used
3173
     */
3174
    logger?: Logger;
3175
}
3176

3177
export class References {
1✔
3178
    private cache = new Cache();
1,633✔
3179
    public assignmentStatements = [] as AssignmentStatement[];
1,633✔
3180
    public classStatements = [] as ClassStatement[];
1,633✔
3181

3182
    public get classStatementLookup() {
3183
        if (!this._classStatementLookup) {
17✔
3184
            this._classStatementLookup = new Map();
15✔
3185
            for (const stmt of this.classStatements) {
15✔
3186
                this._classStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
2✔
3187
            }
3188
        }
3189
        return this._classStatementLookup;
17✔
3190
    }
3191
    private _classStatementLookup: Map<string, ClassStatement>;
3192

3193
    public functionExpressions = [] as FunctionExpression[];
1,633✔
3194
    public functionStatements = [] as FunctionStatement[];
1,633✔
3195
    /**
3196
     * A map of function statements, indexed by fully-namespaced lower function name.
3197
     */
3198
    public get functionStatementLookup() {
3199
        if (!this._functionStatementLookup) {
17✔
3200
            this._functionStatementLookup = new Map();
15✔
3201
            for (const stmt of this.functionStatements) {
15✔
3202
                this._functionStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
13✔
3203
            }
3204
        }
3205
        return this._functionStatementLookup;
17✔
3206
    }
3207
    private _functionStatementLookup: Map<string, FunctionStatement>;
3208

3209
    public interfaceStatements = [] as InterfaceStatement[];
1,633✔
3210

3211
    public get interfaceStatementLookup() {
3212
        if (!this._interfaceStatementLookup) {
×
3213
            this._interfaceStatementLookup = new Map();
×
3214
            for (const stmt of this.interfaceStatements) {
×
3215
                this._interfaceStatementLookup.set(stmt.fullName.toLowerCase(), stmt);
×
3216
            }
3217
        }
3218
        return this._interfaceStatementLookup;
×
3219
    }
3220
    private _interfaceStatementLookup: Map<string, InterfaceStatement>;
3221

3222
    public enumStatements = [] as EnumStatement[];
1,633✔
3223

3224
    public get enumStatementLookup() {
3225
        return this.cache.getOrAdd('enums', () => {
18✔
3226
            const result = new Map<string, EnumStatement>();
16✔
3227
            for (const stmt of this.enumStatements) {
16✔
3228
                result.set(stmt.fullName.toLowerCase(), stmt);
1✔
3229
            }
3230
            return result;
16✔
3231
        });
3232
    }
3233

3234
    public constStatements = [] as ConstStatement[];
1,633✔
3235

3236
    public get constStatementLookup() {
3237
        return this.cache.getOrAdd('consts', () => {
×
3238
            const result = new Map<string, ConstStatement>();
×
3239
            for (const stmt of this.constStatements) {
×
3240
                result.set(stmt.fullName.toLowerCase(), stmt);
×
3241
            }
3242
            return result;
×
3243
        });
3244
    }
3245

3246
    /**
3247
     * A collection of full expressions. This excludes intermediary expressions.
3248
     *
3249
     * Example 1:
3250
     * `a.b.c` is composed of `a` (variableExpression)  `.b` (DottedGetExpression) `.c` (DottedGetExpression)
3251
     * This will only contain the final `.c` DottedGetExpression because `.b` and `a` can both be derived by walking back from the `.c` DottedGetExpression.
3252
     *
3253
     * Example 2:
3254
     * `name.space.doSomething(a.b.c)` will result in 2 entries in this list. the `CallExpression` for `doSomething`, and the `.c` DottedGetExpression.
3255
     *
3256
     * Example 3:
3257
     * `value = SomeEnum.value > 2 or SomeEnum.otherValue < 10` will result in 4 entries. `SomeEnum.value`, `2`, `SomeEnum.otherValue`, `10`
3258
     */
3259
    public expressions = new Set<Expression>();
1,633✔
3260

3261
    public importStatements = [] as ImportStatement[];
1,633✔
3262
    public libraryStatements = [] as LibraryStatement[];
1,633✔
3263
    public namespaceStatements = [] as NamespaceStatement[];
1,633✔
3264
    public newExpressions = [] as NewExpression[];
1,633✔
3265
    public propertyHints = {} as Record<string, string>;
1,633✔
3266
}
3267

3268
class CancelStatementError extends Error {
3269
    constructor() {
3270
        super('CancelStatement');
2✔
3271
    }
3272
}
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