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

rokucommunity / brighterscript / #15513

28 Mar 2026 11:39PM UTC coverage: 88.966% (-0.07%) from 89.035%
#15513

push

web-flow
Merge f9c27a6f4 into f3673e7df

8183 of 9705 branches covered (84.32%)

Branch coverage included in aggregate %.

105 of 111 new or added lines in 7 files covered. (94.59%)

1 existing line in 1 file now uncovered.

10427 of 11213 relevant lines covered (92.99%)

1991.81 hits per line

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

91.86
/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,
366✔
29
        public operator: Token,
366✔
30
        public right: Expression
366✔
31
    ) {
32
        super();
366✔
33
        this.range = util.createBoundingRange(this.left, this.operator, this.right);
366✔
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) {
866!
50
            walk(this, 'left', visitor, options);
866✔
51
            walk(this, 'right', visitor, options);
866✔
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,
740✔
78
        /**
79
         * Can either be `(`, or `?(` for optional chaining
80
         */
81
        readonly openingParen: Token,
740✔
82
        readonly closingParen: Token,
740✔
83
        readonly args: Expression[],
740✔
84
        unused?: any
85
    ) {
86
        super();
740✔
87
        this.range = util.createBoundingRange(this.callee, this.openingParen, ...args ?? [], this.closingParen);
740✔
88
    }
89

90
    public readonly range: Range | undefined;
91

92
    /**
93
     * When named arguments are used and successfully resolved during validation, this holds the
94
     * positional-ordered argument list used for transpilation. If undefined, `args` is used directly.
95
     */
96
    public resolvedArgs?: Expression[];
97

98
    /**
99
     * Get the name of the wrapping namespace (if it exists)
100
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
101
     */
102
    public get namespaceName() {
103
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
104
    }
105

106
    transpile(state: BrsTranspileState, nameOverride?: string) {
107
        let result: TranspileResult = [];
257✔
108

109
        //transpile the name
110
        if (nameOverride) {
257✔
111
            result.push(state.sourceNode(this.callee, nameOverride));
9✔
112
        } else {
113
            result.push(...this.callee.transpile(state));
248✔
114
        }
115

116
        result.push(
257✔
117
            state.transpileToken(this.openingParen)
118
        );
119
        // Use resolvedArgs when named arguments have been reordered during validation,
120
        // otherwise fall back to the original args list
121
        const argsToTranspile = this.resolvedArgs ?? this.args;
257✔
122
        for (let i = 0; i < argsToTranspile.length; i++) {
257✔
123
            //add comma between args
124
            if (i > 0) {
155✔
125
                result.push(', ');
22✔
126
            }
127
            let arg = argsToTranspile[i];
155✔
128
            result.push(...arg.transpile(state));
155✔
129
        }
130
        if (this.closingParen) {
257!
131
            result.push(
257✔
132
                state.transpileToken(this.closingParen)
133
            );
134
        }
135
        return result;
257✔
136
    }
137

138
    walk(visitor: WalkVisitor, options: WalkOptions) {
139
        if (options.walkMode & InternalWalkMode.walkExpressions) {
2,223!
140
            walk(this, 'callee', visitor, options);
2,223✔
141
            walkArray(this.args, visitor, options, this);
2,223✔
142
        }
143
    }
144

145
    public clone() {
146
        return this.finalizeClone(
13✔
147
            new CallExpression(
148
                this.callee?.clone(),
39✔
149
                util.cloneToken(this.openingParen),
150
                util.cloneToken(this.closingParen),
151
                this.args?.map(e => e?.clone())
12✔
152
            ),
153
            ['callee', 'args']
154
        );
155
    }
156
}
157

158
export class NamedArgumentExpression extends Expression {
1✔
159
    constructor(
160
        readonly name: Identifier,
24✔
161
        readonly colon: Token,
24✔
162
        readonly value: Expression
24✔
163
    ) {
164
        super();
24✔
165
        this.range = util.createBoundingRange(this.name, this.colon, this.value);
24✔
166
    }
167

168
    public readonly range: Range | undefined;
169

170
    transpile(state: BrsTranspileState) {
171
        // Named arguments are resolved to positional order during validation.
172
        // If reached during transpile (e.g. for an unresolvable call), emit the value only.
NEW
173
        return this.value.transpile(state);
×
174
    }
175

176
    walk(visitor: WalkVisitor, options: WalkOptions) {
177
        // eslint-disable-next-line no-bitwise
178
        if (options.walkMode & InternalWalkMode.walkExpressions) {
68!
179
            walk(this, 'value', visitor, options);
68✔
180
        }
181
    }
182

183
    public clone() {
NEW
184
        return this.finalizeClone(
×
185
            new NamedArgumentExpression(
186
                util.cloneToken(this.name),
187
                util.cloneToken(this.colon),
188
                this.value?.clone()
×
189
            ),
190
            ['value']
191
        );
192
    }
193
}
194

