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

rokucommunity / brighterscript / #12717

14 Jun 2024 08:20PM UTC coverage: 85.629% (-2.3%) from 87.936%
#12717

push

web-flow
Merge 94311dc0a into 42db50190

10808 of 13500 branches covered (80.06%)

Branch coverage included in aggregate %.

6557 of 7163 new or added lines in 96 files covered. (91.54%)

83 existing lines in 17 files now uncovered.

12270 of 13451 relevant lines covered (91.22%)

26531.66 hits per line

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

90.6
/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, FunctionStatement, 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, 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

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

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

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

59
    public readonly kind = AstNodeKind.BinaryExpression;
2,808✔
60

61
    public readonly location: Location | undefined;
62

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

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

80

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

99
    get leadingTrivia(): Token[] {
NEW
100
        return this.left.leadingTrivia;
×
101
    }
102
}
103

104

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

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

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

140
    public readonly kind = AstNodeKind.CallExpression;
2,127✔
141

142
    public readonly location: Location | undefined;
143

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

147
        //transpile the name
148
        if (nameOverride) {
1,411✔
149
            result.push(state.sourceNode(this.callee, nameOverride));
12✔
150
        } else {
151
            result.push(...this.callee.transpile(state));
1,399✔
152
        }
153

154
        result.push(
1,411✔
155
            state.transpileToken(this.tokens.openingParen, '(')
156
        );
157
        for (let i = 0; i < this.args.length; i++) {
1,411✔
158
            //add comma between args
159
            if (i > 0) {
1,010✔
160
                result.push(', ');
310✔
161
            }
162
            let arg = this.args[i];
1,010✔
163
            result.push(...arg.transpile(state));
1,010✔
164
        }
165
        if (this.tokens.closingParen) {
1,411!
166
            result.push(
1,411✔
167
                state.transpileToken(this.tokens.closingParen)
168
            );
169
        }
170
        return result;
1,411✔
171
    }
172

173
    walk(visitor: WalkVisitor, options: WalkOptions) {
174
        if (options.walkMode & InternalWalkMode.walkExpressions) {
9,097!
175
            walk(this, 'callee', visitor, options);
9,097✔
176
            walkArray(this.args, visitor, options, this);
9,097✔
177
        }
178
    }
179

180
    getType(options: GetTypeOptions) {
181
        const calleeType = this.callee.getType(options);
922✔
182
        if (options.ignoreCall) {
922!
NEW
183
            return calleeType;
×
184
        }
185
        if (isNewExpression(this.parent)) {
922✔
186
            return calleeType;
307✔
187
        }
188
        const specialCaseReturnType = util.getSpecialCaseCallExpressionReturnType(this);
615✔
189
        if (specialCaseReturnType) {
615✔
190
            return specialCaseReturnType;
97✔
191
        }
192
        if (isCallableType(calleeType) && (!isReferenceType(calleeType.returnType) || calleeType.returnType?.isResolvable())) {
518!
193
            return calleeType.returnType;
218✔
194
        }
195
        if (!isReferenceType(calleeType) && (calleeType as BaseFunctionType)?.returnType?.isResolvable()) {
300✔
196
            return (calleeType as BaseFunctionType).returnType;
257✔
197
        }
198
        return new TypePropertyReferenceType(calleeType, 'returnType');
43✔
199
    }
200

201
    get leadingTrivia(): Token[] {
202
        return this.callee.leadingTrivia;
90✔
203
    }
204
}
205

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

240
    public readonly kind = AstNodeKind.FunctionExpression;
3,245✔
241

242
    readonly parameters: FunctionParameterExpression[];
243
    public readonly body: Block;
244
    public readonly returnTypeExpression?: TypeExpression;
245

246
    readonly tokens: {
247
        readonly functionType?: Token;
248
        readonly endFunctionType?: Token;
249
        readonly leftParen?: Token;
250
        readonly rightParen?: Token;
251
        readonly as?: Token;
252
    };
253

254
    /**
255
     * If this function is part of a FunctionStatement, this will be set. Otherwise this will be undefined
256
     */
257
    public functionStatement?: FunctionStatement;
258

259
    public get leadingTrivia(): Token[] {
260
        return this.tokens.functionType?.leadingTrivia;
2,717!
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,535✔
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,253✔
284
        let results = [] as TranspileResult;
1,253✔
285
        //'function'|'sub'
286
        results.push(
1,253✔
287
            state.transpileToken(this.tokens.functionType, 'function')
288
        );
289
        //functionName?
290
        if (name) {
1,253✔
291
            results.push(
1,188✔
292
                ' ',
293
                state.transpileToken(name)
294
            );
295
        }
296
        //leftParen
297
        results.push(
1,253✔
298
            state.transpileToken(this.tokens.leftParen)
299
        );
300
        //parameters
301
        for (let i = 0; i < this.parameters.length; i++) {
1,253✔
302
            let param = this.parameters[i];
1,903✔
303
            //add commas
304
            if (i > 0) {
1,903✔
305
                results.push(', ');
942✔
306
            }
307
            //add parameter
308
            results.push(param.transpile(state));
1,903✔
309
        }
310
        //right paren
311
        results.push(
1,253✔
312
            state.transpileToken(this.tokens.rightParen)
313
        );
314
        //as [Type]
315
        if (!state.options.removeParameterTypes && this.returnTypeExpression) {
1,253✔
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,253✔
326
        if (includeBody) {
1,253!
327
            state.lineage.unshift(this);
1,253✔
328
            let body = this.body.transpile(state);
1,253✔
329
            hasBody = body.length > 0;
1,253✔
330
            state.lineage.shift();
1,253✔
331
            results.push(...body);
1,253✔
332
        }
333

334
        const lastLocatable = hasBody ? this.body : this.returnTypeExpression ?? this.tokens.leftParen ?? this.tokens.functionType;
1,253!
335
        results.push(
1,253✔
336
            ...state.transpileEndBlockToken(lastLocatable, this.tokens.endFunctionType, `end ${this.tokens.functionType ?? 'function'}`)
3,759!
337
        );
338
        return results;
1,253✔
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) {
14,691!
378
            walkArray(this.parameters, visitor, options, this);
14,691✔
379
            walk(this, 'returnTypeExpression', visitor, options);
14,691✔
380
            //This is the core of full-program walking...it allows us to step into sub functions
381
            if (options.walkMode & InternalWalkMode.recurseChildFunctions) {
14,691!
382
                walk(this, 'body', visitor, options);
14,691✔
383
            }
384
        }
385
    }
386

387
    public getType(options: GetTypeOptions): TypedFunctionType {
388
        //if there's a defined return type, use that
389
        let returnType = this.returnTypeExpression?.getType({ ...options, typeChain: undefined });
3,551✔
390
        const isSub = this.tokens.functionType?.kind === TokenKind.Sub;
3,551!
391
        //if we don't have a return type and this is a sub, set the return type to `void`. else use `dynamic`
392
        if (!returnType) {
3,551✔
393
            returnType = isSub ? VoidType.instance : DynamicType.instance;
3,004✔
394
        }
395

396
        const resultType = new TypedFunctionType(returnType);
3,551✔
397
        resultType.isSub = isSub;
3,551✔
398
        for (let param of this.parameters) {
3,551✔
399
            resultType.addParameter(param.tokens.name.text, param.getType({ ...options, typeChain: undefined }), !!param.defaultValue);
1,931✔
400
        }
401
        // Figure out this function's name if we can
402
        let funcName = '';
3,551✔
403
        if (isMethodStatement(this.parent) || isInterfaceMethodStatement(this.parent)) {
3,551✔
404
            funcName = this.parent.getName(ParseMode.BrighterScript);
268✔
405
            if (options.typeChain) {
268✔
406
                // Get the typechain info from the parent class
407
                this.parent.parent?.getType(options);
1!
408
            }
409
        } else if (isFunctionStatement(this.parent)) {
3,283✔
410
            funcName = this.parent.getName(ParseMode.BrighterScript);
3,232✔
411
        }
412
        if (funcName) {
3,551✔
413
            resultType.setName(funcName);
3,500✔
414
        }
415
        options.typeChain?.push(new TypeChainEntry({ name: funcName, type: resultType, data: options.data, astNode: this }));
3,551✔
416
        return resultType;
3,551✔
417
    }
418
}
419

420
export class FunctionParameterExpression extends Expression {
1✔
421
    constructor(options: {
422
        name: Identifier;
423
        equals?: Token;
424
        defaultValue?: Expression;
425
        as?: Token;
426
        typeExpression?: TypeExpression;
427
    }) {
428
        super();
2,640✔
429
        this.tokens = {
2,640✔
430
            name: options.name,
431
            equals: options.equals,
432
            as: options.as
433
        };
434
        this.defaultValue = options.defaultValue;
2,640✔
435
        this.typeExpression = options.typeExpression;
2,640✔
436
    }
437

438
    public readonly kind = AstNodeKind.FunctionParameterExpression;
2,640✔
439

440
    readonly tokens: {
441
        readonly name: Identifier;
442
        readonly equals?: Token;
443
        readonly as?: Token;
444
    };
445

446
    public readonly defaultValue?: Expression;
447
    public readonly typeExpression?: TypeExpression;
448

449
    public getType(options: GetTypeOptions) {
450
        const paramType = this.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime, typeChain: undefined }) ??
4,741✔
451
            this.defaultValue?.getType({ ...options, flags: SymbolTypeFlag.runtime, typeChain: undefined }) ??
11,845✔
452
            DynamicType.instance;
453
        options.typeChain?.push(new TypeChainEntry({ name: this.tokens.name.text, type: paramType, data: options.data, astNode: this }));
4,741✔
454
        return paramType;
4,741✔
455
    }
