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

rokucommunity / brighterscript / #12837

24 Jul 2024 05:52PM UTC coverage: 87.936% (+2.3%) from 85.65%
#12837

push

TwitchBronBron
0.67.4

6069 of 7376 branches covered (82.28%)

Branch coverage included in aggregate %.

8793 of 9525 relevant lines covered (92.31%)

1741.63 hits per line

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

92.77
/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
    TypeCastExpression,
87
    UnaryExpression,
88
    VariableExpression,
89
    XmlAttributeGetExpression
90
} from './Expression';
91
import type { Diagnostic, Range } from 'vscode-languageserver';
92
import type { Logger } from '../logging';
93
import { createLogger } from '../logging';
1✔
94
import { isAAMemberExpression, isAnnotationExpression, isBinaryExpression, isCallExpression, isCallfuncExpression, isMethodStatement, isCommentStatement, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression } from '../astUtils/reflection';
1✔
95
import { createVisitor, WalkMode } from '../astUtils/visitors';
1✔
96
import { createStringLiteral, createToken } from '../astUtils/creators';
1✔
97
import { Cache } from '../Cache';
1✔
98
import type { Expression, Statement } from './AstNode';
99
import { SymbolTable } from '../SymbolTable';
1✔
100
import type { BscType } from '../types/BscType';
101

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

108
    /**
109
     * The current token index
110
     */
111
    public current: number;
112

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

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

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

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

143
    private _references = new References();
1,831✔
144

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

152
    private addPropertyHints(item: Token | AALiteralExpression) {
153
        if (isToken(item)) {
1,115✔
154
            const name = item.text;
923✔
155
            this._references.propertyHints[name.toLowerCase()] = name;
923✔
156
        } else {
157
            for (const member of item.elements) {
192✔
158
                if (!isCommentStatement(member)) {
222✔
159
                    const name = member.keyToken.text;
200✔
160
                    if (!name.startsWith('"')) {
200✔
161
                        this._references.propertyHints[name.toLowerCase()] = name;
168✔
162
                    }
163
                }
164
            }
165
        }
166
    }
167

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

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

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

183
    private globalTerminators = [] as TokenKind[][];
1,831✔
184

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

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

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

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

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

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

237
        this.ast = this.body();
1,816✔
238

239
        //now that we've built the AST, link every node to its parent
240
        this.ast.link();
1,816✔
241
        return this;
1,816✔
242
    }
243

244
    private logger: Logger;
245

246
    private body() {
247
        const parentAnnotations = this.enterAnnotationBlock();
2,046✔
248

249
        let body = new Body([]);
2,046✔
250
        if (this.tokens.length > 0) {
2,046✔
251
            this.consumeStatementSeparators(true);
2,045✔
252

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

278
        this.exitAnnotationBlock(parentAnnotations);
2,046✔
279
        return body;
2,046✔
280
    }
281

282
    private sanitizeParseOptions(options: ParseOptions) {
283
        options ??= {};
1,816✔
284
        options.mode ??= ParseMode.BrightScript;
1,816✔
285
        options.trackLocations ??= true;
1,816✔
286
        return options;
1,816✔
287
    }
288

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

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

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

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

324
            if (this.checkLibrary()) {
3,622✔
325
                return this.libraryStatement();
12✔
326
            }
327

328
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
3,610✔
329
                return this.constDeclaration();
50✔
330
            }
331

332
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
3,560✔
333
                return this.annotationExpression();
29✔
334
            }
335

336
            if (this.check(TokenKind.Comment)) {
3,531✔
337
                return this.commentStatement();
203✔
338
            }
339

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

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

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

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

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

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

411
        if (!type) {
49!
412
            this.diagnostics.push({
×
413
                ...DiagnosticMessages.functionParameterTypeIsInvalid(name.text, typeToken.text),
414
                range: typeToken.range
415
            });
416
            throw this.lastDiagnosticAsError();
×
417
        }
418

419
        return new InterfaceFieldStatement(name, asToken, typeToken, type, optionalKeyword);
49✔
420
    }
421

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

430
        let params = [] as FunctionParameterExpression[];
17✔
431
        if (!this.check(TokenKind.RightParen)) {
17✔
432
            do {
2✔
433
                if (params.length >= CallExpression.MaximumArguments) {
3!
434
                    this.diagnostics.push({
×
435
                        ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments),
436
                        range: this.peek().range
437
                    });
438
                }
439

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

459
        return new InterfaceMethodStatement(
17✔
460
            functionType,
461
            name,
462
            leftParen,
463
            params,
464
            rightParen,
465
            asToken,
466
            returnTypeToken,
467
            util.tokenToBscType(returnTypeToken),
468
            optionalKeyword
469
        );
470
    }
471

472
    private interfaceDeclaration(): InterfaceStatement {
473
        this.warnIfNotBrighterScriptMode('interface declarations');
43✔
474

475
        const parentAnnotations = this.enterAnnotationBlock();
43✔
476

477
        const interfaceToken = this.consume(
43✔
478
            DiagnosticMessages.expectedKeyword(TokenKind.Interface),
479
            TokenKind.Interface
480
        );
481
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
43✔
482

483
        let extendsToken: Token;
484
        let parentInterfaceName: NamespacedVariableNameExpression;
485

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

500
                let decl: Statement;
501

502
                //collect leading annotations
503
                if (this.check(TokenKind.At)) {
69✔
504
                    this.annotationExpression();
2✔
505
                }
506

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

517
                    //methods (function/sub keyword followed by opening paren)
518
                } else if (this.checkAny(TokenKind.Function, TokenKind.Sub) && this.checkAnyNext(TokenKind.Identifier, ...AllowedProperties)) {
20✔
519
                    decl = this.interfaceMethodStatement(optionalKeyword);
17✔
520

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

537
            //ensure statement separator
538
            this.consumeStatementSeparators();
69✔
539
        }
540

541
        //consume the final `end interface` token
542
        const endInterfaceToken = this.consumeToken(TokenKind.EndInterface);
43✔
543

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

557
    private enumDeclaration(): EnumStatement {
558
        const result = new EnumStatement({} as any, []);
106✔
559
        this.warnIfNotBrighterScriptMode('enum declarations');
106✔
560

561
        const parentAnnotations = this.enterAnnotationBlock();
106✔
562

563
        result.tokens.enum = this.consume(
106✔
564
            DiagnosticMessages.expectedKeyword(TokenKind.Enum),
565
            TokenKind.Enum
566
        );
567

568
        result.tokens.name = this.tryIdentifier(...this.allowedLocalIdentifiers);
106✔
569

570
        this.consumeStatementSeparators();
106✔
571
        //gather up all members
572
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
106✔
573
            try {
195✔
574
                let decl: EnumMemberStatement | CommentStatement;
575

576
                //collect leading annotations
577
                if (this.check(TokenKind.At)) {
195!
578
                    this.annotationExpression();
×
579
                }
580

581
                //members
582
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
195✔
583
                    decl = this.enumMemberStatement();
192✔
584

585
                    //comments
586
                } else if (this.check(TokenKind.Comment)) {
3!
587
                    decl = this.commentStatement();
3✔
588
                }
589

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

602
            //ensure statement separator
603
            this.consumeStatementSeparators();
195✔
604
            //break out of this loop if we encountered the `EndEnum` token
605
            if (this.check(TokenKind.EndEnum)) {
195✔
606
                break;
100✔
607
            }
608
        }
609

610
        //consume the final `end interface` token
611
        result.tokens.endEnum = this.consumeToken(TokenKind.EndEnum);
106✔
612

613
        this._references.enumStatements.push(result);
106✔
614
        this.exitAnnotationBlock(parentAnnotations);
106✔
615
        return result;
106✔
616
    }
617

618
    /**
619
     * A BrighterScript class declaration
620
     */
621
    private classDeclaration(): ClassStatement {
622
        this.warnIfNotBrighterScriptMode('class declarations');
453✔
623

624
        const parentAnnotations = this.enterAnnotationBlock();
453✔
625

626
        let classKeyword = this.consume(
453✔
627
            DiagnosticMessages.expectedKeyword(TokenKind.Class),
628
            TokenKind.Class
629
        );
630
        let extendsKeyword: Token;
631
        let parentClassName: NamespacedVariableNameExpression;
632

633
        //get the class name
634
        let className = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('class'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
453✔
635

636
        //see if the class inherits from parent
637
        if (this.peek().text.toLowerCase() === 'extends') {
453✔
638
            extendsKeyword = this.advance();
71✔
639
            parentClassName = this.getNamespacedVariableNameExpression();
71✔
640
        }
641

642
        //ensure statement separator
643
        this.consumeStatementSeparators();
452✔
644

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

652
                if (this.check(TokenKind.At)) {
443✔
653
                    this.annotationExpression();
15✔
654
                }
655

656
                if (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private)) {
442✔
657
                    //use actual access modifier
658
                    accessModifier = this.advance();
62✔
659
                }
660

661
                let overrideKeyword: Token;
662
                if (this.peek().text.toLowerCase() === 'override') {
442✔
663
                    overrideKeyword = this.advance();
17✔
664
                }
665

666
                //methods (function/sub keyword OR identifier followed by opening paren)
667
                if (this.checkAny(TokenKind.Function, TokenKind.Sub) || (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.LeftParen))) {
442✔
668
                    const funcDeclaration = this.functionDeclaration(false, false);
246✔
669

670
                    //remove this function from the lists because it's not a callable
671
                    const functionStatement = this._references.functionStatements.pop();
246✔
672

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

681
                    decl = new MethodStatement(
246✔
682
                        accessModifier,
683
                        funcDeclaration.name,
684
                        funcDeclaration.func,
685
                        overrideKeyword
686
                    );
687

688
                    //refer to this statement as parent of the expression
689
                    functionStatement.func.functionStatement = decl as MethodStatement;
246✔
690

691
                    //fields
692
                } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
196✔
693

694
                    decl = this.fieldDeclaration(accessModifier);
174✔
695

696
                    //class fields cannot be overridden
697
                    if (overrideKeyword) {
173!
698
                        this.diagnostics.push({
×
699
                            ...DiagnosticMessages.classFieldCannotBeOverridden(),
700
                            range: overrideKeyword.range
701
                        });
702
                    }
703

704
                    //comments
705
                } else if (this.check(TokenKind.Comment)) {
22✔
706
                    decl = this.commentStatement();
8✔
707
                }
708

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

718
            //ensure statement separator
719
            this.consumeStatementSeparators();
443✔
720
        }
721

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

730
        const result = new ClassStatement(
452✔
731
            classKeyword,
732
            className,
733
            body,
734
            endingKeyword,
735
            extendsKeyword,
736
            parentClassName
737
        );
738

739
        this._references.classStatements.push(result);
452✔
740
        this.exitAnnotationBlock(parentAnnotations);
452✔
741
        return result;
452✔
742
    }
743

744
    private fieldDeclaration(accessModifier: Token | null) {
745

746
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
174✔
747

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

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

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

792
        let initialValue: Expression;
793
        let equal: Token;
794
        //if there is a field initializer
795
        if (this.check(TokenKind.Equal)) {
174✔
796
            equal = this.advance();
38✔
797
            initialValue = this.expression();
38✔
798
        }
799

800
        return new FieldStatement(
173✔
801
            accessModifier,
802
            name,
803
            asToken,
804
            fieldType,
805
            equal,
806
            initialValue,
807
            optionalKeyword
808
        );
809
    }
810

811
    /**
812
     * An array of CallExpression for the current function body
813
     */
814
    private callExpressions = [];
1,831✔
815

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

849
            if (isAnonymous) {
1,486✔
850
                leftParen = this.consume(
72✔
851
                    DiagnosticMessages.expectedLeftParenAfterCallable(functionTypeText),
852
                    TokenKind.LeftParen
853
                );
854
            } else {
855
                name = this.consume(
1,414✔
856
                    DiagnosticMessages.expectedNameAfterCallableKeyword(functionTypeText),
857
                    TokenKind.Identifier,
858
                    ...AllowedProperties
859
                ) as Identifier;
860
                leftParen = this.consume(
1,412✔
861
                    DiagnosticMessages.expectedLeftParenAfterCallableName(functionTypeText),
862
                    TokenKind.LeftParen
863
                );
864

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

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

884
            let params = [] as FunctionParameterExpression[];
1,483✔
885
            let asToken: Token;
886
            let typeToken: Token;
887
            if (!this.check(TokenKind.RightParen)) {
1,483✔
888
                do {
260✔
889
                    params.push(this.functionParameter());
603✔
890
                } while (this.match(TokenKind.Comma));
891
            }
892
            let rightParen = this.advance();
1,483✔
893

894
            if (this.check(TokenKind.As)) {
1,483✔
895
                asToken = this.advance();
74✔
896

897
                typeToken = this.typeToken();
74✔
898

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

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

915
                return haveFoundOptional || !!param.defaultValue;
603✔
916
            }, false);
