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

rokucommunity / brighterscript / #13768

16 Jan 2024 07:10PM UTC coverage: 88.101% (+0.05%) from 88.056%
#13768

push

TwitchBronBron
0.65.17

5759 of 7011 branches covered (82.14%)

Branch coverage included in aggregate %.

8612 of 9301 relevant lines covered (92.59%)

1666.09 hits per line

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

92.11
/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
import { SymbolTable } from '../SymbolTable';
1✔
98
import type { BscType } from '../types/BscType';
99

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

106
    /**
107
     * The current token index
108
     */
109
    public current: number;
110

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

116
    public get statements() {
117
        return this.ast.statements;
486✔
118
    }
119

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

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

141
    private _references = new References();
1,750✔
142

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

150
    private addPropertyHints(item: Token | AALiteralExpression) {
151
        if (isToken(item)) {
1,069✔
152
            const name = item.text;
879✔
153
            this._references.propertyHints[name.toLowerCase()] = name;
879✔
154
        } else {
155
            for (const member of item.elements) {
190✔
156
                if (!isCommentStatement(member)) {
219✔
157
                    const name = member.keyToken.text;
198✔
158
                    if (!name.startsWith('"')) {
198✔
159
                        this._references.propertyHints[name.toLowerCase()] = name;
167✔
160
                    }
161
                }
162
            }
163
        }
164
    }
165

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

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

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

181
    private globalTerminators = [] as TokenKind[][];
1,750✔
182

183
    /**
184
     * 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
185
     * based on the parse mode
186
     */
187
    private allowedLocalIdentifiers: TokenKind[];
188

189
    /**
190
     * Annotations collected which should be attached to the next statement
191
     */
192
    private pendingAnnotations: AnnotationExpression[];
193

194
    /**
195
     * Get the currently active global terminators
196
     */
197
    private peekGlobalTerminators() {
198
        return this.globalTerminators[this.globalTerminators.length - 1] ?? [];
5,935✔
199
    }
200

201
    /**
202
     * Static wrapper around creating a new parser and parsing a list of tokens
203
     */
204
    public static parse(toParse: Token[] | string, options?: ParseOptions): Parser {
205
        return new Parser().parse(toParse, options);
1,734✔
206
    }
207

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

233
        this.ast = this.body();
1,735✔
234

235
        //now that we've built the AST, link every node to its parent
236
        this.ast.link();
1,735✔
237
        return this;
1,735✔
238
    }
239

240
    private logger: Logger;
241

242
    private body() {
243
        const parentAnnotations = this.enterAnnotationBlock();
1,944✔
244

245
        let body = new Body([]);
1,944✔
246
        if (this.tokens.length > 0) {
1,944✔
247
            this.consumeStatementSeparators(true);
1,943✔
248

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

274
        this.exitAnnotationBlock(parentAnnotations);
1,944✔
275
        return body;
1,944✔
276
    }
277

278
    private sanitizeParseOptions(options: ParseOptions) {
279
        return {
1,735✔
280
            mode: 'brightscript',
281
            ...(options || {})
2,120✔
282
        } as ParseOptions;
283
    }
284

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

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

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

314
    private declaration(): Statement | AnnotationExpression | undefined {
315
        try {
4,552✔
316
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
4,552✔
317
                return this.functionDeclaration(false);
1,122✔
318
            }
319

320
            if (this.checkLibrary()) {
3,430✔
321
                return this.libraryStatement();
11✔
322
            }
323

324
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
3,419✔
325
                return this.constDeclaration();
39✔
326
            }
327

328
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
3,380✔
329
                return this.annotationExpression();
28✔
330
            }
331

332
            if (this.check(TokenKind.Comment)) {
3,352✔
333
                return this.commentStatement();
199✔
334
            }
335

336
            //catch certain global terminators to prevent unnecessary lookahead (i.e. like `end namespace`, no need to continue)
337
            if (this.checkAny(...this.peekGlobalTerminators())) {
3,153!
338
                return;
×
339
            }
340

341
            return this.statement();
3,153✔
342
        } catch (error: any) {
343
            //if the error is not a diagnostic, then log the error for debugging purposes
344
            if (!error.isDiagnostic) {
123!
345
                this.logger.error(error);
×
346
            }
347
            this.synchronize();
123✔
348
        }
349
    }
350

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

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

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

393
    /**
394
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration`
395
     */
396
    private interfaceFieldStatement(optionalKeyword?: Token) {
397
        const name = this.identifier(...AllowedProperties);
26✔
398
        let asToken: Token;
399
        let typeToken: Token;
400
        let type: BscType;
401
        if (this.check(TokenKind.As)) {
26!
402
            asToken = this.consumeToken(TokenKind.As);
26✔
403
            typeToken = this.typeToken();
26✔
404
            type = util.tokenToBscType(typeToken);
26✔
405
        }
406

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

415
        return new InterfaceFieldStatement(name, asToken, typeToken, type, optionalKeyword);
26✔
416
    }
417

418
    /**
419
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration()`
420
     */
421
    private interfaceMethodStatement(optionalKeyword?: Token) {
422
        const functionType = this.advance();
11✔
423
        const name = this.identifier(...AllowedProperties);
11✔
424
        const leftParen = this.consume(DiagnosticMessages.expectedToken(TokenKind.LeftParen), TokenKind.LeftParen);
11✔
425

426
        let params = [] as FunctionParameterExpression[];
11✔
427
        if (!this.check(TokenKind.RightParen)) {
11✔
428
            do {
1✔
429
                if (params.length >= CallExpression.MaximumArguments) {
2!
430
                    this.diagnostics.push({
×
431
                        ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments),
432
                        range: this.peek().range
433
                    });
434
                }
435

436
                params.push(this.functionParameter());
2✔
437
            } while (this.match(TokenKind.Comma));
438
        }
439
        const rightParen = this.consumeToken(TokenKind.RightParen);
11✔
440
        let asToken = null as Token;
11✔
441
        let returnTypeToken = null as Token;
11✔
442
        if (this.check(TokenKind.As)) {
11!
443
            asToken = this.advance();
11✔
444
            returnTypeToken = this.typeToken();
11✔
445
            const returnType = util.tokenToBscType(returnTypeToken);
11✔
446
            if (!returnType) {
11!
447
                this.diagnostics.push({
×
448
                    ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, returnTypeToken.text),
449
                    range: returnTypeToken.range
450
                });
451
                throw this.lastDiagnosticAsError();
×
452
            }
453
        }
454

455
        return new InterfaceMethodStatement(
11✔
456
            functionType,
457
            name,
458
            leftParen,
459
            params,
460
            rightParen,
461
            asToken,
462
            returnTypeToken,
463
            util.tokenToBscType(returnTypeToken),
464
            optionalKeyword
465
        );
466
    }
467

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

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

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

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

482
        if (this.peek().text.toLowerCase() === 'extends') {
22!
483
            extendsToken = this.advance();
×
484
            parentInterfaceName = this.getNamespacedVariableNameExpression();
×
485
        }
486
        this.consumeStatementSeparators();
22✔
487
        //gather up all interface members (Fields, Methods)
488
        let body = [] as Statement[];
22✔
489
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
22✔
490
            try {
40✔
491
                let decl: Statement;
492

493
                //collect leading annotations
494
                if (this.check(TokenKind.At)) {
40✔
495
                    this.annotationExpression();
2✔
496
                }
497

498
                const optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
40✔
499
                //fields
500
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkAnyNext(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
40✔
501
                    decl = this.interfaceFieldStatement(optionalKeyword);
26✔
502
                    //field with name = 'optional'
503
                } else if (optionalKeyword && this.checkAny(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
14!
504
                    //rewind one place, so that 'optional' is the field name
505
                    this.current--;
×
506
                    decl = this.interfaceFieldStatement();
×
507

508
                    //methods (function/sub keyword followed by opening paren)
509
                } else if (this.checkAny(TokenKind.Function, TokenKind.Sub) && this.checkAnyNext(TokenKind.Identifier, ...AllowedProperties)) {
14✔
510
                    decl = this.interfaceMethodStatement(optionalKeyword);
11✔
511

512
                    //comments
513
                } else if (this.check(TokenKind.Comment)) {
3✔
514
                    decl = this.commentStatement();
1✔
515
                }
516
                if (decl) {
40✔
517
                    this.consumePendingAnnotations(decl);
38✔
518
                    body.push(decl);
38✔
519
                } else {
520
                    //we didn't find a declaration...flag tokens until next line
521
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
522
                }
523
            } catch (e) {
524
                //throw out any failed members and move on to the next line
525
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
526
            }
527

528
            //ensure statement separator
529
            this.consumeStatementSeparators();
40✔
530
            //break out of this loop if we encountered the `EndInterface` token not followed by `as`
531
            if (this.check(TokenKind.EndInterface) && !this.checkNext(TokenKind.As)) {
40✔
532
                break;
22✔
533
            }
534
        }
535

536
        //consume the final `end interface` token
537
        const endInterfaceToken = this.consumeToken(TokenKind.EndInterface);
22✔
538

539
        const statement = new InterfaceStatement(
22✔
540
            interfaceToken,
541
            nameToken,
542
            extendsToken,
543
            parentInterfaceName,
544
            body,
545
            endInterfaceToken
546
        );
547
        this._references.interfaceStatements.push(statement);
22✔
548
        this.exitAnnotationBlock(parentAnnotations);
22✔
549
        return statement;
22✔
550
    }
551

552
    private enumDeclaration(): EnumStatement {
553
        const result = new EnumStatement({} as any, []);
94✔
554
        this.warnIfNotBrighterScriptMode('enum declarations');
94✔
555

556
        const parentAnnotations = this.enterAnnotationBlock();
94✔
557

558
        result.tokens.enum = this.consume(
94✔
559
            DiagnosticMessages.expectedKeyword(TokenKind.Enum),
560
            TokenKind.Enum
561
        );
562

563
        result.tokens.name = this.tryIdentifier(...this.allowedLocalIdentifiers);
94✔
564

565
        this.consumeStatementSeparators();
94✔
566
        //gather up all members
567
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
94✔
568
            try {
179✔
569
                let decl: EnumMemberStatement | CommentStatement;
570

571
                //collect leading annotations
572
                if (this.check(TokenKind.At)) {
179!
573
                    this.annotationExpression();
×
574
                }
575

576
                //members
577
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
179✔
578
                    decl = this.enumMemberStatement();
176✔
579

580
                    //comments
581
                } else if (this.check(TokenKind.Comment)) {
3!
582
                    decl = this.commentStatement();
3✔
583
                }
584

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

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

605
        //consume the final `end interface` token
606
        result.tokens.endEnum = this.consumeToken(TokenKind.EndEnum);
94✔
607

608
        this._references.enumStatements.push(result);
94✔
609
        this.exitAnnotationBlock(parentAnnotations);
94✔
610
        return result;
94✔
611
    }
612

613
    /**
614
     * A BrighterScript class declaration
615
     */
616
    private classDeclaration(): ClassStatement {
617
        this.warnIfNotBrighterScriptMode('class declarations');
439✔
618

619
        const parentAnnotations = this.enterAnnotationBlock();
439✔
620

621
        let classKeyword = this.consume(
439✔
622
            DiagnosticMessages.expectedKeyword(TokenKind.Class),
623
            TokenKind.Class
624
        );
625
        let extendsKeyword: Token;
626
        let parentClassName: NamespacedVariableNameExpression;
627

628
        //get the class name
629
        let className = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('class'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
439✔
630

631
        //see if the class inherits from parent
632
        if (this.peek().text.toLowerCase() === 'extends') {
439✔
633
            extendsKeyword = this.advance();
70✔
634
            parentClassName = this.getNamespacedVariableNameExpression();
70✔
635
        }
636

637
        //ensure statement separator
638
        this.consumeStatementSeparators();
438✔
639

640
        //gather up all class members (Fields, Methods)
641
        let body = [] as Statement[];
438✔
642
        while (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private, TokenKind.Function, TokenKind.Sub, TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
438✔
643
            try {
426✔
644
                let decl: Statement;
645
                let accessModifier: Token;
646

647
                if (this.check(TokenKind.At)) {
426✔
648
                    this.annotationExpression();
15✔
649
                }
650

651
                if (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private)) {
425✔
652
                    //use actual access modifier
653
                    accessModifier = this.advance();
62✔
654
                }
655

656
                let overrideKeyword: Token;
657
                if (this.peek().text.toLowerCase() === 'override') {
425✔
658
                    overrideKeyword = this.advance();
17✔
659
                }
660

661
                //methods (function/sub keyword OR identifier followed by opening paren)
662
                if (this.checkAny(TokenKind.Function, TokenKind.Sub) || (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.LeftParen))) {
425✔
663
                    const funcDeclaration = this.functionDeclaration(false, false);
238✔
664

665
                    //remove this function from the lists because it's not a callable
666
                    const functionStatement = this._references.functionStatements.pop();
238✔
667

668
                    //if we have an overrides keyword AND this method is called 'new', that's not allowed
669
                    if (overrideKeyword && funcDeclaration.name.text.toLowerCase() === 'new') {
238!
670
                        this.diagnostics.push({
×
671
                            ...DiagnosticMessages.cannotUseOverrideKeywordOnConstructorFunction(),
672
                            range: overrideKeyword.range
673
                        });
674
                    }
675

676
                    decl = new MethodStatement(
238✔
677
                        accessModifier,
678
                        funcDeclaration.name,
679
                        funcDeclaration.func,
680
                        overrideKeyword
681
                    );
682

683
                    //refer to this statement as parent of the expression
684
                    functionStatement.func.functionStatement = decl as MethodStatement;
238✔
685

686
                    //fields
687
                } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
187✔
688

689
                    decl = this.fieldDeclaration(accessModifier);
165✔
690

691
                    //class fields cannot be overridden
692
                    if (overrideKeyword) {
164!
693
                        this.diagnostics.push({
×
694
                            ...DiagnosticMessages.classFieldCannotBeOverridden(),
695
                            range: overrideKeyword.range
696
                        });
697
                    }
698

699
                    //comments
700
                } else if (this.check(TokenKind.Comment)) {
22✔
701
                    decl = this.commentStatement();
8✔
702
                }
703

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

713
            //ensure statement separator
714
            this.consumeStatementSeparators();
426✔
715
        }
