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

rokucommunity / brighterscript / #15441

24 Mar 2026 07:36PM UTC coverage: 88.992% (-0.001%) from 88.993%
#15441

push

web-flow
Add computed property names (compile-time support only) (#1658)

7958 of 9428 branches covered (84.41%)

Branch coverage included in aggregate %.

72 of 78 new or added lines in 5 files covered. (92.31%)

1 existing line in 1 file now uncovered.

10200 of 10976 relevant lines covered (92.93%)

1960.9 hits per line

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

92.25
/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, CommentStatement, FunctionStatement, NamespaceStatement } from './Statement';
5
import type { Range } 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 { createVisitor, WalkMode } from '../astUtils/visitors';
1✔
12
import { walk, InternalWalkMode, walkArray } from '../astUtils/visitors';
1✔
13
import { isAAIndexedMemberExpression, isAALiteralExpression, isArrayLiteralExpression, isCallExpression, isCallfuncExpression, isCommentStatement, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isIntegerType, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isNewExpression, isStringType, isTemplateStringExpression, isTypeCastExpression, isUnaryExpression, isVariableExpression, isVoidType } from '../astUtils/reflection';
1✔
14
import type { TranspileResult, TypedefProvider } from '../interfaces';
15
import { VoidType } from '../types/VoidType';
1✔
16
import { DynamicType } from '../types/DynamicType';
1✔
17
import type { BscType } from '../types/BscType';
18
import { FunctionType } from '../types/FunctionType';
1✔
19
import type { AstNode } from './AstNode';
20
import { Expression } from './AstNode';
1✔
21
import { SymbolTable } from '../SymbolTable';
1✔
22
import { SourceNode } from 'source-map';
1✔
23

24
export type ExpressionVisitor = (expression: Expression, parent: Expression) => void;
25

26
export class BinaryExpression extends Expression {
1✔
27
    constructor(
28
        public left: Expression,
365✔
29
        public operator: Token,
365✔
30
        public right: Expression
365✔
31
    ) {
32
        super();
365✔
33
        this.range = util.createBoundingRange(this.left, this.operator, this.right);
365✔
34
    }
35

36
    public readonly range: Range | undefined;
37

38
    transpile(state: BrsTranspileState) {
39
        return [
159✔
40
            state.sourceNode(this.left, this.left.transpile(state)),
41
            ' ',
42
            state.transpileToken(this.operator),
43
            ' ',
44
            state.sourceNode(this.right, this.right.transpile(state))
45
        ];
46
    }
47

48
    walk(visitor: WalkVisitor, options: WalkOptions) {
49
        if (options.walkMode & InternalWalkMode.walkExpressions) {
865!
50
            walk(this, 'left', visitor, options);
865✔
51
            walk(this, 'right', visitor, options);
865✔
52
        }
53
    }
54

55
    public clone() {
56
        return this.finalizeClone(
3✔
57
            new BinaryExpression(
58
                this.left?.clone(),
9✔
59
                util.cloneToken(this.operator),
60
                this.right?.clone()
9✔
61
            ),
62
            ['left', 'right']
63
        );
64
    }
65
}
66

67
export class CallExpression extends Expression {
1✔
68
    /**
69
     * Number of parameters that can be defined on a function
70
     *
71
     * Prior to Roku OS 11.5, this was 32
72
     * As of Roku OS 11.5, this is 63
73
     */
74
    static MaximumArguments = 63;
1✔
75

76
    constructor(
77
        readonly callee: Expression,
712✔
78
        /**
79
         * Can either be `(`, or `?(` for optional chaining
80
         */
81
        readonly openingParen: Token,
712✔
82
        readonly closingParen: Token,
712✔
83
        readonly args: Expression[],
712✔
84
        unused?: any
85
    ) {
86
        super();
712✔
87
        this.range = util.createBoundingRange(this.callee, this.openingParen, ...args ?? [], this.closingParen);
712✔
88
    }
89

90
    public readonly range: Range | undefined;
91

92
    /**
93
     * Get the name of the wrapping namespace (if it exists)
94
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
95
     */
96
    public get namespaceName() {
97
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
98
    }
99

100
    transpile(state: BrsTranspileState, nameOverride?: string) {
101
        let result: TranspileResult = [];
252✔
102

103
        //transpile the name
104
        if (nameOverride) {
252✔
105
            result.push(state.sourceNode(this.callee, nameOverride));
9✔
106
        } else {
107
            result.push(...this.callee.transpile(state));
243✔
108
        }
109

110
        result.push(
252✔
111
            state.transpileToken(this.openingParen)
112
        );
113
        for (let i = 0; i < this.args.length; i++) {
252✔
114
            //add comma between args
115
            if (i > 0) {
142✔
116
                result.push(', ');
14✔
117
            }
118
            let arg = this.args[i];
142✔
119
            result.push(...arg.transpile(state));
142✔
120
        }
121
        if (this.closingParen) {
252!
122
            result.push(
252✔
123
                state.transpileToken(this.closingParen)
124
            );
125
        }
126
        return result;
252✔
127
    }
128

129
    walk(visitor: WalkVisitor, options: WalkOptions) {
130
        if (options.walkMode & InternalWalkMode.walkExpressions) {
2,144!
131
            walk(this, 'callee', visitor, options);
2,144✔
132
            walkArray(this.args, visitor, options, this);
2,144✔
133
        }
134
    }
135

136
    public clone() {
137
        return this.finalizeClone(
13✔
138
            new CallExpression(
139
                this.callee?.clone(),
39✔
140
                util.cloneToken(this.openingParen),
141
                util.cloneToken(this.closingParen),
142
                this.args?.map(e => e?.clone())
12✔
143
            ),
144
            ['callee', 'args']
145
        );
146
    }
147
}
148

