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

rokucommunity / brighterscript / #13342

25 Nov 2024 08:44PM UTC coverage: 89.053% (+2.2%) from 86.874%
#13342

push

web-flow
Merge 961502182 into c5674f5d8

7359 of 8712 branches covered (84.47%)

Branch coverage included in aggregate %.

55 of 64 new or added lines in 9 files covered. (85.94%)

544 existing lines in 54 files now uncovered.

9724 of 10471 relevant lines covered (92.87%)

1825.46 hits per line

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

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

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

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

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

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

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

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

143
    private _references = new References();
2,101✔
144

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

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

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

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

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

183
    private globalTerminators = [] as TokenKind[][];
2,101✔
184

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

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

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

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

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

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

237
        this.ast = this.body();
2,086✔
238

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

244
    private logger: Logger;
245

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

500
                let decl: Statement;
501

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1118
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
3,439✔
1119
            return this.printStatement();
659✔
1120
        }
1121
        if (this.check(TokenKind.Dim)) {
2,780✔
1122
            return this.dimStatement();
43✔
1123
        }
1124

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

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

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

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

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

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

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

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

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

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

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

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

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

1216
        if (this.check(TokenKind.Namespace)) {
1,021✔
1217
            return this.namespaceStatement();
243✔
1218
        }
1219

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

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

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

1232
        this.consumeStatementSeparators();
22✔
1233

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

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

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

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

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

1265
        //TODO: newline allowed?
1266

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

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

1279
        this.consumeStatementSeparators();
33✔
1280

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

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

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

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

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

1333
        this.consumeStatementSeparators();
20✔
1334

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

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

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

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

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

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

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

1384
        this.namespaceAndFunctionDepth++;
243✔
1385

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

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

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

1405
        this.namespaceAndFunctionDepth--;
242✔
1406

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1914
            if (!isIfStatement(elseBranch)) {
177✔
1915
                if (this.check(TokenKind.EndIf)) {
137✔
1916
                    endIfToken = this.advance();
135✔
1917

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2115
    private printStatement(): PrintStatement {
2116
        let printKeyword = this.advance();
659✔
2117

2118
        let values: (
2119
            | Expression
2120
            | PrintSeparatorTab
2121
            | PrintSeparatorSpace)[] = [];
659✔
2122

2123
        while (!this.checkEndOfStatement()) {
659✔
2124
            if (this.check(TokenKind.Semicolon)) {
743✔
2125
                values.push(this.advance() as PrintSeparatorSpace);
20✔
2126
            } else if (this.check(TokenKind.Comma)) {
723✔
2127
                values.push(this.advance() as PrintSeparatorTab);
13✔
2128
            } else if (this.check(TokenKind.Else)) {
710✔
2129
                break; // inline branch
7✔
2130
            } else {
2131
                values.push(this.expression());
703✔
2132
            }
2133
        }
2134

2135
        //print statements can be empty, so look for empty print conditions
2136
        if (!values.length) {
658✔
2137
            let emptyStringLiteral = createStringLiteral('');
4✔
2138
            values.push(emptyStringLiteral);
4✔
2139
        }
2140

2141
        let last = values[values.length - 1];
658✔
2142
        if (isToken(last)) {
658✔
2143
            // TODO: error, expected value
2144
        }
2145

2146
        return new PrintStatement({ print: printKeyword }, values);
658✔
2147
    }
2148

2149
    /**
2150
     * Parses a return statement with an optional return value.
2151
     * @returns an AST representation of a return statement.
2152
     */
2153
    private returnStatement(): ReturnStatement {
2154
        let tokens = { return: this.previous() };
179✔
2155

2156
        if (this.checkEndOfStatement()) {
179✔
2157
            return new ReturnStatement(tokens);
5✔
2158
        }
2159

2160
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
174✔
2161
        return new ReturnStatement(tokens, toReturn);
173✔
2162
    }
2163

2164
    /**
2165
     * Parses a `label` statement
2166
     * @returns an AST representation of an `label` statement.
2167
     */
2168
    private labelStatement() {
2169
        let tokens = {
12✔
2170
            identifier: this.advance(),
2171
            colon: this.advance()
2172
        };
2173

2174
        //label must be alone on its line, this is probably not a label
2175
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
12✔
2176
            //rewind and cancel
2177
            this.current -= 2;
2✔
2178
            throw new CancelStatementError();
2✔
2179
        }
2180

2181
        return new LabelStatement(tokens);
10✔
2182
    }
2183

2184
    /**
2185
     * Parses a `continue` statement
2186
     */
2187
    private continueStatement() {
2188
        return new ContinueStatement({
12✔
2189
            continue: this.advance(),
2190
            loopType: this.tryConsume(
2191
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2192
                TokenKind.While, TokenKind.For
2193
            )
2194
        });
2195
    }
2196

2197
    /**
2198
     * Parses a `goto` statement
2199
     * @returns an AST representation of an `goto` statement.
2200
     */
2201
    private gotoStatement() {
2202
        let tokens = {
12✔
2203
            goto: this.advance(),
2204
            label: this.consume(
2205
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2206
                TokenKind.Identifier
2207
            )
2208
        };
2209

2210
        return new GotoStatement(tokens);
10✔
2211
    }
2212

2213
    /**
2214
     * Parses an `end` statement
2215
     * @returns an AST representation of an `end` statement.
2216
     */
2217
    private endStatement() {
2218
        let endTokens = { end: this.advance() };
8✔
2219

2220
        return new EndStatement(endTokens);
8✔
2221
    }
2222
    /**
2223
     * Parses a `stop` statement
2224
     * @returns an AST representation of a `stop` statement
2225
     */
2226
    private stopStatement() {
2227
        let tokens = { stop: this.advance() };
16✔
2228

2229
        return new StopStatement(tokens);
16✔
2230
    }
2231

2232
    /**
2233
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2234
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2235
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2236
     *                    ignored.
2237
     */
2238
    private block(...terminators: BlockTerminator[]): Block | undefined {
2239
        const parentAnnotations = this.enterAnnotationBlock();
2,058✔
2240

2241
        this.consumeStatementSeparators(true);
2,058✔
2242
        let startingToken = this.peek();
2,058✔
2243

2244
        const statements: Statement[] = [];
2,058✔
2245
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators)) {
2,058✔
2246
            //grab the location of the current token
2247
            let loopCurrent = this.current;
2,416✔
2248
            let dec = this.declaration();
2,416✔
2249
            if (dec) {
2,416✔
2250
                if (!isAnnotationExpression(dec)) {
2,333✔
2251
                    this.consumePendingAnnotations(dec);
2,326✔
2252
                    statements.push(dec);
2,326✔
2253
                }
2254

2255
                //ensure statement separator
2256
                this.consumeStatementSeparators();
2,333✔
2257

2258
            } else {
2259
                //something went wrong. reset to the top of the loop
2260
                this.current = loopCurrent;
83✔
2261

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

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

2269
                //consume potential separators
2270
                this.consumeStatementSeparators(true);
83✔
2271
            }
2272
        }
