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

rokucommunity / brighterscript / #14030

17 Mar 2025 03:35PM UTC coverage: 89.102% (-0.02%) from 89.12%
#14030

push

web-flow
Adds Alias statement syntax from v1 to v0 (#1430)

7498 of 8869 branches covered (84.54%)

Branch coverage included in aggregate %.

21 of 24 new or added lines in 5 files covered. (87.5%)

1 existing line in 1 file now uncovered.

9844 of 10594 relevant lines covered (92.92%)

1835.67 hits per line

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

92.82
/src/parser/Parser.ts
1
import type { Token, Identifier } from '../lexer/Token';
2
import { isToken } from '../lexer/Token';
1✔
3
import type { BlockTerminator } from '../lexer/TokenKind';
4
import { Lexer } from '../lexer/Lexer';
1✔
5
import {
1✔
6
    AllowedLocalIdentifiers,
7
    AllowedProperties,
8
    AssignmentOperators,
9
    BrighterScriptSourceLiterals,
10
    DeclarableTypes,
11
    DisallowedFunctionIdentifiersText,
12
    DisallowedLocalIdentifiersText,
13
    TokenKind
14
} from '../lexer/TokenKind';
15
import type {
16
    PrintSeparatorSpace,
17
    PrintSeparatorTab
18
} from './Statement';
19
import {
1✔
20
    AssignmentStatement,
21
    Block,
22
    Body,
23
    CatchStatement,
24
    ContinueStatement,
25
    ClassStatement,
26
    ConstStatement,
27
    CommentStatement,
28
    DimStatement,
29
    DottedSetStatement,
30
    EndStatement,
31
    EnumMemberStatement,
32
    EnumStatement,
33
    ExitForStatement,
34
    ExitWhileStatement,
35
    ExpressionStatement,
36
    FieldStatement,
37
    ForEachStatement,
38
    ForStatement,
39
    FunctionStatement,
40
    GotoStatement,
41
    IfStatement,
42
    ImportStatement,
43
    IncrementStatement,
44
    IndexedSetStatement,
45
    InterfaceFieldStatement,
46
    InterfaceMethodStatement,
47
    InterfaceStatement,
48
    LabelStatement,
49
    LibraryStatement,
50
    MethodStatement,
51
    NamespaceStatement,
52
    PrintStatement,
53
    ReturnStatement,
54
    StopStatement,
55
    ThrowStatement,
56
    TryCatchStatement,
57
    WhileStatement,
58
    TypecastStatement,
59
    AliasStatement
60
} from './Statement';
61
import type { DiagnosticInfo } from '../DiagnosticMessages';
62
import { DiagnosticMessages } from '../DiagnosticMessages';
1✔
63
import { util } from '../util';
1✔
64
import {
1✔
65
    AALiteralExpression,
66
    AAMemberExpression,
67
    AnnotationExpression,
68
    ArrayLiteralExpression,
69
    BinaryExpression,
70
    CallExpression,
71
    CallfuncExpression,
72
    DottedGetExpression,
73
    EscapedCharCodeLiteralExpression,
74
    FunctionExpression,
75
    FunctionParameterExpression,
76
    GroupingExpression,
77
    IndexedGetExpression,
78
    LiteralExpression,
79
    NamespacedVariableNameExpression,
80
    NewExpression,
81
    NullCoalescingExpression,
82
    RegexLiteralExpression,
83
    SourceLiteralExpression,
84
    TaggedTemplateStringExpression,
85
    TemplateStringExpression,
86
    TemplateStringQuasiExpression,
87
    TernaryExpression,
88
    TypeCastExpression,
89
    UnaryExpression,
90
    VariableExpression,
91
    XmlAttributeGetExpression
92
} from './Expression';
93
import type { Diagnostic, Range } from 'vscode-languageserver';
94
import type { Logger } from '../logging';
95
import { createLogger } from '../logging';
1✔
96
import { isAAMemberExpression, isAnnotationExpression, isBinaryExpression, isCallExpression, isCallfuncExpression, isMethodStatement, isCommentStatement, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression } from '../astUtils/reflection';
1✔
97
import { createVisitor, WalkMode } from '../astUtils/visitors';
1✔
98
import { createStringLiteral, createToken } from '../astUtils/creators';
1✔
99
import { Cache } from '../Cache';
1✔
100
import type { Expression, Statement } from './AstNode';
101
import { SymbolTable } from '../SymbolTable';
1✔
102
import type { BscType } from '../types/BscType';
103

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

110
    /**
111
     * The current token index
112
     */
113
    public current: number;
114

115
    /**
116
     * The list of statements for the parsed file
117
     */
118
    public ast = new Body([]);
2,128✔
119

120
    public get statements() {
121
        return this.ast.statements;
496✔
122
    }
123

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

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

145
    private _references = new References();
2,128✔
146

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

154
    private addPropertyHints(item: Token | AALiteralExpression) {
155
        if (isToken(item)) {
1,174✔
156
            const name = item.text;
972✔
157
            this._references.propertyHints[name.toLowerCase()] = name;
972✔
158
        } else {
159
            for (const member of item.elements) {
202✔
160
                if (!isCommentStatement(member)) {
230✔
161
                    const name = member.keyToken.text;
208✔
162
                    if (!name.startsWith('"')) {
208✔
163
                        this._references.propertyHints[name.toLowerCase()] = name;
176✔
164
                    }
165
                }
166
            }
167
        }
168
    }
169

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

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

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

185
    private globalTerminators = [] as TokenKind[][];
2,128✔
186

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

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

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

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

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

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

239
        this.ast = this.body();
2,113✔
240

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

246
    private logger: Logger;
247

248
    private body() {
249
        const parentAnnotations = this.enterAnnotationBlock();
2,355✔
250

251
        let body = new Body([]);
2,355✔
252
        if (this.tokens.length > 0) {
2,355✔
253
            this.consumeStatementSeparators(true);
2,354✔
254

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

280
        this.exitAnnotationBlock(parentAnnotations);
2,355✔
281
        return body;
2,355✔
282
    }
283

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

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

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

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

320
    private declaration(): Statement | AnnotationExpression | undefined {
321
        try {
5,394✔
322
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
5,394✔
323
                return this.functionDeclaration(false);
1,385✔
324
            }
325

326
            if (this.checkLibrary()) {
4,009✔
327
                return this.libraryStatement();
14✔
328
            }
329

330
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
3,995✔
331
                return this.constDeclaration();
54✔
332
            }
333

334
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
3,941✔
335
                return this.annotationExpression();
44✔
336
            }
337

338
            if (this.check(TokenKind.Comment)) {
3,897✔
339
                return this.commentStatement();
212✔
340
            }
341

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

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

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

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

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

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

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

421
        return new InterfaceFieldStatement(name, asToken, typeToken, type, optionalKeyword);
54✔
422
    }
423

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

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

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

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

474
    private interfaceDeclaration(): InterfaceStatement {
475
        this.warnIfNotBrighterScriptMode('interface declarations');
52✔
476

477
        const parentAnnotations = this.enterAnnotationBlock();
52✔
478

479
        const interfaceToken = this.consume(
52✔
480
            DiagnosticMessages.expectedKeyword(TokenKind.Interface),
481
            TokenKind.Interface
482
        );
483
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
52✔
484

485
        let extendsToken: Token;
486
        let parentInterfaceName: NamespacedVariableNameExpression;
487

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

502
                let decl: Statement;
503

504
                //collect leading annotations
505
                if (this.check(TokenKind.At)) {
79✔
506
                    this.annotationExpression();
2✔
507
                }
508

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

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

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

539
            //ensure statement separator
540
            this.consumeStatementSeparators();
79✔
541
        }
542

543
        //consume the final `end interface` token
544
        const endInterfaceToken = this.consumeToken(TokenKind.EndInterface);
52✔
545

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

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

563
        const parentAnnotations = this.enterAnnotationBlock();
115✔
564

565
        result.tokens.enum = this.consume(
115✔
566
            DiagnosticMessages.expectedKeyword(TokenKind.Enum),
567
            TokenKind.Enum
568
        );
569

570
        result.tokens.name = this.tryIdentifier(...this.allowedLocalIdentifiers);
115✔
571

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

578
                //collect leading annotations
579
                if (this.check(TokenKind.At)) {
205!
580
                    this.annotationExpression();
×
581
                }
582

583
                //members
584
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
205✔
585
                    decl = this.enumMemberStatement();
199✔
586

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

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

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

612
        //consume the final `end interface` token
613
        result.tokens.endEnum = this.consumeToken(TokenKind.EndEnum);
115✔
614

615
        this._references.enumStatements.push(result);
115✔
616
        this.exitAnnotationBlock(parentAnnotations);
115✔
617
        return result;
115✔
618
    }
619

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

626
        const parentAnnotations = this.enterAnnotationBlock();
483✔
627

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

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

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

644
        //ensure statement separator
645
        this.consumeStatementSeparators();
482✔
646

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

654
                if (this.check(TokenKind.At)) {
470✔
655
                    this.annotationExpression();
15✔
656
                }
657

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

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

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

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

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

683
                    decl = new MethodStatement(
265✔
684
                        accessModifier,
685
                        funcDeclaration.name,
686
                        funcDeclaration.func,
687
                        overrideKeyword
688
                    );
689

690
                    //refer to this statement as parent of the expression
691
                    functionStatement.func.functionStatement = decl as MethodStatement;
265✔
692

693
                    //fields
694
                } else if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
204✔
695

696
                    decl = this.fieldDeclaration(accessModifier);
182✔
697

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

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

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

720
            //ensure statement separator
721
            this.consumeStatementSeparators();
470✔
722
        }
723

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

732
        const result = new ClassStatement(
482✔
733
            classKeyword,
734
            className,
735
            body,
736
            endingKeyword,
737
            extendsKeyword,
738
            parentClassName
739
        );
740

741
        this._references.classStatements.push(result);
482✔
742
        this.exitAnnotationBlock(parentAnnotations);
482✔
743
        return result;
482✔
744
    }
745

746
    private fieldDeclaration(accessModifier: Token | null) {
747

748
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
182✔
749

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

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

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

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

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

813
    /**
814
     * An array of CallExpression for the current function body
815
     */
816
    private callExpressions = [];
2,128✔
817

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

851
            if (isAnonymous) {
1,723✔
852
                leftParen = this.consume(
73✔
853
                    DiagnosticMessages.expectedLeftParenAfterCallable(functionTypeText),
854
                    TokenKind.LeftParen
855
                );
856
            } else {
857
                name = this.consume(
1,650✔
858
                    DiagnosticMessages.expectedNameAfterCallableKeyword(functionTypeText),
859
                    TokenKind.Identifier,
860
                    ...AllowedProperties
861
                ) as Identifier;
862
                leftParen = this.consume(
1,648✔
863
                    DiagnosticMessages.expectedLeftParenAfterCallableName(functionTypeText),
864
                    TokenKind.LeftParen
865
                );
866

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1048
        let operator = this.consume(
920✔
1049
            DiagnosticMessages.expectedOperatorAfterIdentifier(AssignmentOperators, name.text),
1050
            ...AssignmentOperators
1051
        );
1052
        let value = this.expression();
919✔
1053

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

1072
        this._references.assignmentStatements.push(result);
912✔
1073
        return result;
912✔
1074
    }
1075

1076
    private checkLibrary() {
1077
        let isLibraryToken = this.check(TokenKind.Library);
7,748✔
1078

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

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

1088
            //definitely not a library statement
1089
        } else {
1090
            return false;
7,734✔
1091
        }
1092
    }
1093

1094
    private checkAlias() {
1095
        let isAliasToken = this.check(TokenKind.Alias);
3,696✔
1096

1097
        //if we are at the top level, any line that starts with "alias" should be considered a alias statement
1098
        if (this.isAtRootLevel() && isAliasToken) {
3,696✔
1099
            return true;
2✔
1100

1101
            //not at root level, alias statements are all invalid here, but try to detect if the tokens look
1102
            //like a alias statement (and let the alias function handle emitting the diagnostics)
1103
        } else if (isAliasToken && this.checkNext(TokenKind.Identifier)) {
3,694!
NEW
1104
            return true;
×
1105

1106
            //definitely not a alias statement
1107
        } else {
1108
            return false;
3,694✔
1109
        }
1110
    }
1111

1112
    private statement(): Statement | undefined {
1113
        if (this.checkLibrary()) {
3,739!
1114
            return this.libraryStatement();
×
1115
        }
1116

1117
        if (this.check(TokenKind.Import)) {
3,739✔
1118
            return this.importStatement();
38✔
1119
        }
1120

1121
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
3,701✔
1122
            return this.typecastStatement();
5✔
1123
        }