149
export class FunctionExpression extends Expression implements TypedefProvider {
1✔
150
    constructor(
151
        readonly parameters: FunctionParameterExpression[],
2,019✔
152
        public body: Block,
2,019✔
153
        readonly functionType: Token | null,
2,019✔
154
        public end: Token,
2,019✔
155
        readonly leftParen: Token,
2,019✔
156
        readonly rightParen: Token,
2,019✔
157
        readonly asToken?: Token,
2,019✔
158
        readonly returnTypeToken?: Token
2,019✔
159
    ) {
160
        super();
2,019✔
161
        this.setReturnType(); // set the initial return type that we parse
2,019✔
162

163
        //if there's a body, and it doesn't have a SymbolTable, assign one
164
        if (this.body && !this.body.symbolTable) {
2,019✔
165
            this.body.symbolTable = new SymbolTable(`Function Body`);
146✔
166
        }
167
        this.symbolTable = new SymbolTable('FunctionExpression', () => this.parent?.getSymbolTable());
2,019!
168
    }
169

170
    /**
171
     * The type this function returns
172
     */
173
    public returnType: BscType;
174

175
    /**
176
     * Does this method require the return type to be present after transpile (useful for `as void` or the `as boolean` in `onKeyEvent`)
177
     */
178
    private requiresReturnType: boolean;
179

180
    /**
181
     * Get the name of the wrapping namespace (if it exists)
182
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
183
     */
184
    public get namespaceName() {
185
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
186
    }
187

188
    /**
189
     * Get the name of the wrapping namespace (if it exists)
190
     * @deprecated use `.findAncestor(isFunctionExpression)` instead.
191
     */
192
    public get parentFunction() {
193
        return this.findAncestor<FunctionExpression>(isFunctionExpression);
1,116✔
194
    }
195

196
    /**
197
     * The list of function calls that are declared within this function scope. This excludes CallExpressions
198
     * declared in child functions
199
     */
200
    public callExpressions = [] as CallExpression[];
2,019✔
201

202
    /**
203
     * If this function is part of a FunctionStatement, this will be set. Otherwise this will be undefined
204
     */
205
    public functionStatement?: FunctionStatement;
206

207
    /**
208
     * A list of all child functions declared directly within this function
209
     * @deprecated use `.walk(createVisitor({ FunctionExpression: ()=>{}), { walkMode: WalkMode.visitAllRecursive })` instead
210
     */
211
    public get childFunctionExpressions() {
212
        const expressions = [] as FunctionExpression[];
4✔
213
        this.walk(createVisitor({
4✔
214
            FunctionExpression: (expression) => {
215
                expressions.push(expression);
6✔
216
            }
217
        }), {
218
            walkMode: WalkMode.visitAllRecursive
219
        });
220
        return expressions;
4✔
221
    }
222

223
    /**
224
     * The range of the function, starting at the 'f' in function or 's' in sub (or the open paren if the keyword is missing),
225
     * and ending with the last n' in 'end function' or 'b' in 'end sub'
226
     */
227
    public get range() {
228
        return util.createBoundingRange(
5,165✔
229
            this.functionType, this.leftParen,
230
            ...this.parameters ?? [],
15,495✔
231
            this.rightParen,
232
            this.asToken,
233
            this.returnTypeToken,
234
            this.end
235
        );
236
    }
237

238
    transpile(state: BrsTranspileState, name?: Identifier, includeBody = true) {
384✔
239
        let results = [] as TranspileResult;
384✔
240
        //'function'|'sub'
241
        results.push(
384✔
242
            state.transpileToken(this.functionType!)
243
        );
244
        //functionName?
245
        if (name) {
384✔
246
            results.push(
379✔
247
                ' ',
248
                state.transpileToken(name)
249
            );
250
        }
251
        //leftParen
252
        results.push(
384✔
253
            state.transpileToken(this.leftParen)
254
        );
255
        //parameters
256
        for (let i = 0; i < this.parameters.length; i++) {
384✔
257
            let param = this.parameters[i];
125✔
258
            //add commas
259
            if (i > 0) {
125✔
260
                results.push(', ');
49✔
261
            }
262
            //add parameter
263
            results.push(param.transpile(state));
125✔
264
        }
265
        //right paren
266
        results.push(
384✔
267
            state.transpileToken(this.rightParen)
268
        );
269
        //as [Type]
270
        this.setReturnType(); // check one more time before transpile
384✔
271
        if (this.asToken && !(state.options.removeParameterTypes && !this.requiresReturnType)) {
384✔
272
            results.push(
39✔
273
                ' ',
274
                //as
275
                state.transpileToken(this.asToken),
276
                ' ',
277
                //return type
278
                state.sourceNode(this.returnTypeToken!, this.returnType.toTypeString())
279
            );
280
        }
281
        if (includeBody) {
384!
282
            state.lineage.unshift(this);
384✔
283
            let body = this.body.transpile(state);
384✔
284
            state.lineage.shift();
384✔
285
            results.push(...body);
384✔
286
        }
287
        results.push('\n');
384✔
288
        //'end sub'|'end function'
289
        results.push(
384✔
290
            state.indent(),
291
            state.transpileToken(this.end)
292
        );
293
        return results;
384✔
294
    }
295

296
    getTypedef(state: BrsTranspileState) {
297
        let results = [
34✔
298
            new SourceNode(1, 0, null, [
299
                //'function'|'sub'
300
                this.functionType?.text,
102!
301
                //functionName?
302
                ...(isFunctionStatement(this.parent) || isMethodStatement(this.parent) ? [' ', this.parent.name?.text ?? ''] : []),
295!
303
                //leftParen
304
                '(',
305
                //parameters
306
                ...(
307
                    this.parameters?.map((param, i) => ([
15!
308
                        //separating comma
309
                        i > 0 ? ', ' : '',
15✔
310
                        ...param.getTypedef(state)
311
                    ])) ?? []
34!
312
                ) as any,
313
                //right paren
314
                ')',
315
                //as <ReturnType>
316
                ...(this.asToken ? [
34✔
317
                    ' as ',
318
                    this.returnTypeToken?.text
15!
319
                ] : []),
320
                '\n',
321
                state.indent(),
322
                //'end sub'|'end function'
323
                this.end.text
324
            ])
325
        ];
326
        return results;
34✔
327
    }
328

329
    walk(visitor: WalkVisitor, options: WalkOptions) {
330
        if (options.walkMode & InternalWalkMode.walkExpressions) {
4,688!
331
            walkArray(this.parameters, visitor, options, this);
4,688✔
332

333
            //This is the core of full-program walking...it allows us to step into sub functions
334
            if (options.walkMode & InternalWalkMode.recurseChildFunctions) {
4,688!
335
                walk(this, 'body', visitor, options);
4,688✔
336
            }
337
        }
338
    }
339

340
    getFunctionType(): FunctionType {
341
        let functionType = new FunctionType(this.returnType);
1,876✔
342
        functionType.isSub = this.functionType?.text === 'sub';
1,876!
343
        for (let param of this.parameters) {
1,876✔
344
            functionType.addParameter(param.name.text, param.type, !!param.typeToken);
669✔
345
        }
346
        return functionType;
1,876✔
347
    }
348

349
    private setReturnType() {
350

351
        /**
352
         * RokuOS methods can be written several different ways:
353
         * 1. Function() : return withValue
354
         * 2. Function() as type : return withValue
355
         * 3. Function() as void : return
356
         *
357
         * 4. Sub() : return
358
         * 5. Sub () as void : return
359
         * 6. Sub() as type : return withValue
360
         *
361
         * Formats (1), (2), and (6) throw a compile error if there IS NOT a return value in the function body.
362
         * Formats (3), (4), and (5) throw a compile error if there IS a return value in the function body.
363
         *
364
         * 7. Additionally, as a special case, the OS requires that `onKeyEvent()` be defined with `as boolean`
365
         */
366

367
        const isSub = this.functionType?.text.toLowerCase() === 'sub';
2,403!
368

369
        if (this.returnTypeToken) {
2,403✔
370
            this.returnType = util.tokenToBscType(this.returnTypeToken);
161✔
371
        } else if (isSub) {
2,242✔
372
            this.returnType = new VoidType();
1,823✔
373
        } else {
374
            this.returnType = DynamicType.instance;
419✔
375
        }
376

377
        if ((isFunctionStatement(this.parent) || isMethodStatement(this.parent)) && this.parent?.name?.text.toLowerCase() === 'onkeyevent') {
2,403!
378
            // onKeyEvent() requires 'as Boolean' otherwise RokuOS throws errors
379
            this.requiresReturnType = true;
1✔
380
        } else if (isSub && !isVoidType(this.returnType)) { // format (6)
2,402✔
381
            this.requiresReturnType = true;
58✔
382
        } else if (this.returnTypeToken && isVoidType(this.returnType)) { // format (3)
2,344✔
383
            this.requiresReturnType = true;
17✔
384
        }
385
    }
386

387
    public clone() {
388
        const clone = this.finalizeClone(
108✔
389
            new FunctionExpression(
390
                this.parameters?.map(e => e?.clone()),
7✔
391
                this.body?.clone(),
324✔
392
                util.cloneToken(this.functionType),
393
                util.cloneToken(this.end),
394
                util.cloneToken(this.leftParen),
395
                util.cloneToken(this.rightParen),
396
                util.cloneToken(this.asToken),
397
                util.cloneToken(this.returnTypeToken)
398
            ),
399
            ['body']
400
        );
401

402
        //rebuild the .callExpressions list in the clone
403
        clone.body?.walk?.((node) => {
108✔
404
            if (isCallExpression(node) && !isNewExpression(node.parent)) {
210✔
405
                clone.callExpressions.push(node);
6✔
406
            }
407
        }, { walkMode: WalkMode.visitExpressions });
408
        return clone;
108✔
409
    }
410
}
411

412
export class FunctionParameterExpression extends Expression {
1✔
413
    constructor(
414
        public name: Identifier,
707✔
415
        public typeToken?: Token,
707✔
416
        public defaultValue?: Expression,
707✔
417
        public asToken?: Token
707✔
418
    ) {
419
        super();
707✔
420
        if (typeToken) {
707✔
421
            this.type = util.tokenToBscType(typeToken);
339✔
422
        } else {
423
            this.type = new DynamicType();
368✔
424
        }
425
    }
426

427
    public type: BscType;
428

429
    public get range(): Range | undefined {
430
        return util.createBoundingRange(
456✔
431
            this.name,
432
            this.asToken,
433
            this.typeToken,
434
            this.defaultValue
435
        );
436
    }
437

438
    public transpile(state: BrsTranspileState) {
439
        let result = [
142✔
440
            //name
441
            state.transpileToken(this.name)
442
        ] as any[];
443
        //default value
444
        if (this.defaultValue) {
142✔
445
            result.push(' = ');
9✔
446
            result.push(this.defaultValue.transpile(state));
9✔
447
        }
448
        //type declaration
449
        if (this.asToken && !state.options.removeParameterTypes) {
142✔
450
            result.push(' ');
91✔
451
            result.push(state.transpileToken(this.asToken));
91✔
452
            result.push(' ');
91✔
453
            result.push(state.sourceNode(this.typeToken!, this.type.toTypeString()));
91✔
454
        }
455

456
        return result;
142✔
457
    }
458

459
    public getTypedef(state: BrsTranspileState): TranspileResult {
460
        const results = [this.name.text] as TranspileResult;
15✔
461

462
        if (this.defaultValue) {
15✔
463
            results.push(' = ', ...(this.defaultValue.getTypedef(state) ?? this.defaultValue.transpile(state)));
7!
464
        }
465

466
        if (this.asToken) {
15✔
467
            results.push(' as ');
11✔
468

469
            if (this.typeToken) {
11!
470
                results.push(this.typeToken.text);
11✔
471
            }
472
        }
473

474
        return results;
15✔
475
    }
476

477
    walk(visitor: WalkVisitor, options: WalkOptions) {
478
        // eslint-disable-next-line no-bitwise
479
        if (this.defaultValue && options.walkMode & InternalWalkMode.walkExpressions) {
2,145✔
480
            walk(this, 'defaultValue', visitor, options);
706✔
481
        }
482
    }
483

484
    public clone() {
485
        return this.finalizeClone(
14✔
486
            new FunctionParameterExpression(
487
                util.cloneToken(this.name),
488
                util.cloneToken(this.typeToken),
489
                this.defaultValue?.clone(),
42✔
490
                util.cloneToken(this.asToken)
491
            ),
492
            ['defaultValue']
493
        );
494
    }
495
}
496

