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

rokucommunity / brighterscript / #12930

13 Aug 2024 05:02PM UTC coverage: 86.193% (-1.7%) from 87.933%
#12930

push

web-flow
Merge 58ad447a2 into 0e968f1c3

10630 of 13125 branches covered (80.99%)

Branch coverage included in aggregate %.

6675 of 7284 new or added lines in 99 files covered. (91.64%)

84 existing lines in 18 files now uncovered.

12312 of 13492 relevant lines covered (91.25%)

26865.48 hits per line

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

90.54
/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, 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

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,916✔
45
        this.tokens = {
2,916✔
46
            operator: options.operator
47
        };
48
        this.left = options.left;
2,916✔
49
        this.right = options.right;
2,916✔
50
        this.location = util.createBoundingLocation(this.left, this.tokens.operator, this.right);
2,916✔
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,916✔
60

61
    public readonly location: Location | undefined;
62

63
    transpile(state: BrsTranspileState): TranspileResult {
64
        return [
2,926✔
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,531!
75
            walk(this, 'left', visitor, options);
9,531✔
76
            walk(this, 'right', visitor, options);
9,531✔
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,198✔
121
        this.tokens = {
2,198✔
122
            openingParen: options.openingParen,
123
            closingParen: options.closingParen
124
        };
125
        this.callee = options.callee;
2,198✔
126
        this.args = options.args ?? [];
2,198✔
127
        this.location = util.createBoundingLocation(this.callee, this.tokens.openingParen, ...this.args, this.tokens.closingParen);
2,198✔
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,198✔
141

142
    public readonly location: Location | undefined;
143

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

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

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

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

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

205
    get leadingTrivia(): Token[] {
206
        return this.callee.leadingTrivia;
108✔
207
    }
208
}
209

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

244
    public readonly kind = AstNodeKind.FunctionExpression;
3,319✔
245

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

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

258
    /**
259
     * If this function is part of a FunctionStatement, this will be set. Otherwise this will be undefined
260
     */
261
    public functionStatement?: FunctionStatement;
262

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

267
    public get endTrivia(): Token[] {
268
        return this.tokens.endFunctionType?.leadingTrivia;
2!
269
    }
270

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

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

338
        const lastLocatable = hasBody ? this.body : this.returnTypeExpression ?? this.tokens.leftParen ?? this.tokens.functionType;
1,304!
339
        results.push(
1,304✔
340
            ...state.transpileEndBlockToken(lastLocatable, this.tokens.endFunctionType, `end ${this.tokens.functionType ?? 'function'}`)
3,912!
341
        );
342
        return results;
1,304✔
343
    }
344

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

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

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

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

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

442
    public readonly kind = AstNodeKind.FunctionParameterExpression;
2,716✔
443

444
    readonly tokens: {
445
        readonly name: Identifier;
446
        readonly equals?: Token;
447
        readonly as?: Token;
448
    };
449

450
    public readonly defaultValue?: Expression;
451
    public readonly typeExpression?: TypeExpression;
452

453
    public getType(options: GetTypeOptions) {
454
        const paramType = this.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime, typeChain: undefined }) ??
4,763✔
455
            this.defaultValue?.getType({ ...options, flags: SymbolTypeFlag.runtime, typeChain: undefined }) ??
11,897✔
456
            DynamicType.instance;
457
        options.typeChain?.push(new TypeChainEntry({ name: this.tokens.name.text, type: paramType, data: options.data, astNode: this }));
4,763✔
458
        return paramType;
4,763✔
459
    }
460

461
    public get location(): Location | undefined {
462
        return util.createBoundingLocation(
8,568✔
463
            this.tokens.name,
464
            this.tokens.as,
465
            this.typeExpression,
466
            this.tokens.equals,
467
            this.defaultValue
468
        );
469
    }
470

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

491
        return result;
1,984✔
492
    }
493

494
    public getTypedef(state: BrsTranspileState): TranspileResult {
495
        const results = [this.tokens.name.text] as TranspileResult;
73✔
496

497
        if (this.defaultValue) {
73!
498
            results.push(' = ', ...this.defaultValue.transpile(state));
×
499
        }
500

501
        if (this.tokens.as) {
73✔
502
            results.push(' as ');
6✔
503

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

511
        return results;
73✔
512
    }
513

514
    walk(visitor: WalkVisitor, options: WalkOptions) {
515
        // eslint-disable-next-line no-bitwise
516
        if (options.walkMode & InternalWalkMode.walkExpressions) {
11,463!
517
            walk(this, 'defaultValue', visitor, options);
11,463✔
518
            walk(this, 'typeExpression', visitor, options);
11,463✔
519
        }
520
    }
521

522
    get leadingTrivia(): Token[] {
523
        return this.tokens.name.leadingTrivia;
34✔
524
    }
525
}
526

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

543
        this.location = util.createBoundingLocation(this.obj, this.tokens.dot, this.tokens.name);
2,611✔
544
    }
545

546
    readonly tokens: {
547
        readonly name: Identifier;
548
        readonly dot?: Token;
549
    };
550
    readonly obj: Expression;
551

552
    public readonly kind = AstNodeKind.DottedGetExpression;
2,611✔
553

554
    public readonly location: Location | undefined;
555

556
    transpile(state: BrsTranspileState) {
557
        //if the callee starts with a namespace name, transpile the name
558
        if (state.file.calleeStartsWithNamespace(this)) {
828✔
559
            return [
8✔
560
                ...state.transpileLeadingCommentsForAstNode(this),
561
                state.sourceNode(this, this.getName(ParseMode.BrightScript))
562
            ];
563
        } else {
564
            return [
820✔
565
                ...this.obj.transpile(state),
566
                state.transpileToken(this.tokens.dot, '.'),
567
                state.transpileToken(this.tokens.name)
568
            ];
569
        }
570
    }
571

572
    walk(visitor: WalkVisitor, options: WalkOptions) {
573
        if (options.walkMode & InternalWalkMode.walkExpressions) {
9,972!
574
            walk(this, 'obj', visitor, options);
9,972✔
575
        }
576
    }
577

578
    getType(options: GetTypeOptions) {
579
        const objType = this.obj?.getType(options);
5,566!
580
        let result = objType?.getMemberType(this.tokens.name?.text, options);
5,566!
581

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

604
    getName(parseMode: ParseMode) {
605
        return util.getAllDottedGetPartsAsString(this, parseMode);
34✔
606
    }
607

608
    get leadingTrivia(): Token[] {
609
        return this.obj.leadingTrivia;
93✔
610
    }
611

612
}
613

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

