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

rokucommunity / brighterscript / #13074

25 Sep 2024 04:16PM UTC coverage: 86.525% (-1.4%) from 87.933%
#13074

push

web-flow
Merge c610b9e4e into 56dcaaa63

10903 of 13389 branches covered (81.43%)

Branch coverage included in aggregate %.

6936 of 7533 new or added lines in 100 files covered. (92.07%)

83 existing lines in 18 files now uncovered.

12548 of 13714 relevant lines covered (91.5%)

27593.28 hits per line

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

91.59
/src/parser/Expression.ts
1
/* eslint-disable no-bitwise */
2
import type { Token, Identifier } from '../lexer/Token';
3
import { TokenKind } from '../lexer/TokenKind';
1✔
4
import type { Block, NamespaceStatement } from './Statement';
5
import type { Location } from 'vscode-languageserver';
6
import util from '../util';
1✔
7
import type { BrsTranspileState } from './BrsTranspileState';
8
import { ParseMode } from './Parser';
1✔
9
import * as fileUrl from 'file-url';
1✔
10
import type { WalkOptions, WalkVisitor } from '../astUtils/visitors';
11
import { WalkMode } from '../astUtils/visitors';
1✔
12
import { walk, InternalWalkMode, walkArray } from '../astUtils/visitors';
1✔
13
import { isAALiteralExpression, isAAMemberExpression, isArrayLiteralExpression, isArrayType, isCallExpression, isCallableType, isCallfuncExpression, isComponentType, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isIntegerType, isInterfaceMethodStatement, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isNewExpression, isPrimitiveType, isReferenceType, isStringType, isTemplateStringExpression, isTypecastExpression, isUnaryExpression, isVariableExpression } from '../astUtils/reflection';
1✔
14
import type { GetTypeOptions, TranspileResult, TypedefProvider } from '../interfaces';
15
import { TypeChainEntry } from '../interfaces';
1✔
16
import { VoidType } from '../types/VoidType';
1✔
17
import { DynamicType } from '../types/DynamicType';
1✔
18
import type { BscType } from '../types/BscType';
19
import type { AstNode } from './AstNode';
20
import { AstNodeKind, Expression } from './AstNode';
1✔
21
import { SymbolTable } from '../SymbolTable';
1✔
22
import { SourceNode } from 'source-map';
1✔
23
import type { TranspileState } from './TranspileState';
24
import { StringType } from '../types/StringType';
1✔
25
import { TypePropertyReferenceType } from '../types/ReferenceType';
1✔
26
import { UnionType } from '../types/UnionType';
1✔
27
import { ArrayType } from '../types/ArrayType';
1✔
28
import { AssociativeArrayType } from '../types/AssociativeArrayType';
1✔
29
import type { ComponentType } from '../types/ComponentType';
30
import { createToken } from '../astUtils/creators';
1✔
31
import { TypedFunctionType } from '../types';
1✔
32
import { SymbolTypeFlag } from '../SymbolTypeFlag';
1✔
33
import { FunctionType } from '../types/FunctionType';
1✔
34
import type { BaseFunctionType } from '../types/BaseFunctionType';
35
import { brsDocParser } from './BrightScriptDocParser';
1✔
36

37
export type ExpressionVisitor = (expression: Expression, parent: Expression) => void;
38

39
export class BinaryExpression extends Expression {
1✔
40
    constructor(options: {
41
        left: Expression;
42
        operator: Token;
43
        right: Expression;
44
    }) {
45
        super();
3,062✔
46
        this.tokens = {
3,062✔
47
            operator: options.operator
48
        };
49
        this.left = options.left;
3,062✔
50
        this.right = options.right;
3,062✔
51
        this.location = util.createBoundingLocation(this.left, this.tokens.operator, this.right);
3,062✔
52
    }
53
    readonly tokens: {
54
        readonly operator: Token;
55
    };
56

57
    public readonly left: Expression;
58
    public readonly right: Expression;
59

60
    public readonly kind = AstNodeKind.BinaryExpression;
3,062✔
61

62
    public readonly location: Location | undefined;
63

64
    transpile(state: BrsTranspileState): TranspileResult {
65
        return [
3,052✔
66
            state.sourceNode(this.left, this.left.transpile(state)),
67
            ' ',
68
            state.transpileToken(this.tokens.operator),
69
            ' ',
70
            state.sourceNode(this.right, this.right.transpile(state))
71
        ];
72
    }
73

74
    walk(visitor: WalkVisitor, options: WalkOptions) {
75
        if (options.walkMode & InternalWalkMode.walkExpressions) {
9,964!
76
            walk(this, 'left', visitor, options);
9,964✔
77
            walk(this, 'right', visitor, options);
9,964✔
78
        }
79
    }
80

81

82
    public getType(options: GetTypeOptions): BscType {
83
        const operatorKind = this.tokens.operator.kind;
339✔
84
        if (options.flags & SymbolTypeFlag.typetime) {
339✔
85
            // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
86
            switch (operatorKind) {
79✔
87
                case TokenKind.Or:
78✔
88
                    return new UnionType([this.left.getType(options), this.right.getType(options)]);
78✔
89
                //TODO: Intersection Types?, eg. case TokenKind.And:
90
            }
91
        } else if (options.flags & SymbolTypeFlag.runtime) {
260!
92
            return util.binaryOperatorResultType(
260✔
93
                this.left.getType(options),
94
                this.tokens.operator,
95
                this.right.getType(options));
96
        }
97
        return DynamicType.instance;
1✔
98
    }
99

100
    get leadingTrivia(): Token[] {
101
        return this.left.leadingTrivia;
1,581✔
102
    }
103
}
104

105

106
export class CallExpression extends Expression {
1✔
107
    /**
108
     * Number of parameters that can be defined on a function
109
     *
110
     * Prior to Roku OS 11.5, this was 32
111
     * As of Roku OS 11.5, this is 63
112
     */
113
    static MaximumArguments = 63;
1✔
114

115
    constructor(options: {
116
        callee: Expression;
117
        openingParen?: Token;
118
        args?: Expression[];
119
        closingParen?: Token;
120
    }) {
121
        super();
2,266✔
122
        this.tokens = {
2,266✔
123
            openingParen: options.openingParen,
124
            closingParen: options.closingParen
125
        };
126
        this.callee = options.callee;
2,266✔
127
        this.args = options.args ?? [];
2,266✔
128
        this.location = util.createBoundingLocation(this.callee, this.tokens.openingParen, ...this.args, this.tokens.closingParen);
2,266✔
129
    }
130

131
    readonly callee: Expression;
132
    readonly args: Expression[];
133
    readonly tokens: {
134
        /**
135
         * Can either be `(`, or `?(` for optional chaining - defaults to '('
136
         */
137
        readonly openingParen?: Token;
138
        readonly closingParen?: Token;
139
    };
140

141
    public readonly kind = AstNodeKind.CallExpression;
2,266✔
142

143
    public readonly location: Location | undefined;
144

145
    transpile(state: BrsTranspileState, nameOverride?: string) {
146
        let result: TranspileResult = [];
1,524✔
147

148
        //transpile the name
149
        if (nameOverride) {
1,524✔
150
            result.push(
12✔
151
                //transpile leading comments since we're bypassing callee.transpile (which would normally do this)
152
                ...state.transpileLeadingCommentsForAstNode(this),
153
                state.sourceNode(this.callee, nameOverride)
154
            );
155
        } else {
156
            result.push(...this.callee.transpile(state));
1,512✔
157
        }
158

159
        result.push(
1,524✔
160
            state.transpileToken(this.tokens.openingParen, '(')
161
        );
162
        for (let i = 0; i < this.args.length; i++) {
1,524✔
163
            //add comma between args
164
            if (i > 0) {
1,092✔
165
                result.push(', ');
336✔
166
            }
167
            let arg = this.args[i];
1,092✔
168
            result.push(...arg.transpile(state));
1,092✔
169
        }
170
        if (this.tokens.closingParen) {
1,524!
171
            result.push(
1,524✔
172
                state.transpileToken(this.tokens.closingParen)
173
            );
174
        }
175
        return result;
1,524✔
176
    }
177

178
    walk(visitor: WalkVisitor, options: WalkOptions) {
179
        if (options.walkMode & InternalWalkMode.walkExpressions) {
9,550!
180
            walk(this, 'callee', visitor, options);
9,550✔
181
            walkArray(this.args, visitor, options, this);
9,550✔
182
        }
183
    }
184

185
    getType(options: GetTypeOptions) {
186
        const calleeType = this.callee.getType(options);
943✔
187
        if (options.ignoreCall) {
943!
NEW
188
            return calleeType;
×
189
        }
190
        if (isNewExpression(this.parent)) {
943✔
191
            return calleeType;
320✔
192
        }
193
        const specialCaseReturnType = util.getSpecialCaseCallExpressionReturnType(this);
623✔
194
        if (specialCaseReturnType) {
623✔
195
            return specialCaseReturnType;
100✔
196
        }
197
        if (isCallableType(calleeType) && (!isReferenceType(calleeType.returnType) || calleeType.returnType?.isResolvable())) {
523!
198
            return calleeType.returnType;
222✔
199
        }
200
        if (!isReferenceType(calleeType) && (calleeType as BaseFunctionType)?.returnType?.isResolvable()) {
301✔
201
            return (calleeType as BaseFunctionType).returnType;
257✔
202
        }
203
        return new TypePropertyReferenceType(calleeType, 'returnType');
44✔
204
    }
205

206
    get leadingTrivia(): Token[] {
207
        return this.callee.leadingTrivia;
7,843✔
208
    }
209
}
210