716

717
        let endingKeyword = this.advance();
438✔
718
        if (endingKeyword.kind !== TokenKind.EndClass) {
438✔
719
            this.diagnostics.push({
3✔
720
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('class'),
721
                range: endingKeyword.range
722
            });
723
        }
724

725
        const result = new ClassStatement(
438✔
726
            classKeyword,
727
            className,
728
            body,
729
            endingKeyword,
730
            extendsKeyword,
731
            parentClassName
732
        );
733

734
        this._references.classStatements.push(result);
438✔
735
        this.exitAnnotationBlock(parentAnnotations);
438✔
736
        return result;
438✔
737
    }
738

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

741
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
165✔
742

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

766
        let name = this.consume(
165✔
767
            DiagnosticMessages.expectedClassFieldIdentifier(),
768
            TokenKind.Identifier,
769
            ...AllowedProperties
770
        ) as Identifier;
771
        let asToken: Token;
772
        let fieldType: Token;
773
        //look for `as SOME_TYPE`
774
        if (this.check(TokenKind.As)) {
165✔
775
            asToken = this.advance();
112✔
776
            fieldType = this.typeToken();
112✔
777

778
            //no field type specified
779
            if (!util.tokenToBscType(fieldType)) {
112✔
780
                this.diagnostics.push({
1✔
781
                    ...DiagnosticMessages.expectedValidTypeToFollowAsKeyword(),
782
                    range: this.peek().range
783
                });
784
            }
785
        }
786

787
        let initialValue: Expression;
788
        let equal: Token;
789
        //if there is a field initializer
790
        if (this.check(TokenKind.Equal)) {
165✔
791
            equal = this.advance();
38✔
792
            initialValue = this.expression();
38✔
793
        }
794

795
        return new FieldStatement(
164✔
796
            accessModifier,
797
            name,
798
            asToken,
799
            fieldType,
800
            equal,
801
            initialValue,
802
            optionalKeyword
803
        );
804
    }
805

806
    /**
807
     * An array of CallExpression for the current function body
808
     */
809
    private callExpressions = [];
1,750✔
810

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

844
            if (isAnonymous) {
1,432✔
845
                leftParen = this.consume(
72✔
846
                    DiagnosticMessages.expectedLeftParenAfterCallable(functionTypeText),
847
                    TokenKind.LeftParen
848
                );
849
            } else {
850
                name = this.consume(
1,360✔
851
                    DiagnosticMessages.expectedNameAfterCallableKeyword(functionTypeText),
852
                    TokenKind.Identifier,
853
                    ...AllowedProperties
854
                ) as Identifier;
855
                leftParen = this.consume(
1,358✔
856
                    DiagnosticMessages.expectedLeftParenAfterCallableName(functionTypeText),
857
                    TokenKind.LeftParen
858
                );
859

860
                //prevent functions from ending with type designators
861
                let lastChar = name.text[name.text.length - 1];
1,357✔
862
                if (['$', '%', '!', '#', '&'].includes(lastChar)) {
1,357✔
863
                    //don't throw this error; let the parser continue
864
                    this.diagnostics.push({
8✔
865
                        ...DiagnosticMessages.functionNameCannotEndWithTypeDesignator(functionTypeText, name.text, lastChar),
866
                        range: name.range
867
                    });
868
                }
869

870
                //flag functions with keywords for names (only for standard functions)
871
                if (checkIdentifier && DisallowedFunctionIdentifiersText.has(name.text.toLowerCase())) {
1,357✔
872
                    this.diagnostics.push({
1✔
873
                        ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
874
                        range: name.range
875
                    });
876
                }
877
            }
878

879
            let params = [] as FunctionParameterExpression[];
1,429✔
880
            let asToken: Token;
881
            let typeToken: Token;
882
            if (!this.check(TokenKind.RightParen)) {
1,429✔
883
                do {
241✔
884
                    if (params.length >= CallExpression.MaximumArguments) {
385!
885
                        this.diagnostics.push({
×
886
                            ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments),
887
                            range: this.peek().range
888
                        });
889
                    }
890

891
                    params.push(this.functionParameter());
385✔
892
                } while (this.match(TokenKind.Comma));
893
            }
894
            let rightParen = this.advance();
1,429✔
895

896
            if (this.check(TokenKind.As)) {
1,429✔
897
                asToken = this.advance();
70✔
898

899
                typeToken = this.typeToken();
70✔
900

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

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

917
                return haveFoundOptional || !!param.defaultValue;
385✔
918
            }, false);
919

920
            this.consumeStatementSeparators(true);
1,429✔
921

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

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

939
            this._references.functionExpressions.push(func);
1,429✔
940

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

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

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

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

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

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

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

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

1001
        let typeToken: Token | undefined;
1002
        let defaultValue;
1003

1004
        // parse argument default value
1005
        if (this.match(TokenKind.Equal)) {
387✔
1006
            // it seems any expression is allowed here -- including ones that operate on other arguments!
1007
            defaultValue = this.expression();
48✔
1008
        }
1009

1010
        let asToken = null;
387✔
1011
        if (this.check(TokenKind.As)) {
387✔
1012
            asToken = this.advance();
249✔
1013

1014
            typeToken = this.typeToken();
249✔
1015

1016
            if (!util.tokenToBscType(typeToken, this.options.mode === ParseMode.BrighterScript)) {
249✔
1017
                this.diagnostics.push({
2✔
1018
                    ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, typeToken.text),
1019
                    range: typeToken.range
1020
                });
1021
            }
1022
        }
1023
        return new FunctionParameterExpression(
387✔
1024
            name,
1025
            typeToken,
1026
            defaultValue,
1027
            asToken
1028
        );
1029
    }
1030

1031
    private assignment(): AssignmentStatement {
1032
        let name = this.advance() as Identifier;
842✔
1033
        //add diagnostic if name is a reserved word that cannot be used as an identifier
1034
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
842✔
1035
            this.diagnostics.push({
12✔
1036
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
1037
                range: name.range
1038
            });
1039
        }
1040
        let operator = this.consume(
842✔
1041
            DiagnosticMessages.expectedOperatorAfterIdentifier(AssignmentOperators, name.text),
1042
            ...AssignmentOperators
1043
        );
1044
        let value = this.expression();
841✔
1045

1046
        let result: AssignmentStatement;
1047
        if (operator.kind === TokenKind.Equal) {
834✔
1048
            result = new AssignmentStatement(operator, name, value);
791✔
1049
        } else {
1050
            const nameExpression = new VariableExpression(name);
43✔
1051
            result = new AssignmentStatement(
43✔
1052
                operator,
1053
                name,
1054
                new BinaryExpression(nameExpression, operator, value)
1055
            );
1056
            this.addExpressionsToReferences(nameExpression);
43✔
1057
            if (isBinaryExpression(value)) {
43✔
1058
                //remove the right-hand-side expression from this assignment operator, and replace with the full assignment expression
1059
                this._references.expressions.delete(value);
3✔
1060
            }
1061
            this._references.expressions.add(result);
43✔
1062
        }
1063

1064
        this._references.assignmentStatements.push(result);
834✔
1065
        return result;
834✔
1066
    }
1067

1068
    private checkLibrary() {
1069
        let isLibraryToken = this.check(TokenKind.Library);
6,637✔
1070

1071
        //if we are at the top level, any line that starts with "library" should be considered a library statement
1072
        if (this.isAtRootLevel() && isLibraryToken) {
6,637✔
1073
            return true;
10✔
1074

1075
            //not at root level, library statements are all invalid here, but try to detect if the tokens look
1076
            //like a library statement (and let the libraryStatement function handle emitting the diagnostics)
1077
        } else if (isLibraryToken && this.checkNext(TokenKind.StringLiteral)) {
6,627✔
1078
            return true;
1✔
1079

1080
            //definitely not a library statement
1081
        } else {
1082
            return false;
6,626✔
1083
        }
1084
    }
1085

1086
    private statement(): Statement | undefined {
1087
        if (this.checkLibrary()) {
3,207!
1088
            return this.libraryStatement();
×
1089
        }
1090

1091
        if (this.check(TokenKind.Import)) {
3,207✔
1092
            return this.importStatement();
33✔
1093
        }
1094

1095
        if (this.check(TokenKind.Stop)) {
3,174✔
1096
            return this.stopStatement();
14✔
1097
        }
1098

1099
        if (this.check(TokenKind.If)) {
3,160✔
1100
            return this.ifStatement();
144✔
1101
        }
1102

1103
        //`try` must be followed by a block, otherwise it could be a local variable
1104
        if (this.check(TokenKind.Try) && this.checkAnyNext(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
3,016✔
1105
            return this.tryCatchStatement();
23✔
1106
        }
1107

1108
        if (this.check(TokenKind.Throw)) {
2,993✔
1109
            return this.throwStatement();
7✔
1110
        }
1111

1112
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
2,986✔
1113
            return this.printStatement();
531✔
1114
        }
1115
        if (this.check(TokenKind.Dim)) {
2,455✔
1116
            return this.dimStatement();
39✔
1117
        }
1118

1119
        if (this.check(TokenKind.While)) {
2,416✔
1120
            return this.whileStatement();
18✔
1121
        }
1122

1123
        if (this.check(TokenKind.ExitWhile)) {
2,398✔
1124
            return this.exitWhile();
5✔
1125
        }
1126

1127
        if (this.check(TokenKind.For)) {
2,393✔
1128
            return this.forStatement();
27✔
1129
        }
1130

1131
        if (this.check(TokenKind.ForEach)) {
2,366✔
1132
            return this.forEachStatement();
17✔
1133
        }
1134

1135
        if (this.check(TokenKind.ExitFor)) {
2,349✔
1136
            return this.exitFor();
2✔
1137
        }
1138

1139
        if (this.check(TokenKind.End)) {
2,347✔
1140
            return this.endStatement();
6✔
1141
        }
1142

1143
        if (this.match(TokenKind.Return)) {
2,341✔
1144
            return this.returnStatement();
145✔
1145
        }
1146

1147
        if (this.check(TokenKind.Goto)) {
2,196✔
1148
            return this.gotoStatement();
9✔
1149
        }
1150

1151
        //the continue keyword (followed by `for`, `while`, or a statement separator)
1152
        if (this.check(TokenKind.Continue) && this.checkAnyNext(TokenKind.While, TokenKind.For, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
2,187✔
1153
            return this.continueStatement();
10✔
1154
        }
1155

1156
        //does this line look like a label? (i.e.  `someIdentifier:` )
1157
        if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
2,177✔
1158
            try {
9✔
1159
                return this.labelStatement();
9✔
1160
            } catch (err) {
1161
                if (!(err instanceof CancelStatementError)) {
2!
1162
                    throw err;
×
1163
                }
1164
                //not a label, try something else
1165
            }
1166
        }
1167

1168
        // BrightScript is like python, in that variables can be declared without a `var`,
1169
        // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
1170
        // out what to do with it.
1171
        if (
2,170✔
1172
            this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers) &&
4,192✔
1173
            this.checkAnyNext(...AssignmentOperators)
1174
        ) {
1175
            return this.assignment();
815✔
1176
        }
1177

1178
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1179
        if (this.check(TokenKind.Interface)) {
1,355✔
1180
            return this.interfaceDeclaration();
22✔
1181
        }
1182

1183
        if (this.check(TokenKind.Class)) {
1,333✔
1184
            return this.classDeclaration();
439✔
1185
        }
1186

1187
        if (this.check(TokenKind.Namespace)) {
894✔
1188
            return this.namespaceStatement();
210✔
1189
        }
1190

1191
        if (this.check(TokenKind.Enum)) {
684✔
1192
            return this.enumDeclaration();
94✔
1193
        }
1194

1195
        // TODO: support multi-statements
1196
        return this.setStatement();
590✔
1197
    }