2273

2274
        if (this.isAtEnd()) {
2,058✔
2275
            return undefined;
5✔
2276
            // TODO: Figure out how to handle unterminated blocks well
2277
        } else if (terminators.length > 0) {
2,053✔
2278
            //did we hit end-sub / end-function while looking for some other terminator?
2279
            //if so, we need to restore the statement separator
2280
            let prev = this.previous().kind;
356✔
2281
            let peek = this.peek().kind;
356✔
2282
            if (
356✔
2283
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
716!
2284
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2285
            ) {
2286
                this.current--;
6✔
2287
            }
2288
        }
2289

2290
        this.exitAnnotationBlock(parentAnnotations);
2,053✔
2291
        return new Block(statements, startingToken.range);
2,053✔
2292
    }
2293

2294
    /**
2295
     * Attach pending annotations to the provided statement,
2296
     * and then reset the annotations array
2297
     */
2298
    consumePendingAnnotations(statement: Statement) {
2299
        if (this.pendingAnnotations.length) {
5,890✔
2300
            statement.annotations = this.pendingAnnotations;
45✔
2301
            this.pendingAnnotations = [];
45✔
2302
        }
2303
    }
2304

2305
    enterAnnotationBlock() {
2306
        const pending = this.pendingAnnotations;
5,024✔
2307
        this.pendingAnnotations = [];
5,024✔
2308
        return pending;
5,024✔
2309
    }
2310

2311
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2312
        // non consumed annotations are an error
2313
        if (this.pendingAnnotations.length) {
5,018✔
2314
            for (const annotation of this.pendingAnnotations) {
4✔
2315
                this.diagnostics.push({
6✔
2316
                    ...DiagnosticMessages.unusedAnnotation(),
2317
                    range: annotation.range
2318
                });
2319
            }
2320
        }
2321
        this.pendingAnnotations = parentAnnotations;
5,018✔
2322
    }
2323

2324
    private expression(findTypeCast = true): Expression {
3,977✔
2325
        let expression = this.anonymousFunction();
4,223✔
2326
        let asToken: Token;
2327
        let typeToken: Token;
2328
        if (findTypeCast) {
4,188✔
2329
            do {
3,942✔
2330
                if (this.check(TokenKind.As)) {
3,955✔
2331
                    this.warnIfNotBrighterScriptMode('type cast');
13✔
2332
                    // Check if this expression is wrapped in any type casts
2333
                    // allows for multiple casts:
2334
                    // myVal = foo() as dynamic as string
2335

2336
                    asToken = this.advance();
13✔
2337
                    typeToken = this.typeToken();
13✔
2338
                    if (asToken && typeToken) {
13!
2339
                        expression = new TypeCastExpression(expression, asToken, typeToken);
13✔
2340
                    }
2341
                } else {
2342
                    break;
3,942✔
2343
                }
2344

2345
            } while (asToken && typeToken);
26✔
2346
        }
2347
        this._references.expressions.add(expression);
4,188✔
2348
        return expression;
4,188✔
2349
    }
2350

2351
    private anonymousFunction(): Expression {
2352
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
4,223✔
2353
            const func = this.functionDeclaration(true);
72✔
2354
            //if there's an open paren after this, this is an IIFE
2355
            if (this.check(TokenKind.LeftParen)) {
72✔
2356
                return this.finishCall(this.advance(), func);
3✔
2357
            } else {
2358
                return func;
69✔
2359
            }
2360
        }
2361

2362
        let expr = this.boolean();
4,151✔
2363