917

918
            this.consumeStatementSeparators(true);
1,483✔
919

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

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

937
            this._references.functionExpressions.push(func);
1,483✔
938

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

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

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

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

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

977
                return result;
1,411✔
978
            }
979
        } finally {
980
            this.namespaceAndFunctionDepth--;
1,486✔
981
            //restore the previous CallExpression list
982
            this.callExpressions = previousCallExpressions;
1,486✔
983
        }
984
    }
985

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

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

999
        let typeToken: Token | undefined;
1000
        let defaultValue;
1001

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

1008
        let asToken = null;
606✔
1009
        if (this.check(TokenKind.As)) {
606✔
1010
            asToken = this.advance();
268✔
1011

1012
            typeToken = this.typeToken();
268✔
1013

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

1029
    private assignment(): AssignmentStatement {
1030
        let name = this.advance() as Identifier;
870✔
1031
        //add diagnostic if name is a reserved word that cannot be used as an identifier
1032
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
870✔
1033
            this.diagnostics.push({
12✔
1034
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
1035
                range: name.range
1036
            });
1037
        }
1038
        if (this.check(TokenKind.As)) {
870✔
1039
            // v1 syntax allows type declaration on lhs of assignment
1040
            this.warnIfNotBrighterScriptMode('typed assignment');
2✔
1041

1042
            this.advance(); // skip 'as'
2✔
1043
            this.typeToken(); // skip typeToken;
2✔
1044
        }
1045

1046
        let operator = this.consume(
870✔
1047
            DiagnosticMessages.expectedOperatorAfterIdentifier(AssignmentOperators, name.text),
1048
            ...AssignmentOperators
1049
        );
1050
        let value = this.expression();
869✔
1051

1052
        let result: AssignmentStatement;
1053
        if (operator.kind === TokenKind.Equal) {
862✔
1054
            result = new AssignmentStatement(operator, name, value);
818✔
1055
        } else {
1056
            const nameExpression = new VariableExpression(name);
44✔
1057
            result = new AssignmentStatement(
44✔
1058
                operator,
1059
                name,
1060
                new BinaryExpression(nameExpression, operator, value)
1061
            );
1062
            this.addExpressionsToReferences(nameExpression);
44✔
1063
            if (isBinaryExpression(value)) {
44✔
1064
                //remove the right-hand-side expression from this assignment operator, and replace with the full assignment expression
1065
                this._references.expressions.delete(value);
3✔
1066
            }
1067
            this._references.expressions.add(result);
44✔
1068
        }
1069

1070
        this._references.assignmentStatements.push(result);
862✔
1071
        return result;
862✔
1072
    }
1073

1074
    private checkLibrary() {
1075
        let isLibraryToken = this.check(TokenKind.Library);
7,004✔
1076

1077
        //if we are at the top level, any line that starts with "library" should be considered a library statement
1078
        if (this.isAtRootLevel() && isLibraryToken) {
7,004✔
1079
            return true;
11✔
1080

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

1086
            //definitely not a library statement
1087
        } else {
1088
            return false;
6,992✔
1089
        }
1090
    }
1091

1092
    private statement(): Statement | undefined {
1093
        if (this.checkLibrary()) {
3,382!
1094
            return this.libraryStatement();
×
1095
        }
1096

1097
        if (this.check(TokenKind.Import)) {
3,382✔
1098
            return this.importStatement();
34✔
1099
        }
1100

1101
        if (this.check(TokenKind.Stop)) {
3,348✔
1102
            return this.stopStatement();
15✔
1103
        }
1104

1105
        if (this.check(TokenKind.If)) {
3,333✔
1106
            return this.ifStatement();
146✔
1107
        }
1108

1109
        //`try` must be followed by a block, otherwise it could be a local variable
1110
        if (this.check(TokenKind.Try) && this.checkAnyNext(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
3,187✔
1111
            return this.tryCatchStatement();
24✔
1112
        }
1113

1114
        if (this.check(TokenKind.Throw)) {
3,163✔
1115
            return this.throwStatement();
9✔
1116
        }
1117

1118
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
3,154✔
1119
            return this.printStatement();
572✔
1120
        }
1121
        if (this.check(TokenKind.Dim)) {
2,582✔
1122
            return this.dimStatement();
40✔
1123
        }
1124

1125
        if (this.check(TokenKind.While)) {
2,542✔
1126
            return this.whileStatement();
19✔
1127
        }
1128

1129
        if (this.check(TokenKind.ExitWhile)) {
2,523✔
1130
            return this.exitWhile();
6✔
1131
        }
1132

1133
        if (this.check(TokenKind.For)) {
2,517✔
1134
            return this.forStatement();
29✔
1135
        }
1136

1137
        if (this.check(TokenKind.ForEach)) {
2,488✔
1138
            return this.forEachStatement();
18✔
1139
        }
1140

1141
        if (this.check(TokenKind.ExitFor)) {
2,470✔
1142
            return this.exitFor();
3✔
1143
        }
1144

1145
        if (this.check(TokenKind.End)) {
2,467✔
1146
            return this.endStatement();
7✔
1147
        }
1148

1149
        if (this.match(TokenKind.Return)) {
2,460✔
1150
            return this.returnStatement();
149✔
1151
        }
1152

1153
        if (this.check(TokenKind.Goto)) {
2,311✔
1154
            return this.gotoStatement();
11✔
1155
        }
1156

1157
        //the continue keyword (followed by `for`, `while`, or a statement separator)
1158
        if (this.check(TokenKind.Continue) && this.checkAnyNext(TokenKind.While, TokenKind.For, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
2,300✔
1159
            return this.continueStatement();
11✔
1160
        }
1161

1162
        //does this line look like a label? (i.e.  `someIdentifier:` )
1163
        if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
2,289✔
1164
            try {
11✔
1165
                return this.labelStatement();
11✔
1166
            } catch (err) {
1167
                if (!(err instanceof CancelStatementError)) {
2!
1168
                    throw err;
×
1169
                }
1170
                //not a label, try something else
1171
            }
1172
        }
1173

1174
        // BrightScript is like python, in that variables can be declared without a `var`,
1175
        // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
1176
        // out what to do with it.
1177
        if (
2,280✔
1178
            this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)
1179
        ) {
1180
            if (this.checkAnyNext(...AssignmentOperators)) {
2,120✔
1181
                return this.assignment();
839✔
1182
            } else if (this.checkNext(TokenKind.As)) {
1,281✔
1183
                // may be a typed assignment - this is v1 syntax
1184
                const backtrack = this.current;
3✔
1185
                let validTypeExpression = false;
3✔
1186
                try {
3✔
1187
                    // skip the identifier, and check for valid type expression
1188
                    this.advance();
3✔
1189
                    // skip the 'as'
1190
                    this.advance();
3✔
1191
                    // check if there is a valid type
1192
                    const typeToken = this.typeToken(true);
3✔
1193
                    const allowedNameKinds = [TokenKind.Identifier, ...DeclarableTypes, ...this.allowedLocalIdentifiers];
3✔
1194
                    validTypeExpression = allowedNameKinds.includes(typeToken.kind);
3✔
1195
                } catch (e) {
1196
                    // ignore any errors
1197
                } finally {
1198
                    this.current = backtrack;
3✔
1199
                }
1200
                if (validTypeExpression) {
3✔
1201
                    // there is a valid 'as' and type expression
1202
                    return this.assignment();
2✔
1203
                }
1204
            }
1205
        }
1206

1207
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1208
        if (this.check(TokenKind.Interface)) {
1,439✔
1209
            return this.interfaceDeclaration();
43✔
1210
        }
1211

1212
        if (this.check(TokenKind.Class)) {
1,396✔
1213
            return this.classDeclaration();
453✔
1214
        }
1215

1216
        if (this.check(TokenKind.Namespace)) {
943✔
1217
            return this.namespaceStatement();
231✔
1218
        }
1219

1220
        if (this.check(TokenKind.Enum)) {
712✔
1221
            return this.enumDeclaration();
106✔
1222
        }
1223

1224
        // TODO: support multi-statements
1225
        return this.setStatement();
606✔
1226
    }
1227

1228
    private whileStatement(): WhileStatement {
1229
        const whileKeyword = this.advance();
19✔
1230
        const condition = this.expression();
19✔
1231

1232
        this.consumeStatementSeparators();
18✔
1233

1234
        const whileBlock = this.block(TokenKind.EndWhile);
18✔
1235
        let endWhile: Token;
1236
        if (!whileBlock || this.peek().kind !== TokenKind.EndWhile) {
18✔
1237
            this.diagnostics.push({
1✔
1238
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('while'),
1239
                range: this.peek().range
1240
            });
1241
            if (!whileBlock) {
1!
1242
                throw this.lastDiagnosticAsError();
×
1243
            }
1244
        } else {
1245
            endWhile = this.advance();
17✔
1246
        }
1247

1248
        return new WhileStatement(
18✔
1249
            { while: whileKeyword, endWhile: endWhile },
1250
            condition,
1251
            whileBlock
1252
        );
1253
    }
1254

1255
    private exitWhile(): ExitWhileStatement {
1256
        let keyword = this.advance();
6✔
1257

1258
        return new ExitWhileStatement({ exitWhile: keyword });
6✔
1259
    }
1260

1261
    private forStatement(): ForStatement {
1262
        const forToken = this.advance();
29✔
1263
        const initializer = this.assignment();
29✔
1264

1265
        //TODO: newline allowed?
1266

1267
        const toToken = this.advance();
28✔
1268
        const finalValue = this.expression();
28✔
1269
        let incrementExpression: Expression | undefined;
1270
        let stepToken: Token | undefined;
1271

1272
        if (this.check(TokenKind.Step)) {
28✔
1273
            stepToken = this.advance();
8✔
1274
            incrementExpression = this.expression();
8✔
1275
        } else {
1276
            // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
1277
        }
1278

1279
        this.consumeStatementSeparators();
28✔
1280

1281
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
28✔
1282
        let endForToken: Token;
1283
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
28✔
1284
            this.diagnostics.push({
1✔
1285
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1286
                range: this.peek().range
1287
            });
1288
            if (!body) {
1!
1289
                throw this.lastDiagnosticAsError();
×
1290
            }
1291
        } else {
1292
            endForToken = this.advance();
27✔
1293
        }
1294

1295
        // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
1296
        // stays around in scope with whatever value it was when the loop exited.
1297
        return new ForStatement(
28✔
1298
            forToken,
1299
            initializer,
1300
            toToken,
1301
            finalValue,
1302
            body,
1303
            endForToken,
1304
            stepToken,
1305
            incrementExpression
1306
        );
1307
    }
1308

1309
    private forEachStatement(): ForEachStatement {
1310
        let forEach = this.advance();
18✔
1311
        let name = this.advance();
18✔
1312

1313
        let maybeIn = this.peek();
18✔
1314
        if (this.check(TokenKind.Identifier) && maybeIn.text.toLowerCase() === 'in') {
18!
1315
            this.advance();
18✔
1316
        } else {
1317
            this.diagnostics.push({
×
1318
                ...DiagnosticMessages.expectedInAfterForEach(name.text),
1319
                range: this.peek().range
1320
            });
1321
            throw this.lastDiagnosticAsError();
×
1322
        }
1323

1324
        let target = this.expression();
18✔
1325
        if (!target) {
18!
1326
            this.diagnostics.push({
×
1327
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1328
                range: this.peek().range
1329
            });
1330
            throw this.lastDiagnosticAsError();
×
1331
        }
1332

1333
        this.consumeStatementSeparators();
18✔
1334

1335
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
18✔
1336
        if (!body) {
18!
1337
            this.diagnostics.push({
×
1338
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1339
                range: this.peek().range
1340
            });
1341
            throw this.lastDiagnosticAsError();
×
1342
        }
1343

1344
        let endFor = this.advance();
18✔
1345

1346
        return new ForEachStatement(
18✔
1347
            {
1348
                forEach: forEach,
1349
                in: maybeIn,
1350
                endFor: endFor
1351
            },
1352
            name,
1353
            target,
1354
            body
1355
        );
1356
    }
1357

1358
    private exitFor(): ExitForStatement {
1359
        let keyword = this.advance();
3✔
1360

1361
        return new ExitForStatement({ exitFor: keyword });
3✔
1362
    }
1363

1364
    private commentStatement() {
1365
        //if this comment is on the same line as the previous statement,
1366
        //then this comment should be treated as a single-line comment
1367
        let prev = this.previous();
215✔
1368
        if (prev?.range?.end.line === this.peek().range?.start.line) {
215✔
1369
            return new CommentStatement([this.advance()]);
125✔
1370
        } else {
1371
            let comments = [this.advance()];
90✔
1372
            while (this.check(TokenKind.Newline) && this.checkNext(TokenKind.Comment)) {
90✔
1373
                this.advance();
17✔
1374
                comments.push(this.advance());
17✔
1375
            }
1376
            return new CommentStatement(comments);
90✔
1377
        }
1378
    }