1124

1125
        if (this.checkAlias()) {
3,696✔
1126
            return this.aliasStatement();
2✔
1127
        }
1128

1129
        if (this.check(TokenKind.Stop)) {
3,694✔
1130
            return this.stopStatement();
16✔
1131
        }
1132

1133
        if (this.check(TokenKind.If)) {
3,678✔
1134
            return this.ifStatement();
169✔
1135
        }
1136

1137
        //`try` must be followed by a block, otherwise it could be a local variable
1138
        if (this.check(TokenKind.Try) && this.checkAnyNext(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
3,509✔
1139
            return this.tryCatchStatement();
27✔
1140
        }
1141

1142
        if (this.check(TokenKind.Throw)) {
3,482✔
1143
            return this.throwStatement();
11✔
1144
        }
1145

1146
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
3,471✔
1147
            return this.printStatement();
665✔
1148
        }
1149
        if (this.check(TokenKind.Dim)) {
2,806✔
1150
            return this.dimStatement();
43✔
1151
        }
1152

1153
        if (this.check(TokenKind.While)) {
2,763✔
1154
            return this.whileStatement();
23✔
1155
        }
1156

1157
        if (this.check(TokenKind.ExitWhile)) {
2,740✔
1158
            return this.exitWhile();
7✔
1159
        }
1160

1161
        if (this.check(TokenKind.For)) {
2,733✔
1162
            return this.forStatement();
34✔
1163
        }
1164

1165
        if (this.check(TokenKind.ForEach)) {
2,699✔
1166
            return this.forEachStatement();
20✔
1167
        }
1168

1169
        if (this.check(TokenKind.ExitFor)) {
2,679✔
1170
            return this.exitFor();
4✔
1171
        }
1172

1173
        if (this.check(TokenKind.End)) {
2,675✔
1174
            return this.endStatement();
8✔
1175
        }
1176

1177
        if (this.match(TokenKind.Return)) {
2,667✔
1178
            return this.returnStatement();
186✔
1179
        }
1180

1181
        if (this.check(TokenKind.Goto)) {
2,481✔
1182
            return this.gotoStatement();
12✔
1183
        }
1184

1185
        //the continue keyword (followed by `for`, `while`, or a statement separator)
1186
        if (this.check(TokenKind.Continue) && this.checkAnyNext(TokenKind.While, TokenKind.For, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
2,469✔
1187
            return this.continueStatement();
12✔
1188
        }
1189

1190
        //does this line look like a label? (i.e.  `someIdentifier:` )
1191
        if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
2,457✔
1192
            try {
12✔
1193
                return this.labelStatement();
12✔
1194
            } catch (err) {
1195
                if (!(err instanceof CancelStatementError)) {
2!
1196
                    throw err;
×
1197
                }
1198
                //not a label, try something else
1199
            }
1200
        }
1201

1202
        // BrightScript is like python, in that variables can be declared without a `var`,
1203
        // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
1204
        // out what to do with it.
1205
        if (
2,447✔
1206
            this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)
1207
        ) {
1208
            if (this.checkAnyNext(...AssignmentOperators)) {
2,276✔
1209
                return this.assignment();
884✔
1210
            } else if (this.checkNext(TokenKind.As)) {
1,392✔
1211
                // may be a typed assignment - this is v1 syntax
1212
                const backtrack = this.current;
3✔
1213
                let validTypeExpression = false;
3✔
1214
                try {
3✔
1215
                    // skip the identifier, and check for valid type expression
1216
                    this.advance();
3✔
1217
                    // skip the 'as'
1218
                    this.advance();
3✔
1219
                    // check if there is a valid type
1220
                    const typeToken = this.typeToken(true);
3✔
1221
                    const allowedNameKinds = [TokenKind.Identifier, ...DeclarableTypes, ...this.allowedLocalIdentifiers];
3✔
1222
                    validTypeExpression = allowedNameKinds.includes(typeToken.kind);
3✔
1223
                } catch (e) {
1224
                    // ignore any errors
1225
                } finally {
1226
                    this.current = backtrack;
3✔
1227
                }
1228
                if (validTypeExpression) {
3✔
1229
                    // there is a valid 'as' and type expression
1230
                    return this.assignment();
2✔
1231
                }
1232
            }
1233
        }
1234

1235
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1236
        if (this.check(TokenKind.Interface)) {
1,561✔
1237
            return this.interfaceDeclaration();
52✔
1238
        }
1239

1240
        if (this.check(TokenKind.Class)) {
1,509✔
1241
            return this.classDeclaration();
483✔
1242
        }
1243

1244
        if (this.check(TokenKind.Namespace)) {
1,026✔
1245
            return this.namespaceStatement();
243✔
1246
        }
1247

1248
        if (this.check(TokenKind.Enum)) {
783✔
1249
            return this.enumDeclaration();
115✔
1250
        }
1251

1252
        // TODO: support multi-statements
1253
        return this.setStatement();
668✔
1254
    }
1255

1256
    private whileStatement(): WhileStatement {
1257
        const whileKeyword = this.advance();
23✔
1258
        const condition = this.expression();
23✔
1259

1260
        this.consumeStatementSeparators();
22✔
1261

1262
        const whileBlock = this.block(TokenKind.EndWhile);
22✔
1263
        let endWhile: Token;
1264
        if (!whileBlock || this.peek().kind !== TokenKind.EndWhile) {
22✔
1265
            this.diagnostics.push({
1✔
1266
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('while'),
1267
                range: this.peek().range
1268
            });
1269
            if (!whileBlock) {
1!
1270
                throw this.lastDiagnosticAsError();
×
1271
            }
1272
        } else {
1273
            endWhile = this.advance();
21✔
1274
        }
1275

1276
        return new WhileStatement(
22✔
1277
            { while: whileKeyword, endWhile: endWhile },
1278
            condition,
1279
            whileBlock
1280
        );
1281
    }
1282

1283
    private exitWhile(): ExitWhileStatement {
1284
        let keyword = this.advance();
7✔
1285

1286
        return new ExitWhileStatement({ exitWhile: keyword });
7✔
1287
    }
1288

1289
    private forStatement(): ForStatement {
1290
        const forToken = this.advance();
34✔
1291
        const initializer = this.assignment();
34✔
1292

1293
        //TODO: newline allowed?
1294

1295
        const toToken = this.advance();
33✔
1296
        const finalValue = this.expression();
33✔
1297
        let incrementExpression: Expression | undefined;
1298
        let stepToken: Token | undefined;
1299

1300
        if (this.check(TokenKind.Step)) {
33✔
1301
            stepToken = this.advance();
10✔
1302
            incrementExpression = this.expression();
10✔
1303
        } else {
1304
            // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
1305
        }
1306

1307
        this.consumeStatementSeparators();
33✔
1308

1309
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
33✔
1310
        let endForToken: Token;
1311
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
33✔
1312
            this.diagnostics.push({
1✔
1313
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1314
                range: this.peek().range
1315
            });
1316
            if (!body) {
1!
1317
                throw this.lastDiagnosticAsError();
×
1318
            }
1319
        } else {
1320
            endForToken = this.advance();
32✔
1321
        }
1322

1323
        // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
1324
        // stays around in scope with whatever value it was when the loop exited.
1325
        return new ForStatement(
33✔
1326
            forToken,
1327
            initializer,
1328
            toToken,
1329
            finalValue,
1330
            body,
1331
            endForToken,
1332
            stepToken,
1333
            incrementExpression
1334
        );
1335
    }
1336

1337
    private forEachStatement(): ForEachStatement {
1338
        let forEach = this.advance();
20✔
1339
        let name = this.advance();
20✔
1340

1341
        let maybeIn = this.peek();
20✔
1342
        if (this.check(TokenKind.Identifier) && maybeIn.text.toLowerCase() === 'in') {
20!
1343
            this.advance();
20✔
1344
        } else {
1345
            this.diagnostics.push({
×
1346
                ...DiagnosticMessages.expectedInAfterForEach(name.text),
1347
                range: this.peek().range
1348
            });
1349
            throw this.lastDiagnosticAsError();
×
1350
        }
1351

1352
        let target = this.expression();
20✔
1353
        if (!target) {
20!
1354
            this.diagnostics.push({
×
1355
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1356
                range: this.peek().range
1357
            });
1358
            throw this.lastDiagnosticAsError();
×
1359
        }
1360

1361
        this.consumeStatementSeparators();
20✔
1362

1363
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
20✔
1364
        if (!body) {
20!
1365
            this.diagnostics.push({
×
1366
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1367
                range: this.peek().range
1368
            });
1369
            throw this.lastDiagnosticAsError();
×
1370
        }
1371

1372
        let endFor = this.advance();
20✔
1373

1374
        return new ForEachStatement(
20✔
1375
            {
1376
                forEach: forEach,
1377
                in: maybeIn,
1378
                endFor: endFor
1379
            },
1380
            name,
1381
            target,
1382
            body
1383
        );
1384
    }
1385

1386
    private exitFor(): ExitForStatement {
1387
        let keyword = this.advance();
4✔
1388

1389
        return new ExitForStatement({ exitFor: keyword });
4✔
1390
    }
1391

1392
    private commentStatement() {
1393
        //if this comment is on the same line as the previous statement,
1394
        //then this comment should be treated as a single-line comment
1395
        let prev = this.previous();
227✔
1396
        if (prev?.range?.end.line === this.peek().range?.start.line) {
227✔
1397
            return new CommentStatement([this.advance()]);
128✔
1398
        } else {
1399
            let comments = [this.advance()];
99✔
1400
            while (this.check(TokenKind.Newline) && this.checkNext(TokenKind.Comment)) {
99✔
1401
                this.advance();
20✔
1402
                comments.push(this.advance());
20✔
1403
            }
1404
            return new CommentStatement(comments);
99✔
1405
        }
1406
    }
1407

1408
    private namespaceStatement(): NamespaceStatement | undefined {
1409
        this.warnIfNotBrighterScriptMode('namespace');
243✔
1410
        let keyword = this.advance();
243✔
1411

1412
        this.namespaceAndFunctionDepth++;
243✔
1413

1414
        let name = this.getNamespacedVariableNameExpression();
243✔
1415
        //set the current namespace name
1416
        let result = new NamespaceStatement(keyword, name, null, null);
242✔
1417

1418
        this.globalTerminators.push([TokenKind.EndNamespace]);
242✔
1419
        let body = this.body();
242✔
1420
        this.globalTerminators.pop();
242✔
1421

1422
        let endKeyword: Token;
1423
        if (this.check(TokenKind.EndNamespace)) {
242✔
1424
            endKeyword = this.advance();
241✔
1425
        } else {
1426
            //the `end namespace` keyword is missing. add a diagnostic, but keep parsing
1427
            this.diagnostics.push({
1✔
1428
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'),
1429
                range: keyword.range
1430
            });
1431
        }
1432

1433
        this.namespaceAndFunctionDepth--;
242✔
1434

1435
        result.body = body;
242✔
1436
        result.endKeyword = endKeyword;
242✔
1437
        this._references.namespaceStatements.push(result);
242✔
1438
        //cache the range property so that plugins can't affect it
1439
        result.cacheRange();
242✔
1440
        result.body.symbolTable.name += `: namespace '${result.name}'`;
242✔
1441
        return result;
242✔
1442
    }
1443

1444
    /**
1445
     * Get an expression with identifiers separated by periods. Useful for namespaces and class extends
1446
     */
1447
    private getNamespacedVariableNameExpression(ignoreDiagnostics = false) {
365✔
1448
        let firstIdentifier: Identifier;
1449
        if (ignoreDiagnostics) {
449✔
1450
            if (this.checkAny(...this.allowedLocalIdentifiers)) {
2!
1451
                firstIdentifier = this.advance() as Identifier;
×
1452
            } else {
1453
                throw new Error();
2✔
1454
            }
1455
        } else {
1456
            firstIdentifier = this.consume(
447✔
1457
                DiagnosticMessages.expectedIdentifierAfterKeyword(this.previous().text),
1458
                TokenKind.Identifier,
1459
                ...this.allowedLocalIdentifiers
1460
            ) as Identifier;
1461
        }
1462
        let expr: DottedGetExpression | VariableExpression;
1463

1464
        if (firstIdentifier) {
444!
1465
            // force it into an identifier so the AST makes some sense
1466
            firstIdentifier.kind = TokenKind.Identifier;
444✔
1467
            const varExpr = new VariableExpression(firstIdentifier);
444✔
1468
            expr = varExpr;
444✔
1469

1470
            //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
1471
            while (this.check(TokenKind.Dot)) {
444✔
1472
                let dot = this.tryConsume(
145✔
1473
                    DiagnosticMessages.unexpectedToken(this.peek().text),
1474
                    TokenKind.Dot
1475
                );
1476
                if (!dot) {
145!
1477
                    break;
×
1478
                }
1479
                let identifier = this.tryConsume(
145✔
1480
                    DiagnosticMessages.expectedIdentifier(),
1481
                    TokenKind.Identifier,
1482
                    ...this.allowedLocalIdentifiers,
1483
                    ...AllowedProperties
1484
                ) as Identifier;
1485

1486
                if (!identifier) {
145✔
1487
                    break;
3✔
1488
                }
1489
                // force it into an identifier so the AST makes some sense
1490
                identifier.kind = TokenKind.Identifier;
142✔
1491
                expr = new DottedGetExpression(expr, identifier, dot);
142✔
1492
            }
1493
        }
1494
        return new NamespacedVariableNameExpression(expr);
444✔
1495
    }