497
export class NamespacedVariableNameExpression extends Expression {
1✔
498
    constructor(
499
        //if this is a `DottedGetExpression`, it must be comprised only of `VariableExpression`s
500
        readonly expression: DottedGetExpression | VariableExpression
509✔
501
    ) {
502
        super();
509✔
503
        this.range = expression?.range;
509✔
504
    }
505
    range: Range | undefined;
506

507
    transpile(state: BrsTranspileState) {
508
        return [
4✔
509
            state.sourceNode(this, this.getName(ParseMode.BrightScript))
510
        ];
511
    }
512

513
    getTypedef(state) {
514
        return [
×
515
            state.sourceNode(this, this.getName(ParseMode.BrighterScript))
516
        ];
517
    }
518

519
    public getNameParts() {
520
        let parts = [] as string[];
3,741✔
521
        if (isVariableExpression(this.expression)) {
3,741✔
522
            parts.push(this.expression.name.text);
2,666✔
523
        } else {
524
            let expr = this.expression;
1,075✔
525

526
            parts.push(expr.name.text);
1,075✔
527

528
            while (isVariableExpression(expr) === false) {
1,075✔
529
                expr = expr.obj as DottedGetExpression;
1,250✔
530
                parts.unshift(expr.name.text);
1,250✔
531
            }
532
        }
533
        return parts;
3,741✔
534
    }
535

536
    getName(parseMode: ParseMode) {
537
        if (parseMode === ParseMode.BrighterScript) {
3,693✔
538
            return this.getNameParts().join('.');
3,457✔
539
        } else {
540
            return this.getNameParts().join('_');
236✔
541
        }
542
    }
543

544
    walk(visitor: WalkVisitor, options: WalkOptions) {
545
        this.expression?.link();
1,174!
546
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,174✔
547
            walk(this, 'expression', visitor, options);
1,150✔
548
        }
549
    }
550

551
    public clone() {
552
        return this.finalizeClone(
6✔
553
            new NamespacedVariableNameExpression(
554
                this.expression?.clone()
18✔
555
            )
556
        );
557
    }
558
}
559

560
export class DottedGetExpression extends Expression {
1✔
561
    constructor(
562
        readonly obj: Expression,
1,178✔
563
        readonly name: Identifier,
1,178✔
564
        /**
565
         * Can either be `.`, or `?.` for optional chaining
566
         */
567
        readonly dot: Token
1,178✔
568
    ) {
569
        super();
1,178✔
570
        this.range = util.createBoundingRange(this.obj, this.dot, this.name);
1,178✔
571
    }
572

573
    public readonly range: Range | undefined;
574

575
    transpile(state: BrsTranspileState) {
576
        //if the callee starts with a namespace name, transpile the name
577
        if (state.file.calleeStartsWithNamespace(this)) {
245✔
578
            return new NamespacedVariableNameExpression(this as DottedGetExpression | VariableExpression).transpile(state);
3✔
579
        } else {
580
            return [
242✔
581
                ...this.obj.transpile(state),
582
                state.transpileToken(this.dot),
583
                state.transpileToken(this.name)
584
            ];
585
        }
586
    }
587

588
    getTypedef(state: BrsTranspileState) {
589
        //always transpile the dots for typedefs
590
        return [
4✔
591
            ...(this.obj.getTypedef ? this.obj.getTypedef(state) : this.obj.transpile(state)),
4!
592
            state.transpileToken(this.dot),
593
            state.transpileToken(this.name)
594
        ];
595
    }
596

597
    walk(visitor: WalkVisitor, options: WalkOptions) {
598
        if (options.walkMode & InternalWalkMode.walkExpressions) {
3,040!
599
            walk(this, 'obj', visitor, options);
3,040✔
600
        }
601
    }
602

603
    public clone() {
604
        return this.finalizeClone(
8✔
605
            new DottedGetExpression(
606
                this.obj?.clone(),
24✔
607
                util.cloneToken(this.name),
608
                util.cloneToken(this.dot)
609
            ),
610
            ['obj']
611
        );
612
    }
613
}
614

615
export class XmlAttributeGetExpression extends Expression {
1✔
616
    constructor(
617
        readonly obj: Expression,
15✔
618
        readonly name: Identifier,
15✔
619
        /**
620
         * Can either be `@`, or `?@` for optional chaining
621
         */
622
        readonly at: Token
15✔
623
    ) {
624
        super();
15✔
625
        this.range = util.createBoundingRange(this.obj, this.at, this.name);
15✔
626
    }
627

628
    public readonly range: Range | undefined;
629

630
    transpile(state: BrsTranspileState) {
631
        return [
3✔
632
            ...this.obj.transpile(state),
633
            state.transpileToken(this.at),
634
            state.transpileToken(this.name)
635
        ];
636
    }
637

638
    walk(visitor: WalkVisitor, options: WalkOptions) {
639
        if (options.walkMode & InternalWalkMode.walkExpressions) {
26!
640
            walk(this, 'obj', visitor, options);
26✔
641
        }
642
    }
643

644
    public clone() {
645
        return this.finalizeClone(
2✔
646
            new XmlAttributeGetExpression(
647
                this.obj?.clone(),
6✔
648
                util.cloneToken(this.name),
649
                util.cloneToken(this.at)
650
            ),
651
            ['obj']
652
        );
653
    }
654
}
655

656
export class IndexedGetExpression extends Expression {
1✔
657
    constructor(
658
        public obj: Expression,
152✔
659
        public index: Expression,
152✔
660
        /**
661
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.`
662
         */
663
        public openingSquare: Token,
152✔
664
        public closingSquare: Token,
152✔
665
        public questionDotToken?: Token, //  ? or ?.
152✔
666
        /**
667
         * More indexes, separated by commas
668
         */
669
        public additionalIndexes?: Expression[]
152✔
670
    ) {
671
        super();
152✔
672
        this.range = util.createBoundingRange(this.obj, this.openingSquare, this.questionDotToken, this.openingSquare, this.index, this.closingSquare);
152✔
673
        this.additionalIndexes ??= [];
152✔
674
    }
675

676
    public readonly range: Range | undefined;
677

678
    transpile(state: BrsTranspileState) {
679
        const result = [];
64✔
680
        result.push(
64✔
681
            ...this.obj.transpile(state),
682
            this.questionDotToken ? state.transpileToken(this.questionDotToken) : '',
64✔
683
            state.transpileToken(this.openingSquare)
684
        );
685
        const indexes = [this.index, ...this.additionalIndexes ?? []];
64!
686
        for (let i = 0; i < indexes.length; i++) {
64✔
687
            //add comma between indexes
688
            if (i > 0) {
72✔
689
                result.push(', ');
8✔
690
            }
691
            let index = indexes[i];
72✔
692
            result.push(
72✔
693
                ...(index?.transpile(state) ?? [])
432!
694
            );
695
        }
696
        result.push(
64✔
697
            this.closingSquare ? state.transpileToken(this.closingSquare) : ''
64!
698
        );
699
        return result;
64✔
700
    }
701

702
    walk(visitor: WalkVisitor, options: WalkOptions) {
703
        if (options.walkMode & InternalWalkMode.walkExpressions) {
336!
704
            walk(this, 'obj', visitor, options);
336✔
705
            walk(this, 'index', visitor, options);
336✔
706
            walkArray(this.additionalIndexes, visitor, options, this);
336✔
707
        }
708
    }
709

710
    public clone() {
711
        return this.finalizeClone(
5✔
712
            new IndexedGetExpression(
713
                this.obj?.clone(),
15✔
714
                this.index?.clone(),
15✔
715
                util.cloneToken(this.openingSquare),
716
                util.cloneToken(this.closingSquare),
717
                util.cloneToken(this.questionDotToken),
718
                this.additionalIndexes?.map(e => e?.clone())
2✔
719
            ),
720
            ['obj', 'index', 'additionalIndexes']
721
        );
722
    }
723
}
724

725
export class GroupingExpression extends Expression {
1✔
726
    constructor(
727
        readonly tokens: {
40✔
728
            left: Token;
729
            right: Token;
730
        },
731
        public expression: Expression
40✔
732
    ) {
733
        super();
40✔
734
        this.range = util.createBoundingRange(this.tokens.left, this.expression, this.tokens.right);
40✔
735
    }
736

737
    public readonly range: Range | undefined;
738

739
    transpile(state: BrsTranspileState) {
740
        if (isTypeCastExpression(this.expression)) {
12✔
741
            return this.expression.transpile(state);
7✔
742
        }
743
        return [
5✔
744
            state.transpileToken(this.tokens.left),
745
            ...this.expression.transpile(state),
746
            state.transpileToken(this.tokens.right)
747
        ];
748
    }
749

750
    walk(visitor: WalkVisitor, options: WalkOptions) {
751
        if (options.walkMode & InternalWalkMode.walkExpressions) {
87!
752
            walk(this, 'expression', visitor, options);
87✔
753
        }
754
    }
755

756
    public clone() {
757
        return this.finalizeClone(
2✔
758
            new GroupingExpression(
759
                {
760
                    left: util.cloneToken(this.tokens.left),
761
                    right: util.cloneToken(this.tokens.right)
762
                },
763
                this.expression?.clone()
6✔
764
            ),
765
            ['expression']
766
        );
767
    }
768
}
769