629
    public readonly kind = AstNodeKind.XmlAttributeGetExpression;
10✔
630

631
    public readonly tokens: {
632
        name: Identifier;
633
        at?: Token;
634
    };
635

636
    public readonly obj: Expression;
637

638
    public readonly location: Location | undefined;
639

640
    transpile(state: BrsTranspileState) {
641
        return [
3✔
642
            ...this.obj.transpile(state),
643
            state.transpileToken(this.tokens.at, '@'),
644
            state.transpileToken(this.tokens.name)
645
        ];
646
    }
647

648
    walk(visitor: WalkVisitor, options: WalkOptions) {
649
        if (options.walkMode & InternalWalkMode.walkExpressions) {
28!
650
            walk(this, 'obj', visitor, options);
28✔
651
        }
652
    }
653

654
    get leadingTrivia(): Token[] {
NEW
655
        return this.obj.leadingTrivia;
×
656
    }
657
}
658

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

681
    public readonly kind = AstNodeKind.IndexedGetExpression;
143✔
682

683
    public readonly obj: Expression;
684
    public readonly indexes: Expression[];
685

686
    readonly tokens: {
687
        /**
688
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.` - defaults to '[' in transpile
689
         */
690
        readonly openingSquare?: Token;
691
        readonly closingSquare?: Token;
692
        readonly questionDot?: Token; //  ? or ?.
693
    };
694

695
    public readonly location: Location | undefined;
696

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

720
    walk(visitor: WalkVisitor, options: WalkOptions) {
721
        if (options.walkMode & InternalWalkMode.walkExpressions) {
492!
722
            walk(this, 'obj', visitor, options);
492✔
723
            walkArray(this.indexes, visitor, options, this);
492✔
724
        }
725
    }
726

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

736
    get leadingTrivia(): Token[] {
737
        return this.obj.leadingTrivia;
24✔
738
    }
739
}
740

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

756
    public readonly tokens: {
757
        readonly leftParen?: Token;
758
        readonly rightParen?: Token;
759
    };
760
    public readonly expression: Expression;
761

762
    public readonly kind = AstNodeKind.GroupingExpression;
46✔
763

764
    public readonly location: Location | undefined;
765

766
    transpile(state: BrsTranspileState) {
767
        if (isTypecastExpression(this.expression)) {
13✔
768
            return this.expression.transpile(state);
7✔
769
        }
770
        return [
6✔
771
            state.transpileToken(this.tokens.leftParen),
772
            ...this.expression.transpile(state),
773
            state.transpileToken(this.tokens.rightParen)
774
        ];
775
    }
776

777
    walk(visitor: WalkVisitor, options: WalkOptions) {
778
        if (options.walkMode & InternalWalkMode.walkExpressions) {
154!
779
            walk(this, 'expression', visitor, options);
154✔
780
        }
781
    }
782

783
    getType(options: GetTypeOptions) {
784
        return this.expression.getType(options);
62✔
785
    }
786

787
    get leadingTrivia(): Token[] {
788
        return this.tokens.leftParen?.leadingTrivia;
2!
789
    }
790
}
791

792
export class LiteralExpression extends Expression {
1✔
793
    constructor(options: {
794
        value: Token;
795
    }) {
796
        super();
6,879✔
797
        this.tokens = {
6,879✔
798
            value: options.value
799
        };
800
    }
801

802
    public readonly tokens: {
803
        readonly value: Token;
804
    };
805

806
    public readonly kind = AstNodeKind.LiteralExpression;
6,879✔
807

808
    public get location() {
809
        return this.tokens.value.location;
19,423✔
810
    }
811

812
    public getType(options?: GetTypeOptions) {
813
        return util.tokenToBscType(this.tokens.value);
3,174✔
814
    }
815

816
    transpile(state: BrsTranspileState) {
817
        let text: string;
818
        if (this.tokens.value.kind === TokenKind.TemplateStringQuasi) {
4,196✔
819
            //wrap quasis with quotes (and escape inner quotemarks)
820
            text = `"${this.tokens.value.text.replace(/"/g, '""')}"`;
28✔
821

822
        } else if (this.tokens.value.kind === TokenKind.StringLiteral) {
4,168✔
823
            text = this.tokens.value.text;
2,794✔
824
            //add trailing quotemark if it's missing. We will have already generated a diagnostic for this.
825
            if (text.endsWith('"') === false) {
2,794✔
826
                text += '"';
1✔
827
            }
828
        } else {
829
            text = this.tokens.value.text;
1,374✔
830
        }
831

832
        return [
4,196✔
833
            state.transpileToken({ ...this.tokens.value, text: text })
834
        ];
835
    }
836

837
    walk(visitor: WalkVisitor, options: WalkOptions) {
838
        //nothing to walk
839
    }
840

841
    get leadingTrivia(): Token[] {
842
        return this.tokens.value.leadingTrivia;
53✔
843
    }
844
}
845

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

859
    public readonly kind = AstNodeKind.EscapedCharCodeLiteralExpression;
29✔
860

861
    public readonly tokens: {
862
        readonly value: Token & { charCode: number };
863
    };
864

865
    public readonly location: Location;
866

867
    transpile(state: BrsTranspileState) {
868
        return [
13✔
869
            state.sourceNode(this, `chr(${this.tokens.value.charCode})`)
870
        ];
871
    }
872

873
    walk(visitor: WalkVisitor, options: WalkOptions) {
874
        //nothing to walk
875
    }
876
}
877