1496

1497
    /**
1498
     * Add an 'unexpected token' diagnostic for any token found between current and the first stopToken found.
1499
     */
1500
    private flagUntil(...stopTokens: TokenKind[]) {
1501
        while (!this.checkAny(...stopTokens) && !this.isAtEnd()) {
6!
1502
            let token = this.advance();
×
1503
            this.diagnostics.push({
×
1504
                ...DiagnosticMessages.unexpectedToken(token.text),
1505
                range: token.range
1506
            });
1507
        }
1508
    }
1509

1510
    /**
1511
     * Consume tokens until one of the `stopTokenKinds` is encountered
1512
     * @param stopTokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
1513
     * @returns - the list of tokens consumed, EXCLUDING the `stopTokenKind` (you can use `this.peek()` to see which one it was)
1514
     */
1515
    private consumeUntil(...stopTokenKinds: TokenKind[]) {
1516
        let result = [] as Token[];
83✔
1517
        //take tokens until we encounter one of the stopTokenKinds
1518
        while (!stopTokenKinds.includes(this.peek().kind)) {
83✔
1519
            result.push(this.advance());
241✔
1520
        }
1521
        return result;
83✔
1522
    }
1523

1524
    private constDeclaration(): ConstStatement | undefined {
1525
        this.warnIfNotBrighterScriptMode('const declaration');
54✔
1526
        const constToken = this.advance();
54✔
1527
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
54✔
1528
        const equalToken = this.consumeToken(TokenKind.Equal);
54✔
1529
        const expression = this.expression();
54✔
1530
        const statement = new ConstStatement({
54✔
1531
            const: constToken,
1532
            name: nameToken,
1533
            equals: equalToken
1534
        }, expression);
1535
        this._references.constStatements.push(statement);
54✔
1536
        return statement;
54✔
1537
    }
1538

1539
    private libraryStatement(): LibraryStatement | undefined {
1540
        let libStatement = new LibraryStatement({
14✔
1541
            library: this.advance(),
1542
            //grab the next token only if it's a string
1543
            filePath: this.tryConsume(
1544
                DiagnosticMessages.expectedStringLiteralAfterKeyword('library'),
1545
                TokenKind.StringLiteral
1546
            )
1547
        });
1548

1549
        this._references.libraryStatements.push(libStatement);
14✔
1550
        return libStatement;
14✔
1551
    }
1552

1553
    private importStatement() {
1554
        this.warnIfNotBrighterScriptMode('import statements');
38✔
1555
        let importStatement = new ImportStatement(
38✔
1556
            this.advance(),
1557
            //grab the next token only if it's a string
1558
            this.tryConsume(
1559
                DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
1560
                TokenKind.StringLiteral
1561
            )
1562
        );
1563

1564
        this._references.importStatements.push(importStatement);
38✔
1565
        return importStatement;
38✔
1566
    }
1567

1568
    private typecastStatement() {
1569
        this.warnIfNotBrighterScriptMode('typecast statements');
5✔
1570
        const typecastToken = this.advance();
5✔
1571
        const obj = this.identifier(...this.allowedLocalIdentifiers);
5✔
1572
        const asToken = this.advance();
5✔
1573
        const typeToken = this.typeToken();
5✔
1574
        return new TypecastStatement({
5✔
1575
            typecast: typecastToken,
1576
            obj: obj,
1577
            as: asToken,
1578
            type: typeToken
1579
        });
1580
    }
1581

1582
    private aliasStatement() {
1583
        this.warnIfNotBrighterScriptMode('alias statements');
2✔
1584
        const aliasToken = this.advance();
2✔
1585
        const name = this.identifier(...this.allowedLocalIdentifiers);
2✔
1586
        const equals = this.consumeToken(TokenKind.Equal);
2✔
1587
        const value = this.identifier(...this.allowedLocalIdentifiers);
2✔
1588
        return new AliasStatement({
2✔
1589
            alias: aliasToken,
1590
            name: name,
1591
            equals: equals,
1592
            value: value
1593
        });
1594
    }
1595

1596
    private annotationExpression() {
1597
        const atToken = this.advance();
61✔
1598
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
61✔
1599
        if (identifier) {
61✔
1600
            identifier.kind = TokenKind.Identifier;
60✔
1601
        }
1602
        let annotation = new AnnotationExpression(atToken, identifier);
61✔
1603
        this.pendingAnnotations.push(annotation);
60✔
1604

1605
        //optional arguments
1606
        if (this.check(TokenKind.LeftParen)) {
60✔
1607
            let leftParen = this.advance();
24✔
1608
            annotation.call = this.finishCall(leftParen, annotation, false);
24✔
1609
        }
1610
        return annotation;
60✔
1611
    }
1612

1613
    private ternaryExpression(test?: Expression): TernaryExpression {
1614
        this.warnIfNotBrighterScriptMode('ternary operator');
93✔
1615
        if (!test) {
93!
1616
            test = this.expression();
×
1617
        }
1618
        const questionMarkToken = this.advance();
93✔
1619

1620
        //consume newlines or comments
1621
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
93✔
1622
            this.advance();
8✔
1623
        }
1624

1625
        let consequent: Expression;
1626
        try {
93✔
1627
            consequent = this.expression();
93✔
1628
        } catch { }
1629

1630
        //consume newlines or comments
1631
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
93✔
1632
            this.advance();
6✔
1633
        }
1634

1635
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
93✔
1636

1637
        //consume newlines
1638
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
93✔
1639
            this.advance();
12✔
1640
        }
1641
        let alternate: Expression;
1642
        try {
93✔
1643
            alternate = this.expression();
93✔
1644
        } catch { }
1645

1646
        return new TernaryExpression(test, questionMarkToken, consequent, colonToken, alternate);
93✔
1647
    }
1648

1649
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1650
        this.warnIfNotBrighterScriptMode('null coalescing operator');
29✔
1651
        const questionQuestionToken = this.advance();
29✔
1652
        const alternate = this.expression();
29✔
1653
        return new NullCoalescingExpression(test, questionQuestionToken, alternate);
29✔
1654
    }
1655

1656
    private regexLiteralExpression() {
1657
        this.warnIfNotBrighterScriptMode('regular expression literal');
45✔
1658
        return new RegexLiteralExpression({
45✔
1659
            regexLiteral: this.advance()
1660
        });
1661
    }
1662

1663
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1664
        this.warnIfNotBrighterScriptMode('template string');
51✔
1665

1666
        //get the tag name
1667
        let tagName: Identifier;
1668
        if (isTagged) {
51✔
1669
            tagName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties) as Identifier;
8✔
1670
            // force it into an identifier so the AST makes some sense
1671
            tagName.kind = TokenKind.Identifier;
8✔
1672
        }
1673

1674
        let quasis = [] as TemplateStringQuasiExpression[];
51✔
1675
        let expressions = [];
51✔
1676
        let openingBacktick = this.peek();
51✔
1677
        this.advance();
51✔
1678
        let currentQuasiExpressionParts = [];
51✔
1679
        while (!this.isAtEnd() && !this.check(TokenKind.BackTick)) {
51✔
1680
            let next = this.peek();
194✔
1681
            if (next.kind === TokenKind.TemplateStringQuasi) {
194✔
1682
                //a quasi can actually be made up of multiple quasis when it includes char literals
1683
                currentQuasiExpressionParts.push(
122✔
1684
                    new LiteralExpression(next)
1685
                );
1686
                this.advance();
122✔
1687
            } else if (next.kind === TokenKind.EscapedCharCodeLiteral) {
72✔
1688
                currentQuasiExpressionParts.push(
31✔
1689
                    new EscapedCharCodeLiteralExpression(<any>next)
1690
                );
1691
                this.advance();
31✔
1692
            } else {
1693
                //finish up the current quasi
1694
                quasis.push(
41✔
1695
                    new TemplateStringQuasiExpression(currentQuasiExpressionParts)
1696
                );
1697
                currentQuasiExpressionParts = [];
41✔
1698

1699
                if (next.kind === TokenKind.TemplateStringExpressionBegin) {
41!
1700
                    this.advance();
41✔
1701
                }
1702
                //now keep this expression
1703
                expressions.push(this.expression());
41✔
1704
                if (!this.isAtEnd() && this.check(TokenKind.TemplateStringExpressionEnd)) {
41!
1705
                    //TODO is it an error if this is not present?
1706
                    this.advance();
41✔
1707
                } else {
1708
                    this.diagnostics.push({
×
1709
                        ...DiagnosticMessages.unterminatedTemplateExpression(),
1710
                        range: util.getRange(openingBacktick, this.peek())
1711
                    });
1712
                    throw this.lastDiagnosticAsError();
×
1713
                }
1714
            }
1715
        }
1716

1717
        //store the final set of quasis
1718
        quasis.push(
51✔
1719
            new TemplateStringQuasiExpression(currentQuasiExpressionParts)
1720
        );
1721

1722
        if (this.isAtEnd()) {
51✔
1723
            //error - missing backtick
1724
            this.diagnostics.push({
2✔
1725
                ...DiagnosticMessages.unterminatedTemplateStringAtEndOfFile(),
1726
                range: util.getRange(openingBacktick, this.peek())
1727
            });
1728
            throw this.lastDiagnosticAsError();
2✔
1729

1730
        } else {
1731
            let closingBacktick = this.advance();
49✔
1732
            if (isTagged) {
49✔
1733
                return new TaggedTemplateStringExpression(tagName, openingBacktick, quasis, expressions, closingBacktick);
8✔
1734
            } else {
1735
                return new TemplateStringExpression(openingBacktick, quasis, expressions, closingBacktick);
41✔
1736
            }
1737
        }
1738
    }
1739

1740
    private tryCatchStatement(): TryCatchStatement {
1741
        const tryToken = this.advance();
27✔
1742
        const statement = new TryCatchStatement(
27✔
1743
            { try: tryToken }
1744
        );
1745

1746
        //ensure statement separator
1747
        this.consumeStatementSeparators();
27✔
1748

1749
        statement.tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
27✔
1750

1751
        const peek = this.peek();
27✔
1752
        if (peek.kind !== TokenKind.Catch) {
27✔
1753
            this.diagnostics.push({
2✔
1754
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1755
                range: this.peek().range
1756
            });
1757
            //gracefully handle end-try
1758
            if (peek.kind === TokenKind.EndTry) {
2✔
1759
                statement.tokens.endTry = this.advance();
1✔
1760
            }
1761
            return statement;
2✔
1762
        }
1763
        const catchStmt = new CatchStatement({ catch: this.advance() });
25✔
1764
        statement.catchStatement = catchStmt;
25✔
1765

1766
        const exceptionVarToken = this.tryConsume(DiagnosticMessages.missingExceptionVarToFollowCatch(), TokenKind.Identifier, ...this.allowedLocalIdentifiers);
25✔
1767
        if (exceptionVarToken) {
25✔
1768
            // force it into an identifier so the AST makes some sense
1769
            exceptionVarToken.kind = TokenKind.Identifier;
23✔
1770
            catchStmt.exceptionVariable = exceptionVarToken as Identifier;
23✔
1771
        }
1772

1773
        //ensure statement sepatator
1774
        this.consumeStatementSeparators();
25✔
1775

1776
        catchStmt.catchBranch = this.block(TokenKind.EndTry);
25✔
1777

1778
        if (this.peek().kind !== TokenKind.EndTry) {
25✔
1779
            this.diagnostics.push({
1✔
1780
                ...DiagnosticMessages.expectedEndTryToTerminateTryCatch(),
1781
                range: this.peek().range
1782
            });
1783
        } else {
1784
            statement.tokens.endTry = this.advance();
24✔
1785
        }
1786
        return statement;
25✔
1787
    }