211
export class FunctionExpression extends Expression implements TypedefProvider {
1✔
212
    constructor(options: {
213
        functionType?: Token;
214
        leftParen?: Token;
215
        parameters?: FunctionParameterExpression[];
216
        rightParen?: Token;
217
        as?: Token;
218
        returnTypeExpression?: TypeExpression;
219
        body: Block;
220
        endFunctionType?: Token;
221
    }) {
222
        super();
3,422✔
223
        this.tokens = {
3,422✔
224
            functionType: options.functionType,
225
            leftParen: options.leftParen,
226
            rightParen: options.rightParen,
227
            as: options.as,
228
            endFunctionType: options.endFunctionType
229
        };
230
        this.parameters = options.parameters ?? [];
3,422!
231
        this.body = options.body;
3,422✔
232
        this.returnTypeExpression = options.returnTypeExpression;
3,422✔
233
        //if there's a body, and it doesn't have a SymbolTable, assign one
234
        if (this.body) {
3,422!
235
            if (!this.body.symbolTable) {
3,422!
236
                this.body.symbolTable = new SymbolTable(`Block`, () => this.getSymbolTable());
26,397✔
237
            } else {
NEW
238
                this.body.symbolTable.pushParentProvider(() => this.getSymbolTable());
×
239
            }
240
            this.body.parent = this;
3,422✔
241
        }
242
        this.symbolTable = new SymbolTable('FunctionExpression', () => this.parent?.getSymbolTable());
32,683!
243
    }
244

245
    public readonly kind = AstNodeKind.FunctionExpression;
3,422✔
246

247
    readonly parameters: FunctionParameterExpression[];
248
    public readonly body: Block;
249
    public readonly returnTypeExpression?: TypeExpression;
250

251
    readonly tokens: {
252
        readonly functionType?: Token;
253
        readonly endFunctionType?: Token;
254
        readonly leftParen?: Token;
255
        readonly rightParen?: Token;
256
        readonly as?: Token;
257
    };
258

259
    public get leadingTrivia(): Token[] {
260
        return this.tokens.functionType?.leadingTrivia;
25,898!
261
    }
262

263
    public get endTrivia(): Token[] {
264
        return this.tokens.endFunctionType?.leadingTrivia;
2!
265
    }
266

267
    /**
268
     * The range of the function, starting at the 'f' in function or 's' in sub (or the open paren if the keyword is missing),
269
     * and ending with the last n' in 'end function' or 'b' in 'end sub'
270
     */
271
    public get location(): Location {
272
        return util.createBoundingLocation(
8,987✔
273
            this.tokens.functionType,
274
            this.tokens.leftParen,
275
            ...this.parameters,
276
            this.tokens.rightParen,
277
            this.tokens.as,
278
            this.returnTypeExpression,
279
            this.tokens.endFunctionType
280
        );
281
    }
282

283
    transpile(state: BrsTranspileState, name?: Identifier, includeBody = true) {
1,365✔
284
        let results = [] as TranspileResult;
1,365✔
285
        //'function'|'sub'
286
        results.push(
1,365✔
287
            state.transpileToken(this.tokens.functionType, 'function', false, state.skipLeadingComments)
288
        );
289
        //functionName?
290
        if (name) {
1,365✔
291
            results.push(
1,294✔
292
                ' ',
293
                state.transpileToken(name)
294
            );
295
        }
296
        //leftParen
297
        results.push(
1,365✔
298
            state.transpileToken(this.tokens.leftParen)
299
        );
300
        //parameters
301
        for (let i = 0; i < this.parameters.length; i++) {
1,365✔
302
            let param = this.parameters[i];
2,066✔
303
            //add commas
304
            if (i > 0) {
2,066✔
305
                results.push(', ');
1,020✔
306
            }
307
            //add parameter
308
            results.push(param.transpile(state));
2,066✔
309
        }
310
        //right paren
311
        results.push(
1,365✔
312
            state.transpileToken(this.tokens.rightParen)
313
        );
314
        //as [Type]
315
        if (!state.options.removeParameterTypes && this.returnTypeExpression) {
1,365✔
316
            results.push(
34✔
317
                ' ',
318
                //as
319
                state.transpileToken(this.tokens.as, 'as'),
320
                ' ',
321
                //return type
322
                ...this.returnTypeExpression.transpile(state)
323
            );
324
        }
325
        let hasBody = false;
1,365✔
326
        if (includeBody) {
1,365!
327
            state.lineage.unshift(this);
1,365✔
328
            let body = this.body.transpile(state);
1,365✔
329
            hasBody = body.length > 0;
1,365✔
330
            state.lineage.shift();
1,365✔
331
            results.push(...body);
1,365✔
332
        }
333

334
        const lastLocatable = hasBody ? this.body : this.returnTypeExpression ?? this.tokens.leftParen ?? this.tokens.functionType;
1,365!
335
        results.push(
1,365✔
336
            ...state.transpileEndBlockToken(lastLocatable, this.tokens.endFunctionType, `end ${this.tokens.functionType ?? 'function'}`)
4,095!
337
        );
338
        return results;
1,365✔
339
    }
340

341
    getTypedef(state: BrsTranspileState) {
342
        let results = [
62✔
343
            new SourceNode(1, 0, null, [
344
                //'function'|'sub'
345
                this.tokens.functionType?.text ?? 'function',
372!
346
                //functionName?
347
                ...(isFunctionStatement(this.parent) || isMethodStatement(this.parent) ? [' ', this.parent.tokens.name?.text ?? ''] : []),
519!
348
                //leftParen
349
                '(',
350
                //parameters
351
                ...(
352
                    this.parameters?.map((param, i) => ([
73!
353
                        //separating comma
354
                        i > 0 ? ', ' : '',
73✔
355
                        ...param.getTypedef(state)
356
                    ])) ?? []
62!
357
                ) as any,
358
                //right paren
359
                ')',
360
                //as <ReturnType>
361
                ...(this.returnTypeExpression ? [
62✔
362
                    ' ',
363
                    this.tokens.as?.text ?? 'as',
18!
364
                    ' ',
365
                    ...this.returnTypeExpression.getTypedef(state)
366
                ] : []),
367
                '\n',
368
                state.indent(),
369
                //'end sub'|'end function'
370
                this.tokens.endFunctionType?.text ?? `end ${this.tokens.functionType ?? 'function'}`
372!
371
            ])
372
        ];
373
        return results;
62✔
374
    }
375

376
    walk(visitor: WalkVisitor, options: WalkOptions) {
377
        if (options.walkMode & InternalWalkMode.walkExpressions) {
15,437!
378
            walkArray(this.parameters, visitor, options, this);
15,437✔
379
            walk(this, 'returnTypeExpression', visitor, options);
15,437✔
380
            //This is the core of full-program walking...it allows us to step into sub functions
381
            if (options.walkMode & InternalWalkMode.recurseChildFunctions) {
15,437!
382
                walk(this, 'body', visitor, options);
15,437✔
383
            }
384
        }
385
    }
386

387
    public getType(options: GetTypeOptions): TypedFunctionType {
388
        //if there's a defined return type, use that
389
        let returnType: BscType;
390

391
        const docs = brsDocParser.parseNode(this.findAncestor(isFunctionStatement));
3,785✔
392

393
        returnType = util.chooseTypeFromCodeOrDocComment(
3,785✔
394
            this.returnTypeExpression?.getType({ ...options, typeChain: undefined }),
11,355✔
NEW
395
            docs.getReturnBscType({ ...options, tableProvider: () => this.getSymbolTable() }),
×
396
            options
397
        );
398

399
        const isSub = this.tokens.functionType?.kind === TokenKind.Sub;
3,785!
400
        //if we don't have a return type and this is a sub, set the return type to `void`. else use `dynamic`
401
        if (!returnType) {
3,785✔
402
            returnType = isSub ? VoidType.instance : DynamicType.instance;
3,199✔
403
        }
404

405
        const resultType = new TypedFunctionType(returnType);
3,785✔
406
        resultType.isSub = isSub;
3,785✔
407
        for (let param of this.parameters) {
3,785✔
408
            resultType.addParameter(param.tokens.name.text, param.getType({ ...options, typeChain: undefined }), !!param.defaultValue);
1,990✔
409
        }
410
        // Figure out this function's name if we can
411
        let funcName = '';
3,785✔
412
        if (isMethodStatement(this.parent) || isInterfaceMethodStatement(this.parent)) {
3,785✔
413
            funcName = this.parent.getName(ParseMode.BrighterScript);
342✔
414
            if (options.typeChain) {
342✔
415
                // Get the typechain info from the parent class
416
                this.parent.parent?.getType(options);
1!
417
            }
418
        } else if (isFunctionStatement(this.parent)) {
3,443✔
419
            funcName = this.parent.getName(ParseMode.BrighterScript);
3,380✔
420
        }
421
        if (funcName) {
3,785✔
422
            resultType.setName(funcName);
3,722✔
423
        }
424
        options.typeChain?.push(new TypeChainEntry({ name: funcName, type: resultType, data: options.data, astNode: this }));
3,785✔
425
        return resultType;
3,785✔
426
    }
427
}
428

429
export class FunctionParameterExpression extends Expression {
1✔
430
    constructor(options: {
431
        name: Identifier;
432
        equals?: Token;
433
        defaultValue?: Expression;
434
        as?: Token;
435
        typeExpression?: TypeExpression;
436
    }) {
437
        super();
2,830✔
438
        this.tokens = {
2,830✔
439
            name: options.name,
440
            equals: options.equals,
441
            as: options.as
442
        };
443
        this.defaultValue = options.defaultValue;
2,830✔
444
        this.typeExpression = options.typeExpression;
2,830✔
445
    }
446

447
    public readonly kind = AstNodeKind.FunctionParameterExpression;
2,830✔
448

449
    readonly tokens: {
450
        readonly name: Identifier;
451
        readonly equals?: Token;
452
        readonly as?: Token;
453
    };
454

455
    public readonly defaultValue?: Expression;
456
    public readonly typeExpression?: TypeExpression;
457

458
    public getType(options: GetTypeOptions) {
459
        const docs = brsDocParser.parseNode(this.findAncestor(isFunctionStatement));
5,133✔
460
        const paramName = this.tokens.name.text;
5,133✔
461

462
        const paramTypeFromCode = this.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime, typeChain: undefined }) ??
5,133✔
463
            this.defaultValue?.getType({ ...options, flags: SymbolTypeFlag.runtime, typeChain: undefined });
8,031✔
464
        const paramTypeFromDoc = docs.getParamBscType(paramName, { ...options, fullName: paramName, tableProvider: () => this.getSymbolTable() });
5,133✔
465

466
        let paramType = util.chooseTypeFromCodeOrDocComment(paramTypeFromCode, paramTypeFromDoc, options) ?? DynamicType.instance;
5,133✔
467
        options.typeChain?.push(new TypeChainEntry({ name: paramName, type: paramType, data: options.data, astNode: this }));
5,133✔
468
        return paramType;
5,133✔
469
    }
470

471
    public get location(): Location | undefined {
472
        return util.createBoundingLocation(
8,832✔
473
            this.tokens.name,
474
            this.tokens.as,
475
            this.typeExpression,
476
            this.tokens.equals,
477
            this.defaultValue
478
        );
479
    }
480

481
    public transpile(state: BrsTranspileState) {
482
        let result: TranspileResult = [
2,074✔
483
            //name
484
            state.transpileToken(this.tokens.name)
485
        ];
486
        //default value
487
        if (this.defaultValue) {
2,074✔
488
            result.push(' = ');
9✔
489
            result.push(this.defaultValue.transpile(state));
9✔
490
        }
491
        //type declaration
492
        if (this.typeExpression && !state.options.removeParameterTypes) {
2,074✔
493
            result.push(' ');
65✔
494
            result.push(state.transpileToken(this.tokens.as, 'as'));
65✔
495
            result.push(' ');
65✔
496
            result.push(
65✔
497
                ...(this.typeExpression?.transpile(state) ?? [])
390!
498
            );
499
        }
500

501
        return result;
2,074✔
502
    }
503

504
    public getTypedef(state: BrsTranspileState): TranspileResult {
505
        const results = [this.tokens.name.text] as TranspileResult;
73✔
506

507
        if (this.defaultValue) {
73!
508
            results.push(' = ', ...this.defaultValue.transpile(state));
×
509
        }
510

511
        if (this.tokens.as) {
73✔
512
            results.push(' as ');
6✔
513

514
            // TODO: Is this conditional needed? Will typeToken always exist
515
            // so long as `asToken` exists?
516
            if (this.typeExpression) {
6!
517
                results.push(...(this.typeExpression?.getTypedef(state) ?? ['']));
6!
518
            }
519
        }
520

521
        return results;
73✔
522
    }