878
export class ArrayLiteralExpression extends Expression {
1✔
879
    constructor(options: {
880
        elements: Array<Expression>;
881
        open?: Token;
882
        close?: Token;
883
    }) {
884
        super();
129✔
885
        this.tokens = {
129✔
886
            open: options.open,
887
            close: options.close
888
        };
889
        this.elements = options.elements;
129✔
890
        this.location = util.createBoundingLocation(this.tokens.open, ...this.elements, this.tokens.close);
129✔
891
    }
892

893
    public readonly elements: Array<Expression>;
894

895
    public readonly tokens: {
896
        readonly open?: Token;
897
        readonly close?: Token;
898
    };
899

900
    public readonly kind = AstNodeKind.ArrayLiteralExpression;
129✔
901

902
    public readonly location: Location | undefined;
903

904
    transpile(state: BrsTranspileState) {
905
        let result: TranspileResult = [];
46✔
906
        result.push(
46✔
907
            state.transpileToken(this.tokens.open, '[')
908
        );
909
        let hasChildren = this.elements.length > 0;
46✔
910
        state.blockDepth++;
46✔
911

912
        for (let i = 0; i < this.elements.length; i++) {
46✔
913
            let previousElement = this.elements[i - 1];
53✔
914
            let element = this.elements[i];
53✔
915

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

933
        return result;
46✔
934
    }
935

936
    walk(visitor: WalkVisitor, options: WalkOptions) {
937
        if (options.walkMode & InternalWalkMode.walkExpressions) {
618!
938
            walkArray(this.elements, visitor, options, this);
618✔
939
        }
940
    }
941

942
    getType(options: GetTypeOptions): BscType {
943
        const innerTypes = this.elements.map(expr => expr.getType(options));
169✔
944
        return new ArrayType(...innerTypes);
127✔
945
    }
946
    get leadingTrivia(): Token[] {
947
        return this.tokens.open?.leadingTrivia;
8!
948
    }
949

950
    get endTrivia(): Token[] {
951
        return this.tokens.close?.leadingTrivia;
2!
952
    }
953
}
954

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

973
    public readonly kind = AstNodeKind.AAMemberExpression;
263✔
974

975
    public readonly location: Location | undefined;
976

977
    public readonly tokens: {
978
        readonly key: Token;
979
        readonly colon?: Token;
980
        readonly comma?: Token;
981
    };
982

983
    /** The expression evaluated to determine the member's initial value. */
984
    public readonly value: Expression;
985

986
    transpile(state: BrsTranspileState) {
987
        //TODO move the logic from AALiteralExpression loop into this function
988
        return [];
×
989
    }
990

991
    walk(visitor: WalkVisitor, options: WalkOptions) {
992
        walk(this, 'value', visitor, options);
877✔
993
    }
994

995
    getType(options: GetTypeOptions): BscType {
996
        return this.value.getType(options);
185✔
997
    }
998

999
    get leadingTrivia(): Token[] {
1000
        return this.tokens.key.leadingTrivia;
103✔
1001
    }
1002

1003
}
1004

1005
export class AALiteralExpression extends Expression {
1✔
1006
    constructor(options: {
1007
        elements: Array<AAMemberExpression>;
1008
        open?: Token;
1009
        close?: Token;
1010
    }) {
1011
        super();
250✔
1012
        this.tokens = {
250✔
1013
            open: options.open,
1014
            close: options.close
1015
        };
1016
        this.elements = options.elements;
250✔
1017
        this.location = util.createBoundingLocation(this.tokens.open, ...this.elements, this.tokens.close);
250✔
1018
    }
1019

1020
    public readonly elements: Array<AAMemberExpression>;
1021
    public readonly tokens: {
1022
        readonly open?: Token;
1023
        readonly close?: Token;
1024
    };
1025

1026
    public readonly kind = AstNodeKind.AALiteralExpression;
250✔
1027

1028
    public readonly location: Location | undefined;
1029

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

1047
            //don't indent if comment is same-line
1048
            if (util.isLeadingCommentOnSameLine(this.tokens.open, element) ||
35✔
1049
                util.isLeadingCommentOnSameLine(previousElement, element)) {
1050
                result.push(' ');
7✔
1051
            } else {
1052
                //indent line
1053
                result.push(state.indent());
28✔
1054
            }
1055

1056
            //key
1057
            result.push(
35✔
1058
                state.transpileToken(element.tokens.key)
1059
            );
1060
            //colon
1061
            result.push(
35✔
1062
                state.transpileToken(element.tokens.colon, ':'),
1063
                ' '
1064
            );
1065
            //value
1066
            result.push(...element.value.transpile(state));
35✔
1067

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

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

1079
        return result;
54✔
1080
    }
1081

1082
    walk(visitor: WalkVisitor, options: WalkOptions) {
1083
        if (options.walkMode & InternalWalkMode.walkExpressions) {
907!
1084
            walkArray(this.elements, visitor, options, this);
907✔
1085
        }
1086
    }
1087

1088
    getType(options: GetTypeOptions): BscType {
1089
        const resultType = new AssociativeArrayType();
185✔
1090
        resultType.addBuiltInInterfaces();
185✔
1091
        for (const element of this.elements) {
185✔
1092
            if (isAAMemberExpression(element)) {
185!
1093
                resultType.addMember(element.tokens.key.text, { definingNode: element }, element.getType(options), SymbolTypeFlag.runtime);
185✔
1094
            }
1095
        }
1096
        return resultType;
185✔
1097
    }
1098

1099
    public get leadingTrivia(): Token[] {
NEW
1100
        return this.tokens.open?.leadingTrivia;
×
1101
    }
1102

1103
    public get endTrivia(): Token[] {
1104
        return this.tokens.close?.leadingTrivia;
1!
1105
    }
1106

1107
}
1108

1109
export class UnaryExpression extends Expression {
1✔
1110
    constructor(options: {
1111
        operator: Token;
1112
        right: Expression;
1113
    }) {
1114
        super();
62✔
1115
        this.tokens = {
62✔
1116
            operator: options.operator
1117
        };
1118
        this.right = options.right;
62✔
1119
        this.location = util.createBoundingLocation(this.tokens.operator, this.right);
62✔
1120
    }
1121

1122
    public readonly kind = AstNodeKind.UnaryExpression;
62✔
1123

1124
    public readonly location: Location | undefined;
1125

1126
    public readonly tokens: {
1127
        readonly operator: Token;
1128
    };
1129
    public readonly right: Expression;
1130

1131
    transpile(state: BrsTranspileState) {
1132
        let separatingWhitespace: string | undefined;
1133
        if (isVariableExpression(this.right)) {
12✔
1134
            separatingWhitespace = this.right.tokens.name.leadingWhitespace;
6✔
1135
        } else if (isLiteralExpression(this.right)) {
6✔
1136
            separatingWhitespace = this.right.tokens.value.leadingWhitespace;
2✔
1137
        } else {
1138
            separatingWhitespace = ' ';
4✔
1139
        }
1140

1141
        return [
12✔
1142
            state.transpileToken(this.tokens.operator),
1143
            separatingWhitespace,
1144
            ...this.right.transpile(state)
1145
        ];
1146
    }
1147

1148
    walk(visitor: WalkVisitor, options: WalkOptions) {
1149
        if (options.walkMode & InternalWalkMode.walkExpressions) {
233!
1150
            walk(this, 'right', visitor, options);
233✔
1151
        }
1152
    }
1153

1154
    getType(options: GetTypeOptions): BscType {
1155
        return util.unaryOperatorResultType(this.tokens.operator, this.right.getType(options));
48✔
1156
    }
1157

1158
    public get leadingTrivia(): Token[] {
NEW
1159
        return this.tokens.operator.leadingTrivia;
×
1160
    }
1161
}
1162