195
export class FunctionExpression extends Expression implements TypedefProvider {
1✔
196
    constructor(
197
        readonly parameters: FunctionParameterExpression[],
2,090✔
198
        public body: Block,
2,090✔
199
        readonly functionType: Token | null,
2,090✔
200
        public end: Token,
2,090✔
201
        readonly leftParen: Token,
2,090✔
202
        readonly rightParen: Token,
2,090✔
203
        readonly asToken?: Token,
2,090✔
204
        readonly returnTypeToken?: Token
2,090✔
205
    ) {
206
        super();
2,090✔
207
        this.setReturnType(); // set the initial return type that we parse
2,090✔
208

209
        //if there's a body, and it doesn't have a SymbolTable, assign one
210
        if (this.body && !this.body.symbolTable) {
2,090✔
211
            this.body.symbolTable = new SymbolTable(`Function Body`);
146✔
212
        }
213
        this.symbolTable = new SymbolTable('FunctionExpression', () => this.parent?.getSymbolTable());
2,090!
214
    }
215

216
    /**
217
     * The type this function returns
218
     */
219
    public returnType: BscType;
220

221
    /**
222
     * Does this method require the return type to be present after transpile (useful for `as void` or the `as boolean` in `onKeyEvent`)
223
     */
224
    private requiresReturnType: boolean;
225

226
    /**
227
     * Get the name of the wrapping namespace (if it exists)
228
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
229
     */
230
    public get namespaceName() {
231
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
232
    }
233

234
    /**
235
     * Get the name of the wrapping namespace (if it exists)
236
     * @deprecated use `.findAncestor(isFunctionExpression)` instead.
237
     */
238
    public get parentFunction() {
239
        return this.findAncestor<FunctionExpression>(isFunctionExpression);
1,187✔
240
    }
241

242
    /**
243
     * The list of function calls that are declared within this function scope. This excludes CallExpressions
244
     * declared in child functions
245
     */
246
    public callExpressions = [] as CallExpression[];
2,090✔
247

248
    /**
249
     * If this function is part of a FunctionStatement, this will be set. Otherwise this will be undefined
250
     */
251
    public functionStatement?: FunctionStatement;
252

253
    /**
254
     * A list of all child functions declared directly within this function
255
     * @deprecated use `.walk(createVisitor({ FunctionExpression: ()=>{}), { walkMode: WalkMode.visitAllRecursive })` instead
256
     */
257
    public get childFunctionExpressions() {
258
        const expressions = [] as FunctionExpression[];
4✔
259
        this.walk(createVisitor({
4✔
260
            FunctionExpression: (expression) => {
261
                expressions.push(expression);
6✔
262
            }
263
        }), {
264
            walkMode: WalkMode.visitAllRecursive
265
        });
266
        return expressions;
4✔
267
    }
268

269
    /**
270
     * The range of the function, starting at the 'f' in function or 's' in sub (or the open paren if the keyword is missing),
271
     * and ending with the last n' in 'end function' or 'b' in 'end sub'
272
     */
273
    public get range() {
274
        return util.createBoundingRange(
5,432✔
275
            this.functionType, this.leftParen,
276
            ...this.parameters ?? [],
16,296✔
277
            this.rightParen,
278
            this.asToken,
279
            this.returnTypeToken,
280
            this.end
281
        );
282
    }
283

284
    transpile(state: BrsTranspileState, name?: Identifier, includeBody = true) {
394✔
285
        let results = [] as TranspileResult;
394✔
286
        //'function'|'sub'
287
        results.push(
394✔
288
            state.transpileToken(this.functionType!)
289
        );
290
        //functionName?
291
        if (name) {
394✔
292
            results.push(
389✔
293
                ' ',
294
                state.transpileToken(name)
295
            );
296
        }
297
        //leftParen
298
        results.push(
394✔
299
            state.transpileToken(this.leftParen)
300
        );
301
        //parameters
302
        for (let i = 0; i < this.parameters.length; i++) {
394✔
303
            let param = this.parameters[i];
138✔
304
            //add commas
305
            if (i > 0) {
138✔
306
                results.push(', ');
57✔
307
            }
308
            //add parameter
309
            results.push(param.transpile(state));
138✔
310
        }
311
        //right paren
312
        results.push(
394✔
313
            state.transpileToken(this.rightParen)
314
        );
315
        //as [Type]
316
        this.setReturnType(); // check one more time before transpile
394✔
317
        if (this.asToken && !(state.options.removeParameterTypes && !this.requiresReturnType)) {
394✔
318
            results.push(
39✔
319
                ' ',
320
                //as
321
                state.transpileToken(this.asToken),
322
                ' ',
323
                //return type
324
                state.sourceNode(this.returnTypeToken!, this.returnType.toTypeString())
325
            );
326
        }
327
        if (includeBody) {
394!
328
            state.lineage.unshift(this);
394✔
329
            let body = this.body.transpile(state);
394✔
330
            state.lineage.shift();
394✔
331
            results.push(...body);
394✔
332
        }
333
        results.push('\n');
394✔
334
        //'end sub'|'end function'
335
        results.push(
394✔
336
            state.indent(),
337
            state.transpileToken(this.end)
338
        );
339
        return results;
394✔
340
    }
341

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

375
    walk(visitor: WalkVisitor, options: WalkOptions) {
376
        if (options.walkMode & InternalWalkMode.walkExpressions) {
4,975!
377
            walkArray(this.parameters, visitor, options, this);
4,975✔
378

379
            //This is the core of full-program walking...it allows us to step into sub functions
380
            if (options.walkMode & InternalWalkMode.recurseChildFunctions) {
4,975!
381
                walk(this, 'body', visitor, options);
4,975✔
382
            }
383
        }
384
    }
385

386
    getFunctionType(): FunctionType {
387
        let functionType = new FunctionType(this.returnType);
1,947✔
388
        functionType.isSub = this.functionType?.text === 'sub';
1,947!
389
        for (let param of this.parameters) {
1,947✔
390
            functionType.addParameter(param.name.text, param.type, !!param.typeToken);
703✔
391
        }
392
        return functionType;
1,947✔
393
    }
394

395
    private setReturnType() {
396

397
        /**
398
         * RokuOS methods can be written several different ways:
399
         * 1. Function() : return withValue
400
         * 2. Function() as type : return withValue
401
         * 3. Function() as void : return
402
         *
403
         * 4. Sub() : return
404
         * 5. Sub () as void : return
405
         * 6. Sub() as type : return withValue
406
         *
407
         * Formats (1), (2), and (6) throw a compile error if there IS NOT a return value in the function body.
408
         * Formats (3), (4), and (5) throw a compile error if there IS a return value in the function body.
409
         *
410
         * 7. Additionally, as a special case, the OS requires that `onKeyEvent()` be defined with `as boolean`
411
         */
412

413
        const isSub = this.functionType?.text.toLowerCase() === 'sub';
2,484!
414

415
        if (this.returnTypeToken) {
2,484✔
416
            this.returnType = util.tokenToBscType(this.returnTypeToken);
168✔
417
        } else if (isSub) {
2,316✔
418
            this.returnType = new VoidType();
1,870✔
419
        } else {
420
            this.returnType = DynamicType.instance;
446✔
421
        }
422

423
        if ((isFunctionStatement(this.parent) || isMethodStatement(this.parent)) && this.parent?.name?.text.toLowerCase() === 'onkeyevent') {
2,484!
424
            // onKeyEvent() requires 'as Boolean' otherwise RokuOS throws errors
425
            this.requiresReturnType = true;
1✔
426
        } else if (isSub && !isVoidType(this.returnType)) { // format (6)
2,483✔
427
            this.requiresReturnType = true;
64✔
428
        } else if (this.returnTypeToken && isVoidType(this.returnType)) { // format (3)
2,419✔
429
            this.requiresReturnType = true;
18✔
430
        }
431
    }
432

433
    public clone() {
434
        const clone = this.finalizeClone(
108✔
435
            new FunctionExpression(
436
                this.parameters?.map(e => e?.clone()),
7✔
437
                this.body?.clone(),
324✔
438
                util.cloneToken(this.functionType),
439
                util.cloneToken(this.end),
440
                util.cloneToken(this.leftParen),
441
                util.cloneToken(this.rightParen),
442
                util.cloneToken(this.asToken),
443
                util.cloneToken(this.returnTypeToken)
444
            ),
445
            ['body']
446
        );
447

448
        //rebuild the .callExpressions list in the clone
449
        clone.body?.walk?.((node) => {
108✔
450
            if (isCallExpression(node) && !isNewExpression(node.parent)) {
210✔
451
                clone.callExpressions.push(node);
6✔
452
            }
453
        }, { walkMode: WalkMode.visitExpressions });
454
        return clone;
108✔
455
    }
456
}
457

458
export class FunctionParameterExpression extends Expression {
1✔
459
    constructor(
460
        public name: Identifier,
741✔
461
        public typeToken?: Token,
741✔
462
        public defaultValue?: Expression,
741✔
463
        public asToken?: Token
741✔
464
    ) {
465
        super();
741✔
466
        if (typeToken) {
741✔
467
            this.type = util.tokenToBscType(typeToken);
359✔
468
        } else {
469
            this.type = new DynamicType();
382✔
470
        }
471
    }
472

473
    public type: BscType;
474

475
    public get range(): Range | undefined {
476
        return util.createBoundingRange(
456✔
477
            this.name,
478
            this.asToken,
479
            this.typeToken,
480
            this.defaultValue
481
        );
482
    }
483

484
    public transpile(state: BrsTranspileState) {
485
        let result = [
155✔
486
            //name
487
            state.transpileToken(this.name)
488
        ] as any[];
489
        //default value
490
        if (this.defaultValue) {
155✔
491
            result.push(' = ');
13✔
492
            result.push(this.defaultValue.transpile(state));
13✔
493
        }
494
        //type declaration
495
        if (this.asToken && !state.options.removeParameterTypes) {
155✔
496
            result.push(' ');
100✔
497
            result.push(state.transpileToken(this.asToken));
100✔
498
            result.push(' ');
100✔
499
            result.push(state.sourceNode(this.typeToken!, this.type.toTypeString()));
100✔
500
        }
501

502
        return result;
155✔
503
    }
504

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

508
        if (this.defaultValue) {
15✔
509
            results.push(' = ', ...(this.defaultValue.getTypedef(state) ?? this.defaultValue.transpile(state)));
7!
510
        }
511

512
        if (this.asToken) {
15✔
513
            results.push(' as ');
11✔
514

515
            if (this.typeToken) {
11!
516
                results.push(this.typeToken.text);
11✔
517
            }
518
        }
519

520
        return results;
15✔
521
    }
522

523
    walk(visitor: WalkVisitor, options: WalkOptions) {
524
        // eslint-disable-next-line no-bitwise
525
        if (this.defaultValue && options.walkMode & InternalWalkMode.walkExpressions) {
2,260✔
526
            walk(this, 'defaultValue', visitor, options);
728✔
527
        }
528
    }
529

530
    public clone() {
531
        return this.finalizeClone(
14✔
532
            new FunctionParameterExpression(
533
                util.cloneToken(this.name),
534
                util.cloneToken(this.typeToken),
535
                this.defaultValue?.clone(),
42✔
536
                util.cloneToken(this.asToken)
537
            ),
538
            ['defaultValue']
539
        );
540
    }
541
}
542

543
export class NamespacedVariableNameExpression extends Expression {
1✔
544
    constructor(
545
        //if this is a `DottedGetExpression`, it must be comprised only of `VariableExpression`s
546
        readonly expression: DottedGetExpression | VariableExpression
513✔
547
    ) {
548
        super();
513✔
549
        this.range = expression?.range;
513✔
550
    }
551
    range: Range | undefined;
552

553
    transpile(state: BrsTranspileState) {
554
        return [
4✔
555
            state.sourceNode(this, this.getName(ParseMode.BrightScript))
556
        ];
557
    }
558

559
    getTypedef(state) {
560
        return [
×
561
            state.sourceNode(this, this.getName(ParseMode.BrighterScript))
562
        ];
563
    }
564

565
    public getNameParts() {
566
        let parts = [] as string[];
3,746✔
567
        if (isVariableExpression(this.expression)) {
3,746✔
568
            parts.push(this.expression.name.text);
2,669✔
569
        } else {
570
            let expr = this.expression;
1,077✔
571

572
            parts.push(expr.name.text);
1,077✔
573

574
            while (isVariableExpression(expr) === false) {
1,077✔
575
                expr = expr.obj as DottedGetExpression;
1,252✔
576
                parts.unshift(expr.name.text);
1,252✔
577
            }
578
        }
579
        return parts;
3,746✔
580
    }
581

582
    getName(parseMode: ParseMode) {
583
        if (parseMode === ParseMode.BrighterScript) {
3,698✔
584
            return this.getNameParts().join('.');
3,462✔
585
        } else {
586
            return this.getNameParts().join('_');
236✔
587
        }
588
    }
589

590
    walk(visitor: WalkVisitor, options: WalkOptions) {
591
        this.expression?.link();
1,191!
592
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,191✔
593
            walk(this, 'expression', visitor, options);
1,167✔
594
        }
595
    }
596