523

524
    walk(visitor: WalkVisitor, options: WalkOptions) {
525
        // eslint-disable-next-line no-bitwise
526
        if (options.walkMode & InternalWalkMode.walkExpressions) {
11,831!
527
            walk(this, 'defaultValue', visitor, options);
11,831✔
528
            walk(this, 'typeExpression', visitor, options);
11,831✔
529
        }
530
    }
531

532
    get leadingTrivia(): Token[] {
533
        return this.tokens.name.leadingTrivia;
4,382✔
534
    }
535
}
536

537
export class DottedGetExpression extends Expression {
1✔
538
    constructor(options: {
539
        obj: Expression;
540
        name: Identifier;
541
        /**
542
         * Can either be `.`, or `?.` for optional chaining - defaults in transpile to '.'
543
         */
544
        dot?: Token;
545
    }) {
546
        super();
2,692✔
547
        this.tokens = {
2,692✔
548
            name: options.name,
549
            dot: options.dot
550
        };
551
        this.obj = options.obj;
2,692✔
552

553
        this.location = util.createBoundingLocation(this.obj, this.tokens.dot, this.tokens.name);
2,692✔
554
    }
555

556
    readonly tokens: {
557
        readonly name: Identifier;
558
        readonly dot?: Token;
559
    };
560
    readonly obj: Expression;
561

562
    public readonly kind = AstNodeKind.DottedGetExpression;
2,692✔
563

564
    public readonly location: Location | undefined;
565

566
    transpile(state: BrsTranspileState) {
567
        //if the callee starts with a namespace name, transpile the name
568
        if (state.file.calleeStartsWithNamespace(this)) {
867✔
569
            return [
8✔
570
                ...state.transpileLeadingCommentsForAstNode(this),
571
                state.sourceNode(this, this.getName(ParseMode.BrightScript))
572
            ];
573
        } else {
574
            return [
859✔
575
                ...this.obj.transpile(state),
576
                state.transpileToken(this.tokens.dot, '.'),
577
                state.transpileToken(this.tokens.name)
578
            ];
579
        }
580
    }
581

582
    walk(visitor: WalkVisitor, options: WalkOptions) {
583
        if (options.walkMode & InternalWalkMode.walkExpressions) {
10,211!
584
            walk(this, 'obj', visitor, options);
10,211✔
585
        }
586
    }
587

588
    getType(options: GetTypeOptions) {
589
        const objType = this.obj?.getType(options);
6,099!
590
        let result = objType?.getMemberType(this.tokens.name?.text, options);
6,099!
591

592
        if (util.isClassUsedAsFunction(result, this, options)) {
6,099✔
593
            // treat this class constructor as a function
594
            result = FunctionType.instance;
11✔
595
        }
596
        options.typeChain?.push(new TypeChainEntry({
6,099✔
597
            name: this.tokens.name?.text,
7,797!
598
            type: result,
599
            data: options.data,
600
            location: this.tokens.name?.location ?? this.location,
15,594!
601
            astNode: this
602
        }));
603
        if (result ||
6,099✔
604
            options.flags & SymbolTypeFlag.typetime ||
605
            (isPrimitiveType(objType) || isCallableType(objType))) {
606
            // All types should be known at typeTime, or the obj is well known
607
            return result;
6,068✔
608
        }
609
        // It is possible at runtime that a value has been added dynamically to an object, or something
610
        // TODO: maybe have a strict flag on this?
611
        return DynamicType.instance;
31✔
612
    }
613

614
    getName(parseMode: ParseMode) {
615
        return util.getAllDottedGetPartsAsString(this, parseMode);
34✔
616
    }
617

618
    get leadingTrivia(): Token[] {
619
        return this.obj.leadingTrivia;
13,901✔
620
    }
621

622
}
623

624
export class XmlAttributeGetExpression extends Expression {
1✔
625
    constructor(options: {
626
        obj: Expression;
627
        /**
628
         * Can either be `@`, or `?@` for optional chaining - defaults to '@'
629
         */
630
        at?: Token;
631
        name: Identifier;
632
    }) {
633
        super();
10✔
634
        this.obj = options.obj;
10✔
635
        this.tokens = { at: options.at, name: options.name };
10✔
636
        this.location = util.createBoundingLocation(this.obj, this.tokens.at, this.tokens.name);
10✔
637
    }
638

639
    public readonly kind = AstNodeKind.XmlAttributeGetExpression;
10✔
640

641
    public readonly tokens: {
642
        name: Identifier;
643
        at?: Token;
644
    };
645

646
    public readonly obj: Expression;
647

648
    public readonly location: Location | undefined;
649

650
    transpile(state: BrsTranspileState) {
651
        return [
3✔
652
            ...this.obj.transpile(state),
653
            state.transpileToken(this.tokens.at, '@'),
654
            state.transpileToken(this.tokens.name)
655
        ];
656
    }
657

658
    walk(visitor: WalkVisitor, options: WalkOptions) {
659
        if (options.walkMode & InternalWalkMode.walkExpressions) {
28!
660
            walk(this, 'obj', visitor, options);
28✔
661
        }
662
    }
663

664
    get leadingTrivia(): Token[] {
665
        return this.obj.leadingTrivia;
15✔
666
    }
667
}
668

669
export class IndexedGetExpression extends Expression {
1✔
670
    constructor(options: {
671
        obj: Expression;
672
        indexes: Expression[];
673
        /**
674
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.` - defaults to '[' in transpile
675
         */
676
        openingSquare?: Token;
677
        closingSquare?: Token;
678
        questionDot?: Token;//  ? or ?.
679
    }) {
680
        super();
143✔
681
        this.tokens = {
143✔
682
            openingSquare: options.openingSquare,
683
            closingSquare: options.closingSquare,
684
            questionDot: options.questionDot
685
        };
686
        this.obj = options.obj;
143✔
687
        this.indexes = options.indexes;
143✔
688
        this.location = util.createBoundingLocation(this.obj, this.tokens.openingSquare, this.tokens.questionDot, this.tokens.openingSquare, ...this.indexes, this.tokens.closingSquare);
143✔
689
    }
690

691
    public readonly kind = AstNodeKind.IndexedGetExpression;
143✔
692

693
    public readonly obj: Expression;
694
    public readonly indexes: Expression[];
695

696
    readonly tokens: {
697
        /**
698
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.` - defaults to '[' in transpile
699
         */
700
        readonly openingSquare?: Token;
701
        readonly closingSquare?: Token;
702
        readonly questionDot?: Token; //  ? or ?.
703
    };
704

705
    public readonly location: Location | undefined;
706

707
    transpile(state: BrsTranspileState) {
708
        const result = [];
64✔
709
        result.push(
64✔
710
            ...this.obj.transpile(state),
711
            this.tokens.questionDot ? state.transpileToken(this.tokens.questionDot) : '',
64✔
712
            state.transpileToken(this.tokens.openingSquare, '[')
713
        );
714
        for (let i = 0; i < this.indexes.length; i++) {
64✔
715
            //add comma between indexes
716
            if (i > 0) {
72✔
717
                result.push(', ');
8✔
718
            }
719
            let index = this.indexes[i];
72✔
720
            result.push(
72✔
721
                ...(index?.transpile(state) ?? [])
432!
722
            );
723
        }
724
        result.push(
64✔
725
            state.transpileToken(this.tokens.closingSquare, ']')
726
        );
727
        return result;
64✔
728
    }
729

730
    walk(visitor: WalkVisitor, options: WalkOptions) {
731
        if (options.walkMode & InternalWalkMode.walkExpressions) {
492!
732
            walk(this, 'obj', visitor, options);
492✔
733
            walkArray(this.indexes, visitor, options, this);
492✔
734
        }
735
    }
736

737
    getType(options: GetTypeOptions): BscType {
738
        const objType = this.obj.getType(options);
201✔
739
        if (isArrayType(objType)) {
201✔
740
            // This is used on an array. What is the default type of that array?
741
            return objType.defaultType;
10✔
742
        }
743
        return super.getType(options);
191✔
744
    }
745

746
    get leadingTrivia(): Token[] {
747
        return this.obj.leadingTrivia;
1,021✔
748
    }
749
}
750

751
export class GroupingExpression extends Expression {
1✔
752
    constructor(options: {
753
        leftParen?: Token;
754
        rightParen?: Token;
755
        expression: Expression;
756
    }) {
757
        super();
46✔
758
        this.tokens = {
46✔
759
            rightParen: options.rightParen,
760
            leftParen: options.leftParen
761
        };
762
        this.expression = options.expression;
46✔
763
        this.location = util.createBoundingLocation(this.tokens.leftParen, this.expression, this.tokens.rightParen);
46✔
764
    }
765

766
    public readonly tokens: {
767
        readonly leftParen?: Token;
768
        readonly rightParen?: Token;
769
    };
770
    public readonly expression: Expression;
771

772
    public readonly kind = AstNodeKind.GroupingExpression;
46✔
773

774
    public readonly location: Location | undefined;
775

776
    transpile(state: BrsTranspileState) {
777
        if (isTypecastExpression(this.expression)) {
13✔
778
            return this.expression.transpile(state);
7✔
779
        }
780
        return [
6✔
781
            state.transpileToken(this.tokens.leftParen),
782
            ...this.expression.transpile(state),
783
            state.transpileToken(this.tokens.rightParen)
784
        ];
785
    }
786

787
    walk(visitor: WalkVisitor, options: WalkOptions) {
788
        if (options.walkMode & InternalWalkMode.walkExpressions) {
154!
789
            walk(this, 'expression', visitor, options);
154✔
790
        }
791
    }
792

793
    getType(options: GetTypeOptions) {
794
        return this.expression.getType(options);
62✔
795
    }
796

797
    get leadingTrivia(): Token[] {
798
        return this.tokens.leftParen?.leadingTrivia;
255!
799
    }
800
}
801

802
export class LiteralExpression extends Expression {
1✔
803
    constructor(options: {
804
        value: Token;
805
    }) {
806
        super();
7,075✔
807
        this.tokens = {
7,075✔
808
            value: options.value
809
        };
810
    }
811

812
    public readonly tokens: {
813
        readonly value: Token;
814
    };
815

816
    public readonly kind = AstNodeKind.LiteralExpression;
7,075✔
817

818
    public get location() {
819
        return this.tokens.value.location;
20,057✔
820
    }
821

822
    public getType(options?: GetTypeOptions) {
823
        return util.tokenToBscType(this.tokens.value);
3,243✔
824
    }
825

826
    transpile(state: BrsTranspileState) {
827
        let text: string;
828
        if (this.tokens.value.kind === TokenKind.TemplateStringQuasi) {
4,362✔
829
            //wrap quasis with quotes (and escape inner quotemarks)
830
            text = `"${this.tokens.value.text.replace(/"/g, '""')}"`;
28✔
831

832
        } else if (this.tokens.value.kind === TokenKind.StringLiteral) {
4,334✔
833
            text = this.tokens.value.text;
2,906✔
834
            //add trailing quotemark if it's missing. We will have already generated a diagnostic for this.
835
            if (text.endsWith('"') === false) {
2,906✔
836
                text += '"';
1✔
837
            }
838
        } else {
839
            text = this.tokens.value.text;
1,428✔
840
        }
841

842
        return [
4,362✔
843
            state.transpileToken({ ...this.tokens.value, text: text })
844
        ];
845
    }
846

847
    walk(visitor: WalkVisitor, options: WalkOptions) {
848
        //nothing to walk
849
    }
850

851
    get leadingTrivia(): Token[] {
852
        return this.tokens.value.leadingTrivia;
10,711✔
853
    }
854
}
855