1379

1380
    private namespaceStatement(): NamespaceStatement | undefined {
1381
        this.warnIfNotBrighterScriptMode('namespace');
231✔
1382
        let keyword = this.advance();
231✔
1383

1384
        this.namespaceAndFunctionDepth++;
231✔
1385

1386
        let name = this.getNamespacedVariableNameExpression();
231✔
1387
        //set the current namespace name
1388
        let result = new NamespaceStatement(keyword, name, null, null);
230✔
1389

1390
        this.globalTerminators.push([TokenKind.EndNamespace]);
230✔
1391
        let body = this.body();
230✔
1392
        this.globalTerminators.pop();
230✔
1393

1394
        let endKeyword: Token;
1395
        if (this.check(TokenKind.EndNamespace)) {
230✔
1396
            endKeyword = this.advance();
229✔
1397
        } else {
1398
            //the `end namespace` keyword is missing. add a diagnostic, but keep parsing
1399
            this.diagnostics.push({
1✔
1400
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'),
1401
                range: keyword.range
1402
            });
1403
        }
1404

1405
        this.namespaceAndFunctionDepth--;
230✔
1406

1407
        result.body = body;
230✔
1408
        result.endKeyword = endKeyword;
230✔
1409
        this._references.namespaceStatements.push(result);
230✔
1410
        //cache the range property so that plugins can't affect it
1411
        result.cacheRange();
230✔
1412
        result.body.symbolTable.name += `: namespace '${result.name}'`;
230✔
1413
        return result;
230✔
1414
    }
1415

1416
    /**
1417
     * Get an expression with identifiers separated by periods. Useful for namespaces and class extends
1418
     */
1419
    private getNamespacedVariableNameExpression(ignoreDiagnostics = false) {
344✔
1420
        let firstIdentifier: Identifier;
1421
        if (ignoreDiagnostics) {
422✔
1422
            if (this.checkAny(...this.allowedLocalIdentifiers)) {
2!
1423
                firstIdentifier = this.advance() as Identifier;
×
1424
            } else {
1425
                throw new Error();
2✔
1426
            }
1427
        } else {
1428
            firstIdentifier = this.consume(
420✔
1429
                DiagnosticMessages.expectedIdentifierAfterKeyword(this.previous().text),
1430
                TokenKind.Identifier,
1431
                ...this.allowedLocalIdentifiers
1432
            ) as Identifier;
1433
        }
1434
        let expr: DottedGetExpression | VariableExpression;
1435

1436
        if (firstIdentifier) {
417!
1437
            // force it into an identifier so the AST makes some sense
1438
            firstIdentifier.kind = TokenKind.Identifier;
417✔
1439
            const varExpr = new VariableExpression(firstIdentifier);
417✔
1440
            expr = varExpr;
417✔
1441

1442
            //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
1443
            while (this.check(TokenKind.Dot)) {
417✔
1444
                let dot = this.tryConsume(
142✔
1445
                    DiagnosticMessages.unexpectedToken(this.peek().text),
1446
                    TokenKind.Dot
1447
                );
1448
                if (!dot) {
142!
1449
                    break;
×
1450
                }
1451
                let identifier = this.tryConsume(
142✔
1452
                    DiagnosticMessages.expectedIdentifier(),
1453
                    TokenKind.Identifier,
1454
                    ...this.allowedLocalIdentifiers,
1455
                    ...AllowedProperties
1456
                ) as Identifier;
1457

1458
                if (!identifier) {
142✔
1459
                    break;
3✔
1460
                }
1461
                // force it into an identifier so the AST makes some sense
1462
                identifier.kind = TokenKind.Identifier;
139✔
1463
                expr = new DottedGetExpression(expr, identifier, dot);
139✔
1464
            }
1465
        }
1466
        return new NamespacedVariableNameExpression(expr);
417✔
1467
    }
1468

1469
    /**
1470
     * Add an 'unexpected token' diagnostic for any token found between current and the first stopToken found.
1471
     */
1472
    private flagUntil(...stopTokens: TokenKind[]) {
1473
        while (!this.checkAny(...stopTokens) && !this.isAtEnd()) {
5!
1474
            let token = this.advance();
×
1475
            this.diagnostics.push({
×
1476
                ...DiagnosticMessages.unexpectedToken(token.text),
1477
                range: token.range
1478
            });
1479
        }
1480
    }
1481

1482
    /**
1483
     * Consume tokens until one of the `stopTokenKinds` is encountered
1484
     * @param stopTokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
1485
     * @returns - the list of tokens consumed, EXCLUDING the `stopTokenKind` (you can use `this.peek()` to see which one it was)
1486
     */
1487
    private consumeUntil(...stopTokenKinds: TokenKind[]) {
1488
        let result = [] as Token[];
82✔
1489
        //take tokens until we encounter one of the stopTokenKinds
1490
        while (!stopTokenKinds.includes(this.peek().kind)) {
82✔
1491
            result.push(this.advance());
239✔
1492
        }
1493
        return result;
82✔
1494
    }
1495

1496
    private constDeclaration(): ConstStatement | undefined {
1497
        this.warnIfNotBrighterScriptMode('const declaration');
50✔
1498
        const constToken = this.advance();
50✔
1499
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
50✔
1500
        const equalToken = this.consumeToken(TokenKind.Equal);
50✔
1501
        const expression = this.expression();
50✔
1502
        const statement = new ConstStatement({
50✔
1503
            const: constToken,
1504
            name: nameToken,
1505
            equals: equalToken
1506
        }, expression);
1507
        this._references.constStatements.push(statement);
50✔
1508
        return statement;
50✔
1509
    }
1510

1511
    private libraryStatement(): LibraryStatement | undefined {
1512
        let libStatement = new LibraryStatement({
12✔
1513
            library: this.advance(),
1514
            //grab the next token only if it's a string
1515
            filePath: this.tryConsume(
1516
                DiagnosticMessages.expectedStringLiteralAfterKeyword('library'),
1517
                TokenKind.StringLiteral
1518
            )
1519
        });
1520

1521
        this._references.libraryStatements.push(libStatement);
12✔
1522
        return libStatement;
12✔
1523
    }
1524

1525
    private importStatement() {
1526
        this.warnIfNotBrighterScriptMode('import statements');
34✔
1527
        let importStatement = new ImportStatement(
34✔
1528
            this.advance(),
1529
            //grab the next token only if it's a string
1530
            this.tryConsume(
1531
                DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
1532
                TokenKind.StringLiteral
1533
            )
1534
        );
1535

1536
        this._references.importStatements.push(importStatement);
34✔
1537
        return importStatement;
34✔
1538
    }
1539

1540
    private annotationExpression() {
1541
        const atToken = this.advance();
46✔
1542
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
46✔
1543
        if (identifier) {
46✔
1544
            identifier.kind = TokenKind.Identifier;
45✔
1545
        }
1546
        let annotation = new AnnotationExpression(atToken, identifier);
46✔
1547
        this.pendingAnnotations.push(annotation);
45✔
1548

1549
        //optional arguments
1550
        if (this.check(TokenKind.LeftParen)) {
45✔
1551
            let leftParen = this.advance();
9✔
1552
            annotation.call = this.finishCall(leftParen, annotation, false);
9✔
1553
        }
1554
        return annotation;
45✔
1555
    }
1556

1557
    private ternaryExpression(test?: Expression): TernaryExpression {
1558
        this.warnIfNotBrighterScriptMode('ternary operator');
74✔
1559
        if (!test) {
74!
1560
            test = this.expression();
×
1561
        }
1562
        const questionMarkToken = this.advance();
74✔
1563

1564
        //consume newlines or comments
1565
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
74✔
1566
            this.advance();
8✔
1567
        }
1568

1569
        let consequent: Expression;
1570
        try {
74✔
1571
            consequent = this.expression();
74✔
1572
        } catch { }
1573

1574
        //consume newlines or comments
1575
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
74✔
1576
            this.advance();
6✔
1577
        }
1578

1579
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
74✔
1580

1581
        //consume newlines
1582
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
74✔
1583
            this.advance();
12✔
1584
        }
1585
        let alternate: Expression;
1586
        try {
74✔
1587
            alternate = this.expression();
74✔
1588
        } catch { }
1589

1590
        return new TernaryExpression(test, questionMarkToken, consequent, colonToken, alternate);
74✔
1591
    }
1592

1593
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1594
        this.warnIfNotBrighterScriptMode('null coalescing operator');
27✔
1595
        const questionQuestionToken = this.advance();
27✔
1596
        const alternate = this.expression();
27✔
1597
        return new NullCoalescingExpression(test, questionQuestionToken, alternate);
27✔
1598
    }
1599

1600
    private regexLiteralExpression() {
1601
        this.warnIfNotBrighterScriptMode('regular expression literal');
44✔
1602
        return new RegexLiteralExpression({
44✔
1603
            regexLiteral: this.advance()
1604
        });
1605
    }
1606

1607
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1608
        this.warnIfNotBrighterScriptMode('template string');
39✔
1609

1610
        //get the tag name
1611
        let tagName: Identifier;
1612
        if (isTagged) {
39✔
1613
            tagName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties) as Identifier;
5✔
1614
            // force it into an identifier so the AST makes some sense
1615
            tagName.kind = TokenKind.Identifier;
5✔
1616
        }
1617

1618
        let quasis = [] as TemplateStringQuasiExpression[];
39✔
1619
        let expressions = [];
39✔
1620
        let openingBacktick = this.peek();
39✔
1621
        this.advance();
39✔
1622
        let currentQuasiExpressionParts = [];
39✔
1623
        while (!this.isAtEnd() && !this.check(TokenKind.BackTick)) {
39✔
1624
            let next = this.peek();
150✔
1625
            if (next.kind === TokenKind.TemplateStringQuasi) {
150✔
1626
                //a quasi can actually be made up of multiple quasis when it includes char literals
1627
                currentQuasiExpressionParts.push(
94✔
1628
                    new LiteralExpression(next)
1629
                );
1630
                this.advance();
94✔
1631
            } else if (next.kind === TokenKind.EscapedCharCodeLiteral) {
56✔
1632
                currentQuasiExpressionParts.push(
24✔
1633
                    new EscapedCharCodeLiteralExpression(<any>next)
1634
                );
1635
                this.advance();
24✔
1636
            } else {
1637
                //finish up the current quasi
1638
                quasis.push(
32✔
1639
                    new TemplateStringQuasiExpression(currentQuasiExpressionParts)
1640
                );
1641
                currentQuasiExpressionParts = [];
32✔
1642

1643
                if (next.kind === TokenKind.TemplateStringExpressionBegin) {
32!
1644
                    this.advance();
32✔
1645
                }
1646
                //now keep this expression
1647
                expressions.push(this.expression());
32✔
1648
                if (!this.isAtEnd() && this.check(TokenKind.TemplateStringExpressionEnd)) {
32!
1649
                    //TODO is it an error if this is not present?
1650
                    this.advance();
32✔
1651
                } else {
1652
                    this.diagnostics.push({
×
1653
                        ...DiagnosticMessages.unterminatedTemplateExpression(),
1654
                        range: util.getRange(openingBacktick, this.peek())
1655
                    });
1656
                    throw this.lastDiagnosticAsError();
×
1657
                }
1658
            }
1659
        }
1660

1661
        //store the final set of quasis
1662
        quasis.push(
39✔
1663
            new TemplateStringQuasiExpression(currentQuasiExpressionParts)
1664
        );
1665

1666
        if (this.isAtEnd()) {
39✔
1667
            //error - missing backtick
1668
            this.diagnostics.push({
2✔
1669
                ...DiagnosticMessages.unterminatedTemplateStringAtEndOfFile(),
1670
                range: util.getRange(openingBacktick, this.peek())
1671
            });
1672
            throw this.lastDiagnosticAsError();
2✔
1673

1674
        } else {
1675
            let closingBacktick = this.advance();
37✔
1676
            if (isTagged) {
37✔
1677
                return new TaggedTemplateStringExpression(tagName, openingBacktick, quasis, expressions, closingBacktick);
5✔
1678
            } else {
1679
                return new TemplateStringExpression(openingBacktick, quasis, expressions, closingBacktick);
32✔
1680
            }
1681
        }
1682
    }
1683