597
    public clone() {
598
        return this.finalizeClone(
6✔
599
            new NamespacedVariableNameExpression(
600
                this.expression?.clone()
18✔
601
            )
602
        );
603
    }
604
}
605

606
export class DottedGetExpression extends Expression {
1✔
607
    constructor(
608
        readonly obj: Expression,
1,178✔
609
        readonly name: Identifier,
1,178✔
610
        /**
611
         * Can either be `.`, or `?.` for optional chaining
612
         */
613
        readonly dot: Token
1,178✔
614
    ) {
615
        super();
1,178✔
616
        this.range = util.createBoundingRange(this.obj, this.dot, this.name);
1,178✔
617
    }
618

619
    public readonly range: Range | undefined;
620

621
    transpile(state: BrsTranspileState) {
622
        //if the callee starts with a namespace name, transpile the name
623
        if (state.file.calleeStartsWithNamespace(this)) {
245✔
624
            return new NamespacedVariableNameExpression(this as DottedGetExpression | VariableExpression).transpile(state);
3✔
625
        } else {
626
            return [
242✔
627
                ...this.obj.transpile(state),
628
                state.transpileToken(this.dot),
629
                state.transpileToken(this.name)
630
            ];
631
        }
632
    }
633

634
    getTypedef(state: BrsTranspileState) {
635
        //always transpile the dots for typedefs
636
        return [
4✔
637
            ...(this.obj.getTypedef ? this.obj.getTypedef(state) : this.obj.transpile(state)),
4!
638
            state.transpileToken(this.dot),
639
            state.transpileToken(this.name)
640
        ];
641
    }
642

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

649
    public clone() {
650
        return this.finalizeClone(
8✔
651
            new DottedGetExpression(
652
                this.obj?.clone(),
24✔
653
                util.cloneToken(this.name),
654
                util.cloneToken(this.dot)
655
            ),
656
            ['obj']
657
        );
658
    }
659
}
660

661
export class XmlAttributeGetExpression extends Expression {
1✔
662
    constructor(
663
        readonly obj: Expression,
15✔
664
        readonly name: Identifier,
15✔
665
        /**
666
         * Can either be `@`, or `?@` for optional chaining
667
         */
668
        readonly at: Token
15✔
669
    ) {
670
        super();
15✔
671
        this.range = util.createBoundingRange(this.obj, this.at, this.name);
15✔
672
    }
673

674
    public readonly range: Range | undefined;
675

676
    transpile(state: BrsTranspileState) {
677
        return [
3✔
678
            ...this.obj.transpile(state),
679
            state.transpileToken(this.at),
680
            state.transpileToken(this.name)
681
        ];
682
    }
683

684
    walk(visitor: WalkVisitor, options: WalkOptions) {
685
        if (options.walkMode & InternalWalkMode.walkExpressions) {
26!
686
            walk(this, 'obj', visitor, options);
26✔
687
        }
688
    }
689

690
    public clone() {
691
        return this.finalizeClone(
2✔
692
            new XmlAttributeGetExpression(
693
                this.obj?.clone(),
6✔
694
                util.cloneToken(this.name),
695
                util.cloneToken(this.at)
696
            ),
697
            ['obj']
698
        );
699
    }
700
}
701

702
export class IndexedGetExpression extends Expression {
1✔
703
    constructor(
704
        public obj: Expression,
152✔
705
        public index: Expression,
152✔
706
        /**
707
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.`
708
         */
709
        public openingSquare: Token,
152✔
710
        public closingSquare: Token,
152✔
711
        public questionDotToken?: Token, //  ? or ?.
152✔
712
        /**
713
         * More indexes, separated by commas
714
         */
715
        public additionalIndexes?: Expression[]
152✔
716
    ) {
717
        super();
152✔
718
        this.range = util.createBoundingRange(this.obj, this.openingSquare, this.questionDotToken, this.openingSquare, this.index, this.closingSquare);
152✔
719
        this.additionalIndexes ??= [];
152✔
720
    }
721

722
    public readonly range: Range | undefined;
723

724
    transpile(state: BrsTranspileState) {
725
        const result = [];
64✔
726
        result.push(
64✔
727
            ...this.obj.transpile(state),
728
            this.questionDotToken ? state.transpileToken(this.questionDotToken) : '',
64✔
729
            state.transpileToken(this.openingSquare)
730
        );
731
        const indexes = [this.index, ...this.additionalIndexes ?? []];
64!
732
        for (let i = 0; i < indexes.length; i++) {
64✔
733
            //add comma between indexes
734
            if (i > 0) {
72✔
735
                result.push(', ');
8✔
736
            }
737
            let index = indexes[i];
72✔
738
            result.push(
72✔
739
                ...(index?.transpile(state) ?? [])
432!
740
            );
741
        }
742
        result.push(
64✔
743
            this.closingSquare ? state.transpileToken(this.closingSquare) : ''
64!
744
        );
745
        return result;
64✔
746
    }
747

748
    walk(visitor: WalkVisitor, options: WalkOptions) {
749
        if (options.walkMode & InternalWalkMode.walkExpressions) {
336!
750
            walk(this, 'obj', visitor, options);
336✔
751
            walk(this, 'index', visitor, options);
336✔
752
            walkArray(this.additionalIndexes, visitor, options, this);
336✔
753
        }
754
    }
755

756
    public clone() {
757
        return this.finalizeClone(
5✔
758
            new IndexedGetExpression(
759
                this.obj?.clone(),
15✔
760
                this.index?.clone(),
15✔
761
                util.cloneToken(this.openingSquare),
762
                util.cloneToken(this.closingSquare),
763
                util.cloneToken(this.questionDotToken),
764
                this.additionalIndexes?.map(e => e?.clone())
2✔
765
            ),
766
            ['obj', 'index', 'additionalIndexes']
767
        );
768
    }
769
}
770

771
export class GroupingExpression extends Expression {
1✔
772
    constructor(
773
        readonly tokens: {
40✔
774
            left: Token;
775
            right: Token;
776
        },
777
        public expression: Expression
40✔
778
    ) {
779
        super();
40✔
780
        this.range = util.createBoundingRange(this.tokens.left, this.expression, this.tokens.right);
40✔
781
    }
782

783
    public readonly range: Range | undefined;
784

785
    transpile(state: BrsTranspileState) {
786
        if (isTypeCastExpression(this.expression)) {
12✔
787
            return this.expression.transpile(state);
7✔
788
        }
789
        return [
5✔
790
            state.transpileToken(this.tokens.left),
791
            ...this.expression.transpile(state),
792
            state.transpileToken(this.tokens.right)
793
        ];
794
    }
795

796
    walk(visitor: WalkVisitor, options: WalkOptions) {
797
        if (options.walkMode & InternalWalkMode.walkExpressions) {
87!
798
            walk(this, 'expression', visitor, options);
87✔
799
        }
800
    }
801

802
    public clone() {
803
        return this.finalizeClone(
2✔
804
            new GroupingExpression(
805
                {
806
                    left: util.cloneToken(this.tokens.left),
807
                    right: util.cloneToken(this.tokens.right)
808
                },
809
                this.expression?.clone()
6✔
810
            ),
811
            ['expression']
812
        );
813
    }
814
}
815

816
export class LiteralExpression extends Expression {
1✔
817
    constructor(
818
        public token: Token
3,558✔
819
    ) {
820
        super();
3,558✔
821
        this.type = util.tokenToBscType(token);
3,558✔
822
    }
823

824
    public get range() {
825
        return this.token.range;
8,194✔
826
    }
827

828
    /**
829
     * The (data) type of this expression
830
     */
831
    public type: BscType;
832

833
    transpile(state: BrsTranspileState) {
834
        let text: string;
835
        if (this.token.kind === TokenKind.TemplateStringQuasi) {
895✔
836
            //wrap quasis with quotes (and escape inner quotemarks)
837
            text = `"${this.token.text.replace(/"/g, '""')}"`;
35✔
838

839
        } else if (isStringType(this.type)) {
860✔
840
            text = this.token.text;
355✔
841
            //add trailing quotemark if it's missing. We will have already generated a diagnostic for this.
842
            if (text.endsWith('"') === false) {
355✔
843
                text += '"';
1✔
844
            }
845
        } else {
846
            text = this.token.text;
505✔
847
        }
848

849
        return [
895✔
850
            state.sourceNode(this, text)
851
        ];
852
    }
853

854
    walk(visitor: WalkVisitor, options: WalkOptions) {
855
        //nothing to walk
856
    }
857

858
    public clone() {
859
        return this.finalizeClone(
104✔
860
            new LiteralExpression(
861
                util.cloneToken(this.token)
862
            )
863
        );
864
    }
865
}
866

867
/**
868
 * This is a special expression only used within template strings. It exists so we can prevent producing lots of empty strings
869
 * during template string transpile by identifying these expressions explicitly and skipping the bslib_toString around them
870
 */