1163
export class VariableExpression extends Expression {
1✔
1164
    constructor(options: {
1165
        name: Identifier;
1166
    }) {
1167
        super();
9,621✔
1168
        this.tokens = {
9,621✔
1169
            name: options.name
1170
        };
1171
        this.location = this.tokens.name?.location;
9,621!
1172
    }
1173

1174
    public readonly tokens: {
1175
        readonly name: Identifier;
1176
    };
1177

1178
    public readonly kind = AstNodeKind.VariableExpression;
9,621✔
1179

1180
    public readonly location: Location;
1181

1182
    public getName(parseMode?: ParseMode) {
1183
        return this.tokens.name.text;
13,726✔
1184
    }
1185

1186
    transpile(state: BrsTranspileState) {
1187
        let result: TranspileResult = [];
5,673✔
1188
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
5,673✔
1189
        //if the callee is the name of a known namespace function
1190
        if (namespace && state.file.calleeIsKnownNamespaceFunction(this, namespace.getName(ParseMode.BrighterScript))) {
5,673✔
1191
            result.push(
9✔
1192
                //transpile leading comments since the token isn't being transpiled directly
1193
                ...state.transpileLeadingCommentsForAstNode(this),
1194
                state.sourceNode(this, [
1195
                    namespace.getName(ParseMode.BrightScript),
1196
                    '_',
1197
                    this.getName(ParseMode.BrightScript)
1198
                ])
1199
            );
1200

1201
            //transpile  normally
1202
        } else {
1203
            result.push(
5,664✔
1204
                state.transpileToken(this.tokens.name)
1205
            );
1206
        }
1207
        return result;
5,673✔
1208
    }
1209

1210
    walk(visitor: WalkVisitor, options: WalkOptions) {
1211
        //nothing to walk
1212
    }
1213

1214

1215
    getType(options: GetTypeOptions) {
1216
        let resultType: BscType = util.tokenToBscType(this.tokens.name);
13,595✔
1217
        const nameKey = this.getName();
13,595✔
1218
        if (!resultType) {
13,595✔
1219
            const symbolTable = this.getSymbolTable();
9,804✔
1220
            resultType = symbolTable?.getSymbolType(nameKey, { ...options, fullName: nameKey, tableProvider: () => this.getSymbolTable() });
21,252!
1221

1222
            if (util.isClassUsedAsFunction(resultType, this, options)) {
9,804✔
1223
                resultType = FunctionType.instance;
20✔
1224
            }
1225

1226
        }
1227
        options?.typeChain?.push(new TypeChainEntry({ name: nameKey, type: resultType, data: options.data, astNode: this }));
13,595!
1228
        return resultType;
13,595✔
1229
    }
1230

1231
    get leadingTrivia(): Token[] {
1232
        return this.tokens.name.leadingTrivia;
238✔
1233
    }
1234
}
1235

1236
export class SourceLiteralExpression extends Expression {
1✔
1237
    constructor(options: {
1238
        value: Token;
1239
    }) {
1240
        super();
35✔
1241
        this.tokens = {
35✔
1242
            value: options.value
1243
        };
1244
        this.location = this.tokens.value?.location;
35!
1245
    }
1246

1247
    public readonly location: Location;
1248

1249
    public readonly kind = AstNodeKind.SourceLiteralExpression;
35✔
1250

1251
    public readonly tokens: {
1252
        readonly value: Token;
1253
    };
1254

1255
    /**
1256
     * Find the index of the function in its parent
1257
     */
1258
    private findFunctionIndex(parentFunction: FunctionExpression, func: FunctionExpression) {
1259
        let index = -1;
4✔
1260
        parentFunction.findChild((node) => {
4✔
1261
            if (isFunctionExpression(node)) {
12✔
1262
                index++;
4✔
1263
                if (node === func) {
4!
1264
                    return true;
4✔
1265
                }
1266
            }
1267
        }, {
1268
            walkMode: WalkMode.visitAllRecursive
1269
        });
1270
        return index;
4✔
1271
    }
1272

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

1289
    /**
1290
     * Get the line number from our token or from the closest ancestor that has a range
1291
     */
1292
    private getClosestLineNumber() {
1293
        let node: AstNode = this;
7✔
1294
        while (node) {
7✔
1295
            if (node.location?.range) {
17✔
1296
                return node.location.range.start.line + 1;
5✔
1297
            }
1298
            node = node.parent;
12✔
1299
        }
1300
        return -1;
2✔
1301
    }
1302

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

1337
        }
1338
        return [
31✔
1339
            state.sourceNode(this, text)
1340
        ];
1341
    }
1342

1343
    walk(visitor: WalkVisitor, options: WalkOptions) {
1344
        //nothing to walk
1345
    }
1346

1347
    get leadingTrivia(): Token[] {
NEW
1348
        return this.tokens.value.leadingTrivia;
×
1349
    }
1350
}
1351

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

1370
    public readonly kind = AstNodeKind.NewExpression;
128✔
1371

1372
    public readonly location: Location | undefined;
1373

1374
    public readonly tokens: {
1375
        readonly new?: Token;
1376
    };
1377
    public readonly call: CallExpression;
1378

1379
    /**
1380
     * The name of the class to initialize (with optional namespace prefixed)
1381
     */
1382
    public get className() {
1383
        //the parser guarantees the callee of a new statement's call object will be
1384
        //either a VariableExpression or a DottedGet
1385
        return this.call.callee as (VariableExpression | DottedGetExpression);
92✔
1386
    }
1387

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