1198

1199
    private whileStatement(): WhileStatement {
1200
        const whileKeyword = this.advance();
18✔
1201
        const condition = this.expression();
18✔
1202

1203
        this.consumeStatementSeparators();
17✔
1204

1205
        const whileBlock = this.block(TokenKind.EndWhile);
17✔
1206
        let endWhile: Token;
1207
        if (!whileBlock || this.peek().kind !== TokenKind.EndWhile) {
17✔
1208
            this.diagnostics.push({
1✔
1209
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('while'),
1210
                range: this.peek().range
1211
            });
1212
            if (!whileBlock) {
1!
1213
                throw this.lastDiagnosticAsError();
×
1214
            }
1215
        } else {
1216
            endWhile = this.advance();
16✔
1217
        }
1218

1219
        return new WhileStatement(
17✔
1220
            { while: whileKeyword, endWhile: endWhile },
1221
            condition,
1222
            whileBlock
1223
        );
1224
    }
1225

1226
    private exitWhile(): ExitWhileStatement {
1227
        let keyword = this.advance();
5✔
1228

1229
        return new ExitWhileStatement({ exitWhile: keyword });
5✔
1230
    }
1231

1232
    private forStatement(): ForStatement {
1233
        const forToken = this.advance();
27✔
1234
        const initializer = this.assignment();
27✔
1235

1236
        //TODO: newline allowed?
1237

1238
        const toToken = this.advance();
26✔
1239
        const finalValue = this.expression();
26✔
1240
        let incrementExpression: Expression | undefined;
1241
        let stepToken: Token | undefined;
1242

1243
        if (this.check(TokenKind.Step)) {
26✔
1244
            stepToken = this.advance();
7✔
1245
            incrementExpression = this.expression();
7✔
1246
        } else {
1247
            // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
1248
        }
1249

1250
        this.consumeStatementSeparators();
26✔
1251

1252
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
26✔
1253
        let endForToken: Token;
1254
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
26✔
1255
            this.diagnostics.push({
1✔
1256
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1257
                range: this.peek().range
1258
            });
1259
            if (!body) {
1!
1260
                throw this.lastDiagnosticAsError();
×
1261
            }
1262
        } else {
1263
            endForToken = this.advance();
25✔
1264
        }
1265

1266
        // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
1267
        // stays around in scope with whatever value it was when the loop exited.
1268
        return new ForStatement(
26✔
1269
            forToken,
1270
            initializer,
1271
            toToken,
1272
            finalValue,
1273
            body,
1274
            endForToken,
1275
            stepToken,
1276
            incrementExpression
1277
        );
1278
    }
1279

1280
    private forEachStatement(): ForEachStatement {
1281
        let forEach = this.advance();
17✔
1282
        let name = this.advance();
17✔
1283

1284
        let maybeIn = this.peek();
17✔
1285
        if (this.check(TokenKind.Identifier) && maybeIn.text.toLowerCase() === 'in') {
17!
1286
            this.advance();
17✔
1287
        } else {
1288
            this.diagnostics.push({
×
1289
                ...DiagnosticMessages.expectedInAfterForEach(name.text),
1290
                range: this.peek().range
1291
            });
1292
            throw this.lastDiagnosticAsError();
×
1293
        }
1294

1295
        let target = this.expression();
17✔
1296
        if (!target) {
17!
1297
            this.diagnostics.push({
×
1298
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1299
                range: this.peek().range
1300
            });
1301
            throw this.lastDiagnosticAsError();
×
1302
        }
1303

1304
        this.consumeStatementSeparators();
17✔
1305

1306
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
17✔
1307
        if (!body) {
17!
1308
            this.diagnostics.push({
×
1309
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1310
                range: this.peek().range
1311
            });
1312
            throw this.lastDiagnosticAsError();
×
1313
        }
1314

1315
        let endFor = this.advance();
17✔
1316

1317
        return new ForEachStatement(
17✔
1318
            {
1319
                forEach: forEach,
1320
                in: maybeIn,
1321
                endFor: endFor
1322
            },
1323
            name,
1324
            target,
1325
            body
1326
        );
1327
    }
1328

1329
    private exitFor(): ExitForStatement {
1330
        let keyword = this.advance();
2✔
1331

1332
        return new ExitForStatement({ exitFor: keyword });
2✔
1333
    }
1334

1335
    private commentStatement() {
1336
        //if this comment is on the same line as the previous statement,
1337
        //then this comment should be treated as a single-line comment
1338
        let prev = this.previous();
211✔
1339
        if (prev?.range.end.line === this.peek().range.start.line) {
211✔
1340
            return new CommentStatement([this.advance()]);
121✔
1341
        } else {
1342
            let comments = [this.advance()];
90✔
1343
            while (this.check(TokenKind.Newline) && this.checkNext(TokenKind.Comment)) {
90✔
1344
                this.advance();
17✔
1345
                comments.push(this.advance());
17✔
1346
            }
1347
            return new CommentStatement(comments);
90✔
1348
        }
1349
    }
1350

1351
    private namespaceStatement(): NamespaceStatement | undefined {
1352
        this.warnIfNotBrighterScriptMode('namespace');
210✔
1353
        let keyword = this.advance();
210✔
1354

1355
        this.namespaceAndFunctionDepth++;
210✔
1356

1357
        let name = this.getNamespacedVariableNameExpression();
210✔
1358
        //set the current namespace name
1359
        let result = new NamespaceStatement(keyword, name, null, null);
209✔
1360

1361
        this.globalTerminators.push([TokenKind.EndNamespace]);
209✔
1362
        let body = this.body();
209✔
1363
        this.globalTerminators.pop();
209✔
1364

1365
        let endKeyword: Token;
1366
        if (this.check(TokenKind.EndNamespace)) {
209✔
1367
            endKeyword = this.advance();
208✔
1368
        } else {
1369
            //the `end namespace` keyword is missing. add a diagnostic, but keep parsing
1370
            this.diagnostics.push({
1✔
1371
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'),
1372
                range: keyword.range
1373
            });
1374
        }
1375

1376
        this.namespaceAndFunctionDepth--;
209✔
1377

1378
        result.body = body;
209✔
1379
        result.endKeyword = endKeyword;
209✔
1380
        this._references.namespaceStatements.push(result);
209✔
1381
        //cache the range property so that plugins can't affect it
1382
        result.cacheRange();
209✔
1383
        result.body.symbolTable.name += `: namespace '${result.name}'`;
209✔
1384
        return result;
209✔
1385
    }
1386

1387
    /**
1388
     * Get an expression with identifiers separated by periods. Useful for namespaces and class extends
1389
     */
1390
    private getNamespacedVariableNameExpression() {
1391
        let firstIdentifier = this.consume(
367✔
1392
            DiagnosticMessages.expectedIdentifierAfterKeyword(this.previous().text),
1393
            TokenKind.Identifier,
1394
            ...this.allowedLocalIdentifiers
1395
        ) as Identifier;
1396

1397
        let expr: DottedGetExpression | VariableExpression;
1398

1399
        if (firstIdentifier) {
364!
1400
            // force it into an identifier so the AST makes some sense
1401
            firstIdentifier.kind = TokenKind.Identifier;
364✔
1402
            const varExpr = new VariableExpression(firstIdentifier);
364✔
1403
            expr = varExpr;
364✔
1404

1405
            //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
1406
            while (this.check(TokenKind.Dot)) {
364✔
1407
                let dot = this.tryConsume(
132✔
1408
                    DiagnosticMessages.unexpectedToken(this.peek().text),
1409
                    TokenKind.Dot
1410
                );
1411
                if (!dot) {
132!
1412
                    break;
×
1413
                }
1414
                let identifier = this.tryConsume(
132✔
1415
                    DiagnosticMessages.expectedIdentifier(),
1416
                    TokenKind.Identifier,
1417
                    ...this.allowedLocalIdentifiers,
1418
                    ...AllowedProperties
1419
                ) as Identifier;
1420

1421
                if (!identifier) {
132✔
1422
                    break;
3✔
1423
                }
1424
                // force it into an identifier so the AST makes some sense
1425
                identifier.kind = TokenKind.Identifier;
129✔
1426
                expr = new DottedGetExpression(expr, identifier, dot);
129✔
1427
            }
1428
        }
1429
        return new NamespacedVariableNameExpression(expr);
364✔
1430
    }
1431

1432
    /**
1433
     * Add an 'unexpected token' diagnostic for any token found between current and the first stopToken found.
1434
     */
1435
    private flagUntil(...stopTokens: TokenKind[]) {
1436
        while (!this.checkAny(...stopTokens) && !this.isAtEnd()) {
4!
1437
            let token = this.advance();
×
1438
            this.diagnostics.push({
×
1439
                ...DiagnosticMessages.unexpectedToken(token.text),
1440
                range: token.range
1441
            });
1442
        }
1443
    }
1444

1445
    /**
1446
     * Consume tokens until one of the `stopTokenKinds` is encountered
1447
     * @param stopTokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
1448
     * @returns - the list of tokens consumed, EXCLUDING the `stopTokenKind` (you can use `this.peek()` to see which one it was)
1449
     */
1450
    private consumeUntil(...stopTokenKinds: TokenKind[]) {
1451
        let result = [] as Token[];
82✔
1452
        //take tokens until we encounter one of the stopTokenKinds
1453
        while (!stopTokenKinds.includes(this.peek().kind)) {
82✔
1454
            result.push(this.advance());
239✔
1455
        }
1456
        return result;
82✔
1457
    }
1458

1459
    private constDeclaration(): ConstStatement | undefined {
1460
        this.warnIfNotBrighterScriptMode('const declaration');
39✔
1461
        const constToken = this.advance();
39✔
1462
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
39✔
1463
        const equalToken = this.consumeToken(TokenKind.Equal);
39✔
1464
        const expression = this.expression();
39✔
1465
        const statement = new ConstStatement({
39✔
1466
            const: constToken,
1467
            name: nameToken,
1468
            equals: equalToken
1469
        }, expression);
1470
        this._references.constStatements.push(statement);
39✔
1471
        return statement;
39✔
1472
    }
1473

1474
    private libraryStatement(): LibraryStatement | undefined {
1475
        let libStatement = new LibraryStatement({
11✔
1476
            library: this.advance(),
1477
            //grab the next token only if it's a string
1478
            filePath: this.tryConsume(
1479
                DiagnosticMessages.expectedStringLiteralAfterKeyword('library'),
1480
                TokenKind.StringLiteral
1481
            )
1482
        });
1483

1484
        this._references.libraryStatements.push(libStatement);
11✔
1485
        return libStatement;
11✔
1486
    }
1487

1488
    private importStatement() {
1489
        this.warnIfNotBrighterScriptMode('import statements');
33✔
1490
        let importStatement = new ImportStatement(
33✔
1491
            this.advance(),
1492
            //grab the next token only if it's a string
1493
            this.tryConsume(
1494
                DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
1495
                TokenKind.StringLiteral
1496
            )
1497
        );
1498

1499
        this._references.importStatements.push(importStatement);
33✔
1500
        return importStatement;
33✔
1501
    }
1502

1503
    private annotationExpression() {
1504
        const atToken = this.advance();
45✔
1505
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
45✔
1506
        if (identifier) {
45✔
1507
            identifier.kind = TokenKind.Identifier;
44✔
1508
        }
1509
        let annotation = new AnnotationExpression(atToken, identifier);
45✔
1510
        this.pendingAnnotations.push(annotation);
44✔
1511

1512
        //optional arguments
1513
        if (this.check(TokenKind.LeftParen)) {
44✔
1514
            let leftParen = this.advance();
8✔
1515
            annotation.call = this.finishCall(leftParen, annotation, false);
8✔
1516
        }
1517
        return annotation;
44✔
1518
    }
1519

1520
    private ternaryExpression(test?: Expression): TernaryExpression {
1521
        this.warnIfNotBrighterScriptMode('ternary operator');
70✔
1522
        if (!test) {
70!
1523
            test = this.expression();
×
1524
        }
1525
        const questionMarkToken = this.advance();
70✔
1526

1527
        //consume newlines or comments
1528
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
70✔
1529
            this.advance();
8✔
1530
        }
1531

1532
        let consequent: Expression;
1533
        try {
70✔
1534
            consequent = this.expression();
70✔
1535
        } catch { }
1536

1537
        //consume newlines or comments
1538
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
70✔
1539
            this.advance();
6✔
1540
        }
1541

1542
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
70✔
1543

1544
        //consume newlines
1545
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
70✔
1546
            this.advance();
12✔
1547
        }
1548
        let alternate: Expression;
1549
        try {
70✔
1550
            alternate = this.expression();
70✔
1551
        } catch { }
1552

1553
        return new TernaryExpression(test, questionMarkToken, consequent, colonToken, alternate);
70✔
1554
    }
1555

1556
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1557
        this.warnIfNotBrighterScriptMode('null coalescing operator');
23✔
1558
        const questionQuestionToken = this.advance();
23✔
1559
        const alternate = this.expression();
23✔
1560
        return new NullCoalescingExpression(test, questionQuestionToken, alternate);