856
/**
857
 * This is a special expression only used within template strings. It exists so we can prevent producing lots of empty strings
858
 * during template string transpile by identifying these expressions explicitly and skipping the bslib_toString around them
859
 */
860
export class EscapedCharCodeLiteralExpression extends Expression {
1✔
861
    constructor(options: {
862
        value: Token & { charCode: number };
863
    }) {
864
        super();
29✔
865
        this.tokens = { value: options.value };
29✔
866
        this.location = this.tokens.value.location;
29✔
867
    }
868

869
    public readonly kind = AstNodeKind.EscapedCharCodeLiteralExpression;
29✔
870

871
    public readonly tokens: {
872
        readonly value: Token & { charCode: number };
873
    };
874

875
    public readonly location: Location;
876

877
    transpile(state: BrsTranspileState) {
878
        return [
13✔
879
            state.sourceNode(this, `chr(${this.tokens.value.charCode})`)
880
        ];
881
    }
882

883
    walk(visitor: WalkVisitor, options: WalkOptions) {
884
        //nothing to walk
885
    }
886
}
887

888
export class ArrayLiteralExpression extends Expression {
1✔
889
    constructor(options: {
890
        elements: Array<Expression>;
891
        open?: Token;
892
        close?: Token;
893
    }) {
894
        super();
134✔
895
        this.tokens = {
134✔
896
            open: options.open,
897
            close: options.close
898
        };
899
        this.elements = options.elements;
134✔
900
        this.location = util.createBoundingLocation(this.tokens.open, ...this.elements, this.tokens.close);
134✔
901
    }
902

903
    public readonly elements: Array<Expression>;
904

905
    public readonly tokens: {
906
        readonly open?: Token;
907
        readonly close?: Token;
908
    };
909

910
    public readonly kind = AstNodeKind.ArrayLiteralExpression;
134✔
911

912
    public readonly location: Location | undefined;
913

914
    transpile(state: BrsTranspileState) {
915
        let result: TranspileResult = [];
50✔
916
        result.push(
50✔
917
            state.transpileToken(this.tokens.open, '[')
918
        );
919
        let hasChildren = this.elements.length > 0;
50✔
920
        state.blockDepth++;
50✔
921

922
        for (let i = 0; i < this.elements.length; i++) {
50✔
923
            let previousElement = this.elements[i - 1];
53✔
924
            let element = this.elements[i];
53✔
925

926
            if (util.isLeadingCommentOnSameLine(previousElement ?? this.tokens.open, element)) {
53✔
927
                result.push(' ');
3✔
928
            } else {
929
                result.push(
50✔
930
                    '\n',
931
                    state.indent()
932
                );
933
            }
934
            result.push(
53✔
935
                ...element.transpile(state)
936
            );
937
        }
938
        state.blockDepth--;
50✔
939
        //add a newline between open and close if there are elements
940
        const lastLocatable = this.elements[this.elements.length - 1] ?? this.tokens.open;
50✔
941
        result.push(...state.transpileEndBlockToken(lastLocatable, this.tokens.close, ']', hasChildren));
50✔
942

943
        return result;
50✔
944
    }
945

946
    walk(visitor: WalkVisitor, options: WalkOptions) {
947
        if (options.walkMode & InternalWalkMode.walkExpressions) {
648!
948
            walkArray(this.elements, visitor, options, this);
648✔
949
        }
950
    }
951

952
    getType(options: GetTypeOptions): BscType {
953
        const innerTypes = this.elements.map(expr => expr.getType(options));
172✔
954
        return new ArrayType(...innerTypes);
133✔
955
    }
956
    get leadingTrivia(): Token[] {
957
        return this.tokens.open?.leadingTrivia;
445!
958
    }
959

960
    get endTrivia(): Token[] {
961
        return this.tokens.close?.leadingTrivia;
2!
962
    }
963
}
964

965
export class AAMemberExpression extends Expression {
1✔
966
    constructor(options: {
967
        key: Token;
968
        colon?: Token;
969
        /** The expression evaluated to determine the member's initial value. */
970
        value: Expression;
971
        comma?: Token;
972
    }) {
973
        super();
266✔
974
        this.tokens = {
266✔
975
            key: options.key,
976
            colon: options.colon,
977
            comma: options.comma
978
        };
979
        this.value = options.value;
266✔
980
        this.location = util.createBoundingLocation(this.tokens.key, this.tokens.colon, this.value);
266✔
981
    }
982

983
    public readonly kind = AstNodeKind.AAMemberExpression;
266✔
984

985
    public readonly location: Location | undefined;
986

987
    public readonly tokens: {
988
        readonly key: Token;
989
        readonly colon?: Token;
990
        readonly comma?: Token;
991
    };
992

993
    /** The expression evaluated to determine the member's initial value. */
994
    public readonly value: Expression;
995

996
    transpile(state: BrsTranspileState) {
997
        //TODO move the logic from AALiteralExpression loop into this function
998
        return [];
×
999
    }
1000

1001
    walk(visitor: WalkVisitor, options: WalkOptions) {
1002
        walk(this, 'value', visitor, options);
896✔
1003
    }
1004

1005
    getType(options: GetTypeOptions): BscType {
1006
        return this.value.getType(options);
192✔
1007
    }
1008

1009
    get leadingTrivia(): Token[] {
1010
        return this.tokens.key.leadingTrivia;
681✔
1011
    }
1012

1013
}
1014

1015
export class AALiteralExpression extends Expression {
1✔
1016
    constructor(options: {
1017
        elements: Array<AAMemberExpression>;
1018
        open?: Token;
1019
        close?: Token;
1020
    }) {
1021
        super();
255✔
1022
        this.tokens = {
255✔
1023
            open: options.open,
1024
            close: options.close
1025
        };
1026
        this.elements = options.elements;
255✔
1027
        this.location = util.createBoundingLocation(this.tokens.open, ...this.elements, this.tokens.close);
255✔
1028
    }
1029

1030
    public readonly elements: Array<AAMemberExpression>;
1031
    public readonly tokens: {
1032
        readonly open?: Token;
1033
        readonly close?: Token;
1034
    };
1035

1036
    public readonly kind = AstNodeKind.AALiteralExpression;
255✔
1037

1038
    public readonly location: Location | undefined;
1039

1040
    transpile(state: BrsTranspileState) {
1041
        let result: TranspileResult = [];
55✔
1042
        //open curly
1043
        result.push(
55✔
1044
            state.transpileToken(this.tokens.open, '{')
1045
        );
1046
        let hasChildren = this.elements.length > 0;
55✔
1047
        //add newline if the object has children and the first child isn't a comment starting on the same line as opening curly
1048
        if (hasChildren && !util.isLeadingCommentOnSameLine(this.tokens.open, this.elements[0])) {
55✔
1049
            result.push('\n');
24✔
1050
        }
1051
        state.blockDepth++;
55✔
1052
        for (let i = 0; i < this.elements.length; i++) {
55✔
1053
            let element = this.elements[i];
36✔
1054
            let previousElement = this.elements[i - 1];
36✔
1055
            let nextElement = this.elements[i + 1];
36✔
1056

1057
            //don't indent if comment is same-line
1058
            if (util.isLeadingCommentOnSameLine(this.tokens.open, element) ||
36✔
1059
                util.isLeadingCommentOnSameLine(previousElement, element)) {
1060
                result.push(' ');
7✔
1061
            } else {
1062
                //indent line
1063
                result.push(state.indent());
29✔
1064
            }
1065

1066
            //key
1067
            result.push(
36✔
1068
                state.transpileToken(element.tokens.key)
1069
            );
1070
            //colon
1071
            result.push(
36✔
1072
                state.transpileToken(element.tokens.colon, ':'),
1073
                ' '
1074
            );
1075
            //value
1076
            result.push(...element.value.transpile(state));
36✔
1077

1078
            //if next element is a same-line comment, skip the newline
1079
            if (nextElement && !util.isLeadingCommentOnSameLine(element, nextElement)) {
36✔
1080
                //add a newline between statements
1081
                result.push('\n');
5✔
1082
            }
1083
        }
1084
        state.blockDepth--;
55✔
1085

1086
        const lastElement = this.elements[this.elements.length - 1] ?? this.tokens.open;
55✔
1087
        result.push(...state.transpileEndBlockToken(lastElement, this.tokens.close, '}', hasChildren));
55✔
1088

1089
        return result;
55✔
1090
    }
1091

1092
    walk(visitor: WalkVisitor, options: WalkOptions) {
1093
        if (options.walkMode & InternalWalkMode.walkExpressions) {
938!
1094
            walkArray(this.elements, visitor, options, this);
938✔
1095
        }
1096
    }
1097

1098
    getType(options: GetTypeOptions): BscType {
1099
        const resultType = new AssociativeArrayType();
193✔
1100
        resultType.addBuiltInInterfaces();
193✔
1101
        for (const element of this.elements) {
193✔
1102
            if (isAAMemberExpression(element)) {
192!
1103
                resultType.addMember(element.tokens.key.text, { definingNode: element }, element.getType(options), SymbolTypeFlag.runtime);
192✔
1104
            }
1105
        }
1106
        return resultType;
193✔
1107
    }
1108

1109
    public get leadingTrivia(): Token[] {
1110
        return this.tokens.open?.leadingTrivia;
668!
1111
    }
1112

1113
    public get endTrivia(): Token[] {
1114
        return this.tokens.close?.leadingTrivia;
1!
1115
    }
1116

1117
}
1118

1119
export class UnaryExpression extends Expression {
1✔
1120
    constructor(options: {
1121
        operator: Token;
1122
        right: Expression;
1123
    }) {
1124
        super();
62✔
1125
        this.tokens = {
62✔
1126
            operator: options.operator
1127
        };
1128
        this.right = options.right;
62✔
1129
        this.location = util.createBoundingLocation(this.tokens.operator, this.right);
62✔
1130
    }
1131

1132
    public readonly kind = AstNodeKind.UnaryExpression;
62✔
1133

1134
    public readonly location: Location | undefined;
1135

1136
    public readonly tokens: {
1137
        readonly operator: Token;
1138
    };
1139
    public readonly right: Expression;
1140

1141
    transpile(state: BrsTranspileState) {
1142
        let separatingWhitespace: string | undefined;
1143
        if (isVariableExpression(this.right)) {
12✔
1144
            separatingWhitespace = this.right.tokens.name.leadingWhitespace;
6✔
1145
        } else if (isLiteralExpression(this.right)) {
6✔
1146
            separatingWhitespace = this.right.tokens.value.leadingWhitespace;
2✔
1147
        } else {
1148
            separatingWhitespace = ' ';
4✔
1149
        }
1150

1151
        return [
12✔
1152
            state.transpileToken(this.tokens.operator),
1153
            separatingWhitespace,
1154
            ...this.right.transpile(state)
1155
        ];
1156
    }
1157

1158
    walk(visitor: WalkVisitor, options: WalkOptions) {
1159
        if (options.walkMode & InternalWalkMode.walkExpressions) {
233!
1160
            walk(this, 'right', visitor, options);
233✔
1161
        }
1162
    }
1163

1164
    getType(options: GetTypeOptions): BscType {
1165
        return util.unaryOperatorResultType(this.tokens.operator, this.right.getType(options));
48✔
1166
    }
1167

1168
    public get leadingTrivia(): Token[] {
1169
        return this.tokens.operator.leadingTrivia;
185✔
1170
    }
1171
}
1172