2364
        if (this.check(TokenKind.Question)) {
4,116✔
2365
            return this.ternaryExpression(expr);
91✔
2366
        } else if (this.check(TokenKind.QuestionQuestion)) {
4,025✔
2367
            return this.nullCoalescingExpression(expr);
29✔
2368
        } else {
2369
            return expr;
3,996✔
2370
        }
2371
    }
2372

2373
    private boolean(): Expression {
2374
        let expr = this.relational();
4,151✔
2375

2376
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
4,116✔
2377
            let operator = this.previous();
28✔
2378
            let right = this.relational();
28✔
2379
            this.addExpressionsToReferences(expr, right);
28✔
2380
            expr = new BinaryExpression(expr, operator, right);
28✔
2381
        }
2382

2383
        return expr;
4,116✔
2384
    }
2385

2386
    private relational(): Expression {
2387
        let expr = this.additive();
4,195✔
2388

2389
        while (
4,160✔
2390
            this.matchAny(
2391
                TokenKind.Equal,
2392
                TokenKind.LessGreater,
2393
                TokenKind.Greater,
2394
                TokenKind.GreaterEqual,
2395
                TokenKind.Less,
2396
                TokenKind.LessEqual
2397
            )
2398
        ) {
2399
            let operator = this.previous();
149✔
2400
            let right = this.additive();
149✔
2401
            this.addExpressionsToReferences(expr, right);
149✔
2402
            expr = new BinaryExpression(expr, operator, right);
149✔
2403
        }
2404

2405
        return expr;
4,160✔
2406
    }
2407

2408
    private addExpressionsToReferences(...expressions: Expression[]) {
2409
        for (const expression of expressions) {
336✔
2410
            if (!isBinaryExpression(expression)) {
626✔
2411
                this.references.expressions.add(expression);
584✔
2412
            }
2413
        }
2414
    }
2415

2416
    // TODO: bitshift
2417

2418
    private additive(): Expression {
2419
        let expr = this.multiplicative();
4,344✔
2420

2421
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
4,309✔
2422
            let operator = this.previous();
86✔
2423
            let right = this.multiplicative();
86✔
2424
            this.addExpressionsToReferences(expr, right);
86✔
2425
            expr = new BinaryExpression(expr, operator, right);
86✔
2426
        }
2427

2428
        return expr;
4,309✔
2429
    }
2430

2431
    private multiplicative(): Expression {
2432
        let expr = this.exponential();
4,430✔
2433

2434
        while (this.matchAny(
4,395✔
2435
            TokenKind.Forwardslash,
2436
            TokenKind.Backslash,
2437
            TokenKind.Star,
2438
            TokenKind.Mod,
2439
            TokenKind.LeftShift,
2440
            TokenKind.RightShift
2441
        )) {
2442
            let operator = this.previous();
21✔
2443
            let right = this.exponential();
21✔
2444
            this.addExpressionsToReferences(expr, right);
21✔
2445
            expr = new BinaryExpression(expr, operator, right);
21✔
2446
        }
2447

2448
        return expr;
4,395✔
2449
    }
2450

2451
    private exponential(): Expression {
2452
        let expr = this.prefixUnary();
4,451✔
2453

2454
        while (this.match(TokenKind.Caret)) {
4,416✔
2455
            let operator = this.previous();
6✔
2456
            let right = this.prefixUnary();
6✔
2457
            this.addExpressionsToReferences(expr, right);
6✔
2458
            expr = new BinaryExpression(expr, operator, right);
6✔
2459
        }
2460

2461
        return expr;
4,416✔
2462
    }
2463

2464
    private prefixUnary(): Expression {
2465
        const nextKind = this.peek().kind;
4,479✔
2466
        if (nextKind === TokenKind.Not) {
4,479✔
2467
            this.current++; //advance
16✔
2468
            let operator = this.previous();
16✔
2469
            let right = this.relational();
16✔
2470
            return new UnaryExpression(operator, right);
16✔
2471
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
4,463✔
2472
            this.current++; //advance
22✔
2473
            let operator = this.previous();
22✔
2474
            let right = this.prefixUnary();
22✔
2475
            return new UnaryExpression(operator, right);
22✔
2476
        }
2477
        return this.call();
4,441✔
2478
    }
2479

2480
    private indexedGet(expr: Expression) {
2481
        let openingSquare = this.previous();
140✔
2482
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
140✔
2483
        let indexes: Expression[] = [];
140✔
2484

2485

2486
        //consume leading newlines
2487
        while (this.match(TokenKind.Newline)) { }
140✔
2488

2489
        try {
140✔
2490
            indexes.push(
140✔
2491
                this.expression()
2492
            );
2493
            //consume additional indexes separated by commas
2494
            while (this.check(TokenKind.Comma)) {
139✔
2495
                //discard the comma
2496
                this.advance();
17✔
2497
                indexes.push(
17✔
2498
                    this.expression()
2499
                );
2500
            }
2501
        } catch (error) {
2502
            this.rethrowNonDiagnosticError(error);
1✔
2503
        }
2504
        //consume trailing newlines
2505
        while (this.match(TokenKind.Newline)) { }
140✔
2506

2507
        const closingSquare = this.tryConsume(
140✔
2508
            DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
2509
            TokenKind.RightSquareBracket
2510
        );
2511

2512
        return new IndexedGetExpression(expr, indexes.shift(), openingSquare, closingSquare, questionDotToken, indexes);
140✔
2513
    }