23✔
1561
    }
1562

1563
    private regexLiteralExpression() {
1564
        this.warnIfNotBrighterScriptMode('regular expression literal');
43✔
1565
        return new RegexLiteralExpression({
43✔
1566
            regexLiteral: this.advance()
1567
        });
1568
    }
1569

1570
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1571
        this.warnIfNotBrighterScriptMode('template string');
37✔
1572

1573
        //get the tag name
1574
        let tagName: Identifier;
1575
        if (isTagged) {
37✔
1576
            tagName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties) as Identifier;
4✔
1577
            // force it into an identifier so the AST makes some sense
1578
            tagName.kind = TokenKind.Identifier;
4✔
1579
        }
1580

1581
        let quasis = [] as TemplateStringQuasiExpression[];
37✔
1582
        let expressions = [];
37✔
1583
        let openingBacktick = this.peek();
37✔
1584
        this.advance();
37✔
1585
        let currentQuasiExpressionParts = [];
37✔
1586
        while (!this.isAtEnd() && !this.check(TokenKind.BackTick)) {
37✔
1587
            let next = this.peek();
138✔
1588
            if (next.kind === TokenKind.TemplateStringQuasi) {
138✔
1589
                //a quasi can actually be made up of multiple quasis when it includes char literals
1590
                currentQuasiExpressionParts.push(
87✔
1591
                    new LiteralExpression(next)
1592
                );
1593
                this.advance();
87✔
1594
            } else if (next.kind === TokenKind.EscapedCharCodeLiteral) {
51✔
1595
                currentQuasiExpressionParts.push(
23✔
1596
                    new EscapedCharCodeLiteralExpression(<any>next)
1597
                );
1598
                this.advance();
23✔
1599
            } else {
1600
                //finish up the current quasi
1601
                quasis.push(
28✔
1602
                    new TemplateStringQuasiExpression(currentQuasiExpressionParts)
1603
                );
1604
                currentQuasiExpressionParts = [];
28✔
1605

1606
                if (next.kind === TokenKind.TemplateStringExpressionBegin) {
28!
1607
                    this.advance();
28✔
1608
                }
1609
                //now keep this expression
1610
                expressions.push(this.expression());
28✔
1611
                if (!this.isAtEnd() && this.check(TokenKind.TemplateStringExpressionEnd)) {
28!
1612
                    //TODO is it an error if this is not present?
1613
                    this.advance();
28✔
1614
                } else {
1615
                    this.diagnostics.push({
×
1616
                        ...DiagnosticMessages.unterminatedTemplateExpression(),
1617
                        range: util.getRange(openingBacktick, this.peek())
1618
                    });
1619
                    throw this.lastDiagnosticAsError();
×
1620
                }
1621
            }
1622
        }
1623

1624
        //store the final set of quasis
1625
        quasis.push(
37✔
1626
            new TemplateStringQuasiExpression(currentQuasiExpressionParts)
1627
        );
1628

1629
        if (this.isAtEnd()) {
37✔
1630
            //error - missing backtick
1631
            this.diagnostics.push({
2✔
1632
                ...DiagnosticMessages.unterminatedTemplateStringAtEndOfFile(),
1633
                range: util.getRange(openingBacktick, this.peek())
1634
            });
1635
            throw this.lastDiagnosticAsError();
2✔
1636

1637
        } else {
1638
            let closingBacktick = this.advance();
35✔
1639
            if (isTagged) {
35✔
1640
                return new TaggedTemplateStringExpression(tagName, openingBacktick, quasis, expressions, closingBacktick);
4✔
1641
            } else {
1642
                return new TemplateStringExpression(openingBacktick, quasis, expressions, closingBacktick);
31✔
1643
            }
1644
        }
1645
    }
1646

1647
    private tryCatchStatement(): TryCatchStatement {
1648
        const tryToken = this.advance();
23✔
1649
        const statement = new TryCatchStatement(
23✔
1650
            { try: tryToken }
1651
        );
1652

1653
        //ensure statement separator
1654
        this.consumeStatementSeparators();
23✔
1655

1656
        statement.tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
23✔
1657

1658
        const peek = this.peek();
23✔
1659
        if (peek.kind !== TokenKind.Catch) {
23✔
1660
            this.diagnostics.push({
2✔
1661
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1662
                range: this.peek().range
1663
            });
1664
            //gracefully handle end-try
1665
            if (peek.kind === TokenKind.EndTry) {
2✔
1666
                statement.tokens.endTry = this.advance();
1✔
1667
            }
1668
            return statement;
2✔
1669
        }
1670
        const catchStmt = new CatchStatement({ catch: this.advance() });
21✔
1671
        statement.catchStatement = catchStmt;
21✔
1672

1673
        const exceptionVarToken = this.tryConsume(DiagnosticMessages.missingExceptionVarToFollowCatch(), TokenKind.Identifier, ...this.allowedLocalIdentifiers);
21✔
1674
        if (exceptionVarToken) {
21✔
1675
            // force it into an identifier so the AST makes some sense
1676
            exceptionVarToken.kind = TokenKind.Identifier;
19✔
1677
            catchStmt.exceptionVariable = exceptionVarToken as Identifier;
19✔
1678
        }
1679

1680
        //ensure statement sepatator
1681
        this.consumeStatementSeparators();
21✔
1682

1683
        catchStmt.catchBranch = this.block(TokenKind.EndTry);
21✔
1684

1685
        if (this.peek().kind !== TokenKind.EndTry) {
21✔
1686
            this.diagnostics.push({
1✔
1687
                ...DiagnosticMessages.expectedEndTryToTerminateTryCatch(),
1688
                range: this.peek().range
1689
            });
1690
        } else {
1691
            statement.tokens.endTry = this.advance();
20✔
1692
        }
1693
        return statement;
21✔
1694
    }
1695

1696
    private throwStatement() {
1697
        const throwToken = this.advance();
7✔
1698
        let expression: Expression;
1699
        if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
7✔
1700
            this.diagnostics.push({
1✔
1701
                ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(),
1702
                range: throwToken.range
1703
            });
1704
        } else {
1705
            expression = this.expression();
6✔
1706
        }
1707
        return new ThrowStatement(throwToken, expression);
5✔
1708
    }
1709

1710
    private dimStatement() {
1711
        const dim = this.advance();
39✔
1712

1713
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
39✔
1714
        // force to an identifier so the AST makes some sense
1715
        if (identifier) {
39✔
1716
            identifier.kind = TokenKind.Identifier;
37✔
1717
        }
1718

1719
        let leftSquareBracket = this.tryConsume(DiagnosticMessages.missingLeftSquareBracketAfterDimIdentifier(), TokenKind.LeftSquareBracket);
39✔
1720

1721
        let expressions: Expression[] = [];
39✔
1722
        let expression: Expression;
1723
        do {
39✔
1724
            try {
74✔
1725
                expression = this.expression();
74✔
1726
                expressions.push(expression);
69✔
1727
                if (this.check(TokenKind.Comma)) {
69✔
1728
                    this.advance();
35✔
1729
                } else {
1730
                    // will also exit for right square braces
1731
                    break;
34✔
1732
                }
1733
            } catch (error) {
1734
            }
1735
        } while (expression);
1736

1737
        if (expressions.length === 0) {
39✔
1738
            this.diagnostics.push({
5✔
1739
                ...DiagnosticMessages.missingExpressionsInDimStatement(),
1740
                range: this.peek().range
1741
            });
1742
        }
1743
        let rightSquareBracket = this.tryConsume(DiagnosticMessages.missingRightSquareBracketAfterDimIdentifier(), TokenKind.RightSquareBracket);
39✔
1744
        return new DimStatement(dim, identifier, leftSquareBracket, expressions, rightSquareBracket);
39✔
1745
    }
1746

1747
    private ifStatement(): IfStatement {
1748
        // colon before `if` is usually not allowed, unless it's after `then`
1749
        if (this.current > 0) {
186✔
1750
            const prev = this.previous();
181✔
1751
            if (prev.kind === TokenKind.Colon) {
181✔
1752
                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then) {
3✔
1753
                    this.diagnostics.push({
1✔
1754
                        ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
1755
                        range: prev.range
1756
                    });
1757
                }
1758
            }
1759
        }
1760

1761
        const ifToken = this.advance();
186✔
1762
        const startingRange = ifToken.range;
186✔
1763

1764
        const condition = this.expression();
186✔
1765
        let thenBranch: Block;
1766
        let elseBranch: IfStatement | Block | undefined;
1767

1768
        let thenToken: Token | undefined;
1769
        let endIfToken: Token | undefined;
1770
        let elseToken: Token | undefined;
1771

1772
        //optional `then`
1773
        if (this.check(TokenKind.Then)) {
184✔
1774
            thenToken = this.advance();
126✔
1775
        }
1776

1777
        //is it inline or multi-line if?
1778
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
184✔
1779

1780
        if (isInlineIfThen) {
184✔
1781
            /*** PARSE INLINE IF STATEMENT ***/
1782

1783
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
32✔
1784

1785
            if (!thenBranch) {
32!
1786
                this.diagnostics.push({
×
1787
                    ...DiagnosticMessages.expectedStatementToFollowConditionalCondition(ifToken.text),
1788
                    range: this.peek().range
1789
                });
1790
                throw this.lastDiagnosticAsError();
×
1791
            } else {
1792
                this.ensureInline(thenBranch.statements);
32✔
1793
            }
1794

1795
            //else branch
1796
            if (this.check(TokenKind.Else)) {
32✔
1797
                elseToken = this.advance();
19✔
1798

1799
                if (this.check(TokenKind.If)) {
19✔
1800
                    // recurse-read `else if`
1801
                    elseBranch = this.ifStatement();
4✔
1802

1803
                    //no multi-line if chained with an inline if
1804
                    if (!elseBranch.isInline) {
4✔
1805
                        this.diagnostics.push({
2✔
1806
                            ...DiagnosticMessages.expectedInlineIfStatement(),
1807
                            range: elseBranch.range
1808
                        });
1809
                    }
1810

1811
                } else if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
15!
1812
                    //expecting inline else branch
1813
                    this.diagnostics.push({
×
1814
                        ...DiagnosticMessages.expectedInlineIfStatement(),
1815
                        range: this.peek().range
1816
                    });
1817
                    throw this.lastDiagnosticAsError();
×
1818
                } else {
1819
                    elseBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
15✔
1820

1821
                    if (elseBranch) {
15!
1822
                        this.ensureInline(elseBranch.statements);
15✔
1823
                    }
1824
                }
1825

1826
                if (!elseBranch) {
19!
1827
                    //missing `else` branch
1828
                    this.diagnostics.push({
×
1829
                        ...DiagnosticMessages.expectedStatementToFollowElse(),
1830
                        range: this.peek().range
1831
                    });
1832
                    throw this.lastDiagnosticAsError();
×
1833
                }
1834
            }
1835

1836
            if (!elseBranch || !isIfStatement(elseBranch)) {
32✔
1837
                //enforce newline at the end of the inline if statement
1838
                const peek = this.peek();
28✔
1839
                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && !this.isAtEnd()) {
28✔
1840
                    //ignore last error if it was about a colon
1841
                    if (this.previous().kind === TokenKind.Colon) {
3!
1842
                        this.diagnostics.pop();
3✔
1843
                        this.current--;
3✔
1844
                    }
1845
                    //newline is required
1846
                    this.diagnostics.push({
3✔
1847
                        ...DiagnosticMessages.expectedFinalNewline(),
1848
                        range: this.peek().range
1849
                    });
1850
                }
1851
            }
1852

1853
        } else {
1854
            /*** PARSE MULTI-LINE IF STATEMENT ***/
1855

1856
            thenBranch = this.blockConditionalBranch(ifToken);
152✔
1857

1858
            //ensure newline/colon before next keyword
1859
            this.ensureNewLineOrColon();
150✔
1860

1861
            //else branch
1862
            if (this.check(TokenKind.Else)) {
150✔
1863
                elseToken = this.advance();
79✔
1864

1865
                if (this.check(TokenKind.If)) {
79✔
1866
                    // recurse-read `else if`
1867
                    elseBranch = this.ifStatement();
38✔
1868

1869
                } else {
1870
                    elseBranch = this.blockConditionalBranch(ifToken);
41✔
1871

1872
                    //ensure newline/colon before next keyword
1873
                    this.ensureNewLineOrColon();
41✔
1874
                }
1875
            }
1876

1877
            if (!isIfStatement(elseBranch)) {
150✔
1878
                if (this.check(TokenKind.EndIf)) {
112✔
1879
                    endIfToken = this.advance();
110✔
1880

1881
                } else {
1882
                    //missing endif
1883
                    this.diagnostics.push({
2✔
1884
                        ...DiagnosticMessages.expectedEndIfToCloseIfStatement(startingRange.start),
1885
                        range: ifToken.range
1886
                    });
1887
                }
1888
            }
1889
        }
1890