1788

1789
    private throwStatement() {
1790
        const throwToken = this.advance();
11✔
1791
        let expression: Expression;
1792
        if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
11✔
1793
            this.diagnostics.push({
1✔
1794
                ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(),
1795
                range: throwToken.range
1796
            });
1797
        } else {
1798
            expression = this.expression();
10✔
1799
        }
1800
        return new ThrowStatement(throwToken, expression);
9✔
1801
    }
1802

1803
    private dimStatement() {
1804
        const dim = this.advance();
43✔
1805

1806
        let identifier = this.tryConsume(DiagnosticMessages.expectedIdentifierAfterKeyword('dim'), TokenKind.Identifier, ...this.allowedLocalIdentifiers) as Identifier;
43✔
1807
        // force to an identifier so the AST makes some sense
1808
        if (identifier) {
43✔
1809
            identifier.kind = TokenKind.Identifier;
41✔
1810
        }
1811

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

1814
        let expressions: Expression[] = [];
43✔
1815
        let expression: Expression;
1816
        do {
43✔
1817
            try {
82✔
1818
                expression = this.expression();
82✔
1819
                expressions.push(expression);
77✔
1820
                if (this.check(TokenKind.Comma)) {
77✔
1821
                    this.advance();
39✔
1822
                } else {
1823
                    // will also exit for right square braces
1824
                    break;
38✔
1825
                }
1826
            } catch (error) {
1827
            }
1828
        } while (expression);
1829

1830
        if (expressions.length === 0) {
43✔
1831
            this.diagnostics.push({
5✔
1832
                ...DiagnosticMessages.missingExpressionsInDimStatement(),
1833
                range: this.peek().range
1834
            });
1835
        }
1836
        let rightSquareBracket = this.tryConsume(DiagnosticMessages.missingRightSquareBracketAfterDimIdentifier(), TokenKind.RightSquareBracket);
43✔
1837
        return new DimStatement(dim, identifier, leftSquareBracket, expressions, rightSquareBracket);
43✔
1838
    }
1839

1840
    private ifStatement(): IfStatement {
1841
        // colon before `if` is usually not allowed, unless it's after `then`
1842
        if (this.current > 0) {
213✔
1843
            const prev = this.previous();
208✔
1844
            if (prev.kind === TokenKind.Colon) {
208✔
1845
                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then) {
3✔
1846
                    this.diagnostics.push({
1✔
1847
                        ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
1848
                        range: prev.range
1849
                    });
1850
                }
1851
            }
1852
        }
1853

1854
        const ifToken = this.advance();
213✔
1855
        const startingRange = ifToken.range;
213✔
1856

1857
        const condition = this.expression();
213✔
1858
        let thenBranch: Block;
1859
        let elseBranch: IfStatement | Block | undefined;
1860

1861
        let thenToken: Token | undefined;
1862
        let endIfToken: Token | undefined;
1863
        let elseToken: Token | undefined;
1864

1865
        //optional `then`
1866
        if (this.check(TokenKind.Then)) {
211✔
1867
            thenToken = this.advance();
146✔
1868
        }
1869

1870
        //is it inline or multi-line if?
1871
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
211✔
1872

1873
        if (isInlineIfThen) {
211✔
1874
            /*** PARSE INLINE IF STATEMENT ***/
1875

1876
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
32✔
1877

1878
            if (!thenBranch) {
32!
1879
                this.diagnostics.push({
×
1880
                    ...DiagnosticMessages.expectedStatementToFollowConditionalCondition(ifToken.text),
1881
                    range: this.peek().range
1882
                });
1883
                throw this.lastDiagnosticAsError();
×
1884
            } else {
1885
                this.ensureInline(thenBranch.statements);
32✔
1886
            }
1887

1888
            //else branch
1889
            if (this.check(TokenKind.Else)) {
32✔
1890
                elseToken = this.advance();
19✔
1891

1892
                if (this.check(TokenKind.If)) {
19✔
1893
                    // recurse-read `else if`
1894
                    elseBranch = this.ifStatement();
4✔
1895

1896
                    //no multi-line if chained with an inline if
1897
                    if (!elseBranch.isInline) {
4✔
1898
                        this.diagnostics.push({
2✔
1899
                            ...DiagnosticMessages.expectedInlineIfStatement(),
1900
                            range: elseBranch.range
1901
                        });
1902
                    }
1903

1904
                } else if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
15!
1905
                    //expecting inline else branch
1906
                    this.diagnostics.push({
×
1907
                        ...DiagnosticMessages.expectedInlineIfStatement(),
1908
                        range: this.peek().range
1909
                    });
1910
                    throw this.lastDiagnosticAsError();
×
1911
                } else {
1912
                    elseBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
15✔
1913

1914
                    if (elseBranch) {
15!
1915
                        this.ensureInline(elseBranch.statements);
15✔
1916
                    }
1917
                }
1918

1919
                if (!elseBranch) {
19!
1920
                    //missing `else` branch
1921
                    this.diagnostics.push({
×
1922
                        ...DiagnosticMessages.expectedStatementToFollowElse(),
1923
                        range: this.peek().range
1924
                    });
1925
                    throw this.lastDiagnosticAsError();
×
1926
                }
1927
            }
1928

1929
            if (!elseBranch || !isIfStatement(elseBranch)) {
32✔
1930
                //enforce newline at the end of the inline if statement
1931
                const peek = this.peek();
28✔
1932
                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && !this.isAtEnd()) {
28✔
1933
                    //ignore last error if it was about a colon
1934
                    if (this.previous().kind === TokenKind.Colon) {
3!
1935
                        this.diagnostics.pop();
3✔
1936
                        this.current--;
3✔
1937
                    }
1938
                    //newline is required
1939
                    this.diagnostics.push({
3✔
1940
                        ...DiagnosticMessages.expectedFinalNewline(),
1941
                        range: this.peek().range
1942
                    });
1943
                }
1944
            }
1945

1946
        } else {
1947
            /*** PARSE MULTI-LINE IF STATEMENT ***/
1948

1949
            thenBranch = this.blockConditionalBranch(ifToken);
179✔
1950

1951
            //ensure newline/colon before next keyword
1952
            this.ensureNewLineOrColon();
177✔
1953

1954
            //else branch
1955
            if (this.check(TokenKind.Else)) {
177✔
1956
                elseToken = this.advance();
92✔
1957

1958
                if (this.check(TokenKind.If)) {
92✔
1959
                    // recurse-read `else if`
1960
                    elseBranch = this.ifStatement();
40✔
1961

1962
                } else {
1963
                    elseBranch = this.blockConditionalBranch(ifToken);
52✔
1964

1965
                    //ensure newline/colon before next keyword
1966
                    this.ensureNewLineOrColon();
52✔
1967
                }
1968
            }
1969

1970
            if (!isIfStatement(elseBranch)) {
177✔
1971
                if (this.check(TokenKind.EndIf)) {
137✔
1972
                    endIfToken = this.advance();
135✔
1973

1974
                } else {
1975
                    //missing endif
1976
                    this.diagnostics.push({
2✔
1977
                        ...DiagnosticMessages.expectedEndIfToCloseIfStatement(startingRange.start),
1978
                        range: ifToken.range
1979
                    });
1980
                }
1981
            }
1982
        }
1983

1984
        return new IfStatement(
209✔
1985
            {
1986
                if: ifToken,
1987
                then: thenToken,
1988
                endIf: endIfToken,
1989
                else: elseToken
1990
            },
1991
            condition,
1992
            thenBranch,
1993
            elseBranch,
1994
            isInlineIfThen
1995
        );
1996
    }
1997

1998
    //consume a `then` or `else` branch block of an `if` statement
1999
    private blockConditionalBranch(ifToken: Token) {
2000
        //keep track of the current error count, because if the then branch fails,
2001
        //we will trash them in favor of a single error on if
2002
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
231✔
2003

2004
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
2005
        // a trailing "end if" or "else if"
2006
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
231✔
2007

2008
        if (!branch) {
231✔
2009
            //throw out any new diagnostics created as a result of a `then` block parse failure.
2010
            //the block() function will discard the current line, so any discarded diagnostics will
2011
            //resurface if they are legitimate, and not a result of a malformed if statement
2012
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
2✔
2013

2014
            //this whole if statement is bogus...add error to the if token and hard-fail
2015
            this.diagnostics.push({
2✔
2016
                ...DiagnosticMessages.expectedEndIfElseIfOrElseToTerminateThenBlock(),
2017
                range: ifToken.range
2018
            });
2019
            throw this.lastDiagnosticAsError();
2✔
2020
        }
2021
        return branch;
229✔
2022
    }
2023

2024
    private ensureNewLineOrColon(silent = false) {
229✔
2025
        const prev = this.previous().kind;
436✔
2026
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
436✔
2027
            if (!silent) {
121✔
2028
                this.diagnostics.push({
6✔
2029
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2030
                    range: this.peek().range
2031
                });
2032
            }
2033
            return false;
121✔
2034
        }
2035
        return true;
315✔
2036
    }
2037

2038
    //ensure each statement of an inline block is single-line
2039
    private ensureInline(statements: Statement[]) {
2040
        for (const stat of statements) {
47✔
2041
            if (isIfStatement(stat) && !stat.isInline) {
54✔
2042
                this.diagnostics.push({
2✔
2043
                    ...DiagnosticMessages.expectedInlineIfStatement(),
2044
                    range: stat.range
2045
                });
2046
            }
2047
        }
2048
    }
2049

2050
    //consume inline branch of an `if` statement
2051
    private inlineConditionalBranch(...additionalTerminators: BlockTerminator[]): Block | undefined {
2052
        let statements = [];
54✔
2053
        //attempt to get the next statement without using `this.declaration`
2054
        //which seems a bit hackish to get to work properly
2055
        let statement = this.statement();
54✔
2056
        if (!statement) {
54!
2057
            return undefined;
×
2058
        }
2059
        statements.push(statement);
54✔
2060
        const startingRange = statement.range;
54✔
2061

2062
        //look for colon statement separator
2063
        let foundColon = false;
54✔
2064
        while (this.match(TokenKind.Colon)) {
54✔
2065
            foundColon = true;
12✔
2066
        }
2067

2068
        //if a colon was found, add the next statement or err if unexpected
2069
        if (foundColon) {
54✔
2070
            if (!this.checkAny(TokenKind.Newline, ...additionalTerminators)) {
12✔
2071
                //if not an ending keyword, add next statement
2072
                let extra = this.inlineConditionalBranch(...additionalTerminators);
7✔
2073
                if (!extra) {
7!
2074
                    return undefined;
×
2075
                }
2076
                statements.push(...extra.statements);
7✔
2077
            } else {
2078
                //error: colon before next keyword
2079
                const colon = this.previous();
5✔
2080
                this.diagnostics.push({
5✔
2081
                    ...DiagnosticMessages.unexpectedToken(colon.text),
2082
                    range: colon.range
2083
                });
2084
            }
2085
        }
2086
        return new Block(statements, startingRange);
54✔
2087
    }
2088

2089
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2090
        let expressionStart = this.peek();
353✔
2091

2092
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
353✔
2093
            let operator = this.advance();
20✔
2094

2095
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
20✔
2096
                this.diagnostics.push({
1✔
2097
                    ...DiagnosticMessages.consecutiveIncrementDecrementOperatorsAreNotAllowed(),
2098
                    range: this.peek().range
2099
                });
2100
                throw this.lastDiagnosticAsError();
1✔
2101
            } else if (isCallExpression(expr)) {
19✔
2102
                this.diagnostics.push({
1✔
2103
                    ...DiagnosticMessages.incrementDecrementOperatorsAreNotAllowedAsResultOfFunctionCall(),
2104
                    range: expressionStart.range
2105
                });
2106
                throw this.lastDiagnosticAsError();
1✔
2107
            }
2108

2109
            const result = new IncrementStatement(expr, operator);
18✔
2110
            this._references.expressions.add(result);
18✔
2111
            return result;
18✔
2112
        }
2113

2114
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
333✔
2115
            return new ExpressionStatement(expr);
269✔
2116
        }
2117

2118
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an assignment
2119
        this.diagnostics.push({
64✔
2120
            ...DiagnosticMessages.expectedStatementOrFunctionCallButReceivedExpression(),
2121
            range: expressionStart.range
2122
        });
2123
        throw this.lastDiagnosticAsError();
64✔
2124
    }
2125