2514

2515
    private newExpression() {
2516
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
44✔
2517
        let newToken = this.advance();
44✔
2518

2519
        let nameExpr = this.getNamespacedVariableNameExpression();
44✔
2520
        let leftParen = this.consume(
44✔
2521
            DiagnosticMessages.unexpectedToken(this.peek().text),
2522
            TokenKind.LeftParen,
2523
            TokenKind.QuestionLeftParen
2524
        );
2525
        let call = this.finishCall(leftParen, nameExpr);
40✔
2526
        //pop the call from the  callExpressions list because this is technically something else
2527
        this.callExpressions.pop();
40✔
2528
        let result = new NewExpression(newToken, call);
40✔
2529
        this._references.newExpressions.push(result);
40✔
2530
        return result;
40✔
2531
    }
2532

2533
    /**
2534
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2535
     */
2536
    private callfunc(callee: Expression): Expression {
2537
        this.warnIfNotBrighterScriptMode('callfunc operator');
25✔
2538
        let operator = this.previous();
25✔
2539
        let methodName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
25✔
2540
        // force it into an identifier so the AST makes some sense
2541
        methodName.kind = TokenKind.Identifier;
24✔
2542
        let openParen = this.consume(DiagnosticMessages.expectedOpenParenToFollowCallfuncIdentifier(), TokenKind.LeftParen);
24✔
2543
        let call = this.finishCall(openParen, callee, false);
24✔
2544

2545
        return new CallfuncExpression(callee, operator, methodName as Identifier, openParen, call.args, call.closingParen);
24✔
2546
    }
2547

2548
    private call(): Expression {
2549
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
5,105✔
2550
            return this.newExpression();
44✔
2551
        }
2552
        let expr = this.primary();
5,061✔
2553
        //an expression to keep for _references
2554
        let referenceCallExpression: Expression;
2555
        while (true) {
4,995✔
2556
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
6,628✔
2557
                expr = this.finishCall(this.previous(), expr);
535✔
2558
                //store this call expression in references
2559
                referenceCallExpression = expr;
535✔
2560

2561
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
6,093✔
2562
                expr = this.indexedGet(expr);
138✔
2563

2564
            } else if (this.match(TokenKind.Callfunc)) {
5,955✔
2565
                expr = this.callfunc(expr);
25✔
2566
                //store this callfunc expression in references
2567
                referenceCallExpression = expr;
24✔
2568

2569
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
5,930✔
2570
                if (this.match(TokenKind.LeftSquareBracket)) {
960✔
2571
                    expr = this.indexedGet(expr);
2✔
2572
                } else {
2573
                    let dot = this.previous();
958✔
2574
                    let name = this.tryConsume(
958✔
2575
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2576
                        TokenKind.Identifier,
2577
                        ...AllowedProperties
2578
                    );
2579
                    if (!name) {
958✔
2580
                        break;
24✔
2581
                    }
2582

2583
                    // force it into an identifier so the AST makes some sense
2584
                    name.kind = TokenKind.Identifier;
934✔
2585
                    expr = new DottedGetExpression(expr, name as Identifier, dot);
934✔
2586

2587
                    this.addPropertyHints(name);
934✔
2588
                }
2589

2590
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
4,970✔
2591
                let dot = this.advance();
11✔
2592
                let name = this.tryConsume(
11✔
2593
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2594
                    TokenKind.Identifier,
2595
                    ...AllowedProperties
2596
                );
2597

2598
                // force it into an identifier so the AST makes some sense
2599
                name.kind = TokenKind.Identifier;
11✔
2600
                if (!name) {
11!
UNCOV
2601
                    break;
×
2602
                }
2603
                expr = new XmlAttributeGetExpression(expr, name as Identifier, dot);
11✔
2604
                //only allow a single `@` expression
2605
                break;
11✔
2606

2607
            } else {
2608
                break;
4,959✔
2609
            }
2610
        }
2611
        //if we found a callExpression, add it to `expressions` in references
2612
        if (referenceCallExpression) {
4,994✔
2613
            this._references.expressions.add(referenceCallExpression);
522✔
2614
        }
2615
        return expr;
4,994✔
2616
    }
2617

2618
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
578✔
2619
        let args = [] as Expression[];
626✔
2620
        while (this.match(TokenKind.Newline)) { }
626✔
2621

