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

rokucommunity / brighterscript / #13117

01 Oct 2024 08:24AM UTC coverage: 86.842% (-1.4%) from 88.193%
#13117

push

web-flow
Merge abd960cd5 into 3a2dc7282

11537 of 14048 branches covered (82.13%)

Branch coverage included in aggregate %.

6991 of 7582 new or added lines in 100 files covered. (92.21%)

83 existing lines in 18 files now uncovered.

12692 of 13852 relevant lines covered (91.63%)

29478.96 hits per line

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

91.03
/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
    AllowedTypeIdentifiers,
8
    DeclarableTypes,
9
    AllowedProperties,
10
    AssignmentOperators,
11
    BrighterScriptSourceLiterals,
12
    DisallowedFunctionIdentifiersText,
13
    DisallowedLocalIdentifiersText,
14
    TokenKind,
15
    BlockTerminators,
16
    ReservedWords,
17
    CompoundAssignmentOperators,
18
    BinaryExpressionOperatorTokens
19
} from '../lexer/TokenKind';
20
import type {
21
    PrintSeparatorSpace,
22
    PrintSeparatorTab
23
} from './Statement';
24
import {
1✔
25
    AliasStatement,
26
    AssignmentStatement,
27
    Block,
28
    Body,
29
    CatchStatement,
30
    ContinueStatement,
31
    ClassStatement,
32
    ConstStatement,
33
    ConditionalCompileStatement,
34
    DimStatement,
35
    DottedSetStatement,
36
    EndStatement,
37
    EnumMemberStatement,
38
    EnumStatement,
39
    ExitStatement,
40
    ExpressionStatement,
41
    ForEachStatement,
42
    FieldStatement,
43
    ForStatement,
44
    FunctionStatement,
45
    GotoStatement,
46
    IfStatement,
47
    ImportStatement,
48
    IncrementStatement,
49
    IndexedSetStatement,
50
    InterfaceFieldStatement,
51
    InterfaceMethodStatement,
52
    InterfaceStatement,
53
    LabelStatement,
54
    LibraryStatement,
55
    MethodStatement,
56
    NamespaceStatement,
57
    PrintStatement,
58
    ReturnStatement,
59
    StopStatement,
60
    ThrowStatement,
61
    TryCatchStatement,
62
    WhileStatement,
63
    TypecastStatement,
64
    ConditionalCompileConstStatement,
65
    ConditionalCompileErrorStatement,
66
    AugmentedAssignmentStatement
67
} from './Statement';
68
import type { DiagnosticInfo } from '../DiagnosticMessages';
69
import { DiagnosticMessages } from '../DiagnosticMessages';
1✔
70
import { util } from '../util';
1✔
71
import {
1✔
72
    AALiteralExpression,
73
    AAMemberExpression,
74
    AnnotationExpression,
75
    ArrayLiteralExpression,
76
    BinaryExpression,
77
    CallExpression,
78
    CallfuncExpression,
79
    DottedGetExpression,
80
    EscapedCharCodeLiteralExpression,
81
    FunctionExpression,
82
    FunctionParameterExpression,
83
    GroupingExpression,
84
    IndexedGetExpression,
85
    LiteralExpression,
86
    NewExpression,
87
    NullCoalescingExpression,
88
    RegexLiteralExpression,
89
    SourceLiteralExpression,
90
    TaggedTemplateStringExpression,
91
    TemplateStringExpression,
92
    TemplateStringQuasiExpression,
93
    TernaryExpression,
94
    TypecastExpression,
95
    TypeExpression,
96
    TypedArrayExpression,
97
    UnaryExpression,
98
    VariableExpression,
99
    XmlAttributeGetExpression
100
} from './Expression';
101
import type { Range } from 'vscode-languageserver';
102
import type { Logger } from '../logging';
103
import { createLogger } from '../logging';
1✔
104
import { isAnnotationExpression, isCallExpression, isCallfuncExpression, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression, isConditionalCompileStatement, isLiteralBoolean, isTypecastExpression } from '../astUtils/reflection';
1✔
105
import { createStringLiteral, createToken } from '../astUtils/creators';
1✔
106
import type { Expression, Statement } from './AstNode';
107
import type { BsDiagnostic, DeepWriteable } from '../interfaces';
108

109
export class Parser {
1✔
110
    /**
111
     * The array of tokens passed to `parse()`
112
     */
113
    public tokens = [] as Token[];
3,440✔
114

115
    /**
116
     * The current token index
117
     */
118
    public current: number;
119

120
    /**
121
     * The list of statements for the parsed file
122
     */
123
    public ast = new Body({ statements: [] });
3,440✔
124

125
    public get eofToken(): Token {
126
        const lastToken = this.tokens?.[this.tokens.length - 1];
644!
127
        if (lastToken?.kind === TokenKind.Eof) {
644!
128
            return lastToken;
644✔
129
        }
130
    }
131

132
    /**
133
     * The top-level symbol table for the body of this file.
134
     */
135
    public get symbolTable() {
136
        return this.ast.symbolTable;
11,527✔
137
    }
138

139
    /**
140
     * The list of diagnostics found during the parse process
141
     */
142
    public diagnostics: BsDiagnostic[];
143

144
    /**
145
     * The depth of the calls to function declarations. Helps some checks know if they are at the root or not.
146
     */
147
    private namespaceAndFunctionDepth: number;
148

149
    /**
150
     * The options used to parse the file
151
     */
152
    public options: ParseOptions;
153

154
    private globalTerminators = [] as TokenKind[][];
3,440✔
155

156
    /**
157
     * 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
158
     * based on the parse mode
159
     */
160
    private allowedLocalIdentifiers: TokenKind[];
161

162
    /**
163
     * Annotations collected which should be attached to the next statement
164
     */
165
    private pendingAnnotations: AnnotationExpression[];
166

167
    /**
168
     * Get the currently active global terminators
169
     */
170
    private peekGlobalTerminators() {
171
        return this.globalTerminators[this.globalTerminators.length - 1] ?? [];
16,418✔
172
    }
173

174
    /**
175
     * Static wrapper around creating a new parser and parsing a list of tokens
176
     */
177
    public static parse(toParse: Token[] | string, options?: ParseOptions): Parser {
178
        return new Parser().parse(toParse, options);
3,421✔
179
    }
180

181
    /**
182
     * Parses an array of `Token`s into an abstract syntax tree
183
     * @param toParse the array of tokens to parse. May not contain any whitespace tokens
184
     * @returns the same instance of the parser which contains the diagnostics and statements
185
     */
186
    public parse(toParse: Token[] | string, options?: ParseOptions) {
187
        this.logger = options?.logger ?? createLogger();
3,428✔
188
        options = this.sanitizeParseOptions(options);
3,428✔
189
        this.options = options;
3,428✔
190

191
        let tokens: Token[];
192
        if (typeof toParse === 'string') {
3,428✔
193
            tokens = Lexer.scan(toParse, {
521✔
194
                trackLocations: options.trackLocations,
195
                srcPath: options?.srcPath
1,563!
196
            }).tokens;
197
        } else {
198
            tokens = toParse;
2,907✔
199
        }
200
        this.tokens = tokens;
3,428✔
201
        this.allowedLocalIdentifiers = [
3,428✔
202
            ...AllowedLocalIdentifiers,
203
            //when in plain brightscript mode, the BrighterScript source literals can be used as regular variables
204
            ...(this.options.mode === ParseMode.BrightScript ? BrighterScriptSourceLiterals : [])
3,428✔
205
        ];
206
        this.current = 0;
3,428✔
207
        this.diagnostics = [];
3,428✔
208
        this.namespaceAndFunctionDepth = 0;
3,428✔
209
        this.pendingAnnotations = [];
3,428✔
210

211
        this.ast = this.body();
3,428✔
212
        this.ast.bsConsts = options.bsConsts;
3,428✔
213
        //now that we've built the AST, link every node to its parent
214
        this.ast.link();
3,428✔
215
        return this;
3,428✔
216
    }
217

218
    private logger: Logger;
219

220
    private body() {
221
        const parentAnnotations = this.enterAnnotationBlock();
4,036✔
222

223
        let body = new Body({ statements: [] });
4,036✔
224
        if (this.tokens.length > 0) {
4,036✔
225
            this.consumeStatementSeparators(true);
4,035✔
226

227
            try {
4,035✔
228
                while (
4,035✔
229
                    //not at end of tokens
230
                    !this.isAtEnd() &&
16,373✔
231
                    //the next token is not one of the end terminators
232
                    !this.checkAny(...this.peekGlobalTerminators())
233
                ) {
234
                    let dec = this.declaration();
5,866✔
235
                    if (dec) {
5,866✔
236
                        if (!isAnnotationExpression(dec)) {
5,833✔
237
                            this.consumePendingAnnotations(dec);
5,782✔
238
                            body.statements.push(dec);
5,782✔
239
                            //ensure statement separator
240
                            this.consumeStatementSeparators(false);
5,782✔
241
                        } else {
242
                            this.consumeStatementSeparators(true);
51✔
243
                        }
244
                    }
245
                }
246
            } catch (parseError) {
247
                //do nothing with the parse error for now. perhaps we can remove this?
248
                console.error(parseError);
×
249
            }
250
        }
251

252
        this.exitAnnotationBlock(parentAnnotations);
4,036✔
253
        return body;
4,036✔
254
    }
255

256
    private sanitizeParseOptions(options: ParseOptions) {
257
        options ??= {
3,428✔
258
            srcPath: undefined
259
        };
260
        options.mode ??= ParseMode.BrightScript;
3,428✔
261
        options.trackLocations ??= true;
3,428✔
262
        return options;
3,428✔
263
    }
264

265
    /**
266
     * Determine if the parser is currently parsing tokens at the root level.
267
     */
268
    private isAtRootLevel() {
269
        return this.namespaceAndFunctionDepth === 0;
39,914✔
270
    }
271

272
    /**
273
     * Throws an error if the input file type is not BrighterScript
274
     */
275
    private warnIfNotBrighterScriptMode(featureName: string) {
276
        if (this.options.mode !== ParseMode.BrighterScript) {
2,468✔
277
            let diagnostic = {
169✔
278
                ...DiagnosticMessages.bsFeatureNotSupportedInBrsFiles(featureName),
279
                location: this.peek().location
280
            };
281
            this.diagnostics.push(diagnostic);
169✔
282
        }
283
    }
284

285
    /**
286
     * Throws an exception using the last diagnostic message
287
     */
288
    private lastDiagnosticAsError() {
289
        let error = new Error(this.diagnostics[this.diagnostics.length - 1]?.message ?? 'Unknown error');
94!
290
        (error as any).isDiagnostic = true;
94✔
291
        return error;
94✔
292
    }
293

294
    private declaration(): Statement | AnnotationExpression | undefined {
295
        try {
13,270✔
296
            if (this.checkAny(TokenKind.HashConst)) {
13,270✔
297
                return this.conditionalCompileConstStatement();
21✔
298
            }
299
            if (this.checkAny(TokenKind.HashIf)) {
13,249✔
300
                return this.conditionalCompileStatement();
41✔
301
            }
302
            if (this.checkAny(TokenKind.HashError)) {
13,208✔
303
                return this.conditionalCompileErrorStatement();
10✔
304
            }
305

306
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
13,198✔
307
                return this.functionDeclaration(false);
3,073✔
308
            }
309

310
            if (this.checkLibrary()) {
10,125✔
311
                return this.libraryStatement();
13✔
312
            }
313

314
            if (this.checkAlias()) {
10,112✔
315
                return this.aliasStatement();
33✔
316
            }
317

318
            if (this.check(TokenKind.Const) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
10,079✔
319
                return this.constDeclaration();
155✔
320
            }
321

322
            if (this.check(TokenKind.At) && this.checkNext(TokenKind.Identifier)) {
9,924✔
323
                return this.annotationExpression();
58✔
324
            }
325

326
            //catch certain global terminators to prevent unnecessary lookahead (i.e. like `end namespace`, no need to continue)
327
            if (this.checkAny(...this.peekGlobalTerminators())) {
9,866!
328
                return;
×
329
            }
330

331
            return this.statement();
9,866✔
332
        } catch (error: any) {
333
            //if the error is not a diagnostic, then log the error for debugging purposes
334
            if (!error.isDiagnostic) {
83✔
335
                this.logger.error(error);
1✔
336
            }
337
            this.synchronize();
83✔
338
        }
339
    }
340

341
    /**
342
     * Try to get an identifier. If not found, add diagnostic and return undefined
343
     */
344
    private tryIdentifier(...additionalTokenKinds: TokenKind[]): Identifier | undefined {
345
        const identifier = this.tryConsume(
165✔
346
            DiagnosticMessages.expectedIdentifier(),
347
            TokenKind.Identifier,
348
            ...additionalTokenKinds
349
        ) as Identifier;
350
        if (identifier) {
165✔
351
            // force the name into an identifier so the AST makes some sense
352
            identifier.kind = TokenKind.Identifier;
164✔
353
            return identifier;
164✔
354
        }
355
    }
356

357
    private identifier(...additionalTokenKinds: TokenKind[]) {
358
        const identifier = this.consume(
779✔
359
            DiagnosticMessages.expectedIdentifier(),
360
            TokenKind.Identifier,
361
            ...additionalTokenKinds
362
        ) as Identifier;
363
        // force the name into an identifier so the AST makes some sense
364
        identifier.kind = TokenKind.Identifier;
779✔
365
        return identifier;
779✔
366
    }
367

368
    private enumMemberStatement() {
369
        const name = this.consume(
318✔
370
            DiagnosticMessages.expectedClassFieldIdentifier(),
371
            TokenKind.Identifier,
372
            ...AllowedProperties
373
        ) as Identifier;
374
        let equalsToken: Token;
375
        let value: Expression;
376
        //look for `= SOME_EXPRESSION`
377
        if (this.check(TokenKind.Equal)) {
318✔
378
            equalsToken = this.advance();
176✔
379
            value = this.expression();
176✔
380
        }
381
        const statement = new EnumMemberStatement({ name: name, equals: equalsToken, value: value });
318✔
382
        return statement;
318✔
383
    }
384

385
    /**
386
     * Create a new InterfaceMethodStatement. This should only be called from within `interfaceDeclaration`
387
     */
388
    private interfaceFieldStatement(optionalKeyword?: Token) {
389
        const name = this.identifier(...AllowedProperties);
185✔
390
        let asToken;
391
        let typeExpression;
392
        if (this.check(TokenKind.As)) {
185✔
393
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
167✔
394
        }
395
        return new InterfaceFieldStatement({ name: name, as: asToken, typeExpression: typeExpression, optional: optionalKeyword });
185✔
396
    }
397

398
    private consumeAsTokenAndTypeExpression(ignoreDiagnostics = false): [Token, TypeExpression] {
1,333✔
399
        let asToken = this.consumeToken(TokenKind.As);
1,343✔
400
        let typeExpression: TypeExpression;
401
        if (asToken) {
1,343!
402
            //if there's nothing after the `as`, add a diagnostic and continue
403
            if (this.checkEndOfStatement()) {
1,343✔
404
                if (!ignoreDiagnostics) {
2✔
405
                    this.diagnostics.push({
1✔
406
                        ...DiagnosticMessages.expectedIdentifierAfterKeyword(asToken.text),
407
                        location: asToken.location
408
                    });
409
                }
410
                //consume the statement separator
411
                this.consumeStatementSeparators();
2✔
412
            } else if (this.peek().kind !== TokenKind.Identifier && !this.checkAny(...DeclarableTypes, ...AllowedTypeIdentifiers)) {
1,341✔
413
                if (!ignoreDiagnostics) {
6!
414
                    this.diagnostics.push({
6✔
415
                        ...DiagnosticMessages.expectedIdentifierAfterKeyword(asToken.text),
416
                        location: asToken.location
417
                    });
418
                }
419
            } else {
420
                typeExpression = this.typeExpression();
1,335✔
421
            }
422
        }
423
        return [asToken, typeExpression];
1,343✔
424
    }
425

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

434
        let params = [] as FunctionParameterExpression[];
42✔
435
        if (!this.check(TokenKind.RightParen)) {
42✔
436
            do {
9✔
437
                if (params.length >= CallExpression.MaximumArguments) {
11!
438
                    this.diagnostics.push({
×
439
                        ...DiagnosticMessages.tooManyCallableParameters(params.length, CallExpression.MaximumArguments),
440
                        location: this.peek().location
441
                    });
442
                }
443

444
                params.push(this.functionParameter());
11✔
445
            } while (this.match(TokenKind.Comma));
446
        }
447
        const rightParen = this.consumeToken(TokenKind.RightParen);
42✔
448
        // let asToken = null as Token;
449
        // let returnTypeExpression: TypeExpression;
450
        let asToken: Token;
451
        let returnTypeExpression: TypeExpression;