770
export class LiteralExpression extends Expression {
1✔
771
    constructor(
772
        public token: Token
3,508✔
773
    ) {
774
        super();
3,508✔
775
        this.type = util.tokenToBscType(token);
3,508✔
776
    }
777

778
    public get range() {
779
        return this.token.range;
8,051✔
780
    }
781

782
    /**
783
     * The (data) type of this expression
784
     */
785
    public type: BscType;
786

787
    transpile(state: BrsTranspileState) {
788
        let text: string;
789
        if (this.token.kind === TokenKind.TemplateStringQuasi) {
878✔
790
            //wrap quasis with quotes (and escape inner quotemarks)
791
            text = `"${this.token.text.replace(/"/g, '""')}"`;
35✔
792

793
        } else if (isStringType(this.type)) {
843✔
794
            text = this.token.text;
347✔
795
            //add trailing quotemark if it's missing. We will have already generated a diagnostic for this.
796
            if (text.endsWith('"') === false) {
347✔
797
                text += '"';
1✔
798
            }
799
        } else {
800
            text = this.token.text;
496✔
801
        }
802

803
        return [
878✔
804
            state.sourceNode(this, text)
805
        ];
806
    }
807

808
    walk(visitor: WalkVisitor, options: WalkOptions) {
809
        //nothing to walk
810
    }
811

812
    public clone() {
813
        return this.finalizeClone(
101✔
814
            new LiteralExpression(
815
                util.cloneToken(this.token)
816
            )
817
        );
818
    }
819
}
820

821
/**
822
 * This is a special expression only used within template strings. It exists so we can prevent producing lots of empty strings
823
 * during template string transpile by identifying these expressions explicitly and skipping the bslib_toString around them
824
 */
825
export class EscapedCharCodeLiteralExpression extends Expression {
1✔
826
    constructor(
827
        readonly token: Token & { charCode: number }
37✔
828
    ) {
829
        super();
37✔
830
        this.range = token.range;
37✔
831
    }
832
    readonly range: Range;
833

834
    transpile(state: BrsTranspileState) {
835
        return [
15✔
836
            state.sourceNode(this, `chr(${this.token.charCode})`)
837
        ];
838
    }
839

840
    walk(visitor: WalkVisitor, options: WalkOptions) {
841
        //nothing to walk
842
    }
843

844
    public clone() {
845
        return this.finalizeClone(
3✔
846
            new EscapedCharCodeLiteralExpression(
847
                util.cloneToken(this.token)
848
            )
849
        );
850
    }
851
}
852

853
export class ArrayLiteralExpression extends Expression {
1✔
854
    constructor(
855
        readonly elements: Array<Expression | CommentStatement>,
134✔
856
        readonly open: Token,
134✔
857
        readonly close: Token,
134✔
858
        readonly hasSpread = false
134✔
859
    ) {
860
        super();
134✔
861
        this.range = util.createBoundingRange(this.open, ...this.elements ?? [], this.close);
134✔
862
    }
863

864
    public readonly range: Range | undefined;
865

866
    transpile(state: BrsTranspileState) {
867
        let result = [] as TranspileResult;
60✔
868
        result.push(
60✔
869
            state.transpileToken(this.open)
870
        );
871
        let hasChildren = this.elements.length > 0;
60✔
872
        state.blockDepth++;
60✔
873

874
        for (let i = 0; i < this.elements.length; i++) {
60✔
875
            let previousElement = this.elements[i - 1];
92✔
876
            let element = this.elements[i];
92✔
877

878
            if (isCommentStatement(element)) {
92✔
879
                //if the comment is on the same line as opening square or previous statement, don't add newline
880
                if (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element)) {
7✔
881
                    result.push(' ');
4✔
882
                } else {
883
                    result.push(
3✔
884
                        '\n',
885
                        state.indent()
886
                    );
887
                }
888
                state.lineage.unshift(this);
7✔
889
                result.push(element.transpile(state));
7✔
890
                state.lineage.shift();
7✔
891
            } else {
892
                result.push('\n');
85✔
893

894
                result.push(
85✔
895
                    state.indent(),
896
                    ...element.transpile(state)
897
                );
898
            }
899
        }
900
        state.blockDepth--;
60✔
901
        //add a newline between open and close if there are elements
902
        if (hasChildren) {
60✔
903
            result.push('\n');
38✔
904
            result.push(state.indent());
38✔
905
        }
906
        if (this.close) {
60!
907
            result.push(
60✔
908
                state.transpileToken(this.close)
909
            );
910
        }
911
        return result;
60✔
912
    }
913

914
    walk(visitor: WalkVisitor, options: WalkOptions) {
915
        if (options.walkMode & InternalWalkMode.walkExpressions) {
395!
916
            walkArray(this.elements, visitor, options, this);
395✔
917
        }
918
    }
919

920
    public clone() {
921
        return this.finalizeClone(
4✔
922
            new ArrayLiteralExpression(
923
                this.elements?.map(e => e?.clone()),
6✔
924
                util.cloneToken(this.open),
925
                util.cloneToken(this.close),
926
                this.hasSpread
927
            ),
928
            ['elements']
929
        );
930
    }
931
}
932

933
export class AAMemberExpression extends Expression {
1✔
934
    constructor(
935
        public keyToken: Token,
218✔
936
        public colonToken: Token,
218✔
937
        /** The expression evaluated to determine the member's initial value. */
938
        public value: Expression
218✔
939
    ) {
940
        super();
218✔
941
        this.range = util.createBoundingRange(this.keyToken, this.colonToken, this.value);
218✔
942
    }
943

944
    public range: Range | undefined;
945
    public commaToken?: Token;
946

947
    transpile(state: BrsTranspileState) {
UNCOV
948
        return [];
×
949
    }
950

951
    walk(visitor: WalkVisitor, options: WalkOptions) {
952
        walk(this, 'value', visitor, options);
399✔
953
    }
954

955
    public clone() {
956
        return this.finalizeClone(
4✔
957
            new AAMemberExpression(
958
                util.cloneToken(this.keyToken),
959
                util.cloneToken(this.colonToken),
960
                this.value?.clone()
12✔
961
            ),
962
            ['value']
963
        );
964
    }
965
}
966

967
export class AAIndexedMemberExpression extends Expression {
1✔
968
    constructor(options: {
969
        leftBracket: Token;
970
        key: Expression;
971
        rightBracket: Token;
972
        colon: Token;
973
        /** The expression evaluated to determine the member's initial value. */
974
        value: Expression;
975
    }) {
976
        super();
31✔
977
        this.key = options.key;
31✔
978
        this.tokens = {
31✔
979
            leftBracket: options.leftBracket,
980
            rightBracket: options.rightBracket,
981
            colon: options.colon
982
        };
983
        this.value = options.value;
31✔
984
        this.range = util.createBoundingRange(this.tokens.leftBracket, this.key, this.tokens.rightBracket, this.tokens.colon, this.value);
31✔
985
    }
986

987
    public readonly tokens: {
988
        readonly leftBracket: Token;
989
        readonly rightBracket: Token;
990
        readonly colon: Token;
991
    };
992

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

997
    public range: Range | undefined;
998
    public commaToken?: Token;
999

1000
    transpile(state: BrsTranspileState) {
NEW
1001
        return [];
×
1002
    }
1003

1004
    walk(visitor: WalkVisitor, options: WalkOptions) {
1005
        walk(this, 'key', visitor, options);
81✔
1006
        walk(this, 'value', visitor, options);
81✔
1007
    }
1008

1009
    public clone() {
1010
        return this.finalizeClone(
2✔
1011
            new AAIndexedMemberExpression({
1012
                leftBracket: util.cloneToken(this.tokens.leftBracket),
1013
                key: this.key?.clone(),
6!
1014
                rightBracket: util.cloneToken(this.tokens.rightBracket),
1015
                colon: util.cloneToken(this.tokens.colon),
1016
                value: this.value?.clone()
6✔
1017
            }),
1018
            ['key', 'value']
1019
        );
1020
    }
1021
}
1022

1023
export class AALiteralExpression extends Expression {
1✔
1024
    constructor(
1025
        readonly elements: Array<AAMemberExpression | AAIndexedMemberExpression | CommentStatement>,
244✔
1026
        readonly open: Token,
244✔
1027
        readonly close: Token
244✔
1028
    ) {
1029
        super();
244✔
1030
        this.range = util.createBoundingRange(this.open, ...this.elements ?? [], this.close);
244✔
1031
    }
1032

1033
    public readonly range: Range | undefined;
1034

1035
    transpile(state: BrsTranspileState) {
1036
        let result = [] as TranspileResult;
69✔
1037
        //open curly
1038
        result.push(
69✔
1039
            state.transpileToken(this.open)
1040
        );
1041
        let hasChildren = this.elements.length > 0;
69✔
1042
        //add newline if the object has children and the first child isn't a comment starting on the same line as opening curly
1043
        if (hasChildren && (isCommentStatement(this.elements[0]) === false || !util.linesTouch(this.elements[0], this.open))) {
69✔
1044
            result.push('\n');
37✔
1045
        }
1046
        state.blockDepth++;
69✔
1047
        for (let i = 0; i < this.elements.length; i++) {
69✔
1048
            let element = this.elements[i];
71✔
1049
            let previousElement = this.elements[i - 1];
71✔
1050
            let nextElement = this.elements[i + 1];
71✔
1051

1052
            //don't indent if comment is same-line
1053
            if (isCommentStatement(element as any) &&
71✔
1054
                (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element))
1055
            ) {
1056
                result.push(' ');
10✔
1057

1058
                //indent line
1059
            } else {
1060
                result.push(state.indent());
61✔
1061
            }
1062

1063
            //render comments
1064
            if (isCommentStatement(element)) {
71✔
1065
                result.push(...element.transpile(state));
14✔
1066
            } else {
1067
                //key
1068
                if ('tokens' in element) {
57✔
1069
                    //computed key: transpile the resolved expression (pre-transpile overrides it to a literal)
1070
                    result.push(...element.key.transpile(state));
8✔
1071
                } else {
1072
                    result.push(
49✔
1073
                        state.transpileToken(element.keyToken)
1074
                    );
1075
                }
1076
                //colon
1077
                result.push(
57✔
1078
                    state.transpileToken('tokens' in element ? element.tokens.colon : element.colonToken),
57✔
1079
                    ' '
1080
                );
1081

1082
                //value
1083
                result.push(...element.value.transpile(state));
57✔
1084
            }
1085

1086

1087
            //if next element is a same-line comment, skip the newline
1088
            if (nextElement && isCommentStatement(nextElement) && nextElement.range?.start.line === element.range?.start.line) {
71!
1089

1090
                //add a newline between statements
1091
            } else {
1092
                result.push('\n');
63✔
1093
            }
1094
        }
1095
        state.blockDepth--;
69✔
1096

1097
        //only indent the closing curly if we have children
1098
        if (hasChildren) {
69✔
1099
            result.push(state.indent());
39✔
1100
        }
1101
        //close curly
1102
        if (this.close) {
69!
1103
            result.push(
69✔
1104
                state.transpileToken(this.close)
1105
            );
1106
        }
1107
        return result;
69✔
1108
    }