1173
export class VariableExpression extends Expression {
1✔
1174
    constructor(options: {
1175
        name: Identifier;
1176
    }) {
1177
        super();
10,173✔
1178
        this.tokens = {
10,173✔
1179
            name: options.name
1180
        };
1181
        this.location = this.tokens.name?.location;
10,173!
1182
    }
1183

1184
    public readonly tokens: {
1185
        readonly name: Identifier;
1186
    };
1187

1188
    public readonly kind = AstNodeKind.VariableExpression;
10,173✔
1189

1190
    public readonly location: Location;
1191

1192
    public getName(parseMode?: ParseMode) {
1193
        return this.tokens.name.text;
20,485✔
1194
    }
1195

1196
    transpile(state: BrsTranspileState) {
1197
        let result: TranspileResult = [];
5,928✔
1198
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
5,928✔
1199
        //if the callee is the name of a known namespace function
1200
        if (namespace && util.isCalleeMemberOfNamespace(this.tokens.name.text, this, namespace)) {
5,928✔
1201
            result.push(
8✔
1202
                //transpile leading comments since the token isn't being transpiled directly
1203
                ...state.transpileLeadingCommentsForAstNode(this),
1204
                state.sourceNode(this, [
1205
                    namespace.getName(ParseMode.BrightScript),
1206
                    '_',
1207
                    this.getName(ParseMode.BrightScript)
1208
                ])
1209
            );
1210
            //transpile  normally
1211
        } else {
1212
            result.push(
5,920✔
1213
                state.transpileToken(this.tokens.name)
1214
            );
1215
        }
1216
        return result;
5,928✔
1217
    }
1218

1219
    walk(visitor: WalkVisitor, options: WalkOptions) {
1220
        //nothing to walk
1221
    }
1222

1223

1224
    getType(options: GetTypeOptions) {
1225
        let resultType: BscType = util.tokenToBscType(this.tokens.name);
20,355✔
1226
        const nameKey = this.getName();
20,355✔
1227
        if (!resultType) {
20,355✔
1228
            const symbolTable = this.getSymbolTable();
16,400✔
1229
            resultType = symbolTable?.getSymbolType(nameKey, { ...options, fullName: nameKey, tableProvider: () => this.getSymbolTable() });
32,988!
1230

1231
            if (util.isClassUsedAsFunction(resultType, this, options)) {
16,400✔
1232
                resultType = FunctionType.instance;
20✔
1233
            }
1234

1235
        }
1236
        options?.typeChain?.push(new TypeChainEntry({ name: nameKey, type: resultType, data: options.data, astNode: this }));
20,355!
1237
        return resultType;
20,355✔
1238
    }
1239

1240
    get leadingTrivia(): Token[] {
1241
        return this.tokens.name.leadingTrivia;
34,409✔
1242
    }
1243
}
1244

1245
export class SourceLiteralExpression extends Expression {
1✔
1246
    constructor(options: {
1247
        value: Token;
1248
    }) {
1249
        super();
35✔
1250
        this.tokens = {
35✔
1251
            value: options.value
1252
        };
1253
        this.location = this.tokens.value?.location;
35!
1254
    }
1255

1256
    public readonly location: Location;
1257

1258
    public readonly kind = AstNodeKind.SourceLiteralExpression;
35✔
1259

1260
    public readonly tokens: {
1261
        readonly value: Token;
1262
    };
1263

1264
    /**
1265
     * Find the index of the function in its parent
1266
     */
1267
    private findFunctionIndex(parentFunction: FunctionExpression, func: FunctionExpression) {
1268
        let index = -1;
4✔
1269
        parentFunction.findChild((node) => {
4✔
1270
            if (isFunctionExpression(node)) {
12✔
1271
                index++;
4✔
1272
                if (node === func) {
4!
1273
                    return true;
4✔
1274
                }
1275
            }
1276
        }, {
1277
            walkMode: WalkMode.visitAllRecursive
1278
        });
1279
        return index;
4✔
1280
    }
1281

1282
    private getFunctionName(state: BrsTranspileState, parseMode: ParseMode) {
1283
        let func = this.findAncestor<FunctionExpression>(isFunctionExpression);
8✔
1284
        let nameParts = [] as TranspileResult;
8✔
1285
        let parentFunction: FunctionExpression;
1286
        while ((parentFunction = func.findAncestor<FunctionExpression>(isFunctionExpression))) {
8✔
1287
            let index = this.findFunctionIndex(parentFunction, func);
4✔
1288
            nameParts.unshift(`anon${index}`);
4✔
1289
            func = parentFunction;
4✔
1290
        }
1291
        //get the index of this function in its parent
1292
        if (isFunctionStatement(func.parent)) {
8!
1293
            nameParts.unshift(
8✔
1294
                func.parent.getName(parseMode)
1295
            );
1296
        }
1297
        return nameParts.join('$');
8✔
1298
    }
1299

1300
    /**
1301
     * Get the line number from our token or from the closest ancestor that has a range
1302
     */
1303
    private getClosestLineNumber() {
1304
        let node: AstNode = this;
7✔
1305
        while (node) {
7✔
1306
            if (node.location?.range) {
17✔
1307
                return node.location.range.start.line + 1;
5✔
1308
            }
1309
            node = node.parent;
12✔
1310
        }
1311
        return -1;
2✔
1312
    }
1313

1314
    transpile(state: BrsTranspileState) {
1315
        let text: string;
1316
        switch (this.tokens.value.kind) {
31✔
1317
            case TokenKind.SourceFilePathLiteral:
40✔
1318
                const pathUrl = fileUrl(state.srcPath);
3✔
1319
                text = `"${pathUrl.substring(0, 4)}" + "${pathUrl.substring(4)}"`;
3✔
1320
                break;
3✔
1321
            case TokenKind.SourceLineNumLiteral:
1322
                //TODO find first parent that has range, or default to -1
1323
                text = `${this.getClosestLineNumber()}`;
4✔
1324
                break;
4✔
1325
            case TokenKind.FunctionNameLiteral:
1326
                text = `"${this.getFunctionName(state, ParseMode.BrightScript)}"`;
4✔
1327
                break;
4✔
1328
            case TokenKind.SourceFunctionNameLiteral:
1329
                text = `"${this.getFunctionName(state, ParseMode.BrighterScript)}"`;
4✔
1330
                break;
4✔
1331
            case TokenKind.SourceLocationLiteral:
1332
                const locationUrl = fileUrl(state.srcPath);
3✔
1333
                //TODO find first parent that has range, or default to -1
1334
                text = `"${locationUrl.substring(0, 4)}" + "${locationUrl.substring(4)}:${this.getClosestLineNumber()}"`;
3✔
1335
                break;
3✔
1336
            case TokenKind.PkgPathLiteral:
1337
                text = `"${util.sanitizePkgPath(state.file.pkgPath)}"`;
2✔
1338
                break;
2✔
1339
            case TokenKind.PkgLocationLiteral:
1340
                text = `"${util.sanitizePkgPath(state.file.pkgPath)}:" + str(LINE_NUM)`;
2✔
1341
                break;
2✔
1342
            case TokenKind.LineNumLiteral:
1343
            default:
1344
                //use the original text (because it looks like a variable)
1345
                text = this.tokens.value.text;
9✔
1346
                break;
9✔
1347

1348
        }
1349
        return [
31✔
1350
            state.sourceNode(this, text)
1351
        ];
1352
    }
1353

1354
    walk(visitor: WalkVisitor, options: WalkOptions) {
1355
        //nothing to walk
1356
    }
1357

1358
    get leadingTrivia(): Token[] {
1359
        return this.tokens.value.leadingTrivia;
152✔
1360
    }
1361
}
1362

1363
/**
1364
 * This expression transpiles and acts exactly like a CallExpression,
1365
 * except we need to uniquely identify these statements so we can
1366
 * do more type checking.
1367
 */
1368
export class NewExpression extends Expression {
1✔
1369
    constructor(options: {
1370
        new?: Token;
1371
        call: CallExpression;
1372
    }) {
1373
        super();
128✔
1374
        this.tokens = {
128✔
1375
            new: options.new
1376
        };
1377
        this.call = options.call;
128✔
1378
        this.location = util.createBoundingLocation(this.tokens.new, this.call);
128✔
1379
    }
1380

1381
    public readonly kind = AstNodeKind.NewExpression;
128✔
1382

1383
    public readonly location: Location | undefined;
1384

1385
    public readonly tokens: {
1386
        readonly new?: Token;
1387
    };
1388
    public readonly call: CallExpression;
1389

1390
    /**
1391
     * The name of the class to initialize (with optional namespace prefixed)
1392
     */
1393
    public get className() {
1394
        //the parser guarantees the callee of a new statement's call object will be
1395
        //either a VariableExpression or a DottedGet
1396
        return this.call.callee as (VariableExpression | DottedGetExpression);
92✔
1397
    }
1398

1399
    public transpile(state: BrsTranspileState) {
1400
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
15✔
1401
        const cls = state.file.getClassFileLink(
15✔
1402
            this.className.getName(ParseMode.BrighterScript),
1403
            namespace?.getName(ParseMode.BrighterScript)
45✔
1404
        )?.item;
15✔
1405
        //new statements within a namespace block can omit the leading namespace if the class resides in that same namespace.
1406
        //So we need to figure out if this is a namespace-omitted class, or if this class exists without a namespace.
1407
        return this.call.transpile(state, cls?.getName(ParseMode.BrightScript));
15✔
1408
    }
1409

1410
    walk(visitor: WalkVisitor, options: WalkOptions) {
1411
        if (options.walkMode & InternalWalkMode.walkExpressions) {
813!
1412
            walk(this, 'call', visitor, options);
813✔
1413
        }
1414
    }
1415

1416
    getType(options: GetTypeOptions) {
1417
        const result = this.call.getType(options);
320✔
1418
        if (options.typeChain) {
320✔
1419
            // modify last typechain entry to show it is a new ...()
1420
            const lastEntry = options.typeChain[options.typeChain.length - 1];
3✔
1421
            if (lastEntry) {
3!
1422
                lastEntry.astNode = this;
3✔
1423
            }
1424
        }
1425
        return result;
320✔
1426
    }
1427

1428
    get leadingTrivia(): Token[] {
1429
        return this.tokens.new.leadingTrivia;
571✔
1430
    }
1431
}
1432