871
export class EscapedCharCodeLiteralExpression extends Expression {
1✔
872
    constructor(
873
        readonly token: Token & { charCode: number }
37✔
874
    ) {
875
        super();
37✔
876
        this.range = token.range;
37✔
877
    }
878
    readonly range: Range;
879

880
    transpile(state: BrsTranspileState) {
881
        return [
15✔
882
            state.sourceNode(this, `chr(${this.token.charCode})`)
883
        ];
884
    }
885

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

890
    public clone() {
891
        return this.finalizeClone(
3✔
892
            new EscapedCharCodeLiteralExpression(
893
                util.cloneToken(this.token)
894
            )
895
        );
896
    }
897
}
898

899
export class ArrayLiteralExpression extends Expression {
1✔
900
    constructor(
901
        readonly elements: Array<Expression | CommentStatement>,
134✔
902
        readonly open: Token,
134✔
903
        readonly close: Token,
134✔
904
        readonly hasSpread = false
134✔
905
    ) {
906
        super();
134✔
907
        this.range = util.createBoundingRange(this.open, ...this.elements ?? [], this.close);
134✔
908
    }
909

910
    public readonly range: Range | undefined;
911

912
    transpile(state: BrsTranspileState) {
913
        let result = [] as TranspileResult;
60✔
914
        result.push(
60✔
915
            state.transpileToken(this.open)
916
        );
917
        let hasChildren = this.elements.length > 0;
60✔
918
        state.blockDepth++;
60✔
919

920
        for (let i = 0; i < this.elements.length; i++) {
60✔
921
            let previousElement = this.elements[i - 1];
92✔
922
            let element = this.elements[i];
92✔
923

924
            if (isCommentStatement(element)) {
92✔
925
                //if the comment is on the same line as opening square or previous statement, don't add newline
926
                if (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element)) {
7✔
927
                    result.push(' ');
4✔
928
                } else {
929
                    result.push(
3✔
930
                        '\n',
931
                        state.indent()
932
                    );
933
                }
934
                state.lineage.unshift(this);
7✔
935
                result.push(element.transpile(state));
7✔
936
                state.lineage.shift();
7✔
937
            } else {
938
                result.push('\n');
85✔
939

940
                result.push(
85✔
941
                    state.indent(),
942
                    ...element.transpile(state)
943
                );
944
            }
945
        }
946
        state.blockDepth--;
60✔
947
        //add a newline between open and close if there are elements
948
        if (hasChildren) {
60✔
949
            result.push('\n');
38✔
950
            result.push(state.indent());
38✔
951
        }
952
        if (this.close) {
60!
953
            result.push(
60✔
954
                state.transpileToken(this.close)
955
            );
956
        }
957
        return result;
60✔
958
    }
959

960
    walk(visitor: WalkVisitor, options: WalkOptions) {
961
        if (options.walkMode & InternalWalkMode.walkExpressions) {
395!
962
            walkArray(this.elements, visitor, options, this);
395✔
963
        }
964
    }
965

966
    public clone() {
967
        return this.finalizeClone(
4✔
968
            new ArrayLiteralExpression(
969
                this.elements?.map(e => e?.clone()),
6✔
970
                util.cloneToken(this.open),
971
                util.cloneToken(this.close),
972
                this.hasSpread
973
            ),
974
            ['elements']
975
        );
976
    }
977
}
978

979
export class AAMemberExpression extends Expression {
1✔
980
    constructor(
981
        public keyToken: Token,
218✔
982
        public colonToken: Token,
218✔
983
        /** The expression evaluated to determine the member's initial value. */
984
        public value: Expression
218✔
985
    ) {
986
        super();
218✔
987
        this.range = util.createBoundingRange(this.keyToken, this.colonToken, this.value);
218✔
988
    }
989

990
    public range: Range | undefined;
991
    public commaToken?: Token;
992

993
    transpile(state: BrsTranspileState) {
994
        return [];
×
995
    }
996

997
    walk(visitor: WalkVisitor, options: WalkOptions) {
998
        walk(this, 'value', visitor, options);
399✔
999
    }
1000

1001
    public clone() {
1002
        return this.finalizeClone(
4✔
1003
            new AAMemberExpression(
1004
                util.cloneToken(this.keyToken),
1005
                util.cloneToken(this.colonToken),
1006
                this.value?.clone()
12✔
1007
            ),
1008
            ['value']
1009
        );
1010
    }
1011
}
1012

1013
export class AAIndexedMemberExpression extends Expression {
1✔
1014
    constructor(options: {
1015
        leftBracket: Token;
1016
        key: Expression;
1017
        rightBracket: Token;
1018
        colon: Token;
1019
        /** The expression evaluated to determine the member's initial value. */
1020
        value: Expression;
1021
    }) {
1022
        super();
31✔
1023
        this.key = options.key;
31✔
1024
        this.tokens = {
31✔
1025
            leftBracket: options.leftBracket,
1026
            rightBracket: options.rightBracket,
1027
            colon: options.colon
1028
        };
1029
        this.value = options.value;
31✔
1030
        this.range = util.createBoundingRange(this.tokens.leftBracket, this.key, this.tokens.rightBracket, this.tokens.colon, this.value);
31✔
1031
    }
1032

1033
    public readonly tokens: {
1034
        readonly leftBracket: Token;
1035
        readonly rightBracket: Token;
1036
        readonly colon: Token;
1037
    };
1038

1039
    public key: Expression;
1040
    /** The expression evaluated to determine the member's initial value. */
1041
    public value: Expression;
1042

1043
    public range: Range | undefined;
1044
    public commaToken?: Token;
1045

1046
    transpile(state: BrsTranspileState) {
1047
        return [];
×
1048
    }
1049

1050
    walk(visitor: WalkVisitor, options: WalkOptions) {
1051
        walk(this, 'key', visitor, options);
81✔
1052
        walk(this, 'value', visitor, options);
81✔
1053
    }
1054

1055
    public clone() {
1056
        return this.finalizeClone(
2✔
1057
            new AAIndexedMemberExpression({
1058
                leftBracket: util.cloneToken(this.tokens.leftBracket),
1059
                key: this.key?.clone(),
6!
1060
                rightBracket: util.cloneToken(this.tokens.rightBracket),
1061
                colon: util.cloneToken(this.tokens.colon),
1062
                value: this.value?.clone()
6✔
1063
            }),
1064
            ['key', 'value']
1065
        );
1066
    }
1067
}
1068

1069
export class AALiteralExpression extends Expression {
1✔
1070
    constructor(
1071
        readonly elements: Array<AAMemberExpression | AAIndexedMemberExpression | CommentStatement>,
244✔
1072
        readonly open: Token,
244✔
1073
        readonly close: Token
244✔
1074
    ) {
1075
        super();
244✔
1076
        this.range = util.createBoundingRange(this.open, ...this.elements ?? [], this.close);
244✔
1077
    }
1078

1079
    public readonly range: Range | undefined;
1080

1081
    transpile(state: BrsTranspileState) {
1082
        let result = [] as TranspileResult;
69✔
1083
        //open curly
1084
        result.push(
69✔
1085
            state.transpileToken(this.open)
1086
        );
1087
        let hasChildren = this.elements.length > 0;
69✔
1088
        //add newline if the object has children and the first child isn't a comment starting on the same line as opening curly
1089
        if (hasChildren && (isCommentStatement(this.elements[0]) === false || !util.linesTouch(this.elements[0], this.open))) {
69✔
1090
            result.push('\n');
37✔
1091
        }
1092
        state.blockDepth++;
69✔
1093
        for (let i = 0; i < this.elements.length; i++) {
69✔
1094
            let element = this.elements[i];
71✔
1095
            let previousElement = this.elements[i - 1];
71✔
1096
            let nextElement = this.elements[i + 1];
71✔
1097

1098
            //don't indent if comment is same-line
1099
            if (isCommentStatement(element as any) &&
71✔
1100
                (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element))
1101
            ) {
1102
                result.push(' ');
10✔
1103

1104
                //indent line
1105
            } else {
1106
                result.push(state.indent());
61✔
1107
            }
1108

1109
            //render comments
1110
            if (isCommentStatement(element)) {
71✔
1111
                result.push(...element.transpile(state));
14✔
1112
            } else {
1113
                //key
1114
                if ('tokens' in element) {
57✔
1115
                    //computed key: transpile the resolved expression (pre-transpile overrides it to a literal)
1116
                    result.push(...element.key.transpile(state));
8✔
1117
                } else {
1118
                    result.push(
49✔
1119
                        state.transpileToken(element.keyToken)
1120
                    );
1121
                }
1122
                //colon
1123
                result.push(
57✔
1124
                    state.transpileToken('tokens' in element ? element.tokens.colon : element.colonToken),
57✔
1125
                    ' '
1126
                );
1127

1128
                //value
1129
                result.push(...element.value.transpile(state));
57✔
1130
            }
1131

1132

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

1136
                //add a newline between statements
1137
            } else {
1138
                result.push('\n');
63✔
1139
            }
1140
        }
1141
        state.blockDepth--;
69✔
1142

1143
        //only indent the closing curly if we have children
1144
        if (hasChildren) {
69✔
1145
            result.push(state.indent());
39✔
1146
        }
1147
        //close curly