456

457
    public get location(): Location | undefined {
458
        return util.createBoundingLocation(
8,373✔
459
            this.tokens.name,
460
            this.tokens.as,
461
            this.typeExpression,
462
            this.tokens.equals,
463
            this.defaultValue
464
        );
465
    }
466

467
    public transpile(state: BrsTranspileState) {
468
        let result: TranspileResult = [
1,911✔
469
            //name
470
            state.transpileToken(this.tokens.name)
471
        ];
472
        //default value
473
        if (this.defaultValue) {
1,911✔
474
            result.push(' = ');
9✔
475
            result.push(this.defaultValue.transpile(state));
9✔
476
        }
477
        //type declaration
478
        if (this.typeExpression && !state.options.removeParameterTypes) {
1,911✔
479
            result.push(' ');
65✔
480
            result.push(state.transpileToken(this.tokens.as, 'as'));
65✔
481
            result.push(' ');
65✔
482
            result.push(
65✔
483
                ...(this.typeExpression?.transpile(state) ?? [])
390!
484
            );
485
        }
486

487
        return result;
1,911✔
488
    }
489

490
    public getTypedef(state: BrsTranspileState): TranspileResult {
491
        const results = [this.tokens.name.text] as TranspileResult;
73✔
492

493
        if (this.defaultValue) {
73!
494
            results.push(' = ', ...this.defaultValue.transpile(state));
×
495
        }
496

497
        if (this.tokens.as) {
73✔
498
            results.push(' as ');
6✔
499

500
            // TODO: Is this conditional needed? Will typeToken always exist
501
            // so long as `asToken` exists?
502
            if (this.typeExpression) {
6!
503
                results.push(...(this.typeExpression?.getTypedef(state) ?? ['']));
6!
504
            }
505
        }
506

507
        return results;
73✔
508
    }
509

510
    walk(visitor: WalkVisitor, options: WalkOptions) {
511
        // eslint-disable-next-line no-bitwise
512
        if (options.walkMode & InternalWalkMode.walkExpressions) {
11,217!
513
            walk(this, 'defaultValue', visitor, options);
11,217✔
514
            walk(this, 'typeExpression', visitor, options);
11,217✔
515
        }
516
    }
517

518
    get leadingTrivia(): Token[] {
519
        return this.tokens.name.leadingTrivia;
29✔
520
    }
521
}
522

523
export class DottedGetExpression extends Expression {
1✔
524
    constructor(options: {
525
        obj: Expression;
526
        name: Identifier;
527
        /**
528
         * Can either be `.`, or `?.` for optional chaining - defaults in transpile to '.'
529
         */
530
        dot?: Token;
531
    }) {
532
        super();
2,575✔
533
        this.tokens = {
2,575✔
534
            name: options.name,
535
            dot: options.dot
536
        };
537
        this.obj = options.obj;
2,575✔
538

539
        this.location = util.createBoundingLocation(this.obj, this.tokens.dot, this.tokens.name);
2,575✔
540
    }
541

542
    readonly tokens: {
543
        readonly name: Identifier;
544
        readonly dot?: Token;
545
    };
546
    readonly obj: Expression;
547

548
    public readonly kind = AstNodeKind.DottedGetExpression;
2,575✔
549

550
    public readonly location: Location | undefined;
551

552
    transpile(state: BrsTranspileState) {
553
        //if the callee starts with a namespace name, transpile the name
554
        if (state.file.calleeStartsWithNamespace(this)) {
800✔
555
            return [
6✔
556
                state.sourceNode(this, this.getName(ParseMode.BrightScript))
557
            ];
558
        } else {
559
            return [
794✔
560
                ...this.obj.transpile(state),
561
                state.transpileToken(this.tokens.dot, '.'),
562
                state.transpileToken(this.tokens.name)
563
            ];
564
        }
565
    }
566

567
    walk(visitor: WalkVisitor, options: WalkOptions) {
568
        if (options.walkMode & InternalWalkMode.walkExpressions) {
9,855!
569
            walk(this, 'obj', visitor, options);
9,855✔
570
        }
571
    }
572

573
    getType(options: GetTypeOptions) {
574
        const objType = this.obj?.getType(options);
5,544!
575
        let result = objType?.getMemberType(this.tokens.name?.text, options);
5,544!
576

577
        if (util.isClassUsedAsFunction(result, this, options)) {
5,544✔
578
            // treat this class constructor as a function
579
            result = FunctionType.instance;
10✔
580
        }
581
        options.typeChain?.push(new TypeChainEntry({
5,544✔
582
            name: this.tokens.name?.text,
7,635!
583
            type: result,
584
            data: options.data,
585
            location: this.tokens.name?.location ?? this.location,
15,270!
586
            astNode: this
587
        }));
588
        if (result ||
5,544✔
589
            options.flags & SymbolTypeFlag.typetime ||
590
            (isPrimitiveType(objType) || isCallableType(objType))) {
591
            // All types should be known at typeTime, or the obj is well known
592
            return result;
5,513✔
593
        }
594
        // It is possible at runtime that a value has been added dynamically to an object, or something
595
        // TODO: maybe have a strict flag on this?
596
        return DynamicType.instance;
31✔
597
    }
598

599
    getName(parseMode: ParseMode) {
600
        return util.getAllDottedGetPartsAsString(this, parseMode);
32✔
601
    }
602

603
    get leadingTrivia(): Token[] {
604
        return this.obj.leadingTrivia;
72✔
605
    }
606

607
}
608

609
export class XmlAttributeGetExpression extends Expression {
1✔
610
    constructor(options: {
611
        obj: Expression;
612
        /**
613
         * Can either be `@`, or `?@` for optional chaining - defaults to '@'
614
         */
615
        at?: Token;
616
        name: Identifier;
617
    }) {
618
        super();
10✔
619
        this.obj = options.obj;
10✔
620
        this.tokens = { at: options.at, name: options.name };
10✔
621
        this.location = util.createBoundingLocation(this.obj, this.tokens.at, this.tokens.name);
10✔
622
    }
623

624
    public readonly kind = AstNodeKind.XmlAttributeGetExpression;
10✔
625

626
    public readonly tokens: {
627
        name: Identifier;
628
        at?: Token;
629
    };
630

631
    public readonly obj: Expression;
632

633
    public readonly location: Location | undefined;
634

635
    transpile(state: BrsTranspileState) {
636
        return [
3✔
637
            ...this.obj.transpile(state),
638
            state.transpileToken(this.tokens.at, '@'),
639
            state.transpileToken(this.tokens.name)
640
        ];
641
    }
642

643
    walk(visitor: WalkVisitor, options: WalkOptions) {
644
        if (options.walkMode & InternalWalkMode.walkExpressions) {
28!
645
            walk(this, 'obj', visitor, options);
28✔
646
        }
647
    }
648

649
    get leadingTrivia(): Token[] {
NEW
650
        return this.obj.leadingTrivia;
×
651
    }
652
}
653

654
export class IndexedGetExpression extends Expression {
1✔
655
    constructor(options: {
656
        obj: Expression;
657
        indexes: Expression[];
658
        /**
659
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.` - defaults to '[' in transpile
660
         */
661
        openingSquare?: Token;
662
        closingSquare?: Token;
663
        questionDot?: Token;//  ? or ?.
664
    }) {
665
        super();
143✔
666
        this.tokens = {
143✔
667
            openingSquare: options.openingSquare,
668
            closingSquare: options.closingSquare,
669
            questionDot: options.questionDot
670
        };
671
        this.obj = options.obj;
143✔
672
        this.indexes = options.indexes;
143✔
673
        this.location = util.createBoundingLocation(this.obj, this.tokens.openingSquare, this.tokens.questionDot, this.tokens.openingSquare, ...this.indexes, this.tokens.closingSquare);
143✔
674
    }
675

676
    public readonly kind = AstNodeKind.IndexedGetExpression;
143✔
677

678
    public readonly obj: Expression;
679
    public readonly indexes: Expression[];
680

681
    readonly tokens: {
682
        /**
683
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.` - defaults to '[' in transpile
684
         */
685
        readonly openingSquare?: Token;
686
        readonly closingSquare?: Token;
687
        readonly questionDot?: Token; //  ? or ?.
688
    };
689

690
    public readonly location: Location | undefined;
691

692
    transpile(state: BrsTranspileState) {
693
        const result = [];
64✔
694
        result.push(
64✔
695
            ...this.obj.transpile(state),
696
            this.tokens.questionDot ? state.transpileToken(this.tokens.questionDot) : '',
64✔
697
            state.transpileToken(this.tokens.openingSquare, '[')
698
        );
699
        for (let i = 0; i < this.indexes.length; i++) {
64✔
700
            //add comma between indexes
701
            if (i > 0) {
72✔
702
                result.push(', ');
8✔
703
            }
704
            let index = this.indexes[i];
72✔
705
            result.push(
72✔
706
                ...(index?.transpile(state) ?? [])
432!
707
            );
708
        }
709
        result.push(
64✔
710
            state.transpileToken(this.tokens.closingSquare, ']')
711
        );
712
        return result;
64✔
713
    }
714

715
    walk(visitor: WalkVisitor, options: WalkOptions) {
716
        if (options.walkMode & InternalWalkMode.walkExpressions) {
492!
717
            walk(this, 'obj', visitor, options);
492✔
718
            walkArray(this.indexes, visitor, options, this);
492✔
719
        }
720
    }
721

722
    getType(options: GetTypeOptions): BscType {
723
        const objType = this.obj.getType(options);
201✔
724
        if (isArrayType(objType)) {
201✔
725
            // This is used on an array. What is the default type of that array?
726
            return objType.defaultType;
10✔
727
        }
728
        return super.getType(options);
191✔
729
    }
730

731
    get leadingTrivia(): Token[] {
732
        return this.obj.leadingTrivia;
24✔
733
    }
734
}
735