1433
export class CallfuncExpression extends Expression {
1✔
1434
    constructor(options: {
1435
        callee: Expression;
1436
        operator?: Token;
1437
        methodName: Identifier;
1438
        openingParen?: Token;
1439
        args?: Expression[];
1440
        closingParen?: Token;
1441
    }) {
1442
        super();
26✔
1443
        this.tokens = {
26✔
1444
            operator: options.operator,
1445
            methodName: options.methodName,
1446
            openingParen: options.openingParen,
1447
            closingParen: options.closingParen
1448
        };
1449
        this.callee = options.callee;
26✔
1450
        this.args = options.args ?? [];
26!
1451

1452
        this.location = util.createBoundingLocation(
26✔
1453
            this.callee,
1454
            this.tokens.operator,
1455
            this.tokens.methodName,
1456
            this.tokens.openingParen,
1457
            ...this.args,
1458
            this.tokens.closingParen
1459
        );
1460
    }
1461

1462
    public readonly callee: Expression;
1463
    public readonly args: Expression[];
1464

1465
    public readonly tokens: {
1466
        readonly operator: Token;
1467
        readonly methodName: Identifier;
1468
        readonly openingParen?: Token;
1469
        readonly closingParen?: Token;
1470
    };
1471

1472
    public readonly kind = AstNodeKind.CallfuncExpression;
26✔
1473

1474
    public readonly location: Location | undefined;
1475

1476
    public transpile(state: BrsTranspileState) {
1477
        let result = [] as TranspileResult;
9✔
1478
        result.push(
9✔
1479
            ...this.callee.transpile(state),
1480
            state.sourceNode(this.tokens.operator, '.callfunc'),
1481
            state.transpileToken(this.tokens.openingParen, '('),
1482
            //the name of the function
1483
            state.sourceNode(this.tokens.methodName, ['"', this.tokens.methodName.text, '"'])
1484
        );
1485
        if (this.args?.length > 0) {
9!
1486
            result.push(', ');
4✔
1487
            //transpile args
1488
            for (let i = 0; i < this.args.length; i++) {
4✔
1489
                //add comma between args
1490
                if (i > 0) {
7✔
1491
                    result.push(', ');
3✔
1492
                }
1493
                let arg = this.args[i];
7✔
1494
                result.push(...arg.transpile(state));
7✔
1495
            }
1496
        } else if (state.options.legacyCallfuncHandling) {
5✔
1497
            result.push(', ', 'invalid');
2✔
1498
        }
1499

1500
        result.push(
9✔
1501
            state.transpileToken(this.tokens.closingParen, ')')
1502
        );
1503
        return result;
9✔
1504
    }
1505

1506
    walk(visitor: WalkVisitor, options: WalkOptions) {
1507
        if (options.walkMode & InternalWalkMode.walkExpressions) {
132!
1508
            walk(this, 'callee', visitor, options);
132✔
1509
            walkArray(this.args, visitor, options, this);
132✔
1510
        }
1511
    }
1512

1513
    getType(options: GetTypeOptions) {
1514
        let result: BscType = DynamicType.instance;
4✔
1515
        // a little hacky here with checking options.ignoreCall because callFuncExpression has the method name
1516
        // It's nicer for CallExpression, because it's a call on any expression.
1517
        const calleeType = this.callee.getType({ ...options, flags: SymbolTypeFlag.runtime });
4✔
1518
        if (isComponentType(calleeType) || isReferenceType(calleeType)) {
4!
1519
            const funcType = (calleeType as ComponentType).getCallFuncType?.(this.tokens.methodName.text, options);
4!
1520
            if (funcType) {
4✔
1521
                options.typeChain?.push(new TypeChainEntry({
3✔
1522
                    name: this.tokens.methodName.text,
1523
                    type: funcType,
1524
                    data: options.data,
1525
                    location: this.tokens.methodName.location,
1526
                    separatorToken: createToken(TokenKind.Callfunc),
1527
                    astNode: this
1528
                }));
1529
                if (options.ignoreCall) {
3✔
1530
                    result = funcType;
1✔
1531
                }
1532
            }
1533
            /* TODO:
1534
                make callfunc return types work
1535
            else if (isCallableType(funcType) && (!isReferenceType(funcType.returnType) || funcType.returnType.isResolvable())) {
1536
                result = funcType.returnType;
1537
            } else if (!isReferenceType(funcType) && (funcType as any)?.returnType?.isResolvable()) {
1538
                result = (funcType as any).returnType;
1539
            } else {
1540
                return new TypePropertyReferenceType(funcType, 'returnType');
1541
            }
1542
            */
1543
        }
1544

1545
        return result;
4✔
1546
    }
1547

1548
    get leadingTrivia(): Token[] {
1549
        return this.callee.leadingTrivia;
233✔
1550
    }
1551
}
1552

1553
/**
1554
 * Since template strings can contain newlines, we need to concatenate multiple strings together with chr() calls.
1555
 * This is a single expression that represents the string contatenation of all parts of a single quasi.
1556
 */
1557
export class TemplateStringQuasiExpression extends Expression {
1✔
1558
    constructor(options: {
1559
        expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>;
1560
    }) {
1561
        super();
74✔
1562
        this.expressions = options.expressions;
74✔
1563
        this.location = util.createBoundingLocation(
74✔
1564
            ...this.expressions
1565
        );
1566
    }
1567

1568
    public readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>;
1569
    public readonly kind = AstNodeKind.TemplateStringQuasiExpression;
74✔
1570

1571
    readonly location: Location | undefined;
1572

1573
    transpile(state: BrsTranspileState, skipEmptyStrings = true) {
35✔
1574
        let result = [] as TranspileResult;
43✔
1575
        let plus = '';
43✔
1576
        for (let expression of this.expressions) {
43✔
1577
            //skip empty strings
1578
            //TODO what does an empty string literal expression look like?
1579
            if (expression.tokens.value.text === '' && skipEmptyStrings === true) {
68✔
1580
                continue;
27✔
1581
            }
1582
            result.push(
41✔
1583
                plus,
1584
                ...expression.transpile(state)
1585
            );
1586
            plus = ' + ';
41✔
1587
        }
1588
        return result;
43✔
1589
    }
1590

1591
    walk(visitor: WalkVisitor, options: WalkOptions) {
1592
        if (options.walkMode & InternalWalkMode.walkExpressions) {
314!
1593
            walkArray(this.expressions, visitor, options, this);
314✔
1594
        }
1595
    }
1596
}
1597

1598
export class TemplateStringExpression extends Expression {
1✔
1599
    constructor(options: {
1600
        openingBacktick?: Token;
1601
        quasis: TemplateStringQuasiExpression[];
1602
        expressions: Expression[];
1603
        closingBacktick?: Token;
1604
    }) {
1605
        super();
35✔
1606
        this.tokens = {
35✔
1607
            openingBacktick: options.openingBacktick,
1608
            closingBacktick: options.closingBacktick
1609
        };
1610
        this.quasis = options.quasis;
35✔
1611
        this.expressions = options.expressions;
35✔
1612
        this.location = util.createBoundingLocation(
35✔
1613
            this.tokens.openingBacktick,
1614
            this.quasis[0],
1615
            this.quasis[this.quasis.length - 1],
1616
            this.tokens.closingBacktick
1617
        );
1618
    }
1619

1620
    public readonly kind = AstNodeKind.TemplateStringExpression;
35✔
1621

1622
    public readonly tokens: {
1623
        readonly openingBacktick?: Token;
1624
        readonly closingBacktick?: Token;
1625
    };
1626
    public readonly quasis: TemplateStringQuasiExpression[];
1627
    public readonly expressions: Expression[];
1628

1629
    public readonly location: Location | undefined;
1630

1631
    public getType(options: GetTypeOptions) {
1632
        return StringType.instance;
34✔
1633
    }
1634

1635
    transpile(state: BrsTranspileState) {
1636
        if (this.quasis.length === 1 && this.expressions.length === 0) {
20✔
1637
            return this.quasis[0].transpile(state);
10✔
1638
        }
1639
        let result = ['('];
10✔
1640
        let plus = '';
10✔
1641
        //helper function to figure out when to include the plus
1642
        function add(...items) {
1643
            if (items.length > 0) {
40✔
1644
                result.push(
29✔
1645
                    plus,
1646
                    ...items
1647
                );
1648
            }
1649
            //set the plus after the first occurance of a nonzero length set of items
1650
            if (plus === '' && items.length > 0) {
40✔
1651
                plus = ' + ';
10✔
1652
            }
1653
        }
1654

1655
        for (let i = 0; i < this.quasis.length; i++) {
10✔
1656
            let quasi = this.quasis[i];
25✔
1657
            let expression = this.expressions[i];
25✔
1658

1659
            add(
25✔
1660
                ...quasi.transpile(state)
1661
            );
1662
            if (expression) {
25✔
1663
                //skip the toString wrapper around certain expressions
1664
                if (
15✔
1665
                    isEscapedCharCodeLiteralExpression(expression) ||
34✔
1666
                    (isLiteralExpression(expression) && isStringType(expression.getType()))
1667
                ) {
1668
                    add(
3✔
1669
                        ...expression.transpile(state)
1670
                    );
1671

1672
                    //wrap all other expressions with a bslib_toString call to prevent runtime type mismatch errors
1673
                } else {
1674
                    add(
12✔
1675
                        state.bslibPrefix + '_toString(',
1676
                        ...expression.transpile(state),
1677
                        ')'
1678
                    );
1679
                }
1680
            }
1681
        }
1682
        //the expression should be wrapped in parens so it can be used line a single expression at runtime
1683
        result.push(')');
10✔
1684

1685
        return result;
10✔
1686
    }
1687

1688
    walk(visitor: WalkVisitor, options: WalkOptions) {
1689
        if (options.walkMode & InternalWalkMode.walkExpressions) {
152!
1690
            //walk the quasis and expressions in left-to-right order
1691
            for (let i = 0; i < this.quasis.length; i++) {
152✔
1692
                walk(this.quasis, i, visitor, options, this);
257✔
1693

1694
                //this skips the final loop iteration since we'll always have one more quasi than expression
1695
                if (this.expressions[i]) {
257✔
1696
                    walk(this.expressions, i, visitor, options, this);
105✔
1697
                }
1698
            }
1699
        }
1700
    }
1701
}
1702