452
        if (this.check(TokenKind.As)) {
42✔
453
            [asToken, returnTypeExpression] = this.consumeAsTokenAndTypeExpression();
30✔
454
        }
455

456
        return new InterfaceMethodStatement({
42✔
457
            functionType: functionType,
458
            name: name,
459
            leftParen: leftParen,
460
            params: params,
461
            rightParen: rightParen,
462
            as: asToken,
463
            returnTypeExpression: returnTypeExpression,
464
            optional: optionalKeyword
465
        });
466
    }
467

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

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

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

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

482
        if (this.peek().text.toLowerCase() === 'extends') {
155✔
483
            extendsToken = this.advance();
8✔
484
            if (this.checkEndOfStatement()) {
8!
NEW
485
                this.diagnostics.push({
×
486
                    ...DiagnosticMessages.expectedIdentifierAfterKeyword(extendsToken.text),
487
                    location: extendsToken.location
488
                });
489
            } else {
490
                parentInterfaceName = this.typeExpression();
8✔
491
            }
492
        }
493
        this.consumeStatementSeparators();
155✔
494
        //gather up all interface members (Fields, Methods)
495
        let body = [] as Statement[];
155✔
496
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
155✔
497
            try {
384✔
498
                //break out of this loop if we encountered the `EndInterface` token not followed by `as`
499
                if (this.check(TokenKind.EndInterface) && !this.checkNext(TokenKind.As)) {
384✔
500
                    break;
155✔
501
                }
502

503
                let decl: Statement;
504

505
                //collect leading annotations
506
                if (this.check(TokenKind.At)) {
229✔
507
                    this.annotationExpression();
2✔
508
                }
509
                const optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
229✔
510
                //fields
511
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkAnyNext(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
229✔
512
                    decl = this.interfaceFieldStatement(optionalKeyword);
183✔
513
                    //field with name = 'optional'
514
                } else if (optionalKeyword && this.checkAny(TokenKind.As, TokenKind.Newline, TokenKind.Comment)) {
46✔
515
                    //rewind one place, so that 'optional' is the field name
516
                    this.current--;
2✔
517
                    decl = this.interfaceFieldStatement();
2✔
518

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

523
                }
524
                if (decl) {
229✔
525
                    this.consumePendingAnnotations(decl);
227✔
526
                    body.push(decl);
227✔
527
                } else {
528
                    //we didn't find a declaration...flag tokens until next line
529
                    this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
2✔
530
                }
531
            } catch (e) {
532
                //throw out any failed members and move on to the next line
UNCOV
533
                this.flagUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
×
534
            }
535

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

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

543
        const statement = new InterfaceStatement({
155✔
544
            interface: interfaceToken,
545
            name: nameToken,
546
            extends: extendsToken,
547
            parentInterfaceName: parentInterfaceName,
548
            body: body,
549
            endInterface: endInterfaceToken
550
        });
551
        this.exitAnnotationBlock(parentAnnotations);
155✔
552
        return statement;
155✔
553
    }
554

555
    private enumDeclaration(): EnumStatement {
556
        const enumToken = this.consume(
165✔
557
            DiagnosticMessages.expectedKeyword(TokenKind.Enum),
558
            TokenKind.Enum
559
        );
560
        const nameToken = this.tryIdentifier(...this.allowedLocalIdentifiers);
165✔
561

562
        this.warnIfNotBrighterScriptMode('enum declarations');
165✔
563

564
        const parentAnnotations = this.enterAnnotationBlock();
165✔
565

566
        this.consumeStatementSeparators();
165✔
567

568
        const body: Array<EnumMemberStatement> = [];
165✔
569
        //gather up all members
570
        while (this.checkAny(TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
165✔
571
            try {
318✔
572
                let decl: EnumMemberStatement;
573

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

579
                //members
580
                if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
318!
581
                    decl = this.enumMemberStatement();
318✔
582
                }
583

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

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

604
        //consume the final `end interface` token
605
        const endEnumToken = this.consumeToken(TokenKind.EndEnum);
165✔
606

607
        const result = new EnumStatement({
164✔
608
            enum: enumToken,
609
            name: nameToken,
610
            body: body,
611
            endEnum: endEnumToken
612
        });
613

614
        this.exitAnnotationBlock(parentAnnotations);
164✔
615
        return result;
164✔
616
    }
617

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

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

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

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

636
        //see if the class inherits from parent
637
        if (this.peek().text.toLowerCase() === 'extends') {
675✔
638
            extendsKeyword = this.advance();
101✔
639
            if (this.checkEndOfStatement()) {
101✔
640
                this.diagnostics.push({
1✔
641
                    ...DiagnosticMessages.expectedIdentifierAfterKeyword(extendsKeyword.text),
642
                    location: extendsKeyword.location
643
                });
644
            } else {
645
                parentClassName = this.typeExpression();
100✔
646
            }
647
        }
648

649
        //ensure statement separator
650
        this.consumeStatementSeparators();
675✔
651

652
        //gather up all class members (Fields, Methods)
653
        let body = [] as Statement[];
675✔
654
        while (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private, TokenKind.Function, TokenKind.Sub, TokenKind.Comment, TokenKind.Identifier, TokenKind.At, ...AllowedProperties)) {
675✔
655
            try {
695✔
656
                let decl: Statement;
657
                let accessModifier: Token;
658

659
                if (this.check(TokenKind.At)) {
695✔
660
                    this.annotationExpression();
15✔
661
                }
662

663
                if (this.checkAny(TokenKind.Public, TokenKind.Protected, TokenKind.Private)) {
694✔
664
                    //use actual access modifier
665
                    accessModifier = this.advance();
96✔
666
                }
667

668
                let overrideKeyword: Token;
669
                if (this.peek().text.toLowerCase() === 'override') {
694✔
670
                    overrideKeyword = this.advance();
17✔
671
                }
672
                //methods (function/sub keyword OR identifier followed by opening paren)
673
                if (this.checkAny(TokenKind.Function, TokenKind.Sub) || (this.checkAny(TokenKind.Identifier, ...AllowedProperties) && this.checkNext(TokenKind.LeftParen))) {
694✔
674
                    const funcDeclaration = this.functionDeclaration(false, false);
356✔
675

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

684
                    decl = new MethodStatement({
356✔
685
                        modifiers: accessModifier,
686
                        name: funcDeclaration.tokens.name,
687
                        func: funcDeclaration.func,
688
                        override: overrideKeyword
689
                    });
690

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

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

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

704
                }
705

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

715
            //ensure statement separator
716
            this.consumeStatementSeparators();
695✔
717
        }
718

719
        let endingKeyword = this.advance();
675✔
720
        if (endingKeyword.kind !== TokenKind.EndClass) {
675✔
721
            this.diagnostics.push({
4✔
722
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('class'),
723
                location: endingKeyword.location
724
            });
725
        }
726

727
        const result = new ClassStatement({
675✔
728
            class: classKeyword,
729
            name: className,
730
            body: body,
731
            endClass: endingKeyword,
732
            extends: extendsKeyword,
733
            parentClassName: parentClassName
734
        });
735

736
        this.exitAnnotationBlock(parentAnnotations);
675✔
737
        return result;
675✔
738
    }
739

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

742
        let optionalKeyword = this.consumeTokenIf(TokenKind.Optional);
324✔
743

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

767
        let name = this.consume(
324✔
768
            DiagnosticMessages.expectedClassFieldIdentifier(),
769
            TokenKind.Identifier,
770
            ...AllowedProperties
771
        ) as Identifier;
772

773
        let asToken: Token;
774
        let fieldTypeExpression: TypeExpression;
775
        //look for `as SOME_TYPE`
776
        if (this.check(TokenKind.As)) {
324✔
777
            [asToken, fieldTypeExpression] = this.consumeAsTokenAndTypeExpression();
217✔
778
        }
779

780
        let initialValue: Expression;
781
        let equal: Token;
782
        //if there is a field initializer
783
        if (this.check(TokenKind.Equal)) {
324✔
784
            equal = this.advance();
79✔
785
            initialValue = this.expression();
79✔
786
        }
787

788
        return new FieldStatement({
323✔
789
            accessModifier: accessModifier,
790
            name: name,
791
            as: asToken,
792
            typeExpression: fieldTypeExpression,
793
            equals: equal,
794
            initialValue: initialValue,
795
            optional: optionalKeyword
796
        });
797
    }
798

799
    /**
800
     * An array of CallExpression for the current function body
801
     */
802
    private callExpressions = [];
3,440✔
803

804
    private functionDeclaration(isAnonymous: true, checkIdentifier?: boolean, onlyCallableAsMember?: boolean): FunctionExpression;
805
    private functionDeclaration(isAnonymous: false, checkIdentifier?: boolean, onlyCallableAsMember?: boolean): FunctionStatement;
806
    private functionDeclaration(isAnonymous: boolean, checkIdentifier = true, onlyCallableAsMember = false) {
6,668✔
807
        let previousCallExpressions = this.callExpressions;
3,512✔
808
        this.callExpressions = [];
3,512✔
809
        try {
3,512✔
810
            //track depth to help certain statements need to know if they are contained within a function body
811
            this.namespaceAndFunctionDepth++;
3,512✔
812
            let functionType: Token;
813
            if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
3,512✔
814
                functionType = this.advance();
3,510✔
815
            } else {
816
                this.diagnostics.push({
2✔
817
                    ...DiagnosticMessages.missingCallableKeyword(),
818
                    location: this.peek().location
819
                });
820
                //TODO we should probably eliminate this entirely, since it's not present in the source code
821
                functionType = {
2✔
822
                    isReserved: true,
823
                    kind: TokenKind.Function,
824
                    text: 'function',
825
                    //zero-length location means derived
826
                    location: this.peek().location,
827
                    leadingWhitespace: '',
828
                    leadingTrivia: []
829
                };
830
            }
831
            let isSub = functionType?.kind === TokenKind.Sub;
3,512!
832
            let functionTypeText = isSub ? 'sub' : 'function';
3,512✔
833
            let name: Identifier;
834
            let leftParen: Token;
835

836
            if (isAnonymous) {
3,512✔
837
                leftParen = this.consume(
83✔
838
                    DiagnosticMessages.expectedLeftParenAfterCallable(functionTypeText),
839
                    TokenKind.LeftParen
840
                );
841
            } else {
842
                name = this.consume(
3,429✔
843
                    DiagnosticMessages.expectedNameAfterCallableKeyword(functionTypeText),
844
                    TokenKind.Identifier,
845
                    ...AllowedProperties
846
                ) as Identifier;
847
                leftParen = this.consume(
3,427✔
848
                    DiagnosticMessages.expectedLeftParenAfterCallableName(functionTypeText),
849
                    TokenKind.LeftParen
850
                );
851

852
                //prevent functions from ending with type designators
853
                let lastChar = name.text[name.text.length - 1];
3,426✔
854
                if (['$', '%', '!', '#', '&'].includes(lastChar)) {
3,426✔
855
                    //don't throw this error; let the parser continue
856
                    this.diagnostics.push({
8✔
857
                        ...DiagnosticMessages.functionNameCannotEndWithTypeDesignator(functionTypeText, name.text, lastChar),
858
                        location: name.location
859
                    });
860
                }
861

862
                //flag functions with keywords for names (only for standard functions)
863
                if (checkIdentifier && DisallowedFunctionIdentifiersText.has(name.text.toLowerCase())) {
3,426✔
864
                    this.diagnostics.push({
1✔
865
                        ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
866
                        location: name.location
867
                    });
868
                }
869
            }
870

871
            let params = [] as FunctionParameterExpression[];
3,509✔
872
            let asToken: Token;
873
            let typeExpression: TypeExpression;
874
            if (!this.check(TokenKind.RightParen)) {
3,509✔
875
                do {
1,603✔
876
                    params.push(this.functionParameter());
2,848✔
877
                } while (this.match(TokenKind.Comma));
878
            }
879
            let rightParen = this.advance();
3,509✔
880

881
            if (this.check(TokenKind.As)) {
3,509✔
882
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
257✔
883
            }
884

885
            params.reduce((haveFoundOptional: boolean, param: FunctionParameterExpression) => {
3,509✔
886
                if (haveFoundOptional && !param.defaultValue) {
2,848!
887
                    this.diagnostics.push({
×
888
                        ...DiagnosticMessages.requiredParameterMayNotFollowOptionalParameter(param.tokens.name.text),
889
                        location: param.location
890
                    });
891
                }
892

893
                return haveFoundOptional || !!param.defaultValue;
2,848✔
894
            }, false);
895

896
            this.consumeStatementSeparators(true);
3,509✔
897

898

899
            //support ending the function with `end sub` OR `end function`
900
            let body = this.block();
3,509✔
901
            //if the parser was unable to produce a block, make an empty one so the AST makes some sense...
902

903
            // consume 'end sub' or 'end function'
904
            const endFunctionType = this.advance();
3,509✔
905
            let expectedEndKind = isSub ? TokenKind.EndSub : TokenKind.EndFunction;
3,509✔
906

907
            //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
908
            //add an error but don't hard-fail so the AST can continue more gracefully
909
            if (endFunctionType.kind !== expectedEndKind) {
3,509✔
910
                this.diagnostics.push({
9✔
911
                    ...DiagnosticMessages.mismatchedEndCallableKeyword(functionTypeText, endFunctionType.text),
912
                    location: endFunctionType.location
913
                });
914
            }
915

916
            if (!body) {
3,509✔
917
                body = new Block({ statements: [] });
3✔
918
            }
919

920
            let func = new FunctionExpression({
3,509✔
921
                parameters: params,
922
                body: body,
923
                functionType: functionType,
924
                endFunctionType: endFunctionType,
925
                leftParen: leftParen,
926
                rightParen: rightParen,
927
                as: asToken,
928
                returnTypeExpression: typeExpression
929
            });
930

931
            if (isAnonymous) {
3,509✔
932
                return func;
83✔
933
            } else {
934
                let result = new FunctionStatement({ name: name, func: func });
3,426✔
935
                return result;
3,426✔
936
            }
937
        } finally {
938
            this.namespaceAndFunctionDepth--;
3,512✔
939
            //restore the previous CallExpression list
940
            this.callExpressions = previousCallExpressions;
3,512✔
941
        }
942
    }
943