736
export class GroupingExpression extends Expression {
1✔
737
    constructor(options: {
738
        leftParen?: Token;
739
        rightParen?: Token;
740
        expression: Expression;
741
    }) {
742
        super();
46✔
743
        this.tokens = {
46✔
744
            rightParen: options.rightParen,
745
            leftParen: options.leftParen
746
        };
747
        this.expression = options.expression;
46✔
748
        this.location = util.createBoundingLocation(this.tokens.leftParen, this.expression, this.tokens.rightParen);
46✔
749
    }
750

751
    public readonly tokens: {
752
        readonly leftParen?: Token;
753
        readonly rightParen?: Token;
754
    };
755
    public readonly expression: Expression;
756

757
    public readonly kind = AstNodeKind.GroupingExpression;
46✔
758

759
    public readonly location: Location | undefined;
760

761
    transpile(state: BrsTranspileState) {
762
        if (isTypecastExpression(this.expression)) {
13✔
763
            return this.expression.transpile(state);
7✔
764
        }
765
        return [
6✔
766
            state.transpileToken(this.tokens.leftParen),
767
            ...this.expression.transpile(state),
768
            state.transpileToken(this.tokens.rightParen)
769
        ];
770
    }
771

772
    walk(visitor: WalkVisitor, options: WalkOptions) {
773
        if (options.walkMode & InternalWalkMode.walkExpressions) {
154!
774
            walk(this, 'expression', visitor, options);
154✔
775
        }
776
    }
777

778
    getType(options: GetTypeOptions) {
779
        return this.expression.getType(options);
62✔
780
    }
781

782
    get leadingTrivia(): Token[] {
783
        return this.tokens.leftParen?.leadingTrivia;
2!
784
    }
785
}
786

787
export class LiteralExpression extends Expression {
1✔
788
    constructor(options: {
789
        value: Token;
790
    }) {
791
        super();
6,717✔
792
        this.tokens = {
6,717✔
793
            value: options.value
794
        };
795
    }
796

797
    public readonly tokens: {
798
        readonly value: Token;
799
    };
800

801
    public readonly kind = AstNodeKind.LiteralExpression;
6,717✔
802

803
    public get location() {
804
        return this.tokens.value.location;
18,861✔
805
    }
806

807
    public getType(options?: GetTypeOptions) {
808
        return util.tokenToBscType(this.tokens.value);
3,128✔
809
    }
810

811
    transpile(state: BrsTranspileState) {
812
        let text: string;
813
        if (this.tokens.value.kind === TokenKind.TemplateStringQuasi) {
4,059✔
814
            //wrap quasis with quotes (and escape inner quotemarks)
815
            text = `"${this.tokens.value.text.replace(/"/g, '""')}"`;
28✔
816

817
        } else if (this.tokens.value.kind === TokenKind.StringLiteral) {
4,031✔
818
            text = this.tokens.value.text;
2,694✔
819
            //add trailing quotemark if it's missing. We will have already generated a diagnostic for this.
820
            if (text.endsWith('"') === false) {
2,694✔
821
                text += '"';
1✔
822
            }
823
        } else {
824
            text = this.tokens.value.text;
1,337✔
825
        }
826

827
        return [
4,059✔
828
            state.transpileToken({ ...this.tokens.value, text: text })
829
        ];
830
    }
831

832
    walk(visitor: WalkVisitor, options: WalkOptions) {
833
        //nothing to walk
834
    }
835

836
    get leadingTrivia(): Token[] {
837
        return this.tokens.value.leadingTrivia;
53✔
838
    }
839
}
840

841
/**
842
 * This is a special expression only used within template strings. It exists so we can prevent producing lots of empty strings
843
 * during template string transpile by identifying these expressions explicitly and skipping the bslib_toString around them
844
 */
845
export class EscapedCharCodeLiteralExpression extends Expression {
1✔
846
    constructor(options: {
847
        value: Token & { charCode: number };
848
    }) {
849
        super();
25✔
850
        this.tokens = { value: options.value };
25✔
851
        this.location = this.tokens.value.location;
25✔
852
    }
853

854
    public readonly kind = AstNodeKind.EscapedCharCodeLiteralExpression;
25✔
855

856
    public readonly tokens: {
857
        readonly value: Token & { charCode: number };
858
    };
859

860
    public readonly location: Location;
861

862
    transpile(state: BrsTranspileState) {
863
        return [
13✔
864
            state.sourceNode(this, `chr(${this.tokens.value.charCode})`)
865
        ];
866
    }
867

868
    walk(visitor: WalkVisitor, options: WalkOptions) {
869
        //nothing to walk
870
    }
871
}
872

873
export class ArrayLiteralExpression extends Expression {
1✔
874
    constructor(options: {
875
        elements: Array<Expression>;
876
        open?: Token;
877
        close?: Token;
878
    }) {
879
        super();
128✔
880
        this.tokens = {
128✔
881
            open: options.open,
882
            close: options.close
883
        };
884
        this.elements = options.elements;
128✔
885
        this.location = util.createBoundingLocation(this.tokens.open, ...this.elements, this.tokens.close);
128✔
886
    }
887

888
    public readonly elements: Array<Expression>;
889

890
    public readonly tokens: {
891
        readonly open?: Token;
892
        readonly close?: Token;
893
    };
894

895
    public readonly kind = AstNodeKind.ArrayLiteralExpression;
128✔
896

897
    public readonly location: Location | undefined;
898

899
    transpile(state: BrsTranspileState) {
900
        let result: TranspileResult = [];
46✔
901
        result.push(
46✔
902
            state.transpileToken(this.tokens.open, '[')
903
        );
904
        let hasChildren = this.elements.length > 0;
46✔
905
        state.blockDepth++;
46✔
906

907
        for (let i = 0; i < this.elements.length; i++) {
46✔
908
            let previousElement = this.elements[i - 1];
53✔
909
            let element = this.elements[i];
53✔
910

911
            if (util.isLeadingCommentOnSameLine(previousElement ?? this.tokens.open, element)) {
53✔
912
                result.push(' ');
3✔
913
            } else {
914
                result.push(
50✔
915
                    '\n',
916
                    state.indent()
917
                );
918
            }
919
            result.push(
53✔
920
                ...element.transpile(state)
921
            );
922
        }
923
        state.blockDepth--;
46✔
924
        //add a newline between open and close if there are elements
925
        const lastLocatable = this.elements[this.elements.length - 1] ?? this.tokens.open;
46✔
926
        result.push(...state.transpileEndBlockToken(lastLocatable, this.tokens.close, ']', hasChildren));
46✔
927

928
        return result;
46✔
929
    }
930

931
    walk(visitor: WalkVisitor, options: WalkOptions) {
932
        if (options.walkMode & InternalWalkMode.walkExpressions) {
618!
933
            walkArray(this.elements, visitor, options, this);
618✔
934
        }
935
    }
936

937
    getType(options: GetTypeOptions): BscType {
938
        const innerTypes = this.elements.map(expr => expr.getType(options));
169✔
939
        return new ArrayType(...innerTypes);
127✔
940
    }
941
    get leadingTrivia(): Token[] {
942
        return this.tokens.open?.leadingTrivia;
8!
943
    }
944

945
    get endTrivia(): Token[] {
946
        return this.tokens.close?.leadingTrivia;
2!
947
    }
948
}
949

950
export class AAMemberExpression extends Expression {
1✔
951
    constructor(options: {
952
        key: Token;
953
        colon?: Token;
954
        /** The expression evaluated to determine the member's initial value. */
955
        value: Expression;
956
        comma?: Token;
957
    }) {
958
        super();
261✔
959
        this.tokens = {
261✔
960
            key: options.key,
961
            colon: options.colon,
962
            comma: options.comma
963
        };
964
        this.value = options.value;
261✔
965
        this.location = util.createBoundingLocation(this.tokens.key, this.tokens.colon, this.value);
261✔
966
    }
967

968
    public readonly kind = AstNodeKind.AAMemberExpression;
261✔
969

970
    public readonly location: Location | undefined;
971

972
    public readonly tokens: {
973
        readonly key: Token;
974
        readonly colon?: Token;
975
        readonly comma?: Token;
976
    };
977

978
    /** The expression evaluated to determine the member's initial value. */
979
    public readonly value: Expression;
980

981
    transpile(state: BrsTranspileState) {
982
        //TODO move the logic from AALiteralExpression loop into this function
983
        return [];
×
984
    }
985

986
    walk(visitor: WalkVisitor, options: WalkOptions) {
987
        walk(this, 'value', visitor, options);
871✔
988
    }
989

990
    getType(options: GetTypeOptions): BscType {
991
        return this.value.getType(options);
184✔
992
    }
993

994
    get leadingTrivia(): Token[] {
995
        return this.tokens.key.leadingTrivia;
103✔
996
    }
997

998
}
999