2126
    private setStatement(): DottedSetStatement | IndexedSetStatement | ExpressionStatement | IncrementStatement | AssignmentStatement {
2127
        /**
2128
         * Attempts to find an expression-statement or an increment statement.
2129
         * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
2130
         * statements aren't valid expressions. They _do_ however fall under the same parsing
2131
         * priority as standalone function calls though, so we can parse them in the same way.
2132
         */
2133
        let expr = this.call();
668✔
2134
        if (this.checkAny(...AssignmentOperators) && !(isCallExpression(expr))) {
632✔
2135
            let left = expr;
282✔
2136
            let operator = this.advance();
282✔
2137
            let right = this.expression();
282✔
2138

2139
            // Create a dotted or indexed "set" based on the left-hand side's type
2140
            if (isIndexedGetExpression(left)) {
282✔
2141
                return new IndexedSetStatement(
40✔
2142
                    left.obj,
2143
                    left.index,
2144
                    operator.kind === TokenKind.Equal
2145
                        ? right
40✔
2146
                        : new BinaryExpression(left, operator, right),
2147
                    left.openingSquare,
2148
                    left.closingSquare,
2149
                    left.additionalIndexes,
2150
                    operator.kind === TokenKind.Equal
2151
                        ? operator
40✔
2152
                        : { kind: TokenKind.Equal, text: '=', range: operator.range }
2153
                );
2154
            } else if (isDottedGetExpression(left)) {
242✔
2155
                return new DottedSetStatement(
239✔
2156
                    left.obj,
2157
                    left.name,
2158
                    operator.kind === TokenKind.Equal
2159
                        ? right
239✔
2160
                        : new BinaryExpression(left, operator, right),
2161
                    left.dot,
2162
                    operator.kind === TokenKind.Equal
2163
                        ? operator
239✔
2164
                        : { kind: TokenKind.Equal, text: '=', range: operator.range }
2165
                );
2166
            }
2167
        }
2168
        return this.expressionStatement(expr);
353✔
2169
    }
2170

2171
    private printStatement(): PrintStatement {
2172
        let printKeyword = this.advance();
665✔
2173

2174
        let values: (
2175
            | Expression
2176
            | PrintSeparatorTab
2177
            | PrintSeparatorSpace)[] = [];
665✔
2178

2179
        while (!this.checkEndOfStatement()) {
665✔
2180
            if (this.check(TokenKind.Semicolon)) {
749✔
2181
                values.push(this.advance() as PrintSeparatorSpace);
20✔
2182
            } else if (this.check(TokenKind.Comma)) {
729✔
2183
                values.push(this.advance() as PrintSeparatorTab);
13✔
2184
            } else if (this.check(TokenKind.Else)) {
716✔
2185
                break; // inline branch
7✔
2186
            } else {
2187
                values.push(this.expression());
709✔
2188
            }
2189
        }
2190

2191
        //print statements can be empty, so look for empty print conditions
2192
        if (!values.length) {
664✔
2193
            let emptyStringLiteral = createStringLiteral('');
4✔
2194
            values.push(emptyStringLiteral);
4✔
2195
        }
2196

2197
        let last = values[values.length - 1];
664✔
2198
        if (isToken(last)) {
664✔
2199
            // TODO: error, expected value
2200
        }
2201

2202
        return new PrintStatement({ print: printKeyword }, values);
664✔
2203
    }
2204

2205
    /**
2206
     * Parses a return statement with an optional return value.
2207
     * @returns an AST representation of a return statement.
2208
     */
2209
    private returnStatement(): ReturnStatement {
2210
        let tokens = { return: this.previous() };
186✔
2211

2212
        if (this.checkEndOfStatement()) {
186✔
2213
            return new ReturnStatement(tokens);
5✔
2214
        }
2215

2216
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
181✔
2217
        return new ReturnStatement(tokens, toReturn);
180✔
2218
    }
2219

2220
    /**
2221
     * Parses a `label` statement
2222
     * @returns an AST representation of an `label` statement.
2223
     */
2224
    private labelStatement() {
2225
        let tokens = {
12✔
2226
            identifier: this.advance(),
2227
            colon: this.advance()
2228
        };
2229

2230
        //label must be alone on its line, this is probably not a label
2231
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
12✔
2232
            //rewind and cancel
2233
            this.current -= 2;
2✔
2234
            throw new CancelStatementError();
2✔
2235
        }
2236

2237
        return new LabelStatement(tokens);
10✔
2238
    }
2239

2240
    /**
2241
     * Parses a `continue` statement
2242
     */
2243
    private continueStatement() {
2244
        return new ContinueStatement({
12✔
2245
            continue: this.advance(),
2246
            loopType: this.tryConsume(
2247
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2248
                TokenKind.While, TokenKind.For
2249
            )
2250
        });
2251
    }
2252

2253
    /**
2254
     * Parses a `goto` statement
2255
     * @returns an AST representation of an `goto` statement.
2256
     */
2257
    private gotoStatement() {
2258
        let tokens = {
12✔
2259
            goto: this.advance(),
2260
            label: this.consume(
2261
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2262
                TokenKind.Identifier
2263
            )
2264
        };
2265

2266
        return new GotoStatement(tokens);
10✔
2267
    }
2268

2269
    /**
2270
     * Parses an `end` statement
2271
     * @returns an AST representation of an `end` statement.
2272
     */
2273
    private endStatement() {
2274
        let endTokens = { end: this.advance() };
8✔
2275

2276
        return new EndStatement(endTokens);
8✔
2277
    }
2278
    /**
2279
     * Parses a `stop` statement
2280
     * @returns an AST representation of a `stop` statement
2281
     */
2282
    private stopStatement() {
2283
        let tokens = { stop: this.advance() };
16✔
2284

2285
        return new StopStatement(tokens);
16✔
2286
    }
2287

2288
    /**
2289
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2290
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2291
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2292
     *                    ignored.
2293
     */
2294
    private block(...terminators: BlockTerminator[]): Block | undefined {
2295
        const parentAnnotations = this.enterAnnotationBlock();
2,076✔
2296

2297
        this.consumeStatementSeparators(true);
2,076✔
2298
        let startingToken = this.peek();
2,076✔
2299

2300
        const statements: Statement[] = [];
2,076✔
2301
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators)) {
2,076✔
2302
            //grab the location of the current token
2303
            let loopCurrent = this.current;
2,438✔
2304
            let dec = this.declaration();
2,438✔
2305
            if (dec) {
2,438✔
2306
                if (!isAnnotationExpression(dec)) {
2,355✔
2307
                    this.consumePendingAnnotations(dec);
2,348✔
2308
                    statements.push(dec);
2,348✔
2309
                }
2310

2311
                //ensure statement separator
2312
                this.consumeStatementSeparators();
2,355✔
2313

2314
            } else {
2315
                //something went wrong. reset to the top of the loop
2316
                this.current = loopCurrent;
83✔
2317

2318
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2319
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
83✔
2320

2321
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2322
                //but there's already an error in the file being parsed, so just leave this line here
2323
                this.advance();
83✔
2324

2325
                //consume potential separators
2326
                this.consumeStatementSeparators(true);
83✔
2327
            }
2328
        }
2329

2330
        if (this.isAtEnd()) {
2,076✔
2331
            return undefined;
5✔
2332
            // TODO: Figure out how to handle unterminated blocks well
2333
        } else if (terminators.length > 0) {
2,071✔
2334
            //did we hit end-sub / end-function while looking for some other terminator?
2335
            //if so, we need to restore the statement separator
2336
            let prev = this.previous().kind;
356✔
2337
            let peek = this.peek().kind;
356✔
2338
            if (
356✔
2339
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
716!
2340
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2341
            ) {
2342
                this.current--;
6✔
2343
            }
2344
        }
2345

2346
        this.exitAnnotationBlock(parentAnnotations);
2,071✔
2347
        return new Block(statements, startingToken.range);
2,071✔
2348
    }
2349

2350
    /**
2351
     * Attach pending annotations to the provided statement,
2352
     * and then reset the annotations array
2353
     */
2354
    consumePendingAnnotations(statement: Statement) {
2355
        if (this.pendingAnnotations.length) {
5,956✔
2356
            statement.annotations = this.pendingAnnotations;
45✔
2357
            this.pendingAnnotations = [];
45✔
2358
        }
2359
    }
2360

2361
    enterAnnotationBlock() {
2362
        const pending = this.pendingAnnotations;
5,081✔
2363
        this.pendingAnnotations = [];
5,081✔
2364
        return pending;
5,081✔
2365
    }
2366

2367
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2368
        // non consumed annotations are an error
2369
        if (this.pendingAnnotations.length) {
5,075✔
2370
            for (const annotation of this.pendingAnnotations) {
4✔
2371
                this.diagnostics.push({
6✔
2372
                    ...DiagnosticMessages.unusedAnnotation(),
2373
                    range: annotation.range
2374
                });
2375
            }
2376
        }
2377
        this.pendingAnnotations = parentAnnotations;
5,075✔
2378
    }
2379

2380
    private expression(findTypeCast = true): Expression {
4,007✔
2381
        let expression = this.anonymousFunction();
4,253✔
2382
        let asToken: Token;
2383
        let typeToken: Token;
2384
        if (findTypeCast) {
4,218✔
2385
            do {
3,972✔
2386
                if (this.check(TokenKind.As)) {
3,985✔
2387
                    this.warnIfNotBrighterScriptMode('type cast');
13✔
2388
                    // Check if this expression is wrapped in any type casts
2389
                    // allows for multiple casts:
2390
                    // myVal = foo() as dynamic as string
2391

2392
                    asToken = this.advance();
13✔
2393
                    typeToken = this.typeToken();
13✔
2394
                    if (asToken && typeToken) {
13!
2395
                        expression = new TypeCastExpression(expression, asToken, typeToken);
13✔
2396
                    }
2397
                } else {
2398
                    break;
3,972✔
2399
                }
2400

2401
            } while (asToken && typeToken);
26✔
2402
        }
2403
        this._references.expressions.add(expression);
4,218✔
2404
        return expression;
4,218✔
2405
    }
2406

2407
    private anonymousFunction(): Expression {
2408
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
4,253✔
2409
            const func = this.functionDeclaration(true);
73✔
2410
            //if there's an open paren after this, this is an IIFE
2411
            if (this.check(TokenKind.LeftParen)) {
73✔
2412
                return this.finishCall(this.advance(), func);
3✔
2413
            } else {
2414
                return func;
70✔
2415
            }
2416
        }
2417

2418
        let expr = this.boolean();
4,180✔
2419

2420
        if (this.check(TokenKind.Question)) {
4,145✔
2421
            return this.ternaryExpression(expr);
93✔
2422
        } else if (this.check(TokenKind.QuestionQuestion)) {
4,052✔
2423
            return this.nullCoalescingExpression(expr);
29✔
2424
        } else {
2425
            return expr;
4,023✔
2426
        }
2427
    }
2428

2429
    private boolean(): Expression {
2430
        let expr = this.relational();
4,180✔
2431

2432
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
4,145✔
2433
            let operator = this.previous();
28✔
2434
            let right = this.relational();
28✔
2435
            this.addExpressionsToReferences(expr, right);
28✔
2436
            expr = new BinaryExpression(expr, operator, right);
28✔
2437
        }
2438

2439
        return expr;
4,145✔
2440
    }
2441

2442
    private relational(): Expression {
2443
        let expr = this.additive();
4,224✔
2444

2445
        while (
4,189✔
2446
            this.matchAny(
2447
                TokenKind.Equal,
2448
                TokenKind.LessGreater,
2449
                TokenKind.Greater,
2450
                TokenKind.GreaterEqual,
2451
                TokenKind.Less,
2452
                TokenKind.LessEqual
2453
            )
2454
        ) {
2455
            let operator = this.previous();
149✔
2456
            let right = this.additive();
149✔
2457
            this.addExpressionsToReferences(expr, right);
149✔
2458
            expr = new BinaryExpression(expr, operator, right);
149✔
2459
        }
2460

2461
        return expr;
4,189✔
2462
    }
2463

2464
    private addExpressionsToReferences(...expressions: Expression[]) {
2465
        for (const expression of expressions) {
336✔
2466
            if (!isBinaryExpression(expression)) {
626✔
2467
                this.references.expressions.add(expression);
584✔
2468
            }
2469
        }
2470
    }
2471

2472
    // TODO: bitshift
2473

2474
    private additive(): Expression {
2475
        let expr = this.multiplicative();
4,373✔
2476

2477
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
4,338✔
2478
            let operator = this.previous();
86✔
2479
            let right = this.multiplicative();
86✔
2480
            this.addExpressionsToReferences(expr, right);
86✔
2481
            expr = new BinaryExpression(expr, operator, right);
86✔
2482
        }