1684
    private tryCatchStatement(): TryCatchStatement {
1685
        const tryToken = this.advance();
24✔
1686
        const statement = new TryCatchStatement(
24✔
1687
            { try: tryToken }
1688
        );
1689

1690
        //ensure statement separator
1691
        this.consumeStatementSeparators();
24✔
1692

1693
        statement.tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
24✔
1694

1695
        const peek = this.peek();
24✔
1696
        if (peek.kind !== TokenKind.Catch) {
24✔
1697
            this.diagnostics.push({
2✔
1698
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1699
                range: this.peek().range
1700
            });
1701
            //gracefully handle end-try
1702
            if (peek.kind === TokenKind.EndTry) {
2✔
1703
                statement.tokens.endTry = this.advance();
1✔
1704
            }
1705
            return statement;
2✔
1706
        }
1707
        const catchStmt = new CatchStatement({ catch: this.advance() });
22✔
1708
        statement.catchStatement = catchStmt;
22✔
1709

1710
        const exceptionVarToken = this.tryConsume(DiagnosticMessages.missingExceptionVarToFollowCatch(), TokenKind.Identifier, ...this.allowedLocalIdentifiers);
22✔
1711
        if (exceptionVarToken) {
22✔
1712
            // force it into an identifier so the AST makes some sense
1713
            exceptionVarToken.kind = TokenKind.Identifier;
20✔
1714
            catchStmt.exceptionVariable = exceptionVarToken as Identifier;
20✔
1715
        }
1716

1717
        //ensure statement sepatator
1718
        this.consumeStatementSeparators();
22✔
1719

1720
        catchStmt.catchBranch = this.block(TokenKind.EndTry);
22✔
1721

1722
        if (this.peek().kind !== TokenKind.EndTry) {
22✔
1723
            this.diagnostics.push({
1✔
1724
                ...DiagnosticMessages.expectedEndTryToTerminateTryCatch(),
1725
                range: this.peek().range
1726
            });
1727
        } else {
1728
            statement.tokens.endTry = this.advance();
21✔
1729
        }
1730
        return statement;
22✔
1731
    }
1732

1733
    private throwStatement() {
1734
        const throwToken = this.advance();
9✔
1735
        let expression: Expression;
1736
        if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
9✔
1737
            this.diagnostics.push({
1✔
1738
                ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(),
1739
                range: throwToken.range
1740
            });
1741
        } else {
1742
            expression = this.expression();
8✔
1743
        }
1744
        return new ThrowStatement(throwToken, expression);
7✔
1745
    }
1746

1747
    private dimStatement() {
1748
        const dim = this.advance();
40✔
1749

1750
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
40✔
1751
        // force to an identifier so the AST makes some sense
1752
        if (identifier) {
40✔
1753
            identifier.kind = TokenKind.Identifier;
38✔
1754
        }
1755

1756
        let leftSquareBracket = this.tryConsume(DiagnosticMessages.missingLeftSquareBracketAfterDimIdentifier(), TokenKind.LeftSquareBracket);
40✔
1757

1758
        let expressions: Expression[] = [];
40✔
1759
        let expression: Expression;
1760
        do {
40✔
1761
            try {
76✔
1762
                expression = this.expression();
76✔
1763
                expressions.push(expression);
71✔
1764
                if (this.check(TokenKind.Comma)) {
71✔
1765
                    this.advance();
36✔
1766
                } else {
1767
                    // will also exit for right square braces
1768
                    break;
35✔
1769
                }
1770
            } catch (error) {
1771
            }
1772
        } while (expression);
1773

1774
        if (expressions.length === 0) {
40✔
1775
            this.diagnostics.push({
5✔
1776
                ...DiagnosticMessages.missingExpressionsInDimStatement(),
1777
                range: this.peek().range
1778
            });
1779
        }
1780
        let rightSquareBracket = this.tryConsume(DiagnosticMessages.missingRightSquareBracketAfterDimIdentifier(), TokenKind.RightSquareBracket);
40✔
1781
        return new DimStatement(dim, identifier, leftSquareBracket, expressions, rightSquareBracket);
40✔
1782
    }
1783

1784
    private ifStatement(): IfStatement {
1785
        // colon before `if` is usually not allowed, unless it's after `then`
1786
        if (this.current > 0) {
189✔
1787
            const prev = this.previous();
184✔
1788
            if (prev.kind === TokenKind.Colon) {
184✔
1789
                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then) {
3✔
1790
                    this.diagnostics.push({
1✔
1791
                        ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
1792
                        range: prev.range
1793
                    });
1794
                }
1795
            }
1796
        }
1797

1798
        const ifToken = this.advance();
189✔
1799
        const startingRange = ifToken.range;
189✔
1800

1801
        const condition = this.expression();
189✔
1802
        let thenBranch: Block;
1803
        let elseBranch: IfStatement | Block | undefined;
1804

1805
        let thenToken: Token | undefined;
1806
        let endIfToken: Token | undefined;
1807
        let elseToken: Token | undefined;
1808

1809
        //optional `then`
1810
        if (this.check(TokenKind.Then)) {
187✔
1811
            thenToken = this.advance();
127✔
1812
        }
1813

1814
        //is it inline or multi-line if?
1815
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
187✔
1816

1817
        if (isInlineIfThen) {
187✔
1818
            /*** PARSE INLINE IF STATEMENT ***/
1819

1820
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
32✔
1821

1822
            if (!thenBranch) {
32!
1823
                this.diagnostics.push({
×
1824
                    ...DiagnosticMessages.expectedStatementToFollowConditionalCondition(ifToken.text),
1825
                    range: this.peek().range
1826
                });
1827
                throw this.lastDiagnosticAsError();
×
1828
            } else {
1829
                this.ensureInline(thenBranch.statements);
32✔
1830
            }
1831

1832
            //else branch
1833
            if (this.check(TokenKind.Else)) {
32✔
1834
                elseToken = this.advance();
19✔
1835

1836
                if (this.check(TokenKind.If)) {
19✔
1837
                    // recurse-read `else if`
1838
                    elseBranch = this.ifStatement();
4✔
1839

1840
                    //no multi-line if chained with an inline if
1841
                    if (!elseBranch.isInline) {
4✔
1842
                        this.diagnostics.push({
2✔
1843
                            ...DiagnosticMessages.expectedInlineIfStatement(),
1844
                            range: elseBranch.range
1845
                        });
1846
                    }
1847

1848
                } else if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
15!
1849
                    //expecting inline else branch
1850
                    this.diagnostics.push({
×
1851
                        ...DiagnosticMessages.expectedInlineIfStatement(),
1852
                        range: this.peek().range
1853
                    });
1854
                    throw this.lastDiagnosticAsError();
×
1855
                } else {
1856
                    elseBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
15✔
1857

1858
                    if (elseBranch) {
15!
1859
                        this.ensureInline(elseBranch.statements);
15✔
1860
                    }
1861
                }
1862

1863
                if (!elseBranch) {
19!
1864
                    //missing `else` branch
1865
                    this.diagnostics.push({
×
1866
                        ...DiagnosticMessages.expectedStatementToFollowElse(),
1867
                        range: this.peek().range
1868
                    });
1869
                    throw this.lastDiagnosticAsError();
×
1870
                }
1871
            }
1872

1873
            if (!elseBranch || !isIfStatement(elseBranch)) {
32✔
1874
                //enforce newline at the end of the inline if statement
1875
                const peek = this.peek();
28✔
1876
                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && !this.isAtEnd()) {
28✔
1877
                    //ignore last error if it was about a colon
1878
                    if (this.previous().kind === TokenKind.Colon) {
3!
1879
                        this.diagnostics.pop();
3✔
1880
                        this.current--;
3✔
1881
                    }
1882
                    //newline is required
1883
                    this.diagnostics.push({
3✔
1884
                        ...DiagnosticMessages.expectedFinalNewline(),
1885
                        range: this.peek().range
1886
                    });
1887
                }
1888
            }
1889

1890
        } else {
1891
            /*** PARSE MULTI-LINE IF STATEMENT ***/
1892

1893
            thenBranch = this.blockConditionalBranch(ifToken);
155✔
1894

1895
            //ensure newline/colon before next keyword
1896
            this.ensureNewLineOrColon();
153✔
1897

1898
            //else branch
1899
            if (this.check(TokenKind.Else)) {
153✔
1900
                elseToken = this.advance();
82✔
1901

1902
                if (this.check(TokenKind.If)) {
82✔
1903
                    // recurse-read `else if`
1904
                    elseBranch = this.ifStatement();
39✔
1905

1906
                } else {
1907
                    elseBranch = this.blockConditionalBranch(ifToken);
43✔
1908

1909
                    //ensure newline/colon before next keyword
1910
                    this.ensureNewLineOrColon();
43✔
1911
                }
1912
            }
1913

1914
            if (!isIfStatement(elseBranch)) {
153✔
1915
                if (this.check(TokenKind.EndIf)) {
114✔
1916
                    endIfToken = this.advance();
112✔
1917

1918
                } else {
1919
                    //missing endif
1920
                    this.diagnostics.push({
2✔
1921
                        ...DiagnosticMessages.expectedEndIfToCloseIfStatement(startingRange.start),
1922
                        range: ifToken.range
1923
                    });
1924
                }
1925
            }
1926
        }
1927

1928
        return new IfStatement(
185✔
1929
            {
1930
                if: ifToken,
1931
                then: thenToken,
1932
                endIf: endIfToken,
1933
                else: elseToken
1934
            },
1935
            condition,
1936
            thenBranch,
1937
            elseBranch,
1938
            isInlineIfThen
1939
        );
1940
    }
1941

1942
    //consume a `then` or `else` branch block of an `if` statement
1943
    private blockConditionalBranch(ifToken: Token) {
1944
        //keep track of the current error count, because if the then branch fails,
1945
        //we will trash them in favor of a single error on if
1946
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
198✔
1947

1948
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
1949
        // a trailing "end if" or "else if"
1950
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
198✔
1951

1952
        if (!branch) {
198✔
1953
            //throw out any new diagnostics created as a result of a `then` block parse failure.
1954
            //the block() function will discard the current line, so any discarded diagnostics will
1955
            //resurface if they are legitimate, and not a result of a malformed if statement
1956
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
2✔
1957

1958
            //this whole if statement is bogus...add error to the if token and hard-fail
1959
            this.diagnostics.push({
2✔
1960
                ...DiagnosticMessages.expectedEndIfElseIfOrElseToTerminateThenBlock(),
1961
                range: ifToken.range
1962
            });
1963
            throw this.lastDiagnosticAsError();
2✔
1964
        }
1965
        return branch;
196✔
1966
    }
1967

1968
    private ensureNewLineOrColon(silent = false) {
196✔
1969
        const prev = this.previous().kind;
399✔
1970
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
399✔
1971
            if (!silent) {
120✔
1972
                this.diagnostics.push({
6✔
1973
                    ...DiagnosticMessages.expectedNewlineOrColon(),
1974
                    range: this.peek().range
1975
                });
1976
            }
1977
            return false;
120✔
1978
        }
1979
        return true;
279✔
1980
    }
1981

1982
    //ensure each statement of an inline block is single-line
1983
    private ensureInline(statements: Statement[]) {
1984
        for (const stat of statements) {
47✔
1985
            if (isIfStatement(stat) && !stat.isInline) {
54✔
1986
                this.diagnostics.push({
2✔
1987
                    ...DiagnosticMessages.expectedInlineIfStatement(),
1988
                    range: stat.range
1989
                });
1990
            }
1991
        }
1992
    }
1993

1994
    //consume inline branch of an `if` statement
1995
    private inlineConditionalBranch(...additionalTerminators: BlockTerminator[]): Block | undefined {
1996
        let statements = [];
54✔
1997
        //attempt to get the next statement without using `this.declaration`
1998
        //which seems a bit hackish to get to work properly
1999
        let statement = this.statement();
54✔
2000
        if (!statement) {
54!
2001
            return undefined;
×
2002
        }
2003
        statements.push(statement);
54✔
2004
        const startingRange = statement.range;
54✔
2005

2006
        //look for colon statement separator
2007
        let foundColon = false;
54✔
2008
        while (this.match(TokenKind.Colon)) {
54✔
2009
            foundColon = true;
12✔
2010
        }
2011

2012
        //if a colon was found, add the next statement or err if unexpected
2013
        if (foundColon) {
54✔
2014
            if (!this.checkAny(TokenKind.Newline, ...additionalTerminators)) {
12✔
2015
                //if not an ending keyword, add next statement
2016
                let extra = this.inlineConditionalBranch(...additionalTerminators);
7✔
2017
                if (!extra) {
7!
2018
                    return undefined;
×
2019
                }
2020
                statements.push(...extra.statements);
7✔
2021
            } else {
2022
                //error: colon before next keyword
2023
                const colon = this.previous();
5✔
2024
                this.diagnostics.push({
5✔
2025
                    ...DiagnosticMessages.unexpectedToken(colon.text),
2026
                    range: colon.range
2027
                });
2028
            }
2029
        }
2030
        return new Block(statements, startingRange);
54✔
2031
    }
2032

2033
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2034
        let expressionStart = this.peek();
310✔
2035

2036
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
310✔
2037
            let operator = this.advance();
18✔
2038