1148
        if (this.close) {
69!
1149
            result.push(
69✔
1150
                state.transpileToken(this.close)
1151
            );
1152
        }
1153
        return result;
69✔
1154
    }
1155

1156
    walk(visitor: WalkVisitor, options: WalkOptions) {
1157
        if (options.walkMode & InternalWalkMode.walkExpressions) {
506!
1158
            walkArray(this.elements, visitor, options, this);
506✔
1159
        }
1160
    }
1161

1162
    public clone() {
1163
        return this.finalizeClone(
8✔
1164
            new AALiteralExpression(
1165
                this.elements?.map(e => e?.clone()),
7✔
1166
                util.cloneToken(this.open),
1167
                util.cloneToken(this.close)
1168
            ),
1169
            ['elements']
1170
        );
1171
    }
1172
}
1173

1174
export class UnaryExpression extends Expression {
1✔
1175
    constructor(
1176
        public operator: Token,
41✔
1177
        public right: Expression
41✔
1178
    ) {
1179
        super();
41✔
1180
        this.range = util.createBoundingRange(this.operator, this.right);
41✔
1181
    }
1182

1183
    public readonly range: Range | undefined;
1184

1185
    transpile(state: BrsTranspileState) {
1186
        let separatingWhitespace: string | undefined;
1187
        if (isVariableExpression(this.right)) {
14✔
1188
            separatingWhitespace = this.right.name.leadingWhitespace;
6✔
1189
        } else if (isLiteralExpression(this.right)) {
8✔
1190
            separatingWhitespace = this.right.token.leadingWhitespace;
3✔
1191
        }
1192

1193
        return [
14✔
1194
            state.transpileToken(this.operator),
1195
            separatingWhitespace ?? ' ',
42✔
1196
            ...this.right.transpile(state)
1197
        ];
1198
    }
1199

1200
    walk(visitor: WalkVisitor, options: WalkOptions) {
1201
        if (options.walkMode & InternalWalkMode.walkExpressions) {
91!
1202
            walk(this, 'right', visitor, options);
91✔
1203
        }
1204
    }
1205

1206
    public clone() {
1207
        return this.finalizeClone(
2✔
1208
            new UnaryExpression(
1209
                util.cloneToken(this.operator),
1210
                this.right?.clone()
6✔
1211
            ),
1212
            ['right']
1213
        );
1214
    }
1215
}
1216

1217
export class VariableExpression extends Expression {
1✔
1218
    constructor(
1219
        readonly name: Identifier
2,298✔
1220
    ) {
1221
        super();
2,298✔
1222
        this.range = this.name?.range;
2,298!
1223
    }
1224

1225
    public readonly range: Range;
1226

1227
    public getName(parseMode: ParseMode) {
1228
        return this.name.text;
30✔
1229
    }
1230

1231
    transpile(state: BrsTranspileState) {
1232
        let result = [] as TranspileResult;
434✔
1233
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
434✔
1234
        //if the callee is the name of a known namespace function
1235
        if (namespace && state.file.calleeIsKnownNamespaceFunction(this, namespace.getName(ParseMode.BrighterScript))) {
434✔
1236
            result.push(
5✔
1237
                state.sourceNode(this, [
1238
                    namespace.getName(ParseMode.BrightScript),
1239
                    '_',
1240
                    this.getName(ParseMode.BrightScript)
1241
                ])
1242
            );
1243

1244
            //transpile  normally
1245
        } else {
1246
            result.push(
429✔
1247
                state.transpileToken(this.name)
1248
            );
1249
        }
1250
        return result;
434✔
1251
    }
1252

1253
    getTypedef(state: BrsTranspileState) {
1254
        return [
4✔
1255
            state.transpileToken(this.name)
1256
        ];
1257
    }
1258

1259
    walk(visitor: WalkVisitor, options: WalkOptions) {
1260
        //nothing to walk
1261
    }
1262

1263
    public clone() {
1264
        return this.finalizeClone(
42✔
1265
            new VariableExpression(
1266
                util.cloneToken(this.name)
1267
            )
1268
        );
1269
    }
1270
}
1271

1272
export class SourceLiteralExpression extends Expression {
1✔
1273
    constructor(
1274
        readonly token: Token
37✔
1275
    ) {
1276
        super();
37✔
1277
        this.range = token?.range;
37!
1278
    }
1279

1280
    public readonly range: Range;
1281

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

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

1311
    transpile(state: BrsTranspileState) {
1312
        let text: string;
1313
        switch (this.token.kind) {
31✔
1314
            case TokenKind.SourceFilePathLiteral:
40!
1315
                const pathUrl = fileUrl(state.srcPath);
3✔
1316
                text = `"${pathUrl.substring(0, 4)}" + "${pathUrl.substring(4)}"`;
3✔
1317
                break;
3✔
1318
            case TokenKind.SourceLineNumLiteral:
1319
                //TODO find first parent that has range, or default to -1
1320
                text = `${this.getClosestLineNumber()}`;
4✔
1321
                break;
4✔
1322
            case TokenKind.FunctionNameLiteral:
1323
                text = `"${this.getFunctionName(state, ParseMode.BrightScript)}"`;
4✔
1324
                break;
4✔
1325
            case TokenKind.SourceFunctionNameLiteral:
1326
                text = `"${this.getFunctionName(state, ParseMode.BrighterScript)}"`;
4✔
1327
                break;
4✔
1328
            case TokenKind.SourceNamespaceNameLiteral:
1329
                let namespaceParts = this.getFunctionName(state, ParseMode.BrighterScript).split('.');
×
1330
                namespaceParts.pop(); // remove the function name
×
1331

1332
                text = `"${namespaceParts.join('.')}"`;
×
1333
                break;
×
1334
            case TokenKind.SourceNamespaceRootNameLiteral:
1335
                let namespaceRootParts = this.getFunctionName(state, ParseMode.BrighterScript).split('.');
×
1336
                namespaceRootParts.pop(); // remove the function name
×
1337

1338
                let rootNamespace = namespaceRootParts.shift() ?? '';
×
1339
                text = `"${rootNamespace}"`;
×
1340
                break;
×
1341
            case TokenKind.SourceLocationLiteral:
1342
                const locationUrl = fileUrl(state.srcPath);
3✔
1343
                //TODO find first parent that has range, or default to -1
1344
                text = `"${locationUrl.substring(0, 4)}" + "${locationUrl.substring(4)}:${this.getClosestLineNumber()}"`;
3✔
1345
                break;
3✔
1346
            case TokenKind.PkgPathLiteral:
1347
                let pkgPath1 = `pkg:/${state.file.pkgPath}`
2✔
1348
                    .replace(/\\/g, '/')
1349
                    .replace(/\.bs$/i, '.brs');
1350

1351
                text = `"${pkgPath1}"`;
2✔
1352
                break;
2✔
1353
            case TokenKind.PkgLocationLiteral:
1354
                let pkgPath2 = `pkg:/${state.file.pkgPath}`
2✔
1355
                    .replace(/\\/g, '/')
1356
                    .replace(/\.bs$/i, '.brs');
1357

1358
                text = `"${pkgPath2}:" + str(LINE_NUM)`;
2✔
1359
                break;
2✔
1360
            case TokenKind.LineNumLiteral:
1361
            default:
1362
                //use the original text (because it looks like a variable)
1363
                text = this.token.text;
9✔
1364
                break;
9✔
1365

1366
        }
1367
        return [
31✔
1368
            state.sourceNode(this, text)
1369
        ];
1370
    }
1371

1372
    walk(visitor: WalkVisitor, options: WalkOptions) {
1373
        //nothing to walk
1374
    }
1375

1376
    public clone() {
1377
        return this.finalizeClone(
1✔
1378
            new SourceLiteralExpression(
1379
                util.cloneToken(this.token)
1380
            )
1381
        );
1382
    }
1383
}
1384

1385
/**
1386
 * This expression transpiles and acts exactly like a CallExpression,
1387
 * except we need to uniquely identify these statements so we can
1388
 * do more type checking.
1389
 */
1390
export class NewExpression extends Expression {
1✔
1391
    constructor(
1392
        readonly newKeyword: Token,
44✔
1393
        readonly call: CallExpression
44✔
1394
    ) {
1395
        super();
44✔
1396
        this.range = util.createBoundingRange(this.newKeyword, this.call);
44✔
1397
    }
1398

1399
    /**
1400
     * The name of the class to initialize (with optional namespace prefixed)
1401
     */
1402
    public get className() {
1403
        //the parser guarantees the callee of a new statement's call object will be
1404
        //a NamespacedVariableNameExpression
1405
        return this.call.callee as NamespacedVariableNameExpression;
105✔
1406
    }
1407

1408
    public readonly range: Range | undefined;
1409

1410
    public transpile(state: BrsTranspileState) {
1411
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
10✔
1412
        const cls = state.file.getClassFileLink(
10✔
1413
            this.className.getName(ParseMode.BrighterScript),
1414
            namespace?.getName(ParseMode.BrighterScript)
30✔
1415
        )?.item;
10✔
1416
        //new statements within a namespace block can omit the leading namespace if the class resides in that same namespace.
1417
        //So we need to figure out if this is a namespace-omitted class, or if this class exists without a namespace.
1418
        return this.call.transpile(state, cls?.getName(ParseMode.BrightScript));
10✔
1419
    }
1420

1421
    walk(visitor: WalkVisitor, options: WalkOptions) {
1422
        if (options.walkMode & InternalWalkMode.walkExpressions) {
185!
1423
            walk(this, 'call', visitor, options);
185✔
1424
        }
1425
    }
1426

1427
    public clone() {
1428
        return this.finalizeClone(
2✔
1429
            new NewExpression(
1430
                util.cloneToken(this.newKeyword),
1431
                this.call?.clone()
6✔
1432
            ),
1433
            ['call']
1434
        );
1435
    }
1436
}
1437