944
    private functionParameter(): FunctionParameterExpression {
945
        if (!this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
2,859!
946
            this.diagnostics.push({
×
947
                ...DiagnosticMessages.expectedParameterNameButFound(this.peek().text),
948
                location: this.peek().location
949
            });
950
            throw this.lastDiagnosticAsError();
×
951
        }
952

953
        let name = this.advance() as Identifier;
2,859✔
954
        // force the name into an identifier so the AST makes some sense
955
        name.kind = TokenKind.Identifier;
2,859✔
956

957
        let typeExpression: TypeExpression;
958
        let defaultValue;
959
        let equalToken: Token;
960
        // parse argument default value
961
        if ((equalToken = this.consumeTokenIf(TokenKind.Equal))) {
2,859✔
962
            // it seems any expression is allowed here -- including ones that operate on other arguments!
963
            defaultValue = this.expression(false);
355✔
964
        }
965

966
        let asToken: Token = null;
2,859✔
967
        if (this.check(TokenKind.As)) {
2,859✔
968
            [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
586✔
969

970
        }
971
        return new FunctionParameterExpression({
2,859✔
972
            name: name,
973
            equals: equalToken,
974
            defaultValue: defaultValue,
975
            as: asToken,
976
            typeExpression: typeExpression
977
        });
978
    }
979

980
    private assignment(allowTypedAssignment = false): AssignmentStatement {
1,431✔
981
        let name = this.advance() as Identifier;
1,440✔
982
        //add diagnostic if name is a reserved word that cannot be used as an identifier
983
        if (DisallowedLocalIdentifiersText.has(name.text.toLowerCase())) {
1,440✔
984
            this.diagnostics.push({
12✔
985
                ...DiagnosticMessages.cannotUseReservedWordAsIdentifier(name.text),
986
                location: name.location
987
            });
988
        }
989
        let asToken: Token;
990
        let typeExpression: TypeExpression;
991

992
        if (allowTypedAssignment) {
1,440✔
993
            //look for `as SOME_TYPE`
994
            if (this.check(TokenKind.As)) {
9!
995
                this.warnIfNotBrighterScriptMode('typed assignment');
9✔
996

997
                [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
9✔
998
            }
999
        }
1000

1001
        let operator = this.consume(
1,440✔
1002
            DiagnosticMessages.expectedOperatorAfterIdentifier([TokenKind.Equal], name.text),
1003
            ...[TokenKind.Equal]
1004
        );
1005
        let value = this.expression();
1,437✔
1006

1007
        let result = new AssignmentStatement({ equals: operator, name: name, value: value, as: asToken, typeExpression: typeExpression });
1,430✔
1008

1009
        return result;
1,430✔
1010
    }
1011

1012
    private augmentedAssignment(): AugmentedAssignmentStatement {
1013
        let item = this.expression();
60✔
1014

1015
        let operator = this.consume(
60✔
1016
            DiagnosticMessages.expectedToken(...CompoundAssignmentOperators),
1017
            ...CompoundAssignmentOperators
1018
        );
1019
        let value = this.expression();
60✔
1020

1021
        let result = new AugmentedAssignmentStatement({
60✔
1022
            item: item,
1023
            operator: operator,
1024
            value: value
1025
        });
1026

1027
        return result;
60✔
1028
    }
1029

1030
    private checkLibrary() {
1031
        let isLibraryToken = this.check(TokenKind.Library);
20,077✔
1032

1033
        //if we are at the top level, any line that starts with "library" should be considered a library statement
1034
        if (this.isAtRootLevel() && isLibraryToken) {
20,077✔
1035
            return true;
12✔
1036

1037
            //not at root level, library statements are all invalid here, but try to detect if the tokens look
1038
            //like a library statement (and let the libraryStatement function handle emitting the diagnostics)
1039
        } else if (isLibraryToken && this.checkNext(TokenKind.StringLiteral)) {
20,065✔
1040
            return true;
1✔
1041

1042
            //definitely not a library statement
1043
        } else {
1044
            return false;
20,064✔
1045
        }
1046
    }
1047

1048
    private checkAlias() {
1049
        let isAliasToken = this.check(TokenKind.Alias);
19,837✔
1050

1051
        //if we are at the top level, any line that starts with "alias" should be considered a alias statement
1052
        if (this.isAtRootLevel() && isAliasToken) {
19,837✔
1053
            return true;
31✔
1054

1055
            //not at root level, alias statements are all invalid here, but try to detect if the tokens look
1056
            //like a alias statement (and let the alias function handle emitting the diagnostics)
1057
        } else if (isAliasToken && this.checkNext(TokenKind.Identifier)) {
19,806✔
1058
            return true;
2✔
1059

1060
            //definitely not a alias statement
1061
        } else {
1062
            return false;
19,804✔
1063
        }
1064
    }
1065

1066
    private statement(): Statement | undefined {
1067
        if (this.checkLibrary()) {
9,952!
1068
            return this.libraryStatement();
×
1069
        }
1070

1071
        if (this.check(TokenKind.Import)) {
9,952✔
1072
            return this.importStatement();
203✔
1073
        }
1074

1075
        if (this.check(TokenKind.Typecast) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
9,749✔
1076
            return this.typecastStatement();
24✔
1077
        }
1078

1079
        if (this.checkAlias()) {
9,725!
NEW
1080
            return this.aliasStatement();
×
1081
        }
1082

1083
        if (this.check(TokenKind.Stop)) {
9,725✔
1084
            return this.stopStatement();
16✔
1085
        }
1086

1087
        if (this.check(TokenKind.If)) {
9,709✔
1088
            return this.ifStatement();
1,054✔
1089
        }
1090

1091
        //`try` must be followed by a block, otherwise it could be a local variable
1092
        if (this.check(TokenKind.Try) && this.checkAnyNext(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
8,655✔
1093
            return this.tryCatchStatement();
35✔
1094
        }
1095

1096
        if (this.check(TokenKind.Throw)) {
8,620✔
1097
            return this.throwStatement();
12✔
1098
        }
1099

1100
        if (this.checkAny(TokenKind.Print, TokenKind.Question)) {
8,608✔
1101
            return this.printStatement();
1,145✔
1102
        }
1103
        if (this.check(TokenKind.Dim)) {
7,463✔
1104
            return this.dimStatement();
43✔
1105
        }
1106

1107
        if (this.check(TokenKind.While)) {
7,420✔
1108
            return this.whileStatement();
32✔
1109
        }
1110

1111
        if (this.checkAny(TokenKind.Exit, TokenKind.ExitWhile)) {
7,388✔
1112
            return this.exitStatement();
22✔
1113
        }
1114

1115
        if (this.check(TokenKind.For)) {
7,366✔
1116
            return this.forStatement();
39✔
1117
        }
1118

1119
        if (this.check(TokenKind.ForEach)) {
7,327✔
1120
            return this.forEachStatement();
36✔
1121
        }
1122

1123
        if (this.check(TokenKind.End)) {
7,291✔
1124
            return this.endStatement();
8✔
1125
        }
1126

1127
        if (this.match(TokenKind.Return)) {
7,283✔
1128
            return this.returnStatement();
3,039✔
1129
        }
1130

1131
        if (this.check(TokenKind.Goto)) {
4,244✔
1132
            return this.gotoStatement();
12✔
1133
        }
1134

1135
        //the continue keyword (followed by `for`, `while`, or a statement separator)
1136
        if (this.check(TokenKind.Continue) && this.checkAnyNext(TokenKind.While, TokenKind.For, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
4,232✔
1137
            return this.continueStatement();
12✔
1138
        }
1139

1140
        //does this line look like a label? (i.e.  `someIdentifier:` )
1141
        if (this.check(TokenKind.Identifier) && this.checkNext(TokenKind.Colon) && this.checkPrevious(TokenKind.Newline)) {
4,220✔
1142
            try {
12✔
1143
                return this.labelStatement();
12✔
1144
            } catch (err) {
1145
                if (!(err instanceof CancelStatementError)) {
2!
1146
                    throw err;
×
1147
                }
1148
                //not a label, try something else
1149
            }
1150
        }
1151

1152
        // BrightScript is like python, in that variables can be declared without a `var`,
1153
        // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
1154
        // out what to do with it.
1155
        if (
4,210✔
1156
            this.checkAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers)
1157
        ) {
1158
            if (this.checkAnyNext(...AssignmentOperators)) {
3,981✔
1159
                if (this.checkAnyNext(...CompoundAssignmentOperators)) {
1,432✔
1160
                    return this.augmentedAssignment();
60✔
1161
                }
1162
                return this.assignment();
1,372✔
1163
            } else if (this.checkNext(TokenKind.As)) {
2,549✔
1164
                // may be a typed assignment
1165
                const backtrack = this.current;
10✔
1166
                let validTypeExpression = false;
10✔
1167

1168
                try {
10✔
1169
                    // skip the identifier, and check for valid type expression
1170
                    this.advance();
10✔
1171
                    const parts = this.consumeAsTokenAndTypeExpression(true);
10✔
1172
                    validTypeExpression = !!(parts?.[0] && parts?.[1]);
10!
1173
                } catch (e) {
1174
                    // ignore any errors
1175
                } finally {
1176
                    this.current = backtrack;
10✔
1177
                }
1178
                if (validTypeExpression) {
10✔
1179
                    // there is a valid 'as' and type expression
1180
                    return this.assignment(true);
9✔
1181
                }
1182
            }
1183
        }
1184

1185
        //some BrighterScript keywords are allowed as a local identifiers, so we need to check for them AFTER the assignment check
1186
        if (this.check(TokenKind.Interface)) {
2,769✔
1187
            return this.interfaceDeclaration();
155✔
1188
        }
1189

1190
        if (this.check(TokenKind.Class)) {
2,614✔
1191
            return this.classDeclaration();
675✔
1192
        }
1193

1194
        if (this.check(TokenKind.Namespace)) {
1,939✔
1195
            return this.namespaceStatement();
609✔
1196
        }
1197

1198
        if (this.check(TokenKind.Enum)) {
1,330✔
1199
            return this.enumDeclaration();
165✔
1200
        }
1201

1202
        // TODO: support multi-statements
1203
        return this.setStatement();
1,165✔
1204
    }
1205

1206
    private whileStatement(): WhileStatement {
1207
        const whileKeyword = this.advance();
32✔
1208
        const condition = this.expression();
32✔
1209

1210
        this.consumeStatementSeparators();
31✔
1211

1212
        const whileBlock = this.block(TokenKind.EndWhile);
31✔
1213
        let endWhile: Token;
1214
        if (!whileBlock || this.peek().kind !== TokenKind.EndWhile) {
31✔
1215
            this.diagnostics.push({
1✔
1216
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('while'),
1217
                location: this.peek().location
1218
            });
1219
            if (!whileBlock) {
1!
1220
                throw this.lastDiagnosticAsError();
×
1221
            }
1222
        } else {
1223
            endWhile = this.advance();
30✔
1224
        }
1225

1226
        return new WhileStatement({
31✔
1227
            while: whileKeyword,
1228
            endWhile: endWhile,
1229
            condition: condition,
1230
            body: whileBlock
1231
        });
1232
    }
1233

1234
    private exitStatement(): ExitStatement {
1235
        let exitToken = this.advance();
22✔
1236
        if (exitToken.kind === TokenKind.ExitWhile) {
22✔
1237
            // `exitwhile` is allowed in code, and means `exit while`
1238
            // use an ExitStatement that is nicer to work with by breaking the `exit` and `while` tokens apart
1239

1240
            const exitText = exitToken.text.substring(0, 4);
5✔
1241
            const whileText = exitToken.text.substring(4);
5✔
1242
            const originalRange = exitToken.location.range;
5✔
1243
            const originalStart = originalRange.start;
4✔
1244

1245
            const exitRange = util.createRange(
4✔
1246
                originalStart.line,
1247
                originalStart.character,
1248
                originalStart.line,
1249
                originalStart.character + 4);
1250
            const whileRange = util.createRange(
4✔
1251
                originalStart.line,
1252
                originalStart.character + 4,
1253
                originalStart.line,
1254
                originalStart.character + exitToken.text.length);
1255

1256
            exitToken = createToken(TokenKind.Exit, exitText, util.createLocationFromRange(exitToken.location.uri, exitRange));
4✔
1257
            this.tokens[this.current - 1] = exitToken;
4✔
1258
            const newLoopToken = createToken(TokenKind.While, whileText, util.createLocationFromRange(exitToken.location.uri, whileRange));
4✔
1259
            this.tokens.splice(this.current, 0, newLoopToken);
4✔
1260
        }
1261

1262
        const loopTypeToken = this.tryConsume(
21✔
1263
            DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
1264
            TokenKind.While, TokenKind.For
1265
        );
1266

1267
        return new ExitStatement({
21✔
1268
            exit: exitToken,
1269
            loopType: loopTypeToken
1270
        });
1271
    }
1272

1273
    private forStatement(): ForStatement {
1274
        const forToken = this.advance();
39✔
1275
        const initializer = this.assignment();
39✔
1276

1277
        //TODO: newline allowed?
1278

1279
        const toToken = this.advance();
38✔
1280
        const finalValue = this.expression();
38✔
1281
        let incrementExpression: Expression | undefined;
1282
        let stepToken: Token | undefined;
1283

1284
        if (this.check(TokenKind.Step)) {
38✔
1285
            stepToken = this.advance();
10✔
1286
            incrementExpression = this.expression();
10✔
1287
        } else {
1288
            // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
1289
        }
1290

1291
        this.consumeStatementSeparators();
38✔
1292

1293
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
38✔
1294
        let endForToken: Token;
1295
        if (!body || !this.checkAny(TokenKind.EndFor, TokenKind.Next)) {
38✔
1296
            this.diagnostics.push({
1✔
1297
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1298
                location: this.peek().location
1299
            });
1300
            if (!body) {
1!
1301
                throw this.lastDiagnosticAsError();
×
1302
            }
1303
        } else {
1304
            endForToken = this.advance();
37✔
1305
        }
1306

1307
        // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
1308
        // stays around in scope with whatever value it was when the loop exited.
1309
        return new ForStatement({
38✔
1310
            for: forToken,
1311
            counterDeclaration: initializer,
1312
            to: toToken,
1313
            finalValue: finalValue,
1314
            body: body,
1315
            endFor: endForToken,
1316
            step: stepToken,
1317
            increment: incrementExpression
1318
        });
1319
    }
1320

1321
    private forEachStatement(): ForEachStatement {
1322
        let forEach = this.advance();
36✔
1323
        let name = this.advance();
36✔
1324

1325
        let maybeIn = this.peek();
36✔
1326
        if (this.check(TokenKind.Identifier) && maybeIn.text.toLowerCase() === 'in') {
36!
1327
            this.advance();
36✔
1328
        } else {
1329
            this.diagnostics.push({
×
1330
                ...DiagnosticMessages.expectedInAfterForEach(name.text),
1331
                location: this.peek().location
1332
            });
1333
            throw this.lastDiagnosticAsError();
×
1334
        }
1335
        maybeIn.kind = TokenKind.In;
36✔
1336

1337
        let target = this.expression();
36✔
1338
        if (!target) {
36!
1339
            this.diagnostics.push({
×
1340
                ...DiagnosticMessages.expectedExpressionAfterForEachIn(),
1341
                location: this.peek().location
1342
            });
1343
            throw this.lastDiagnosticAsError();
×
1344
        }
1345

1346
        this.consumeStatementSeparators();
36✔
1347

1348
        let body = this.block(TokenKind.EndFor, TokenKind.Next);
36✔
1349
        if (!body) {
36!
1350
            this.diagnostics.push({
×
1351
                ...DiagnosticMessages.expectedEndForOrNextToTerminateForLoop(),
1352
                location: this.peek().location
1353
            });
1354
            throw this.lastDiagnosticAsError();
×
1355
        }
1356

1357
        let endFor = this.advance();
36✔
1358

1359
        return new ForEachStatement({
36✔
1360
            forEach: forEach,
1361
            in: maybeIn,
1362
            endFor: endFor,
1363
            item: name,
1364
            target: target,
1365
            body: body
1366
        });
1367
    }
1368

1369
    private namespaceStatement(): NamespaceStatement | undefined {
1370
        this.warnIfNotBrighterScriptMode('namespace');
609✔
1371
        let keyword = this.advance();
609✔
1372

1373
        this.namespaceAndFunctionDepth++;
609✔
1374

1375
        let name = this.identifyingExpression();
609✔
1376
        //set the current namespace name
1377

1378
        this.globalTerminators.push([TokenKind.EndNamespace]);
608✔
1379
        let body = this.body();
608✔
1380
        this.globalTerminators.pop();
608✔
1381

1382
        let endKeyword: Token;
1383
        if (this.check(TokenKind.EndNamespace)) {
608✔
1384
            endKeyword = this.advance();
606✔
1385
        } else {
1386
            //the `end namespace` keyword is missing. add a diagnostic, but keep parsing
1387
            this.diagnostics.push({
2✔
1388
                ...DiagnosticMessages.couldNotFindMatchingEndKeyword('namespace'),
1389
                location: keyword.location
1390
            });
1391
        }
1392

1393
        this.namespaceAndFunctionDepth--;
608✔
1394

1395
        let result = new NamespaceStatement({
608✔
1396
            namespace: keyword,
1397
            nameExpression: name,
1398
            body: body,
1399
            endNamespace: endKeyword
1400
        });
1401

1402
        //cache the range property so that plugins can't affect it
1403
        result.cacheLocation();
608✔
1404
        result.body.symbolTable.name += `: namespace '${result.name}'`;
608✔
1405
        return result;
608✔
1406
    }
1407

1408
    /**
1409
     * Get an expression with identifiers separated by periods. Useful for namespaces and class extends
1410
     */
1411
    private identifyingExpression(allowedTokenKinds?: TokenKind[]): DottedGetExpression | VariableExpression {
1412
        allowedTokenKinds = allowedTokenKinds ?? this.allowedLocalIdentifiers;
1,260✔
1413
        let firstIdentifier = this.consume(
1,260✔
1414
            DiagnosticMessages.expectedIdentifierAfterKeyword(this.previous().text),
1415
            TokenKind.Identifier,
1416
            ...allowedTokenKinds
1417
        ) as Identifier;
1418

1419
        let expr: DottedGetExpression | VariableExpression;
1420

1421
        if (firstIdentifier) {
1,259!
1422
            // force it into an identifier so the AST makes some sense
1423
            firstIdentifier.kind = TokenKind.Identifier;
1,259✔
1424
            const varExpr = new VariableExpression({ name: firstIdentifier });
1,259✔
1425
            expr = varExpr;
1,259✔
1426

1427
            //consume multiple dot identifiers (i.e. `Name.Space.Can.Have.Many.Parts`)
1428
            while (this.check(TokenKind.Dot)) {
1,259✔
1429
                let dot = this.tryConsume(
450✔
1430
                    DiagnosticMessages.unexpectedToken(this.peek().text),
1431
                    TokenKind.Dot
1432
                );
1433
                if (!dot) {
450!
1434
                    break;
×
1435
                }
1436
                let identifier = this.tryConsume(
450✔
1437
                    DiagnosticMessages.expectedIdentifier(),
1438
                    TokenKind.Identifier,
1439
                    ...allowedTokenKinds,
1440
                    ...AllowedProperties
1441
                ) as Identifier;
1442

1443
                if (!identifier) {
450✔
1444
                    break;
3✔
1445
                }
1446
                // force it into an identifier so the AST makes some sense
1447
                identifier.kind = TokenKind.Identifier;
447✔
1448
                expr = new DottedGetExpression({ obj: expr, name: identifier, dot: dot });
447✔
1449
            }
1450
        }
1451
        return expr;
1,259✔
1452
    }
1453
    /**
1454
     * Add an 'unexpected token' diagnostic for any token found between current and the first stopToken found.
1455
     */
1456
    private flagUntil(...stopTokens: TokenKind[]) {
1457
        while (!this.checkAny(...stopTokens) && !this.isAtEnd()) {
4!
1458
            let token = this.advance();
×
1459
            this.diagnostics.push({
×
1460
                ...DiagnosticMessages.unexpectedToken(token.text),
1461
                location: token.location
1462
            });
1463
        }
1464
    }
1465

1466
    /**
1467
     * Consume tokens until one of the `stopTokenKinds` is encountered
1468
     * @param stopTokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
1469
     * @returns - the list of tokens consumed, EXCLUDING the `stopTokenKind` (you can use `this.peek()` to see which one it was)
1470
     */
1471
    private consumeUntil(...stopTokenKinds: TokenKind[]) {
1472
        let result = [] as Token[];
61✔
1473
        //take tokens until we encounter one of the stopTokenKinds
1474
        while (!stopTokenKinds.includes(this.peek().kind)) {
61✔
1475
            result.push(this.advance());
136✔
1476
        }
1477
        return result;
61✔
1478
    }
1479

1480
    private constDeclaration(): ConstStatement | undefined {
1481
        this.warnIfNotBrighterScriptMode('const declaration');
155✔
1482
        const constToken = this.advance();
155✔
1483
        const nameToken = this.identifier(...this.allowedLocalIdentifiers);
155✔
1484
        const equalToken = this.consumeToken(TokenKind.Equal);
155✔
1485
        const expression = this.expression();
155✔
1486
        const statement = new ConstStatement({
155✔
1487
            const: constToken,
1488
            name: nameToken,
1489
            equals: equalToken,
1490
            value: expression
1491
        });
1492
        return statement;
155✔
1493
    }
1494

1495
    private libraryStatement(): LibraryStatement | undefined {
1496
        let libStatement = new LibraryStatement({
13✔
1497
            library: this.advance(),
1498
            //grab the next token only if it's a string
1499
            filePath: this.tryConsume(
1500
                DiagnosticMessages.expectedStringLiteralAfterKeyword('library'),
1501
                TokenKind.StringLiteral
1502
            )
1503
        });
1504

1505
        return libStatement;
13✔
1506
    }
1507

1508
    private importStatement() {
1509
        this.warnIfNotBrighterScriptMode('import statements');
203✔
1510
        let importStatement = new ImportStatement({
203✔
1511
            import: this.advance(),
1512
            //grab the next token only if it's a string
1513
            path: this.tryConsume(
1514
                DiagnosticMessages.expectedStringLiteralAfterKeyword('import'),
1515
                TokenKind.StringLiteral
1516
            )
1517
        });
1518

1519
        return importStatement;
203✔
1520
    }
1521

1522
    private typecastStatement() {
1523
        this.warnIfNotBrighterScriptMode('typecast statements');
24✔
1524
        const typecastToken = this.advance();
24✔
1525
        const typecastExpr = this.expression();
24✔
1526
        if (isTypecastExpression(typecastExpr)) {
24!
1527
            return new TypecastStatement({
24✔
1528
                typecast: typecastToken,
1529
                typecastExpression: typecastExpr
1530
            });
1531
        }
NEW
1532
        this.diagnostics.push({
×
1533
            ...DiagnosticMessages.expectedIdentifierAfterKeyword('typecast'),
1534
            location: {
1535
                uri: typecastToken.location.uri,
1536
                range: util.createBoundingRange(typecastToken, this.peek())
1537
            }
1538
        });
NEW
1539
        throw this.lastDiagnosticAsError();
×
1540
    }
1541

1542
    private aliasStatement(): AliasStatement | undefined {
1543
        this.warnIfNotBrighterScriptMode('alias statements');
33✔
1544
        const aliasToken = this.advance();
33✔
1545
        const name = this.tryConsume(
33✔
1546
            DiagnosticMessages.expectedIdentifierAfterKeyword('alias'),
1547
            TokenKind.Identifier
1548
        );
1549
        const equals = this.tryConsume(
33✔
1550
            DiagnosticMessages.expectedToken(TokenKind.Equal),
1551
            TokenKind.Equal
1552
        );
1553
        let value = this.identifyingExpression();
33✔
1554

1555
        let aliasStmt = new AliasStatement({
33✔
1556
            alias: aliasToken,
1557
            name: name,
1558
            equals: equals,
1559
            value: value
1560

1561
        });
1562

1563
        return aliasStmt;
33✔
1564
    }
1565

1566
    private annotationExpression() {
1567
        const atToken = this.advance();
75✔
1568
        const identifier = this.tryConsume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
75✔
1569
        if (identifier) {
75✔
1570
            identifier.kind = TokenKind.Identifier;
74✔
1571
        }
1572
        let annotation = new AnnotationExpression({ at: atToken, name: identifier });
75✔
1573
        this.pendingAnnotations.push(annotation);
74✔
1574

1575
        //optional arguments
1576
        if (this.check(TokenKind.LeftParen)) {
74✔
1577
            let leftParen = this.advance();
30✔
1578
            annotation.call = this.finishCall(leftParen, annotation, false);
30✔
1579
        }
1580
        return annotation;
74✔
1581
    }
1582

1583
    private ternaryExpression(test?: Expression): TernaryExpression {
1584
        this.warnIfNotBrighterScriptMode('ternary operator');
78✔
1585
        if (!test) {
78!
1586
            test = this.expression();
×
1587
        }
1588
        const questionMarkToken = this.advance();
78✔
1589

1590
        //consume newlines or comments
1591
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
78✔
1592
            this.advance();
7✔
1593
        }
1594

1595
        let consequent: Expression;
1596
        try {
78✔
1597
            consequent = this.expression();
78✔
1598
        } catch { }
1599

1600
        //consume newlines or comments
1601
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
78✔
1602
            this.advance();
5✔
1603
        }
1604

1605
        const colonToken = this.tryConsumeToken(TokenKind.Colon);
78✔
1606

1607
        //consume newlines
1608
        while (this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
78✔
1609
            this.advance();
11✔
1610
        }
1611
        let alternate: Expression;
1612
        try {
78✔
1613
            alternate = this.expression();
78✔
1614
        } catch { }
1615

1616
        return new TernaryExpression({
78✔
1617
            test: test,
1618
            questionMark: questionMarkToken,
1619
            consequent: consequent,
1620
            colon: colonToken,
1621
            alternate: alternate
1622
        });
1623
    }
1624

1625
    private nullCoalescingExpression(test: Expression): NullCoalescingExpression {
1626
        this.warnIfNotBrighterScriptMode('null coalescing operator');
34✔
1627
        const questionQuestionToken = this.advance();
34✔
1628
        const alternate = this.expression();
34✔
1629
        return new NullCoalescingExpression({
34✔
1630
            consequent: test,
1631
            questionQuestion: questionQuestionToken,
1632
            alternate: alternate
1633
        });
1634
    }
1635

1636
    private regexLiteralExpression() {
1637
        this.warnIfNotBrighterScriptMode('regular expression literal');
45✔
1638
        return new RegexLiteralExpression({
45✔
1639
            regexLiteral: this.advance()
1640
        });
1641
    }
1642

1643
    private templateString(isTagged: boolean): TemplateStringExpression | TaggedTemplateStringExpression {
1644
        this.warnIfNotBrighterScriptMode('template string');
51✔
1645

1646
        //get the tag name
1647
        let tagName: Identifier;
1648
        if (isTagged) {
51✔
1649
            tagName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties) as Identifier;
8✔
1650
            // force it into an identifier so the AST makes some sense
1651
            tagName.kind = TokenKind.Identifier;
8✔
1652
        }
1653

1654
        let quasis = [] as TemplateStringQuasiExpression[];
51✔
1655
        let expressions = [];
51✔
1656
        let openingBacktick = this.peek();
51✔
1657
        this.advance();
51✔
1658
        let currentQuasiExpressionParts = [];
51✔
1659
        while (!this.isAtEnd() && !this.check(TokenKind.BackTick)) {
51✔
1660
            let next = this.peek();
194✔
1661
            if (next.kind === TokenKind.TemplateStringQuasi) {
194✔
1662
                //a quasi can actually be made up of multiple quasis when it includes char literals
1663
                currentQuasiExpressionParts.push(
122✔
1664
                    new LiteralExpression({ value: next })
1665
                );
1666
                this.advance();
122✔
1667
            } else if (next.kind === TokenKind.EscapedCharCodeLiteral) {
72✔
1668
                currentQuasiExpressionParts.push(
31✔
1669
                    new EscapedCharCodeLiteralExpression({ value: next as Token & { charCode: number } })
1670
                );
1671
                this.advance();
31✔
1672
            } else {
1673
                //finish up the current quasi
1674
                quasis.push(
41✔
1675
                    new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1676
                );
1677
                currentQuasiExpressionParts = [];
41✔
1678

1679
                if (next.kind === TokenKind.TemplateStringExpressionBegin) {
41!
1680
                    this.advance();
41✔
1681
                }
1682
                //now keep this expression
1683
                expressions.push(this.expression());
41✔
1684
                if (!this.isAtEnd() && this.check(TokenKind.TemplateStringExpressionEnd)) {
41!
1685
                    //TODO is it an error if this is not present?
1686
                    this.advance();
41✔
1687
                } else {
1688
                    this.diagnostics.push({
×
1689
                        ...DiagnosticMessages.unterminatedTemplateExpression(),
1690
                        location: {
1691
                            uri: openingBacktick.location.uri,
1692
                            range: util.createBoundingRange(openingBacktick, this.peek())
1693
                        }
1694
                    });
1695
                    throw this.lastDiagnosticAsError();
×
1696
                }
1697
            }
1698
        }
1699

1700
        //store the final set of quasis
1701
        quasis.push(
51✔
1702
            new TemplateStringQuasiExpression({ expressions: currentQuasiExpressionParts })
1703
        );
1704

1705
        if (this.isAtEnd()) {
51✔
1706
            //error - missing backtick
1707
            this.diagnostics.push({
2✔
1708
                ...DiagnosticMessages.unterminatedTemplateStringAtEndOfFile(),
1709
                location: {
1710
                    uri: openingBacktick.location.uri,
1711
                    range: util.createBoundingRange(openingBacktick, this.peek())
1712
                }
1713
            });
1714
            throw this.lastDiagnosticAsError();
2✔
1715

1716
        } else {
1717
            let closingBacktick = this.advance();
49✔
1718
            if (isTagged) {
49✔
1719
                return new TaggedTemplateStringExpression({
8✔
1720
                    tagName: tagName,
1721
                    openingBacktick: openingBacktick,
1722
                    quasis: quasis,
1723
                    expressions: expressions,
1724
                    closingBacktick: closingBacktick
1725
                });
1726
            } else {
1727
                return new TemplateStringExpression({
41✔
1728
                    openingBacktick: openingBacktick,
1729
                    quasis: quasis,
1730
                    expressions: expressions,
1731
                    closingBacktick: closingBacktick
1732
                });
1733
            }
1734
        }
1735
    }