1000
export class AALiteralExpression extends Expression {
1✔
1001
    constructor(options: {
1002
        elements: Array<AAMemberExpression>;
1003
        open?: Token;
1004
        close?: Token;
1005
    }) {
1006
        super();
248✔
1007
        this.tokens = {
248✔
1008
            open: options.open,
1009
            close: options.close
1010
        };
1011
        this.elements = options.elements;
248✔
1012
        this.location = util.createBoundingLocation(this.tokens.open, ...this.elements, this.tokens.close);
248✔
1013
    }
1014

1015
    public readonly elements: Array<AAMemberExpression>;
1016
    public readonly tokens: {
1017
        readonly open?: Token;
1018
        readonly close?: Token;
1019
    };
1020

1021
    public readonly kind = AstNodeKind.AALiteralExpression;
248✔
1022

1023
    public readonly location: Location | undefined;
1024

1025
    transpile(state: BrsTranspileState) {
1026
        let result: TranspileResult = [];
54✔
1027
        //open curly
1028
        result.push(
54✔
1029
            state.transpileToken(this.tokens.open, '{')
1030
        );
1031
        let hasChildren = this.elements.length > 0;
54✔
1032
        //add newline if the object has children and the first child isn't a comment starting on the same line as opening curly
1033
        if (hasChildren && !util.isLeadingCommentOnSameLine(this.tokens.open, this.elements[0])) {
54✔
1034
            result.push('\n');
23✔
1035
        }
1036
        state.blockDepth++;
54✔
1037
        for (let i = 0; i < this.elements.length; i++) {
54✔
1038
            let element = this.elements[i];
35✔
1039
            let previousElement = this.elements[i - 1];
35✔
1040
            let nextElement = this.elements[i + 1];
35✔
1041

1042
            //don't indent if comment is same-line
1043
            if (util.isLeadingCommentOnSameLine(this.tokens.open, element) ||
35✔
1044
                util.isLeadingCommentOnSameLine(previousElement, element)) {
1045
                result.push(' ');
7✔
1046
            } else {
1047
                //indent line
1048
                result.push(state.indent());
28✔
1049
            }
1050

1051
            //key
1052
            result.push(
35✔
1053
                state.transpileToken(element.tokens.key)
1054
            );
1055
            //colon
1056
            result.push(
35✔
1057
                state.transpileToken(element.tokens.colon, ':'),
1058
                ' '
1059
            );
1060
            //value
1061
            result.push(...element.value.transpile(state));
35✔
1062

1063
            //if next element is a same-line comment, skip the newline
1064
            if (nextElement && !util.isLeadingCommentOnSameLine(element, nextElement)) {
35✔
1065
                //add a newline between statements
1066
                result.push('\n');
5✔
1067
            }
1068
        }
1069
        state.blockDepth--;
54✔
1070

1071
        const lastElement = this.elements[this.elements.length - 1] ?? this.tokens.open;
54✔
1072
        result.push(...state.transpileEndBlockToken(lastElement, this.tokens.close, '}', hasChildren));
54✔
1073

1074
        return result;
54✔
1075
    }
1076

1077
    walk(visitor: WalkVisitor, options: WalkOptions) {
1078
        if (options.walkMode & InternalWalkMode.walkExpressions) {
901!
1079
            walkArray(this.elements, visitor, options, this);
901✔
1080
        }
1081
    }
1082

1083
    getType(options: GetTypeOptions): BscType {
1084
        const resultType = new AssociativeArrayType();
184✔
1085
        resultType.addBuiltInInterfaces();
184✔
1086
        for (const element of this.elements) {
184✔
1087
            if (isAAMemberExpression(element)) {
184!
1088
                resultType.addMember(element.tokens.key.text, { definingNode: element }, element.getType(options), SymbolTypeFlag.runtime);
184✔
1089
            }
1090
        }
1091
        return resultType;
184✔
1092
    }
1093

1094
    public get leadingTrivia(): Token[] {
NEW
1095
        return this.tokens.open?.leadingTrivia;
×
1096
    }
1097

1098
    public get endTrivia(): Token[] {
1099
        return this.tokens.close?.leadingTrivia;
1!
1100
    }
1101

1102
}
1103

1104
export class UnaryExpression extends Expression {
1✔
1105
    constructor(options: {
1106
        operator: Token;
1107
        right: Expression;
1108
    }) {
1109
        super();
62✔
1110
        this.tokens = {
62✔
1111
            operator: options.operator
1112
        };
1113
        this.right = options.right;
62✔
1114
        this.location = util.createBoundingLocation(this.tokens.operator, this.right);
62✔
1115
    }
1116

1117
    public readonly kind = AstNodeKind.UnaryExpression;
62✔
1118

1119
    public readonly location: Location | undefined;
1120

1121
    public readonly tokens: {
1122
        readonly operator: Token;
1123
    };
1124
    public readonly right: Expression;
1125

1126
    transpile(state: BrsTranspileState) {
1127
        let separatingWhitespace: string | undefined;
1128
        if (isVariableExpression(this.right)) {
12✔
1129
            separatingWhitespace = this.right.tokens.name.leadingWhitespace;
6✔
1130
        } else if (isLiteralExpression(this.right)) {
6✔
1131
            separatingWhitespace = this.right.tokens.value.leadingWhitespace;
2✔
1132
        } else {
1133
            separatingWhitespace = ' ';
4✔
1134
        }
1135

1136
        return [
12✔
1137
            state.transpileToken(this.tokens.operator),
1138
            separatingWhitespace,
1139
            ...this.right.transpile(state)
1140
        ];
1141
    }
1142

1143
    walk(visitor: WalkVisitor, options: WalkOptions) {
1144
        if (options.walkMode & InternalWalkMode.walkExpressions) {
233!
1145
            walk(this, 'right', visitor, options);
233✔
1146
        }
1147
    }
1148

1149
    getType(options: GetTypeOptions): BscType {
1150
        return util.unaryOperatorResultType(this.tokens.operator, this.right.getType(options));
48✔
1151
    }
1152

1153
    public get leadingTrivia(): Token[] {
NEW
1154
        return this.tokens.operator.leadingTrivia;
×
1155
    }
1156
}
1157

1158
export class VariableExpression extends Expression {
1✔
1159
    constructor(options: {
1160
        name: Identifier;
1161
    }) {
1162
        super();
9,382✔
1163
        this.tokens = {
9,382✔
1164
            name: options.name
1165
        };
1166
        this.location = this.tokens.name?.location;
9,382!
1167
    }
1168

1169
    public readonly tokens: {
1170
        readonly name: Identifier;
1171
    };
1172

1173
    public readonly kind = AstNodeKind.VariableExpression;
9,382✔
1174

1175
    public readonly location: Location;
1176

1177
    public getName(parseMode?: ParseMode) {
1178
        return this.tokens.name.text;
12,897✔
1179
    }
1180

1181
    transpile(state: BrsTranspileState) {
1182
        let result: TranspileResult = [];
5,466✔
1183
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
5,466✔
1184
        //if the callee is the name of a known namespace function
1185
        if (namespace && state.file.calleeIsKnownNamespaceFunction(this, namespace.getName(ParseMode.BrighterScript))) {
5,466✔
1186
            result.push(
8✔
1187
                state.sourceNode(this, [
1188
                    namespace.getName(ParseMode.BrightScript),
1189
                    '_',
1190
                    this.getName(ParseMode.BrightScript)
1191
                ])
1192
            );
1193

1194
            //transpile  normally
1195
        } else {
1196
            result.push(
5,458✔
1197
                state.transpileToken(this.tokens.name)
1198
            );
1199
        }
1200
        return result;
5,466✔
1201
    }
1202

1203
    walk(visitor: WalkVisitor, options: WalkOptions) {
1204
        //nothing to walk
1205
    }
1206

1207

1208
    getType(options: GetTypeOptions) {
1209
        let resultType: BscType = util.tokenToBscType(this.tokens.name);
12,767✔
1210
        const nameKey = this.getName();
12,767✔
1211
        if (!resultType) {
12,767✔
1212
            const symbolTable = this.getSymbolTable();
8,991✔
1213
            resultType = symbolTable?.getSymbolType(nameKey, { ...options, fullName: nameKey, tableProvider: () => this.getSymbolTable() });
19,850!
1214

1215
            if (util.isClassUsedAsFunction(resultType, this, options)) {
8,991✔
1216
                resultType = FunctionType.instance;
20✔
1217
            }
1218

1219
        }
1220
        options?.typeChain?.push(new TypeChainEntry({ name: nameKey, type: resultType, data: options.data, astNode: this }));
12,767!
1221
        return resultType;
12,767✔
1222
    }
1223

1224
    get leadingTrivia(): Token[] {
1225
        return this.tokens.name.leadingTrivia;
203✔
1226
    }
1227
}
1228

