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

rokucommunity / brighterscript / #13029

05 Sep 2024 06:50PM UTC coverage: 86.418%. Remained the same
#13029

push

web-flow
Merge c5c56b09a into 43cbf8b72

10784 of 13272 branches covered (81.25%)

Branch coverage included in aggregate %.

153 of 158 new or added lines in 7 files covered. (96.84%)

157 existing lines in 6 files now uncovered.

12484 of 13653 relevant lines covered (91.44%)

27479.56 hits per line

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

90.53
/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,051✔
46
        this.tokens = {
3,051✔
47
            operator: options.operator
48
        };
49
        this.left = options.left;
3,051✔
50
        this.right = options.right;
3,051✔
51
        this.location = util.createBoundingLocation(this.left, this.tokens.operator, this.right);
3,051✔
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,051✔
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,948!
76
            walk(this, 'left', visitor, options);
9,948✔
77
            walk(this, 'right', visitor, options);
9,948✔
78
        }
79
    }
80

81

82
    public getType(options: GetTypeOptions): BscType {
83
        const operatorKind = this.tokens.operator.kind;
338✔
84
        if (options.flags & SymbolTypeFlag.typetime) {
338✔
85
            // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
86
            switch (operatorKind) {
78✔
87
                case TokenKind.Or:
77✔
88
                    return new UnionType([this.left.getType(options), this.right.getType(options)]);
77✔
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[] {
UNCOV
101
        return this.left.leadingTrivia;
×
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,265✔
122
        this.tokens = {
2,265✔
123
            openingParen: options.openingParen,
124
            closingParen: options.closingParen
125
        };
126
        this.callee = options.callee;
2,265✔
127
        this.args = options.args ?? [];
2,265✔
128
        this.location = util.createBoundingLocation(this.callee, this.tokens.openingParen, ...this.args, this.tokens.closingParen);
2,265✔
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,265✔
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,544!
180
            walk(this, 'callee', visitor, options);
9,544✔
181
            walkArray(this.args, visitor, options, this);
9,544✔
182
        }
183
    }
184

185
    getType(options: GetTypeOptions) {
186
        const calleeType = this.callee.getType(options);
941✔
187
        if (options.ignoreCall) {
941!
UNCOV
188
            return calleeType;
×
189
        }
190
        if (isNewExpression(this.parent)) {
941✔
191
            return calleeType;
320✔
192
        }
193
        const specialCaseReturnType = util.getSpecialCaseCallExpressionReturnType(this);
621✔
194
        if (specialCaseReturnType) {
621✔
195
            return specialCaseReturnType;
98✔
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;
116✔
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,411✔
223
        this.tokens = {
3,411✔
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,411!
231
        this.body = options.body;
3,411✔
232
        this.returnTypeExpression = options.returnTypeExpression;
3,411✔
233
        //if there's a body, and it doesn't have a SymbolTable, assign one
234
        if (this.body) {
3,411!
235
            if (!this.body.symbolTable) {
3,411!
236
                this.body.symbolTable = new SymbolTable(`Block`, () => this.getSymbolTable());
26,368✔
237
            } else {
UNCOV
238
                this.body.symbolTable.pushParentProvider(() => this.getSymbolTable());
×
239
            }
240
            this.body.parent = this;
3,411✔
241
        }
242
        this.symbolTable = new SymbolTable('FunctionExpression', () => this.parent?.getSymbolTable());
32,715!
243
    }
244

245
    public readonly kind = AstNodeKind.FunctionExpression;
3,411✔
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;
11,235!
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,972✔
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,403!
378
            walkArray(this.parameters, visitor, options, this);
15,403✔
379
            walk(this, 'returnTypeExpression', visitor, options);
15,403✔
380
            //This is the core of full-program walking...it allows us to step into sub functions
381
            if (options.walkMode & InternalWalkMode.recurseChildFunctions) {
15,403!
382
                walk(this, 'body', visitor, options);
15,403✔
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

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

394
        returnType = util.chooseTypeFromCodeOrDocComment(
3,776✔
395
            this.returnTypeExpression?.getType({ ...options, typeChain: undefined }),
11,328✔
396
            docs.getReturnBscType(this, { ...options, tableProvider: () => this.getSymbolTable() }),
6✔
397
            options
398
        );
399

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

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

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

448
    public readonly kind = AstNodeKind.FunctionParameterExpression;
2,817✔
449

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

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

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

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

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

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

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

502
        return result;
2,074✔
503
    }
504

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

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

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

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

522
        return results;
73✔
523
    }
524

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

533
    get leadingTrivia(): Token[] {
534
        return this.tokens.name.leadingTrivia;
34✔
535
    }
536
}
537

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

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

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

563
    public readonly kind = AstNodeKind.DottedGetExpression;
2,665✔
564

565
    public readonly location: Location | undefined;
566

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

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

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

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

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

619
    get leadingTrivia(): Token[] {
620
        return this.obj.leadingTrivia;
109✔
621
    }
622

623
}
624

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

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

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

647
    public readonly obj: Expression;
648

649
    public readonly location: Location | undefined;
650

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

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

665
    get leadingTrivia(): Token[] {
UNCOV
666
        return this.obj.leadingTrivia;
×
667
    }
668
}
669

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

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

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

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