1109

1110
    walk(visitor: WalkVisitor, options: WalkOptions) {
1111
        if (options.walkMode & InternalWalkMode.walkExpressions) {
506!
1112
            walkArray(this.elements, visitor, options, this);
506✔
1113
        }
1114
    }
1115

1116
    public clone() {
1117
        return this.finalizeClone(
8✔
1118
            new AALiteralExpression(
1119
                this.elements?.map(e => e?.clone()),
7✔
1120
                util.cloneToken(this.open),
1121
                util.cloneToken(this.close)
1122
            ),
1123
            ['elements']
1124
        );
1125
    }
1126
}
1127

1128
export class UnaryExpression extends Expression {
1✔
1129
    constructor(
1130
        public operator: Token,
41✔
1131
        public right: Expression
41✔
1132
    ) {
1133
        super();
41✔
1134
        this.range = util.createBoundingRange(this.operator, this.right);
41✔
1135
    }
1136

1137
    public readonly range: Range | undefined;
1138

1139
    transpile(state: BrsTranspileState) {
1140
        let separatingWhitespace: string | undefined;
1141
        if (isVariableExpression(this.right)) {
14✔
1142
            separatingWhitespace = this.right.name.leadingWhitespace;
6✔
1143
        } else if (isLiteralExpression(this.right)) {
8✔
1144
            separatingWhitespace = this.right.token.leadingWhitespace;
3✔
1145
        }
1146

1147
        return [
14✔
1148
            state.transpileToken(this.operator),
1149
            separatingWhitespace ?? ' ',
42✔
1150
            ...this.right.transpile(state)
1151
        ];
1152
    }
1153

1154
    walk(visitor: WalkVisitor, options: WalkOptions) {
1155
        if (options.walkMode & InternalWalkMode.walkExpressions) {
91!
1156
            walk(this, 'right', visitor, options);
91✔
1157
        }
1158
    }
1159

1160
    public clone() {
1161
        return this.finalizeClone(
2✔
1162
            new UnaryExpression(
1163
                util.cloneToken(this.operator),
1164
                this.right?.clone()
6✔
1165
            ),
1166
            ['right']
1167
        );
1168
    }
1169
}
1170

1171
export class VariableExpression extends Expression {
1✔
1172
    constructor(
1173
        readonly name: Identifier
2,265✔
1174
    ) {
1175
        super();
2,265✔
1176
        this.range = this.name?.range;
2,265!
1177
    }
1178

1179
    public readonly range: Range;
1180

1181
    public getName(parseMode: ParseMode) {
1182
        return this.name.text;
30✔
1183
    }
1184

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

1198
            //transpile  normally
1199
        } else {
1200
            result.push(
424✔
1201
                state.transpileToken(this.name)
1202
            );
1203
        }
1204
        return result;
429✔
1205
    }
1206

1207
    getTypedef(state: BrsTranspileState) {
1208
        return [
4✔
1209
            state.transpileToken(this.name)
1210
        ];
1211
    }
1212

1213
    walk(visitor: WalkVisitor, options: WalkOptions) {
1214
        //nothing to walk
1215
    }
1216

1217
    public clone() {
1218
        return this.finalizeClone(
42✔
1219
            new VariableExpression(
1220
                util.cloneToken(this.name)
1221
            )
1222
        );
1223
    }
1224
}
1225

1226
export class SourceLiteralExpression extends Expression {
1✔
1227
    constructor(
1228
        readonly token: Token
37✔
1229
    ) {
1230
        super();
37✔
1231
        this.range = token?.range;
37!
1232
    }
1233

1234
    public readonly range: Range;
1235

1236
    private getFunctionName(state: BrsTranspileState, parseMode: ParseMode) {
1237
        let func = this.findAncestor<FunctionExpression>(isFunctionExpression);
8✔
1238
        let nameParts = [] as TranspileResult;
8✔
1239
        while (func.parentFunction) {
8✔
1240
            let index = func.parentFunction.childFunctionExpressions.indexOf(func);
4✔
1241
            nameParts.unshift(`anon${index}`);
4✔
1242
            func = func.parentFunction;
4✔
1243
        }
1244
        //get the index of this function in its parent
1245
        nameParts.unshift(
8✔
1246
            func.functionStatement!.getName(parseMode)
1247
        );
1248
        return nameParts.join('$');
8✔
1249
    }
1250

1251
    /**
1252
     * Get the line number from our token or from the closest ancestor that has a range
1253
     */
1254
    private getClosestLineNumber() {
1255
        let node: AstNode = this;
7✔
1256
        while (node) {
7✔
1257
            if (node.range) {
17✔
1258
                return node.range.start.line + 1;
5✔
1259
            }
1260
            node = node.parent;
12✔
1261
        }
1262
        return -1;
2✔
1263
    }
1264

1265
    transpile(state: BrsTranspileState) {
1266
        let text: string;
1267
        switch (this.token.kind) {
31✔
1268
            case TokenKind.SourceFilePathLiteral:
40!
1269
                const pathUrl = fileUrl(state.srcPath);
3✔
1270
                text = `"${pathUrl.substring(0, 4)}" + "${pathUrl.substring(4)}"`;
3✔
1271
                break;
3✔
1272
            case TokenKind.SourceLineNumLiteral:
1273
                //TODO find first parent that has range, or default to -1
1274
                text = `${this.getClosestLineNumber()}`;
4✔
1275
                break;
4✔
1276
            case TokenKind.FunctionNameLiteral:
1277
                text = `"${this.getFunctionName(state, ParseMode.BrightScript)}"`;
4✔
1278
                break;
4✔
1279
            case TokenKind.SourceFunctionNameLiteral:
1280
                text = `"${this.getFunctionName(state, ParseMode.BrighterScript)}"`;
4✔
1281
                break;
4✔
1282
            case TokenKind.SourceNamespaceNameLiteral:
1283
                let namespaceParts = this.getFunctionName(state, ParseMode.BrighterScript).split('.');
×
1284
                namespaceParts.pop(); // remove the function name
×
1285

1286
                text = `"${namespaceParts.join('.')}"`;
×
1287
                break;
×
1288
            case TokenKind.SourceNamespaceRootNameLiteral:
1289
                let namespaceRootParts = this.getFunctionName(state, ParseMode.BrighterScript).split('.');
×
1290
                namespaceRootParts.pop(); // remove the function name
×
1291

1292
                let rootNamespace = namespaceRootParts.shift() ?? '';
×
1293
                text = `"${rootNamespace}"`;
×
1294
                break;
×
1295
            case TokenKind.SourceLocationLiteral:
1296
                const locationUrl = fileUrl(state.srcPath);
3✔
1297
                //TODO find first parent that has range, or default to -1
1298
                text = `"${locationUrl.substring(0, 4)}" + "${locationUrl.substring(4)}:${this.getClosestLineNumber()}"`;
3✔
1299
                break;
3✔
1300
            case TokenKind.PkgPathLiteral:
1301
                let pkgPath1 = `pkg:/${state.file.pkgPath}`
2✔
1302
                    .replace(/\\/g, '/')
1303
                    .replace(/\.bs$/i, '.brs');
1304

1305
                text = `"${pkgPath1}"`;
2✔
1306
                break;
2✔
1307
            case TokenKind.PkgLocationLiteral:
1308
                let pkgPath2 = `pkg:/${state.file.pkgPath}`
2✔
1309
                    .replace(/\\/g, '/')
1310
                    .replace(/\.bs$/i, '.brs');
1311

1312
                text = `"${pkgPath2}:" + str(LINE_NUM)`;
2✔
1313
                break;
2✔
1314
            case TokenKind.LineNumLiteral:
1315
            default:
1316
                //use the original text (because it looks like a variable)
1317
                text = this.token.text;
9✔
1318
                break;
9✔
1319

1320
        }
1321
        return [
31✔
1322
            state.sourceNode(this, text)
1323
        ];
1324
    }
1325

1326
    walk(visitor: WalkVisitor, options: WalkOptions) {
1327
        //nothing to walk
1328
    }
1329

1330
    public clone() {
1331
        return this.finalizeClone(
1✔
1332
            new SourceLiteralExpression(
1333
                util.cloneToken(this.token)
1334
            )
1335
        );
1336
    }
1337
}
1338

1339
/**
1340
 * This expression transpiles and acts exactly like a CallExpression,
1341
 * except we need to uniquely identify these statements so we can
1342
 * do more type checking.
1343
 */