2039
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
18✔
2040
                this.diagnostics.push({
1✔
2041
                    ...DiagnosticMessages.consecutiveIncrementDecrementOperatorsAreNotAllowed(),
2042
                    range: this.peek().range
2043
                });
2044
                throw this.lastDiagnosticAsError();
1✔
2045
            } else if (isCallExpression(expr)) {
17✔
2046
                this.diagnostics.push({
1✔
2047
                    ...DiagnosticMessages.incrementDecrementOperatorsAreNotAllowedAsResultOfFunctionCall(),
2048
                    range: expressionStart.range
2049
                });
2050
                throw this.lastDiagnosticAsError();
1✔
2051
            }
2052

2053
            const result = new IncrementStatement(expr, operator);
16✔
2054
            this._references.expressions.add(result);
16✔
2055
            return result;
16✔
2056
        }
2057

2058
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
292✔
2059
            return new ExpressionStatement(expr);
229✔
2060
        }
2061

2062
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an assignment
2063
        this.diagnostics.push({
63✔
2064
            ...DiagnosticMessages.expectedStatementOrFunctionCallButReceivedExpression(),
2065
            range: expressionStart.range
2066
        });
2067
        throw this.lastDiagnosticAsError();
63✔
2068
    }
2069

2070
    private setStatement(): DottedSetStatement | IndexedSetStatement | ExpressionStatement | IncrementStatement | AssignmentStatement {
2071
        /**
2072
         * Attempts to find an expression-statement or an increment statement.
2073
         * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
2074
         * statements aren't valid expressions. They _do_ however fall under the same parsing
2075
         * priority as standalone function calls though, so we can parse them in the same way.
2076
         */
2077
        let expr = this.call();
606✔
2078
        if (this.checkAny(...AssignmentOperators) && !(isCallExpression(expr))) {
572✔
2079
            let left = expr;
265✔
2080
            let operator = this.advance();
265✔
2081
            let right = this.expression();
265✔
2082

2083
            // Create a dotted or indexed "set" based on the left-hand side's type
2084
            if (isIndexedGetExpression(left)) {
265✔
2085
                return new IndexedSetStatement(
29✔
2086
                    left.obj,
2087
                    left.index,
2088
                    operator.kind === TokenKind.Equal
2089
                        ? right
29✔
2090
                        : new BinaryExpression(left, operator, right),
2091
                    left.openingSquare,
2092
                    left.closingSquare,
2093
                    left.additionalIndexes
2094
                );
2095
            } else if (isDottedGetExpression(left)) {
236✔
2096
                return new DottedSetStatement(
233✔
2097
                    left.obj,
2098
                    left.name,
2099
                    operator.kind === TokenKind.Equal
2100
                        ? right
233✔
2101
                        : new BinaryExpression(left, operator, right),
2102
                    left.dot
2103
                );
2104
            }
2105
        }
2106
        return this.expressionStatement(expr);
310✔
2107
    }
2108

2109
    private printStatement(): PrintStatement {
2110
        let printKeyword = this.advance();
572✔
2111

2112
        let values: (
2113
            | Expression
2114
            | PrintSeparatorTab
2115
            | PrintSeparatorSpace)[] = [];
572✔
2116

2117
        while (!this.checkEndOfStatement()) {
572✔
2118
            if (this.check(TokenKind.Semicolon)) {
656✔
2119
                values.push(this.advance() as PrintSeparatorSpace);
20✔
2120
            } else if (this.check(TokenKind.Comma)) {
636✔
2121
                values.push(this.advance() as PrintSeparatorTab);
13✔
2122
            } else if (this.check(TokenKind.Else)) {
623✔
2123
                break; // inline branch
7✔
2124
            } else {
2125
                values.push(this.expression());
616✔
2126
            }
2127
        }
2128

2129
        //print statements can be empty, so look for empty print conditions
2130
        if (!values.length) {
571✔
2131
            let emptyStringLiteral = createStringLiteral('');
4✔
2132
            values.push(emptyStringLiteral);
4✔
2133
        }
2134

2135
        let last = values[values.length - 1];
571✔
2136
        if (isToken(last)) {
571✔
2137
            // TODO: error, expected value
2138
        }
2139

2140
        return new PrintStatement({ print: printKeyword }, values);
571✔
2141
    }
2142

2143
    /**
2144
     * Parses a return statement with an optional return value.
2145
     * @returns an AST representation of a return statement.
2146
     */
2147
    private returnStatement(): ReturnStatement {
2148
        let tokens = { return: this.previous() };
149✔
2149

2150
        if (this.checkEndOfStatement()) {
149✔
2151
            return new ReturnStatement(tokens);
8✔
2152
        }
2153

2154
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
141✔
2155
        return new ReturnStatement(tokens, toReturn);
140✔
2156
    }
2157

2158
    /**
2159
     * Parses a `label` statement
2160
     * @returns an AST representation of an `label` statement.
2161
     */
2162
    private labelStatement() {
2163
        let tokens = {
11✔
2164
            identifier: this.advance(),
2165
            colon: this.advance()
2166
        };
2167

2168
        //label must be alone on its line, this is probably not a label
2169
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
11✔
2170
            //rewind and cancel
2171
            this.current -= 2;
2✔
2172
            throw new CancelStatementError();
2✔
2173
        }
2174

2175
        return new LabelStatement(tokens);
9✔
2176
    }
2177

2178
    /**
2179
     * Parses a `continue` statement
2180
     */
2181
    private continueStatement() {
2182
        return new ContinueStatement({
11✔
2183
            continue: this.advance(),
2184
            loopType: this.tryConsume(
2185
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2186
                TokenKind.While, TokenKind.For
2187
            )
2188
        });
2189
    }
2190

2191
    /**
2192
     * Parses a `goto` statement
2193
     * @returns an AST representation of an `goto` statement.
2194
     */
2195
    private gotoStatement() {
2196
        let tokens = {
11✔
2197
            goto: this.advance(),
2198
            label: this.consume(
2199
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2200
                TokenKind.Identifier
2201
            )
2202
        };
2203

2204
        return new GotoStatement(tokens);
9✔
2205
    }
2206

2207
    /**
2208
     * Parses an `end` statement
2209
     * @returns an AST representation of an `end` statement.
2210
     */
2211
    private endStatement() {
2212
        let endTokens = { end: this.advance() };
7✔
2213

2214
        return new EndStatement(endTokens);
7✔
2215
    }
2216
    /**
2217
     * Parses a `stop` statement
2218
     * @returns an AST representation of a `stop` statement
2219
     */
2220
    private stopStatement() {
2221
        let tokens = { stop: this.advance() };
15✔
2222

2223
        return new StopStatement(tokens);
15✔
2224
    }
2225

2226
    /**
2227
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2228
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2229
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2230
     *                    ignored.
2231
     */
2232
    private block(...terminators: BlockTerminator[]): Block | undefined {
2233
        const parentAnnotations = this.enterAnnotationBlock();
1,791✔
2234

2235
        this.consumeStatementSeparators(true);
1,791✔
2236
        let startingToken = this.peek();
1,791✔
2237

2238
        const statements: Statement[] = [];
1,791✔
2239
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators)) {
1,791✔
2240
            //grab the location of the current token
2241
            let loopCurrent = this.current;
2,146✔
2242
            let dec = this.declaration();
2,146✔
2243
            if (dec) {
2,146✔
2244
                if (!isAnnotationExpression(dec)) {
2,064✔
2245
                    this.consumePendingAnnotations(dec);
2,060✔
2246
                    statements.push(dec);
2,060✔
2247
                }
2248

2249
                //ensure statement separator
2250
                this.consumeStatementSeparators();
2,064✔
2251

2252
            } else {
2253
                //something went wrong. reset to the top of the loop
2254
                this.current = loopCurrent;
82✔
2255

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

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

2263
                //consume potential separators
2264
                this.consumeStatementSeparators(true);
82✔
2265
            }
2266
        }
2267

2268
        if (this.isAtEnd()) {
1,791✔
2269
            return undefined;
5✔
2270
            // TODO: Figure out how to handle unterminated blocks well
2271
        } else if (terminators.length > 0) {
1,786✔
2272
            //did we hit end-sub / end-function while looking for some other terminator?
2273
            //if so, we need to restore the statement separator
2274
            let prev = this.previous().kind;
306✔
2275
            let peek = this.peek().kind;
306✔
2276
            if (
306✔
2277
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
616!
2278
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2279
            ) {
2280
                this.current--;
6✔
2281
            }
2282
        }
2283

2284
        this.exitAnnotationBlock(parentAnnotations);
1,786✔
2285
        return new Block(statements, startingToken.range);
1,786✔
2286
    }
2287

2288
    /**
2289
     * Attach pending annotations to the provided statement,
2290
     * and then reset the annotations array
2291
     */
2292
    consumePendingAnnotations(statement: Statement) {
2293
        if (this.pendingAnnotations.length) {
5,326✔
2294
            statement.annotations = this.pendingAnnotations;
30✔
2295
            this.pendingAnnotations = [];
30✔
2296
        }
2297
    }
2298

2299
    enterAnnotationBlock() {
2300
        const pending = this.pendingAnnotations;
4,439✔
2301
        this.pendingAnnotations = [];
4,439✔
2302
        return pending;
4,439✔
2303
    }
2304

2305
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2306
        // non consumed annotations are an error
2307
        if (this.pendingAnnotations.length) {
4,433✔
2308
            for (const annotation of this.pendingAnnotations) {
4✔
2309
                this.diagnostics.push({
6✔
2310
                    ...DiagnosticMessages.unusedAnnotation(),
2311
                    range: annotation.range
2312
                });
2313
            }
2314
        }
2315
        this.pendingAnnotations = parentAnnotations;
4,433✔
2316
    }
2317

2318
    private expression(findTypeCast = true): Expression {
3,618✔
2319
        let expression = this.anonymousFunction();
3,859✔
2320
        let asToken: Token;
2321
        let typeToken: Token;
2322
        if (findTypeCast) {
3,824✔
2323
            do {
3,583✔
2324
                if (this.check(TokenKind.As)) {
3,594✔
2325
                    this.warnIfNotBrighterScriptMode('type cast');
11✔
2326
                    // Check if this expression is wrapped in any type casts
2327
                    // allows for multiple casts:
2328
                    // myVal = foo() as dynamic as string
2329

2330
                    asToken = this.advance();
11✔
2331
                    typeToken = this.typeToken();
11✔
2332
                    if (asToken && typeToken) {
11!
2333
                        expression = new TypeCastExpression(expression, asToken, typeToken);
11✔
2334
                    }
2335
                } else {
2336
                    break;
3,583✔
2337
                }
2338

2339
            } while (asToken && typeToken);
22✔
2340
        }
2341
        this._references.expressions.add(expression);
3,824✔
2342
        return expression;
3,824✔
2343
    }
2344

2345
    private anonymousFunction(): Expression {
2346
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
3,859✔
2347
            const func = this.functionDeclaration(true);
72✔
2348
            //if there's an open paren after this, this is an IIFE
2349
            if (this.check(TokenKind.LeftParen)) {
72✔
2350
                return this.finishCall(this.advance(), func);
3✔
2351
            } else {
2352
                return func;
69✔
2353
            }
2354
        }
2355

2356
        let expr = this.boolean();
3,787✔
2357

2358
        if (this.check(TokenKind.Question)) {
3,752✔
2359
            return this.ternaryExpression(expr);
74✔
2360
        } else if (this.check(TokenKind.QuestionQuestion)) {
3,678✔
2361
            return this.nullCoalescingExpression(expr);
27✔
2362
        } else {
2363
            return expr;
3,651✔
2364
        }
2365
    }
2366

2367
    private boolean(): Expression {
2368
        let expr = this.relational();
3,787✔
2369

2370
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
3,752✔
2371
            let operator = this.previous();
28✔
2372
            let right = this.relational();
28✔
2373
            this.addExpressionsToReferences(expr, right);
28✔
2374
            expr = new BinaryExpression(expr, operator, right);
28✔
2375
        }
2376

2377
        return expr;
3,752✔
2378
    }
2379

2380
    private relational(): Expression {
2381
        let expr = this.additive();
3,829✔
2382

2383
        while (
3,794✔
2384
            this.matchAny(
2385
                TokenKind.Equal,
2386
                TokenKind.LessGreater,
2387
                TokenKind.Greater,
2388
                TokenKind.GreaterEqual,
2389
                TokenKind.Less,
2390
                TokenKind.LessEqual
2391
            )
2392
        ) {
2393
            let operator = this.previous();
147✔
2394
            let right = this.additive();
147✔
2395
            this.addExpressionsToReferences(expr, right);
147✔
2396
            expr = new BinaryExpression(expr, operator, right);
147✔
2397
        }
2398

2399
        return expr;
3,794✔
2400
    }
2401