1229
export class SourceLiteralExpression extends Expression {
1✔
1230
    constructor(options: {
1231
        value: Token;
1232
    }) {
1233
        super();
35✔
1234
        this.tokens = {
35✔
1235
            value: options.value
1236
        };
1237
        this.location = this.tokens.value?.location;
35!
1238
    }
1239

1240
    public readonly location: Location;
1241

1242
    public readonly kind = AstNodeKind.SourceLiteralExpression;
35✔
1243

1244
    public readonly tokens: {
1245
        readonly value: Token;
1246
    };
1247

1248
    /**
1249
     * Find the index of the function in its parent
1250
     */
1251
    private findFunctionIndex(parentFunction: FunctionExpression, func: FunctionExpression) {
1252
        let index = -1;
4✔
1253
        parentFunction.findChild((node) => {
4✔
1254
            if (isFunctionExpression(node)) {
12✔
1255
                index++;
4✔
1256
                if (node === func) {
4!
1257
                    return true;
4✔
1258
                }
1259
            }
1260
        }, {
1261
            walkMode: WalkMode.visitAllRecursive
1262
        });
1263
        return index;
4✔
1264
    }
1265

1266
    private getFunctionName(state: BrsTranspileState, parseMode: ParseMode) {
1267
        let func = this.findAncestor<FunctionExpression>(isFunctionExpression);
8✔
1268
        let nameParts = [] as TranspileResult;
8✔
1269
        let parentFunction: FunctionExpression;
1270
        while ((parentFunction = func.findAncestor<FunctionExpression>(isFunctionExpression))) {
8✔
1271
            let index = this.findFunctionIndex(parentFunction, func);
4✔
1272
            nameParts.unshift(`anon${index}`);
4✔
1273
            func = parentFunction;
4✔
1274
        }
1275
        //get the index of this function in its parent
1276
        nameParts.unshift(
8✔
1277
            func.functionStatement!.getName(parseMode)
1278
        );
1279
        return nameParts.join('$');
8✔
1280
    }
1281

1282
    /**
1283
     * Get the line number from our token or from the closest ancestor that has a range
1284
     */
1285
    private getClosestLineNumber() {
1286
        let node: AstNode = this;
7✔
1287
        while (node) {
7✔
1288
            if (node.location?.range) {
17✔
1289
                return node.location.range.start.line + 1;
5✔
1290
            }
1291
            node = node.parent;
12✔
1292
        }
1293
        return -1;
2✔
1294
    }
1295

1296
    transpile(state: BrsTranspileState) {
1297
        let text: string;
1298
        switch (this.tokens.value.kind) {
31✔
1299
            case TokenKind.SourceFilePathLiteral:
40✔
1300
                const pathUrl = fileUrl(state.srcPath);
3✔
1301
                text = `"${pathUrl.substring(0, 4)}" + "${pathUrl.substring(4)}"`;
3✔
1302
                break;
3✔
1303
            case TokenKind.SourceLineNumLiteral:
1304
                //TODO find first parent that has range, or default to -1
1305
                text = `${this.getClosestLineNumber()}`;
4✔
1306
                break;
4✔
1307
            case TokenKind.FunctionNameLiteral:
1308
                text = `"${this.getFunctionName(state, ParseMode.BrightScript)}"`;
4✔
1309
                break;
4✔
1310
            case TokenKind.SourceFunctionNameLiteral:
1311
                text = `"${this.getFunctionName(state, ParseMode.BrighterScript)}"`;
4✔
1312
                break;
4✔
1313
            case TokenKind.SourceLocationLiteral:
1314
                const locationUrl = fileUrl(state.srcPath);
3✔
1315
                //TODO find first parent that has range, or default to -1
1316
                text = `"${locationUrl.substring(0, 4)}" + "${locationUrl.substring(4)}:${this.getClosestLineNumber()}"`;
3✔
1317
                break;
3✔
1318
            case TokenKind.PkgPathLiteral:
1319
                text = `"${util.sanitizePkgPath(state.file.pkgPath)}"`;
2✔
1320
                break;
2✔
1321
            case TokenKind.PkgLocationLiteral:
1322
                text = `"${util.sanitizePkgPath(state.file.pkgPath)}:" + str(LINE_NUM)`;
2✔
1323
                break;
2✔
1324
            case TokenKind.LineNumLiteral:
1325
            default:
1326
                //use the original text (because it looks like a variable)
1327
                text = this.tokens.value.text;
9✔
1328
                break;
9✔
1329

1330
        }
1331
        return [
31✔
1332
            state.sourceNode(this, text)
1333
        ];
1334
    }
1335

1336
    walk(visitor: WalkVisitor, options: WalkOptions) {
1337
        //nothing to walk
1338
    }
1339

1340
    get leadingTrivia(): Token[] {
NEW
1341
        return this.tokens.value.leadingTrivia;
×
1342
    }
1343
}
1344

1345
/**
1346
 * This expression transpiles and acts exactly like a CallExpression,
1347
 * except we need to uniquely identify these statements so we can
1348
 * do more type checking.
1349
 */
1350
export class NewExpression extends Expression {
1✔
1351
    constructor(options: {
1352
        new?: Token;
1353
        call: CallExpression;
1354
    }) {
1355
        super();
127✔
1356
        this.tokens = {
127✔
1357
            new: options.new
1358
        };
1359
        this.call = options.call;
127✔
1360
        this.location = util.createBoundingLocation(this.tokens.new, this.call);
127✔
1361
    }
1362

1363
    public readonly kind = AstNodeKind.NewExpression;
127✔
1364

1365
    public readonly location: Location | undefined;
1366

1367
    public readonly tokens: {
1368
        readonly new?: Token;
1369
    };
1370
    public readonly call: CallExpression;
1371

1372
    /**
1373
     * The name of the class to initialize (with optional namespace prefixed)
1374
     */
1375
    public get className() {
1376
        //the parser guarantees the callee of a new statement's call object will be
1377
        //either a VariableExpression or a DottedGet
1378
        return this.call.callee as (VariableExpression | DottedGetExpression);
92✔
1379
    }
1380

1381
    public transpile(state: BrsTranspileState) {
1382
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
15✔
1383
        const cls = state.file.getClassFileLink(
15✔
1384
            this.className.getName(ParseMode.BrighterScript),
1385
            namespace?.getName(ParseMode.BrighterScript)
45✔
1386
        )?.item;
15✔
1387
        //new statements within a namespace block can omit the leading namespace if the class resides in that same namespace.
1388
        //So we need to figure out if this is a namespace-omitted class, or if this class exists without a namespace.
1389
        return this.call.transpile(state, cls?.getName(ParseMode.BrightScript));
15✔
1390
    }
1391

1392
    walk(visitor: WalkVisitor, options: WalkOptions) {
1393
        if (options.walkMode & InternalWalkMode.walkExpressions) {
807!
1394
            walk(this, 'call', visitor, options);
807✔
1395
        }
1396
    }
1397

1398
    getType(options: GetTypeOptions) {
1399
        const result = this.call.getType(options);
307✔
1400
        if (options.typeChain) {
307✔
1401
            // modify last typechain entry to show it is a new ...()
1402
            const lastEntry = options.typeChain[options.typeChain.length - 1];
3✔
1403
            if (lastEntry) {
3!
1404
                lastEntry.astNode = this;
3✔
1405
            }
1406
        }
1407
        return result;
307✔
1408
    }
1409

1410
    get leadingTrivia(): Token[] {
NEW
1411
        return this.tokens.new.leadingTrivia;
×
1412
    }
1413
}
1414

1415
export class CallfuncExpression extends Expression {
1✔
1416
    constructor(options: {
1417
        callee: Expression;
1418
        operator?: Token;
1419
        methodName: Identifier;
1420
        openingParen?: Token;
1421
        args?: Expression[];
1422
        closingParen?: Token;
1423
    }) {
1424
        super();
26✔
1425
        this.tokens = {
26✔
1426
            operator: options.operator,
1427
            methodName: options.methodName,
1428
            openingParen: options.openingParen,
1429
            closingParen: options.closingParen
1430
        };
1431
        this.callee = options.callee;
26✔
1432
        this.args = options.args ?? [];
26!
1433

1434
        this.location = util.createBoundingLocation(
26✔
1435
            this.callee,
1436
            this.tokens.operator,
1437
            this.tokens.methodName,
1438
            this.tokens.openingParen,
1439
            ...this.args,
1440
            this.tokens.closingParen
1441
        );
1442
    }
1443

1444
    public readonly callee: Expression;
1445
    public readonly args: Expression[];
1446

1447
    public readonly tokens: {
1448
        readonly operator: Token;
1449
        readonly methodName: Identifier;
1450
        readonly openingParen?: Token;
1451
        readonly closingParen?: Token;
1452
    };
1453

1454
    public readonly kind = AstNodeKind.CallfuncExpression;
26✔
1455

1456
    public readonly location: Location | undefined;
1457

1458
    public transpile(state: BrsTranspileState) {
1459
        let result = [] as TranspileResult;
9✔
1460
        result.push(
9✔
1461
            ...this.callee.transpile(state),
1462
            state.sourceNode(this.tokens.operator, '.callfunc'),
1463
            state.transpileToken(this.tokens.openingParen, '('),
1464
            //the name of the function
1465
            state.sourceNode(this.tokens.methodName, ['"', this.tokens.methodName.text, '"'])
1466
        );
1467
        if (this.args?.length > 0) {
9!
1468
            result.push(', ');
4✔
1469
            //transpile args
1470
            for (let i = 0; i < this.args.length; i++) {
4✔
1471
                //add comma between args
1472
                if (i > 0) {
7✔
1473
                    result.push(', ');
3✔
1474
                }
1475
                let arg = this.args[i];
7✔
1476
                result.push(...arg.transpile(state));
7✔
1477
            }
1478
        } else if (state.options.legacyCallfuncHandling) {
5✔
1479
            result.push(', ', 'invalid');
2✔
1480
        }
1481

1482
        result.push(
9✔
1483
            state.transpileToken(this.tokens.closingParen, ')')
1484
        );
1485
        return result;
9✔
1486
    }
1487

1488
    walk(visitor: WalkVisitor, options: WalkOptions) {
1489
        if (options.walkMode & InternalWalkMode.walkExpressions) {
132!
1490
            walk(this, 'callee', visitor, options);
132✔
1491
            walkArray(this.args, visitor, options, this);
132✔
1492
        }
1493
    }
1494

1495
    getType(options: GetTypeOptions) {
1496
        let result: BscType = DynamicType.instance;
4✔
1497
        // a little hacky here with checking options.ignoreCall because callFuncExpression has the method name
1498
        // It's nicer for CallExpression, because it's a call on any expression.
1499
        const calleeType = this.callee.getType({ ...options, flags: SymbolTypeFlag.runtime });
4✔
1500
        if (isComponentType(calleeType) || isReferenceType(calleeType)) {
4!
1501
            const funcType = (calleeType as ComponentType).getCallFuncType?.(this.tokens.methodName.text, options);
4!
1502
            if (funcType) {
4✔
1503
                options.typeChain?.push(new TypeChainEntry({
3✔
1504
                    name: this.tokens.methodName.text,
1505
                    type: funcType,
1506
                    data: options.data,
1507
                    location: this.tokens.methodName.location,
1508
                    separatorToken: createToken(TokenKind.Callfunc),
1509
                    astNode: this
1510
                }));
1511
                if (options.ignoreCall) {
3✔
1512
                    result = funcType;
1✔
1513
                }
1514
            }
1515
            /* TODO:
1516
                make callfunc return types work
1517
            else if (isCallableType(funcType) && (!isReferenceType(funcType.returnType) || funcType.returnType.isResolvable())) {
1518
                result = funcType.returnType;
1519
            } else if (!isReferenceType(funcType) && (funcType as any)?.returnType?.isResolvable()) {
1520
                result = (funcType as any).returnType;
1521
            } else {
1522
                return new TypePropertyReferenceType(funcType, 'returnType');
1523
            }
1524
            */
1525
        }
1526

1527
        return result;
4✔
1528
    }
1529

1530
    get leadingTrivia(): Token[] {
1531
        return this.callee.leadingTrivia;
18✔
1532
    }
1533
}
1534