1736

1737
    private tryCatchStatement(): TryCatchStatement {
1738
        const tryToken = this.advance();
35✔
1739
        let endTryToken: Token;
1740
        let catchStmt: CatchStatement;
1741
        //ensure statement separator
1742
        this.consumeStatementSeparators();
35✔
1743

1744
        let tryBranch = this.block(TokenKind.Catch, TokenKind.EndTry);
35✔
1745

1746
        const peek = this.peek();
35✔
1747
        if (peek.kind !== TokenKind.Catch) {
35✔
1748
            this.diagnostics.push({
2✔
1749
                ...DiagnosticMessages.expectedCatchBlockInTryCatch(),
1750
                location: this.peek()?.location
6!
1751
            });
1752
        } else {
1753
            const catchToken = this.advance();
33✔
1754

1755
            //get the exception variable as an expression
1756
            let exceptionVariableExpression: Expression;
1757
            //if we consumed any statement separators, that means we don't have an exception variable
1758
            if (this.consumeStatementSeparators(true)) {
33✔
1759
                //no exception variable. That's fine in BrighterScript but not in brightscript. But that'll get caught by the validator later...
1760
            } else {
1761
                exceptionVariableExpression = this.expression(true);
27✔
1762
                this.consumeStatementSeparators();
27✔
1763
            }
1764

1765
            const catchBranch = this.block(TokenKind.EndTry);
33✔
1766
            catchStmt = new CatchStatement({
33✔
1767
                catch: catchToken,
1768
                exceptionVariableExpression: exceptionVariableExpression,
1769
                catchBranch: catchBranch
1770
            });
1771
        }
1772
        if (this.peek().kind !== TokenKind.EndTry) {
35✔
1773
            this.diagnostics.push({
2✔
1774
                ...DiagnosticMessages.expectedEndTryToTerminateTryCatch(),
1775
                location: this.peek().location
1776
            });
1777
        } else {
1778
            endTryToken = this.advance();
33✔
1779
        }
1780

1781
        const statement = new TryCatchStatement({
35✔
1782
            try: tryToken,
1783
            tryBranch: tryBranch,
1784
            catchStatement: catchStmt,
1785
            endTry: endTryToken
1786
        }
1787
        );
1788
        return statement;
35✔
1789
    }
1790

1791
    private throwStatement() {
1792
        const throwToken = this.advance();
12✔
1793
        let expression: Expression;
1794
        if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
12✔
1795
            this.diagnostics.push({
2✔
1796
                ...DiagnosticMessages.missingExceptionExpressionAfterThrowKeyword(),
1797
                location: throwToken.location
1798
            });
1799
        } else {
1800
            expression = this.expression();
10✔
1801
        }
1802
        return new ThrowStatement({ throw: throwToken, expression: expression });
10✔
1803
    }
1804

1805
    private dimStatement() {
1806
        const dim = this.advance();
43✔
1807

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

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

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

1832
        if (expressions.length === 0) {
43✔
1833
            this.diagnostics.push({
5✔
1834
                ...DiagnosticMessages.missingExpressionsInDimStatement(),
1835
                location: this.peek().location
1836
            });
1837
        }
1838
        let rightSquareBracket = this.tryConsume(DiagnosticMessages.missingRightSquareBracketAfterDimIdentifier(), TokenKind.RightSquareBracket);
43✔
1839
        return new DimStatement({
43✔
1840
            dim: dim,
1841
            name: identifier,
1842
            openingSquare: leftSquareBracket,
1843
            dimensions: expressions,
1844
            closingSquare: rightSquareBracket
1845
        });
1846
    }
1847

1848
    private nestedInlineConditionalCount = 0;
3,440✔
1849