1891
        return new IfStatement(
182✔
1892
            {
1893
                if: ifToken,
1894
                then: thenToken,
1895
                endIf: endIfToken,
1896
                else: elseToken
1897
            },
1898
            condition,
1899
            thenBranch,
1900
            elseBranch,
1901
            isInlineIfThen
1902
        );
1903
    }
1904

1905
    //consume a `then` or `else` branch block of an `if` statement
1906
    private blockConditionalBranch(ifToken: Token) {
1907
        //keep track of the current error count, because if the then branch fails,
1908
        //we will trash them in favor of a single error on if
1909
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
193✔
1910

1911
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
1912
        // a trailing "end if" or "else if"
1913
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
193✔
1914

1915
        if (!branch) {
193✔
1916
            //throw out any new diagnostics created as a result of a `then` block parse failure.
1917
            //the block() function will discard the current line, so any discarded diagnostics will
1918
            //resurface if they are legitimate, and not a result of a malformed if statement
1919
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
2✔
1920

1921
            //this whole if statement is bogus...add error to the if token and hard-fail
1922
            this.diagnostics.push({
2✔
1923
                ...DiagnosticMessages.expectedEndIfElseIfOrElseToTerminateThenBlock(),
1924
                range: ifToken.range
1925
            });
1926
            throw this.lastDiagnosticAsError();
2✔
1927
        }
1928
        return branch;
191✔
1929
    }
1930

1931
    private ensureNewLineOrColon(silent = false) {
191✔
1932
        const prev = this.previous().kind;
394✔
1933
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
394✔
1934
            if (!silent) {
120✔
1935
                this.diagnostics.push({
6✔
1936
                    ...DiagnosticMessages.expectedNewlineOrColon(),
1937
                    range: this.peek().range
1938
                });
1939
            }
1940
            return false;
120✔
1941
        }
1942
        return true;
274✔
1943
    }
1944

1945
    //ensure each statement of an inline block is single-line
1946
    private ensureInline(statements: Statement[]) {
1947
        for (const stat of statements) {
47✔
1948
            if (isIfStatement(stat) && !stat.isInline) {
54✔
1949
                this.diagnostics.push({
2✔
1950
                    ...DiagnosticMessages.expectedInlineIfStatement(),
1951
                    range: stat.range
1952
                });
1953
            }
1954
        }
1955
    }
1956

1957
    //consume inline branch of an `if` statement
1958
    private inlineConditionalBranch(...additionalTerminators: BlockTerminator[]): Block | undefined {
1959
        let statements = [];
54✔
1960
        //attempt to get the next statement without using `this.declaration`
1961
        //which seems a bit hackish to get to work properly
1962
        let statement = this.statement();
54✔
1963
        if (!statement) {
54!
1964
            return undefined;
×
1965
        }
1966
        statements.push(statement);
54✔
1967
        const startingRange = statement.range;
54✔
1968

1969
        //look for colon statement separator
1970
        let foundColon = false;
54✔
1971
        while (this.match(TokenKind.Colon)) {
54✔
1972
            foundColon = true;
12✔
1973
        }
1974

1975
        //if a colon was found, add the next statement or err if unexpected
1976
        if (foundColon) {
54✔
1977
            if (!this.checkAny(TokenKind.Newline, ...additionalTerminators)) {
12✔
1978
                //if not an ending keyword, add next statement
1979
                let extra = this.inlineConditionalBranch(...additionalTerminators);
7✔
1980
                if (!extra) {
7!
1981
                    return undefined;
×
1982
                }
1983
                statements.push(...extra.statements);
7✔
1984
            } else {
1985
                //error: colon before next keyword
1986
                const colon = this.previous();
5✔
1987
                this.diagnostics.push({
5✔
1988
                    ...DiagnosticMessages.unexpectedToken(colon.text),
1989
                    range: colon.range
1990
                });
1991
            }
1992
        }
1993
        return new Block(statements, startingRange);
54✔
1994
    }
1995

1996
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
1997
        let expressionStart = this.peek();
304✔
1998

1999
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
304✔
2000
            let operator = this.advance();
17✔
2001

2002
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
17✔
2003
                this.diagnostics.push({
1✔
2004
                    ...DiagnosticMessages.consecutiveIncrementDecrementOperatorsAreNotAllowed(),
2005
                    range: this.peek().range
2006
                });
2007
                throw this.lastDiagnosticAsError();
1✔
2008
            } else if (isCallExpression(expr)) {
16✔
2009
                this.diagnostics.push({
1✔
2010
                    ...DiagnosticMessages.incrementDecrementOperatorsAreNotAllowedAsResultOfFunctionCall(),
2011
                    range: expressionStart.range
2012
                });
2013
                throw this.lastDiagnosticAsError();
1✔
2014
            }
2015

2016
            const result = new IncrementStatement(expr, operator);
15✔
2017
            this._references.expressions.add(result);
15✔
2018
            return result;
15✔
2019
        }
2020

2021
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
287✔
2022
            return new ExpressionStatement(expr);
224✔
2023
        }
2024

2025
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an assignment
2026
        this.diagnostics.push({
63✔
2027
            ...DiagnosticMessages.expectedStatementOrFunctionCallButReceivedExpression(),
2028
            range: expressionStart.range
2029
        });
2030
        throw this.lastDiagnosticAsError();
63✔
2031
    }
2032

2033
    private setStatement(): DottedSetStatement | IndexedSetStatement | ExpressionStatement | IncrementStatement | AssignmentStatement {
2034
        /**
2035
         * Attempts to find an expression-statement or an increment statement.
2036
         * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
2037
         * statements aren't valid expressions. They _do_ however fall under the same parsing
2038
         * priority as standalone function calls though, so we can parse them in the same way.
2039
         */
2040
        let expr = this.call();
590✔
2041
        if (this.checkAny(...AssignmentOperators) && !(isCallExpression(expr))) {
556✔
2042
            let left = expr;
255✔
2043
            let operator = this.advance();
255✔
2044
            let right = this.expression();
255✔
2045

2046
            // Create a dotted or indexed "set" based on the left-hand side's type
2047
            if (isIndexedGetExpression(left)) {
255✔
2048
                return new IndexedSetStatement(
24✔
2049
                    left.obj,
2050
                    left.index,
2051
                    operator.kind === TokenKind.Equal
2052
                        ? right
24✔
2053
                        : new BinaryExpression(left, operator, right),
2054
                    left.openingSquare,
2055
                    left.closingSquare
2056
                );
2057
            } else if (isDottedGetExpression(left)) {
231✔
2058
                return new DottedSetStatement(
228✔
2059
                    left.obj,
2060
                    left.name,
2061
                    operator.kind === TokenKind.Equal
2062
                        ? right
228✔
2063
                        : new BinaryExpression(left, operator, right),
2064
                    left.dot
2065
                );
2066
            }
2067
        }
2068
        return this.expressionStatement(expr);
304✔
2069
    }
2070

2071
    private printStatement(): PrintStatement {
2072
        let printKeyword = this.advance();
531✔
2073

2074
        let values: (
2075
            | Expression
2076
            | PrintSeparatorTab
2077
            | PrintSeparatorSpace)[] = [];
531✔
2078

2079
        while (!this.checkEndOfStatement()) {
531✔
2080
            if (this.check(TokenKind.Semicolon)) {
615✔
2081
                values.push(this.advance() as PrintSeparatorSpace);
20✔
2082
            } else if (this.check(TokenKind.Comma)) {
595✔
2083
                values.push(this.advance() as PrintSeparatorTab);
13✔
2084
            } else if (this.check(TokenKind.Else)) {
582✔
2085
                break; // inline branch
7✔
2086
            } else {
2087
                values.push(this.expression());
575✔
2088
            }
2089
        }
2090

2091
        //print statements can be empty, so look for empty print conditions
2092
        if (!values.length) {
530✔
2093
            let emptyStringLiteral = createStringLiteral('');
4✔
2094
            values.push(emptyStringLiteral);
4✔
2095
        }
2096

2097
        let last = values[values.length - 1];
530✔
2098
        if (isToken(last)) {
530✔
2099
            // TODO: error, expected value
2100
        }
2101

2102
        return new PrintStatement({ print: printKeyword }, values);
530✔
2103
    }
2104

2105
    /**
2106
     * Parses a return statement with an optional return value.
2107
     * @returns an AST representation of a return statement.
2108
     */
2109
    private returnStatement(): ReturnStatement {
2110
        let tokens = { return: this.previous() };
145✔
2111

2112
        if (this.checkEndOfStatement()) {
145✔
2113
            return new ReturnStatement(tokens);
8✔
2114
        }
2115

2116
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
137✔
2117
        return new ReturnStatement(tokens, toReturn);
136✔
2118
    }
2119

2120
    /**
2121
     * Parses a `label` statement
2122
     * @returns an AST representation of an `label` statement.
2123
     */
2124
    private labelStatement() {
2125
        let tokens = {
9✔
2126
            identifier: this.advance(),
2127
            colon: this.advance()
2128
        };
2129

2130
        //label must be alone on its line, this is probably not a label
2131
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
9✔
2132
            //rewind and cancel
2133
            this.current -= 2;
2✔
2134
            throw new CancelStatementError();
2✔
2135
        }
2136

2137
        return new LabelStatement(tokens);
7✔
2138
    }
2139

2140
    /**
2141
     * Parses a `continue` statement
2142
     */
2143
    private continueStatement() {
2144
        return new ContinueStatement({
10✔
2145
            continue: this.advance(),
2146
            loopType: this.tryConsume(
2147
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2148
                TokenKind.While, TokenKind.For
2149
            )
2150
        });
2151
    }
2152

2153
    /**
2154
     * Parses a `goto` statement
2155
     * @returns an AST representation of an `goto` statement.
2156
     */
2157
    private gotoStatement() {
2158
        let tokens = {
9✔
2159
            goto: this.advance(),
2160
            label: this.consume(
2161
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2162
                TokenKind.Identifier
2163
            )
2164
        };
2165

2166
        return new GotoStatement(tokens);
7✔
2167
    }
2168

2169
    /**
2170
     * Parses an `end` statement
2171
     * @returns an AST representation of an `end` statement.
2172
     */
2173
    private endStatement() {
2174
        let endTokens = { end: this.advance() };
6✔
2175

2176
        return new EndStatement(endTokens);
6✔
2177
    }
2178
    /**
2179
     * Parses a `stop` statement
2180
     * @returns an AST representation of a `stop` statement
2181
     */
2182
    private stopStatement() {
2183
        let tokens = { stop: this.advance() };
14✔
2184

2185
        return new StopStatement(tokens);
14✔
2186
    }
2187

2188
    /**
2189
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2190
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2191
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2192
     *                    ignored.
2193
     */
2194
    private block(...terminators: BlockTerminator[]): Block | undefined {
2195
        const parentAnnotations = this.enterAnnotationBlock();
1,726✔
2196

2197
        this.consumeStatementSeparators(true);
1,726✔
2198
        let startingToken = this.peek();
1,726✔
2199

2200
        const statements: Statement[] = [];
1,726✔
2201
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators)) {
1,726✔
2202
            //grab the location of the current token
2203
            let loopCurrent = this.current;
2,039✔
2204
            let dec = this.declaration();
2,039✔
2205
            if (dec) {
2,039✔
2206
                if (!isAnnotationExpression(dec)) {
1,957✔
2207
                    this.consumePendingAnnotations(dec);
1,953✔
2208
                    statements.push(dec);
1,953✔
2209
                }
2210

2211
                //ensure statement separator
2212
                this.consumeStatementSeparators();
1,957✔
2213

2214
            } else {
2215
                //something went wrong. reset to the top of the loop
2216
                this.current = loopCurrent;
82✔
2217

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

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

2225
                //consume potential separators
2226
                this.consumeStatementSeparators(true);
82✔
2227
            }
2228
        }
2229

2230
        if (this.isAtEnd()) {
1,726✔
2231
            return undefined;
5✔
2232
            // TODO: Figure out how to handle unterminated blocks well
2233
        } else if (terminators.length > 0) {
1,721✔
2234
            //did we hit end-sub / end-function while looking for some other terminator?
2235
            //if so, we need to restore the statement separator
2236
            let prev = this.previous().kind;
295✔
2237
            let peek = this.peek().kind;
295✔
2238
            if (
295✔
2239
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
594!
2240
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2241
            ) {
2242
                this.current--;
6✔
2243
            }
2244
        }
2245

2246
        this.exitAnnotationBlock(parentAnnotations);
1,721✔
2247
        return new Block(statements, startingToken.range);
1,721✔
2248
    }
2249

2250
    /**
2251
     * Attach pending annotations to the provided statement,
2252
     * and then reset the annotations array
2253
     */
2254
    consumePendingAnnotations(statement: Statement) {
2255
        if (this.pendingAnnotations.length) {
5,028✔
2256
            statement.annotations = this.pendingAnnotations;
29✔
2257
            this.pendingAnnotations = [];
29✔
2258
        }
2259
    }
2260

2261
    enterAnnotationBlock() {
2262
        const pending = this.pendingAnnotations;
4,225✔
2263
        this.pendingAnnotations = [];
4,225✔
2264
        return pending;
4,225✔
2265
    }