1344
export class NewExpression extends Expression {
1✔
1345
    constructor(
1346
        readonly newKeyword: Token,
43✔
1347
        readonly call: CallExpression
43✔
1348
    ) {
1349
        super();
43✔
1350
        this.range = util.createBoundingRange(this.newKeyword, this.call);
43✔
1351
    }
1352

1353
    /**
1354
     * The name of the class to initialize (with optional namespace prefixed)
1355
     */
1356
    public get className() {
1357
        //the parser guarantees the callee of a new statement's call object will be
1358
        //a NamespacedVariableNameExpression
1359
        return this.call.callee as NamespacedVariableNameExpression;
104✔
1360
    }
1361

1362
    public readonly range: Range | undefined;
1363

1364
    public transpile(state: BrsTranspileState) {
1365
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
10✔
1366
        const cls = state.file.getClassFileLink(
10✔
1367
            this.className.getName(ParseMode.BrighterScript),
1368
            namespace?.getName(ParseMode.BrighterScript)
30✔
1369
        )?.item;
10✔
1370
        //new statements within a namespace block can omit the leading namespace if the class resides in that same namespace.
1371
        //So we need to figure out if this is a namespace-omitted class, or if this class exists without a namespace.
1372
        return this.call.transpile(state, cls?.getName(ParseMode.BrightScript));
10✔
1373
    }
1374

1375
    walk(visitor: WalkVisitor, options: WalkOptions) {
1376
        if (options.walkMode & InternalWalkMode.walkExpressions) {
182!
1377
            walk(this, 'call', visitor, options);
182✔
1378
        }
1379
    }
1380

1381
    public clone() {
1382
        return this.finalizeClone(
2✔
1383
            new NewExpression(
1384
                util.cloneToken(this.newKeyword),
1385
                this.call?.clone()
6✔
1386
            ),
1387
            ['call']
1388
        );
1389
    }
1390
}
1391

1392
export class CallfuncExpression extends Expression {
1✔
1393
    constructor(
1394
        readonly callee: Expression,
28✔
1395
        readonly operator: Token,
28✔
1396
        readonly methodName: Identifier,
28✔
1397
        readonly openingParen: Token,
28✔
1398
        readonly args: Expression[],
28✔
1399
        readonly closingParen: Token
28✔
1400
    ) {
1401
        super();
28✔
1402
        this.range = util.createBoundingRange(
28✔
1403
            callee,
1404
            operator,
1405
            methodName,
1406
            openingParen,
1407
            ...args ?? [],
84✔
1408
            closingParen
1409
        );
1410
    }
1411

1412
    public readonly range: Range | undefined;
1413

1414
    /**
1415
     * Get the name of the wrapping namespace (if it exists)
1416
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
1417
     */
1418
    public get namespaceName() {
1419
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
1420
    }
1421

1422
    public transpile(state: BrsTranspileState) {
1423
        let result = [] as TranspileResult;
8✔
1424
        result.push(
8✔
1425
            ...this.callee.transpile(state),
1426
            state.sourceNode(this.operator, '.callfunc'),
1427
            state.transpileToken(this.openingParen),
1428
            //the name of the function
1429
            state.sourceNode(this.methodName, ['"', this.methodName.text, '"']),
1430
            ', '
1431
        );
1432
        //transpile args
1433
        //callfunc with zero args never gets called, so pass invalid as the first parameter if there are no args
1434
        if (this.args.length === 0) {
8✔
1435
            result.push('invalid');
5✔
1436
        } else {
1437
            for (let i = 0; i < this.args.length; i++) {
3✔
1438
                //add comma between args
1439
                if (i > 0) {
6✔
1440
                    result.push(', ');
3✔
1441
                }
1442
                let arg = this.args[i];
6✔
1443
                result.push(...arg.transpile(state));
6✔
1444
            }
1445
        }
1446
        result.push(
8✔
1447
            state.transpileToken(this.closingParen)
1448
        );
1449
        return result;
8✔
1450
    }
1451

1452
    walk(visitor: WalkVisitor, options: WalkOptions) {
1453
        if (options.walkMode & InternalWalkMode.walkExpressions) {
65!
1454
            walk(this, 'callee', visitor, options);
65✔
1455
            walkArray(this.args, visitor, options, this);
65✔
1456
        }
1457
    }
1458

1459
    public clone() {
1460
        return this.finalizeClone(
3✔
1461
            new CallfuncExpression(
1462
                this.callee?.clone(),
9✔
1463
                util.cloneToken(this.operator),
1464
                util.cloneToken(this.methodName),
1465
                util.cloneToken(this.openingParen),
1466
                this.args?.map(e => e?.clone()),
2✔
1467
                util.cloneToken(this.closingParen)
1468
            ),
1469
            ['callee', 'args']
1470
        );
1471
    }
1472
}
1473

1474
/**
1475
 * Since template strings can contain newlines, we need to concatenate multiple strings together with chr() calls.
1476
 * This is a single expression that represents the string contatenation of all parts of a single quasi.
1477
 */
1478
export class TemplateStringQuasiExpression extends Expression {
1✔
1479
    constructor(
1480
        readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>
114✔
1481
    ) {
1482
        super();
114✔
1483
        this.range = util.createBoundingRange(
114✔
1484
            ...expressions ?? []
342✔
1485
        );
1486
    }
1487
    readonly range: Range | undefined;
1488

1489
    transpile(state: BrsTranspileState, skipEmptyStrings = true) {
41✔
1490
        let result = [] as TranspileResult;
49✔
1491
        let plus = '';
49✔
1492
        for (let expression of this.expressions) {
49✔
1493
            //skip empty strings
1494
            //TODO what does an empty string literal expression look like?
1495
            if (expression.token.text === '' && skipEmptyStrings === true) {
78✔
1496
                continue;
28✔
1497
            }
1498
            result.push(
50✔
1499
                plus,
1500
                ...expression.transpile(state)
1501
            );
1502
            plus = ' + ';
50✔
1503
        }
1504
        return result;
49✔
1505
    }
1506

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

1513
    public clone() {
1514
        return this.finalizeClone(
15✔
1515
            new TemplateStringQuasiExpression(
1516
                this.expressions?.map(e => e?.clone())
20✔
1517
            ),
1518
            ['expressions']
1519
        );
1520
    }
1521
}
1522

1523
export class TemplateStringExpression extends Expression {
1✔
1524
    constructor(
1525
        readonly openingBacktick: Token,
53✔
1526
        readonly quasis: TemplateStringQuasiExpression[],
53✔
1527
        readonly expressions: Expression[],
53✔
1528
        readonly closingBacktick: Token
53✔
1529
    ) {
1530
        super();
53✔
1531
        this.range = util.createBoundingRange(
53✔
1532
            openingBacktick,
1533
            quasis?.[0],
159✔
1534
            quasis?.[quasis?.length - 1],
315!
1535
            closingBacktick
1536
        );
1537
    }
1538

1539
    public readonly range: Range | undefined;
1540

1541
    transpile(state: BrsTranspileState) {
1542
        //if this is essentially just a normal brightscript string but with backticks, transpile it as a normal string without parens
1543
        if (this.expressions.length === 0 && this.quasis.length === 1 && this.quasis[0].expressions.length === 1) {
24✔
1544
            return this.quasis[0].transpile(state);
6✔
1545
        }
1546
        let result = ['('];
18✔
1547
        let plus = '';
18✔
1548
        //helper function to figure out when to include the plus
1549
        function add(...items) {
1550
            if (items.length > 0) {
52✔
1551
                result.push(
40✔
1552
                    plus,
1553
                    ...items
1554
                );
1555
            }
1556
            //set the plus after the first occurance of a nonzero length set of items
1557
            if (plus === '' && items.length > 0) {
52✔
1558
                plus = ' + ';
18✔
1559
            }
1560
        }
1561

1562
        for (let i = 0; i < this.quasis.length; i++) {
18✔
1563
            let quasi = this.quasis[i];
35✔
1564
            let expression = this.expressions[i];
35✔
1565

1566
            add(
35✔
1567
                ...quasi.transpile(state)
1568
            );
1569
            if (expression) {
35✔
1570
                //skip the toString wrapper around certain expressions
1571
                if (
17✔
1572
                    isEscapedCharCodeLiteralExpression(expression) ||
39✔
1573
                    (isLiteralExpression(expression) && isStringType(expression.type))
1574
                ) {
1575
                    add(
3✔
1576
                        ...expression.transpile(state)
1577
                    );
1578

1579
                    //wrap all other expressions with a bslib_toString call to prevent runtime type mismatch errors
1580
                } else {
1581
                    add(
14✔
1582
                        state.bslibPrefix + '_toString(',
1583
                        ...expression.transpile(state),
1584
                        ')'
1585
                    );
1586
                }
1587
            }
1588
        }
1589
        //the expression should be wrapped in parens so it can be used line a single expression at runtime
1590
        result.push(')');
18✔
1591

1592
        return result;
18✔
1593
    }
1594

1595
    walk(visitor: WalkVisitor, options: WalkOptions) {
1596
        if (options.walkMode & InternalWalkMode.walkExpressions) {
129!
1597
            //walk the quasis and expressions in left-to-right order
1598
            for (let i = 0; i < this.quasis?.length; i++) {
129✔
1599
                walk(this.quasis, i, visitor, options, this);
221✔
1600

1601
                //this skips the final loop iteration since we'll always have one more quasi than expression
1602
                if (this.expressions[i]) {
221✔
1603
                    walk(this.expressions, i, visitor, options, this);
92✔
1604
                }
1605
            }
1606
        }
1607
    }
1608

1609
    public clone() {
1610
        return this.finalizeClone(
7✔
1611
            new TemplateStringExpression(
1612
                util.cloneToken(this.openingBacktick),
1613
                this.quasis?.map(e => e?.clone()),
12✔
1614
                this.expressions?.map(e => e?.clone()),
6✔
1615
                util.cloneToken(this.closingBacktick)
1616
            ),
1617
            ['quasis', 'expressions']
1618
        );
1619
    }
1620
}
1621