2483

2484
        return expr;
4,338✔
2485
    }
2486

2487
    private multiplicative(): Expression {
2488
        let expr = this.exponential();
4,459✔
2489

2490
        while (this.matchAny(
4,424✔
2491
            TokenKind.Forwardslash,
2492
            TokenKind.Backslash,
2493
            TokenKind.Star,
2494
            TokenKind.Mod,
2495
            TokenKind.LeftShift,
2496
            TokenKind.RightShift
2497
        )) {
2498
            let operator = this.previous();
21✔
2499
            let right = this.exponential();
21✔
2500
            this.addExpressionsToReferences(expr, right);
21✔
2501
            expr = new BinaryExpression(expr, operator, right);
21✔
2502
        }
2503

2504
        return expr;
4,424✔
2505
    }
2506

2507
    private exponential(): Expression {
2508
        let expr = this.prefixUnary();
4,480✔
2509

2510
        while (this.match(TokenKind.Caret)) {
4,445✔
2511
            let operator = this.previous();
6✔
2512
            let right = this.prefixUnary();
6✔
2513
            this.addExpressionsToReferences(expr, right);
6✔
2514
            expr = new BinaryExpression(expr, operator, right);
6✔
2515
        }
2516

2517
        return expr;
4,445✔
2518
    }
2519

2520
    private prefixUnary(): Expression {
2521
        const nextKind = this.peek().kind;
4,508✔
2522
        if (nextKind === TokenKind.Not) {
4,508✔
2523
            this.current++; //advance
16✔
2524
            let operator = this.previous();
16✔
2525
            let right = this.relational();
16✔
2526
            return new UnaryExpression(operator, right);
16✔
2527
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
4,492✔
2528
            this.current++; //advance
22✔
2529
            let operator = this.previous();
22✔
2530
            let right = this.prefixUnary();
22✔
2531
            return new UnaryExpression(operator, right);
22✔
2532
        }
2533
        return this.call();
4,470✔
2534
    }
2535

2536
    private indexedGet(expr: Expression) {
2537
        let openingSquare = this.previous();
143✔
2538
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
143✔
2539
        let indexes: Expression[] = [];
143✔
2540

2541

2542
        //consume leading newlines
2543
        while (this.match(TokenKind.Newline)) { }
143✔
2544

2545
        try {
143✔
2546
            indexes.push(
143✔
2547
                this.expression()
2548
            );
2549
            //consume additional indexes separated by commas
2550
            while (this.check(TokenKind.Comma)) {
142✔
2551
                //discard the comma
2552
                this.advance();
17✔
2553
                indexes.push(
17✔
2554
                    this.expression()
2555
                );
2556
            }
2557
        } catch (error) {
2558
            this.rethrowNonDiagnosticError(error);
1✔
2559
        }
2560
        //consume trailing newlines
2561
        while (this.match(TokenKind.Newline)) { }
143✔
2562

2563
        const closingSquare = this.tryConsume(
143✔
2564
            DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
2565
            TokenKind.RightSquareBracket
2566
        );
2567

2568
        return new IndexedGetExpression(expr, indexes.shift(), openingSquare, closingSquare, questionDotToken, indexes);
143✔
2569
    }
2570

2571
    private newExpression() {
2572
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
44✔
2573
        let newToken = this.advance();
44✔
2574

2575
        let nameExpr = this.getNamespacedVariableNameExpression();
44✔
2576
        let leftParen = this.consume(
44✔
2577
            DiagnosticMessages.unexpectedToken(this.peek().text),
2578
            TokenKind.LeftParen,
2579
            TokenKind.QuestionLeftParen
2580
        );
2581
        let call = this.finishCall(leftParen, nameExpr);
40✔
2582
        //pop the call from the  callExpressions list because this is technically something else
2583
        this.callExpressions.pop();
40✔
2584
        let result = new NewExpression(newToken, call);
40✔
2585
        this._references.newExpressions.push(result);
40✔
2586
        return result;
40✔
2587
    }
2588

2589
    /**
2590
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2591
     */
2592
    private callfunc(callee: Expression): Expression {
2593
        this.warnIfNotBrighterScriptMode('callfunc operator');
25✔
2594
        let operator = this.previous();
25✔
2595
        let methodName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
25✔
2596
        // force it into an identifier so the AST makes some sense
2597
        methodName.kind = TokenKind.Identifier;
24✔
2598
        let openParen = this.consume(DiagnosticMessages.expectedOpenParenToFollowCallfuncIdentifier(), TokenKind.LeftParen);
24✔
2599
        let call = this.finishCall(openParen, callee, false);
24✔
2600

2601
        return new CallfuncExpression(callee, operator, methodName as Identifier, openParen, call.args, call.closingParen);
24✔
2602
    }
2603

2604
    private call(): Expression {
2605
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
5,138✔
2606
            return this.newExpression();
44✔
2607
        }
2608
        let expr = this.primary();
5,094✔
2609
        //an expression to keep for _references
2610
        let referenceCallExpression: Expression;
2611
        while (true) {
5,028✔
2612
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
6,677✔
2613
                expr = this.finishCall(this.previous(), expr);
537✔
2614
                //store this call expression in references
2615
                referenceCallExpression = expr;
537✔
2616

2617
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
6,140✔
2618
                expr = this.indexedGet(expr);
141✔
2619

2620
            } else if (this.match(TokenKind.Callfunc)) {
5,999✔
2621
                expr = this.callfunc(expr);
25✔
2622
                //store this callfunc expression in references
2623
                referenceCallExpression = expr;
24✔
2624

2625
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
5,974✔
2626
                if (this.match(TokenKind.LeftSquareBracket)) {
971✔
2627
                    expr = this.indexedGet(expr);
2✔
2628
                } else {
2629
                    let dot = this.previous();
969✔
2630
                    let name = this.tryConsume(
969✔
2631
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2632
                        TokenKind.Identifier,
2633
                        ...AllowedProperties
2634
                    );
2635
                    if (!name) {
969✔
2636
                        break;
24✔
2637
                    }
2638

2639
                    // force it into an identifier so the AST makes some sense
2640
                    name.kind = TokenKind.Identifier;
945✔
2641
                    expr = new DottedGetExpression(expr, name as Identifier, dot);
945✔
2642

2643
                    this.addPropertyHints(name);
945✔
2644
                }
2645

2646
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
5,003✔
2647
                let dot = this.advance();
11✔
2648
                let name = this.tryConsume(
11✔
2649
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2650
                    TokenKind.Identifier,
2651
                    ...AllowedProperties
2652
                );
2653

2654
                // force it into an identifier so the AST makes some sense
2655
                name.kind = TokenKind.Identifier;
11✔
2656
                if (!name) {
11!
2657
                    break;
×
2658
                }
2659
                expr = new XmlAttributeGetExpression(expr, name as Identifier, dot);
11✔
2660
                //only allow a single `@` expression
2661
                break;
11✔
2662

2663
            } else {
2664
                break;
4,992✔
2665
            }
2666
        }
2667
        //if we found a callExpression, add it to `expressions` in references
2668
        if (referenceCallExpression) {
5,027✔
2669
            this._references.expressions.add(referenceCallExpression);
524✔
2670
        }
2671
        return expr;
5,027✔
2672
    }
2673

2674
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
580✔
2675
        let args = [] as Expression[];
628✔
2676
        while (this.match(TokenKind.Newline)) { }
628✔
2677

2678
        if (!this.check(TokenKind.RightParen)) {
628✔
2679
            do {
325✔
2680
                while (this.match(TokenKind.Newline)) { }
496✔
2681

2682
                if (args.length >= CallExpression.MaximumArguments) {
496!
2683
                    this.diagnostics.push({
×
2684
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
2685
                        range: this.peek().range
2686
                    });
2687
                    throw this.lastDiagnosticAsError();
×
2688
                }
2689
                try {
496✔
2690
                    args.push(this.expression());
496✔
2691
                } catch (error) {
2692
                    this.rethrowNonDiagnosticError(error);
4✔
2693
                    // we were unable to get an expression, so don't continue
2694
                    break;
4✔
2695
                }
2696
            } while (this.match(TokenKind.Comma));
2697
        }
2698

2699
        while (this.match(TokenKind.Newline)) { }
628✔
2700

2701
        const closingParen = this.tryConsume(
628✔
2702
            DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
2703
            TokenKind.RightParen
2704
        );
2705

2706
        let expression = new CallExpression(callee, openingParen, closingParen, args);
628✔
2707
        if (addToCallExpressionList) {
628✔
2708
            this.callExpressions.push(expression);
580✔
2709
        }
2710
        return expression;
628✔
2711
    }
2712

2713
    /**
2714
     * Tries to get the next token as a type
2715
     * Allows for built-in types (double, string, etc.) or namespaced custom types in Brighterscript mode
2716
     * Will return a token of whatever is next to be parsed
2717
     * Will allow v1 type syntax (typed arrays, union types), but there is no validation on types used this way
2718
     */
2719
    private typeToken(ignoreDiagnostics = false): Token {
578✔
2720
        let typeToken: Token;
2721
        let lookForUnions = true;
581✔
2722
        let isAUnion = false;
581✔
2723
        let resultToken;
2724
        while (lookForUnions) {
581✔
2725
            lookForUnions = false;
588✔
2726

2727
            if (this.checkAny(...DeclarableTypes)) {
588✔
2728
                // Token is a built in type
2729
                typeToken = this.advance();
480✔
2730
            } else if (this.options.mode === ParseMode.BrighterScript) {
108✔
2731
                try {
84✔
2732
                    // see if we can get a namespaced identifer
2733
                    const qualifiedType = this.getNamespacedVariableNameExpression(ignoreDiagnostics);
84✔
2734
                    typeToken = createToken(TokenKind.Identifier, qualifiedType.getName(this.options.mode), qualifiedType.range);
81✔
2735
                } catch {
2736
                    //could not get an identifier - just get whatever's next
2737
                    typeToken = this.advance();
3✔
2738
                }
2739
            } else {
2740
                // just get whatever's next
2741
                typeToken = this.advance();
24✔
2742
            }
2743
            resultToken = resultToken ?? typeToken;
588✔
2744
            if (resultToken && this.options.mode === ParseMode.BrighterScript) {
588✔
2745
                // check for brackets
2746
                while (this.check(TokenKind.LeftSquareBracket) && this.peekNext().kind === TokenKind.RightSquareBracket) {
491✔
2747
                    const leftBracket = this.advance();
10✔
2748
                    const rightBracket = this.advance();
10✔
2749
                    typeToken = createToken(TokenKind.Identifier, typeToken.text + leftBracket.text + rightBracket.text, util.createBoundingRange(typeToken, leftBracket, rightBracket));
10✔
2750
                    resultToken = createToken(TokenKind.Dynamic, null, typeToken.range);
10✔
2751
                }
2752

2753
                if (this.check(TokenKind.Or)) {
491✔
2754
                    lookForUnions = true;
7✔
2755
                    let orToken = this.advance();
7✔
2756
                    resultToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(resultToken, typeToken, orToken));
7✔
2757
                    isAUnion = true;
7✔
2758
                }
2759
            }
2760
        }
2761
        if (isAUnion) {
581✔
2762
            resultToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(resultToken, typeToken));
5✔
2763
        }
2764
        return resultToken;
581✔
2765
    }
2766