1850
    private ifStatement(incrementNestedCount = true): IfStatement {
1,976✔
1851
        // colon before `if` is usually not allowed, unless it's after `then`
1852
        if (this.current > 0) {
1,986✔
1853
            const prev = this.previous();
1,981✔
1854
            if (prev.kind === TokenKind.Colon) {
1,981✔
1855
                if (this.current > 1 && this.tokens[this.current - 2].kind !== TokenKind.Then && this.nestedInlineConditionalCount === 0) {
4✔
1856
                    this.diagnostics.push({
1✔
1857
                        ...DiagnosticMessages.unexpectedColonBeforeIfStatement(),
1858
                        location: prev.location
1859
                    });
1860
                }
1861
            }
1862
        }
1863

1864
        const ifToken = this.advance();
1,986✔
1865

1866
        const condition = this.expression();
1,986✔
1867
        let thenBranch: Block;
1868
        let elseBranch: IfStatement | Block | undefined;
1869

1870
        let thenToken: Token | undefined;
1871
        let endIfToken: Token | undefined;
1872
        let elseToken: Token | undefined;
1873

1874
        //optional `then`
1875
        if (this.check(TokenKind.Then)) {
1,984✔
1876
            thenToken = this.advance();
1,583✔
1877
        }
1878

1879
        //is it inline or multi-line if?
1880
        const isInlineIfThen = !this.checkAny(TokenKind.Newline, TokenKind.Colon, TokenKind.Comment);
1,984✔
1881

1882
        if (isInlineIfThen) {
1,984✔
1883
            /*** PARSE INLINE IF STATEMENT ***/
1884
            if (!incrementNestedCount) {
48✔
1885
                this.nestedInlineConditionalCount++;
5✔
1886
            }
1887

1888
            thenBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
48✔
1889

1890
            if (!thenBranch) {
48!
1891
                this.diagnostics.push({
×
1892
                    ...DiagnosticMessages.expectedStatementToFollowConditionalCondition(ifToken.text),
1893
                    location: this.peek().location
1894
                });
1895
                throw this.lastDiagnosticAsError();
×
1896
            } else {
1897
                this.ensureInline(thenBranch.statements);
48✔
1898
            }
1899

1900
            //else branch
1901
            if (this.check(TokenKind.Else)) {
48✔
1902
                elseToken = this.advance();
33✔
1903

1904
                if (this.check(TokenKind.If)) {
33✔
1905
                    // recurse-read `else if`
1906
                    elseBranch = this.ifStatement(false);
10✔
1907

1908
                    //no multi-line if chained with an inline if
1909
                    if (!elseBranch.isInline) {
9✔
1910
                        this.diagnostics.push({
4✔
1911
                            ...DiagnosticMessages.expectedInlineIfStatement(),
1912
                            location: elseBranch.location
1913
                        });
1914
                    }
1915

1916
                } else if (this.checkAny(TokenKind.Newline, TokenKind.Colon)) {
23✔
1917
                    //expecting inline else branch
1918
                    this.diagnostics.push({
3✔
1919
                        ...DiagnosticMessages.expectedInlineIfStatement(),
1920
                        location: this.peek().location
1921
                    });
1922
                    throw this.lastDiagnosticAsError();
3✔
1923
                } else {
1924
                    elseBranch = this.inlineConditionalBranch(TokenKind.Else, TokenKind.EndIf);
20✔
1925

1926
                    if (elseBranch) {
20!
1927
                        this.ensureInline(elseBranch.statements);
20✔
1928
                    }
1929
                }
1930

1931
                if (!elseBranch) {
29!
1932
                    //missing `else` branch
1933
                    this.diagnostics.push({
×
1934
                        ...DiagnosticMessages.expectedStatementToFollowElse(),
1935
                        location: this.peek().location
1936
                    });
1937
                    throw this.lastDiagnosticAsError();
×
1938
                }
1939
            }
1940

1941
            if (!elseBranch || !isIfStatement(elseBranch)) {
44✔
1942
                //enforce newline at the end of the inline if statement
1943
                const peek = this.peek();
35✔
1944
                if (peek.kind !== TokenKind.Newline && peek.kind !== TokenKind.Comment && peek.kind !== TokenKind.Else && !this.isAtEnd()) {
35✔
1945
                    //ignore last error if it was about a colon
1946
                    if (this.previous().kind === TokenKind.Colon) {
3!
1947
                        this.diagnostics.pop();
3✔
1948
                        this.current--;
3✔
1949
                    }
1950
                    //newline is required
1951
                    this.diagnostics.push({
3✔
1952
                        ...DiagnosticMessages.expectedFinalNewline(),
1953
                        location: this.peek().location
1954
                    });
1955
                }
1956
            }
1957
            this.nestedInlineConditionalCount--;
44✔
1958
        } else {
1959
            /*** PARSE MULTI-LINE IF STATEMENT ***/
1960

1961
            thenBranch = this.blockConditionalBranch(ifToken);
1,936✔
1962

1963
            //ensure newline/colon before next keyword
1964
            this.ensureNewLineOrColon();
1,933✔
1965

1966
            //else branch
1967
            if (this.check(TokenKind.Else)) {
1,933✔
1968
                elseToken = this.advance();
1,550✔
1969

1970
                if (this.check(TokenKind.If)) {
1,550✔
1971
                    // recurse-read `else if`
1972
                    elseBranch = this.ifStatement();
922✔
1973

1974
                } else {
1975
                    elseBranch = this.blockConditionalBranch(ifToken);
628✔
1976

1977
                    //ensure newline/colon before next keyword
1978
                    this.ensureNewLineOrColon();
628✔
1979
                }
1980
            }
1981

1982
            if (!isIfStatement(elseBranch)) {
1,933✔
1983
                if (this.check(TokenKind.EndIf)) {
1,011✔
1984
                    endIfToken = this.advance();
1,008✔
1985

1986
                } else {
1987
                    //missing endif
1988
                    this.diagnostics.push({
3✔
1989
                        ...DiagnosticMessages.expectedEndIfToCloseIfStatement(ifToken.location?.range.start),
9!
1990
                        location: ifToken.location
1991
                    });
1992
                }
1993
            }
1994
        }
1995

1996
        return new IfStatement({
1,977✔
1997
            if: ifToken,
1998
            then: thenToken,
1999
            endIf: endIfToken,
2000
            else: elseToken,
2001
            condition: condition,
2002
            thenBranch: thenBranch,
2003
            elseBranch: elseBranch
2004
        });
2005
    }
2006

2007
    //consume a `then` or `else` branch block of an `if` statement
2008
    private blockConditionalBranch(ifToken: Token) {
2009
        //keep track of the current error count, because if the then branch fails,
2010
        //we will trash them in favor of a single error on if
2011
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
2,564✔
2012

2013
        // we're parsing a multi-line ("block") form of the BrightScript if/then and must find
2014
        // a trailing "end if" or "else if"
2015
        let branch = this.block(TokenKind.EndIf, TokenKind.Else);
2,564✔
2016

2017
        if (!branch) {
2,564✔
2018
            //throw out any new diagnostics created as a result of a `then` block parse failure.
2019
            //the block() function will discard the current line, so any discarded diagnostics will
2020
            //resurface if they are legitimate, and not a result of a malformed if statement
2021
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
3✔
2022

2023
            //this whole if statement is bogus...add error to the if token and hard-fail
2024
            this.diagnostics.push({
3✔
2025
                ...DiagnosticMessages.expectedEndIfElseIfOrElseToTerminateThenBlock(),
2026
                location: ifToken.location
2027
            });
2028
            throw this.lastDiagnosticAsError();
3✔
2029
        }
2030
        return branch;
2,561✔
2031
    }
2032

2033
    private conditionalCompileStatement(): ConditionalCompileStatement {
2034
        const hashIfToken = this.advance();
56✔
2035
        let notToken: Token | undefined;
2036

2037
        if (this.check(TokenKind.Not)) {
56✔
2038
            notToken = this.advance();
7✔
2039
        }
2040

2041
        if (!this.checkAny(TokenKind.True, TokenKind.False, TokenKind.Identifier)) {
56✔
2042
            this.diagnostics.push({
1✔
2043
                ...DiagnosticMessages.invalidHashIfValue(),
2044
                location: this.peek()?.location
3!
2045
            });
2046
        }
2047

2048

2049
        const condition = this.advance();
56✔
2050

2051
        let thenBranch: Block;
2052
        let elseBranch: ConditionalCompileStatement | Block | undefined;
2053

2054
        let hashEndIfToken: Token | undefined;
2055
        let hashElseToken: Token | undefined;
2056

2057
        //keep track of the current error count
2058
        //if this is `#if false` remove all diagnostics.
2059
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
56✔
2060

2061
        thenBranch = this.blockConditionalCompileBranch(hashIfToken);
56✔
2062
        const conditionTextLower = condition.text.toLowerCase();
55✔
2063
        if (!this.options.bsConsts?.get(conditionTextLower) || conditionTextLower === 'false') {
55✔
2064
            //throw out any new diagnostics created as a result of a false block
2065
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
45✔
2066
        }
2067

2068
        this.ensureNewLine();
55✔
2069
        this.advance();
55✔
2070

2071
        //else branch
2072
        if (this.check(TokenKind.HashElseIf)) {
55✔
2073
            // recurse-read `#else if`
2074
            elseBranch = this.conditionalCompileStatement();
15✔
2075
            this.ensureNewLine();
15✔
2076

2077
        } else if (this.check(TokenKind.HashElse)) {
40✔
2078
            hashElseToken = this.advance();
10✔
2079
            let diagnosticsLengthBeforeBlock = this.diagnostics.length;
10✔
2080
            elseBranch = this.blockConditionalCompileBranch(hashIfToken);
10✔
2081

2082
            if (condition.text.toLowerCase() === 'true') {
10✔
2083
                //throw out any new diagnostics created as a result of a false block
2084
                this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
1✔
2085
            }
2086
            this.ensureNewLine();
10✔
2087
            this.advance();
10✔
2088
        }
2089

2090
        if (!isConditionalCompileStatement(elseBranch)) {
55✔
2091

2092
            if (this.check(TokenKind.HashEndIf)) {
40!
2093
                hashEndIfToken = this.advance();
40✔
2094

2095
            } else {
2096
                //missing #endif
NEW
2097
                this.diagnostics.push({
×
2098
                    ...DiagnosticMessages.expectedHashEndIfToCloseHashIf(hashIfToken.location?.range.start.line),
×
2099
                    location: hashIfToken.location
2100
                });
2101
            }
2102
        }
2103

2104
        return new ConditionalCompileStatement({
55✔
2105
            hashIf: hashIfToken,
2106
            hashElse: hashElseToken,
2107
            hashEndIf: hashEndIfToken,
2108
            not: notToken,
2109
            condition: condition,
2110
            thenBranch: thenBranch,
2111
            elseBranch: elseBranch
2112
        });
2113
    }
2114

2115
    //consume a conditional compile branch block of an `#if` statement
2116
    private blockConditionalCompileBranch(hashIfToken: Token) {
2117
        //keep track of the current error count, because if the then branch fails,
2118
        //we will trash them in favor of a single error on if
2119
        let diagnosticsLengthBeforeBlock = this.diagnostics.length;
66✔
2120

2121
        //parsing until trailing "#end if", "#else", "#else if"
2122
        let branch = this.conditionalCompileBlock();
66✔
2123

2124
        if (!branch) {
65!
2125
            //throw out any new diagnostics created as a result of a `then` block parse failure.
2126
            //the block() function will discard the current line, so any discarded diagnostics will
2127
            //resurface if they are legitimate, and not a result of a malformed if statement
NEW
2128
            this.diagnostics.splice(diagnosticsLengthBeforeBlock, this.diagnostics.length - diagnosticsLengthBeforeBlock);
×
2129

2130
            //this whole if statement is bogus...add error to the if token and hard-fail
NEW
2131
            this.diagnostics.push({
×
2132
                ...DiagnosticMessages.expectedTerminatorOnConditionalCompileBlock(),
2133
                location: hashIfToken.location
2134
            });
NEW
2135
            throw this.lastDiagnosticAsError();
×
2136
        }
2137
        return branch;
65✔
2138
    }
2139

2140
    /**
2141
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2142
     * Always looks for `#end if` or `#else`
2143
     */
2144
    private conditionalCompileBlock(): Block | undefined {
2145
        const parentAnnotations = this.enterAnnotationBlock();
66✔
2146

2147
        this.consumeStatementSeparators(true);
66✔
2148
        const unsafeTerminators = BlockTerminators;
66✔
2149
        const conditionalEndTokens = [TokenKind.HashElse, TokenKind.HashElseIf, TokenKind.HashEndIf];
66✔
2150
        const terminators = [...conditionalEndTokens, ...unsafeTerminators];
66✔
2151
        this.globalTerminators.push(conditionalEndTokens);
66✔
2152
        const statements: Statement[] = [];
66✔
2153
        while (!this.isAtEnd() && !this.checkAny(...terminators)) {
66✔
2154
            //grab the location of the current token
2155
            let loopCurrent = this.current;
69✔
2156
            let dec = this.declaration();
69✔
2157
            if (dec) {
69✔
2158
                if (!isAnnotationExpression(dec)) {
68!
2159
                    this.consumePendingAnnotations(dec);
68✔
2160
                    statements.push(dec);
68✔
2161
                }
2162

2163
                const peekKind = this.peek().kind;
68✔
2164
                if (conditionalEndTokens.includes(peekKind)) {
68✔
2165
                    // current conditional compile branch was closed by other statement, rewind to preceding newline
2166
                    this.current--;
1✔
2167
                }
2168
                //ensure statement separator
2169
                this.consumeStatementSeparators();
68✔
2170

2171
            } else {
2172
                //something went wrong. reset to the top of the loop
2173
                this.current = loopCurrent;
1✔
2174

2175
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2176
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
1✔
2177

2178
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2179
                //but there's already an error in the file being parsed, so just leave this line here
2180
                this.advance();
1✔
2181

2182
                //consume potential separators
2183
                this.consumeStatementSeparators(true);
1✔
2184
            }
2185
        }
2186
        this.globalTerminators.pop();
66✔
2187

2188

2189
        if (this.isAtEnd()) {
66!
NEW
2190
            return undefined;
×
2191
            // TODO: Figure out how to handle unterminated blocks well
2192
        } else {
2193
            //did we  hit an unsafe terminator?
2194
            //if so, we need to restore the statement separator
2195
            let prev = this.previous();
66✔
2196
            let prevKind = prev.kind;
66✔
2197
            let peek = this.peek();
66✔
2198
            let peekKind = this.peek().kind;
66✔
2199
            if (
66✔
2200
                (peekKind === TokenKind.HashEndIf || peekKind === TokenKind.HashElse || peekKind === TokenKind.HashElseIf) &&
173✔
2201
                (prevKind === TokenKind.Newline)
2202
            ) {
2203
                this.current--;
65✔
2204
            } else if (unsafeTerminators.includes(peekKind) &&
1!
2205
                (prevKind === TokenKind.Newline || prevKind === TokenKind.Colon)
2206
            ) {
2207
                this.diagnostics.push({
1✔
2208
                    ...DiagnosticMessages.unsafeUnmatchedTerminatorInConditionalCompileBlock(peek.text),
2209
                    location: peek.location
2210
                });
2211
                throw this.lastDiagnosticAsError();
1✔
2212
            } else {
NEW
2213
                return undefined;
×
2214
            }
2215
        }
2216
        this.exitAnnotationBlock(parentAnnotations);
65✔
2217
        return new Block({ statements: statements });
65✔
2218
    }
2219

2220
    private conditionalCompileConstStatement() {
2221
        const hashConstToken = this.advance();
21✔
2222

2223
        const constName = this.peek();
21✔
2224
        //disallow using keywords for const names
2225
        if (ReservedWords.has(constName?.text.toLowerCase())) {
21!
2226
            this.diagnostics.push({
1✔
2227
                ...DiagnosticMessages.constNameCannotBeReservedWord(),
2228
                location: constName?.location
3!
2229
            });
2230

2231
            this.lastDiagnosticAsError();
1✔
2232
            return;
1✔
2233
        }
2234
        const assignment = this.assignment();
20✔
2235
        if (assignment) {
18!
2236
            // check for something other than #const <name> = <otherName|true|false>
2237
            if (assignment.tokens.as || assignment.typeExpression) {
18!
NEW
2238
                this.diagnostics.push({
×
2239
                    ...DiagnosticMessages.unexpectedToken(assignment.tokens.as?.text || assignment.typeExpression?.getName(ParseMode.BrighterScript)),
×
2240
                    location: assignment.tokens.as?.location ?? assignment.typeExpression?.location
×
2241
                });
NEW
2242
                this.lastDiagnosticAsError();
×
2243
            }
2244

2245
            if (isVariableExpression(assignment.value) || isLiteralBoolean(assignment.value)) {
18✔
2246
                //value is an identifier or a boolean
2247
                //check for valid identifiers will happen in program validation
2248
            } else {
2249
                this.diagnostics.push({
2✔
2250
                    ...DiagnosticMessages.invalidHashConstValue(),
2251
                    location: assignment.value.location
2252
                });
2253
                this.lastDiagnosticAsError();
2✔
2254
            }
2255
        } else {
NEW
2256
            return undefined;
×
2257
        }
2258

2259
        if (!this.check(TokenKind.Newline)) {
18!
NEW
2260
            this.diagnostics.push({
×
2261
                ...DiagnosticMessages.expectedNewlineInConditionalCompile(),
2262
                location: this.peek().location
2263
            });
NEW
2264
            throw this.lastDiagnosticAsError();
×
2265
        }
2266

2267
        return new ConditionalCompileConstStatement({ hashConst: hashConstToken, assignment: assignment });
18✔
2268
    }
2269

2270
    private conditionalCompileErrorStatement() {
2271
        const hashErrorToken = this.advance();
10✔
2272
        const tokensUntilEndOfLine = this.consumeUntil(TokenKind.Newline);
10✔
2273
        const message = createToken(TokenKind.HashErrorMessage, tokensUntilEndOfLine.map(t => t.text).join(' '));
10✔
2274
        return new ConditionalCompileErrorStatement({ hashError: hashErrorToken, message: message });
10✔
2275
    }
2276

2277
    private ensureNewLine() {
2278
        //ensure newline before next keyword
2279
        if (!this.check(TokenKind.Newline)) {
80!
NEW
2280
            this.diagnostics.push({
×
2281
                ...DiagnosticMessages.expectedNewlineInConditionalCompile(),
2282
                location: this.peek().location
2283
            });
NEW
2284
            throw this.lastDiagnosticAsError();
×
2285
        }
2286
    }
2287