1622
export class TaggedTemplateStringExpression extends Expression {
1✔
1623
    constructor(
1624
        readonly tagName: Identifier,
12✔
1625
        readonly openingBacktick: Token,
12✔
1626
        readonly quasis: TemplateStringQuasiExpression[],
12✔
1627
        readonly expressions: Expression[],
12✔
1628
        readonly closingBacktick: Token
12✔
1629
    ) {
1630
        super();
12✔
1631
        this.range = util.createBoundingRange(
12✔
1632
            tagName,
1633
            openingBacktick,
1634
            quasis?.[0],
36✔
1635
            quasis?.[quasis?.length - 1],
69!
1636
            closingBacktick
1637
        );
1638
    }
1639

1640
    public readonly range: Range | undefined;
1641

1642
    transpile(state: BrsTranspileState) {
1643
        let result = [] as TranspileResult;
3✔
1644
        result.push(
3✔
1645
            state.transpileToken(this.tagName),
1646
            '(['
1647
        );
1648

1649
        //add quasis as the first array
1650
        for (let i = 0; i < this.quasis.length; i++) {
3✔
1651
            let quasi = this.quasis[i];
8✔
1652
            //separate items with a comma
1653
            if (i > 0) {
8✔
1654
                result.push(
5✔
1655
                    ', '
1656
                );
1657
            }
1658
            result.push(
8✔
1659
                ...quasi.transpile(state, false)
1660
            );
1661
        }
1662
        result.push(
3✔
1663
            '], ['
1664
        );
1665

1666
        //add expressions as the second array
1667
        for (let i = 0; i < this.expressions.length; i++) {
3✔
1668
            let expression = this.expressions[i];
5✔
1669
            if (i > 0) {
5✔
1670
                result.push(
2✔
1671
                    ', '
1672
                );
1673
            }
1674
            result.push(
5✔
1675
                ...expression.transpile(state)
1676
            );
1677
        }
1678
        result.push(
3✔
1679
            state.sourceNode(this.closingBacktick, '])')
1680
        );
1681
        return result;
3✔
1682
    }
1683

1684
    walk(visitor: WalkVisitor, options: WalkOptions) {
1685
        if (options.walkMode & InternalWalkMode.walkExpressions) {
21!
1686
            //walk the quasis and expressions in left-to-right order
1687
            for (let i = 0; i < this.quasis?.length; i++) {
21✔
1688
                walk(this.quasis, i, visitor, options, this);
48✔
1689

1690
                //this skips the final loop iteration since we'll always have one more quasi than expression
1691
                if (this.expressions[i]) {
48✔
1692
                    walk(this.expressions, i, visitor, options, this);
27✔
1693
                }
1694
            }
1695
        }
1696
    }
1697

1698
    public clone() {
1699
        return this.finalizeClone(
3✔
1700
            new TaggedTemplateStringExpression(
1701
                util.cloneToken(this.tagName),
1702
                util.cloneToken(this.openingBacktick),
1703
                this.quasis?.map(e => e?.clone()),
5✔
1704
                this.expressions?.map(e => e?.clone()),
3✔
1705
                util.cloneToken(this.closingBacktick)
1706
            ),
1707
            ['quasis', 'expressions']
1708
        );
1709
    }
1710
}
1711

1712
export class AnnotationExpression extends Expression {
1✔
1713
    constructor(
1714
        readonly atToken: Token,
70✔
1715
        readonly nameToken: Token
70✔
1716
    ) {
1717
        super();
70✔
1718
        this.name = nameToken.text;
70✔
1719
    }
1720

1721
    public get range() {
1722
        return util.createBoundingRange(
32✔
1723
            this.atToken,
1724
            this.nameToken,
1725
            this.call
1726
        );
1727
    }
1728

1729
    public name: string;
1730
    public call: CallExpression | undefined;
1731

1732
    /**
1733
     * Convert annotation arguments to JavaScript types
1734
     * @param strict If false, keep Expression objects not corresponding to JS types
1735
     */
1736
    getArguments(strict = true): ExpressionValue[] {
10✔
1737
        if (!this.call) {
11✔
1738
            return [];
1✔
1739
        }
1740
        return this.call.args.map(e => expressionToValue(e, strict));
20✔
1741
    }
1742

1743
    transpile(state: BrsTranspileState) {
1744
        return [];
3✔
1745
    }
1746

1747
    walk(visitor: WalkVisitor, options: WalkOptions) {
1748
        //nothing to walk
1749
    }
1750
    getTypedef(state: BrsTranspileState) {
1751
        return [
9✔
1752
            '@',
1753
            this.name,
1754
            ...(this.call?.transpile(state) ?? [])
54✔
1755
        ];
1756
    }
1757

1758
    public clone() {
1759
        const clone = this.finalizeClone(
8✔
1760
            new AnnotationExpression(
1761
                util.cloneToken(this.atToken),
1762
                util.cloneToken(this.nameToken)
1763
            )
1764
        );
1765
        return clone;
8✔
1766
    }
1767
}
1768

1769
export class TernaryExpression extends Expression {
1✔
1770
    constructor(
1771
        readonly test: Expression,
96✔
1772
        readonly questionMarkToken: Token,
96✔
1773
        readonly consequent?: Expression,
96✔
1774
        readonly colonToken?: Token,
96✔
1775
        readonly alternate?: Expression
96✔
1776
    ) {
1777
        super();
96✔
1778
        this.range = util.createBoundingRange(
96✔
1779
            test,
1780
            questionMarkToken,
1781
            consequent,
1782
            colonToken,
1783
            alternate
1784
        );
1785
    }
1786

1787
    public range: Range | undefined;
1788

1789
    transpile(state: BrsTranspileState) {
1790
        let result = [] as TranspileResult;
18✔
1791
        const file = state.file;
18✔
1792
        let consequentInfo = util.getExpressionInfo(this.consequent!, file);
18✔
1793
        let alternateInfo = util.getExpressionInfo(this.alternate!, file);
18✔
1794

1795
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1796
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
18✔
1797
        //discard names of global functions that cannot be passed by reference
1798
        allUniqueVarNames = allUniqueVarNames.filter(name => {
18✔
1799
            return !nonReferenceableFunctions.includes(name.toLowerCase());
23✔
1800
        });
1801

1802
        let mutatingExpressions = [
18✔
1803
            ...consequentInfo.expressions,
1804
            ...alternateInfo.expressions
1805
        ].filter(e => e instanceof CallExpression || e instanceof CallfuncExpression || e instanceof DottedGetExpression);
120✔
1806

1807
        if (mutatingExpressions.length > 0) {
18✔
1808
            result.push(
10✔
1809
                state.sourceNode(
1810
                    this.questionMarkToken,
1811
                    //write all the scope variables as parameters.
1812
                    //TODO handle when there are more than 31 parameters
1813
                    `(function(${['__bsCondition', ...allUniqueVarNames].join(', ')})`
1814
                ),
1815
                state.newline,
1816
                //double indent so our `end function` line is still indented one at the end
1817
                state.indent(2),
1818
                state.sourceNode(this.test, `if __bsCondition then`),
1819
                state.newline,
1820
                state.indent(1),
1821
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
30!
1822
                ...this.consequent?.transpile(state) ?? [state.sourceNode(this.questionMarkToken, 'invalid')],
60!
1823
                state.newline,
1824
                state.indent(-1),
1825
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'else'),
30!
1826
                state.newline,
1827
                state.indent(1),
1828
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
30!
1829
                ...this.alternate?.transpile(state) ?? [state.sourceNode(this.consequent ?? this.questionMarkToken, 'invalid')],
60!
1830
                state.newline,
1831
                state.indent(-1),
1832
                state.sourceNode(this.questionMarkToken, 'end if'),
1833
                state.newline,
1834
                state.indent(-1),
1835
                state.sourceNode(this.questionMarkToken, 'end function)('),
1836
                ...this.test.transpile(state),
1837
                state.sourceNode(this.questionMarkToken, `${['', ...allUniqueVarNames].join(', ')})`)
1838
            );
1839
            state.blockDepth--;
10✔
1840
        } else {
1841
            result.push(
8✔
1842
                state.sourceNode(this.test, state.bslibPrefix + `_ternary(`),
1843
                ...this.test.transpile(state),
1844
                state.sourceNode(this.test, `, `),
1845
                ...this.consequent?.transpile(state) ?? ['invalid'],
48✔
1846
                `, `,
1847
                ...this.alternate?.transpile(state) ?? ['invalid'],
48✔
1848
                `)`
1849
            );
1850
        }
1851
        return result;
18✔
1852
    }
1853

1854
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1855
        if (options.walkMode & InternalWalkMode.walkExpressions) {
236!
1856
            walk(this, 'test', visitor, options);
236✔
1857
            walk(this, 'consequent', visitor, options);
236✔
1858
            walk(this, 'alternate', visitor, options);
236✔
1859
        }