1703
export class TaggedTemplateStringExpression extends Expression {
1✔
1704
    constructor(options: {
1705
        tagName: Identifier;
1706
        openingBacktick?: Token;
1707
        quasis: TemplateStringQuasiExpression[];
1708
        expressions: Expression[];
1709
        closingBacktick?: Token;
1710
    }) {
1711
        super();
6✔
1712
        this.tokens = {
6✔
1713
            tagName: options.tagName,
1714
            openingBacktick: options.openingBacktick,
1715
            closingBacktick: options.closingBacktick
1716
        };
1717
        this.quasis = options.quasis;
6✔
1718
        this.expressions = options.expressions;
6✔
1719

1720
        this.location = util.createBoundingLocation(
6✔
1721
            this.tokens.tagName,
1722
            this.tokens.openingBacktick,
1723
            this.quasis[0],
1724
            this.quasis[this.quasis.length - 1],
1725
            this.tokens.closingBacktick
1726
        );
1727
    }
1728

1729
    public readonly kind = AstNodeKind.TaggedTemplateStringExpression;
6✔
1730

1731
    public readonly tokens: {
1732
        readonly tagName: Identifier;
1733
        readonly openingBacktick?: Token;
1734
        readonly closingBacktick?: Token;
1735
    };
1736

1737
    public readonly quasis: TemplateStringQuasiExpression[];
1738
    public readonly expressions: Expression[];
1739

1740
    public readonly location: Location | undefined;
1741

1742
    transpile(state: BrsTranspileState) {
1743
        let result = [] as TranspileResult;
3✔
1744
        result.push(
3✔
1745
            state.transpileToken(this.tokens.tagName),
1746
            '(['
1747
        );
1748

1749
        //add quasis as the first array
1750
        for (let i = 0; i < this.quasis.length; i++) {
3✔
1751
            let quasi = this.quasis[i];
8✔
1752
            //separate items with a comma
1753
            if (i > 0) {
8✔
1754
                result.push(
5✔
1755
                    ', '
1756
                );
1757
            }
1758
            result.push(
8✔
1759
                ...quasi.transpile(state, false)
1760
            );
1761
        }
1762
        result.push(
3✔
1763
            '], ['
1764
        );
1765

1766
        //add expressions as the second array
1767
        for (let i = 0; i < this.expressions.length; i++) {
3✔
1768
            let expression = this.expressions[i];
5✔
1769
            if (i > 0) {
5✔
1770
                result.push(
2✔
1771
                    ', '
1772
                );
1773
            }
1774
            result.push(
5✔
1775
                ...expression.transpile(state)
1776
            );
1777
        }
1778
        result.push(
3✔
1779
            state.sourceNode(this.tokens.closingBacktick, '])')
1780
        );
1781
        return result;
3✔
1782
    }
1783

1784
    walk(visitor: WalkVisitor, options: WalkOptions) {
1785
        if (options.walkMode & InternalWalkMode.walkExpressions) {
23!
1786
            //walk the quasis and expressions in left-to-right order
1787
            for (let i = 0; i < this.quasis.length; i++) {
23✔
1788
                walk(this.quasis, i, visitor, options, this);
57✔
1789

1790
                //this skips the final loop iteration since we'll always have one more quasi than expression
1791
                if (this.expressions[i]) {
57✔
1792
                    walk(this.expressions, i, visitor, options, this);
34✔
1793
                }
1794
            }
1795
        }
1796
    }
1797
}
1798

1799
export class AnnotationExpression extends Expression {
1✔
1800
    constructor(options: {
1801
        at?: Token;
1802
        name: Token;
1803
        call?: CallExpression;
1804
    }) {
1805
        super();
68✔
1806
        this.tokens = {
68✔
1807
            at: options.at,
1808
            name: options.name
1809
        };
1810
        this.call = options.call;
68✔
1811
        this.name = this.tokens.name.text;
68✔
1812
    }
1813

1814
    public readonly kind = AstNodeKind.AnnotationExpression;
68✔
1815

1816
    public readonly tokens: {
1817
        readonly at: Token;
1818
        readonly name: Token;
1819
    };
1820

1821
    public get location(): Location | undefined {
1822
        return util.createBoundingLocation(
51✔
1823
            this.tokens.at,
1824
            this.tokens.name,
1825
            this.call
1826
        );
1827
    }
1828

1829
    public readonly name: string;
1830

1831
    public call: CallExpression;
1832

1833
    /**
1834
     * Convert annotation arguments to JavaScript types
1835
     * @param strict If false, keep Expression objects not corresponding to JS types
1836
     */
1837
    getArguments(strict = true): ExpressionValue[] {
10✔
1838
        if (!this.call) {
11✔
1839
            return [];
1✔
1840
        }
1841
        return this.call.args.map(e => expressionToValue(e, strict));
20✔
1842
    }
1843

1844
    public get leadingTrivia(): Token[] {
1845
        return this.tokens.at?.leadingTrivia;
50!
1846
    }
1847

1848
    transpile(state: BrsTranspileState) {
1849
        //transpile only our leading comments
1850
        return state.transpileComments(this.leadingTrivia);
16✔
1851
    }
1852

1853
    walk(visitor: WalkVisitor, options: WalkOptions) {
1854
        //nothing to walk
1855
    }
1856
    getTypedef(state: BrsTranspileState) {
1857
        return [
9✔
1858
            '@',
1859
            this.name,
1860
            ...(this.call?.transpile(state) ?? [])
54✔
1861
        ];
1862
    }
1863
}
1864

1865
export class TernaryExpression extends Expression {
1✔
1866
    constructor(options: {
1867
        test: Expression;
1868
        questionMark?: Token;
1869
        consequent?: Expression;
1870
        colon?: Token;
1871
        alternate?: Expression;
1872
    }) {
1873
        super();
76✔
1874
        this.tokens = {
76✔
1875
            questionMark: options.questionMark,
1876
            colon: options.colon
1877
        };
1878
        this.test = options.test;
76✔
1879
        this.consequent = options.consequent;
76✔
1880
        this.alternate = options.alternate;
76✔
1881
        this.location = util.createBoundingLocation(
76✔
1882
            this.test,
1883
            this.tokens.questionMark,
1884
            this.consequent,
1885
            this.tokens.colon,
1886
            this.alternate
1887
        );
1888
    }
1889

1890
    public readonly kind = AstNodeKind.TernaryExpression;
76✔
1891

1892
    public readonly location: Location | undefined;
1893

1894
    public readonly tokens: {
1895
        readonly questionMark?: Token;
1896
        readonly colon?: Token;
1897
    };
1898

1899
    public readonly test: Expression;
1900
    public readonly consequent?: Expression;
1901
    public readonly alternate?: Expression;
1902

1903
    transpile(state: BrsTranspileState) {
1904
        let result = [] as TranspileResult;
29✔
1905
        const file = state.file;
29✔
1906
        let consequentInfo = util.getExpressionInfo(this.consequent!, file);
29✔
1907
        let alternateInfo = util.getExpressionInfo(this.alternate!, file);
29✔
1908

1909
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1910
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
29✔
1911
        let mutatingExpressions = [
29✔
1912
            ...consequentInfo.expressions,
1913
            ...alternateInfo.expressions
1914
        ].filter(e => e instanceof CallExpression || e instanceof CallfuncExpression || e instanceof DottedGetExpression);
134✔
1915

1916
        if (mutatingExpressions.length > 0) {
29✔
1917
            result.push(
8✔
1918
                state.sourceNode(
1919
                    this.tokens.questionMark,
1920
                    //write all the scope variables as parameters.
1921
                    //TODO handle when there are more than 31 parameters
1922
                    `(function(${['__bsCondition', ...allUniqueVarNames].join(', ')})`
1923
                ),
1924
                state.newline,
1925
                //double indent so our `end function` line is still indented one at the end
1926
                state.indent(2),
1927
                state.sourceNode(this.test, `if __bsCondition then`),
1928
                state.newline,
1929
                state.indent(1),
1930
                state.sourceNode(this.consequent ?? this.tokens.questionMark, 'return '),
24!
1931
                ...this.consequent?.transpile(state) ?? [state.sourceNode(this.tokens.questionMark, 'invalid')],
48!
1932
                state.newline,
1933
                state.indent(-1),
1934
                state.sourceNode(this.consequent ?? this.tokens.questionMark, 'else'),
24!
1935
                state.newline,
1936
                state.indent(1),
1937
                state.sourceNode(this.consequent ?? this.tokens.questionMark, 'return '),
24!
1938
                ...this.alternate?.transpile(state) ?? [state.sourceNode(this.consequent ?? this.tokens.questionMark, 'invalid')],
48!
1939
                state.newline,
1940
                state.indent(-1),
1941
                state.sourceNode(this.tokens.questionMark, 'end if'),
1942
                state.newline,
1943
                state.indent(-1),
1944
                state.sourceNode(this.tokens.questionMark, 'end function)('),
1945
                ...this.test.transpile(state),
1946
                state.sourceNode(this.tokens.questionMark, `${['', ...allUniqueVarNames].join(', ')})`)
1947
            );
1948
            state.blockDepth--;
8✔
1949
        } else {
1950
            result.push(
21✔
1951
                state.sourceNode(this.test, state.bslibPrefix + `_ternary(`),
1952
                ...this.test.transpile(state),
1953
                state.sourceNode(this.test, `, `),
1954
                ...this.consequent?.transpile(state) ?? ['invalid'],
126✔
1955
                `, `,
1956
                ...this.alternate?.transpile(state) ?? ['invalid'],
126✔
1957
                `)`
1958
            );
1959
        }
1960
        return result;
29✔
1961
    }
1962

1963
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1964
        if (options.walkMode & InternalWalkMode.walkExpressions) {
220!
1965
            walk(this, 'test', visitor, options);
220✔
1966
            walk(this, 'consequent', visitor, options);
220✔
1967
            walk(this, 'alternate', visitor, options);
220✔
1968
        }
1969
    }
1970

1971
    get leadingTrivia(): Token[] {
1972
        return this.test.leadingTrivia;
139✔
1973
    }
1974
}
1975

1976
export class NullCoalescingExpression extends Expression {
1✔
1977
    constructor(options: {
1978
        consequent: Expression;
1979
        questionQuestion?: Token;
1980
        alternate: Expression;
1981
    }) {
1982
        super();
32✔
1983
        this.tokens = {
32✔
1984
            questionQuestion: options.questionQuestion
1985
        };
1986
        this.consequent = options.consequent;
32✔
1987
        this.alternate = options.alternate;
32✔
1988
        this.location = util.createBoundingLocation(
32✔
1989
            this.consequent,
1990
            this.tokens.questionQuestion,
1991
            this.alternate
1992
        );
1993
    }
1994

1995
    public readonly kind = AstNodeKind.NullCoalescingExpression;
32✔
1996

1997
    public readonly location: Location | undefined;
1998

1999
    public readonly tokens: {
2000
        readonly questionQuestion?: Token;
2001
    };
2002

2003
    public readonly consequent: Expression;
2004
    public readonly alternate: Expression;
2005

2006
    transpile(state: BrsTranspileState) {
2007
        let result = [] as TranspileResult;
10✔
2008
        let consequentInfo = util.getExpressionInfo(this.consequent, state.file);
10✔
2009
        let alternateInfo = util.getExpressionInfo(this.alternate, state.file);
10✔
2010

2011
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
2012
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
10✔
2013
        let hasMutatingExpression = [
10✔
2014
            ...consequentInfo.expressions,
2015
            ...alternateInfo.expressions
2016
        ].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e));
28✔
2017