1399
    walk(visitor: WalkVisitor, options: WalkOptions) {
1400
        if (options.walkMode & InternalWalkMode.walkExpressions) {
813!
1401
            walk(this, 'call', visitor, options);
813✔
1402
        }
1403
    }
1404

1405
    getType(options: GetTypeOptions) {
1406
        const result = this.call.getType(options);
309✔
1407
        if (options.typeChain) {
309✔
1408
            // modify last typechain entry to show it is a new ...()
1409
            const lastEntry = options.typeChain[options.typeChain.length - 1];
3✔
1410
            if (lastEntry) {
3!
1411
                lastEntry.astNode = this;
3✔
1412
            }
1413
        }
1414
        return result;
309✔
1415
    }
1416

1417
    get leadingTrivia(): Token[] {
NEW
1418
        return this.tokens.new.leadingTrivia;
×
1419
    }
1420
}
1421

1422
export class CallfuncExpression extends Expression {
1✔
1423
    constructor(options: {
1424
        callee: Expression;
1425
        operator?: Token;
1426
        methodName: Identifier;
1427
        openingParen?: Token;
1428
        args?: Expression[];
1429
        closingParen?: Token;
1430
    }) {
1431
        super();
26✔
1432
        this.tokens = {
26✔
1433
            operator: options.operator,
1434
            methodName: options.methodName,
1435
            openingParen: options.openingParen,
1436
            closingParen: options.closingParen
1437
        };
1438
        this.callee = options.callee;
26✔
1439
        this.args = options.args ?? [];
26!
1440

1441
        this.location = util.createBoundingLocation(
26✔
1442
            this.callee,
1443
            this.tokens.operator,
1444
            this.tokens.methodName,
1445
            this.tokens.openingParen,
1446
            ...this.args,
1447
            this.tokens.closingParen
1448
        );
1449
    }
1450

1451
    public readonly callee: Expression;
1452
    public readonly args: Expression[];
1453

1454
    public readonly tokens: {
1455
        readonly operator: Token;
1456
        readonly methodName: Identifier;
1457
        readonly openingParen?: Token;
1458
        readonly closingParen?: Token;
1459
    };
1460

1461
    public readonly kind = AstNodeKind.CallfuncExpression;
26✔
1462

1463
    public readonly location: Location | undefined;
1464

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

1489
        result.push(
9✔
1490
            state.transpileToken(this.tokens.closingParen, ')')
1491
        );
1492
        return result;
9✔
1493
    }
1494

1495
    walk(visitor: WalkVisitor, options: WalkOptions) {
1496
        if (options.walkMode & InternalWalkMode.walkExpressions) {
132!
1497
            walk(this, 'callee', visitor, options);
132✔
1498
            walkArray(this.args, visitor, options, this);
132✔
1499
        }
1500
    }
1501

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

1534
        return result;
4✔
1535
    }
1536

1537
    get leadingTrivia(): Token[] {
1538
        return this.callee.leadingTrivia;
18✔
1539
    }
1540
}
1541

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

1557
    public readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>;
1558
    public readonly kind = AstNodeKind.TemplateStringQuasiExpression;
74✔
1559

1560
    readonly location: Location | undefined;
1561

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

1580
    walk(visitor: WalkVisitor, options: WalkOptions) {
1581
        if (options.walkMode & InternalWalkMode.walkExpressions) {
314!
1582
            walkArray(this.expressions, visitor, options, this);
314✔
1583
        }
1584
    }
1585
}
1586

1587
export class TemplateStringExpression extends Expression {
1✔
1588
    constructor(options: {
1589
        openingBacktick?: Token;
1590
        quasis: TemplateStringQuasiExpression[];
1591
        expressions: Expression[];
1592
        closingBacktick?: Token;
1593
    }) {
1594
        super();
35✔
1595
        this.tokens = {
35✔
1596
            openingBacktick: options.openingBacktick,
1597
            closingBacktick: options.closingBacktick
1598
        };
1599
        this.quasis = options.quasis;
35✔
1600
        this.expressions = options.expressions;
35✔
1601
        this.location = util.createBoundingLocation(
35✔
1602
            this.tokens.openingBacktick,
1603
            this.quasis[0],
1604
            this.quasis[this.quasis.length - 1],
1605
            this.tokens.closingBacktick
1606
        );
1607
    }
1608

1609
    public readonly kind = AstNodeKind.TemplateStringExpression;
35✔
1610

1611
    public readonly tokens: {
1612
        readonly openingBacktick?: Token;
1613
        readonly closingBacktick?: Token;
1614
    };
1615
    public readonly quasis: TemplateStringQuasiExpression[];
1616
    public readonly expressions: Expression[];
1617

1618
    public readonly location: Location | undefined;
1619

1620
    public getType(options: GetTypeOptions) {
1621
        return StringType.instance;
34✔
1622
    }
1623

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

1644
        for (let i = 0; i < this.quasis.length; i++) {
10✔
1645
            let quasi = this.quasis[i];
25✔
1646
            let expression = this.expressions[i];
25✔
1647

1648
            add(
25✔
1649
                ...quasi.transpile(state)
1650
            );
1651
            if (expression) {
25✔
1652
                //skip the toString wrapper around certain expressions
1653
                if (
15✔
1654
                    isEscapedCharCodeLiteralExpression(expression) ||
34✔
1655
                    (isLiteralExpression(expression) && isStringType(expression.getType()))
1656
                ) {
1657
                    add(
3✔
1658
                        ...expression.transpile(state)
1659
                    );
1660

1661
                    //wrap all other expressions with a bslib_toString call to prevent runtime type mismatch errors
1662
                } else {
1663
                    add(
12✔
1664
                        state.bslibPrefix + '_toString(',
1665
                        ...expression.transpile(state),
1666
                        ')'
1667
                    );
1668
                }
1669
            }
1670
        }
1671
        //the expression should be wrapped in parens so it can be used line a single expression at runtime
1672
        result.push(')');
10✔
1673

1674
        return result;
10✔
1675
    }
1676

1677
    walk(visitor: WalkVisitor, options: WalkOptions) {
1678
        if (options.walkMode & InternalWalkMode.walkExpressions) {
152!
1679
            //walk the quasis and expressions in left-to-right order
1680
            for (let i = 0; i < this.quasis.length; i++) {
152✔
1681
                walk(this.quasis, i, visitor, options, this);
257✔
1682

1683
                //this skips the final loop iteration since we'll always have one more quasi than expression
1684
                if (this.expressions[i]) {
257✔
1685
                    walk(this.expressions, i, visitor, options, this);
105✔
1686
                }
1687
            }
1688
        }
1689
    }