2266

2267
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2268
        // non consumed annotations are an error
2269
        if (this.pendingAnnotations.length) {
4,219✔
2270
            for (const annotation of this.pendingAnnotations) {
4✔
2271
                this.diagnostics.push({
6✔
2272
                    ...DiagnosticMessages.unusedAnnotation(),
2273
                    range: annotation.range
2274
                });
2275
            }
2276
        }
2277
        this.pendingAnnotations = parentAnnotations;
4,219✔
2278
    }
2279

2280
    private expression(): Expression {
2281
        const expression = this.anonymousFunction();
3,464✔
2282
        this._references.expressions.add(expression);
3,429✔
2283
        return expression;
3,429✔
2284
    }
2285

2286
    private anonymousFunction(): Expression {
2287
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
3,464✔
2288
            const func = this.functionDeclaration(true);
72✔
2289
            //if there's an open paren after this, this is an IIFE
2290
            if (this.check(TokenKind.LeftParen)) {
72✔
2291
                return this.finishCall(this.advance(), func);
3✔
2292
            } else {
2293
                return func;
69✔
2294
            }
2295
        }
2296

2297
        let expr = this.boolean();
3,392✔
2298

2299
        if (this.check(TokenKind.Question)) {
3,357✔
2300
            return this.ternaryExpression(expr);
70✔
2301
        } else if (this.check(TokenKind.QuestionQuestion)) {
3,287✔
2302
            return this.nullCoalescingExpression(expr);
23✔
2303
        } else {
2304
            return expr;
3,264✔
2305
        }
2306
    }
2307

2308
    private boolean(): Expression {
2309
        let expr = this.relational();
3,392✔
2310

2311
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
3,357✔
2312
            let operator = this.previous();
28✔
2313
            let right = this.relational();
28✔
2314
            this.addExpressionsToReferences(expr, right);
28✔
2315
            expr = new BinaryExpression(expr, operator, right);
28✔
2316
        }
2317

2318
        return expr;
3,357✔
2319
    }
2320

2321
    private relational(): Expression {
2322
        let expr = this.additive();
3,434✔
2323

2324
        while (
3,399✔
2325
            this.matchAny(
2326
                TokenKind.Equal,
2327
                TokenKind.LessGreater,
2328
                TokenKind.Greater,
2329
                TokenKind.GreaterEqual,
2330
                TokenKind.Less,
2331
                TokenKind.LessEqual
2332
            )
2333
        ) {
2334
            let operator = this.previous();
142✔
2335
            let right = this.additive();
142✔
2336
            this.addExpressionsToReferences(expr, right);
142✔
2337
            expr = new BinaryExpression(expr, operator, right);
142✔
2338
        }
2339

2340
        return expr;
3,399✔
2341
    }
2342

2343
    private addExpressionsToReferences(...expressions: Expression[]) {
2344
        for (const expression of expressions) {
316✔
2345
            if (!isBinaryExpression(expression)) {
589✔
2346
                this.references.expressions.add(expression);
547✔
2347
            }
2348
        }
2349
    }
2350

2351
    // TODO: bitshift
2352

2353
    private additive(): Expression {
2354
        let expr = this.multiplicative();
3,576✔
2355

2356
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
3,541✔
2357
            let operator = this.previous();
76✔
2358
            let right = this.multiplicative();
76✔
2359
            this.addExpressionsToReferences(expr, right);
76✔
2360
            expr = new BinaryExpression(expr, operator, right);
76✔
2361
        }
2362

2363
        return expr;
3,541✔
2364
    }
2365

2366
    private multiplicative(): Expression {
2367
        let expr = this.exponential();
3,652✔
2368

2369
        while (this.matchAny(
3,617✔
2370
            TokenKind.Forwardslash,
2371
            TokenKind.Backslash,
2372
            TokenKind.Star,
2373
            TokenKind.Mod,
2374
            TokenKind.LeftShift,
2375
            TokenKind.RightShift
2376
        )) {
2377
            let operator = this.previous();
21✔
2378
            let right = this.exponential();
21✔
2379
            this.addExpressionsToReferences(expr, right);
21✔
2380
            expr = new BinaryExpression(expr, operator, right);
21✔
2381
        }
2382

2383
        return expr;
3,617✔
2384
    }
2385

2386
    private exponential(): Expression {
2387
        let expr = this.prefixUnary();
3,673✔
2388

2389
        while (this.match(TokenKind.Caret)) {
3,638✔
2390
            let operator = this.previous();
6✔
2391
            let right = this.prefixUnary();
6✔
2392
            this.addExpressionsToReferences(expr, right);
6✔
2393
            expr = new BinaryExpression(expr, operator, right);
6✔
2394
        }
2395

2396
        return expr;
3,638✔
2397
    }
2398

2399
    private prefixUnary(): Expression {
2400
        const nextKind = this.peek().kind;
3,699✔
2401
        if (nextKind === TokenKind.Not) {
3,699✔
2402
            this.current++; //advance
14✔
2403
            let operator = this.previous();
14✔
2404
            let right = this.relational();
14✔
2405
            return new UnaryExpression(operator, right);
14✔
2406
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
3,685✔
2407
            this.current++; //advance
20✔
2408
            let operator = this.previous();
20✔
2409
            let right = this.prefixUnary();
20✔
2410
            return new UnaryExpression(operator, right);
20✔
2411
        }
2412
        return this.call();
3,665✔
2413
    }
2414

2415
    private indexedGet(expr: Expression) {
2416
        let openingSquare = this.previous();
109✔
2417
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
109✔
2418
        let index: Expression;
2419
        let closingSquare: Token;
2420
        while (this.match(TokenKind.Newline)) { }
109✔
2421
        try {
109✔
2422
            index = this.expression();
109✔
2423
        } catch (error) {
2424
            this.rethrowNonDiagnosticError(error);
1✔
2425
        }
2426

2427
        while (this.match(TokenKind.Newline)) { }
109✔
2428
        closingSquare = this.tryConsume(
109✔
2429
            DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
2430
            TokenKind.RightSquareBracket
2431
        );
2432

2433
        return new IndexedGetExpression(expr, index, openingSquare, closingSquare, questionDotToken);
109✔
2434
    }
2435

2436
    private newExpression() {
2437
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
40✔
2438
        let newToken = this.advance();
40✔
2439

2440
        let nameExpr = this.getNamespacedVariableNameExpression();
40✔
2441
        let leftParen = this.consume(
40✔
2442
            DiagnosticMessages.unexpectedToken(this.peek().text),
2443
            TokenKind.LeftParen,
2444
            TokenKind.QuestionLeftParen
2445
        );
2446
        let call = this.finishCall(leftParen, nameExpr);
36✔
2447
        //pop the call from the  callExpressions list because this is technically something else
2448
        this.callExpressions.pop();
36✔
2449
        let result = new NewExpression(newToken, call);
36✔
2450
        this._references.newExpressions.push(result);
36✔
2451
        return result;
36✔
2452
    }
2453

2454
    /**
2455
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2456
     */
2457
    private callfunc(callee: Expression): Expression {
2458
        this.warnIfNotBrighterScriptMode('callfunc operator');
18✔
2459
        let operator = this.previous();
18✔
2460
        let methodName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
18✔
2461
        // force it into an identifier so the AST makes some sense
2462
        methodName.kind = TokenKind.Identifier;
17✔
2463
        let openParen = this.consume(DiagnosticMessages.expectedOpenParenToFollowCallfuncIdentifier(), TokenKind.LeftParen);
17✔
2464
        let call = this.finishCall(openParen, callee, false);
17✔
2465

2466
        return new CallfuncExpression(callee, operator, methodName as Identifier, openParen, call.args, call.closingParen);
17✔
2467
    }
2468

2469
    private call(): Expression {
2470
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
4,255✔
2471
            return this.newExpression();
40✔
2472
        }
2473
        let expr = this.primary();
4,215✔
2474
        //an expression to keep for _references
2475
        let referenceCallExpression: Expression;
2476
        while (true) {
4,151✔
2477
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
5,609✔
2478
                expr = this.finishCall(this.previous(), expr);
480✔
2479
                //store this call expression in references
2480
                referenceCallExpression = expr;
480✔
2481

2482
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
5,129✔
2483
                expr = this.indexedGet(expr);
107✔
2484

2485
            } else if (this.match(TokenKind.Callfunc)) {
5,022✔
2486
                expr = this.callfunc(expr);
18✔
2487
                //store this callfunc expression in references
2488
                referenceCallExpression = expr;
17✔
2489

2490
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
5,004✔
2491
                if (this.match(TokenKind.LeftSquareBracket)) {
876✔
2492
                    expr = this.indexedGet(expr);
2✔
2493
                } else {
2494
                    let dot = this.previous();
874✔
2495
                    let name = this.tryConsume(
874✔
2496
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2497
                        TokenKind.Identifier,
2498
                        ...AllowedProperties
2499
                    );
2500
                    if (!name) {
874✔
2501
                        break;
22✔
2502
                    }
2503

2504
                    // force it into an identifier so the AST makes some sense
2505
                    name.kind = TokenKind.Identifier;
852✔
2506
                    expr = new DottedGetExpression(expr, name as Identifier, dot);
852✔
2507

2508
                    this.addPropertyHints(name);
852✔
2509
                }
2510

2511
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
4,128✔
2512
                let dot = this.advance();
8✔
2513
                let name = this.tryConsume(
8✔
2514
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2515
                    TokenKind.Identifier,
2516
                    ...AllowedProperties
2517
                );
2518

2519
                // force it into an identifier so the AST makes some sense
2520
                name.kind = TokenKind.Identifier;
8✔
2521
                if (!name) {
8!
2522
                    break;
×
2523
                }
2524
                expr = new XmlAttributeGetExpression(expr, name as Identifier, dot);
8✔
2525
                //only allow a single `@` expression
2526
                break;
8✔
2527

2528
            } else {
2529
                break;
4,120✔
2530
            }
2531
        }
2532
        //if we found a callExpression, add it to `expressions` in references
2533
        if (referenceCallExpression) {
4,150✔
2534
            this._references.expressions.add(referenceCallExpression);
460✔
2535
        }
2536
        return expr;
4,150✔
2537
    }
2538

2539
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
519✔
2540
        let args = [] as Expression[];
544✔
2541
        while (this.match(TokenKind.Newline)) { }
544✔
2542

2543
        if (!this.check(TokenKind.RightParen)) {
544✔
2544
            do {
300✔
2545
                while (this.match(TokenKind.Newline)) { }
459✔
2546

2547
                if (args.length >= CallExpression.MaximumArguments) {
459!
2548
                    this.diagnostics.push({
×
2549
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
2550
                        range: this.peek().range
2551
                    });
2552
                    throw this.lastDiagnosticAsError();
×
2553
                }
2554
                try {
459✔
2555
                    args.push(this.expression());
459✔
2556
                } catch (error) {
2557
                    this.rethrowNonDiagnosticError(error);
4✔
2558
                    // we were unable to get an expression, so don't continue
2559
                    break;
4✔
2560
                }
2561
            } while (this.match(TokenKind.Comma));
2562
        }
2563

2564
        while (this.match(TokenKind.Newline)) { }
544✔
2565

2566
        const closingParen = this.tryConsume(
544✔
2567
            DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
2568
            TokenKind.RightParen
2569
        );
2570

2571
        let expression = new CallExpression(callee, openingParen, closingParen, args);
544✔
2572
        if (addToCallExpressionList) {
544✔
2573
            this.callExpressions.push(expression);
519✔
2574
        }
2575
        return expression;
544✔
2576
    }
2577

2578
    /**
2579
     * Tries to get the next token as a type
2580
     * Allows for built-in types (double, string, etc.) or namespaced custom types in Brighterscript mode
2581
     * Will return a token of whatever is next to be parsed
2582
     */
2583
    private typeToken(): Token {
2584
        let typeToken: Token;
2585

2586
        if (this.checkAny(...DeclarableTypes)) {
468✔
2587
            // Token is a built in type
2588
            typeToken = this.advance();
399✔
2589
        } else if (this.options.mode === ParseMode.BrighterScript) {
69✔
2590
            try {
47✔
2591
                // see if we can get a namespaced identifer
2592
                const qualifiedType = this.getNamespacedVariableNameExpression();
47✔
2593
                typeToken = createToken(TokenKind.Identifier, qualifiedType.getName(this.options.mode), qualifiedType.range);
46✔
2594
            } catch {
2595
                //could not get an identifier - just get whatever's next
2596
                typeToken = this.advance();
1✔
2597
            }
2598
        } else {
2599
            // just get whatever's next
2600
            typeToken = this.advance();
22✔
2601
        }
2602
        return typeToken;
468✔
2603
    }
2604