2402
    private addExpressionsToReferences(...expressions: Expression[]) {
2403
        for (const expression of expressions) {
326✔
2404
            if (!isBinaryExpression(expression)) {
608✔
2405
                this.references.expressions.add(expression);
566✔
2406
            }
2407
        }
2408
    }
2409

2410
    // TODO: bitshift
2411

2412
    private additive(): Expression {
2413
        let expr = this.multiplicative();
3,976✔
2414

2415
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
3,941✔
2416
            let operator = this.previous();
80✔
2417
            let right = this.multiplicative();
80✔
2418
            this.addExpressionsToReferences(expr, right);
80✔
2419
            expr = new BinaryExpression(expr, operator, right);
80✔
2420
        }
2421

2422
        return expr;
3,941✔
2423
    }
2424

2425
    private multiplicative(): Expression {
2426
        let expr = this.exponential();
4,056✔
2427

2428
        while (this.matchAny(
4,021✔
2429
            TokenKind.Forwardslash,
2430
            TokenKind.Backslash,
2431
            TokenKind.Star,
2432
            TokenKind.Mod,
2433
            TokenKind.LeftShift,
2434
            TokenKind.RightShift
2435
        )) {
2436
            let operator = this.previous();
21✔
2437
            let right = this.exponential();
21✔
2438
            this.addExpressionsToReferences(expr, right);
21✔
2439
            expr = new BinaryExpression(expr, operator, right);
21✔
2440
        }
2441

2442
        return expr;
4,021✔
2443
    }
2444

2445
    private exponential(): Expression {
2446
        let expr = this.prefixUnary();
4,077✔
2447

2448
        while (this.match(TokenKind.Caret)) {
4,042✔
2449
            let operator = this.previous();
6✔
2450
            let right = this.prefixUnary();
6✔
2451
            this.addExpressionsToReferences(expr, right);
6✔
2452
            expr = new BinaryExpression(expr, operator, right);
6✔
2453
        }
2454

2455
        return expr;
4,042✔
2456
    }
2457

2458
    private prefixUnary(): Expression {
2459
        const nextKind = this.peek().kind;
4,105✔
2460
        if (nextKind === TokenKind.Not) {
4,105✔
2461
            this.current++; //advance
14✔
2462
            let operator = this.previous();
14✔
2463
            let right = this.relational();
14✔
2464
            return new UnaryExpression(operator, right);
14✔
2465
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
4,091✔
2466
            this.current++; //advance
22✔
2467
            let operator = this.previous();
22✔
2468
            let right = this.prefixUnary();
22✔
2469
            return new UnaryExpression(operator, right);
22✔
2470
        }
2471
        return this.call();
4,069✔
2472
    }
2473

2474
    private indexedGet(expr: Expression) {
2475
        let openingSquare = this.previous();
126✔
2476
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
126✔
2477
        let indexes: Expression[] = [];
126✔
2478

2479

2480
        //consume leading newlines
2481
        while (this.match(TokenKind.Newline)) { }
126✔
2482

2483
        try {
126✔
2484
            indexes.push(
126✔
2485
                this.expression()
2486
            );
2487
            //consume additional indexes separated by commas
2488
            while (this.check(TokenKind.Comma)) {
125✔
2489
                //discard the comma
2490
                this.advance();
13✔
2491
                indexes.push(
13✔
2492
                    this.expression()
2493
                );
2494
            }
2495
        } catch (error) {
2496
            this.rethrowNonDiagnosticError(error);
1✔
2497
        }
2498
        //consume trailing newlines
2499
        while (this.match(TokenKind.Newline)) { }
126✔
2500

2501
        const closingSquare = this.tryConsume(
126✔
2502
            DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
2503
            TokenKind.RightSquareBracket
2504
        );
2505

2506
        return new IndexedGetExpression(expr, indexes.shift(), openingSquare, closingSquare, questionDotToken, indexes);
126✔
2507
    }
2508

2509
    private newExpression() {
2510
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
41✔
2511
        let newToken = this.advance();
41✔
2512

2513
        let nameExpr = this.getNamespacedVariableNameExpression();
41✔
2514
        let leftParen = this.consume(
41✔
2515
            DiagnosticMessages.unexpectedToken(this.peek().text),
2516
            TokenKind.LeftParen,
2517
            TokenKind.QuestionLeftParen
2518
        );
2519
        let call = this.finishCall(leftParen, nameExpr);
37✔
2520
        //pop the call from the  callExpressions list because this is technically something else
2521
        this.callExpressions.pop();
37✔
2522
        let result = new NewExpression(newToken, call);
37✔
2523
        this._references.newExpressions.push(result);
37✔
2524
        return result;
37✔
2525
    }
2526

2527
    /**
2528
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2529
     */
2530
    private callfunc(callee: Expression): Expression {
2531
        this.warnIfNotBrighterScriptMode('callfunc operator');
22✔
2532
        let operator = this.previous();
22✔
2533
        let methodName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
22✔
2534
        // force it into an identifier so the AST makes some sense
2535
        methodName.kind = TokenKind.Identifier;
21✔
2536
        let openParen = this.consume(DiagnosticMessages.expectedOpenParenToFollowCallfuncIdentifier(), TokenKind.LeftParen);
21✔
2537
        let call = this.finishCall(openParen, callee, false);
21✔
2538

2539
        return new CallfuncExpression(callee, operator, methodName as Identifier, openParen, call.args, call.closingParen);
21✔
2540
    }
2541

2542
    private call(): Expression {
2543
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
4,675✔
2544
            return this.newExpression();
41✔
2545
        }
2546
        let expr = this.primary();
4,634✔
2547
        //an expression to keep for _references
2548
        let referenceCallExpression: Expression;
2549
        while (true) {
4,570✔
2550
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
6,105✔
2551
                expr = this.finishCall(this.previous(), expr);
492✔
2552
                //store this call expression in references
2553
                referenceCallExpression = expr;
492✔
2554

2555
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
5,613✔
2556
                expr = this.indexedGet(expr);
124✔
2557

2558
            } else if (this.match(TokenKind.Callfunc)) {
5,489✔
2559
                expr = this.callfunc(expr);
22✔
2560
                //store this callfunc expression in references
2561
                referenceCallExpression = expr;
21✔
2562

2563
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
5,467✔
2564
                if (this.match(TokenKind.LeftSquareBracket)) {
920✔
2565
                    expr = this.indexedGet(expr);
2✔
2566
                } else {
2567
                    let dot = this.previous();
918✔
2568
                    let name = this.tryConsume(
918✔
2569
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2570
                        TokenKind.Identifier,
2571
                        ...AllowedProperties
2572
                    );
2573
                    if (!name) {
918✔
2574
                        break;
22✔
2575
                    }
2576

2577
                    // force it into an identifier so the AST makes some sense
2578
                    name.kind = TokenKind.Identifier;
896✔
2579
                    expr = new DottedGetExpression(expr, name as Identifier, dot);
896✔
2580

2581
                    this.addPropertyHints(name);
896✔
2582
                }
2583

2584
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
4,547✔
2585
                let dot = this.advance();
9✔
2586
                let name = this.tryConsume(
9✔
2587
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2588
                    TokenKind.Identifier,
2589
                    ...AllowedProperties
2590
                );
2591

2592
                // force it into an identifier so the AST makes some sense
2593
                name.kind = TokenKind.Identifier;
9✔
2594
                if (!name) {
9!
2595
                    break;
×
2596
                }
2597
                expr = new XmlAttributeGetExpression(expr, name as Identifier, dot);
9✔
2598
                //only allow a single `@` expression
2599
                break;
9✔
2600

2601
            } else {
2602
                break;
4,538✔
2603
            }
2604
        }
2605
        //if we found a callExpression, add it to `expressions` in references
2606
        if (referenceCallExpression) {
4,569✔
2607
            this._references.expressions.add(referenceCallExpression);
476✔
2608
        }
2609
        return expr;
4,569✔
2610
    }
2611

2612
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
532✔
2613
        let args = [] as Expression[];
562✔
2614
        while (this.match(TokenKind.Newline)) { }
562✔
2615

2616
        if (!this.check(TokenKind.RightParen)) {
562✔
2617
            do {
307✔
2618
                while (this.match(TokenKind.Newline)) { }
468✔
2619

2620
                if (args.length >= CallExpression.MaximumArguments) {
468!
2621
                    this.diagnostics.push({
×
2622
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
2623
                        range: this.peek().range
2624
                    });
2625
                    throw this.lastDiagnosticAsError();
×
2626
                }
2627
                try {
468✔
2628
                    args.push(this.expression());
468✔
2629
                } catch (error) {
2630
                    this.rethrowNonDiagnosticError(error);
4✔
2631
                    // we were unable to get an expression, so don't continue
2632
                    break;
4✔
2633
                }
2634
            } while (this.match(TokenKind.Comma));
2635
        }
2636

2637
        while (this.match(TokenKind.Newline)) { }
562✔
2638

2639
        const closingParen = this.tryConsume(
562✔
2640
            DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
2641
            TokenKind.RightParen
2642
        );
2643

2644
        let expression = new CallExpression(callee, openingParen, closingParen, args);
562✔
2645
        if (addToCallExpressionList) {
562✔
2646
            this.callExpressions.push(expression);
532✔
2647
        }
2648
        return expression;
562✔
2649
    }
2650

2651
    /**
2652
     * Tries to get the next token as a type
2653
     * Allows for built-in types (double, string, etc.) or namespaced custom types in Brighterscript mode
2654
     * Will return a token of whatever is next to be parsed
2655
     * Will allow v1 type syntax (typed arrays, union types), but there is no validation on types used this way
2656
     */
2657
    private typeToken(ignoreDiagnostics = false): Token {
541✔
2658
        let typeToken: Token;
2659
        let lookForUnions = true;
544✔
2660
        let isAUnion = false;
544✔
2661
        let resultToken;
2662
        while (lookForUnions) {
544✔
2663
            lookForUnions = false;
551✔
2664

2665
            if (this.checkAny(...DeclarableTypes)) {
551✔
2666
                // Token is a built in type
2667
                typeToken = this.advance();
451✔
2668
            } else if (this.options.mode === ParseMode.BrighterScript) {
100✔
2669
                try {
78✔
2670
                    // see if we can get a namespaced identifer
2671
                    const qualifiedType = this.getNamespacedVariableNameExpression(ignoreDiagnostics);
78✔
2672
                    typeToken = createToken(TokenKind.Identifier, qualifiedType.getName(this.options.mode), qualifiedType.range);
75✔
2673
                } catch {
2674
                    //could not get an identifier - just get whatever's next
2675
                    typeToken = this.advance();
3✔
2676
                }
2677
            } else {
2678
                // just get whatever's next
2679
                typeToken = this.advance();
22✔
2680
            }
2681
            resultToken = resultToken ?? typeToken;
551✔
2682
            if (resultToken && this.options.mode === ParseMode.BrighterScript) {
551✔
2683
                // check for brackets
2684
                while (this.check(TokenKind.LeftSquareBracket) && this.peekNext().kind === TokenKind.RightSquareBracket) {
461✔
2685
                    const leftBracket = this.advance();
10✔
2686
                    const rightBracket = this.advance();
10✔
2687
                    typeToken = createToken(TokenKind.Identifier, typeToken.text + leftBracket.text + rightBracket.text, util.createBoundingRange(typeToken, leftBracket, rightBracket));
10✔
2688
                    resultToken = createToken(TokenKind.Dynamic, null, typeToken.range);
10✔
2689
                }
2690

2691
                if (this.check(TokenKind.Or)) {
461✔
2692
                    lookForUnions = true;
7✔
2693
                    let orToken = this.advance();
7✔
2694
                    resultToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(resultToken, typeToken, orToken));
7✔
2695
                    isAUnion = true;
7✔
2696
                }
2697
            }
2698
        }
2699
        if (isAUnion) {
544✔
2700
            resultToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(resultToken, typeToken));
5✔
2701
        }
2702
        return resultToken;
544✔
2703
    }
2704