1690
}
1691

1692
export class TaggedTemplateStringExpression extends Expression {
1✔
1693
    constructor(options: {
1694
        tagName: Identifier;
1695
        openingBacktick?: Token;
1696
        quasis: TemplateStringQuasiExpression[];
1697
        expressions: Expression[];
1698
        closingBacktick?: Token;
1699
    }) {
1700
        super();
6✔
1701
        this.tokens = {
6✔
1702
            tagName: options.tagName,
1703
            openingBacktick: options.openingBacktick,
1704
            closingBacktick: options.closingBacktick
1705
        };
1706
        this.quasis = options.quasis;
6✔
1707
        this.expressions = options.expressions;
6✔
1708

1709
        this.location = util.createBoundingLocation(
6✔
1710
            this.tokens.tagName,
1711
            this.tokens.openingBacktick,
1712
            this.quasis[0],
1713
            this.quasis[this.quasis.length - 1],
1714
            this.tokens.closingBacktick
1715
        );
1716
    }
1717

1718
    public readonly kind = AstNodeKind.TaggedTemplateStringExpression;
6✔
1719

1720
    public readonly tokens: {
1721
        readonly tagName: Identifier;
1722
        readonly openingBacktick?: Token;
1723
        readonly closingBacktick?: Token;
1724
    };
1725

1726
    public readonly quasis: TemplateStringQuasiExpression[];
1727
    public readonly expressions: Expression[];
1728

1729
    public readonly location: Location | undefined;
1730

1731
    transpile(state: BrsTranspileState) {
1732
        let result = [] as TranspileResult;
3✔
1733
        result.push(
3✔
1734
            state.transpileToken(this.tokens.tagName),
1735
            '(['
1736
        );
1737

1738
        //add quasis as the first array
1739
        for (let i = 0; i < this.quasis.length; i++) {
3✔
1740
            let quasi = this.quasis[i];
8✔
1741
            //separate items with a comma
1742
            if (i > 0) {
8✔
1743
                result.push(
5✔
1744
                    ', '
1745
                );
1746
            }
1747
            result.push(
8✔
1748
                ...quasi.transpile(state, false)
1749
            );
1750
        }
1751
        result.push(
3✔
1752
            '], ['
1753
        );
1754

1755
        //add expressions as the second array
1756
        for (let i = 0; i < this.expressions.length; i++) {
3✔
1757
            let expression = this.expressions[i];
5✔
1758
            if (i > 0) {
5✔
1759
                result.push(
2✔
1760
                    ', '
1761
                );
1762
            }
1763
            result.push(
5✔
1764
                ...expression.transpile(state)
1765
            );
1766
        }
1767
        result.push(
3✔
1768
            state.sourceNode(this.tokens.closingBacktick, '])')
1769
        );
1770
        return result;
3✔
1771
    }
1772

1773
    walk(visitor: WalkVisitor, options: WalkOptions) {
1774
        if (options.walkMode & InternalWalkMode.walkExpressions) {
23!
1775
            //walk the quasis and expressions in left-to-right order
1776
            for (let i = 0; i < this.quasis.length; i++) {
23✔
1777
                walk(this.quasis, i, visitor, options, this);
57✔
1778

1779
                //this skips the final loop iteration since we'll always have one more quasi than expression
1780
                if (this.expressions[i]) {
57✔
1781
                    walk(this.expressions, i, visitor, options, this);
34✔
1782
                }
1783
            }
1784
        }
1785
    }
1786
}
1787

1788
export class AnnotationExpression extends Expression {
1✔
1789
    constructor(options: {
1790
        at?: Token;
1791
        name: Token;
1792
        call?: CallExpression;
1793
    }) {
1794
        super();
68✔
1795
        this.tokens = {
68✔
1796
            at: options.at,
1797
            name: options.name
1798
        };
1799
        this.call = options.call;
68✔
1800
        this.name = this.tokens.name.text;
68✔
1801
    }
1802

1803
    public readonly kind = AstNodeKind.AnnotationExpression;
68✔
1804

1805
    public readonly tokens: {
1806
        readonly at: Token;
1807
        readonly name: Token;
1808
    };
1809

1810
    public get location(): Location | undefined {
1811
        return util.createBoundingLocation(
51✔
1812
            this.tokens.at,
1813
            this.tokens.name,
1814
            this.call
1815
        );
1816
    }
1817

1818
    public readonly name: string;
1819

1820
    public call: CallExpression;
1821

1822
    /**
1823
     * Convert annotation arguments to JavaScript types
1824
     * @param strict If false, keep Expression objects not corresponding to JS types
1825
     */
1826
    getArguments(strict = true): ExpressionValue[] {
10✔
1827
        if (!this.call) {
11✔
1828
            return [];
1✔
1829
        }
1830
        return this.call.args.map(e => expressionToValue(e, strict));
20✔
1831
    }
1832

1833
    public get leadingTrivia(): Token[] {
1834
        return this.tokens.at?.leadingTrivia;
18!
1835
    }
1836

1837
    transpile(state: BrsTranspileState) {
1838
        //transpile only our leading comments
1839
        return state.transpileComments(this.leadingTrivia);
16✔
1840
    }
1841

1842
    walk(visitor: WalkVisitor, options: WalkOptions) {
1843
        //nothing to walk
1844
    }
1845
    getTypedef(state: BrsTranspileState) {
1846
        return [
9✔
1847
            '@',
1848
            this.name,
1849
            ...(this.call?.transpile(state) ?? [])
54✔
1850
        ];
1851
    }
1852
}
1853