706
    public readonly location: Location | undefined;
707

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

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

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

747
    get leadingTrivia(): Token[] {
748
        return this.obj.leadingTrivia;
24✔
749
    }
750
}
751

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

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

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

775
    public readonly location: Location | undefined;
776

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

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

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

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

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

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

817
    public readonly kind = AstNodeKind.LiteralExpression;
7,070✔
818

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

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

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

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

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

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

852
    get leadingTrivia(): Token[] {
853
        return this.tokens.value.leadingTrivia;
53✔
854
    }
855
}
856

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

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

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

876
    public readonly location: Location;
877

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

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

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

904
    public readonly elements: Array<Expression>;
905

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

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

913
    public readonly location: Location | undefined;
914

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

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

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

944
        return result;
50✔
945
    }
946

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

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

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

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

984
    public readonly kind = AstNodeKind.AAMemberExpression;
264✔
985

986
    public readonly location: Location | undefined;
987

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

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

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

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

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

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

1014
}
1015

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

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

1037
    public readonly kind = AstNodeKind.AALiteralExpression;
253✔
1038

1039
    public readonly location: Location | undefined;
1040

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

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

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

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

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

1090
        return result;
55✔
1091
    }
1092

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

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

1110
    public get leadingTrivia(): Token[] {
UNCOV
1111
        return this.tokens.open?.leadingTrivia;
×
1112
    }
1113

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

1118
}
1119

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

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

1135
    public readonly location: Location | undefined;
1136

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

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

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

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

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

1169
    public get leadingTrivia(): Token[] {
UNCOV
1170
        return this.tokens.operator.leadingTrivia;
×
1171
    }
1172
}
1173

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

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

1189
    public readonly kind = AstNodeKind.VariableExpression;
9,932✔
1190

1191
    public readonly location: Location;
1192

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

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

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

1224

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

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

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

1241
    get leadingTrivia(): Token[] {
1242
        return this.tokens.name.leadingTrivia;
253✔
1243
    }
1244
}
1245

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

1257
    public readonly location: Location;
1258

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

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

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

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

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

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

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

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

1359
    get leadingTrivia(): Token[] {
UNCOV
1360
        return this.tokens.value.leadingTrivia;
×
1361
    }
1362
}
1363

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

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

1384
    public readonly location: Location | undefined;
1385

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

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

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

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

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

1429
    get leadingTrivia(): Token[] {
UNCOV
1430
        return this.tokens.new.leadingTrivia;
×
1431
    }
1432
}
1433

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

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

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

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

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

1475
    public readonly location: Location | undefined;
1476

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

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

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

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

1546
        return result;
4✔
1547
    }
1548

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

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

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

1572
    readonly location: Location | undefined;
1573

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

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

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

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

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

1630
    public readonly location: Location | undefined;
1631

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

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

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

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

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

1686
        return result;
10✔
1687
    }
1688

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

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

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

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

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

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

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

1741
    public readonly location: Location | undefined;
1742

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

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

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

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

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

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

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

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

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

1830
    public readonly name: string;
1831

1832
    public call: CallExpression;
1833

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

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

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

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

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

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

1893
    public readonly location: Location | undefined;
1894

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

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

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

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

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

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

1972
    get leadingTrivia(): Token[] {
UNCOV
1973
        return this.test.leadingTrivia;
×
1974
    }
1975
}
1976

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

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

1998
    public readonly location: Location | undefined;
1999

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

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

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

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

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

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

2074
    get leadingTrivia(): Token[] {
UNCOV
2075
        return this.consequent.leadingTrivia;
×
2076
    }
2077
}
2078

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

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

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

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

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

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

2127
    get leadingTrivia(): Token[] {
UNCOV
2128
        return this.tokens.regexLiteral?.leadingTrivia;
×
2129
    }
2130
}
2131

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

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

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

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

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

2192
    public readonly kind = AstNodeKind.TypeExpression;
1,420✔
2193

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

2199
    public readonly location: Location;
2200

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

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

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

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

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

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

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

2251
    public readonly obj: Expression;
2252

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

2257
    public typeExpression?: TypeExpression;
2258

2259
    public readonly location: Location;
2260

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

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

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

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

2308
    public readonly innerType: Expression;
2309

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

2312
    public readonly location: Location;
2313

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

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

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