2622
        if (!this.check(TokenKind.RightParen)) {
626✔
2623
            do {
324✔
2624
                while (this.match(TokenKind.Newline)) { }
495✔
2625

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

2643
        while (this.match(TokenKind.Newline)) { }
626✔
2644

2645
        const closingParen = this.tryConsume(
626✔
2646
            DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
2647
            TokenKind.RightParen
2648
        );
2649

2650
        let expression = new CallExpression(callee, openingParen, closingParen, args);
626✔
2651
        if (addToCallExpressionList) {
626✔
2652
            this.callExpressions.push(expression);
578✔
2653
        }
2654
        return expression;
626✔
2655
    }
2656

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

2671
            if (this.checkAny(...DeclarableTypes)) {
571✔
2672
                // Token is a built in type
2673
                typeToken = this.advance();
468✔
2674
            } else if (this.options.mode === ParseMode.BrighterScript) {
103✔
2675
                try {
80✔
2676
                    // see if we can get a namespaced identifer
2677
                    const qualifiedType = this.getNamespacedVariableNameExpression(ignoreDiagnostics);
80✔
2678
                    typeToken = createToken(TokenKind.Identifier, qualifiedType.getName(this.options.mode), qualifiedType.range);
77✔
2679
                } catch {
2680
                    //could not get an identifier - just get whatever's next
2681
                    typeToken = this.advance();
3✔
2682
                }
2683
            } else {
2684
                // just get whatever's next
2685
                typeToken = this.advance();
23✔
2686
            }
2687
            resultToken = resultToken ?? typeToken;
571✔
2688
            if (resultToken && this.options.mode === ParseMode.BrighterScript) {
571✔
2689
                // check for brackets
2690
                while (this.check(TokenKind.LeftSquareBracket) && this.peekNext().kind === TokenKind.RightSquareBracket) {
475✔
2691
                    const leftBracket = this.advance();
10✔
2692
                    const rightBracket = this.advance();
10✔
2693
                    typeToken = createToken(TokenKind.Identifier, typeToken.text + leftBracket.text + rightBracket.text, util.createBoundingRange(typeToken, leftBracket, rightBracket));
10✔
2694
                    resultToken = createToken(TokenKind.Dynamic, null, typeToken.range);
10✔
2695
                }
2696

2697
                if (this.check(TokenKind.Or)) {
475✔
2698
                    lookForUnions = true;
7✔
2699
                    let orToken = this.advance();
7✔
2700
                    resultToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(resultToken, typeToken, orToken));
7✔
2701
                    isAUnion = true;
7✔
2702
                }
2703
            }
2704
        }
2705
        if (isAUnion) {
564✔
2706
            resultToken = createToken(TokenKind.Dynamic, null, util.createBoundingRange(resultToken, typeToken));
5✔
2707
        }
2708
        return resultToken;
564✔
2709
    }
2710

2711
    private primary(): Expression {
2712
        switch (true) {
5,061✔
2713
            case this.matchAny(
5,061!
2714
                TokenKind.False,
2715
                TokenKind.True,
2716
                TokenKind.Invalid,
2717
                TokenKind.IntegerLiteral,
2718
                TokenKind.LongIntegerLiteral,
2719
                TokenKind.FloatLiteral,
2720
                TokenKind.DoubleLiteral,
2721
                TokenKind.StringLiteral
2722
            ):
2723
                return new LiteralExpression(this.previous());
3,039✔
2724

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

2729
            //template string
2730
            case this.check(TokenKind.BackTick):
2731
                return this.templateString(false);
43✔
2732

2733
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
2734
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
3,415✔
2735
                return this.templateString(true);
8✔
2736

2737
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
2738
                return new VariableExpression(this.previous() as Identifier);
1,470✔
2739

2740
            case this.match(TokenKind.LeftParen):
2741
                let left = this.previous();
37✔
2742
                let expr = this.expression();
37✔
2743
                let right = this.consume(
36✔
2744
                    DiagnosticMessages.unmatchedLeftParenAfterExpression(),
2745
                    TokenKind.RightParen
2746
                );
2747
                return new GroupingExpression({ left: left, right: right }, expr);
36✔
2748

2749
            case this.matchAny(TokenKind.LeftSquareBracket):
2750
                return this.arrayLiteral();
125✔
2751

2752
            case this.match(TokenKind.LeftCurlyBrace):
2753
                return this.aaLiteral();
193✔
2754

2755
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
UNCOV
2756
                let token = Object.assign(this.previous(), {
×
2757
                    kind: TokenKind.Identifier
2758
                }) as Identifier;
UNCOV
2759
                return new VariableExpression(token);
×
2760

2761
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
UNCOV
2762
                return this.anonymousFunction();
×
2763

2764
            case this.check(TokenKind.RegexLiteral):
2765
                return this.regexLiteralExpression();
45✔
2766

2767
            case this.check(TokenKind.Comment):
2768
                return new CommentStatement([this.advance()]);
3✔
2769

2770
            default:
2771
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
2772
                if (this.checkAny(...this.peekGlobalTerminators())) {
63!
2773
                    //don't throw a diagnostic, just return undefined
2774

2775
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
2776
                } else {
2777
                    this.diagnostics.push({
63✔
2778
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
2779
                        range: this.peek().range
2780
                    });
2781
                    throw this.lastDiagnosticAsError();
63✔
2782
                }
2783
        }
2784
    }
2785

2786
    private arrayLiteral() {
2787
        let elements: Array<Expression | CommentStatement> = [];
125✔
2788
        let openingSquare = this.previous();
125✔
2789

2790
        //add any comment found right after the opening square
2791
        if (this.check(TokenKind.Comment)) {
125✔
2792
            elements.push(new CommentStatement([this.advance()]));
1✔
2793
        }
2794

2795
        while (this.match(TokenKind.Newline)) {
125✔
2796
        }
2797
        let closingSquare: Token;
2798

2799
        if (!this.match(TokenKind.RightSquareBracket)) {
125✔
2800
            try {
94✔
2801
                elements.push(this.expression());
94✔
2802

2803
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
93✔
2804
                    if (this.checkPrevious(TokenKind.Comment) || this.check(TokenKind.Comment)) {
120✔
2805
                        let comment = this.check(TokenKind.Comment) ? this.advance() : this.previous();
4✔
2806
                        elements.push(new CommentStatement([comment]));
4✔
2807
                    }
2808
                    while (this.match(TokenKind.Newline)) {
120✔
2809

2810
                    }
2811

2812
                    if (this.check(TokenKind.RightSquareBracket)) {
120✔
2813
                        break;
30✔
2814
                    }
2815

2816
                    elements.push(this.expression());
90✔
2817
                }
2818
            } catch (error: any) {
2819
                this.rethrowNonDiagnosticError(error);
2✔
2820
            }
2821

2822
            closingSquare = this.tryConsume(
94✔
2823
                DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral(),
2824
                TokenKind.RightSquareBracket
2825
            );
2826
        } else {
2827
            closingSquare = this.previous();
31✔
2828
        }