1535
/**
1536
 * Since template strings can contain newlines, we need to concatenate multiple strings together with chr() calls.
1537
 * This is a single expression that represents the string contatenation of all parts of a single quasi.
1538
 */
1539
export class TemplateStringQuasiExpression extends Expression {
1✔
1540
    constructor(options: {
1541
        expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>;
1542
    }) {
1543
        super();
72✔
1544
        this.expressions = options.expressions;
72✔
1545
        this.location = util.createBoundingLocation(
72✔
1546
            ...this.expressions
1547
        );
1548
    }
1549

1550
    public readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>;
1551
    public readonly kind = AstNodeKind.TemplateStringQuasiExpression;
72✔
1552

1553
    readonly location: Location | undefined;
1554

1555
    transpile(state: BrsTranspileState, skipEmptyStrings = true) {
35✔
1556
        let result = [] as TranspileResult;
43✔
1557
        let plus = '';
43✔
1558
        for (let expression of this.expressions) {
43✔
1559
            //skip empty strings
1560
            //TODO what does an empty string literal expression look like?
1561
            if (expression.tokens.value.text === '' && skipEmptyStrings === true) {
68✔
1562
                continue;
27✔
1563
            }
1564
            result.push(
41✔
1565
                plus,
1566
                ...expression.transpile(state)
1567
            );
1568
            plus = ' + ';
41✔
1569
        }
1570
        return result;
43✔
1571
    }
1572

1573
    walk(visitor: WalkVisitor, options: WalkOptions) {
1574
        if (options.walkMode & InternalWalkMode.walkExpressions) {
314!
1575
            walkArray(this.expressions, visitor, options, this);
314✔
1576
        }
1577
    }
1578
}
1579

1580
export class TemplateStringExpression extends Expression {
1✔
1581
    constructor(options: {
1582
        openingBacktick?: Token;
1583
        quasis: TemplateStringQuasiExpression[];
1584
        expressions: Expression[];
1585
        closingBacktick?: Token;
1586
    }) {
1587
        super();
33✔
1588
        this.tokens = {
33✔
1589
            openingBacktick: options.openingBacktick,
1590
            closingBacktick: options.closingBacktick
1591
        };
1592
        this.quasis = options.quasis;
33✔
1593
        this.expressions = options.expressions;
33✔
1594
        this.location = util.createBoundingLocation(
33✔
1595
            this.tokens.openingBacktick,
1596
            this.quasis[0],
1597
            this.quasis[this.quasis.length - 1],
1598
            this.tokens.closingBacktick
1599
        );
1600
    }
1601

1602
    public readonly kind = AstNodeKind.TemplateStringExpression;
33✔
1603

1604
    public readonly tokens: {
1605
        readonly openingBacktick?: Token;
1606
        readonly closingBacktick?: Token;
1607
    };
1608
    public readonly quasis: TemplateStringQuasiExpression[];
1609
    public readonly expressions: Expression[];
1610

1611
    public readonly location: Location | undefined;
1612

1613
    public getType(options: GetTypeOptions) {
1614
        return StringType.instance;
34✔
1615
    }
1616

1617
    transpile(state: BrsTranspileState) {
1618
        if (this.quasis.length === 1 && this.expressions.length === 0) {
20✔
1619
            return this.quasis[0].transpile(state);
10✔
1620
        }
1621
        let result = ['('];
10✔
1622
        let plus = '';
10✔
1623
        //helper function to figure out when to include the plus
1624
        function add(...items) {
1625
            if (items.length > 0) {
40✔
1626
                result.push(
29✔
1627
                    plus,
1628
                    ...items
1629
                );
1630
            }
1631
            //set the plus after the first occurance of a nonzero length set of items
1632
            if (plus === '' && items.length > 0) {
40✔
1633
                plus = ' + ';
10✔
1634
            }
1635
        }
1636

1637
        for (let i = 0; i < this.quasis.length; i++) {
10✔
1638
            let quasi = this.quasis[i];
25✔
1639
            let expression = this.expressions[i];
25✔
1640

1641
            add(
25✔
1642
                ...quasi.transpile(state)
1643
            );
1644
            if (expression) {
25✔
1645
                //skip the toString wrapper around certain expressions
1646
                if (
15✔
1647
                    isEscapedCharCodeLiteralExpression(expression) ||
34✔
1648
                    (isLiteralExpression(expression) && isStringType(expression.getType()))
1649
                ) {
1650
                    add(
3✔
1651
                        ...expression.transpile(state)
1652
                    );
1653

1654
                    //wrap all other expressions with a bslib_toString call to prevent runtime type mismatch errors
1655
                } else {
1656
                    add(
12✔
1657
                        state.bslibPrefix + '_toString(',
1658
                        ...expression.transpile(state),
1659
                        ')'
1660
                    );
1661
                }
1662
            }
1663
        }
1664
        //the expression should be wrapped in parens so it can be used line a single expression at runtime
1665
        result.push(')');
10✔
1666

1667
        return result;
10✔
1668
    }
1669

1670
    walk(visitor: WalkVisitor, options: WalkOptions) {
1671
        if (options.walkMode & InternalWalkMode.walkExpressions) {
152!
1672
            //walk the quasis and expressions in left-to-right order
1673
            for (let i = 0; i < this.quasis.length; i++) {
152✔
1674
                walk(this.quasis, i, visitor, options, this);
257✔
1675

1676
                //this skips the final loop iteration since we'll always have one more quasi than expression
1677
                if (this.expressions[i]) {
257✔
1678
                    walk(this.expressions, i, visitor, options, this);
105✔
1679
                }
1680
            }
1681
        }
1682
    }
1683
}
1684

1685
export class TaggedTemplateStringExpression extends Expression {
1✔
1686
    constructor(options: {
1687
        tagName: Identifier;
1688
        openingBacktick?: Token;
1689
        quasis: TemplateStringQuasiExpression[];
1690
        expressions: Expression[];
1691
        closingBacktick?: Token;
1692
    }) {
1693
        super();
6✔
1694
        this.tokens = {
6✔
1695
            tagName: options.tagName,
1696
            openingBacktick: options.openingBacktick,
1697
            closingBacktick: options.closingBacktick
1698
        };
1699
        this.quasis = options.quasis;
6✔
1700
        this.expressions = options.expressions;
6✔
1701

1702
        this.location = util.createBoundingLocation(
6✔
1703
            this.tokens.tagName,
1704
            this.tokens.openingBacktick,
1705
            this.quasis[0],
1706
            this.quasis[this.quasis.length - 1],
1707
            this.tokens.closingBacktick
1708
        );
1709
    }
1710

1711
    public readonly kind = AstNodeKind.TaggedTemplateStringExpression;
6✔
1712

1713
    public readonly tokens: {
1714
        readonly tagName: Identifier;
1715
        readonly openingBacktick?: Token;
1716
        readonly closingBacktick?: Token;
1717
    };
1718

1719
    public readonly quasis: TemplateStringQuasiExpression[];
1720
    public readonly expressions: Expression[];
1721

1722
    public readonly location: Location | undefined;
1723

1724
    transpile(state: BrsTranspileState) {
1725
        let result = [] as TranspileResult;
3✔
1726
        result.push(
3✔
1727
            state.transpileToken(this.tokens.tagName),
1728
            '(['
1729
        );
1730

1731
        //add quasis as the first array
1732
        for (let i = 0; i < this.quasis.length; i++) {
3✔
1733
            let quasi = this.quasis[i];
8✔
1734
            //separate items with a comma
1735
            if (i > 0) {
8✔
1736
                result.push(
5✔
1737
                    ', '
1738
                );
1739
            }
1740
            result.push(
8✔
1741
                ...quasi.transpile(state, false)
1742
            );
1743
        }
1744
        result.push(
3✔
1745
            '], ['
1746
        );
1747

1748
        //add expressions as the second array
1749
        for (let i = 0; i < this.expressions.length; i++) {
3✔
1750
            let expression = this.expressions[i];
5✔
1751
            if (i > 0) {
5✔
1752
                result.push(
2✔
1753
                    ', '
1754
                );
1755
            }
1756
            result.push(
5✔
1757
                ...expression.transpile(state)
1758
            );
1759
        }
1760
        result.push(
3✔
1761
            state.sourceNode(this.tokens.closingBacktick, '])')
1762
        );
1763
        return result;
3✔
1764
    }
1765

1766
    walk(visitor: WalkVisitor, options: WalkOptions) {
1767
        if (options.walkMode & InternalWalkMode.walkExpressions) {
23!
1768
            //walk the quasis and expressions in left-to-right order
1769
            for (let i = 0; i < this.quasis.length; i++) {
23✔
1770
                walk(this.quasis, i, visitor, options, this);
57✔
1771

1772
                //this skips the final loop iteration since we'll always have one more quasi than expression
1773
                if (this.expressions[i]) {
57✔
1774
                    walk(this.expressions, i, visitor, options, this);
34✔
1775
                }
1776
            }
1777
        }
1778
    }
1779
}
1780