2018
        if (hasMutatingExpression) {
10✔
2019
            result.push(
6✔
2020
                `(function(`,
2021
                //write all the scope variables as parameters.
2022
                //TODO handle when there are more than 31 parameters
2023
                allUniqueVarNames.join(', '),
2024
                ')',
2025
                state.newline,
2026
                //double indent so our `end function` line is still indented one at the end
2027
                state.indent(2),
2028
                //evaluate the consequent exactly once, and then use it in the following condition
2029
                `__bsConsequent = `,
2030
                ...this.consequent.transpile(state),
2031
                state.newline,
2032
                state.indent(),
2033
                `if __bsConsequent <> invalid then`,
2034
                state.newline,
2035
                state.indent(1),
2036
                'return __bsConsequent',
2037
                state.newline,
2038
                state.indent(-1),
2039
                'else',
2040
                state.newline,
2041
                state.indent(1),
2042
                'return ',
2043
                ...this.alternate.transpile(state),
2044
                state.newline,
2045
                state.indent(-1),
2046
                'end if',
2047
                state.newline,
2048
                state.indent(-1),
2049
                'end function)(',
2050
                allUniqueVarNames.join(', '),
2051
                ')'
2052
            );
2053
            state.blockDepth--;
6✔
2054
        } else {
2055
            result.push(
4✔
2056
                state.bslibPrefix + `_coalesce(`,
2057
                ...this.consequent.transpile(state),
2058
                ', ',
2059
                ...this.alternate.transpile(state),
2060
                ')'
2061
            );
2062
        }
2063
        return result;
10✔
2064
    }
2065

2066
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2067
        if (options.walkMode & InternalWalkMode.walkExpressions) {
82!
2068
            walk(this, 'consequent', visitor, options);
82✔
2069
            walk(this, 'alternate', visitor, options);
82✔
2070
        }
2071
    }
2072

2073
    get leadingTrivia(): Token[] {
2074
        return this.consequent.leadingTrivia;
50✔
2075
    }
2076
}
2077

2078
export class RegexLiteralExpression extends Expression {
1✔
2079
    constructor(options: {
2080
        regexLiteral: Token;
2081
    }) {
2082
        super();
44✔
2083
        this.tokens = {
44✔
2084
            regexLiteral: options.regexLiteral
2085
        };
2086
    }
2087

2088
    public readonly kind = AstNodeKind.RegexLiteralExpression;
44✔
2089
    public readonly tokens: {
2090
        readonly regexLiteral: Token;
2091
    };
2092

2093
    public get location(): Location {
2094
        return this.tokens?.regexLiteral?.location;
79!
2095
    }
2096

2097
    public transpile(state: BrsTranspileState): TranspileResult {
2098
        let text = this.tokens.regexLiteral?.text ?? '';
42!
2099
        let flags = '';
42✔
2100
        //get any flags from the end
2101
        const flagMatch = /\/([a-z]+)$/i.exec(text);
42✔
2102
        if (flagMatch) {
42✔
2103
            text = text.substring(0, flagMatch.index + 1);
2✔
2104
            flags = flagMatch[1];
2✔
2105
        }
2106
        let pattern = text
42✔
2107
            //remove leading and trailing slashes
2108
            .substring(1, text.length - 1)
2109
            //escape quotemarks
2110
            .split('"').join('" + chr(34) + "');
2111

2112
        return [
42✔
2113
            state.sourceNode(this.tokens.regexLiteral, [
2114
                'CreateObject("roRegex", ',
2115
                `"${pattern}", `,
2116
                `"${flags}"`,
2117
                ')'
2118
            ])
2119
        ];
2120
    }
2121

2122
    walk(visitor: WalkVisitor, options: WalkOptions) {
2123
        //nothing to walk
2124
    }
2125

2126
    get leadingTrivia(): Token[] {
2127
        return this.tokens.regexLiteral?.leadingTrivia;
1,023!
2128
    }
2129
}
2130

2131
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
2132
type ExpressionValue = string | number | boolean | Expression | ExpressionValue[] | { [key: string]: ExpressionValue } | null;
2133

2134
function expressionToValue(expr: Expression, strict: boolean): ExpressionValue {
2135
    if (!expr) {
30!
2136
        return null;
×
2137
    }
2138
    if (isUnaryExpression(expr) && isLiteralNumber(expr.right)) {
30✔
2139
        return numberExpressionToValue(expr.right, expr.tokens.operator.text);
1✔
2140
    }
2141
    if (isLiteralString(expr)) {
29✔
2142
        //remove leading and trailing quotes
2143
        return expr.tokens.value.text.replace(/^"/, '').replace(/"$/, '');
5✔
2144
    }
2145
    if (isLiteralNumber(expr)) {
24✔
2146
        return numberExpressionToValue(expr);
11✔
2147
    }
2148

2149
    if (isLiteralBoolean(expr)) {
13✔
2150
        return expr.tokens.value.text.toLowerCase() === 'true';
3✔
2151
    }
2152
    if (isArrayLiteralExpression(expr)) {
10✔
2153
        return expr.elements
3✔
2154
            .map(e => expressionToValue(e, strict));
7✔
2155
    }
2156
    if (isAALiteralExpression(expr)) {
7✔
2157
        return expr.elements.reduce((acc, e) => {
3✔
2158
            acc[e.tokens.key.text] = expressionToValue(e.value, strict);
3✔
2159
            return acc;
3✔
2160
        }, {});
2161
    }
2162
    //for annotations, we only support serializing pure string values
2163
    if (isTemplateStringExpression(expr)) {
4✔
2164
        if (expr.quasis?.length === 1 && expr.expressions.length === 0) {
2!
2165
            return expr.quasis[0].expressions.map(x => x.tokens.value.text).join('');
10✔
2166
        }
2167
    }
2168
    return strict ? null : expr;
2✔
2169
}
2170

2171
function numberExpressionToValue(expr: LiteralExpression, operator = '') {
11✔
2172
    if (isIntegerType(expr.getType()) || isLongIntegerType(expr.getType())) {
12!
2173
        return parseInt(operator + expr.tokens.value.text);
12✔
2174
    } else {
NEW
2175
        return parseFloat(operator + expr.tokens.value.text);
×
2176
    }
2177
}
2178

2179
export class TypeExpression extends Expression implements TypedefProvider {
1✔
2180
    constructor(options: {
2181
        /**
2182
         * The standard AST expression that represents the type for this TypeExpression.
2183
         */
2184
        expression: Expression;
2185
    }) {
2186
        super();
1,422✔
2187
        this.expression = options.expression;
1,422✔
2188
        this.location = this.expression?.location;
1,422!
2189
    }
2190

2191
    public readonly kind = AstNodeKind.TypeExpression;
1,422✔
2192

2193
    /**
2194
     * The standard AST expression that represents the type for this TypeExpression.
2195
     */
2196
    public readonly expression: Expression;
2197

2198
    public readonly location: Location;
2199

2200
    public transpile(state: BrsTranspileState): TranspileResult {
2201
        return [this.getType({ flags: SymbolTypeFlag.typetime }).toTypeString()];
100✔
2202
    }
2203
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2204
        if (options.walkMode & InternalWalkMode.walkExpressions) {
6,617✔
2205
            walk(this, 'expression', visitor, options);
6,492✔
2206
        }
2207
    }
2208

2209
    public getType(options: GetTypeOptions): BscType {
2210
        return this.expression.getType({ ...options, flags: SymbolTypeFlag.typetime });
4,903✔
2211
    }
2212

2213
    getTypedef(state: TranspileState): TranspileResult {
2214
        // TypeDefs should pass through any valid type names
2215
        return this.expression.transpile(state as BrsTranspileState);
33✔
2216
    }
2217

2218
    getName(parseMode = ParseMode.BrighterScript): string {
195✔
2219
        //TODO: this may not support Complex Types, eg. generics or Unions
2220
        return util.getAllDottedGetPartsAsString(this.expression, parseMode);
195✔
2221
    }
2222

2223
    getNameParts(): string[] {
2224
        //TODO: really, this code is only used to get Namespaces. It could be more clear.
2225
        return util.getAllDottedGetParts(this.expression).map(x => x.text);
20✔
2226
    }
2227
}
2228

2229
export class TypecastExpression extends Expression {
1✔
2230
    constructor(options: {
2231
        obj: Expression;
2232
        as?: Token;
2233
        typeExpression?: TypeExpression;
2234
    }) {
2235
        super();
63✔
2236
        this.tokens = {
63✔
2237
            as: options.as
2238
        };
2239
        this.obj = options.obj;
63✔
2240
        this.typeExpression = options.typeExpression;
63✔
2241
        this.location = util.createBoundingLocation(
63✔
2242
            this.obj,
2243
            this.tokens.as,
2244
            this.typeExpression
2245
        );
2246
    }
2247

2248
    public readonly kind = AstNodeKind.TypecastExpression;
63✔
2249

2250
    public readonly obj: Expression;
2251

2252
    public readonly tokens: {
2253
        readonly as?: Token;
2254
    };
2255

2256
    public typeExpression?: TypeExpression;
2257

2258
    public readonly location: Location;
2259

2260
    public transpile(state: BrsTranspileState): TranspileResult {
2261
        return this.obj.transpile(state);
13✔
2262
    }
2263
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2264
        if (options.walkMode & InternalWalkMode.walkExpressions) {
315!
2265
            walk(this, 'obj', visitor, options);
315✔
2266
            walk(this, 'typeExpression', visitor, options);
315✔
2267
        }
2268
    }
2269

2270
    public getType(options: GetTypeOptions): BscType {
2271
        const result = this.typeExpression.getType(options);
80✔
2272
        if (options.typeChain) {
80✔
2273
            // modify last typechain entry to show it is a typecast
2274
            const lastEntry = options.typeChain[options.typeChain.length - 1];
18✔
2275
            if (lastEntry) {
18!
2276
                lastEntry.astNode = this;
18✔
2277
            }
2278
        }
2279
        return result;
80✔
2280
    }
2281
}
2282

2283
export class TypedArrayExpression extends Expression {
1✔
2284
    constructor(options: {
2285
        innerType: Expression;
2286
        leftBracket?: Token;
2287
        rightBracket?: Token;
2288
    }) {
2289
        super();
29✔
2290
        this.tokens = {
29✔
2291
            leftBracket: options.leftBracket,
2292
            rightBracket: options.rightBracket
2293
        };
2294
        this.innerType = options.innerType;
29✔
2295
        this.location = util.createBoundingLocation(
29✔
2296
            this.innerType,
2297
            this.tokens.leftBracket,
2298
            this.tokens.rightBracket
2299
        );
2300
    }
2301

2302
    public readonly tokens: {
2303
        readonly leftBracket?: Token;
2304
        readonly rightBracket?: Token;
2305
    };
2306

2307
    public readonly innerType: Expression;
2308

2309
    public readonly kind = AstNodeKind.TypedArrayExpression;
29✔
2310

2311
    public readonly location: Location;
2312

2313
    public transpile(state: BrsTranspileState): TranspileResult {
NEW
2314
        return [this.getType({ flags: SymbolTypeFlag.typetime }).toTypeString()];
×
2315
    }
2316

2317
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2318
        if (options.walkMode & InternalWalkMode.walkExpressions) {
126!
2319
            walk(this, 'innerType', visitor, options);
126✔
2320
        }
2321
    }
2322

2323
    public getType(options: GetTypeOptions): BscType {
2324
        return new ArrayType(this.innerType.getType(options));
122✔
2325
    }
2326
}
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