2829

2830
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2831
        return new ArrayLiteralExpression(elements, openingSquare, closingSquare);
125✔
2832
    }
2833

2834
    private aaLiteral() {
2835
        let openingBrace = this.previous();
193✔
2836
        let members: Array<AAMemberExpression | CommentStatement> = [];
193✔
2837

2838
        let key = () => {
193✔
2839
            let result = {
192✔
2840
                colonToken: null as Token,
2841
                keyToken: null as Token,
2842
                range: null as Range
2843
            };
2844
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
192✔
2845
                result.keyToken = this.identifier(...AllowedProperties);
161✔
2846
            } else if (this.check(TokenKind.StringLiteral)) {
31!
2847
                result.keyToken = this.advance();
31✔
2848
            } else {
UNCOV
2849
                this.diagnostics.push({
×
2850
                    ...DiagnosticMessages.unexpectedAAKey(),
2851
                    range: this.peek().range
2852
                });
UNCOV
2853
                throw this.lastDiagnosticAsError();
×
2854
            }
2855

2856
            result.colonToken = this.consume(
192✔
2857
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
2858
                TokenKind.Colon
2859
            );
2860
            result.range = util.getRange(result.keyToken, result.colonToken);
191✔
2861
            return result;
191✔
2862
        };
2863

2864
        while (this.match(TokenKind.Newline)) { }
193✔
2865
        let closingBrace: Token;
2866
        if (!this.match(TokenKind.RightCurlyBrace)) {
193✔
2867
            let lastAAMember: AAMemberExpression;
2868
            try {
145✔
2869
                if (this.check(TokenKind.Comment)) {
145✔
2870
                    lastAAMember = null;
7✔
2871
                    members.push(new CommentStatement([this.advance()]));
7✔
2872
                } else {
2873
                    let k = key();
138✔
2874
                    let expr = this.expression();
138✔
2875
                    lastAAMember = new AAMemberExpression(
137✔
2876
                        k.keyToken,
2877
                        k.colonToken,
2878
                        expr
2879
                    );
2880
                    members.push(lastAAMember);
137✔
2881
                }
2882

2883
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
144✔
2884
                    // collect comma at end of expression
2885
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
184✔
2886
                        lastAAMember.commaToken = this.previous();
32✔
2887
                    }
2888

2889
                    //check for comment at the end of the current line
2890
                    if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
184✔
2891
                        let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
14✔
2892
                        members.push(new CommentStatement([token]));
14✔
2893
                    } else {
2894
                        this.consumeStatementSeparators(true);
170✔
2895

2896
                        //check for a comment on its own line
2897
                        if (this.check(TokenKind.Comment) || this.checkPrevious(TokenKind.Comment)) {
170✔
2898
                            let token = this.checkPrevious(TokenKind.Comment) ? this.previous() : this.advance();
1!
2899
                            lastAAMember = null;
1✔
2900
                            members.push(new CommentStatement([token]));
1✔
2901
                            continue;
1✔
2902
                        }
2903

2904
                        if (this.check(TokenKind.RightCurlyBrace)) {
169✔
2905
                            break;
115✔
2906
                        }
2907
                        let k = key();
54✔
2908
                        let expr = this.expression();
53✔
2909
                        lastAAMember = new AAMemberExpression(
53✔
2910
                            k.keyToken,
2911
                            k.colonToken,
2912
                            expr
2913
                        );
2914
                        members.push(lastAAMember);
53✔
2915
                    }
2916
                }
2917
            } catch (error: any) {
2918
                this.rethrowNonDiagnosticError(error);
2✔
2919
            }
2920

2921
            closingBrace = this.tryConsume(
145✔
2922
                DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral(),
2923
                TokenKind.RightCurlyBrace
2924
            );
2925
        } else {
2926
            closingBrace = this.previous();
48✔
2927
        }
2928

2929
        const aaExpr = new AALiteralExpression(members, openingBrace, closingBrace);
193✔
2930
        this.addPropertyHints(aaExpr);
193✔
2931
        return aaExpr;
193✔
2932
    }
2933

2934
    /**
2935
     * Pop token if we encounter specified token
2936
     */
2937
    private match(tokenKind: TokenKind) {
2938
        if (this.check(tokenKind)) {
19,786✔
2939
            this.current++; //advance
1,510✔
2940
            return true;
1,510✔
2941
        }
2942
        return false;
18,276✔
2943
    }
2944

2945
    /**
2946
     * Pop token if we encounter a token in the specified list
2947
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
2948
     */