2767
    private primary(): Expression {
2768
        switch (true) {
5,094✔
2769
            case this.matchAny(
5,094!
2770
                TokenKind.False,
2771
                TokenKind.True,
2772
                TokenKind.Invalid,
2773
                TokenKind.IntegerLiteral,
2774
                TokenKind.LongIntegerLiteral,
2775
                TokenKind.FloatLiteral,
2776
                TokenKind.DoubleLiteral,
2777
                TokenKind.StringLiteral
2778
            ):
2779
                return new LiteralExpression(this.previous());
3,051✔
2780

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

2785
            //template string
2786
            case this.check(TokenKind.BackTick):
2787
                return this.templateString(false);
43✔
2788

2789
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
2790
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
3,456✔
2791
                return this.templateString(true);
8✔
2792

2793
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
2794
                return new VariableExpression(this.previous() as Identifier);
1,490✔
2795

2796
            case this.match(TokenKind.LeftParen):
2797
                let left = this.previous();
37✔
2798
                let expr = this.expression();
37✔
2799
                let right = this.consume(
36✔
2800
                    DiagnosticMessages.unmatchedLeftParenAfterExpression(),
2801
                    TokenKind.RightParen
2802
                );
2803
                return new GroupingExpression({ left: left, right: right }, expr);
36✔
2804

2805
            case this.matchAny(TokenKind.LeftSquareBracket):
2806
                return this.arrayLiteral();
125✔
2807

2808
            case this.match(TokenKind.LeftCurlyBrace):
2809
                return this.aaLiteral();
194✔
2810

2811
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
2812
                let token = Object.assign(this.previous(), {
×
2813
                    kind: TokenKind.Identifier
2814
                }) as Identifier;
2815
                return new VariableExpression(token);
×
2816

2817
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
2818
                return this.anonymousFunction();
×
2819

2820
            case this.check(TokenKind.RegexLiteral):
2821
                return this.regexLiteralExpression();
45✔
2822

2823
            case this.check(TokenKind.Comment):
2824
                return new CommentStatement([this.advance()]);
3✔
2825

2826
            default:
2827
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
2828
                if (this.checkAny(...this.peekGlobalTerminators())) {
63!
2829
                    //don't throw a diagnostic, just return undefined
2830

2831
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
2832
                } else {
2833
                    this.diagnostics.push({
63✔
2834
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
2835
                        range: this.peek().range
2836
                    });
2837
                    throw this.lastDiagnosticAsError();
63✔
2838
                }
2839
        }
2840
    }
2841

2842
    private arrayLiteral() {
2843
        let elements: Array<Expression | CommentStatement> = [];
125✔
2844
        let openingSquare = this.previous();
125✔
2845

2846
        //add any comment found right after the opening square
2847
        if (this.check(TokenKind.Comment)) {
125✔
2848
            elements.push(new CommentStatement([this.advance()]));
1✔
2849
        }
2850

2851
        while (this.match(TokenKind.Newline)) {
125✔
2852
        }
2853
        let closingSquare: Token;
2854

2855
        if (!this.match(TokenKind.RightSquareBracket)) {
125✔
2856
            try {
94✔
2857
                elements.push(this.expression());
94✔
2858

2859
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
93✔
2860
                    if (this.checkPrevious(TokenKind.Comment) || this.check(TokenKind.Comment)) {
120✔
2861
                        let comment = this.check(TokenKind.Comment) ? this.advance() : this.previous();
4✔
2862
                        elements.push(new CommentStatement([comment]));
4✔
2863
                    }
2864
                    while (this.match(TokenKind.Newline)) {
120✔
2865

2866
                    }
2867

2868
                    if (this.check(TokenKind.RightSquareBracket)) {
120✔
2869
                        break;
30✔
2870
                    }
2871

2872
                    elements.push(this.expression());
90✔
2873
                }
2874
            } catch (error: any) {
2875
                this.rethrowNonDiagnosticError(error);
2✔
2876
            }
2877

2878
            closingSquare = this.tryConsume(
94✔
2879
                DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral(),
2880
                TokenKind.RightSquareBracket
2881
            );
2882
        } else {
2883
            closingSquare = this.previous();
31✔
2884
        }
2885

2886
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2887
        return new ArrayLiteralExpression(elements, openingSquare, closingSquare);
125✔
2888
    }
2889

2890
    private aaLiteral() {
2891
        let openingBrace = this.previous();
194✔
2892
        let members: Array<AAMemberExpression | CommentStatement> = [];
194✔
2893

2894
        let key = () => {
194✔
2895
            let result = {
194✔
2896
                colonToken: null as Token,
2897
                keyToken: null as Token,
2898
                range: null as Range
2899
            };
2900
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
194✔
2901
                result.keyToken = this.identifier(...AllowedProperties);
163✔
2902
            } else if (this.check(TokenKind.StringLiteral)) {
31!
2903
                result.keyToken = this.advance();
31✔
2904
            } else {
2905
                this.diagnostics.push({
×
2906
                    ...DiagnosticMessages.unexpectedAAKey(),
2907
                    range: this.peek().range
2908
                });
2909
                throw this.lastDiagnosticAsError();
×
2910
            }
2911

2912
            result.colonToken = this.consume(
194✔
2913
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
2914
                TokenKind.Colon
2915
            );
2916
            result.range = util.getRange(result.keyToken, result.colonToken);
193✔
2917
            return result;
193✔
2918
        };
2919

2920
        while (this.match(TokenKind.Newline)) { }
194✔
2921
        let closingBrace: Token;
2922
        if (!this.match(TokenKind.RightCurlyBrace)) {
194✔
2923
            let lastAAMember: AAMemberExpression;
2924
            try {
146✔
2925
                if (this.check(TokenKind.Comment)) {
146✔
2926
                    lastAAMember = null;
7✔
2927
                    members.push(new CommentStatement([this.advance()]));
7✔
2928
                } else {
2929
                    let k = key();
139✔
2930
                    let expr = this.expression();
139✔
2931
                    lastAAMember = new AAMemberExpression(
138✔
2932
                        k.keyToken,
2933
                        k.colonToken,
2934
                        expr
2935
                    );
2936
                    members.push(lastAAMember);
138✔
2937
                }
2938

2939
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
145✔
2940
                    // collect comma at end of expression
2941
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
186✔
2942
                        lastAAMember.commaToken = this.previous();
33✔
2943
                    }
2944

2945
                    //check for comment at the end of the current line
2946
                    if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
186✔
2947
                        let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
14✔
2948
                        members.push(new CommentStatement([token]));
14✔
2949
                    } else {
2950
                        this.consumeStatementSeparators(true);
172✔
2951

2952
                        //check for a comment on its own line
2953
                        if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
172✔
2954
                            let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
1!
2955
                            lastAAMember = null;
1✔
2956
                            members.push(new CommentStatement([token]));
1✔
2957
                            continue;
1✔
2958
                        }
2959

2960
                        if (this.check(TokenKind.RightCurlyBrace)) {
171✔
2961
                            break;
116✔
2962
                        }
2963
                        let k = key();
55✔
2964
                        let expr = this.expression();
54✔
2965
                        lastAAMember = new AAMemberExpression(
54✔
2966
                            k.keyToken,
2967
                            k.colonToken,
2968
                            expr
2969
                        );
2970
                        members.push(lastAAMember);
54✔
2971
                    }
2972
                }
2973
            } catch (error: any) {
2974
                this.rethrowNonDiagnosticError(error);
2✔
2975
            }
2976

2977
            closingBrace = this.tryConsume(
146✔
2978
                DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral(),
2979
                TokenKind.RightCurlyBrace
2980
            );
2981
        } else {
2982
            closingBrace = this.previous();
48✔
2983
        }
2984

2985
        const aaExpr = new AALiteralExpression(members, openingBrace, closingBrace);
194✔
2986
        this.addPropertyHints(aaExpr);
194✔
2987
        return aaExpr;
194✔
2988
    }
2989

2990
    /**
2991
     * Pop token if we encounter specified token
2992
     */
2993
    private match(tokenKind: TokenKind) {
2994
        if (this.check(tokenKind)) {
19,925✔
2995
            this.current++; //advance
1,520✔
2996
            return true;
1,520✔
2997
        }
2998
        return false;
18,405✔
2999
    }
3000

3001
    /**
3002
     * Pop token if we encounter a token in the specified list
3003
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3004
     */
3005
    private matchAny(...tokenKinds: TokenKind[]) {
3006
        for (let tokenKind of tokenKinds) {
69,433✔
3007
            if (this.check(tokenKind)) {
203,957✔
3008
                this.current++; //advance
17,550✔
3009
                return true;
17,550✔
3010
            }
3011
        }
3012
        return false;
51,883✔
3013
    }
3014

3015
    /**
3016
     * If the next series of tokens matches the given set of tokens, pop them all
3017
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
3018
     */
3019
    private matchSequence(...tokenKinds: TokenKind[]) {
3020
        const endIndex = this.current + tokenKinds.length;
6,002✔
3021
        for (let i = 0; i < tokenKinds.length; i++) {
6,002✔
3022
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
6,026!
3023
                return false;
5,999✔
3024
            }
3025
        }
3026
        this.current = endIndex;
3✔
3027
        return true;
3✔
3028
    }
3029

3030
    /**
3031
     * Get next token matching a specified list, or fail with an error
3032
     */
3033
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
3034
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
6,787✔
3035
        if (token) {
6,787✔
3036
            return token;
6,770✔
3037
        } else {
3038
            let error = new Error(diagnosticInfo.message);
17✔
3039
            (error as any).isDiagnostic = true;
17✔
3040
            throw error;
17✔
3041
        }
3042
    }
3043

3044
    /**
3045
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3046
     */
3047
    private consumeTokenIf(tokenKind: TokenKind) {
3048
        if (this.match(tokenKind)) {
261✔
3049
            return this.previous();
11✔
3050
        }
3051
    }
3052

3053
    private consumeToken(tokenKind: TokenKind) {
3054
        return this.consume(
299✔
3055
            DiagnosticMessages.expectedToken(tokenKind),
3056
            tokenKind
3057
        );
3058
    }
3059

3060
    /**
3061
     * Consume, or add a message if not found. But then continue and return undefined
3062
     */
3063
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3064
        const nextKind = this.peek().kind;
10,038✔
3065
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
32,323✔
3066

3067
        if (foundTokenKind) {
10,038✔
3068
            return this.advance();
9,951✔
3069
        }
3070
        this.diagnostics.push({
87✔
3071
            ...diagnostic,
3072
            range: this.peek().range
3073
        });
3074
    }
3075

3076
    private tryConsumeToken(tokenKind: TokenKind) {
3077
        return this.tryConsume(
93✔
3078
            DiagnosticMessages.expectedToken(tokenKind),
3079
            tokenKind
3080
        );
3081
    }
3082

3083
    private consumeStatementSeparators(optional = false) {
3,885✔
3084
        //a comment or EOF mark the end of the statement
3085
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
13,199✔
3086
            return true;
581✔
3087
        }
3088
        let consumed = false;
12,618✔
3089
        //consume any newlines and colons
3090
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
12,618✔
3091
            consumed = true;
10,613✔
3092
        }
3093
        if (!optional && !consumed) {
12,618✔
3094
            this.diagnostics.push({
37✔
3095
                ...DiagnosticMessages.expectedNewlineOrColon(),
3096
                range: this.peek().range
3097
            });
3098
        }
3099
        return consumed;
12,618✔
3100
    }
3101

3102
    private advance(): Token {
3103
        if (!this.isAtEnd()) {
22,785✔
3104
            this.current++;
22,767✔
3105
        }
3106
        return this.previous();
22,785✔
3107
    }
3108

3109
    private checkEndOfStatement(): boolean {
3110
        const nextKind = this.peek().kind;
1,592✔
3111
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
1,592✔
3112
    }
3113

3114
    private checkPrevious(tokenKind: TokenKind): boolean {
3115
        return this.previous()?.kind === tokenKind;
677!
3116
    }
3117

3118
    /**
3119
     * Check that the next token kind is the expected kind
3120
     * @param tokenKind the expected next kind
3121
     * @returns true if the next tokenKind is the expected value
3122
     */
3123
    private check(tokenKind: TokenKind): boolean {
3124
        const nextKind = this.peek().kind;
346,974✔
3125
        if (nextKind === TokenKind.Eof) {
346,974✔
3126
            return false;
7,522✔
3127
        }
3128
        return nextKind === tokenKind;
339,452✔
3129
    }
3130

3131
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3132
        const nextKind = this.peek().kind;
41,973✔
3133
        if (nextKind === TokenKind.Eof) {
41,973✔
3134
            return false;
215✔
3135
        }
3136
        return tokenKinds.includes(nextKind);
41,758✔
3137
    }
3138

3139
    private checkNext(tokenKind: TokenKind): boolean {
3140
        if (this.isAtEnd()) {
4,756!
3141
            return false;
×
3142
        }
3143
        return this.peekNext().kind === tokenKind;
4,756✔
3144
    }
3145

3146
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3147
        if (this.isAtEnd()) {
2,526!
3148
            return false;
×
3149
        }
3150
        const nextKind = this.peekNext().kind;
2,526✔
3151
        return tokenKinds.includes(nextKind);
2,526✔
3152
    }
3153

3154
    private isAtEnd(): boolean {
3155
        return this.peek().kind === TokenKind.Eof;
63,041✔
3156
    }
3157

3158
    private peekNext(): Token {
3159
        if (this.isAtEnd()) {
7,292!
3160
            return this.peek();
×
3161
        }
3162
        return this.tokens[this.current + 1];
7,292✔
3163
    }
3164

3165
    private peek(): Token {
3166
        return this.tokens[this.current];
473,578✔
3167
    }