2705
    private primary(): Expression {
2706
        switch (true) {
4,634✔
2707
            case this.matchAny(
4,634!
2708
                TokenKind.False,
2709
                TokenKind.True,
2710
                TokenKind.Invalid,
2711
                TokenKind.IntegerLiteral,
2712
                TokenKind.LongIntegerLiteral,
2713
                TokenKind.FloatLiteral,
2714
                TokenKind.DoubleLiteral,
2715
                TokenKind.StringLiteral
2716
            ):
2717
                return new LiteralExpression(this.previous());
2,789✔
2718

2719
            //capture source literals (LINE_NUM if brightscript, or a bunch of them if brighterscript)
2720
            case this.matchAny(TokenKind.LineNumLiteral, ...(this.options.mode === ParseMode.BrightScript ? [] : BrighterScriptSourceLiterals)):
1,845✔
2721
                return new SourceLiteralExpression(this.previous());
34✔
2722

2723
            //template string
2724
            case this.check(TokenKind.BackTick):
2725
                return this.templateString(false);
34✔
2726

2727
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
2728
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
3,113✔
2729
                return this.templateString(true);
5✔
2730

2731
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
2732
                return new VariableExpression(this.previous() as Identifier);
1,338✔
2733

2734
            case this.match(TokenKind.LeftParen):
2735
                let left = this.previous();
32✔
2736
                let expr = this.expression();
32✔
2737
                let right = this.consume(
31✔
2738
                    DiagnosticMessages.unmatchedLeftParenAfterExpression(),
2739
                    TokenKind.RightParen
2740
                );
2741
                return new GroupingExpression({ left: left, right: right }, expr);
31✔
2742

2743
            case this.matchAny(TokenKind.LeftSquareBracket):
2744
                return this.arrayLiteral();
110✔
2745

2746
            case this.match(TokenKind.LeftCurlyBrace):
2747
                return this.aaLiteral();
184✔
2748

2749
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
2750
                let token = Object.assign(this.previous(), {
×
2751
                    kind: TokenKind.Identifier
2752
                }) as Identifier;
2753
                return new VariableExpression(token);
×
2754

2755
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
2756
                return this.anonymousFunction();
×
2757

2758
            case this.check(TokenKind.RegexLiteral):
2759
                return this.regexLiteralExpression();
44✔
2760

2761
            case this.check(TokenKind.Comment):
2762
                return new CommentStatement([this.advance()]);
3✔
2763

2764
            default:
2765
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
2766
                if (this.checkAny(...this.peekGlobalTerminators())) {
61!
2767
                    //don't throw a diagnostic, just return undefined
2768

2769
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
2770
                } else {
2771
                    this.diagnostics.push({
61✔
2772
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
2773
                        range: this.peek().range
2774
                    });
2775
                    throw this.lastDiagnosticAsError();
61✔
2776
                }
2777
        }
2778
    }
2779

2780
    private arrayLiteral() {
2781
        let elements: Array<Expression | CommentStatement> = [];
110✔
2782
        let openingSquare = this.previous();
110✔
2783

2784
        //add any comment found right after the opening square
2785
        if (this.check(TokenKind.Comment)) {
110✔
2786
            elements.push(new CommentStatement([this.advance()]));
1✔
2787
        }
2788

2789
        while (this.match(TokenKind.Newline)) {
110✔
2790
        }
2791
        let closingSquare: Token;
2792

2793
        if (!this.match(TokenKind.RightSquareBracket)) {
110✔
2794
            try {
81✔
2795
                elements.push(this.expression());
81✔
2796

2797
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
80✔
2798
                    if (this.checkPrevious(TokenKind.Comment) || this.check(TokenKind.Comment)) {
102✔
2799
                        let comment = this.check(TokenKind.Comment) ? this.advance() : this.previous();
4✔
2800
                        elements.push(new CommentStatement([comment]));
4✔
2801
                    }
2802
                    while (this.match(TokenKind.Newline)) {
102✔
2803

2804
                    }
2805

2806
                    if (this.check(TokenKind.RightSquareBracket)) {
102✔
2807
                        break;
21✔
2808
                    }
2809

2810
                    elements.push(this.expression());
81✔
2811
                }
2812
            } catch (error: any) {
2813
                this.rethrowNonDiagnosticError(error);
2✔
2814
            }
2815

2816
            closingSquare = this.tryConsume(
81✔
2817
                DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral(),
2818
                TokenKind.RightSquareBracket
2819
            );
2820
        } else {
2821
            closingSquare = this.previous();
29✔
2822
        }
2823

2824
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2825
        return new ArrayLiteralExpression(elements, openingSquare, closingSquare);
110✔
2826
    }
2827

2828
    private aaLiteral() {
2829
        let openingBrace = this.previous();
184✔
2830
        let members: Array<AAMemberExpression | CommentStatement> = [];
184✔
2831

2832
        let key = () => {
184✔
2833
            let result = {
186✔
2834
                colonToken: null as Token,
2835
                keyToken: null as Token,
2836
                range: null as Range
2837
            };
2838
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
186✔
2839
                result.keyToken = this.identifier(...AllowedProperties);
155✔
2840
            } else if (this.check(TokenKind.StringLiteral)) {
31!
2841
                result.keyToken = this.advance();
31✔
2842
            } else {
2843
                this.diagnostics.push({
×
2844
                    ...DiagnosticMessages.unexpectedAAKey(),
2845
                    range: this.peek().range
2846
                });
2847
                throw this.lastDiagnosticAsError();
×
2848
            }
2849

2850
            result.colonToken = this.consume(
186✔
2851
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
2852
                TokenKind.Colon
2853
            );
2854
            result.range = util.getRange(result.keyToken, result.colonToken);
185✔
2855
            return result;
185✔
2856
        };
2857

2858
        while (this.match(TokenKind.Newline)) { }
184✔
2859
        let closingBrace: Token;
2860
        if (!this.match(TokenKind.RightCurlyBrace)) {
184✔
2861
            let lastAAMember: AAMemberExpression;
2862
            try {
139✔
2863
                if (this.check(TokenKind.Comment)) {
139✔
2864
                    lastAAMember = null;
7✔
2865
                    members.push(new CommentStatement([this.advance()]));
7✔
2866
                } else {
2867
                    let k = key();
132✔
2868
                    let expr = this.expression();
132✔
2869
                    lastAAMember = new AAMemberExpression(
131✔
2870
                        k.keyToken,
2871
                        k.colonToken,
2872
                        expr
2873
                    );
2874
                    members.push(lastAAMember);
131✔
2875
                }
2876

2877
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
138✔
2878
                    // collect comma at end of expression
2879
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
179✔
2880
                        lastAAMember.commaToken = this.previous();
32✔
2881
                    }
2882

2883
                    //check for comment at the end of the current line
2884
                    if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
179✔
2885
                        let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
14✔
2886
                        members.push(new CommentStatement([token]));
14✔
2887
                    } else {
2888
                        this.consumeStatementSeparators(true);
165✔
2889

2890
                        //check for a comment on its own line
2891
                        if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
165✔
2892
                            let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
1!
2893
                            lastAAMember = null;
1✔
2894
                            members.push(new CommentStatement([token]));
1✔
2895
                            continue;
1✔
2896
                        }
2897

2898
                        if (this.check(TokenKind.RightCurlyBrace)) {
164✔
2899
                            break;
110✔
2900
                        }
2901
                        let k = key();
54✔
2902
                        let expr = this.expression();
53✔
2903
                        lastAAMember = new AAMemberExpression(
53✔
2904
                            k.keyToken,
2905
                            k.colonToken,
2906
                            expr
2907
                        );
2908
                        members.push(lastAAMember);
53✔
2909
                    }
2910
                }
2911
            } catch (error: any) {
2912
                this.rethrowNonDiagnosticError(error);
2✔
2913
            }
2914

2915
            closingBrace = this.tryConsume(
139✔
2916
                DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral(),
2917
                TokenKind.RightCurlyBrace
2918
            );
2919
        } else {
2920
            closingBrace = this.previous();
45✔
2921
        }
2922

2923
        const aaExpr = new AALiteralExpression(members, openingBrace, closingBrace);
184✔
2924
        this.addPropertyHints(aaExpr);
184✔
2925
        return aaExpr;
184✔
2926
    }
2927

2928
    /**
2929
     * Pop token if we encounter specified token
2930
     */
2931
    private match(tokenKind: TokenKind) {
2932
        if (this.check(tokenKind)) {
18,350✔
2933
            this.current++; //advance
1,426✔
2934
            return true;
1,426✔
2935
        }
2936
        return false;
16,924✔
2937
    }
2938

2939
    /**
2940
     * Pop token if we encounter a token in the specified list
2941
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
2942
     */
2943
    private matchAny(...tokenKinds: TokenKind[]) {
2944
        for (let tokenKind of tokenKinds) {
62,611✔
2945
            if (this.check(tokenKind)) {
182,073✔
2946
                this.current++; //advance
15,744✔
2947
                return true;
15,744✔
2948
            }
2949
        }
2950
        return false;
46,867✔
2951
    }
2952

2953
    /**
2954
     * If the next series of tokens matches the given set of tokens, pop them all
2955
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
2956
     */
2957
    private matchSequence(...tokenKinds: TokenKind[]) {
2958
        const endIndex = this.current + tokenKinds.length;
5,492✔
2959
        for (let i = 0; i < tokenKinds.length; i++) {
5,492✔
2960
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
5,516!
2961
                return false;
5,489✔
2962
            }
2963
        }
2964
        this.current = endIndex;
3✔
2965
        return true;
3✔
2966
    }
2967

2968
    /**
2969
     * Get next token matching a specified list, or fail with an error
2970
     */
2971
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
2972
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
6,069✔
2973
        if (token) {
6,069✔
2974
            return token;
6,054✔
2975
        } else {
2976
            let error = new Error(diagnosticInfo.message);
15✔
2977
            (error as any).isDiagnostic = true;
15✔
2978
            throw error;
15✔
2979
        }
2980
    }
2981

2982
    /**
2983
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
2984
     */
2985
    private consumeTokenIf(tokenKind: TokenKind) {
2986
        if (this.match(tokenKind)) {
243✔
2987
            return this.previous();
11✔
2988
        }
2989
    }
2990

2991
    private consumeToken(tokenKind: TokenKind) {
2992
        return this.consume(
265✔
2993
            DiagnosticMessages.expectedToken(tokenKind),
2994
            tokenKind
2995
        );
2996
    }
2997

2998
    /**
2999
     * Consume, or add a message if not found. But then continue and return undefined
3000
     */
3001
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3002
        const nextKind = this.peek().kind;
9,066✔
3003
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
30,240✔
3004

3005
        if (foundTokenKind) {
9,066✔
3006
            return this.advance();
8,983✔
3007
        }
3008
        this.diagnostics.push({
83✔
3009
            ...diagnostic,
3010
            range: this.peek().range
3011
        });
3012
    }
3013

3014
    private tryConsumeToken(tokenKind: TokenKind) {
3015
        return this.tryConsume(
74✔
3016
            DiagnosticMessages.expectedToken(tokenKind),
3017
            tokenKind
3018
        );
3019
    }
3020

3021
    private consumeStatementSeparators(optional = false) {
3,482✔
3022
        //a comment or EOF mark the end of the statement
3023
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
11,651✔
3024
            return true;
519✔
3025
        }
3026
        let consumed = false;
11,132✔
3027
        //consume any newlines and colons
3028
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
11,132✔
3029
            consumed = true;
9,383✔
3030
        }
3031
        if (!optional && !consumed) {
11,132✔
3032
            this.diagnostics.push({
37✔
3033
                ...DiagnosticMessages.expectedNewlineOrColon(),
3034
                range: this.peek().range
3035
            });
3036
        }
3037
        return consumed;
11,132✔
3038
    }
3039

3040
    private advance(): Token {
3041
        if (!this.isAtEnd()) {
20,477✔
3042
            this.current++;
20,459✔
3043
        }
3044
        return this.previous();
20,477✔
3045
    }
3046

3047
    private checkEndOfStatement(): boolean {
3048
        const nextKind = this.peek().kind;
1,369✔
3049
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
1,369✔
3050
    }
3051

3052
    private checkPrevious(tokenKind: TokenKind): boolean {
3053
        return this.previous()?.kind === tokenKind;
637!
3054
    }
3055

3056
    /**
3057
     * Check that the next token kind is the expected kind
3058
     * @param tokenKind the expected next kind
3059
     * @returns true if the next tokenKind is the expected value
3060
     */
3061
    private check(tokenKind: TokenKind): boolean {
3062
        const nextKind = this.peek().kind;
305,402✔
3063
        if (nextKind === TokenKind.Eof) {
305,402✔
3064
            return false;
7,033✔
3065
        }
3066
        return nextKind === tokenKind;
298,369✔
3067
    }
3068

3069
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3070
        const nextKind = this.peek().kind;
37,899✔
3071
        if (nextKind === TokenKind.Eof) {
37,899✔
3072
            return false;
215✔
3073
        }
3074
        return tokenKinds.includes(nextKind);
37,684✔
3075
    }
3076

3077
    private checkNext(tokenKind: TokenKind): boolean {
3078
        if (this.isAtEnd()) {
4,344!
3079
            return false;
×
3080
        }
3081
        return this.peekNext().kind === tokenKind;
4,344✔
3082
    }
3083

3084
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3085
        if (this.isAtEnd()) {
2,338!
3086
            return false;
×
3087
        }
3088
        const nextKind = this.peekNext().kind;
2,338✔
3089
        return tokenKinds.includes(nextKind);
2,338✔
3090
    }
3091

3092
    private isAtEnd(): boolean {
3093
        return this.peek().kind === TokenKind.Eof;
56,419✔
3094
    }
3095

3096
    private peekNext(): Token {
3097
        if (this.isAtEnd()) {
6,692!
3098
            return this.peek();
×
3099
        }
3100
        return this.tokens[this.current + 1];
6,692✔
3101
    }
3102

3103
    private peek(): Token {
3104
        return this.tokens[this.current];
419,121✔
3105
    }
3106

3107
    private previous(): Token {
3108
        return this.tokens[this.current - 1];
29,286✔
3109
    }
3110

3111
    /**
3112
     * Sometimes we catch an error that is a diagnostic.
3113
     * If that's the case, we want to continue parsing.
3114
     * Otherwise, re-throw the error
3115
     *
3116
     * @param error error caught in a try/catch
3117
     */
3118
    private rethrowNonDiagnosticError(error) {
3119
        if (!error.isDiagnostic) {
9!
3120
            throw error;
×
3121
        }
3122
    }
3123

3124
    /**
3125
     * Get the token that is {offset} indexes away from {this.current}
3126
     * @param offset the number of index steps away from current index to fetch
3127
     * @param tokenKinds the desired token must match one of these
3128
     * @example
3129
     * getToken(-1); //returns the previous token.
3130
     * getToken(0);  //returns current token.
3131
     * getToken(1);  //returns next token
3132
     */
3133
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3134
        const token = this.tokens[this.current + offset];
126✔
3135
        if (tokenKinds.includes(token.kind)) {
126✔
3136
            return token;
3✔
3137
        }
3138
    }
3139

3140
    private synchronize() {
3141
        this.advance(); // skip the erroneous token
123✔
3142

3143
        while (!this.isAtEnd()) {
123✔
3144
            if (this.ensureNewLineOrColon(true)) {
203✔
3145
                // end of statement reached
3146
                return;
89✔
3147
            }
3148

3149
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
114✔
3150
                case TokenKind.Namespace:
8!
3151
                case TokenKind.Class:
3152
                case TokenKind.Function:
3153
                case TokenKind.Sub:
3154
                case TokenKind.If:
3155
                case TokenKind.For:
3156
                case TokenKind.ForEach:
3157
                case TokenKind.While:
3158
                case TokenKind.Print:
3159
                case TokenKind.Return:
3160
                    // start parsing again from the next block starter or obvious
3161
                    // expression start
3162
                    return;
1✔
3163
            }
3164

3165
            this.advance();
113✔
3166
        }
3167
    }
3168

3169
    /**
3170
     * References are found during the initial parse.
3171
     * However, sometimes plugins can modify the AST, requiring a full walk to re-compute all references.
3172
     * This does that walk.
3173
     */
3174
    private findReferences() {
3175
        this._references = new References();
7✔
3176
        const excludedExpressions = new Set<Expression>();
7✔
3177

3178
        const visitCallExpression = (e: CallExpression | CallfuncExpression) => {
7✔
3179
            for (const p of e.args) {
14✔
3180
                this._references.expressions.add(p);
7✔
3181
            }
3182
            //add calls that were not excluded (from loop below)
3183
            if (!excludedExpressions.has(e)) {
14✔
3184
                this._references.expressions.add(e);
12✔
3185
            }
3186

3187
            //if this call is part of a longer expression that includes a call higher up, find that higher one and remove it
3188
            if (e.callee) {
14!
3189
                let node: Expression = e.callee;
14✔
3190
                while (node) {
14✔
3191
                    //the primary goal for this loop. If we found a parent call expression, remove it from `references`
3192
                    if (isCallExpression(node)) {
22✔
3193
                        this.references.expressions.delete(node);
2✔
3194
                        excludedExpressions.add(node);
2✔
3195
                        //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.
3196
                        break;
2✔
3197

3198
                        //when we hit a variable expression, we're definitely at the leftmost expression so stop
3199
                    } else if (isVariableExpression(node)) {
20✔
3200
                        break;
12✔
3201
                        //if
3202

3203
                    } else if (isDottedGetExpression(node) || isIndexedGetExpression(node)) {
8!
3204
                        node = node.obj;
8✔
3205
                    } else {
3206
                        //some expression we don't understand. log it and quit the loop
3207
                        this.logger.info('Encountered unknown expression while calculating function expression chain', node);
×
3208
                        break;
×
3209
                    }
3210
                }
3211
            }
3212
        };
3213

3214
        this.ast.walk(createVisitor({
7✔
3215
            AssignmentStatement: s => {
3216
                this._references.assignmentStatements.push(s);
11✔
3217
                this.references.expressions.add(s.value);
11✔
3218
            },
3219
            ClassStatement: s => {
3220
                this._references.classStatements.push(s);
1✔
3221
            },
3222
            ClassFieldStatement: s => {
3223
                if (s.initialValue) {
1!
3224
                    this._references.expressions.add(s.initialValue);
1✔
3225
                }
3226
            },
3227
            NamespaceStatement: s => {
3228
                this._references.namespaceStatements.push(s);
×
3229
            },
3230
            FunctionStatement: s => {
3231
                this._references.functionStatements.push(s);
4✔
3232
            },
3233
            ImportStatement: s => {
3234
                this._references.importStatements.push(s);
1✔
3235
            },
3236
            LibraryStatement: s => {
3237
                this._references.libraryStatements.push(s);
×
3238
            },
3239
            FunctionExpression: (expression, parent) => {
3240
                if (!isMethodStatement(parent)) {
4!
3241
                    this._references.functionExpressions.push(expression);
4✔
3242
                }
3243
            },
3244
            NewExpression: e => {
3245
                this._references.newExpressions.push(e);
×
3246
                for (const p of e.call.args) {
×
3247
                    this._references.expressions.add(p);
×
3248
                }
3249
            },
3250
            ExpressionStatement: s => {
3251
                this._references.expressions.add(s.expression);
7✔
3252
            },
3253
            CallfuncExpression: e => {
3254
                visitCallExpression(e);
1✔
3255
            },
3256
            CallExpression: e => {
3257
                visitCallExpression(e);
13✔
3258
            },
3259
            AALiteralExpression: e => {
3260
                this.addPropertyHints(e);
8✔
3261
                this._references.expressions.add(e);
8✔
3262
                for (const member of e.elements) {
8✔
3263
                    if (isAAMemberExpression(member)) {
16!
3264
                        this._references.expressions.add(member.value);
16✔
3265
                    }
3266
                }
3267
            },
3268
            BinaryExpression: (e, parent) => {
3269
                //walk the chain of binary expressions and add each one to the list of expressions
3270
                const expressions: Expression[] = [e];
14✔
3271
                let expression: Expression;
3272
                while ((expression = expressions.pop())) {
14✔
3273
                    if (isBinaryExpression(expression)) {
64✔
3274
                        expressions.push(expression.left, expression.right);
25✔
3275
                    } else {
3276
                        this._references.expressions.add(expression);
39✔
3277
                    }
3278
                }
3279
            },
3280
            ArrayLiteralExpression: e => {
3281
                for (const element of e.elements) {
1✔
3282
                    //keep everything except comments
3283
                    if (!isCommentStatement(element)) {
1!
3284
                        this._references.expressions.add(element);
1✔
3285
                    }
3286
                }
3287
            },
3288
            DottedGetExpression: e => {
3289
                this.addPropertyHints(e.name);
23✔
3290
            },
3291
            DottedSetStatement: e => {
3292
                this.addPropertyHints(e.name);
4✔
3293
            },
3294
            EnumStatement: e => {
3295
                this._references.enumStatements.push(e);
×
3296
            },
3297
            ConstStatement: s => {
3298
                this._references.constStatements.push(s);
×
3299
            },
3300
            UnaryExpression: e => {
3301
                this._references.expressions.add(e);
×
3302
            },
3303
            IncrementStatement: e => {
3304
                this._references.expressions.add(e);
2✔
3305
            }
3306
        }), {
3307
            walkMode: WalkMode.visitAllRecursive
3308
        });
3309
    }
3310

3311
    public dispose() {
3312
    }
3313
}
3314

3315
export enum ParseMode {
1✔
3316
    BrightScript = 'BrightScript',
1✔
3317
    BrighterScript = 'BrighterScript'
1✔
3318
}
3319

3320
export interface ParseOptions {
3321
    /**
3322
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3323
     */
3324
    mode?: ParseMode;
3325
    /**
3326
     * A logger that should be used for logging. If omitted, a default logger is used
3327
     */
3328
    logger?: Logger;
3329
    /**
3330
     * Should locations be tracked. If false, the `range` property will be omitted
3331
     * @default true
3332
     */
3333
    trackLocations?: boolean;
3334
}
3335

3336
export class References {
1✔
3337
    private cache = new Cache();
1,838✔
3338
    public assignmentStatements = [] as AssignmentStatement[];
1,838✔
3339
    public classStatements = [] as ClassStatement[];
1,838✔
3340

3341
    public get classStatementLookup() {
3342
        if (!this._classStatementLookup) {
17✔
3343
            this._classStatementLookup = new Map();
15✔
3344
            for (const stmt of this.classStatements) {
15✔
3345
                this._classStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
2✔
3346
            }
3347
        }
3348
        return this._classStatementLookup;
17✔
3349
    }
3350
    private _classStatementLookup: Map<string, ClassStatement>;
3351

3352
    public functionExpressions = [] as FunctionExpression[];
1,838✔
3353
    public functionStatements = [] as FunctionStatement[];
1,838✔
3354
    /**
3355
     * A map of function statements, indexed by fully-namespaced lower function name.
3356
     */
3357
    public get functionStatementLookup() {
3358
        if (!this._functionStatementLookup) {
17✔
3359
            this._functionStatementLookup = new Map();
15✔
3360
            for (const stmt of this.functionStatements) {
15✔
3361
                this._functionStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
13✔
3362
            }
3363
        }
3364
        return this._functionStatementLookup;
17✔
3365
    }
3366
    private _functionStatementLookup: Map<string, FunctionStatement>;
3367

3368
    public interfaceStatements = [] as InterfaceStatement[];
1,838✔
3369

3370
    public get interfaceStatementLookup() {
3371
        if (!this._interfaceStatementLookup) {
×
3372
            this._interfaceStatementLookup = new Map();
×
3373
            for (const stmt of this.interfaceStatements) {
×
3374
                this._interfaceStatementLookup.set(stmt.fullName.toLowerCase(), stmt);
×
3375
            }
3376
        }
3377
        return this._interfaceStatementLookup;
×
3378
    }
3379
    private _interfaceStatementLookup: Map<string, InterfaceStatement>;
3380

3381
    public enumStatements = [] as EnumStatement[];
1,838✔
3382

3383
    public get enumStatementLookup() {
3384
        return this.cache.getOrAdd('enums', () => {
18✔
3385
            const result = new Map<string, EnumStatement>();
16✔
3386
            for (const stmt of this.enumStatements) {
16✔
3387
                result.set(stmt.fullName.toLowerCase(), stmt);
1✔
3388
            }
3389
            return result;
16✔
3390
        });
3391
    }
3392

3393
    public constStatements = [] as ConstStatement[];
1,838✔
3394

3395
    public get constStatementLookup() {
3396
        return this.cache.getOrAdd('consts', () => {
×
3397
            const result = new Map<string, ConstStatement>();
×
3398
            for (const stmt of this.constStatements) {
×
3399
                result.set(stmt.fullName.toLowerCase(), stmt);
×
3400
            }
3401
            return result;
×
3402
        });
3403
    }
3404

3405
    /**
3406
     * A collection of full expressions. This excludes intermediary expressions.
3407
     *
3408
     * Example 1:
3409
     * `a.b.c` is composed of `a` (variableExpression)  `.b` (DottedGetExpression) `.c` (DottedGetExpression)
3410
     * This will only contain the final `.c` DottedGetExpression because `.b` and `a` can both be derived by walking back from the `.c` DottedGetExpression.
3411
     *
3412
     * Example 2:
3413
     * `name.space.doSomething(a.b.c)` will result in 2 entries in this list. the `CallExpression` for `doSomething`, and the `.c` DottedGetExpression.
3414
     *
3415
     * Example 3:
3416
     * `value = SomeEnum.value > 2 or SomeEnum.otherValue < 10` will result in 4 entries. `SomeEnum.value`, `2`, `SomeEnum.otherValue`, `10`
3417
     */
3418
    public expressions = new Set<Expression>();
1,838✔
3419

3420
    public importStatements = [] as ImportStatement[];
1,838✔
3421
    public libraryStatements = [] as LibraryStatement[];
1,838✔
3422
    public namespaceStatements = [] as NamespaceStatement[];
1,838✔
3423
    public newExpressions = [] as NewExpression[];
1,838✔
3424
    public propertyHints = {} as Record<string, string>;
1,838✔
3425
}
3426

3427
class CancelStatementError extends Error {
3428
    constructor() {
3429
        super('CancelStatement');
2✔
3430
    }
3431
}
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