2288
    private ensureNewLineOrColon(silent = false) {
2,561✔
2289
        const prev = this.previous().kind;
2,736✔
2290
        if (prev !== TokenKind.Newline && prev !== TokenKind.Colon) {
2,736✔
2291
            if (!silent) {
123✔
2292
                this.diagnostics.push({
6✔
2293
                    ...DiagnosticMessages.expectedNewlineOrColon(),
2294
                    location: this.peek().location
2295
                });
2296
            }
2297
            return false;
123✔
2298
        }
2299
        return true;
2,613✔
2300
    }
2301

2302
    //ensure each statement of an inline block is single-line
2303
    private ensureInline(statements: Statement[]) {
2304
        for (const stat of statements) {
68✔
2305
            if (isIfStatement(stat) && !stat.isInline) {
86✔
2306
                this.diagnostics.push({
2✔
2307
                    ...DiagnosticMessages.expectedInlineIfStatement(),
2308
                    location: stat.location
2309
                });
2310
            }
2311
        }
2312
    }
2313

2314
    //consume inline branch of an `if` statement
2315
    private inlineConditionalBranch(...additionalTerminators: BlockTerminator[]): Block | undefined {
2316
        let statements = [];
86✔
2317
        //attempt to get the next statement without using `this.declaration`
2318
        //which seems a bit hackish to get to work properly
2319
        let statement = this.statement();
86✔
2320
        if (!statement) {
86!
2321
            return undefined;
×
2322
        }
2323
        statements.push(statement);
86✔
2324

2325
        //look for colon statement separator
2326
        let foundColon = false;
86✔
2327
        while (this.match(TokenKind.Colon)) {
86✔
2328
            foundColon = true;
23✔
2329
        }
2330

2331
        //if a colon was found, add the next statement or err if unexpected
2332
        if (foundColon) {
86✔
2333
            if (!this.checkAny(TokenKind.Newline, ...additionalTerminators)) {
23✔
2334
                //if not an ending keyword, add next statement
2335
                let extra = this.inlineConditionalBranch(...additionalTerminators);
18✔
2336
                if (!extra) {
18!
2337
                    return undefined;
×
2338
                }
2339
                statements.push(...extra.statements);
18✔
2340
            } else {
2341
                //error: colon before next keyword
2342
                const colon = this.previous();
5✔
2343
                this.diagnostics.push({
5✔
2344
                    ...DiagnosticMessages.unexpectedToken(colon.text),
2345
                    location: colon.location
2346
                });
2347
            }
2348
        }
2349
        return new Block({ statements: statements });
86✔
2350
    }
2351

2352
    private expressionStatement(expr: Expression): ExpressionStatement | IncrementStatement {
2353
        let expressionStart = this.peek();
774✔
2354

2355
        if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
774✔
2356
            let operator = this.advance();
25✔
2357

2358
            if (this.checkAny(TokenKind.PlusPlus, TokenKind.MinusMinus)) {
25✔
2359
                this.diagnostics.push({
1✔
2360
                    ...DiagnosticMessages.consecutiveIncrementDecrementOperatorsAreNotAllowed(),
2361
                    location: this.peek().location
2362
                });
2363
                throw this.lastDiagnosticAsError();
1✔
2364
            } else if (isCallExpression(expr)) {
24✔
2365
                this.diagnostics.push({
1✔
2366
                    ...DiagnosticMessages.incrementDecrementOperatorsAreNotAllowedAsResultOfFunctionCall(),
2367
                    location: expressionStart.location
2368
                });
2369
                throw this.lastDiagnosticAsError();
1✔
2370
            }
2371

2372
            const result = new IncrementStatement({ value: expr, operator: operator });
23✔
2373
            return result;
23✔
2374
        }
2375

2376
        if (isCallExpression(expr) || isCallfuncExpression(expr)) {
749✔
2377
            return new ExpressionStatement({ expression: expr });
447✔
2378
        }
2379

2380
        if (this.checkAny(...BinaryExpressionOperatorTokens)) {
302✔
2381
            expr = new BinaryExpression({ left: expr, operator: this.advance(), right: this.expression() });
6✔
2382
        }
2383

2384
        //at this point, it's probably an error. However, we recover a little more gracefully by creating an inclosing ExpressionStatement
2385
        this.diagnostics.push({
302✔
2386
            ...DiagnosticMessages.expectedStatementOrFunctionCallButReceivedExpression(),
2387
            location: expressionStart.location
2388
        });
2389
        return new ExpressionStatement({ expression: expr });
302✔
2390
    }
2391

2392
    private setStatement(): DottedSetStatement | IndexedSetStatement | ExpressionStatement | IncrementStatement | AssignmentStatement | AugmentedAssignmentStatement {
2393
        /**
2394
         * Attempts to find an expression-statement or an increment statement.
2395
         * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
2396
         * statements aren't valid expressions. They _do_ however fall under the same parsing
2397
         * priority as standalone function calls though, so we can parse them in the same way.
2398
         */
2399
        let expr = this.call();
1,165✔
2400
        if (this.check(TokenKind.Equal) && !(isCallExpression(expr))) {
1,118✔
2401
            let left = expr;
327✔
2402
            let operator = this.advance();
327✔
2403
            let right = this.expression();
327✔
2404

2405
            // Create a dotted or indexed "set" based on the left-hand side's type
2406
            if (isIndexedGetExpression(left)) {
327✔
2407
                return new IndexedSetStatement({
29✔
2408
                    obj: left.obj,
2409
                    indexes: left.indexes,
2410
                    value: right,
2411
                    openingSquare: left.tokens.openingSquare,
2412
                    closingSquare: left.tokens.closingSquare,
2413
                    equals: operator
2414
                });
2415
            } else if (isDottedGetExpression(left)) {
298✔
2416
                return new DottedSetStatement({
295✔
2417
                    obj: left.obj,
2418
                    name: left.tokens.name,
2419
                    value: right,
2420
                    dot: left.tokens.dot,
2421
                    equals: operator
2422
                });
2423
            }
2424
        } else if (this.checkAny(...CompoundAssignmentOperators) && !(isCallExpression(expr))) {
791✔
2425
            let left = expr;
20✔
2426
            let operator = this.advance();
20✔
2427
            let right = this.expression();
20✔
2428
            return new AugmentedAssignmentStatement({
20✔
2429
                item: left,
2430
                operator: operator,
2431
                value: right
2432
            });
2433
        }
2434
        return this.expressionStatement(expr);
774✔
2435
    }
2436

2437
    private printStatement(): PrintStatement {
2438
        let printKeyword = this.advance();
1,145✔
2439

2440
        let values: (
2441
            | Expression
2442
            | PrintSeparatorTab
2443
            | PrintSeparatorSpace)[] = [];
1,145✔
2444

2445
        while (!this.checkEndOfStatement()) {
1,145✔
2446
            if (this.check(TokenKind.Semicolon)) {
1,254✔
2447
                values.push(this.advance() as PrintSeparatorSpace);
29✔
2448
            } else if (this.check(TokenKind.Comma)) {
1,225✔
2449
                values.push(this.advance() as PrintSeparatorTab);
13✔
2450
            } else if (this.check(TokenKind.Else)) {
1,212✔
2451
                break; // inline branch
22✔
2452
            } else {
2453
                values.push(this.expression());
1,190✔
2454
            }
2455
        }
2456

2457
        //print statements can be empty, so look for empty print conditions
2458
        if (!values.length) {
1,142✔
2459
            const endOfStatementLocation = util.createBoundingLocation(printKeyword, this.peek());
12✔
2460
            let emptyStringLiteral = createStringLiteral('', endOfStatementLocation);
12✔
2461
            values.push(emptyStringLiteral);
12✔
2462
        }
2463

2464
        let last = values[values.length - 1];
1,142✔
2465
        if (isToken(last)) {
1,142✔
2466
            // TODO: error, expected value
2467
        }
2468

2469
        return new PrintStatement({ print: printKeyword, expressions: values });
1,142✔
2470
    }
2471

2472
    /**
2473
     * Parses a return statement with an optional return value.
2474
     * @returns an AST representation of a return statement.
2475
     */
2476
    private returnStatement(): ReturnStatement {
2477
        let options = { return: this.previous() };
3,039✔
2478

2479
        if (this.checkEndOfStatement()) {
3,039✔
2480
            return new ReturnStatement(options);
10✔
2481
        }
2482

2483
        let toReturn = this.check(TokenKind.Else) ? undefined : this.expression();
3,029✔
2484
        return new ReturnStatement({ ...options, value: toReturn });
3,028✔
2485
    }
2486

2487
    /**
2488
     * Parses a `label` statement
2489
     * @returns an AST representation of an `label` statement.
2490
     */
2491
    private labelStatement() {
2492
        let options = {
12✔
2493
            name: this.advance(),
2494
            colon: this.advance()
2495
        };
2496

2497
        //label must be alone on its line, this is probably not a label
2498
        if (!this.checkAny(TokenKind.Newline, TokenKind.Comment)) {
12✔
2499
            //rewind and cancel
2500
            this.current -= 2;
2✔
2501
            throw new CancelStatementError();
2✔
2502
        }
2503

2504
        return new LabelStatement(options);
10✔
2505
    }
2506

2507
    /**
2508
     * Parses a `continue` statement
2509
     */
2510
    private continueStatement() {
2511
        return new ContinueStatement({
12✔
2512
            continue: this.advance(),
2513
            loopType: this.tryConsume(
2514
                DiagnosticMessages.expectedToken(TokenKind.While, TokenKind.For),
2515
                TokenKind.While, TokenKind.For
2516
            )
2517
        });
2518
    }
2519

2520
    /**
2521
     * Parses a `goto` statement
2522
     * @returns an AST representation of an `goto` statement.
2523
     */
2524
    private gotoStatement() {
2525
        let tokens = {
12✔
2526
            goto: this.advance(),
2527
            label: this.consume(
2528
                DiagnosticMessages.expectedLabelIdentifierAfterGotoKeyword(),
2529
                TokenKind.Identifier
2530
            )
2531
        };
2532

2533
        return new GotoStatement(tokens);
10✔
2534
    }
2535

2536
    /**
2537
     * Parses an `end` statement
2538
     * @returns an AST representation of an `end` statement.
2539
     */
2540
    private endStatement() {
2541
        let options = { end: this.advance() };
8✔
2542

2543
        return new EndStatement(options);
8✔
2544
    }
2545
    /**
2546
     * Parses a `stop` statement
2547
     * @returns an AST representation of a `stop` statement
2548
     */
2549
    private stopStatement() {
2550
        let options = { stop: this.advance() };
16✔
2551

2552
        return new StopStatement(options);
16✔
2553
    }
2554

2555
    /**
2556
     * Parses a block, looking for a specific terminating TokenKind to denote completion.
2557
     * Always looks for `end sub`/`end function` to handle unterminated blocks.
2558
     * @param terminators the token(s) that signifies the end of this block; all other terminators are
2559
     *                    ignored.
2560
     */