1438
export class CallfuncExpression extends Expression {
1✔
1439
    constructor(
1440
        readonly callee: Expression,
28✔
1441
        readonly operator: Token,
28✔
1442
        readonly methodName: Identifier,
28✔
1443
        readonly openingParen: Token,
28✔
1444
        readonly args: Expression[],
28✔
1445
        readonly closingParen: Token
28✔
1446
    ) {
1447
        super();
28✔
1448
        this.range = util.createBoundingRange(
28✔
1449
            callee,
1450
            operator,
1451
            methodName,
1452
            openingParen,
1453
            ...args ?? [],
84✔
1454
            closingParen
1455
        );
1456
    }
1457

1458
    public readonly range: Range | undefined;
1459

1460
    /**
1461
     * Get the name of the wrapping namespace (if it exists)
1462
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
1463
     */
1464
    public get namespaceName() {
1465
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
1466
    }
1467

1468
    public transpile(state: BrsTranspileState) {
1469
        let result = [] as TranspileResult;
8✔
1470
        result.push(
8✔
1471
            ...this.callee.transpile(state),
1472
            state.sourceNode(this.operator, '.callfunc'),
1473
            state.transpileToken(this.openingParen),
1474
            //the name of the function
1475
            state.sourceNode(this.methodName, ['"', this.methodName.text, '"']),
1476
            ', '
1477
        );
1478
        //transpile args
1479
        //callfunc with zero args never gets called, so pass invalid as the first parameter if there are no args
1480
        if (this.args.length === 0) {
8✔
1481
            result.push('invalid');
5✔
1482
        } else {
1483
            for (let i = 0; i < this.args.length; i++) {
3✔
1484
                //add comma between args
1485
                if (i > 0) {
6✔
1486
                    result.push(', ');
3✔
1487
                }
1488
                let arg = this.args[i];
6✔
1489
                result.push(...arg.transpile(state));
6✔
1490
            }
1491
        }
1492
        result.push(
8✔
1493
            state.transpileToken(this.closingParen)
1494
        );
1495
        return result;
8✔
1496
    }
1497

1498
    walk(visitor: WalkVisitor, options: WalkOptions) {
1499
        if (options.walkMode & InternalWalkMode.walkExpressions) {
65!
1500
            walk(this, 'callee', visitor, options);
65✔
1501
            walkArray(this.args, visitor, options, this);
65✔
1502
        }
1503
    }
1504

1505
    public clone() {
1506
        return this.finalizeClone(
3✔
1507
            new CallfuncExpression(
1508
                this.callee?.clone(),
9✔
1509
                util.cloneToken(this.operator),
1510
                util.cloneToken(this.methodName),
1511
                util.cloneToken(this.openingParen),
1512
                this.args?.map(e => e?.clone()),
2✔
1513
                util.cloneToken(this.closingParen)
1514
            ),
1515
            ['callee', 'args']
1516
        );
1517
    }
1518
}
1519

1520
/**
1521
 * Since template strings can contain newlines, we need to concatenate multiple strings together with chr() calls.
1522
 * This is a single expression that represents the string contatenation of all parts of a single quasi.
1523
 */
1524
export class TemplateStringQuasiExpression extends Expression {
1✔
1525
    constructor(
1526
        readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>
114✔
1527
    ) {
1528
        super();
114✔
1529
        this.range = util.createBoundingRange(
114✔
1530
            ...expressions ?? []
342✔
1531
        );
1532
    }
1533
    readonly range: Range | undefined;
1534

1535
    transpile(state: BrsTranspileState, skipEmptyStrings = true) {
41✔
1536
        let result = [] as TranspileResult;
49✔
1537
        let plus = '';
49✔
1538
        for (let expression of this.expressions) {
49✔
1539
            //skip empty strings
1540
            //TODO what does an empty string literal expression look like?
1541
            if (expression.token.text === '' && skipEmptyStrings === true) {
78✔
1542
                continue;
28✔
1543
            }
1544
            result.push(
50✔
1545
                plus,
1546
                ...expression.transpile(state)
1547
            );
1548
            plus = ' + ';
50✔
1549
        }
1550
        return result;
49✔
1551
    }
1552

1553
    walk(visitor: WalkVisitor, options: WalkOptions) {
1554
        if (options.walkMode & InternalWalkMode.walkExpressions) {
263!
1555
            walkArray(this.expressions, visitor, options, this);
263✔
1556
        }
1557
    }
1558

1559
    public clone() {
1560
        return this.finalizeClone(
15✔
1561
            new TemplateStringQuasiExpression(
1562
                this.expressions?.map(e => e?.clone())
20✔
1563
            ),
1564
            ['expressions']
1565
        );
1566
    }
1567
}
1568

1569
export class TemplateStringExpression extends Expression {
1✔
1570
    constructor(
1571
        readonly openingBacktick: Token,
53✔
1572
        readonly quasis: TemplateStringQuasiExpression[],
53✔
1573
        readonly expressions: Expression[],
53✔
1574
        readonly closingBacktick: Token
53✔
1575
    ) {
1576
        super();
53✔
1577
        this.range = util.createBoundingRange(
53✔
1578
            openingBacktick,
1579
            quasis?.[0],
159✔
1580
            quasis?.[quasis?.length - 1],
315!
1581
            closingBacktick
1582
        );
1583
    }
1584

1585
    public readonly range: Range | undefined;
1586

1587
    transpile(state: BrsTranspileState) {
1588
        //if this is essentially just a normal brightscript string but with backticks, transpile it as a normal string without parens
1589
        if (this.expressions.length === 0 && this.quasis.length === 1 && this.quasis[0].expressions.length === 1) {
24✔
1590
            return this.quasis[0].transpile(state);
6✔
1591
        }
1592
        let result = ['('];
18✔
1593
        let plus = '';
18✔
1594
        //helper function to figure out when to include the plus
1595
        function add(...items) {
1596
            if (items.length > 0) {
52✔
1597
                result.push(
40✔
1598
                    plus,
1599
                    ...items
1600
                );
1601
            }
1602
            //set the plus after the first occurance of a nonzero length set of items
1603
            if (plus === '' && items.length > 0) {
52✔
1604
                plus = ' + ';
18✔
1605
            }
1606
        }
1607

1608
        for (let i = 0; i < this.quasis.length; i++) {
18✔
1609
            let quasi = this.quasis[i];
35✔
1610
            let expression = this.expressions[i];
35✔
1611

1612
            add(
35✔
1613
                ...quasi.transpile(state)
1614
            );
1615
            if (expression) {
35✔
1616
                //skip the toString wrapper around certain expressions
1617
                if (
17✔
1618
                    isEscapedCharCodeLiteralExpression(expression) ||
39✔
1619
                    (isLiteralExpression(expression) && isStringType(expression.type))
1620
                ) {
1621
                    add(
3✔
1622
                        ...expression.transpile(state)
1623
                    );
1624

1625
                    //wrap all other expressions with a bslib_toString call to prevent runtime type mismatch errors
1626
                } else {
1627
                    add(
14✔
1628
                        state.bslibPrefix + '_toString(',
1629
                        ...expression.transpile(state),
1630
                        ')'
1631
                    );
1632
                }
1633
            }
1634
        }
1635
        //the expression should be wrapped in parens so it can be used line a single expression at runtime
1636
        result.push(')');
18✔
1637

1638
        return result;
18✔
1639
    }
1640

1641
    walk(visitor: WalkVisitor, options: WalkOptions) {
1642
        if (options.walkMode & InternalWalkMode.walkExpressions) {
129!
1643
            //walk the quasis and expressions in left-to-right order
1644
            for (let i = 0; i < this.quasis?.length; i++) {
129✔
1645
                walk(this.quasis, i, visitor, options, this);
221✔
1646

1647
                //this skips the final loop iteration since we'll always have one more quasi than expression
1648
                if (this.expressions[i]) {
221✔
1649
                    walk(this.expressions, i, visitor, options, this);
92✔
1650
                }
1651
            }
1652
        }
1653
    }
1654

1655
    public clone() {
1656
        return this.finalizeClone(
7✔
1657
            new TemplateStringExpression(
1658
                util.cloneToken(this.openingBacktick),
1659
                this.quasis?.map(e => e?.clone()),
12✔
1660
                this.expressions?.map(e => e?.clone()),
6✔
1661
                util.cloneToken(this.closingBacktick)
1662
            ),
1663
            ['quasis', 'expressions']
1664
        );
1665
    }
1666
}
1667

1668
export class TaggedTemplateStringExpression extends Expression {
1✔
1669
    constructor(
1670
        readonly tagName: Identifier,
12✔
1671
        readonly openingBacktick: Token,
12✔
1672
        readonly quasis: TemplateStringQuasiExpression[],
12✔
1673
        readonly expressions: Expression[],
12✔
1674
        readonly closingBacktick: Token
12✔
1675
    ) {
1676
        super();
12✔
1677
        this.range = util.createBoundingRange(
12✔
1678
            tagName,
1679
            openingBacktick,
1680
            quasis?.[0],
36✔
1681
            quasis?.[quasis?.length - 1],
69!
1682
            closingBacktick
1683
        );
1684
    }
1685

1686
    public readonly range: Range | undefined;
1687

1688
    transpile(state: BrsTranspileState) {
1689
        let result = [] as TranspileResult;
3✔
1690
        result.push(
3✔
1691
            state.transpileToken(this.tagName),
1692
            '(['
1693
        );
1694

1695
        //add quasis as the first array
1696
        for (let i = 0; i < this.quasis.length; i++) {
3✔
1697
            let quasi = this.quasis[i];
8✔
1698
            //separate items with a comma
1699
            if (i > 0) {
8✔
1700
                result.push(
5✔
1701
                    ', '
1702
                );
1703
            }
1704
            result.push(
8✔
1705
                ...quasi.transpile(state, false)
1706
            );
1707
        }
1708
        result.push(
3✔
1709
            '], ['
1710
        );
1711

1712
        //add expressions as the second array
1713
        for (let i = 0; i < this.expressions.length; i++) {
3✔
1714
            let expression = this.expressions[i];
5✔
1715
            if (i > 0) {
5✔
1716
                result.push(
2✔
1717
                    ', '
1718
                );
1719
            }
1720
            result.push(
5✔
1721
                ...expression.transpile(state)
1722
            );
1723
        }
1724
        result.push(
3✔
1725
            state.sourceNode(this.closingBacktick, '])')
1726
        );
1727
        return result;
3✔
1728
    }
1729

1730
    walk(visitor: WalkVisitor, options: WalkOptions) {
1731
        if (options.walkMode & InternalWalkMode.walkExpressions) {
21!
1732
            //walk the quasis and expressions in left-to-right order
1733
            for (let i = 0; i < this.quasis?.length; i++) {
21✔
1734
                walk(this.quasis, i, visitor, options, this);
48✔
1735

1736
                //this skips the final loop iteration since we'll always have one more quasi than expression
1737
                if (this.expressions[i]) {
48✔
1738
                    walk(this.expressions, i, visitor, options, this);
27✔
1739
                }
1740
            }
1741
        }
1742
    }
1743

1744
    public clone() {
1745
        return this.finalizeClone(
3✔
1746
            new TaggedTemplateStringExpression(
1747
                util.cloneToken(this.tagName),
1748
                util.cloneToken(this.openingBacktick),
1749
                this.quasis?.map(e => e?.clone()),
5✔
1750
                this.expressions?.map(e => e?.clone()),
3✔
1751
                util.cloneToken(this.closingBacktick)
1752
            ),
1753
            ['quasis', 'expressions']
1754
        );
1755
    }
1756
}
1757

1758
export class AnnotationExpression extends Expression {
1✔
1759
    constructor(
1760
        readonly atToken: Token,
70✔
1761
        readonly nameToken: Token
70✔
1762
    ) {
1763
        super();
70✔
1764
        this.name = nameToken.text;
70✔
1765
    }
1766

1767
    public get range() {
1768
        return util.createBoundingRange(
32✔
1769
            this.atToken,
1770
            this.nameToken,
1771
            this.call
1772
        );
1773
    }
1774

1775
    public name: string;
1776
    public call: CallExpression | undefined;
1777

1778
    /**
1779
     * Convert annotation arguments to JavaScript types
1780
     * @param strict If false, keep Expression objects not corresponding to JS types
1781
     */
1782
    getArguments(strict = true): ExpressionValue[] {
10✔
1783
        if (!this.call) {
11✔
1784
            return [];
1✔
1785
        }
1786
        return this.call.args.map(e => expressionToValue(e, strict));
20✔
1787
    }
1788

1789
    transpile(state: BrsTranspileState) {
1790
        return [];
3✔
1791
    }
1792

1793
    walk(visitor: WalkVisitor, options: WalkOptions) {
1794
        //nothing to walk
1795
    }
1796
    getTypedef(state: BrsTranspileState) {
1797
        return [
9✔
1798
            '@',
1799
            this.name,
1800
            ...(this.call?.transpile(state) ?? [])
54✔
1801
        ];
1802
    }
1803

1804
    public clone() {
1805
        const clone = this.finalizeClone(
8✔
1806
            new AnnotationExpression(
1807
                util.cloneToken(this.atToken),
1808
                util.cloneToken(this.nameToken)
1809
            )
1810
        );
1811
        return clone;
8✔
1812
    }
1813
}
1814

1815
export class TernaryExpression extends Expression {
1✔
1816
    constructor(
1817
        readonly test: Expression,
96✔
1818
        readonly questionMarkToken: Token,
96✔
1819
        readonly consequent?: Expression,
96✔
1820
        readonly colonToken?: Token,
96✔
1821
        readonly alternate?: Expression
96✔
1822
    ) {
1823
        super();
96✔
1824
        this.range = util.createBoundingRange(
96✔
1825
            test,
1826
            questionMarkToken,
1827
            consequent,
1828
            colonToken,
1829
            alternate
1830
        );
1831
    }
1832

1833
    public range: Range | undefined;
1834

1835
    transpile(state: BrsTranspileState) {
1836
        let result = [] as TranspileResult;
18✔
1837
        const file = state.file;
18✔
1838
        let consequentInfo = util.getExpressionInfo(this.consequent!, file);
18✔
1839
        let alternateInfo = util.getExpressionInfo(this.alternate!, file);
18✔
1840

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

1848
        let mutatingExpressions = [
18✔
1849
            ...consequentInfo.expressions,
1850
            ...alternateInfo.expressions
1851
        ].filter(e => e instanceof CallExpression || e instanceof CallfuncExpression || e instanceof DottedGetExpression);
120✔
1852

1853
        if (mutatingExpressions.length > 0) {
18✔
1854
            result.push(
10✔
1855
                state.sourceNode(
1856
                    this.questionMarkToken,
1857
                    //write all the scope variables as parameters.
1858
                    //TODO handle when there are more than 31 parameters
1859
                    `(function(${['__bsCondition', ...allUniqueVarNames].join(', ')})`
1860
                ),
1861
                state.newline,
1862
                //double indent so our `end function` line is still indented one at the end
1863
                state.indent(2),
1864
                state.sourceNode(this.test, `if __bsCondition then`),
1865
                state.newline,
1866
                state.indent(1),
1867
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
30!
1868
                ...this.consequent?.transpile(state) ?? [state.sourceNode(this.questionMarkToken, 'invalid')],
60!
1869
                state.newline,
1870
                state.indent(-1),
1871
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'else'),
30!
1872
                state.newline,
1873
                state.indent(1),
1874
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
30!
1875
                ...this.alternate?.transpile(state) ?? [state.sourceNode(this.consequent ?? this.questionMarkToken, 'invalid')],
60!
1876
                state.newline,
1877
                state.indent(-1),
1878
                state.sourceNode(this.questionMarkToken, 'end if'),
1879
                state.newline,
1880
                state.indent(-1),
1881
                state.sourceNode(this.questionMarkToken, 'end function)('),
1882
                ...this.test.transpile(state),
1883
                state.sourceNode(this.questionMarkToken, `${['', ...allUniqueVarNames].join(', ')})`)
1884
            );
1885
            state.blockDepth--;
10✔
1886
        } else {
1887
            result.push(
8✔
1888
                state.sourceNode(this.test, state.bslibPrefix + `_ternary(`),
1889
                ...this.test.transpile(state),
1890
                state.sourceNode(this.test, `, `),
1891
                ...this.consequent?.transpile(state) ?? ['invalid'],
48✔
1892
                `, `,
1893
                ...this.alternate?.transpile(state) ?? ['invalid'],
48✔
1894
                `)`
1895
            );
1896
        }
1897
        return result;
18✔
1898
    }
1899

1900
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1901
        if (options.walkMode & InternalWalkMode.walkExpressions) {
236!
1902
            walk(this, 'test', visitor, options);
236✔
1903
            walk(this, 'consequent', visitor, options);
236✔
1904
            walk(this, 'alternate', visitor, options);
236✔
1905
        }
1906
    }
1907

1908
    public clone() {
1909
        return this.finalizeClone(
2✔
1910
            new TernaryExpression(
1911
                this.test?.clone(),
6✔
1912
                util.cloneToken(this.questionMarkToken),
1913
                this.consequent?.clone(),
6✔
1914
                util.cloneToken(this.colonToken),
1915
                this.alternate?.clone()
6✔
1916
            ),
1917
            ['test', 'consequent', 'alternate']
1918
        );
1919
    }
1920
}
1921

1922
export class NullCoalescingExpression extends Expression {
1✔
1923
    constructor(
1924
        public consequent: Expression,
32✔
1925
        public questionQuestionToken: Token,
32✔
1926
        public alternate: Expression
32✔
1927
    ) {
1928
        super();
32✔
1929
        this.range = util.createBoundingRange(
32✔
1930
            consequent,
1931
            questionQuestionToken,
1932
            alternate
1933
        );
1934
    }
1935
    public readonly range: Range | undefined;
1936

1937
    transpile(state: BrsTranspileState) {
1938
        let result = [] as TranspileResult;
11✔
1939
        let consequentInfo = util.getExpressionInfo(this.consequent, state.file);
11✔
1940
        let alternateInfo = util.getExpressionInfo(this.alternate, state.file);
11✔
1941

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

1949
        let hasMutatingExpression = [
11✔
1950
            ...consequentInfo.expressions,
1951
            ...alternateInfo.expressions
1952
        ].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e));
30✔
1953

1954
        if (hasMutatingExpression) {
11✔
1955
            result.push(
7✔
1956
                `(function(`,
1957
                //write all the scope variables as parameters.
1958
                //TODO handle when there are more than 31 parameters
1959
                allUniqueVarNames.join(', '),
1960
                ')',
1961
                state.newline,
1962
                //double indent so our `end function` line is still indented one at the end
1963
                state.indent(2),
1964
                //evaluate the consequent exactly once, and then use it in the following condition
1965
                `__bsConsequent = `,
1966
                ...this.consequent.transpile(state),
1967
                state.newline,
1968
                state.indent(),
1969
                `if __bsConsequent <> invalid then`,
1970
                state.newline,
1971
                state.indent(1),
1972
                'return __bsConsequent',
1973
                state.newline,
1974
                state.indent(-1),
1975
                'else',
1976
                state.newline,
1977
                state.indent(1),
1978
                'return ',
1979
                ...this.alternate.transpile(state),
1980
                state.newline,
1981
                state.indent(-1),
1982
                'end if',
1983
                state.newline,
1984
                state.indent(-1),
1985
                'end function)(',
1986
                allUniqueVarNames.join(', '),
1987
                ')'
1988
            );
1989
            state.blockDepth--;
7✔
1990
        } else {
1991
            result.push(
4✔
1992
                state.bslibPrefix + `_coalesce(`,
1993
                ...this.consequent.transpile(state),
1994
                ', ',
1995
                ...this.alternate.transpile(state),
1996
                ')'
1997
            );
1998
        }
1999
        return result;
11✔
2000
    }
2001

2002
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2003
        if (options.walkMode & InternalWalkMode.walkExpressions) {
64!
2004
            walk(this, 'consequent', visitor, options);
64✔
2005
            walk(this, 'alternate', visitor, options);
64✔
2006
        }
2007
    }
2008

2009
    public clone() {
2010
        return this.finalizeClone(
2✔
2011
            new NullCoalescingExpression(
2012
                this.consequent?.clone(),
6✔
2013
                util.cloneToken(this.questionQuestionToken),
2014
                this.alternate?.clone()
6✔
2015
            ),
2016
            ['consequent', 'alternate']
2017
        );
2018
    }
2019
}
2020

2021
export class RegexLiteralExpression extends Expression {
1✔
2022
    public constructor(
2023
        public tokens: {
46✔
2024
            regexLiteral: Token;
2025
        }
2026
    ) {
2027
        super();
46✔
2028
    }
2029

2030
    public get range() {
2031
        return this.tokens?.regexLiteral?.range;
55!
2032
    }
2033

2034
    public transpile(state: BrsTranspileState): TranspileResult {
2035
        let text = this.tokens.regexLiteral?.text ?? '';
42!
2036
        let flags = '';
42✔
2037
        //get any flags from the end
2038
        const flagMatch = /\/([a-z]+)$/i.exec(text);
42✔
2039
        if (flagMatch) {
42✔
2040
            text = text.substring(0, flagMatch.index + 1);
2✔
2041
            flags = flagMatch[1];
2✔
2042
        }
2043
        let pattern = text
42✔
2044
            //remove leading and trailing slashes
2045
            .substring(1, text.length - 1)
2046
            //escape quotemarks
2047
            .split('"').join('" + chr(34) + "');
2048

2049
        return [
42✔
2050
            state.sourceNode(this.tokens.regexLiteral, [
2051
                'CreateObject("roRegex", ',
2052
                `"${pattern}", `,
2053
                `"${flags}"`,
2054
                ')'
2055
            ])
2056
        ];
2057
    }
2058

2059
    walk(visitor: WalkVisitor, options: WalkOptions) {
2060
        //nothing to walk
2061
    }
2062

2063
    public clone() {
2064
        return this.finalizeClone(
1✔
2065
            new RegexLiteralExpression({
2066
                regexLiteral: util.cloneToken(this.tokens.regexLiteral)
2067
            })
2068
        );
2069
    }
2070
}
2071

2072

2073
export class TypeCastExpression extends Expression {
1✔
2074
    constructor(
2075
        public obj: Expression,
19✔
2076
        public asToken: Token,
19✔
2077
        public typeToken: Token
19✔
2078
    ) {
2079
        super();
19✔
2080
        this.range = util.createBoundingRange(
19✔
2081
            this.obj,
2082
            this.asToken,
2083
            this.typeToken
2084
        );
2085
    }
2086

2087
    public range: Range;
2088

2089
    public transpile(state: BrsTranspileState): TranspileResult {
2090
        return this.obj.transpile(state);
11✔
2091
    }
2092
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2093
        if (options.walkMode & InternalWalkMode.walkExpressions) {
51!
2094
            walk(this, 'obj', visitor, options);
51✔
2095
        }
2096
    }
2097

2098
    public clone() {
2099
        return this.finalizeClone(
2✔
2100
            new TypeCastExpression(
2101
                this.obj?.clone(),
6✔
2102
                util.cloneToken(this.asToken),
2103
                util.cloneToken(this.typeToken)
2104
            ),
2105
            ['obj']
2106
        );
2107
    }
2108
}
2109

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

2113
function expressionToValue(expr: Expression, strict: boolean): ExpressionValue {
2114
    if (!expr) {
30!
2115
        return null;
×
2116
    }
2117
    if (isUnaryExpression(expr) && isLiteralNumber(expr.right)) {
30✔
2118
        return numberExpressionToValue(expr.right, expr.operator.text);
1✔
2119
    }
2120
    if (isLiteralString(expr)) {
29✔
2121
        //remove leading and trailing quotes
2122
        return expr.token.text.replace(/^"/, '').replace(/"$/, '');
5✔
2123
    }
2124
    if (isLiteralNumber(expr)) {
24✔
2125
        return numberExpressionToValue(expr);
11✔
2126
    }
2127

2128
    if (isLiteralBoolean(expr)) {
13✔
2129
        return expr.token.text.toLowerCase() === 'true';
3✔
2130
    }
2131
    if (isArrayLiteralExpression(expr)) {
10✔
2132
        return expr.elements
3✔
2133
            .filter(e => !isCommentStatement(e))
7✔
2134
            .map(e => expressionToValue(e, strict));
7✔
2135
    }
2136
    if (isAALiteralExpression(expr)) {
7✔
2137
        return expr.elements.reduce((acc, e) => {
3✔
2138
            if (!isCommentStatement(e) && !(isAAIndexedMemberExpression(e))) {
3!
2139
                acc[e.keyToken.text] = expressionToValue(e.value, strict);
3✔
2140
            }
2141
            return acc;
3✔
2142
        }, {});
2143
    }
2144
    //for annotations, we only support serializing pure string values
2145
    if (isTemplateStringExpression(expr)) {
4✔
2146
        if (expr.quasis?.length === 1 && expr.expressions.length === 0) {
2!
2147
            return expr.quasis[0].expressions.map(x => x.token.text).join('');
10✔
2148
        }
2149
    }
2150
    return strict ? null : expr;
2✔
2151
}
2152

2153
function numberExpressionToValue(expr: LiteralExpression, operator = '') {
11✔
2154
    if (isIntegerType(expr.type) || isLongIntegerType(expr.type)) {
12!
2155
        return parseInt(operator + expr.token.text);
12✔
2156
    } else {
2157
        return parseFloat(operator + expr.token.text);
×
2158
    }
2159
}
2160

2161
/**
2162
 * A list of names of functions that are restricted from being stored to a
2163
 * variable, property, or passed as an argument. (i.e. `type` or `createobject`).
2164
 * Names are stored in lower case.
2165
 */
2166
const nonReferenceableFunctions = [
1✔
2167
    'createobject',
2168
    'type',
2169
    'getglobalaa',
2170
    'box',
2171
    'run',
2172
    'eval',
2173
    'getlastruncompileerror',
2174
    'getlastrunruntimeerror',
2175
    'tab',
2176
    'pos'
2177
];
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