1854
export class TernaryExpression extends Expression {
1✔
1855
    constructor(options: {
1856
        test: Expression;
1857
        questionMark?: Token;
1858
        consequent?: Expression;
1859
        colon?: Token;
1860
        alternate?: Expression;
1861
    }) {
1862
        super();
76✔
1863
        this.tokens = {
76✔
1864
            questionMark: options.questionMark,
1865
            colon: options.colon
1866
        };
1867
        this.test = options.test;
76✔
1868
        this.consequent = options.consequent;
76✔
1869
        this.alternate = options.alternate;
76✔
1870
        this.location = util.createBoundingLocation(
76✔
1871
            this.test,
1872
            this.tokens.questionMark,
1873
            this.consequent,
1874
            this.tokens.colon,
1875
            this.alternate
1876
        );
1877
    }
1878

1879
    public readonly kind = AstNodeKind.TernaryExpression;
76✔
1880

1881
    public readonly location: Location | undefined;
1882

1883
    public readonly tokens: {
1884
        readonly questionMark?: Token;
1885
        readonly colon?: Token;
1886
    };
1887

1888
    public readonly test: Expression;
1889
    public readonly consequent?: Expression;
1890
    public readonly alternate?: Expression;
1891

1892
    transpile(state: BrsTranspileState) {
1893
        let result = [] as TranspileResult;
29✔
1894
        const file = state.file;
29✔
1895
        let consequentInfo = util.getExpressionInfo(this.consequent!, file);
29✔
1896
        let alternateInfo = util.getExpressionInfo(this.alternate!, file);
29✔
1897

1898
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1899
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
29✔
1900
        let mutatingExpressions = [
29✔
1901
            ...consequentInfo.expressions,
1902
            ...alternateInfo.expressions
1903
        ].filter(e => e instanceof CallExpression || e instanceof CallfuncExpression || e instanceof DottedGetExpression);
134✔
1904

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

1952
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1953
        if (options.walkMode & InternalWalkMode.walkExpressions) {
220!
1954
            walk(this, 'test', visitor, options);
220✔
1955
            walk(this, 'consequent', visitor, options);
220✔
1956
            walk(this, 'alternate', visitor, options);
220✔
1957
        }
1958
    }
1959

1960
    get leadingTrivia(): Token[] {
NEW
1961
        return this.test.leadingTrivia;
×
1962
    }
1963
}
1964

1965
export class NullCoalescingExpression extends Expression {
1✔
1966
    constructor(options: {
1967
        consequent: Expression;
1968
        questionQuestion?: Token;
1969
        alternate: Expression;
1970
    }) {
1971
        super();
32✔
1972
        this.tokens = {
32✔
1973
            questionQuestion: options.questionQuestion
1974
        };
1975
        this.consequent = options.consequent;
32✔
1976
        this.alternate = options.alternate;
32✔
1977
        this.location = util.createBoundingLocation(
32✔
1978
            this.consequent,
1979
            this.tokens.questionQuestion,
1980
            this.alternate
1981
        );
1982
    }
1983

1984
    public readonly kind = AstNodeKind.NullCoalescingExpression;
32✔
1985

1986
    public readonly location: Location | undefined;
1987

1988
    public readonly tokens: {
1989
        readonly questionQuestion?: Token;
1990
    };
1991

1992
    public readonly consequent: Expression;
1993
    public readonly alternate: Expression;
1994

1995
    transpile(state: BrsTranspileState) {
1996
        let result = [] as TranspileResult;
10✔
1997
        let consequentInfo = util.getExpressionInfo(this.consequent, state.file);
10✔
1998
        let alternateInfo = util.getExpressionInfo(this.alternate, state.file);
10✔
1999

2000
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
2001
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
10✔
2002
        let hasMutatingExpression = [
10✔
2003
            ...consequentInfo.expressions,
2004
            ...alternateInfo.expressions
2005
        ].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e));
28✔
2006

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

2055
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2056
        if (options.walkMode & InternalWalkMode.walkExpressions) {
82!
2057
            walk(this, 'consequent', visitor, options);
82✔
2058
            walk(this, 'alternate', visitor, options);
82✔
2059
        }
2060
    }
2061

2062
    get leadingTrivia(): Token[] {
NEW
2063
        return this.consequent.leadingTrivia;
×
2064
    }
2065
}
2066

2067
export class RegexLiteralExpression extends Expression {
1✔
2068
    constructor(options: {
2069
        regexLiteral: Token;
2070
    }) {
2071
        super();
44✔
2072
        this.tokens = {
44✔
2073
            regexLiteral: options.regexLiteral
2074
        };
2075
    }
2076

2077
    public readonly kind = AstNodeKind.RegexLiteralExpression;
44✔
2078
    public readonly tokens: {
2079
        readonly regexLiteral: Token;
2080
    };
2081

2082
    public get location(): Location {
2083
        return this.tokens?.regexLiteral?.location;
79!
2084
    }
2085

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

2101
        return [
42✔
2102
            state.sourceNode(this.tokens.regexLiteral, [
2103
                'CreateObject("roRegex", ',
2104
                `"${pattern}", `,
2105
                `"${flags}"`,
2106
                ')'
2107
            ])
2108
        ];
2109
    }
2110

2111
    walk(visitor: WalkVisitor, options: WalkOptions) {
2112
        //nothing to walk
2113
    }
2114

2115
    get leadingTrivia(): Token[] {
NEW
2116
        return this.tokens.regexLiteral?.leadingTrivia;
×
2117
    }
2118
}
2119

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

2123
function expressionToValue(expr: Expression, strict: boolean): ExpressionValue {
2124
    if (!expr) {
30!
2125
        return null;
×
2126
    }
2127
    if (isUnaryExpression(expr) && isLiteralNumber(expr.right)) {
30✔
2128
        return numberExpressionToValue(expr.right, expr.tokens.operator.text);
1✔
2129
    }
2130
    if (isLiteralString(expr)) {
29✔
2131
        //remove leading and trailing quotes
2132
        return expr.tokens.value.text.replace(/^"/, '').replace(/"$/, '');
5✔
2133
    }
2134
    if (isLiteralNumber(expr)) {
24✔
2135
        return numberExpressionToValue(expr);
11✔
2136
    }
2137

2138
    if (isLiteralBoolean(expr)) {
13✔
2139
        return expr.tokens.value.text.toLowerCase() === 'true';
3✔
2140
    }
2141
    if (isArrayLiteralExpression(expr)) {
10✔
2142
        return expr.elements
3✔
2143
            .map(e => expressionToValue(e, strict));
7✔
2144
    }
2145
    if (isAALiteralExpression(expr)) {
7✔
2146
        return expr.elements.reduce((acc, e) => {
3✔
2147
            acc[e.tokens.key.text] = expressionToValue(e.value, strict);
3✔
2148
            return acc;
3✔
2149
        }, {});
2150
    }
2151
    //for annotations, we only support serializing pure string values
2152
    if (isTemplateStringExpression(expr)) {
4✔
2153
        if (expr.quasis?.length === 1 && expr.expressions.length === 0) {
2!
2154
            return expr.quasis[0].expressions.map(x => x.tokens.value.text).join('');
10✔
2155
        }
2156
    }
2157
    return strict ? null : expr;
2✔
2158
}
2159