3168

3169
    private previous(): Token {
3170
        return this.tokens[this.current - 1];
32,398✔
3171
    }
3172

3173
    /**
3174
     * Sometimes we catch an error that is a diagnostic.
3175
     * If that's the case, we want to continue parsing.
3176
     * Otherwise, re-throw the error
3177
     *
3178
     * @param error error caught in a try/catch
3179
     */
3180
    private rethrowNonDiagnosticError(error) {
3181
        if (!error.isDiagnostic) {
9!
3182
            throw error;
×
3183
        }
3184
    }
3185

3186
    /**
3187
     * Get the token that is {offset} indexes away from {this.current}
3188
     * @param offset the number of index steps away from current index to fetch
3189
     * @param tokenKinds the desired token must match one of these
3190
     * @example
3191
     * getToken(-1); //returns the previous token.
3192
     * getToken(0);  //returns current token.
3193
     * getToken(1);  //returns next token
3194
     */
3195
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3196
        const token = this.tokens[this.current + offset];
143✔
3197
        if (tokenKinds.includes(token.kind)) {
143✔
3198
            return token;
3✔
3199
        }
3200
    }
3201

3202
    private synchronize() {
3203
        this.advance(); // skip the erroneous token
128✔
3204

3205
        while (!this.isAtEnd()) {
128✔
3206
            if (this.ensureNewLineOrColon(true)) {
207✔
3207
                // end of statement reached
3208
                return;
92✔
3209
            }
3210

3211
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
115✔
3212
                case TokenKind.Namespace:
8!
3213
                case TokenKind.Class:
3214
                case TokenKind.Function:
3215
                case TokenKind.Sub:
3216
                case TokenKind.If:
3217
                case TokenKind.For:
3218
                case TokenKind.ForEach:
3219
                case TokenKind.While:
3220
                case TokenKind.Print:
3221
                case TokenKind.Return:
3222
                    // start parsing again from the next block starter or obvious
3223
                    // expression start
3224
                    return;
1✔
3225
            }
3226

3227
            this.advance();
114✔
3228
        }
3229
    }
3230

3231
    /**
3232
     * References are found during the initial parse.
3233
     * However, sometimes plugins can modify the AST, requiring a full walk to re-compute all references.
3234
     * This does that walk.
3235
     */
3236
    private findReferences() {
3237
        this._references = new References();
7✔
3238
        const excludedExpressions = new Set<Expression>();
7✔
3239

3240
        const visitCallExpression = (e: CallExpression | CallfuncExpression) => {
7✔
3241
            for (const p of e.args) {
14✔
3242
                this._references.expressions.add(p);
7✔
3243
            }
3244
            //add calls that were not excluded (from loop below)
3245
            if (!excludedExpressions.has(e)) {
14✔
3246
                this._references.expressions.add(e);
12✔
3247
            }
3248

3249
            //if this call is part of a longer expression that includes a call higher up, find that higher one and remove it
3250
            if (e.callee) {
14!
3251
                let node: Expression = e.callee;
14✔
3252
                while (node) {
14✔
3253
                    //the primary goal for this loop. If we found a parent call expression, remove it from `references`
3254
                    if (isCallExpression(node)) {
22✔
3255
                        this.references.expressions.delete(node);
2✔
3256
                        excludedExpressions.add(node);
2✔
3257
                        //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.
3258
                        break;
2✔
3259

3260
                        //when we hit a variable expression, we're definitely at the leftmost expression so stop
3261
                    } else if (isVariableExpression(node)) {
20✔
3262
                        break;
12✔
3263
                        //if
3264

3265
                    } else if (isDottedGetExpression(node) || isIndexedGetExpression(node)) {
8!
3266
                        node = node.obj;
8✔
3267
                    } else {
3268
                        //some expression we don't understand. log it and quit the loop
3269
                        this.logger.info('Encountered unknown expression while calculating function expression chain', node);
×
3270
                        break;
×
3271
                    }
3272
                }
3273
            }
3274
        };
3275

3276
        this.ast.walk(createVisitor({
7✔
3277
            AssignmentStatement: s => {
3278
                this._references.assignmentStatements.push(s);
11✔
3279
                this.references.expressions.add(s.value);
11✔
3280
            },
3281
            ClassStatement: s => {
3282
                this._references.classStatements.push(s);
1✔
3283
            },
3284
            ClassFieldStatement: s => {
3285
                if (s.initialValue) {
1!
3286
                    this._references.expressions.add(s.initialValue);
1✔
3287
                }
3288
            },
3289
            NamespaceStatement: s => {
3290
                this._references.namespaceStatements.push(s);
×
3291
            },
3292
            FunctionStatement: s => {
3293
                this._references.functionStatements.push(s);
4✔
3294
            },
3295
            ImportStatement: s => {
3296
                this._references.importStatements.push(s);
1✔
3297
            },
3298
            LibraryStatement: s => {
3299
                this._references.libraryStatements.push(s);
×
3300
            },
3301
            FunctionExpression: (expression, parent) => {
3302
                if (!isMethodStatement(parent)) {
4!
3303
                    this._references.functionExpressions.push(expression);
4✔
3304
                }
3305
            },
3306
            NewExpression: e => {
3307
                this._references.newExpressions.push(e);
×
3308
                for (const p of e.call.args) {
×
3309
                    this._references.expressions.add(p);
×
3310
                }
3311
            },
3312
            ExpressionStatement: s => {
3313
                this._references.expressions.add(s.expression);
7✔
3314
            },
3315
            CallfuncExpression: e => {
3316
                visitCallExpression(e);
1✔
3317
            },
3318
            CallExpression: e => {
3319
                visitCallExpression(e);
13✔
3320
            },
3321
            AALiteralExpression: e => {
3322
                this.addPropertyHints(e);
8✔
3323
                this._references.expressions.add(e);
8✔
3324
                for (const member of e.elements) {
8✔
3325
                    if (isAAMemberExpression(member)) {
16!
3326
                        this._references.expressions.add(member.value);
16✔
3327
                    }
3328
                }
3329
            },
3330
            BinaryExpression: (e, parent) => {
3331
                //walk the chain of binary expressions and add each one to the list of expressions
3332
                const expressions: Expression[] = [e];
14✔
3333
                let expression: Expression;
3334
                while ((expression = expressions.pop())) {
14✔
3335
                    if (isBinaryExpression(expression)) {
64✔
3336
                        expressions.push(expression.left, expression.right);
25✔
3337
                    } else {
3338
                        this._references.expressions.add(expression);
39✔
3339
                    }
3340
                }
3341
            },
3342
            ArrayLiteralExpression: e => {
3343
                for (const element of e.elements) {
1✔
3344
                    //keep everything except comments
3345
                    if (!isCommentStatement(element)) {
1!
3346
                        this._references.expressions.add(element);
1✔
3347
                    }
3348
                }
3349
            },
3350
            DottedGetExpression: e => {
3351
                this.addPropertyHints(e.name);
23✔
3352
            },
3353
            DottedSetStatement: e => {
3354
                this.addPropertyHints(e.name);
4✔
3355
            },
3356
            EnumStatement: e => {
3357
                this._references.enumStatements.push(e);
×
3358
            },
3359
            ConstStatement: s => {
3360
                this._references.constStatements.push(s);
×
3361
            },
3362
            UnaryExpression: e => {
3363
                this._references.expressions.add(e);
×
3364
            },
3365
            IncrementStatement: e => {
3366
                this._references.expressions.add(e);
2✔
3367
            }
3368
        }), {
3369
            walkMode: WalkMode.visitAllRecursive
3370
        });
3371
    }
3372

3373
    public dispose() {
3374
    }
3375
}
3376

3377
export enum ParseMode {
1✔
3378
    BrightScript = 'BrightScript',
1✔
3379
    BrighterScript = 'BrighterScript'
1✔
3380
}
3381

3382
export interface ParseOptions {
3383
    /**
3384
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3385
     */
3386
    mode?: ParseMode;
3387
    /**
3388
     * A logger that should be used for logging. If omitted, a default logger is used
3389
     */
3390
    logger?: Logger;
3391
    /**
3392
     * Should locations be tracked. If false, the `range` property will be omitted
3393
     * @default true
3394
     */
3395
    trackLocations?: boolean;
3396
}
3397

3398
export class References {
1✔
3399
    private cache = new Cache();
2,135✔
3400
    public assignmentStatements = [] as AssignmentStatement[];
2,135✔
3401
    public classStatements = [] as ClassStatement[];
2,135✔
3402

3403
    public get classStatementLookup() {
3404
        if (!this._classStatementLookup) {
17✔
3405
            this._classStatementLookup = new Map();
15✔
3406
            for (const stmt of this.classStatements) {
15✔
3407
                this._classStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
2✔
3408
            }
3409
        }
3410
        return this._classStatementLookup;
17✔
3411
    }
3412
    private _classStatementLookup: Map<string, ClassStatement>;
3413

3414
    public functionExpressions = [] as FunctionExpression[];
2,135✔
3415
    public functionStatements = [] as FunctionStatement[];
2,135✔
3416
    /**
3417
     * A map of function statements, indexed by fully-namespaced lower function name.
3418
     */
3419
    public get functionStatementLookup() {
3420
        if (!this._functionStatementLookup) {
17✔
3421
            this._functionStatementLookup = new Map();
15✔
3422
            for (const stmt of this.functionStatements) {
15✔
3423
                this._functionStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
13✔
3424
            }
3425
        }
3426
        return this._functionStatementLookup;
17✔
3427
    }
3428
    private _functionStatementLookup: Map<string, FunctionStatement>;
3429

3430
    public interfaceStatements = [] as InterfaceStatement[];
2,135✔
3431

3432
    public get interfaceStatementLookup() {
3433
        if (!this._interfaceStatementLookup) {
×
3434
            this._interfaceStatementLookup = new Map();
×
3435
            for (const stmt of this.interfaceStatements) {
×
3436
                this._interfaceStatementLookup.set(stmt.fullName.toLowerCase(), stmt);
×
3437
            }
3438
        }
3439
        return this._interfaceStatementLookup;
×
3440
    }
3441
    private _interfaceStatementLookup: Map<string, InterfaceStatement>;
3442

3443
    public enumStatements = [] as EnumStatement[];
2,135✔
3444

3445
    public get enumStatementLookup() {
3446
        return this.cache.getOrAdd('enums', () => {
18✔
3447
            const result = new Map<string, EnumStatement>();
16✔
3448
            for (const stmt of this.enumStatements) {
16✔
3449
                result.set(stmt.fullName.toLowerCase(), stmt);
1✔
3450
            }
3451
            return result;
16✔
3452
        });
3453
    }
3454

3455
    public constStatements = [] as ConstStatement[];
2,135✔
3456

3457
    public get constStatementLookup() {
3458
        return this.cache.getOrAdd('consts', () => {
×
3459
            const result = new Map<string, ConstStatement>();
×
3460
            for (const stmt of this.constStatements) {
×
3461
                result.set(stmt.fullName.toLowerCase(), stmt);
×
3462
            }
3463
            return result;
×
3464
        });
3465
    }
3466

3467
    /**
3468
     * A collection of full expressions. This excludes intermediary expressions.
3469
     *
3470
     * Example 1:
3471
     * `a.b.c` is composed of `a` (variableExpression)  `.b` (DottedGetExpression) `.c` (DottedGetExpression)
3472
     * This will only contain the final `.c` DottedGetExpression because `.b` and `a` can both be derived by walking back from the `.c` DottedGetExpression.
3473
     *
3474
     * Example 2:
3475
     * `name.space.doSomething(a.b.c)` will result in 2 entries in this list. the `CallExpression` for `doSomething`, and the `.c` DottedGetExpression.
3476
     *
3477
     * Example 3:
3478
     * `value = SomeEnum.value > 2 or SomeEnum.otherValue < 10` will result in 4 entries. `SomeEnum.value`, `2`, `SomeEnum.otherValue`, `10`
3479
     */
3480
    public expressions = new Set<Expression>();
2,135✔
3481

3482
    public importStatements = [] as ImportStatement[];
2,135✔
3483
    public libraryStatements = [] as LibraryStatement[];
2,135✔
3484
    public namespaceStatements = [] as NamespaceStatement[];
2,135✔
3485
    public newExpressions = [] as NewExpression[];
2,135✔
3486
    public propertyHints = {} as Record<string, string>;
2,135✔
3487
}
3488

3489
class CancelStatementError extends Error {
3490
    constructor() {
3491
        super('CancelStatement');
2✔
3492
    }
3493
}
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