2561
    private block(...terminators: BlockTerminator[]): Block | undefined {
2562
        const parentAnnotations = this.enterAnnotationBlock();
6,246✔
2563

2564
        this.consumeStatementSeparators(true);
6,246✔
2565
        const statements: Statement[] = [];
6,246✔
2566
        const flatGlobalTerminators = this.globalTerminators.flat().flat();
6,246✔
2567
        while (!this.isAtEnd() && !this.checkAny(TokenKind.EndSub, TokenKind.EndFunction, ...terminators, ...flatGlobalTerminators)) {
6,246✔
2568
            //grab the location of the current token
2569
            let loopCurrent = this.current;
7,335✔
2570
            let dec = this.declaration();
7,335✔
2571
            if (dec) {
7,335✔
2572
                if (!isAnnotationExpression(dec)) {
7,285✔
2573
                    this.consumePendingAnnotations(dec);
7,278✔
2574
                    statements.push(dec);
7,278✔
2575
                }
2576

2577
                //ensure statement separator
2578
                this.consumeStatementSeparators();
7,285✔
2579

2580
            } else {
2581
                //something went wrong. reset to the top of the loop
2582
                this.current = loopCurrent;
50✔
2583

2584
                //scrap the entire line (hopefully whatever failed has added a diagnostic)
2585
                this.consumeUntil(TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
50✔
2586

2587
                //trash the next token. this prevents an infinite loop. not exactly sure why we need this,
2588
                //but there's already an error in the file being parsed, so just leave this line here
2589
                this.advance();
50✔
2590

2591
                //consume potential separators
2592
                this.consumeStatementSeparators(true);
50✔
2593
            }
2594
        }
2595

2596
        if (this.isAtEnd()) {
6,246✔
2597
            return undefined;
6✔
2598
            // TODO: Figure out how to handle unterminated blocks well
2599
        } else if (terminators.length > 0) {
6,240✔
2600
            //did we hit end-sub / end-function while looking for some other terminator?
2601
            //if so, we need to restore the statement separator
2602
            let prev = this.previous().kind;
2,734✔
2603
            let peek = this.peek().kind;
2,734✔
2604
            if (
2,734✔
2605
                (peek === TokenKind.EndSub || peek === TokenKind.EndFunction) &&
5,472!
2606
                (prev === TokenKind.Newline || prev === TokenKind.Colon)
2607
            ) {
2608
                this.current--;
6✔
2609
            }
2610
        }
2611

2612
        this.exitAnnotationBlock(parentAnnotations);
6,240✔
2613
        return new Block({ statements: statements });
6,240✔
2614
    }
2615

2616
    /**
2617
     * Attach pending annotations to the provided statement,
2618
     * and then reset the annotations array
2619
     */
2620
    consumePendingAnnotations(statement: Statement) {
2621
        if (this.pendingAnnotations.length) {
14,352✔
2622
            statement.annotations = this.pendingAnnotations;
51✔
2623
            this.pendingAnnotations = [];
51✔
2624
        }
2625
    }
2626

2627
    enterAnnotationBlock() {
2628
        const pending = this.pendingAnnotations;
11,343✔
2629
        this.pendingAnnotations = [];
11,343✔
2630
        return pending;
11,343✔
2631
    }
2632

2633
    exitAnnotationBlock(parentAnnotations: AnnotationExpression[]) {
2634
        // non consumed annotations are an error
2635
        if (this.pendingAnnotations.length) {
11,335✔
2636
            for (const annotation of this.pendingAnnotations) {
5✔
2637
                this.diagnostics.push({
7✔
2638
                    ...DiagnosticMessages.unusedAnnotation(),
2639
                    location: annotation.location
2640
                });
2641
            }
2642
        }
2643
        this.pendingAnnotations = parentAnnotations;
11,335✔
2644
    }
2645

2646
    private expression(findTypecast = true): Expression {
11,350✔
2647
        let expression = this.anonymousFunction();
11,732✔
2648
        let asToken: Token;
2649
        let typeExpression: TypeExpression;
2650
        if (findTypecast) {
11,693✔
2651
            do {
11,338✔
2652
                if (this.check(TokenKind.As)) {
11,404✔
2653
                    this.warnIfNotBrighterScriptMode('type cast');
67✔
2654
                    // Check if this expression is wrapped in any type casts
2655
                    // allows for multiple casts:
2656
                    // myVal = foo() as dynamic as string
2657
                    [asToken, typeExpression] = this.consumeAsTokenAndTypeExpression();
67✔
2658
                    if (asToken && typeExpression) {
67✔
2659
                        expression = new TypecastExpression({ obj: expression, as: asToken, typeExpression: typeExpression });
66✔
2660
                    }
2661
                } else {
2662
                    break;
11,337✔
2663
                }
2664

2665
            } while (asToken && typeExpression);
134✔
2666
        }
2667
        return expression;
11,693✔
2668
    }
2669

2670
    private anonymousFunction(): Expression {
2671
        if (this.checkAny(TokenKind.Sub, TokenKind.Function)) {
11,732✔
2672
            const func = this.functionDeclaration(true);
83✔
2673
            //if there's an open paren after this, this is an IIFE
2674
            if (this.check(TokenKind.LeftParen)) {
83✔
2675
                return this.finishCall(this.advance(), func);
3✔
2676
            } else {
2677
                return func;
80✔
2678
            }
2679
        }
2680

2681
        let expr = this.boolean();
11,649✔
2682

2683
        if (this.check(TokenKind.Question)) {
11,610✔
2684
            return this.ternaryExpression(expr);
78✔
2685
        } else if (this.check(TokenKind.QuestionQuestion)) {
11,532✔
2686
            return this.nullCoalescingExpression(expr);
34✔
2687
        } else {
2688
            return expr;
11,498✔
2689
        }
2690
    }
2691

2692
    private boolean(): Expression {
2693
        let expr = this.relational();
11,649✔
2694

2695
        while (this.matchAny(TokenKind.And, TokenKind.Or)) {
11,610✔
2696
            let operator = this.previous();
29✔
2697
            let right = this.relational();
29✔
2698
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
29✔
2699
        }
2700

2701
        return expr;
11,610✔
2702
    }
2703

2704
    private relational(): Expression {
2705
        let expr = this.additive();
11,705✔
2706

2707
        while (
11,666✔
2708
            this.matchAny(
2709
                TokenKind.Equal,
2710
                TokenKind.LessGreater,
2711
                TokenKind.Greater,
2712
                TokenKind.GreaterEqual,
2713
                TokenKind.Less,
2714
                TokenKind.LessEqual
2715
            )
2716
        ) {
2717
            let operator = this.previous();
1,642✔
2718
            let right = this.additive();
1,642✔
2719
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,642✔
2720
        }
2721

2722
        return expr;
11,666✔
2723
    }
2724

2725
    // TODO: bitshift
2726

2727
    private additive(): Expression {
2728
        let expr = this.multiplicative();
13,347✔
2729

2730
        while (this.matchAny(TokenKind.Plus, TokenKind.Minus)) {
13,308✔
2731
            let operator = this.previous();
1,322✔
2732
            let right = this.multiplicative();
1,322✔
2733
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
1,322✔
2734
        }
2735

2736
        return expr;
13,308✔
2737
    }
2738

2739
    private multiplicative(): Expression {
2740
        let expr = this.exponential();
14,669✔
2741

2742
        while (this.matchAny(
14,630✔
2743
            TokenKind.Forwardslash,
2744
            TokenKind.Backslash,
2745
            TokenKind.Star,
2746
            TokenKind.Mod,
2747
            TokenKind.LeftShift,
2748
            TokenKind.RightShift
2749
        )) {
2750
            let operator = this.previous();
53✔
2751
            let right = this.exponential();
53✔
2752
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
53✔
2753
        }
2754

2755
        return expr;
14,630✔
2756
    }
2757

2758
    private exponential(): Expression {
2759
        let expr = this.prefixUnary();
14,722✔
2760

2761
        while (this.match(TokenKind.Caret)) {
14,683✔
2762
            let operator = this.previous();
8✔
2763
            let right = this.prefixUnary();
8✔
2764
            expr = new BinaryExpression({ left: expr, operator: operator, right: right });
8✔
2765
        }
2766

2767
        return expr;
14,683✔
2768
    }
2769

2770
    private prefixUnary(): Expression {
2771
        const nextKind = this.peek().kind;
14,766✔
2772
        if (nextKind === TokenKind.Not) {
14,766✔
2773
            this.current++; //advance
27✔
2774
            let operator = this.previous();
27✔
2775
            let right = this.relational();
27✔
2776
            return new UnaryExpression({ operator: operator, right: right });
27✔
2777
        } else if (nextKind === TokenKind.Minus || nextKind === TokenKind.Plus) {
14,739✔
2778
            this.current++; //advance
36✔
2779
            let operator = this.previous();
36✔
2780
            let right = (nextKind as any) === TokenKind.Not
36✔
2781
                ? this.boolean()
36!
2782
                : this.prefixUnary();
2783
            return new UnaryExpression({ operator: operator, right: right });
36✔
2784
        }
2785
        return this.call();
14,703✔
2786
    }
2787

2788
    private indexedGet(expr: Expression) {
2789
        let openingSquare = this.previous();
153✔
2790
        let questionDotToken = this.getMatchingTokenAtOffset(-2, TokenKind.QuestionDot);
153✔
2791
        let indexes: Expression[] = [];
153✔
2792

2793

2794
        //consume leading newlines
2795
        while (this.match(TokenKind.Newline)) { }
153✔
2796

2797
        try {
153✔
2798
            indexes.push(
153✔
2799
                this.expression()
2800
            );
2801
            //consume additional indexes separated by commas
2802
            while (this.check(TokenKind.Comma)) {
151✔
2803
                //discard the comma
2804
                this.advance();
17✔
2805
                indexes.push(
17✔
2806
                    this.expression()
2807
                );
2808
            }
2809
        } catch (error) {
2810
            this.rethrowNonDiagnosticError(error);
2✔
2811
        }
2812
        //consume trailing newlines
2813
        while (this.match(TokenKind.Newline)) { }
153✔
2814

2815
        const closingSquare = this.tryConsume(
153✔
2816
            DiagnosticMessages.expectedRightSquareBraceAfterArrayOrObjectIndex(),
2817
            TokenKind.RightSquareBracket
2818
        );
2819

2820
        return new IndexedGetExpression({
153✔
2821
            obj: expr,
2822
            indexes: indexes,
2823
            openingSquare: openingSquare,
2824
            closingSquare: closingSquare,
2825
            questionDot: questionDotToken
2826
        });
2827
    }
2828

2829
    private newExpression() {
2830
        this.warnIfNotBrighterScriptMode(`using 'new' keyword to construct a class`);
133✔
2831
        let newToken = this.advance();
133✔
2832

2833
        let nameExpr = this.identifyingExpression();
133✔
2834
        let leftParen = this.tryConsume(
133✔
2835
            DiagnosticMessages.unexpectedToken(this.peek().text),
2836
            TokenKind.LeftParen,
2837
            TokenKind.QuestionLeftParen
2838
        );
2839

2840
        if (!leftParen) {
133✔
2841
            // new expression without a following call expression
2842
            // wrap the name in an expression
2843
            const endOfStatementLocation = util.createBoundingLocation(newToken, this.peek());
4✔
2844
            const exprStmt = nameExpr ?? createStringLiteral('', endOfStatementLocation);
4!
2845
            return new ExpressionStatement({ expression: exprStmt });
4✔
2846
        }
2847

2848
        let call = this.finishCall(leftParen, nameExpr);
129✔
2849
        //pop the call from the  callExpressions list because this is technically something else
2850
        this.callExpressions.pop();
129✔
2851
        let result = new NewExpression({ new: newToken, call: call });
129✔
2852
        return result;
129✔
2853
    }
2854

2855
    /**
2856
     * A callfunc expression (i.e. `node@.someFunctionOnNode()`)
2857
     */
2858
    private callfunc(callee: Expression): Expression {
2859
        this.warnIfNotBrighterScriptMode('callfunc operator');
31✔
2860
        let operator = this.previous();
31✔
2861
        let methodName = this.consume(DiagnosticMessages.expectedIdentifier(), TokenKind.Identifier, ...AllowedProperties);
31✔
2862
        // force it into an identifier so the AST makes some sense
2863
        methodName.kind = TokenKind.Identifier;
28✔
2864
        let openParen = this.consume(DiagnosticMessages.expectedOpenParenToFollowCallfuncIdentifier(), TokenKind.LeftParen);
28✔
2865
        let call = this.finishCall(openParen, callee, false);
28✔
2866

2867
        return new CallfuncExpression({
28✔
2868
            callee: callee,
2869
            operator: operator,
2870
            methodName: methodName as Identifier,
2871
            openingParen: openParen,
2872
            args: call.args,
2873
            closingParen: call.tokens.closingParen
2874
        });
2875
    }
2876

2877
    private call(): Expression {
2878
        if (this.check(TokenKind.New) && this.checkAnyNext(TokenKind.Identifier, ...this.allowedLocalIdentifiers)) {
15,868✔
2879
            return this.newExpression();
133✔
2880
        }
2881
        let expr = this.primary();
15,735✔
2882

2883
        while (true) {
15,652✔
2884
            if (this.matchAny(TokenKind.LeftParen, TokenKind.QuestionLeftParen)) {
20,193✔
2885
                expr = this.finishCall(this.previous(), expr);
2,094✔
2886
            } else if (this.matchAny(TokenKind.LeftSquareBracket, TokenKind.QuestionLeftSquare) || this.matchSequence(TokenKind.QuestionDot, TokenKind.LeftSquareBracket)) {
18,099✔
2887
                expr = this.indexedGet(expr);
151✔
2888
            } else if (this.match(TokenKind.Callfunc)) {
17,948✔
2889
                expr = this.callfunc(expr);
31✔
2890
            } else if (this.matchAny(TokenKind.Dot, TokenKind.QuestionDot)) {
17,917✔
2891
                if (this.match(TokenKind.LeftSquareBracket)) {
2,307✔
2892
                    expr = this.indexedGet(expr);
2✔
2893
                } else {
2894
                    let dot = this.previous();
2,305✔
2895
                    let name = this.tryConsume(
2,305✔
2896
                        DiagnosticMessages.expectedPropertyNameAfterPeriod(),
2897
                        TokenKind.Identifier,
2898
                        ...AllowedProperties
2899
                    );
2900
                    if (!name) {
2,305✔
2901
                        break;
39✔
2902
                    }
2903

2904
                    // force it into an identifier so the AST makes some sense
2905
                    name.kind = TokenKind.Identifier;
2,266✔
2906
                    expr = new DottedGetExpression({ obj: expr, name: name as Identifier, dot: dot });
2,266✔
2907
                }
2908

2909
            } else if (this.checkAny(TokenKind.At, TokenKind.QuestionAt)) {
15,610✔
2910
                let dot = this.advance();
11✔
2911
                let name = this.tryConsume(
11✔
2912
                    DiagnosticMessages.expectedAttributeNameAfterAtSymbol(),
2913
                    TokenKind.Identifier,
2914
                    ...AllowedProperties
2915
                );
2916

2917
                // force it into an identifier so the AST makes some sense
2918
                name.kind = TokenKind.Identifier;
11✔
2919
                if (!name) {
11!
2920
                    break;
×
2921
                }
2922
                expr = new XmlAttributeGetExpression({ obj: expr, name: name as Identifier, at: dot });
11✔
2923
                //only allow a single `@` expression
2924
                break;
11✔
2925

2926
            } else {
2927
                break;
15,599✔
2928
            }
2929
        }
2930

2931
        return expr;
15,649✔
2932
    }
2933

2934
    private finishCall(openingParen: Token, callee: Expression, addToCallExpressionList = true) {
2,226✔
2935
        let args = [] as Expression[];
2,284✔
2936
        while (this.match(TokenKind.Newline)) { }
2,284✔
2937

2938
        if (!this.check(TokenKind.RightParen)) {
2,284✔
2939
            do {
1,159✔
2940
                while (this.match(TokenKind.Newline)) { }
1,660✔
2941

2942
                if (args.length >= CallExpression.MaximumArguments) {
1,660!
2943
                    this.diagnostics.push({
×
2944
                        ...DiagnosticMessages.tooManyCallableArguments(args.length, CallExpression.MaximumArguments),
2945
                        location: this.peek()?.location
×
2946
                    });
2947
                    throw this.lastDiagnosticAsError();
×
2948
                }
2949
                try {
1,660✔
2950
                    args.push(this.expression());
1,660✔
2951
                } catch (error) {
2952
                    this.rethrowNonDiagnosticError(error);
5✔
2953
                    // we were unable to get an expression, so don't continue
2954
                    break;
5✔
2955
                }
2956
            } while (this.match(TokenKind.Comma));
2957
        }
2958

2959
        while (this.match(TokenKind.Newline)) { }
2,284✔
2960

2961
        const closingParen = this.tryConsume(
2,284✔
2962
            DiagnosticMessages.expectedRightParenAfterFunctionCallArguments(),
2963
            TokenKind.RightParen
2964
        );
2965

2966
        let expression = new CallExpression({
2,284✔
2967
            callee: callee,
2968
            openingParen: openingParen,
2969
            args: args,
2970
            closingParen: closingParen
2971
        });
2972
        if (addToCallExpressionList) {
2,284✔
2973
            this.callExpressions.push(expression);
2,226✔
2974
        }
2975
        return expression;
2,284✔
2976
    }
2977

2978
    /**
2979
     * Creates a TypeExpression, which wraps standard ASTNodes that represent a BscType
2980
     */
2981
    private typeExpression(): TypeExpression {
2982
        const changedTokens: { token: Token; oldKind: TokenKind }[] = [];
1,443✔
2983
        try {
1,443✔
2984
            let expr: Expression = this.getTypeExpressionPart(changedTokens);
1,443✔
2985
            while (this.options.mode === ParseMode.BrighterScript && this.matchAny(TokenKind.Or)) {
1,443✔
2986
                // If we're in Brighterscript mode, allow union types with "or" between types
2987
                // TODO: Handle Union types in parens? eg. "(string or integer)"
2988
                let operator = this.previous();
32✔
2989
                let right = this.getTypeExpressionPart(changedTokens);
32✔
2990
                if (right) {
32!
2991
                    expr = new BinaryExpression({ left: expr, operator: operator, right: right });
32✔
2992
                } else {
NEW
2993
                    break;
×
2994
                }
2995
            }
2996
            if (expr) {
1,443!
2997
                return new TypeExpression({ expression: expr });
1,443✔
2998
            }
2999

3000
        } catch (error) {
3001
            // Something went wrong - reset the kind to what it was previously
NEW
3002
            for (const changedToken of changedTokens) {
×
NEW
3003
                changedToken.token.kind = changedToken.oldKind;
×
3004
            }
NEW
3005
            throw error;
×
3006
        }
3007
    }
3008

3009
    /**
3010
     * Gets a single "part" of a type of a potential Union type
3011
     * Note: this does not NEED to be part of a union type, but the logic is the same
3012
     *
3013
     * @param changedTokens an array that is modified with any tokens that have been changed from their default kind to identifiers - eg. when a keyword is used as type
3014
     * @returns an expression that was successfully parsed
3015
     */
3016
    private getTypeExpressionPart(changedTokens: { token: Token; oldKind: TokenKind }[]) {
3017
        let expr: VariableExpression | DottedGetExpression | TypedArrayExpression;
3018
        if (this.checkAny(...DeclarableTypes)) {
1,475✔
3019
            // if this is just a type, just use directly
3020
            expr = new VariableExpression({ name: this.advance() as Identifier });
990✔
3021
        } else {
3022
            if (this.checkAny(...AllowedTypeIdentifiers)) {
485✔
3023
                // Since the next token is allowed as a type identifier, change the kind
3024
                let nextToken = this.peek();
1✔
3025
                changedTokens.push({ token: nextToken, oldKind: nextToken.kind });
1✔
3026
                nextToken.kind = TokenKind.Identifier;
1✔
3027
            }
3028
            expr = this.identifyingExpression(AllowedTypeIdentifiers);
485✔
3029
        }
3030

3031
        //Check if it has square brackets, thus making it an array
3032
        if (expr && this.check(TokenKind.LeftSquareBracket)) {
1,475✔
3033
            if (this.options.mode === ParseMode.BrightScript) {
28✔
3034
                // typed arrays not allowed in Brightscript
3035
                this.warnIfNotBrighterScriptMode('typed arrays');
1✔
3036
                return expr;
1✔
3037
            }
3038

3039
            // Check if it is an array - that is, if it has `[]` after the type
3040
            // eg. `string[]` or `SomeKlass[]`
3041
            // This is while loop, so it supports multidimensional arrays (eg. integer[][])
3042
            while (this.check(TokenKind.LeftSquareBracket)) {
27✔
3043
                const leftBracket = this.advance();
29✔
3044
                if (this.check(TokenKind.RightSquareBracket)) {
29!
3045
                    const rightBracket = this.advance();
29✔
3046
                    expr = new TypedArrayExpression({ innerType: expr, leftBracket: leftBracket, rightBracket: rightBracket });
29✔
3047
                }
3048
            }
3049
        }
3050

3051
        return expr;
1,474✔
3052
    }
3053

3054
    private primary(): Expression {
3055
        switch (true) {
15,735✔
3056
            case this.matchAny(
15,735!
3057
                TokenKind.False,
3058
                TokenKind.True,
3059
                TokenKind.Invalid,
3060
                TokenKind.IntegerLiteral,
3061
                TokenKind.LongIntegerLiteral,
3062
                TokenKind.FloatLiteral,
3063
                TokenKind.DoubleLiteral,
3064
                TokenKind.StringLiteral
3065
            ):
3066
                return new LiteralExpression({ value: this.previous() });
7,036✔
3067

3068
            //capture source literals (LINE_NUM if brightscript, or a bunch of them if brighterscript)
3069
            case this.matchAny(TokenKind.LineNumLiteral, ...(this.options.mode === ParseMode.BrightScript ? [] : BrighterScriptSourceLiterals)):
8,699✔
3070
                return new SourceLiteralExpression({ value: this.previous() });
35✔
3071

3072
            //template string
3073
            case this.check(TokenKind.BackTick):
3074
                return this.templateString(false);
43✔
3075

3076
            //tagged template string (currently we do not support spaces between the identifier and the backtick)
3077
            case this.checkAny(TokenKind.Identifier, ...AllowedLocalIdentifiers) && this.checkNext(TokenKind.BackTick):
16,664✔
3078
                return this.templateString(true);
8✔
3079

3080
            case this.matchAny(TokenKind.Identifier, ...this.allowedLocalIdentifiers):
3081
                return new VariableExpression({ name: this.previous() as Identifier });
8,042✔
3082

3083
            case this.match(TokenKind.LeftParen):
3084
                let left = this.previous();
48✔
3085
                let expr = this.expression();
48✔
3086
                let right = this.consume(
47✔
3087
                    DiagnosticMessages.unmatchedLeftParenAfterExpression(),
3088
                    TokenKind.RightParen
3089
                );
3090
                return new GroupingExpression({ leftParen: left, rightParen: right, expression: expr });
47✔
3091

3092
            case this.matchAny(TokenKind.LeftSquareBracket):
3093
                return this.arrayLiteral();
138✔
3094

3095
            case this.match(TokenKind.LeftCurlyBrace):
3096
                return this.aaLiteral();
260✔
3097

3098
            case this.matchAny(TokenKind.Pos, TokenKind.Tab):
3099
                let token = Object.assign(this.previous(), {
×
3100
                    kind: TokenKind.Identifier
3101
                }) as Identifier;
NEW
3102
                return new VariableExpression({ name: token });
×
3103

3104
            case this.checkAny(TokenKind.Function, TokenKind.Sub):
3105
                return this.anonymousFunction();
×
3106

3107
            case this.check(TokenKind.RegexLiteral):
3108
                return this.regexLiteralExpression();
45✔
3109

3110
            default:
3111
                //if we found an expected terminator, don't throw a diagnostic...just return undefined
3112
                if (this.checkAny(...this.peekGlobalTerminators())) {
80!
3113
                    //don't throw a diagnostic, just return undefined
3114

3115
                    //something went wrong...throw an error so the upstream processor can scrap this line and move on
3116
                } else {
3117
                    this.diagnostics.push({
80✔
3118
                        ...DiagnosticMessages.unexpectedToken(this.peek().text),
3119
                        location: this.peek()?.location
240!
3120
                    });
3121
                    throw this.lastDiagnosticAsError();
80✔
3122
                }
3123
        }
3124
    }
3125

3126
    private arrayLiteral() {
3127
        let elements: Array<Expression> = [];
138✔
3128
        let openingSquare = this.previous();
138✔
3129

3130
        while (this.match(TokenKind.Newline)) {
138✔
3131
        }
3132
        let closingSquare: Token;
3133

3134
        if (!this.match(TokenKind.RightSquareBracket)) {
138✔
3135
            try {
101✔
3136
                elements.push(this.expression());
101✔
3137

3138
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Comment)) {
100✔
3139

3140
                    while (this.match(TokenKind.Newline)) {
136✔
3141

3142
                    }
3143

3144
                    if (this.check(TokenKind.RightSquareBracket)) {
136✔
3145
                        break;
23✔
3146
                    }
3147

3148
                    elements.push(this.expression());
113✔
3149
                }
3150
            } catch (error: any) {
3151
                this.rethrowNonDiagnosticError(error);
2✔
3152
            }
3153

3154
            closingSquare = this.tryConsume(
101✔
3155
                DiagnosticMessages.unmatchedLeftSquareBraceAfterArrayLiteral(),
3156
                TokenKind.RightSquareBracket
3157
            );
3158
        } else {
3159
            closingSquare = this.previous();
37✔
3160
        }