2160
function numberExpressionToValue(expr: LiteralExpression, operator = '') {
11✔
2161
    if (isIntegerType(expr.getType()) || isLongIntegerType(expr.getType())) {
12!
2162
        return parseInt(operator + expr.tokens.value.text);
12✔
2163
    } else {
NEW
2164
        return parseFloat(operator + expr.tokens.value.text);
×
2165
    }
2166
}
2167

2168
export class TypeExpression extends Expression implements TypedefProvider {
1✔
2169
    constructor(options: {
2170
        /**
2171
         * The standard AST expression that represents the type for this TypeExpression.
2172
         */
2173
        expression: Expression;
2174
    }) {
2175
        super();
1,411✔
2176
        this.expression = options.expression;
1,411✔
2177
        this.location = this.expression?.location;
1,411!
2178
    }
2179

2180
    public readonly kind = AstNodeKind.TypeExpression;
1,411✔
2181

2182
    /**
2183
     * The standard AST expression that represents the type for this TypeExpression.
2184
     */
2185
    public readonly expression: Expression;
2186

2187
    public readonly location: Location;
2188

2189
    public transpile(state: BrsTranspileState): TranspileResult {
2190
        return [this.getType({ flags: SymbolTypeFlag.typetime }).toTypeString()];
100✔
2191
    }
2192
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2193
        if (options.walkMode & InternalWalkMode.walkExpressions) {
6,565✔
2194
            walk(this, 'expression', visitor, options);
6,440✔
2195
        }
2196
    }
2197

2198
    public getType(options: GetTypeOptions): BscType {
2199
        return this.expression.getType({ ...options, flags: SymbolTypeFlag.typetime });
4,736✔
2200
    }
2201

2202
    getTypedef(state: TranspileState): TranspileResult {
2203
        // TypeDefs should pass through any valid type names
2204
        return this.expression.transpile(state as BrsTranspileState);
33✔
2205
    }
2206

2207
    getName(parseMode = ParseMode.BrighterScript): string {
195✔
2208
        //TODO: this may not support Complex Types, eg. generics or Unions
2209
        return util.getAllDottedGetPartsAsString(this.expression, parseMode);
195✔
2210
    }
2211

2212
    getNameParts(): string[] {
2213
        //TODO: really, this code is only used to get Namespaces. It could be more clear.
2214
        return util.getAllDottedGetParts(this.expression).map(x => x.text);
20✔
2215
    }
2216
}
2217

2218
export class TypecastExpression extends Expression {
1✔
2219
    constructor(options: {
2220
        obj: Expression;
2221
        as?: Token;
2222
        typeExpression?: TypeExpression;
2223
    }) {
2224
        super();
63✔
2225
        this.tokens = {
63✔
2226
            as: options.as
2227
        };
2228
        this.obj = options.obj;
63✔
2229
        this.typeExpression = options.typeExpression;
63✔
2230
        this.location = util.createBoundingLocation(
63✔
2231
            this.obj,
2232
            this.tokens.as,
2233
            this.typeExpression
2234
        );
2235
    }
2236

2237
    public readonly kind = AstNodeKind.TypecastExpression;
63✔
2238

2239
    public readonly obj: Expression;
2240

2241
    public readonly tokens: {
2242
        readonly as?: Token;
2243
    };
2244

2245
    public typeExpression?: TypeExpression;
2246

2247
    public readonly location: Location;
2248

2249
    public transpile(state: BrsTranspileState): TranspileResult {
2250
        return this.obj.transpile(state);
13✔
2251
    }
2252
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2253
        if (options.walkMode & InternalWalkMode.walkExpressions) {
315!
2254
            walk(this, 'obj', visitor, options);
315✔
2255
            walk(this, 'typeExpression', visitor, options);
315✔
2256
        }
2257
    }
2258

2259
    public getType(options: GetTypeOptions): BscType {
2260
        const result = this.typeExpression.getType(options);
78✔
2261
        if (options.typeChain) {
78✔
2262
            // modify last typechain entry to show it is a typecast
2263
            const lastEntry = options.typeChain[options.typeChain.length - 1];
18✔
2264
            if (lastEntry) {
18!
2265
                lastEntry.astNode = this;
18✔
2266
            }
2267
        }
2268
        return result;
78✔
2269
    }
2270
}
2271

2272
export class TypedArrayExpression extends Expression {
1✔
2273
    constructor(options: {
2274
        innerType: Expression;
2275
        leftBracket?: Token;
2276
        rightBracket?: Token;
2277
    }) {
2278
        super();
27✔
2279
        this.tokens = {
27✔
2280
            leftBracket: options.leftBracket,
2281
            rightBracket: options.rightBracket
2282
        };
2283
        this.innerType = options.innerType;
27✔
2284
        this.location = util.createBoundingLocation(
27✔
2285
            this.innerType,
2286
            this.tokens.leftBracket,
2287
            this.tokens.rightBracket
2288
        );
2289
    }
2290

2291
    public readonly tokens: {
2292
        readonly leftBracket?: Token;
2293
        readonly rightBracket?: Token;
2294
    };
2295

2296
    public readonly innerType: Expression;
2297

2298
    public readonly kind = AstNodeKind.TypedArrayExpression;
27✔
2299

2300
    public readonly location: Location;
2301

2302
    public transpile(state: BrsTranspileState): TranspileResult {
NEW
2303
        return [this.getType({ flags: SymbolTypeFlag.typetime }).toTypeString()];
×
2304
    }
2305

2306
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2307
        if (options.walkMode & InternalWalkMode.walkExpressions) {
116!
2308
            walk(this, 'innerType', visitor, options);
116✔
2309
        }
2310
    }
2311

2312
    public getType(options: GetTypeOptions): BscType {
2313
        return new ArrayType(this.innerType.getType(options));
114✔
2314
    }
2315
}
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

© 2025 Coveralls, Inc