2949
    private matchAny(...tokenKinds: TokenKind[]) {
2950
        for (let tokenKind of tokenKinds) {
68,844✔
2951
            if (this.check(tokenKind)) {
201,320✔
2952
                this.current++; //advance
17,382✔
2953
                return true;
17,382✔
2954
            }
2955
        }
2956
        return false;
51,462✔
2957
    }
2958

2959
    /**
2960
     * If the next series of tokens matches the given set of tokens, pop them all
2961
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
2962
     */
2963
    private matchSequence(...tokenKinds: TokenKind[]) {
2964
        const endIndex = this.current + tokenKinds.length;
5,958✔
2965
        for (let i = 0; i < tokenKinds.length; i++) {
5,958✔
2966
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
5,982!
2967
                return false;
5,955✔
2968
            }
2969
        }
2970
        this.current = endIndex;
3✔
2971
        return true;
3✔
2972
    }
2973

2974
    /**
2975
     * Get next token matching a specified list, or fail with an error
2976
     */
2977
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
2978
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
6,704✔
2979
        if (token) {
6,704✔
2980
            return token;
6,687✔
2981
        } else {
2982
            let error = new Error(diagnosticInfo.message);
17✔
2983
            (error as any).isDiagnostic = true;
17✔
2984
            throw error;
17✔
2985
        }
2986
    }
2987

2988
    /**
2989
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
2990
     */
2991
    private consumeTokenIf(tokenKind: TokenKind) {
2992
        if (this.match(tokenKind)) {
257✔
2993
            return this.previous();
11✔
2994
        }
2995
    }
2996

2997
    private consumeToken(tokenKind: TokenKind) {
2998
        return this.consume(
293✔
2999
            DiagnosticMessages.expectedToken(tokenKind),
3000
            tokenKind
3001
        );
3002
    }
3003

3004
    /**
3005
     * Consume, or add a message if not found. But then continue and return undefined
3006
     */
3007
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3008
        const nextKind = this.peek().kind;
9,922✔
3009
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
31,407✔
3010

3011
        if (foundTokenKind) {
9,922✔
3012
            return this.advance();
9,835✔
3013
        }
3014
        this.diagnostics.push({
87✔
3015
            ...diagnostic,
3016
            range: this.peek().range
3017
        });
3018
    }
3019

3020
    private tryConsumeToken(tokenKind: TokenKind) {
3021
        return this.tryConsume(
91✔
3022
            DiagnosticMessages.expectedToken(tokenKind),
3023
            tokenKind
3024
        );
3025
    }
3026

3027
    private consumeStatementSeparators(optional = false) {
3,842✔
3028
        //a comment or EOF mark the end of the statement
3029
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
13,056✔
3030
            return true;
575✔
3031
        }
3032
        let consumed = false;
12,481✔
3033
        //consume any newlines and colons
3034
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
12,481✔
3035
            consumed = true;
10,495✔
3036
        }
3037
        if (!optional && !consumed) {
12,481✔
3038
            this.diagnostics.push({
37✔
3039
                ...DiagnosticMessages.expectedNewlineOrColon(),
3040
                range: this.peek().range
3041
            });
3042
        }
3043
        return consumed;
12,481✔
3044
    }
3045

3046
    private advance(): Token {
3047
        if (!this.isAtEnd()) {
22,541✔
3048
            this.current++;
22,523✔
3049
        }
3050
        return this.previous();
22,541✔
3051
    }
3052

3053
    private checkEndOfStatement(): boolean {
3054
        const nextKind = this.peek().kind;
1,573✔
3055
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
1,573✔
3056
    }
3057

3058
    private checkPrevious(tokenKind: TokenKind): boolean {
3059
        return this.previous()?.kind === tokenKind;
671!
3060
    }
3061

3062
    /**
3063
     * Check that the next token kind is the expected kind
3064
     * @param tokenKind the expected next kind
3065
     * @returns true if the next tokenKind is the expected value
3066
     */
3067
    private check(tokenKind: TokenKind): boolean {
3068
        const nextKind = this.peek().kind;
335,711✔
3069
        if (nextKind === TokenKind.Eof) {
335,711✔
3070
            return false;
7,472✔
3071
        }
3072
        return nextKind === tokenKind;
328,239✔
3073
    }
3074

3075
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3076
        const nextKind = this.peek().kind;
41,570✔
3077
        if (nextKind === TokenKind.Eof) {
41,570✔
3078
            return false;
215✔
3079
        }
3080
        return tokenKinds.includes(nextKind);
41,355✔
3081
    }
3082

3083
    private checkNext(tokenKind: TokenKind): boolean {
3084
        if (this.isAtEnd()) {
4,712!
UNCOV
3085
            return false;
×
3086
        }
3087
        return this.peekNext().kind === tokenKind;
4,712✔
3088
    }
3089

3090
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3091
        if (this.isAtEnd()) {
2,499!
UNCOV
3092
            return false;
×
3093
        }
3094
        const nextKind = this.peekNext().kind;
2,499✔
3095
        return tokenKinds.includes(nextKind);
2,499✔
3096
    }
3097

3098
    private isAtEnd(): boolean {
3099
        return this.peek().kind === TokenKind.Eof;
62,392✔
3100
    }
3101

3102
    private peekNext(): Token {
3103
        if (this.isAtEnd()) {
7,221!
UNCOV
3104
            return this.peek();
×
3105
        }
3106
        return this.tokens[this.current + 1];
7,221✔
3107
    }