3161

3162
        //this.consume("Expected newline or ':' after array literal", TokenKind.Newline, TokenKind.Colon, TokenKind.Eof);
3163
        return new ArrayLiteralExpression({ elements: elements, open: openingSquare, close: closingSquare });
138✔
3164
    }
3165

3166
    private aaLiteral() {
3167
        let openingBrace = this.previous();
260✔
3168
        let members: Array<AAMemberExpression> = [];
260✔
3169

3170
        let key = () => {
260✔
3171
            let result = {
273✔
3172
                colonToken: null as Token,
3173
                keyToken: null as Token,
3174
                range: null as Range
3175
            };
3176
            if (this.checkAny(TokenKind.Identifier, ...AllowedProperties)) {
273✔
3177
                result.keyToken = this.identifier(...AllowedProperties);
242✔
3178
            } else if (this.check(TokenKind.StringLiteral)) {
31!
3179
                result.keyToken = this.advance();
31✔
3180
            } else {
3181
                this.diagnostics.push({
×
3182
                    ...DiagnosticMessages.unexpectedAAKey(),
3183
                    location: this.peek().location
3184
                });
3185
                throw this.lastDiagnosticAsError();
×
3186
            }
3187

3188
            result.colonToken = this.consume(
273✔
3189
                DiagnosticMessages.expectedColonBetweenAAKeyAndvalue(),
3190
                TokenKind.Colon
3191
            );
3192
            result.range = util.createBoundingRange(result.keyToken, result.colonToken);
272✔
3193
            return result;
272✔
3194
        };
3195

3196
        while (this.match(TokenKind.Newline)) { }
260✔
3197
        let closingBrace: Token;
3198
        if (!this.match(TokenKind.RightCurlyBrace)) {
260✔
3199
            let lastAAMember: AAMemberExpression;
3200
            try {
188✔
3201
                let k = key();
188✔
3202
                let expr = this.expression();
188✔
3203
                lastAAMember = new AAMemberExpression({
187✔
3204
                    key: k.keyToken,
3205
                    colon: k.colonToken,
3206
                    value: expr
3207
                });
3208
                members.push(lastAAMember);
187✔
3209

3210
                while (this.matchAny(TokenKind.Comma, TokenKind.Newline, TokenKind.Colon, TokenKind.Comment)) {
187✔
3211
                    // collect comma at end of expression
3212
                    if (lastAAMember && this.checkPrevious(TokenKind.Comma)) {
209✔
3213
                        (lastAAMember as DeepWriteable<AAMemberExpression>).tokens.comma = this.previous();
60✔
3214
                    }
3215

3216
                    this.consumeStatementSeparators(true);
209✔
3217

3218
                    if (this.check(TokenKind.RightCurlyBrace)) {
209✔
3219
                        break;
124✔
3220
                    }
3221
                    let k = key();
85✔
3222
                    let expr = this.expression();
84✔
3223
                    lastAAMember = new AAMemberExpression({
84✔
3224
                        key: k.keyToken,
3225
                        colon: k.colonToken,
3226
                        value: expr
3227
                    });
3228
                    members.push(lastAAMember);
84✔
3229

3230
                }
3231
            } catch (error: any) {
3232
                this.rethrowNonDiagnosticError(error);
2✔
3233
            }
3234

3235
            closingBrace = this.tryConsume(
188✔
3236
                DiagnosticMessages.unmatchedLeftCurlyAfterAALiteral(),
3237
                TokenKind.RightCurlyBrace
3238
            );
3239
        } else {
3240
            closingBrace = this.previous();
72✔
3241
        }
3242

3243
        const aaExpr = new AALiteralExpression({ elements: members, open: openingBrace, close: closingBrace });
260✔
3244
        return aaExpr;
260✔
3245
    }
3246

3247
    /**
3248
     * Pop token if we encounter specified token
3249
     */
3250
    private match(tokenKind: TokenKind) {
3251
        if (this.check(tokenKind)) {
58,908✔
3252
            this.current++; //advance
5,884✔
3253
            return true;
5,884✔
3254
        }
3255
        return false;
53,024✔
3256
    }
3257

3258
    /**
3259
     * Pop token if we encounter a token in the specified list
3260
     * @param tokenKinds a list of tokenKinds where any tokenKind in this list will result in a match
3261
     */
3262
    private matchAny(...tokenKinds: TokenKind[]) {
3263
        for (let tokenKind of tokenKinds) {
206,687✔
3264
            if (this.check(tokenKind)) {
578,039✔
3265
                this.current++; //advance
54,807✔
3266
                return true;
54,807✔
3267
            }
3268
        }
3269
        return false;
151,880✔
3270
    }
3271

3272
    /**
3273
     * If the next series of tokens matches the given set of tokens, pop them all
3274
     * @param tokenKinds a list of tokenKinds used to match the next set of tokens
3275
     */
3276
    private matchSequence(...tokenKinds: TokenKind[]) {
3277
        const endIndex = this.current + tokenKinds.length;
17,951✔
3278
        for (let i = 0; i < tokenKinds.length; i++) {
17,951✔
3279
            if (tokenKinds[i] !== this.tokens[this.current + i]?.kind) {
17,975!
3280
                return false;
17,948✔
3281
            }
3282
        }
3283
        this.current = endIndex;
3✔
3284
        return true;
3✔
3285
    }
3286

3287
    /**
3288
     * Get next token matching a specified list, or fail with an error
3289
     */
3290
    private consume(diagnosticInfo: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token {
3291
        let token = this.tryConsume(diagnosticInfo, ...tokenKinds);
14,416✔
3292
        if (token) {
14,416✔
3293
            return token;
14,402✔
3294
        } else {
3295
            let error = new Error(diagnosticInfo.message);
14✔
3296
            (error as any).isDiagnostic = true;
14✔
3297
            throw error;
14✔
3298
        }
3299
    }
3300

3301
    /**
3302
     * Consume next token IF it matches the specified kind. Otherwise, do nothing and return undefined
3303
     */
3304
    private consumeTokenIf(tokenKind: TokenKind) {
3305
        if (this.match(tokenKind)) {
3,412✔
3306
            return this.previous();
394✔
3307
        }
3308
    }
3309

3310
    private consumeToken(tokenKind: TokenKind) {
3311
        return this.consume(
1,860✔
3312
            DiagnosticMessages.expectedToken(tokenKind),
3313
            tokenKind
3314
        );
3315
    }
3316

3317
    /**
3318
     * Consume, or add a message if not found. But then continue and return undefined
3319
     */
3320
    private tryConsume(diagnostic: DiagnosticInfo, ...tokenKinds: TokenKind[]): Token | undefined {
3321
        const nextKind = this.peek().kind;
21,928✔
3322
        let foundTokenKind = tokenKinds.some(tokenKind => nextKind === tokenKind);
47,026✔
3323

3324
        if (foundTokenKind) {
21,928✔
3325
            return this.advance();
21,821✔
3326
        }
3327
        this.diagnostics.push({
107✔
3328
            ...diagnostic,
3329
            location: this.peek()?.location
321!
3330
        });
3331
    }
3332

3333
    private tryConsumeToken(tokenKind: TokenKind) {
3334
        return this.tryConsume(
78✔
3335
            DiagnosticMessages.expectedToken(tokenKind),
3336
            tokenKind
3337
        );
3338
    }
3339

3340
    private consumeStatementSeparators(optional = false) {
9,759✔
3341
        //a comment or EOF mark the end of the statement
3342
        if (this.isAtEnd() || this.check(TokenKind.Comment)) {
29,741✔
3343
            return true;
603✔
3344
        }
3345
        let consumed = false;
29,138✔
3346
        //consume any newlines and colons
3347
        while (this.matchAny(TokenKind.Newline, TokenKind.Colon)) {
29,138✔
3348
            consumed = true;
31,584✔
3349
        }
3350
        if (!optional && !consumed) {
29,138✔
3351
            this.diagnostics.push({
68✔
3352
                ...DiagnosticMessages.expectedNewlineOrColon(),
3353
                location: this.peek()?.location
204!
3354
            });
3355
        }
3356
        return consumed;
29,138✔
3357
    }
3358

3359
    private advance(): Token {
3360
        if (!this.isAtEnd()) {
50,353✔
3361
            this.current++;
50,339✔
3362
        }
3363
        return this.previous();
50,353✔
3364
    }
3365

3366
    private checkEndOfStatement(): boolean {
3367
        const nextKind = this.peek().kind;
6,865✔
3368
        return [TokenKind.Colon, TokenKind.Newline, TokenKind.Comment, TokenKind.Eof].includes(nextKind);
6,865✔
3369
    }
3370

3371
    private checkPrevious(tokenKind: TokenKind): boolean {
3372
        return this.previous()?.kind === tokenKind;
222!
3373
    }
3374

3375
    /**
3376
     * Check that the next token kind is the expected kind
3377
     * @param tokenKind the expected next kind
3378
     * @returns true if the next tokenKind is the expected value
3379
     */
3380
    private check(tokenKind: TokenKind): boolean {
3381
        const nextKind = this.peek().kind;
935,198✔
3382
        if (nextKind === TokenKind.Eof) {
935,198✔
3383
            return false;
11,743✔
3384
        }
3385
        return nextKind === tokenKind;
923,455✔
3386
    }
3387

3388
    private checkAny(...tokenKinds: TokenKind[]): boolean {
3389
        const nextKind = this.peek().kind;
158,302✔
3390
        if (nextKind === TokenKind.Eof) {
158,302✔
3391
            return false;
1,092✔
3392
        }
3393
        return tokenKinds.includes(nextKind);
157,210✔
3394
    }
3395

3396
    private checkNext(tokenKind: TokenKind): boolean {
3397
        if (this.isAtEnd()) {
13,543!
3398
            return false;
×
3399
        }
3400
        return this.peekNext().kind === tokenKind;
13,543✔
3401
    }
3402

3403
    private checkAnyNext(...tokenKinds: TokenKind[]): boolean {
3404
        if (this.isAtEnd()) {
6,050!
3405
            return false;
×
3406
        }
3407
        const nextKind = this.peekNext().kind;
6,050✔
3408
        return tokenKinds.includes(nextKind);
6,050✔
3409
    }
3410

3411
    private isAtEnd(): boolean {
3412
        const peekToken = this.peek();
149,749✔
3413
        return !peekToken || peekToken.kind === TokenKind.Eof;
149,749✔
3414
    }
3415

3416
    private peekNext(): Token {
3417
        if (this.isAtEnd()) {
19,593!
3418
            return this.peek();
×
3419
        }
3420
        return this.tokens[this.current + 1];
19,593✔
3421
    }
3422

3423
    private peek(): Token {
3424
        return this.tokens[this.current];
1,295,268✔
3425
    }
3426

3427
    private previous(): Token {
3428
        return this.tokens[this.current - 1];
86,253✔
3429
    }
3430

3431
    /**
3432
     * Sometimes we catch an error that is a diagnostic.
3433
     * If that's the case, we want to continue parsing.
3434
     * Otherwise, re-throw the error
3435
     *
3436
     * @param error error caught in a try/catch
3437
     */
3438
    private rethrowNonDiagnosticError(error) {
3439
        if (!error.isDiagnostic) {
11!
3440
            throw error;
×
3441
        }
3442
    }
3443

3444
    /**
3445
     * Get the token that is {offset} indexes away from {this.current}
3446
     * @param offset the number of index steps away from current index to fetch
3447
     * @param tokenKinds the desired token must match one of these
3448
     * @example
3449
     * getToken(-1); //returns the previous token.
3450
     * getToken(0);  //returns current token.
3451
     * getToken(1);  //returns next token
3452
     */
3453
    private getMatchingTokenAtOffset(offset: number, ...tokenKinds: TokenKind[]): Token {
3454
        const token = this.tokens[this.current + offset];
153✔
3455
        if (tokenKinds.includes(token.kind)) {
153✔
3456
            return token;
3✔
3457
        }
3458
    }
3459

3460
    private synchronize() {
3461
        this.advance(); // skip the erroneous token
83✔
3462

3463
        while (!this.isAtEnd()) {
83✔
3464
            if (this.ensureNewLineOrColon(true)) {
175✔
3465
                // end of statement reached
3466
                return;
58✔
3467
            }
3468

3469
            switch (this.peek().kind) { //eslint-disable-line @typescript-eslint/switch-exhaustiveness-check
117✔
3470
                case TokenKind.Namespace:
2!
3471
                case TokenKind.Class:
3472
                case TokenKind.Function:
3473
                case TokenKind.Sub:
3474
                case TokenKind.If:
3475
                case TokenKind.For:
3476
                case TokenKind.ForEach:
3477
                case TokenKind.While:
3478
                case TokenKind.Print:
3479
                case TokenKind.Return:
3480
                    // start parsing again from the next block starter or obvious
3481
                    // expression start
3482
                    return;
1✔
3483
            }
3484

3485
            this.advance();
116✔
3486
        }
3487
    }
3488

3489

3490
    public dispose() {
3491
    }
3492
}
3493

3494
export enum ParseMode {
1✔
3495
    BrightScript = 'BrightScript',
1✔
3496
    BrighterScript = 'BrighterScript'
1✔
3497
}
3498

3499
export interface ParseOptions {
3500
    /**
3501
     * The parse mode. When in 'BrightScript' mode, no BrighterScript syntax is allowed, and will emit diagnostics.
3502
     */
3503
    mode?: ParseMode;
3504
    /**
3505
     * A logger that should be used for logging. If omitted, a default logger is used
3506
     */
3507
    logger?: Logger;
3508
    /**
3509
     * Path to the file where this source code originated
3510
     */
3511
    srcPath?: string;
3512
    /**
3513
     * Should locations be tracked. If false, the `range` property will be omitted
3514
     * @default true
3515
     */
3516
    trackLocations?: boolean;
3517
    /**
3518
     *
3519
     */
3520
    bsConsts?: Map<string, boolean>;
3521
}
3522

3523

3524
class CancelStatementError extends Error {
3525
    constructor() {
3526
        super('CancelStatement');
2✔
3527
    }
3528
}
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