1781
export class AnnotationExpression extends Expression {
1✔
1782
    constructor(options: {
1783
        at?: Token;
1784
        name: Token;
1785
        call?: CallExpression;
1786
    }) {
1787
        super();
49✔
1788
        this.tokens = {
49✔
1789
            at: options.at,
1790
            name: options.name
1791
        };
1792
        this.call = options.call;
49✔
1793
        this.name = this.tokens.name.text;
49✔
1794
    }
1795

1796
    public readonly kind = AstNodeKind.AnnotationExpression;
49✔
1797

1798
    public readonly tokens: {
1799
        readonly at: Token;
1800
        readonly name: Token;
1801
    };
1802

1803
    public get location(): Location | undefined {
1804
        return util.createBoundingLocation(
25✔
1805
            this.tokens.at,
1806
            this.tokens.name,
1807
            this.call
1808
        );
1809
    }
1810

1811
    public readonly name: string;
1812

1813
    public call: CallExpression;
1814

1815
    /**
1816
     * Convert annotation arguments to JavaScript types
1817
     * @param strict If false, keep Expression objects not corresponding to JS types
1818
     */
1819
    getArguments(strict = true): ExpressionValue[] {
3✔
1820
        if (!this.call) {
4✔
1821
            return [];
1✔
1822
        }
1823
        return this.call.args.map(e => expressionToValue(e, strict));
13✔
1824
    }
1825

1826
    public get leadingTrivia(): Token[] {
1827
        return this.tokens.at?.leadingTrivia;
2!
1828
    }
1829

1830
    transpile(state: BrsTranspileState) {
1831
        return [];
3✔
1832
    }
1833

1834
    walk(visitor: WalkVisitor, options: WalkOptions) {
1835
        //nothing to walk
1836
    }
1837
    getTypedef(state: BrsTranspileState) {
1838
        return [
9✔
1839
            '@',
1840
            this.name,
1841
            ...(this.call?.transpile(state) ?? [])
54✔
1842
        ];
1843
    }
1844
}
1845

1846
export class TernaryExpression extends Expression {
1✔
1847
    constructor(options: {
1848
        test: Expression;
1849
        questionMark?: Token;
1850
        consequent?: Expression;
1851
        colon?: Token;
1852
        alternate?: Expression;
1853
    }) {
1854
        super();
76✔
1855
        this.tokens = {
76✔
1856
            questionMark: options.questionMark,
1857
            colon: options.colon
1858
        };
1859
        this.test = options.test;
76✔
1860
        this.consequent = options.consequent;
76✔
1861
        this.alternate = options.alternate;
76✔
1862
        this.location = util.createBoundingLocation(
76✔
1863
            this.test,
1864
            this.tokens.questionMark,
1865
            this.consequent,
1866
            this.tokens.colon,
1867
            this.alternate
1868
        );
1869
    }
1870

1871
    public readonly kind = AstNodeKind.TernaryExpression;
76✔
1872

1873
    public readonly location: Location | undefined;
1874

1875
    public readonly tokens: {
1876
        readonly questionMark?: Token;
1877
        readonly colon?: Token;
1878
    };
1879

1880
    public readonly test: Expression;
1881
    public readonly consequent?: Expression;
1882
    public readonly alternate?: Expression;
1883

1884
    transpile(state: BrsTranspileState) {
1885
        let result = [] as TranspileResult;
29✔
1886
        const file = state.file;
29✔
1887
        let consequentInfo = util.getExpressionInfo(this.consequent!, file);
29✔
1888
        let alternateInfo = util.getExpressionInfo(this.alternate!, file);
29✔
1889

1890
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1891
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
29✔
1892
        let mutatingExpressions = [
29✔
1893
            ...consequentInfo.expressions,
1894
            ...alternateInfo.expressions
1895
        ].filter(e => e instanceof CallExpression || e instanceof CallfuncExpression || e instanceof DottedGetExpression);
134✔
1896

1897
        if (mutatingExpressions.length > 0) {
29✔
1898
            result.push(
8✔
1899
                state.sourceNode(
1900
                    this.tokens.questionMark,
1901
                    //write all the scope variables as parameters.
1902
                    //TODO handle when there are more than 31 parameters
1903
                    `(function(${['__bsCondition', ...allUniqueVarNames].join(', ')})`
1904
                ),
1905
                state.newline,
1906
                //double indent so our `end function` line is still indented one at the end
1907
                state.indent(2),
1908
                state.sourceNode(this.test, `if __bsCondition then`),
1909
                state.newline,
1910
                state.indent(1),
1911
                state.sourceNode(this.consequent ?? this.tokens.questionMark, 'return '),
24!
1912
                ...this.consequent?.transpile(state) ?? [state.sourceNode(this.tokens.questionMark, 'invalid')],
48!
1913
                state.newline,
1914
                state.indent(-1),
1915
                state.sourceNode(this.consequent ?? this.tokens.questionMark, 'else'),
24!
1916
                state.newline,
1917
                state.indent(1),
1918
                state.sourceNode(this.consequent ?? this.tokens.questionMark, 'return '),
24!
1919
                ...this.alternate?.transpile(state) ?? [state.sourceNode(this.consequent ?? this.tokens.questionMark, 'invalid')],
48!
1920
                state.newline,
1921
                state.indent(-1),
1922
                state.sourceNode(this.tokens.questionMark, 'end if'),
1923
                state.newline,
1924
                state.indent(-1),
1925
                state.sourceNode(this.tokens.questionMark, 'end function)('),
1926
                ...this.test.transpile(state),
1927
                state.sourceNode(this.tokens.questionMark, `${['', ...allUniqueVarNames].join(', ')})`)
1928
            );
1929
            state.blockDepth--;
8✔
1930
        } else {
1931
            result.push(
21✔
1932
                state.sourceNode(this.test, state.bslibPrefix + `_ternary(`),
1933
                ...this.test.transpile(state),
1934
                state.sourceNode(this.test, `, `),
1935
                ...this.consequent?.transpile(state) ?? ['invalid'],
126✔
1936
                `, `,
1937
                ...this.alternate?.transpile(state) ?? ['invalid'],
126✔
1938
                `)`
1939
            );
1940
        }
1941
        return result;
29✔
1942
    }
1943

1944
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1945
        if (options.walkMode & InternalWalkMode.walkExpressions) {
220!
1946
            walk(this, 'test', visitor, options);
220✔
1947
            walk(this, 'consequent', visitor, options);
220✔
1948
            walk(this, 'alternate', visitor, options);
220✔
1949
        }
1950
    }
1951

1952
    get leadingTrivia(): Token[] {
NEW
1953
        return this.test.leadingTrivia;
×
1954
    }
1955
}
1956

1957
export class NullCoalescingExpression extends Expression {
1✔
1958
    constructor(options: {
1959
        consequent: Expression;
1960
        questionQuestion?: Token;
1961
        alternate: Expression;
1962
    }) {
1963
        super();
32✔
1964
        this.tokens = {
32✔
1965
            questionQuestion: options.questionQuestion
1966
        };
1967
        this.consequent = options.consequent;
32✔
1968
        this.alternate = options.alternate;
32✔
1969
        this.location = util.createBoundingLocation(
32✔
1970
            this.consequent,
1971
            this.tokens.questionQuestion,
1972
            this.alternate
1973
        );
1974
    }
1975

1976
    public readonly kind = AstNodeKind.NullCoalescingExpression;
32✔
1977

1978
    public readonly location: Location | undefined;
1979

1980
    public readonly tokens: {
1981
        readonly questionQuestion?: Token;
1982
    };
1983

1984
    public readonly consequent: Expression;
1985
    public readonly alternate: Expression;
1986

1987
    transpile(state: BrsTranspileState) {
1988
        let result = [] as TranspileResult;
10✔
1989
        let consequentInfo = util.getExpressionInfo(this.consequent, state.file);
10✔
1990
        let alternateInfo = util.getExpressionInfo(this.alternate, state.file);
10✔
1991

1992
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1993
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
10✔
1994
        let hasMutatingExpression = [
10✔
1995
            ...consequentInfo.expressions,
1996
            ...alternateInfo.expressions
1997
        ].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e));
28✔
1998

1999
        if (hasMutatingExpression) {
10✔
2000
            result.push(
6✔
2001
                `(function(`,
2002
                //write all the scope variables as parameters.
2003
                //TODO handle when there are more than 31 parameters
2004
                allUniqueVarNames.join(', '),
2005
                ')',
2006
                state.newline,
2007
                //double indent so our `end function` line is still indented one at the end
2008
                state.indent(2),
2009
                //evaluate the consequent exactly once, and then use it in the following condition
2010
                `__bsConsequent = `,
2011
                ...this.consequent.transpile(state),
2012
                state.newline,
2013
                state.indent(),
2014
                `if __bsConsequent <> invalid then`,
2015
                state.newline,
2016
                state.indent(1),
2017
                'return __bsConsequent',
2018
                state.newline,
2019
                state.indent(-1),
2020
                'else',
2021
                state.newline,
2022
                state.indent(1),
2023
                'return ',
2024
                ...this.alternate.transpile(state),
2025
                state.newline,
2026
                state.indent(-1),
2027
                'end if',
2028
                state.newline,
2029
                state.indent(-1),
2030
                'end function)(',
2031
                allUniqueVarNames.join(', '),
2032
                ')'
2033
            );
2034
            state.blockDepth--;
6✔
2035
        } else {
2036
            result.push(
4✔
2037
                state.bslibPrefix + `_coalesce(`,
2038
                ...this.consequent.transpile(state),
2039
                ', ',
2040
                ...this.alternate.transpile(state),
2041
                ')'
2042
            );
2043
        }