3108

3109
    private peek(): Token {
3110
        return this.tokens[this.current];
461,059✔
3111
    }
3112

3113
    private previous(): Token {
3114
        return this.tokens[this.current - 1];
32,082✔
3115
    }
3116

3117
    /**
3118
     * Sometimes we catch an error that is a diagnostic.
3119
     * If that's the case, we want to continue parsing.
3120
     * Otherwise, re-throw the error
3121
     *
3122
     * @param error error caught in a try/catch
3123
     */
3124
    private rethrowNonDiagnosticError(error) {
3125
        if (!error.isDiagnostic) {
9!
UNCOV
3126
            throw error;
×
3127
        }
3128
    }
3129

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

3146
    private synchronize() {
3147
        this.advance(); // skip the erroneous token
128✔
3148

3149
        while (!this.isAtEnd()) {
128✔
3150
            if (this.ensureNewLineOrColon(true)) {
207✔
3151
                // end of statement reached
3152
                return;
92✔
3153
            }
3154

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

3171
            this.advance();
114✔
3172
        }
3173
    }
3174

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

3184
        const visitCallExpression = (e: CallExpression | CallfuncExpression) => {
7✔
3185
            for (const p of e.args) {
14✔
3186
                this._references.expressions.add(p);
7✔
3187
            }
3188
            //add calls that were not excluded (from loop below)
3189
            if (!excludedExpressions.has(e)) {
14✔
3190
                this._references.expressions.add(e);
12✔
3191
            }
3192

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

3204
                        //when we hit a variable expression, we're definitely at the leftmost expression so stop
3205
                    } else if (isVariableExpression(node)) {
20✔
3206
                        break;
12✔
3207
                        //if
3208

3209
                    } else if (isDottedGetExpression(node) || isIndexedGetExpression(node)) {
8!
3210
                        node = node.obj;
8✔
3211
                    } else {
3212
                        //some expression we don't understand. log it and quit the loop
UNCOV
3213
                        this.logger.info('Encountered unknown expression while calculating function expression chain', node);
×
UNCOV
3214
                        break;
×
3215
                    }
3216
                }
3217
            }
3218
        };
3219

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

3317
    public dispose() {
3318
    }
3319
}
3320

3321
export enum ParseMode {
1✔
3322
    BrightScript = 'BrightScript',
1✔
3323
    BrighterScript = 'BrighterScript'
1✔
3324
}
3325

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

3342
export class References {
1✔
3343
    private cache = new Cache();
2,108✔
3344
    public assignmentStatements = [] as AssignmentStatement[];
2,108✔
3345
    public classStatements = [] as ClassStatement[];
2,108✔
3346

3347
    public get classStatementLookup() {
3348
        if (!this._classStatementLookup) {
17✔
3349
            this._classStatementLookup = new Map();
15✔
3350
            for (const stmt of this.classStatements) {
15✔
3351
                this._classStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
2✔
3352
            }
3353
        }
3354
        return this._classStatementLookup;
17✔
3355
    }
3356
    private _classStatementLookup: Map<string, ClassStatement>;
3357

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

3374
    public interfaceStatements = [] as InterfaceStatement[];
2,108✔
3375

3376
    public get interfaceStatementLookup() {
UNCOV
3377
        if (!this._interfaceStatementLookup) {
×
UNCOV
3378
            this._interfaceStatementLookup = new Map();
×
UNCOV
3379
            for (const stmt of this.interfaceStatements) {
×
UNCOV
3380
                this._interfaceStatementLookup.set(stmt.fullName.toLowerCase(), stmt);
×
3381
            }
3382
        }
UNCOV
3383
        return this._interfaceStatementLookup;
×
3384
    }
3385
    private _interfaceStatementLookup: Map<string, InterfaceStatement>;
3386

3387
    public enumStatements = [] as EnumStatement[];
2,108✔
3388

3389
    public get enumStatementLookup() {
3390
        return this.cache.getOrAdd('enums', () => {
18✔
3391
            const result = new Map<string, EnumStatement>();
16✔
3392
            for (const stmt of this.enumStatements) {
16✔
3393
                result.set(stmt.fullName.toLowerCase(), stmt);
1✔
3394
            }
3395
            return result;
16✔
3396
        });
3397
    }
3398

3399
    public constStatements = [] as ConstStatement[];
2,108✔
3400

3401
    public get constStatementLookup() {
UNCOV
3402
        return this.cache.getOrAdd('consts', () => {
×
UNCOV
3403
            const result = new Map<string, ConstStatement>();
×
UNCOV
3404
            for (const stmt of this.constStatements) {
×
UNCOV
3405
                result.set(stmt.fullName.toLowerCase(), stmt);
×
3406
            }
UNCOV
3407
            return result;
×
3408
        });
3409
    }
3410

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

3426
    public importStatements = [] as ImportStatement[];
2,108✔
3427
    public libraryStatements = [] as LibraryStatement[];
2,108✔
3428
    public namespaceStatements = [] as NamespaceStatement[];
2,108✔
3429
    public newExpressions = [] as NewExpression[];
2,108✔
3430
    public propertyHints = {} as Record<string, string>;
2,108✔
3431
}
3432

3433
class CancelStatementError extends Error {
3434
    constructor() {
3435
        super('CancelStatement');
2✔
3436
    }
3437
}
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