2605
    private primary(): Expression {
2606
        switch (true) {
4,215✔
2607
            case this.matchAny(
4,215!
2608
                TokenKind.False,
2609
                TokenKind.True,
2610
                TokenKind.Invalid,
2611
                TokenKind.IntegerLiteral,
2612
                TokenKind.LongIntegerLiteral,
2613
                TokenKind.FloatLiteral,
2614
                TokenKind.DoubleLiteral,
2615
                TokenKind.StringLiteral
2616
            ):
2617
                return new LiteralExpression(this.previous());
2,475✔
2618

2619
            //capture source literals (LINE_NUM if brightscript, or a bunch of them if brighterscript)
2620
            case this.matchAny(TokenKind.LineNumLiteral, ...(this.options.mode === ParseMode.BrightScript ? [] : BrighterScriptSourceLiterals)):
1,740✔
2621
                return new SourceLiteralExpression(this.previous());
24✔
2622

2623
            //template string
2624
            case this.check(TokenKind.BackTick):
2625
                return this.templateString(false);
33✔
2626

2627
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
2628
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
2,950✔
2629
                return this.templateString(true);
4✔
2630

2631
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
2632
                return new VariableExpression(this.previous() as Identifier);
1,270✔
2633

2634
            case this.match(TokenKind.LeftParen):
2635
                let left = this.previous();
24✔
2636
                let expr = this.expression();
24✔
2637
                let right = this.consume(
23✔
2638
                    DiagnosticMessages.unmatchedLeftParenAfterExpression(),
2639
                    TokenKind.RightParen
2640
                );
2641
                return new GroupingExpression({ left: left, right: right }, expr);
23✔
2642

2643
            case this.matchAny(TokenKind.LeftSquareBracket):
2644
                return this.arrayLiteral();
97✔
2645

2646
            case this.match(TokenKind.LeftCurlyBrace):
2647
                return this.aaLiteral();
182✔
2648

2649
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
2650
                let token = Object.assign(this.previous(), {
×
2651
                    kind: TokenKind.Identifier
2652
                }) as Identifier;
2653
                return new VariableExpression(token);
×
2654

2655
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
2656
                return this.anonymousFunction();
×
2657

2658
            case this.check(TokenKind.RegexLiteral):
2659
                return this.regexLiteralExpression();
43✔
2660

2661
            case this.check(TokenKind.Comment):
2662
                return new CommentStatement([this.advance()]);
2✔
2663

2664
            default:
2665
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
2666
                if (this.checkAny(...this.peekGlobalTerminators())) {
61!
2667
                    //don't throw a diagnostic, just return undefined
2668

2669
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
2670
                } else {
2671
                    this.diagnostics.push({
61✔
2672
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
2673
                        range: this.peek().range
2674
                    });
2675
                    throw this.lastDiagnosticAsError();
61✔
2676
                }
2677
        }
2678
    }
2679

2680
    private arrayLiteral() {
2681
        let elements: Array<Expression | CommentStatement> = [];
97✔
2682
        let openingSquare = this.previous();
97✔
2683

2684
        //add any comment found right after the opening square
2685
        if (this.check(TokenKind.Comment)) {
97✔
2686
            elements.push(new CommentStatement([this.advance()]));
1✔
2687
        }
2688

2689
        while (this.match(TokenKind.Newline)) {
97✔
2690
        }
2691
        let closingSquare: Token;
2692

2693
        if (!this.match(TokenKind.RightSquareBracket)) {
97✔
2694
            try {
70✔
2695
                elements.push(this.expression());
70✔
2696

2697
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
69✔
2698
                    if (this.checkPrevious(TokenKind.Comment) || this.check(TokenKind.Comment)) {
96✔
2699
                        let comment = this.check(TokenKind.Comment) ? this.advance() : this.previous();
3!
2700
                        elements.push(new CommentStatement([comment]));
3✔
2701
                    }
2702
                    while (this.match(TokenKind.Newline)) {
96✔
2703

2704
                    }
2705

2706
                    if (this.check(TokenKind.RightSquareBracket)) {
96✔
2707
                        break;
19✔
2708
                    }
2709

2710
                    elements.push(this.expression());
77✔
2711
                }
2712
            } catch (error: any) {
2713
                this.rethrowNonDiagnosticError(error);
2✔
2714
            }
2715

2716
            closingSquare = this.tryConsume(
70✔
2717
                DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral(),
2718
                TokenKind.RightSquareBracket
2719
            );
2720
        } else {
2721
            closingSquare = this.previous();
27✔
2722
        }
2723

2724
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2725
        return new ArrayLiteralExpression(elements, openingSquare, closingSquare);
97✔
2726
    }
2727

2728
    private aaLiteral() {
2729
        let openingBrace = this.previous();
182✔
2730
        let members: Array<AAMemberExpression | CommentStatement> = [];
182✔
2731

2732
        let key = () => {
182✔
2733
            let result = {
184✔
2734
                colonToken: null as Token,
2735
                keyToken: null as Token,
2736
                range: null as Range
2737
            };
2738
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
184✔
2739
                result.keyToken = this.identifier(...AllowedProperties);
154✔
2740
            } else if (this.check(TokenKind.StringLiteral)) {
30!
2741
                result.keyToken = this.advance();
30✔
2742
            } else {
2743
                this.diagnostics.push({
×
2744
                    ...DiagnosticMessages.unexpectedAAKey(),
2745
                    range: this.peek().range
2746
                });
2747
                throw this.lastDiagnosticAsError();
×
2748
            }
2749

2750
            result.colonToken = this.consume(
184✔
2751
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
2752
                TokenKind.Colon
2753
            );
2754
            result.range = util.getRange(result.keyToken, result.colonToken);
183✔
2755
            return result;
183✔
2756
        };
2757

2758
        while (this.match(TokenKind.Newline)) { }
182✔
2759
        let closingBrace: Token;
2760
        if (!this.match(TokenKind.RightCurlyBrace)) {
182✔
2761
            let lastAAMember: AAMemberExpression;
2762
            try {
138✔
2763
                if (this.check(TokenKind.Comment)) {
138✔
2764
                    lastAAMember = null;
6✔
2765
                    members.push(new CommentStatement([this.advance()]));
6✔
2766
                } else {
2767
                    let k = key();
132✔
2768
                    let expr = this.expression();
132✔
2769
                    lastAAMember = new AAMemberExpression(
131✔
2770
                        k.keyToken,
2771
                        k.colonToken,
2772
                        expr
2773
                    );
2774
                    members.push(lastAAMember);
131✔
2775
                }
2776

2777
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
137✔
2778
                    // collect comma at end of expression
2779
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
176✔
2780
                        lastAAMember.commaToken = this.previous();
32✔
2781
                    }
2782

2783
                    //check for comment at the end of the current line
2784
                    if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
176✔
2785
                        let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
14✔
2786
                        members.push(new CommentStatement([token]));
14✔
2787
                    } else {
2788
                        this.consumeStatementSeparators(true);
162✔
2789

2790
                        //check for a comment on its own line
2791
                        if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
162✔
2792
                            let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
1!
2793
                            lastAAMember = null;
1✔
2794
                            members.push(new CommentStatement([token]));
1✔
2795
                            continue;
1✔
2796
                        }
2797

2798
                        if (this.check(TokenKind.RightCurlyBrace)) {
161✔
2799
                            break;
109✔
2800
                        }
2801
                        let k = key();
52✔
2802
                        let expr = this.expression();
51✔
2803
                        lastAAMember = new AAMemberExpression(
51✔
2804
                            k.keyToken,
2805
                            k.colonToken,
2806
                            expr
2807
                        );
2808
                        members.push(lastAAMember);
51✔
2809
                    }
2810
                }
2811
            } catch (error: any) {
2812
                this.rethrowNonDiagnosticError(error);
2✔
2813
            }
2814

2815
            closingBrace = this.tryConsume(
138✔
2816
                DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral(),
2817
                TokenKind.RightCurlyBrace
2818
            );
2819
        } else {
2820
            closingBrace = this.previous();
44✔
2821
        }
2822

2823
        const aaExpr = new AALiteralExpression(members, openingBrace, closingBrace);
182✔
2824
        this.addPropertyHints(aaExpr);
182✔
2825
        return aaExpr;
182✔
2826
    }
2827

2828
    /**
2829
     * Pop token if we encounter specified token
2830
     */
2831
    private match(tokenKind: TokenKind) {
2832
        if (this.check(tokenKind)) {
16,683✔
2833
            this.current++; //advance
1,007✔
2834
            return true;
1,007✔
2835
        }
2836
        return false;
15,676✔
2837
    }
2838

2839
    /**
2840
     * Pop token if we encounter a token in the specified list
2841
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
2842
     */
2843
    private matchAny(...tokenKinds: TokenKind[]) {
2844
        for (let tokenKind of tokenKinds) {
57,815✔
2845
            if (this.check(tokenKind)) {
169,774✔
2846
                this.current++; //advance
14,721✔
2847
                return true;
14,721✔
2848
            }
2849
        }
2850
        return false;
43,094✔
2851
    }
2852

2853
    /**
2854
     * If the next series of tokens matches the given set of tokens, pop them all
2855
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
2856
     */
2857
    private matchSequence(...tokenKinds: TokenKind[]) {
2858
        const endIndex = this.current + tokenKinds.length;
5,025✔
2859
        for (let i = 0; i < tokenKinds.length; i++) {
5,025✔
2860
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
5,049!
2861
                return false;
5,022✔
2862
            }
2863
        }
2864
        this.current = endIndex;
3✔
2865
        return true;
3✔
2866
    }
2867

2868
    /**
2869
     * Get next token matching a specified list, or fail with an error
2870
     */
2871
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
2872
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
5,645✔
2873
        if (token) {
5,645✔
2874
            return token;
5,630✔
2875
        } else {
2876
            let error = new Error(diagnosticInfo.message);
15✔
2877
            (error as any).isDiagnostic = true;
15✔
2878
            throw error;
15✔
2879
        }
2880
    }
2881

2882
    /**
2883
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
2884
     */
2885
    private consumeTokenIf(tokenKind: TokenKind) {
2886
        if (this.match(tokenKind)) {
205✔
2887
            return this.previous();
11✔
2888
        }
2889
    }
2890

2891
    private consumeToken(tokenKind: TokenKind) {
2892
        return this.consume(
192✔
2893
            DiagnosticMessages.expectedToken(tokenKind),
2894
            tokenKind
2895
        );
2896
    }
2897

2898
    /**
2899
     * Consume, or add a message if not found. But then continue and return undefined
2900
     */
2901
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
2902
        const nextKind = this.peek().kind;
8,492✔
2903
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
29,572✔
2904

2905
        if (foundTokenKind) {
8,492✔
2906
            return this.advance();
8,410✔
2907
        }
2908
        this.diagnostics.push({
82✔
2909
            ...diagnostic,
2910
            range: this.peek().range
2911
        });
2912
    }
2913

2914
    private tryConsumeToken(tokenKind: TokenKind) {
2915
        return this.tryConsume(
70✔
2916
            DiagnosticMessages.expectedToken(tokenKind),
2917
            tokenKind
2918
        );
2919
    }
2920

2921
    private consumeStatementSeparators(optional = false) {
3,260✔
2922
        //a comment or EOF mark the end of the statement
2923
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
11,074✔
2924
            return true;
511✔
2925
        }
2926
        let consumed = false;
10,563✔
2927
        //consume any newlines and colons
2928
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
10,563✔
2929
            consumed = true;
8,856✔
2930
        }
2931
        if (!optional && !consumed) {
10,563✔
2932
            this.diagnostics.push({
37✔
2933
                ...DiagnosticMessages.expectedNewlineOrColon(),
2934
                range: this.peek().range
2935
            });
2936
        }
2937
        return consumed;
10,563✔
2938
    }
2939

2940
    private advance(): Token {
2941
        if (!this.isAtEnd()) {
19,119✔
2942
            this.current++;
19,101✔
2943
        }
2944
        return this.previous();
19,119✔
2945
    }
2946

2947
    private checkEndOfStatement(): boolean {
2948
        const nextKind = this.peek().kind;
1,283✔
2949
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
1,283✔
2950
    }
2951

2952
    private checkPrevious(tokenKind: TokenKind): boolean {
2953
        return this.previous()?.kind === tokenKind;
621!
2954
    }
2955

2956
    private check(tokenKind: TokenKind): boolean {
2957
        const nextKind = this.peek().kind;
279,961✔
2958
        if (nextKind === TokenKind.Eof) {
279,961✔
2959
            return false;
6,681✔
2960
        }
2961
        return nextKind === tokenKind;
273,280✔
2962
    }
2963

2964
    private checkAny(...tokenKinds: TokenKind[]): boolean {
2965
        const nextKind = this.peek().kind;
35,336✔
2966
        if (nextKind === TokenKind.Eof) {
35,336✔
2967
            return false;
215✔
2968
        }
2969
        return tokenKinds.includes(nextKind);
35,121✔
2970
    }
2971

2972
    private checkNext(tokenKind: TokenKind): boolean {
2973
        if (this.isAtEnd()) {
2,919!
2974
            return false;
×
2975
        }
2976
        return this.peekNext().kind === tokenKind;
2,919✔
2977
    }
2978