1860
    }
1861

1862
    public clone() {
1863
        return this.finalizeClone(
2✔
1864
            new TernaryExpression(
1865
                this.test?.clone(),
6✔
1866
                util.cloneToken(this.questionMarkToken),
1867
                this.consequent?.clone(),
6✔
1868
                util.cloneToken(this.colonToken),
1869
                this.alternate?.clone()
6✔
1870
            ),
1871
            ['test', 'consequent', 'alternate']
1872
        );
1873
    }
1874
}
1875

1876
export class NullCoalescingExpression extends Expression {
1✔
1877
    constructor(
1878
        public consequent: Expression,
32✔
1879
        public questionQuestionToken: Token,
32✔
1880
        public alternate: Expression
32✔
1881
    ) {
1882
        super();
32✔
1883
        this.range = util.createBoundingRange(
32✔
1884
            consequent,
1885
            questionQuestionToken,
1886
            alternate
1887
        );
1888
    }
1889
    public readonly range: Range | undefined;
1890

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

1896
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1897
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
11✔
1898
        //discard names of global functions that cannot be passed by reference
1899
        allUniqueVarNames = allUniqueVarNames.filter(name => {
11✔
1900
            return !nonReferenceableFunctions.includes(name.toLowerCase());
22✔
1901
        });
1902

1903
        let hasMutatingExpression = [
11✔
1904
            ...consequentInfo.expressions,
1905
            ...alternateInfo.expressions
1906
        ].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e));
30✔
1907

1908
        if (hasMutatingExpression) {
11✔
1909
            result.push(
7✔
1910
                `(function(`,
1911
                //write all the scope variables as parameters.
1912
                //TODO handle when there are more than 31 parameters
1913
                allUniqueVarNames.join(', '),
1914
                ')',
1915
                state.newline,
1916
                //double indent so our `end function` line is still indented one at the end
1917
                state.indent(2),
1918
                //evaluate the consequent exactly once, and then use it in the following condition
1919
                `__bsConsequent = `,
1920
                ...this.consequent.transpile(state),
1921
                state.newline,
1922
                state.indent(),
1923
                `if __bsConsequent <> invalid then`,
1924
                state.newline,
1925
                state.indent(1),
1926
                'return __bsConsequent',
1927
                state.newline,
1928
                state.indent(-1),
1929
                'else',
1930
                state.newline,
1931
                state.indent(1),
1932
                'return ',
1933
                ...this.alternate.transpile(state),
1934
                state.newline,
1935
                state.indent(-1),
1936
                'end if',
1937
                state.newline,
1938
                state.indent(-1),
1939
                'end function)(',
1940
                allUniqueVarNames.join(', '),
1941
                ')'
1942
            );
1943
            state.blockDepth--;
7✔
1944
        } else {
1945
            result.push(
4✔
1946
                state.bslibPrefix + `_coalesce(`,
1947
                ...this.consequent.transpile(state),
1948
                ', ',
1949
                ...this.alternate.transpile(state),
1950
                ')'
1951
            );
1952
        }
1953
        return result;
11✔
1954
    }
1955

1956
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1957
        if (options.walkMode & InternalWalkMode.walkExpressions) {
64!
1958
            walk(this, 'consequent', visitor, options);
64✔
1959
            walk(this, 'alternate', visitor, options);
64✔
1960
        }
1961
    }
1962

1963
    public clone() {
1964
        return this.finalizeClone(
2✔
1965
            new NullCoalescingExpression(
1966
                this.consequent?.clone(),
6✔
1967
                util.cloneToken(this.questionQuestionToken),
1968
                this.alternate?.clone()
6✔
1969
            ),
1970
            ['consequent', 'alternate']
1971
        );
1972
    }
1973
}
1974

1975
export class RegexLiteralExpression extends Expression {
1✔
1976
    public constructor(
1977
        public tokens: {
46✔
1978
            regexLiteral: Token;
1979
        }
1980
    ) {
1981
        super();
46✔
1982
    }
1983

1984
    public get range() {
1985
        return this.tokens?.regexLiteral?.range;
55!
1986
    }
1987

1988
    public transpile(state: BrsTranspileState): TranspileResult {
1989
        let text = this.tokens.regexLiteral?.text ?? '';
42!
1990
        let flags = '';
42✔
1991
        //get any flags from the end
1992
        const flagMatch = /\/([a-z]+)$/i.exec(text);
42✔
1993
        if (flagMatch) {
42✔
1994
            text = text.substring(0, flagMatch.index + 1);
2✔
1995
            flags = flagMatch[1];
2✔
1996
        }
1997
        let pattern = text
42✔
1998
            //remove leading and trailing slashes
1999
            .substring(1, text.length - 1)
2000
            //escape quotemarks
2001
            .split('"').join('" + chr(34) + "');
2002

2003
        return [
42✔
2004
            state.sourceNode(this.tokens.regexLiteral, [
2005
                'CreateObject("roRegex", ',
2006
                `"${pattern}", `,
2007
                `"${flags}"`,
2008
                ')'
2009
            ])
2010
        ];
2011
    }
2012

2013
    walk(visitor: WalkVisitor, options: WalkOptions) {
2014
        //nothing to walk
2015
    }
2016

2017
    public clone() {
2018
        return this.finalizeClone(
1✔
2019
            new RegexLiteralExpression({
2020
                regexLiteral: util.cloneToken(this.tokens.regexLiteral)
2021
            })
2022
        );
2023
    }
2024
}
2025

2026

2027
export class TypeCastExpression extends Expression {
1✔
2028
    constructor(
2029
        public obj: Expression,
19✔
2030
        public asToken: Token,
19✔
2031
        public typeToken: Token
19✔
2032
    ) {
2033
        super();
19✔
2034
        this.range = util.createBoundingRange(
19✔
2035
            this.obj,
2036
            this.asToken,
2037
            this.typeToken
2038
        );
2039
    }
2040

2041
    public range: Range;
2042

2043
    public transpile(state: BrsTranspileState): TranspileResult {
2044
        return this.obj.transpile(state);
11✔
2045
    }
2046
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2047
        if (options.walkMode & InternalWalkMode.walkExpressions) {
51!
2048
            walk(this, 'obj', visitor, options);
51✔
2049
        }
2050
    }
2051

2052
    public clone() {
2053
        return this.finalizeClone(
2✔
2054
            new TypeCastExpression(
2055
                this.obj?.clone(),
6✔
2056
                util.cloneToken(this.asToken),
2057
                util.cloneToken(this.typeToken)
2058
            ),
2059
            ['obj']
2060
        );
2061
    }
2062
}
2063

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

2067
function expressionToValue(expr: Expression, strict: boolean): ExpressionValue {
2068
    if (!expr) {
30!
2069
        return null;
×
2070
    }
2071
    if (isUnaryExpression(expr) && isLiteralNumber(expr.right)) {
30✔
2072
        return numberExpressionToValue(expr.right, expr.operator.text);
1✔
2073
    }
2074
    if (isLiteralString(expr)) {
29✔
2075
        //remove leading and trailing quotes
2076
        return expr.token.text.replace(/^"/, '').replace(/"$/, '');
5✔
2077
    }
2078
    if (isLiteralNumber(expr)) {
24✔
2079
        return numberExpressionToValue(expr);
11✔
2080
    }
2081

2082
    if (isLiteralBoolean(expr)) {
13✔
2083
        return expr.token.text.toLowerCase() === 'true';
3✔
2084
    }
2085
    if (isArrayLiteralExpression(expr)) {
10✔
2086
        return expr.elements
3✔
2087
            .filter(e => !isCommentStatement(e))
7✔
2088
            .map(e => expressionToValue(e, strict));
7✔
2089
    }
2090
    if (isAALiteralExpression(expr)) {
7✔
2091
        return expr.elements.reduce((acc, e) => {
3✔
2092
            if (!isCommentStatement(e) && !(isAAIndexedMemberExpression(e))) {
3!
2093
                acc[e.keyToken.text] = expressionToValue(e.value, strict);
3✔
2094
            }
2095
            return acc;
3✔
2096
        }, {});
2097
    }
2098
    //for annotations, we only support serializing pure string values
2099
    if (isTemplateStringExpression(expr)) {
4✔
2100
        if (expr.quasis?.length === 1 && expr.expressions.length === 0) {
2!
2101
            return expr.quasis[0].expressions.map(x => x.token.text).join('');
10✔
2102
        }
2103
    }
2104
    return strict ? null : expr;
2✔
2105
}
2106

2107
function numberExpressionToValue(expr: LiteralExpression, operator = '') {
11✔
2108
    if (isIntegerType(expr.type) || isLongIntegerType(expr.type)) {
12!
2109
        return parseInt(operator + expr.token.text);
12✔
2110
    } else {
2111
        return parseFloat(operator + expr.token.text);
×
2112
    }
2113
}
2114

2115
/**
2116
 * A list of names of functions that are restricted from being stored to a
2117
 * variable, property, or passed as an argument. (i.e. `type` or `createobject`).
2118
 * Names are stored in lower case.
2119
 */
2120
const nonReferenceableFunctions = [
1✔
2121
    'createobject',
2122
    'type',
2123
    'getglobalaa',
2124
    'box',
2125
    'run',
2126
    'eval',
2127
    'getlastruncompileerror',
2128
    'getlastrunruntimeerror',
2129
    'tab',
2130
    'pos'
2131
];
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc