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

rokucommunity / brighterscript / #13835

26 Mar 2024 07:42PM UTC coverage: 87.41% (-0.5%) from 87.93%
#13835

push

TwitchBronBron
Bump to node16 so we can use ts-node in unit tests

6199 of 7518 branches covered (82.46%)

Branch coverage included in aggregate %.

8923 of 9782 relevant lines covered (91.22%)

1665.99 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 { Logger } from '../Logger';
1✔
93
import { isAAMemberExpression, isAnnotationExpression, isBinaryExpression, isCallExpression, isCallfuncExpression, isMethodStatement, isCommentStatement, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression } from '../astUtils/reflection';
1✔
94
import { createVisitor, WalkMode } from '../astUtils/visitors';
1✔
95
import { createStringLiteral, createToken } from '../astUtils/creators';
1✔
96
import { Cache } from '../Cache';
1✔
97
import type { Expression, Statement } from './AstNode';
98
import { SymbolTable } from '../SymbolTable';
1✔
99
import type { BscType } from '../types/BscType';
100

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

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

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

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

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

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

142
    private _references = new References();
1,796✔
143

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

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

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

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

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

182
    private globalTerminators = [] as TokenKind[][];
1,796✔
183

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

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

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

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

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

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

236
        this.ast = this.body();
1,781✔
237

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

243
    private logger: Logger;
244

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

499
                let decl: Statement;
500

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

623
        const parentAnnotations = this.enterAnnotationBlock();
448✔
624

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

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

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

641
        //ensure statement separator
642
        this.consumeStatementSeparators();
447✔
643

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

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

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

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

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

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

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

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

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

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

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

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

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

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

717
            //ensure statement separator
718
            this.consumeStatementSeparators();
435✔
719
        }
720

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

914
                return haveFoundOptional || !!param.defaultValue;
598✔
915
            }, false);
916

917
            this.consumeStatementSeparators(true);
1,435✔
918

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

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

936
            this._references.functionExpressions.push(func);
1,435✔
937

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

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

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

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

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

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

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

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

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

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

1007
        let asToken = null;
601✔
1008
        if (this.check(TokenKind.As)) {
601✔
1009
            asToken = this.advance();
265✔
1010

1011
            typeToken = this.typeToken();
265✔
1012

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

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

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

1045
        let operator = this.consume(
850✔
1046
            DiagnosticMessages.expectedOperatorAfterIdentifier(AssignmentOperators, name.text),
1047
            ...AssignmentOperators
1048
        );
1049
        let value = this.expression();
849✔
1050

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

1069
        this._references.assignmentStatements.push(result);
842✔
1070
        return result;
842✔
1071
    }
1072

1073
    private checkLibrary() {
1074
        let isLibraryToken = this.check(TokenKind.Library);
6,795✔
1075

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

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

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

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

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

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

1104
        if (this.check(TokenKind.If)) {
3,229✔
1105
            return this.ifStatement();
127✔
1106
        }
1107

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

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

1117
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
3,069✔
1118
            return this.printStatement();
581✔
1119
        }
1120
        if (this.check(TokenKind.Dim)) {
2,488✔
1121
            return this.dimStatement();
40✔
1122
        }
1123

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

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

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

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

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

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

1148
        if (this.match(TokenKind.Return)) {
2,366✔
1149
            return this.returnStatement();
115✔
1150
        }
1151

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

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

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

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

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

1211
        if (this.check(TokenKind.Class)) {
1,356✔
1212
            return this.classDeclaration();
448✔
1213
        }
1214

1215
        if (this.check(TokenKind.Namespace)) {
908✔
1216
            return this.namespaceStatement();
226✔
1217
        }
1218

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

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

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

1231
        this.consumeStatementSeparators();
18✔
1232

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

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

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

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

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

1264
        //TODO: newline allowed?
1265

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

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

1278
        this.consumeStatementSeparators();
28✔
1279

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

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

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

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

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

1332
        this.consumeStatementSeparators();
18✔
1333

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

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

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

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

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

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

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

1383
        this.namespaceAndFunctionDepth++;
226✔
1384

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

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

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

1404
        this.namespaceAndFunctionDepth--;
225✔
1405

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1797
        const ifToken = this.advance();
170✔
1798
        const startingRange = ifToken.range;
170✔
1799

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

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

1808
        //optional `then`
1809
        if (this.check(TokenKind.Then)) {
168✔
1810
            thenToken = this.advance();
108✔
1811
        }
1812

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

1816
        if (isInlineIfThen) {
168✔
1817
            /*** PARSE INLINE IF STATEMENT ***/
1818

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

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

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

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

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

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

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

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

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

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

1892
            thenBranch = this.blockConditionalBranch(ifToken);
136✔
1893

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

1897
            //else branch
1898
            if (this.check(TokenKind.Else)) {
134✔
1899
                elseToken = this.advance();
73✔
1900

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

1905
                } else {
1906
                    elseBranch = this.blockConditionalBranch(ifToken);
34✔
1907

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

1913
            if (!isIfStatement(elseBranch)) {
134✔
1914
                if (this.check(TokenKind.EndIf)) {
95✔
1915
                    endIfToken = this.advance();
93✔
1916

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2057
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
260✔
2058
            return new ExpressionStatement(expr);
197✔
2059
        }
2060

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

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

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

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

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

2116
        while (!this.checkEndOfStatement()) {
581✔
2117
            if (this.check(TokenKind.Semicolon)) {
665✔
2118
                values.push(this.advance() as PrintSeparatorSpace);
20✔
2119
            } else if (this.check(TokenKind.Comma)) {
645✔
2120
                values.push(this.advance() as PrintSeparatorTab);
13✔
2121
            } else if (this.check(TokenKind.Else)) {
632✔
2122
                break; // inline branch
7✔
2123
            } else {
2124
                values.push(this.expression());
625✔
2125
            }
2126
        }
2127

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

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

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

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

2149
        if (this.checkEndOfStatement()) {
115✔
2150
            return new ReturnStatement(tokens);
4✔
2151
        }
2152

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

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

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

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

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

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

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

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

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

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

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

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

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

2248
                //ensure statement separator
2249
                this.consumeStatementSeparators();
1,968✔
2250

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

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

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

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

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

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

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

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

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

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

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

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

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

2355
        let expr = this.boolean();
3,710✔
2356

2357
        if (this.check(TokenKind.Question)) {
3,675✔
2358
            return this.ternaryExpression(expr);
74✔
2359
        } else if (this.check(TokenKind.QuestionQuestion)) {
3,601✔
2360
            return this.nullCoalescingExpression(expr);
27✔
2361
        } else {
2362
            return expr;
3,574✔
2363
        }
2364
    }
2365

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

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

2376
        return expr;
3,675✔
2377
    }
2378

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

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

2398
        return expr;
3,717✔
2399
    }
2400

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

2409
    // TODO: bitshift
2410

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

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

2421
        return expr;
3,864✔
2422
    }
2423

2424
    private multiplicative(): Expression {
2425
        let expr = this.exponential();
3,979✔
2426

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

2441
        return expr;
3,944✔
2442
    }
2443

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

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

2454
        return expr;
3,965✔
2455
    }
2456

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

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

2478

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

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

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

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

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

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

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

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

2541
    private call(): Expression {
2542
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
4,568✔
2543
            return this.newExpression();
40✔
2544
        }
2545
        let expr = this.primary();
4,528✔
2546
        //an expression to keep for _references
2547
        let referenceCallExpression: Expression;
2548
        while (true) {
4,462✔
2549
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
5,944✔
2550
                expr = this.finishCall(this.previous(), expr);
459✔
2551
                //store this call expression in references
2552
                referenceCallExpression = expr;
459✔
2553

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

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

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

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

2580
                    this.addPropertyHints(name);
877✔
2581
                }
2582

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

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

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

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

2615
        if (!this.check(TokenKind.RightParen)) {
528✔
2616
            do {
299✔
2617
                while (this.match(TokenKind.Newline)) { }
455✔
2618

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

2636
        while (this.match(TokenKind.Newline)) { }
528✔
2637

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2803
                    }
2804

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2938
    /**
2939
     * Pop token if we encounter a token in the specified list
2940
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
2941
     */
2942
    private matchAny(...tokenKinds: TokenKind[]) {
2943
        for (let tokenKind of tokenKinds) {
61,058✔
2944
            if (this.check(tokenKind)) {
178,416✔
2945
                this.current++; //advance
15,279✔
2946
                return true;
15,279✔
2947
            }
2948
        }
2949
        return false;
45,779✔
2950
    }
2951

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

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

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

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

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

3004
        if (foundTokenKind) {
8,883✔
3005
            return this.advance();
8,798✔
3006
        }
3007
        this.diagnostics.push({
85✔
3008
            ...diagnostic,
3009
            range: this.peek().range
3010
        });
3011
    }
3012

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

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

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

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

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

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

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

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

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

3091
    private isAtEnd(): boolean {
3092
        return this.peek().kind === TokenKind.Eof;
54,900✔
3093
    }
3094

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

3102
    private peek(): Token {
3103
        return this.tokens[this.current];
409,135✔
3104
    }
3105

3106
    private previous(): Token {
3107
        return this.tokens[this.current - 1];
28,571✔
3108
    }
3109

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

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

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

3142
        while (!this.isAtEnd()) {
127✔
3143
            if (this.ensureNewLineOrColon(true)) {
205✔
3144
                // end of statement reached
3145
                return;
91✔
3146
            }
3147

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

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

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

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

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

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

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

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

3310
    public dispose() {
3311
    }
3312
}
3313

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

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

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

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

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

3367
    public interfaceStatements = [] as InterfaceStatement[];
1,803✔
3368

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

3380
    public enumStatements = [] as EnumStatement[];
1,803✔
3381

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

3392
    public constStatements = [] as ConstStatement[];
1,803✔
3393

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

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

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

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