2044
        return result;
10✔
2045
    }
2046

2047
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2048
        if (options.walkMode & InternalWalkMode.walkExpressions) {
82!
2049
            walk(this, 'consequent', visitor, options);
82✔
2050
            walk(this, 'alternate', visitor, options);
82✔
2051
        }
2052
    }
2053

2054
    get leadingTrivia(): Token[] {
NEW
2055
        return this.consequent.leadingTrivia;
×
2056
    }
2057
}
2058

2059
export class RegexLiteralExpression extends Expression {
1✔
2060
    constructor(options: {
2061
        regexLiteral: Token;
2062
    }) {
2063
        super();
44✔
2064
        this.tokens = {
44✔
2065
            regexLiteral: options.regexLiteral
2066
        };
2067
    }
2068

2069
    public readonly kind = AstNodeKind.RegexLiteralExpression;
44✔
2070
    public readonly tokens: {
2071
        readonly regexLiteral: Token;
2072
    };
2073

2074
    public get location(): Location {
2075
        return this.tokens?.regexLiteral?.location;
79!
2076
    }
2077

2078
    public transpile(state: BrsTranspileState): TranspileResult {
2079
        let text = this.tokens.regexLiteral?.text ?? '';
42!
2080
        let flags = '';
42✔
2081
        //get any flags from the end
2082
        const flagMatch = /\/([a-z]+)$/i.exec(text);
42✔
2083
        if (flagMatch) {
42✔
2084
            text = text.substring(0, flagMatch.index + 1);
2✔
2085
            flags = flagMatch[1];
2✔
2086
        }
2087
        let pattern = text
42✔
2088
            //remove leading and trailing slashes
2089
            .substring(1, text.length - 1)
2090
            //escape quotemarks
2091
            .split('"').join('" + chr(34) + "');
2092

2093
        return [
42✔
2094
            state.sourceNode(this.tokens.regexLiteral, [
2095
                'CreateObject("roRegex", ',
2096
                `"${pattern}", `,
2097
                `"${flags}"`,
2098
                ')'
2099
            ])
2100
        ];
2101
    }
2102

2103
    walk(visitor: WalkVisitor, options: WalkOptions) {
2104
        //nothing to walk
2105
    }
2106

2107
    get leadingTrivia(): Token[] {
NEW
2108
        return this.tokens.regexLiteral?.leadingTrivia;
×
2109
    }
2110
}
2111

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

2115
function expressionToValue(expr: Expression, strict: boolean): ExpressionValue {
2116
    if (!expr) {
19!
2117
        return null;
×
2118
    }
2119
    if (isUnaryExpression(expr) && isLiteralNumber(expr.right)) {
19✔
2120
        return numberExpressionToValue(expr.right, expr.tokens.operator.text);
1✔
2121
    }
2122
    if (isLiteralString(expr)) {
18✔
2123
        //remove leading and trailing quotes
2124
        return expr.tokens.value.text.replace(/^"/, '').replace(/"$/, '');
4✔
2125
    }
2126
    if (isLiteralNumber(expr)) {
14✔
2127
        return numberExpressionToValue(expr);
6✔
2128
    }
2129

2130
    if (isLiteralBoolean(expr)) {
8✔
2131
        return expr.tokens.value.text.toLowerCase() === 'true';
2✔
2132
    }
2133
    if (isArrayLiteralExpression(expr)) {
6✔
2134
        return expr.elements
2✔
2135
            .map(e => expressionToValue(e, strict));
4✔
2136
    }
2137
    if (isAALiteralExpression(expr)) {
4✔
2138
        return expr.elements.reduce((acc, e) => {
2✔
2139
            acc[e.tokens.key.text] = expressionToValue(e.value, strict);
2✔
2140
            return acc;
2✔
2141
        }, {});
2142
    }
2143
    return strict ? null : expr;
2✔
2144
}
2145

2146
function numberExpressionToValue(expr: LiteralExpression, operator = '') {
6✔
2147
    if (isIntegerType(expr.getType()) || isLongIntegerType(expr.getType())) {
7!
2148
        return parseInt(operator + expr.tokens.value.text);
7✔
2149
    } else {
NEW
2150
        return parseFloat(operator + expr.tokens.value.text);
×
2151
    }
2152
}
2153

2154
export class TypeExpression extends Expression implements TypedefProvider {
1✔
2155
    constructor(options: {
2156
        /**
2157
         * The standard AST expression that represents the type for this TypeExpression.
2158
         */
2159
        expression: Expression;
2160
    }) {
2161
        super();
1,404✔
2162
        this.expression = options.expression;
1,404✔
2163
        this.location = this.expression?.location;
1,404!
2164
    }
2165

2166
    public readonly kind = AstNodeKind.TypeExpression;
1,404✔
2167

2168
    /**
2169
     * The standard AST expression that represents the type for this TypeExpression.
2170
     */
2171
    public readonly expression: Expression;
2172

2173
    public readonly location: Location;
2174

2175
    public transpile(state: BrsTranspileState): TranspileResult {
2176
        return [this.getType({ flags: SymbolTypeFlag.typetime }).toTypeString()];
100✔
2177
    }
2178
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2179
        if (options.walkMode & InternalWalkMode.walkExpressions) {
6,530✔
2180
            walk(this, 'expression', visitor, options);
6,406✔
2181
        }
2182
    }
2183

2184
    public getType(options: GetTypeOptions): BscType {
2185
        return this.expression.getType({ ...options, flags: SymbolTypeFlag.typetime });
4,711✔
2186
    }
2187

2188
    getTypedef(state: TranspileState): TranspileResult {
2189
        // TypeDefs should pass through any valid type names
2190
        return this.expression.transpile(state as BrsTranspileState);
33✔
2191
    }
2192

2193
    getName(parseMode = ParseMode.BrighterScript): string {
194✔
2194
        //TODO: this may not support Complex Types, eg. generics or Unions
2195
        return util.getAllDottedGetPartsAsString(this.expression, parseMode);
194✔
2196
    }
2197

2198
    getNameParts(): string[] {
2199
        //TODO: really, this code is only used to get Namespaces. It could be more clear.
2200
        return util.getAllDottedGetParts(this.expression).map(x => x.text);
20✔
2201
    }
2202
}
2203

2204
export class TypecastExpression extends Expression {
1✔
2205
    constructor(options: {
2206
        obj: Expression;
2207
        as?: Token;
2208
        typeExpression?: TypeExpression;
2209
    }) {
2210
        super();
63✔
2211
        this.tokens = {
63✔
2212
            as: options.as
2213
        };
2214
        this.obj = options.obj;
63✔
2215
        this.typeExpression = options.typeExpression;
63✔
2216
        this.location = util.createBoundingLocation(
63✔
2217
            this.obj,
2218
            this.tokens.as,
2219
            this.typeExpression
2220
        );
2221
    }
2222

2223
    public readonly kind = AstNodeKind.TypecastExpression;
63✔
2224

2225
    public readonly obj: Expression;
2226

2227
    public readonly tokens: {
2228
        readonly as?: Token;
2229
    };
2230

2231
    public typeExpression?: TypeExpression;
2232

2233
    public readonly location: Location;
2234

2235
    public transpile(state: BrsTranspileState): TranspileResult {
2236
        return this.obj.transpile(state);
13✔
2237
    }
2238
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2239
        if (options.walkMode & InternalWalkMode.walkExpressions) {
315!
2240
            walk(this, 'obj', visitor, options);
315✔
2241
            walk(this, 'typeExpression', visitor, options);
315✔
2242
        }
2243
    }
2244

2245
    public getType(options: GetTypeOptions): BscType {
2246
        const result = this.typeExpression.getType(options);
78✔
2247
        if (options.typeChain) {
78✔
2248
            // modify last typechain entry to show it is a typecast
2249
            const lastEntry = options.typeChain[options.typeChain.length - 1];
18✔
2250
            if (lastEntry) {
18!
2251
                lastEntry.astNode = this;
18✔
2252
            }
2253
        }
2254
        return result;
78✔
2255
    }
2256
}
2257

2258
export class TypedArrayExpression extends Expression {
1✔
2259
    constructor(options: {
2260
        innerType: Expression;
2261
        leftBracket?: Token;
2262
        rightBracket?: Token;
2263
    }) {
2264
        super();
27✔
2265
        this.tokens = {
27✔
2266
            leftBracket: options.leftBracket,
2267
            rightBracket: options.rightBracket
2268
        };
2269
        this.innerType = options.innerType;
27✔
2270
        this.location = util.createBoundingLocation(
27✔
2271
            this.innerType,
2272
            this.tokens.leftBracket,
2273
            this.tokens.rightBracket
2274
        );
2275
    }
2276

2277
    public readonly tokens: {
2278
        readonly leftBracket?: Token;
2279
        readonly rightBracket?: Token;
2280
    };
2281

2282
    public readonly innerType: Expression;
2283

2284
    public readonly kind = AstNodeKind.TypedArrayExpression;
27✔
2285

2286
    public readonly location: Location;
2287

2288
    public transpile(state: BrsTranspileState): TranspileResult {
NEW
2289
        return [this.getType({ flags: SymbolTypeFlag.typetime }).toTypeString()];
×
2290
    }
2291

2292
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2293
        if (options.walkMode & InternalWalkMode.walkExpressions) {
116!
2294
            walk(this, 'innerType', visitor, options);
116✔
2295
        }
2296
    }
2297

2298
    public getType(options: GetTypeOptions): BscType {
2299
        return new ArrayType(this.innerType.getType(options));
114✔
2300
    }
2301
}
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