2979
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
2980
        if (this.isAtEnd()) {
2,191!
2981
            return false;
×
2982
        }
2983
        const nextKind = this.peekNext().kind;
2,191✔
2984
        return tokenKinds.includes(nextKind);
2,191✔
2985
    }
2986

2987
    private isAtEnd(): boolean {
2988
        return this.peek().kind === TokenKind.Eof;
50,840✔
2989
    }
2990

2991
    private peekNext(): Token {
2992
        if (this.isAtEnd()) {
5,110!
2993
            return this.peek();
×
2994
        }
2995
        return this.tokens[this.current + 1];
5,110✔
2996
    }
2997

2998
    private peek(): Token {
2999
        return this.tokens[this.current];
384,248✔
3000
    }
3001

3002
    private previous(): Token {
3003
        return this.tokens[this.current - 1];
27,326✔
3004
    }
3005

3006
    /**
3007
     * Sometimes we catch an error that is a diagnostic.
3008
     * If that's the case, we want to continue parsing.
3009
     * Otherwise, re-throw the error
3010
     *
3011
     * @param error error caught in a try/catch
3012
     */
3013
    private rethrowNonDiagnosticError(error) {
3014
        if (!error.isDiagnostic) {
9!
3015
            throw error;
×
3016
        }
3017
    }
3018

3019
    /**
3020
     * Get the token that is {offset} indexes away from {this.current}
3021
     * @param offset the number of index steps away from current index to fetch
3022
     * @param tokenKinds the desired token must match one of these
3023
     * @example
3024
     * getToken(-1); //returns the previous token.
3025
     * getToken(0);  //returns current token.
3026
     * getToken(1);  //returns next token
3027
     */
3028
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3029
        const token = this.tokens[this.current + offset];
109✔
3030
        if (tokenKinds.includes(token.kind)) {
109✔
3031
            return token;
3✔
3032
        }
3033
    }
3034

3035
    private synchronize() {
3036
        this.advance(); // skip the erroneous token
123✔
3037

3038
        while (!this.isAtEnd()) {
123✔
3039
            if (this.ensureNewLineOrColon(true)) {
203✔
3040
                // end of statement reached
3041
                return;
89✔
3042
            }
3043

3044
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
114✔
3045
                case TokenKind.Namespace:
8!
3046
                case TokenKind.Class:
3047
                case TokenKind.Function:
3048
                case TokenKind.Sub:
3049
                case TokenKind.If:
3050
                case TokenKind.For:
3051
                case TokenKind.ForEach:
3052
                case TokenKind.While:
3053
                case TokenKind.Print:
3054
                case TokenKind.Return:
3055
                    // start parsing again from the next block starter or obvious
3056
                    // expression start
3057
                    return;
1✔
3058
            }
3059

3060
            this.advance();
113✔
3061
        }
3062
    }
3063

3064
    /**
3065
     * References are found during the initial parse.
3066
     * However, sometimes plugins can modify the AST, requiring a full walk to re-compute all references.
3067
     * This does that walk.
3068
     */
3069
    private findReferences() {
3070
        this._references = new References();
7✔
3071
        const excludedExpressions = new Set<Expression>();
7✔
3072

3073
        const visitCallExpression = (e: CallExpression | CallfuncExpression) => {
7✔
3074
            for (const p of e.args) {
14✔
3075
                this._references.expressions.add(p);
7✔
3076
            }
3077
            //add calls that were not excluded (from loop below)
3078
            if (!excludedExpressions.has(e)) {
14✔
3079
                this._references.expressions.add(e);
12✔
3080
            }
3081

3082
            //if this call is part of a longer expression that includes a call higher up, find that higher one and remove it
3083
            if (e.callee) {
14!
3084
                let node: Expression = e.callee;
14✔
3085
                while (node) {
14✔
3086
                    //the primary goal for this loop. If we found a parent call expression, remove it from `references`
3087
                    if (isCallExpression(node)) {
22✔
3088
                        this.references.expressions.delete(node);
2✔
3089
                        excludedExpressions.add(node);
2✔
3090
                        //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.
3091
                        break;
2✔
3092

3093
                        //when we hit a variable expression, we're definitely at the leftmost expression so stop
3094
                    } else if (isVariableExpression(node)) {
20✔
3095
                        break;
12✔
3096
                        //if
3097

3098
                    } else if (isDottedGetExpression(node) || isIndexedGetExpression(node)) {
8!
3099
                        node = node.obj;
8✔
3100
                    } else {
3101
                        //some expression we don't understand. log it and quit the loop
3102
                        this.logger.info('Encountered unknown expression while calculating function expression chain', node);
×
3103
                        break;
×
3104
                    }
3105
                }
3106
            }
3107
        };
3108

3109
        this.ast.walk(createVisitor({
7✔
3110
            AssignmentStatement: s => {
3111
                this._references.assignmentStatements.push(s);
11✔
3112
                this.references.expressions.add(s.value);
11✔
3113
            },
3114
            ClassStatement: s => {
3115
                this._references.classStatements.push(s);
1✔
3116
            },
3117
            ClassFieldStatement: s => {
3118
                if (s.initialValue) {
1!
3119
                    this._references.expressions.add(s.initialValue);
1✔
3120
                }
3121
            },
3122
            NamespaceStatement: s => {
3123
                this._references.namespaceStatements.push(s);
×
3124
            },
3125
            FunctionStatement: s => {
3126
                this._references.functionStatements.push(s);
4✔
3127
            },
3128
            ImportStatement: s => {
3129
                this._references.importStatements.push(s);
1✔
3130
            },
3131
            LibraryStatement: s => {
3132
                this._references.libraryStatements.push(s);
×
3133
            },
3134
            FunctionExpression: (expression, parent) => {
3135
                if (!isMethodStatement(parent)) {
4!
3136
                    this._references.functionExpressions.push(expression);
4✔
3137
                }
3138
            },
3139
            NewExpression: e => {
3140
                this._references.newExpressions.push(e);
×
3141
                for (const p of e.call.args) {
×
3142
                    this._references.expressions.add(p);
×
3143
                }
3144
            },
3145
            ExpressionStatement: s => {
3146
                this._references.expressions.add(s.expression);
7✔
3147
            },
3148
            CallfuncExpression: e => {
3149
                visitCallExpression(e);
1✔
3150
            },
3151
            CallExpression: e => {
3152
                visitCallExpression(e);
13✔
3153
            },
3154
            AALiteralExpression: e => {
3155
                this.addPropertyHints(e);
8✔
3156
                this._references.expressions.add(e);
8✔
3157
                for (const member of e.elements) {
8✔
3158
                    if (isAAMemberExpression(member)) {
16!
3159
                        this._references.expressions.add(member.value);
16✔
3160
                    }
3161
                }
3162
            },
3163
            BinaryExpression: (e, parent) => {
3164
                //walk the chain of binary expressions and add each one to the list of expressions
3165
                const expressions: Expression[] = [e];
14✔
3166
                let expression: Expression;
3167
                while ((expression = expressions.pop())) {
14✔
3168
                    if (isBinaryExpression(expression)) {
64✔
3169
                        expressions.push(expression.left, expression.right);
25✔
3170
                    } else {
3171
                        this._references.expressions.add(expression);
39✔
3172
                    }
3173
                }
3174
            },
3175
            ArrayLiteralExpression: e => {
3176
                for (const element of e.elements) {
1✔
3177
                    //keep everything except comments
3178
                    if (!isCommentStatement(element)) {
1!
3179
                        this._references.expressions.add(element);
1✔
3180
                    }
3181
                }
3182
            },
3183
            DottedGetExpression: e => {
3184
                this.addPropertyHints(e.name);
23✔
3185
            },
3186
            DottedSetStatement: e => {
3187
                this.addPropertyHints(e.name);
4✔
3188
            },
3189
            EnumStatement: e => {
3190
                this._references.enumStatements.push(e);
×
3191
            },
3192
            ConstStatement: s => {
3193
                this._references.constStatements.push(s);
×
3194
            },
3195
            UnaryExpression: e => {
3196
                this._references.expressions.add(e);
×
3197
            },
3198
            IncrementStatement: e => {
3199
                this._references.expressions.add(e);
2✔
3200
            }
3201
        }), {
3202
            walkMode: WalkMode.visitAllRecursive
3203
        });
3204
    }
3205

3206
    public dispose() {
3207
    }
3208
}
3209

3210
export enum ParseMode {
1✔
3211
    BrightScript = 'BrightScript',
1✔
3212
    BrighterScript = 'BrighterScript'
1✔
3213
}
3214

3215
export interface ParseOptions {
3216
    /**
3217
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3218
     */
3219
    mode: ParseMode;
3220
    /**
3221
     * A logger that should be used for logging. If omitted, a default logger is used
3222
     */
3223
    logger?: Logger;
3224
}
3225

3226
export class References {
1✔
3227
    private cache = new Cache();
1,757✔
3228
    public assignmentStatements = [] as AssignmentStatement[];
1,757✔
3229
    public classStatements = [] as ClassStatement[];
1,757✔
3230

3231
    public get classStatementLookup() {
3232
        if (!this._classStatementLookup) {
17✔
3233
            this._classStatementLookup = new Map();
15✔
3234
            for (const stmt of this.classStatements) {
15✔
3235
                this._classStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
2✔
3236
            }
3237
        }
3238
        return this._classStatementLookup;
17✔
3239
    }
3240
    private _classStatementLookup: Map<string, ClassStatement>;
3241

3242
    public functionExpressions = [] as FunctionExpression[];
1,757✔
3243
    public functionStatements = [] as FunctionStatement[];
1,757✔
3244
    /**
3245
     * A map of function statements, indexed by fully-namespaced lower function name.
3246
     */
3247
    public get functionStatementLookup() {
3248
        if (!this._functionStatementLookup) {
17✔
3249
            this._functionStatementLookup = new Map();
15✔
3250
            for (const stmt of this.functionStatements) {
15✔
3251
                this._functionStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
13✔
3252
            }
3253
        }
3254
        return this._functionStatementLookup;
17✔
3255
    }
3256
    private _functionStatementLookup: Map<string, FunctionStatement>;
3257

3258
    public interfaceStatements = [] as InterfaceStatement[];
1,757✔
3259

3260
    public get interfaceStatementLookup() {
3261
        if (!this._interfaceStatementLookup) {
×
3262
            this._interfaceStatementLookup = new Map();
×
3263
            for (const stmt of this.interfaceStatements) {
×
3264
                this._interfaceStatementLookup.set(stmt.fullName.toLowerCase(), stmt);
×
3265
            }
3266
        }
3267
        return this._interfaceStatementLookup;
×
3268
    }
3269
    private _interfaceStatementLookup: Map<string, InterfaceStatement>;
3270

3271
    public enumStatements = [] as EnumStatement[];
1,757✔
3272

3273
    public get enumStatementLookup() {
3274
        return this.cache.getOrAdd('enums', () => {
18✔
3275
            const result = new Map<string, EnumStatement>();
16✔
3276
            for (const stmt of this.enumStatements) {
16✔
3277
                result.set(stmt.fullName.toLowerCase(), stmt);
1✔
3278
            }
3279
            return result;
16✔
3280
        });
3281
    }
3282

3283
    public constStatements = [] as ConstStatement[];
1,757✔
3284

3285
    public get constStatementLookup() {
3286
        return this.cache.getOrAdd('consts', () => {
×
3287
            const result = new Map<string, ConstStatement>();
×
3288
            for (const stmt of this.constStatements) {
×
3289
                result.set(stmt.fullName.toLowerCase(), stmt);
×
3290
            }
3291
            return result;
×
3292
        });
3293
    }
3294

3295
    /**
3296
     * A collection of full expressions. This excludes intermediary expressions.
3297
     *
3298
     * Example 1:
3299
     * `a.b.c` is composed of `a` (variableExpression)  `.b` (DottedGetExpression) `.c` (DottedGetExpression)
3300
     * This will only contain the final `.c` DottedGetExpression because `.b` and `a` can both be derived by walking back from the `.c` DottedGetExpression.
3301
     *
3302
     * Example 2:
3303
     * `name.space.doSomething(a.b.c)` will result in 2 entries in this list. the `CallExpression` for `doSomething`, and the `.c` DottedGetExpression.
3304
     *
3305
     * Example 3:
3306
     * `value = SomeEnum.value > 2 or SomeEnum.otherValue < 10` will result in 4 entries. `SomeEnum.value`, `2`, `SomeEnum.otherValue`, `10`
3307
     */
3308
    public expressions = new Set<Expression>();
1,757✔
3309

3310
    public importStatements = [] as ImportStatement[];
1,757✔
3311
    public libraryStatements = [] as LibraryStatement[];
1,757✔
3312
    public namespaceStatements = [] as NamespaceStatement[];
1,757✔
3313
    public newExpressions = [] as NewExpression[];
1,757✔
3314
    public propertyHints = {} as Record<string, string>;
1,757✔
3315
}
3316

3317
class CancelStatementError extends Error {
3318
    constructor() {
3319
        super('CancelStatement');
2✔
3320
    }
3321
}
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