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

rokucommunity / brighterscript / #15673

29 Apr 2026 12:13AM UTC coverage: 88.992% (-0.02%) from 89.009%
#15673

push

web-flow
Merge 52394bb25 into f4cffdc58

8296 of 9822 branches covered (84.46%)

Branch coverage included in aggregate %.

64 of 69 new or added lines in 2 files covered. (92.75%)

10509 of 11309 relevant lines covered (92.93%)

2012.52 hits per line

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

92.2
/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,
383✔
29
        public operator: Token,
383✔
30
        public right: Expression
383✔
31
    ) {
32
        super();
383✔
33
        this.range = util.createBoundingRange(this.left, this.operator, this.right);
383✔
34
    }
35

36
    public readonly range: Range | undefined;
37

38
    transpile(state: BrsTranspileState) {
39
        return [
160✔
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) {
890!
50
            walk(this, 'left', visitor, options);
890✔
51
            walk(this, 'right', visitor, options);
890✔
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,
733✔
78
        /**
79
         * Can either be `(`, or `?(` for optional chaining
80
         */
81
        readonly openingParen: Token,
733✔
82
        readonly closingParen: Token,
733✔
83
        readonly args: Expression[],
733✔
84
        unused?: any
85
    ) {
86
        super();
733✔
87
        this.range = util.createBoundingRange(this.callee, this.openingParen, ...args ?? [], this.closingParen);
733✔
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 = [];
253✔
102

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

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

129
    walk(visitor: WalkVisitor, options: WalkOptions) {
130
        if (options.walkMode & InternalWalkMode.walkExpressions) {
2,203!
131
            walk(this, 'callee', visitor, options);
2,203✔
132
            walkArray(this.args, visitor, options, this);
2,203✔
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,116✔
152
        public body: Block,
2,116✔
153
        readonly functionType: Token | null,
2,116✔
154
        public end: Token,
2,116✔
155
        readonly leftParen: Token,
2,116✔
156
        readonly rightParen: Token,
2,116✔
157
        readonly asToken?: Token,
2,116✔
158
        readonly returnTypeToken?: Token
2,116✔
159
    ) {
160
        super();
2,116✔
161
        this.setReturnType(); // set the initial return type that we parse
2,116✔
162

163
        //if there's a body, and it doesn't have a SymbolTable, assign one
164
        if (this.body && !this.body.symbolTable) {
2,116✔
165
            this.body.symbolTable = new SymbolTable(`Function Body`);
147✔
166
        }
167
        this.symbolTable = new SymbolTable('FunctionExpression', () => this.parent?.getSymbolTable());
2,116!
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,201✔
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,116✔
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,492✔
229
            this.functionType, this.leftParen,
230
            ...this.parameters ?? [],
16,476✔
231
            this.rightParen,
232
            this.asToken,
233
            this.returnTypeToken,
234
            this.end
235
        );
236
    }
237

238
    transpile(state: BrsTranspileState, name?: Identifier, includeBody = true) {
393✔
239
        let results = [] as TranspileResult;
393✔
240
        //'function'|'sub'
241
        results.push(
393✔
242
            state.transpileToken(this.functionType!)
243
        );
244
        //functionName?
245
        if (name) {
393✔
246
            results.push(
388✔
247
                ' ',
248
                state.transpileToken(name)
249
            );
250
        }
251
        //leftParen
252
        results.push(
393✔
253
            state.transpileToken(this.leftParen)
254
        );
255
        //parameters
256
        for (let i = 0; i < this.parameters.length; i++) {
393✔
257
            let param = this.parameters[i];
129✔
258
            //add commas
259
            if (i > 0) {
129✔
260
                results.push(', ');
50✔
261
            }
262
            //add parameter
263
            results.push(param.transpile(state));
129✔
264
        }
265
        //right paren
266
        results.push(
393✔
267
            state.transpileToken(this.rightParen)
268
        );
269
        //as [Type]
270
        this.setReturnType(); // check one more time before transpile
393✔
271
        if (this.asToken && !(state.options.removeParameterTypes && !this.requiresReturnType)) {
393✔
272
            results.push(
41✔
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) {
393!
282
            state.lineage.unshift(this);
393✔
283
            let body = this.body.transpile(state);
393✔
284
            state.lineage.shift();
393✔
285
            results.push(...body);
393✔
286
        }
287
        results.push('\n');
393✔
288
        //'end sub'|'end function'
289
        results.push(
393✔
290
            state.indent(),
291
            state.transpileToken(this.end)
292
        );
293
        return results;
393✔
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) {
5,047!
331
            walkArray(this.parameters, visitor, options, this);
5,047✔
332

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

340
    getFunctionType(): FunctionType {
341
        let functionType = new FunctionType(this.returnType);
1,973✔
342
        functionType.isSub = this.functionType?.text === 'sub';
1,973!
343
        for (let param of this.parameters) {
1,973✔
344
            functionType.addParameter(param.name.text, param.type, !!param.typeToken);
687✔
345
        }
346
        return functionType;
1,973✔
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,509!
368

369
        if (this.returnTypeToken) {
2,509✔
370
            this.returnType = util.tokenToBscType(this.returnTypeToken);
173✔
371
        } else if (isSub) {
2,336✔
372
            this.returnType = new VoidType();
1,891✔
373
        } else {
374
            this.returnType = DynamicType.instance;
445✔
375
        }
376

377
        if ((isFunctionStatement(this.parent) || isMethodStatement(this.parent)) && this.parent?.name?.text.toLowerCase() === 'onkeyevent') {
2,509!
378
            // onKeyEvent() requires 'as Boolean' otherwise RokuOS throws errors
379
            this.requiresReturnType = true;
1✔
380
        } else if (isSub && !isVoidType(this.returnType)) { // format (6)
2,508✔
381
            this.requiresReturnType = true;
66✔
382
        } else if (this.returnTypeToken && isVoidType(this.returnType)) { // format (3)
2,442✔
383
            this.requiresReturnType = true;
18✔
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,
724✔
415
        public typeToken?: Token,
724✔
416
        public defaultValue?: Expression,
724✔
417
        public asToken?: Token
724✔
418
    ) {
419
        super();
724✔
420
        if (typeToken) {
724✔
421
            this.type = util.tokenToBscType(typeToken);
341✔
422
        } else {
423
            this.type = new DynamicType();
383✔
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 = [
146✔
440
            //name
441
            state.transpileToken(this.name)
442
        ] as any[];
443
        //default value
444
        if (this.defaultValue) {
146✔
445
            result.push(' = ');
9✔
446
            result.push(this.defaultValue.transpile(state));
9✔
447
        }
448
        //type declaration
449
        if (this.asToken && !state.options.removeParameterTypes) {
146✔
450
            result.push(' ');
93✔
451
            result.push(state.transpileToken(this.asToken));
93✔
452
            result.push(' ');
93✔
453
            result.push(state.sourceNode(this.typeToken!, this.type.toTypeString()));
93✔
454
        }
455

456
        return result;
146✔
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,190✔
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
527✔
501
    ) {
502
        super();
527✔
503
        this.range = expression?.range;
527✔
504
    }
505
    range: Range | undefined;
506

507
    //memoized name results. The AST is treated as readonly post-parse, so once these
508
    //are computed they remain valid for the lifetime of the expression. Validators
509
    //call `getName` per potential namespace reference per scope, often dozens of
510
    //times against the same expression — memoization here was the single biggest
511
    //wall-time win surfaced by Phase 6 CPU profiling.
512
    private _cachedNameParts: string[] | undefined;
513
    private _cachedNameBrighterScript: string | undefined;
514
    private _cachedNameBrightScript: string | undefined;
515

516
    transpile(state: BrsTranspileState) {
517
        return [
5✔
518
            state.sourceNode(this, this.getName(ParseMode.BrightScript))
519
        ];
520
    }
521

522
    getTypedef(state) {
523
        return [
×
524
            state.sourceNode(this, this.getName(ParseMode.BrighterScript))
525
        ];
526
    }
527

528
    public getNameParts() {
529
        //return a fresh copy so callers can mutate (some use `.pop()` / `.shift()`)
530
        //without disturbing the cache
531
        return this.computeNameParts().slice();
48✔
532
    }
533

534
    private computeNameParts(): string[] {
535
        let parts = this._cachedNameParts;
643✔
536
        if (parts) {
643✔
537
            return parts;
140✔
538
        }
539
        parts = [];
503✔
540
        if (isVariableExpression(this.expression)) {
503✔
541
            parts.push(this.expression.name.text);
366✔
542
        } else {
543
            let expr = this.expression;
137✔
544

545
            parts.push(expr.name.text);
137✔
546

547
            while (isVariableExpression(expr) === false) {
137✔
548
                expr = expr.obj as DottedGetExpression;
170✔
549
                parts.unshift(expr.name.text);
170✔
550
            }
551
        }
552
        this._cachedNameParts = parts;
503✔
553
        return parts;
503✔
554
    }
555

556
    getName(parseMode: ParseMode) {
557
        if (parseMode === ParseMode.BrighterScript) {
1,350✔
558
            if (this._cachedNameBrighterScript !== undefined) {
1,254✔
559
                return this._cachedNameBrighterScript;
755✔
560
            }
561
            this._cachedNameBrighterScript = this.computeNameParts().join('.');
499✔
562
            return this._cachedNameBrighterScript;
499✔
563
        }
564
        if (this._cachedNameBrightScript !== undefined) {
96!
NEW
565
            return this._cachedNameBrightScript;
×
566
        }
567
        this._cachedNameBrightScript = this.computeNameParts().join('_');
96✔
568
        return this._cachedNameBrightScript;
96✔
569
    }
570

571
    walk(visitor: WalkVisitor, options: WalkOptions) {
572
        this.expression?.link();
1,229!
573
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,229✔
574
            walk(this, 'expression', visitor, options);
1,205✔
575
        }
576
    }
577

578
    public clone() {
579
        return this.finalizeClone(
6✔
580
            new NamespacedVariableNameExpression(
581
                this.expression?.clone()
18✔
582
            )
583
        );
584
    }
585
}
586

587
export class DottedGetExpression extends Expression {
1✔
588
    constructor(
589
        readonly obj: Expression,
1,218✔
590
        readonly name: Identifier,
1,218✔
591
        /**
592
         * Can either be `.`, or `?.` for optional chaining
593
         */
594
        readonly dot: Token
1,218✔
595
    ) {
596
        super();
1,218✔
597
        this.range = util.createBoundingRange(this.obj, this.dot, this.name);
1,218✔
598
    }
599

600
    public readonly range: Range | undefined;
601

602
    transpile(state: BrsTranspileState) {
603
        //if the callee starts with a namespace name, transpile the name
604
        if (state.file.calleeStartsWithNamespace(this)) {
246✔
605
            return new NamespacedVariableNameExpression(this as DottedGetExpression | VariableExpression).transpile(state);
4✔
606
        } else {
607
            return [
242✔
608
                ...this.obj.transpile(state),
609
                state.transpileToken(this.dot),
610
                state.transpileToken(this.name)
611
            ];
612
        }
613
    }
614

615
    getTypedef(state: BrsTranspileState) {
616
        //always transpile the dots for typedefs
617
        return [
4✔
618
            ...(this.obj.getTypedef ? this.obj.getTypedef(state) : this.obj.transpile(state)),
4!
619
            state.transpileToken(this.dot),
620
            state.transpileToken(this.name)
621
        ];
622
    }
623

624
    walk(visitor: WalkVisitor, options: WalkOptions) {
625
        if (options.walkMode & InternalWalkMode.walkExpressions) {
3,261!
626
            walk(this, 'obj', visitor, options);
3,261✔
627
        }
628
    }
629

630
    public clone() {
631
        return this.finalizeClone(
8✔
632
            new DottedGetExpression(
633
                this.obj?.clone(),
24✔
634
                util.cloneToken(this.name),
635
                util.cloneToken(this.dot)
636
            ),
637
            ['obj']
638
        );
639
    }
640
}
641

642
export class XmlAttributeGetExpression extends Expression {
1✔
643
    constructor(
644
        readonly obj: Expression,
15✔
645
        readonly name: Identifier,
15✔
646
        /**
647
         * Can either be `@`, or `?@` for optional chaining
648
         */
649
        readonly at: Token
15✔
650
    ) {
651
        super();
15✔
652
        this.range = util.createBoundingRange(this.obj, this.at, this.name);
15✔
653
    }
654

655
    public readonly range: Range | undefined;
656

657
    transpile(state: BrsTranspileState) {
658
        return [
3✔
659
            ...this.obj.transpile(state),
660
            state.transpileToken(this.at),
661
            state.transpileToken(this.name)
662
        ];
663
    }
664

665
    walk(visitor: WalkVisitor, options: WalkOptions) {
666
        if (options.walkMode & InternalWalkMode.walkExpressions) {
26!
667
            walk(this, 'obj', visitor, options);
26✔
668
        }
669
    }
670

671
    public clone() {
672
        return this.finalizeClone(
2✔
673
            new XmlAttributeGetExpression(
674
                this.obj?.clone(),
6✔
675
                util.cloneToken(this.name),
676
                util.cloneToken(this.at)
677
            ),
678
            ['obj']
679
        );
680
    }
681
}
682

683
export class IndexedGetExpression extends Expression {
1✔
684
    constructor(
685
        public obj: Expression,
154✔
686
        public index: Expression,
154✔
687
        /**
688
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.`
689
         */
690
        public openingSquare: Token,
154✔
691
        public closingSquare: Token,
154✔
692
        public questionDotToken?: Token, //  ? or ?.
154✔
693
        /**
694
         * More indexes, separated by commas
695
         */
696
        public additionalIndexes?: Expression[]
154✔
697
    ) {
698
        super();
154✔
699
        this.range = util.createBoundingRange(this.obj, this.openingSquare, this.questionDotToken, this.openingSquare, this.index, this.closingSquare);
154✔
700
        this.additionalIndexes ??= [];
154✔
701
    }
702

703
    public readonly range: Range | undefined;
704

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

729
    walk(visitor: WalkVisitor, options: WalkOptions) {
730
        if (options.walkMode & InternalWalkMode.walkExpressions) {
345!
731
            walk(this, 'obj', visitor, options);
345✔
732
            walk(this, 'index', visitor, options);
345✔
733
            walkArray(this.additionalIndexes, visitor, options, this);
345✔
734
        }
735
    }
736

737
    public clone() {
738
        return this.finalizeClone(
5✔
739
            new IndexedGetExpression(
740
                this.obj?.clone(),
15✔
741
                this.index?.clone(),
15✔
742
                util.cloneToken(this.openingSquare),
743
                util.cloneToken(this.closingSquare),
744
                util.cloneToken(this.questionDotToken),
745
                this.additionalIndexes?.map(e => e?.clone())
2✔
746
            ),
747
            ['obj', 'index', 'additionalIndexes']
748
        );
749
    }
750
}
751

752
export class GroupingExpression extends Expression {
1✔
753
    constructor(
754
        readonly tokens: {
40✔
755
            left: Token;
756
            right: Token;
757
        },
758
        public expression: Expression
40✔
759
    ) {
760
        super();
40✔
761
        this.range = util.createBoundingRange(this.tokens.left, this.expression, this.tokens.right);
40✔
762
    }
763

764
    public readonly range: Range | undefined;
765

766
    transpile(state: BrsTranspileState) {
767
        if (isTypeCastExpression(this.expression)) {
12✔
768
            return this.expression.transpile(state);
7✔
769
        }
770
        return [
5✔
771
            state.transpileToken(this.tokens.left),
772
            ...this.expression.transpile(state),
773
            state.transpileToken(this.tokens.right)
774
        ];
775
    }
776

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

783
    public clone() {
784
        return this.finalizeClone(
2✔
785
            new GroupingExpression(
786
                {
787
                    left: util.cloneToken(this.tokens.left),
788
                    right: util.cloneToken(this.tokens.right)
789
                },
790
                this.expression?.clone()
6✔
791
            ),
792
            ['expression']
793
        );
794
    }
795
}
796

797
export class LiteralExpression extends Expression {
1✔
798
    constructor(
799
        public token: Token
3,582✔
800
    ) {
801
        super();
3,582✔
802
        this.type = util.tokenToBscType(token);
3,582✔
803
    }
804

805
    public get range() {
806
        return this.token.range;
8,217✔
807
    }
808

809
    /**
810
     * The (data) type of this expression
811
     */
812
    public type: BscType;
813

814
    transpile(state: BrsTranspileState) {
815
        let text: string;
816
        if (this.token.kind === TokenKind.TemplateStringQuasi) {
893✔
817
            //wrap quasis with quotes (and escape inner quotemarks)
818
            text = `"${this.token.text.replace(/"/g, '""')}"`;
35✔
819

820
        } else if (isStringType(this.type)) {
858✔
821
            text = this.token.text;
358✔
822
            //add trailing quotemark if it's missing. We will have already generated a diagnostic for this.
823
            if (text.endsWith('"') === false) {
358✔
824
                text += '"';
1✔
825
            }
826
        } else {
827
            text = this.token.text;
500✔
828
        }
829

830
        return [
893✔
831
            state.sourceNode(this, text)
832
        ];
833
    }
834

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

839
    public clone() {
840
        return this.finalizeClone(
101✔
841
            new LiteralExpression(
842
                util.cloneToken(this.token)
843
            )
844
        );
845
    }
846
}
847

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

861
    transpile(state: BrsTranspileState) {
862
        return [
15✔
863
            state.sourceNode(this, `chr(${this.token.charCode})`)
864
        ];
865
    }
866

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

871
    public clone() {
872
        return this.finalizeClone(
3✔
873
            new EscapedCharCodeLiteralExpression(
874
                util.cloneToken(this.token)
875
            )
876
        );
877
    }
878
}
879

880
export class ArrayLiteralExpression extends Expression {
1✔
881
    constructor(
882
        readonly elements: Array<Expression | CommentStatement>,
135✔
883
        readonly open: Token,
135✔
884
        readonly close: Token,
135✔
885
        readonly hasSpread = false
135✔
886
    ) {
887
        super();
135✔
888
        this.range = util.createBoundingRange(this.open, ...this.elements ?? [], this.close);
135✔
889
    }
890

891
    public readonly range: Range | undefined;
892

893
    transpile(state: BrsTranspileState) {
894
        let result = [] as TranspileResult;
60✔
895
        result.push(
60✔
896
            state.transpileToken(this.open)
897
        );
898
        let hasChildren = this.elements.length > 0;
60✔
899
        state.blockDepth++;
60✔
900

901
        for (let i = 0; i < this.elements.length; i++) {
60✔
902
            let previousElement = this.elements[i - 1];
92✔
903
            let element = this.elements[i];
92✔
904

905
            if (isCommentStatement(element)) {
92✔
906
                //if the comment is on the same line as opening square or previous statement, don't add newline
907
                if (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element)) {
7✔
908
                    result.push(' ');
4✔
909
                } else {
910
                    result.push(
3✔
911
                        '\n',
912
                        state.indent()
913
                    );
914
                }
915
                state.lineage.unshift(this);
7✔
916
                result.push(element.transpile(state));
7✔
917
                state.lineage.shift();
7✔
918
            } else {
919
                result.push('\n');
85✔
920

921
                result.push(
85✔
922
                    state.indent(),
923
                    ...element.transpile(state)
924
                );
925
            }
926
        }
927
        state.blockDepth--;
60✔
928
        //add a newline between open and close if there are elements
929
        if (hasChildren) {
60✔
930
            result.push('\n');
38✔
931
            result.push(state.indent());
38✔
932
        }
933
        if (this.close) {
60!
934
            result.push(
60✔
935
                state.transpileToken(this.close)
936
            );
937
        }
938
        return result;
60✔
939
    }
940

941
    walk(visitor: WalkVisitor, options: WalkOptions) {
942
        if (options.walkMode & InternalWalkMode.walkExpressions) {
403!
943
            walkArray(this.elements, visitor, options, this);
403✔
944
        }
945
    }
946

947
    public clone() {
948
        return this.finalizeClone(
4✔
949
            new ArrayLiteralExpression(
950
                this.elements?.map(e => e?.clone()),
6✔
951
                util.cloneToken(this.open),
952
                util.cloneToken(this.close),
953
                this.hasSpread
954
            ),
955
            ['elements']
956
        );
957
    }
958
}
959

960
export class AAMemberExpression extends Expression {
1✔
961
    constructor(
962
        public keyToken: Token,
240✔
963
        public colonToken: Token,
240✔
964
        /** The expression evaluated to determine the member's initial value. */
965
        public value: Expression
240✔
966
    ) {
967
        super();
240✔
968
        this.range = util.createBoundingRange(this.keyToken, this.colonToken, this.value);
240✔
969
    }
970

971
    public range: Range | undefined;
972
    public commaToken?: Token;
973

974
    transpile(state: BrsTranspileState) {
975
        return [];
×
976
    }
977

978
    walk(visitor: WalkVisitor, options: WalkOptions) {
979
        walk(this, 'value', visitor, options);
537✔
980
    }
981

982
    public clone() {
983
        return this.finalizeClone(
4✔
984
            new AAMemberExpression(
985
                util.cloneToken(this.keyToken),
986
                util.cloneToken(this.colonToken),
987
                this.value?.clone()
12✔
988
            ),
989
            ['value']
990
        );
991
    }
992
}
993

994
export class AAIndexedMemberExpression extends Expression {
1✔
995
    constructor(options: {
996
        leftBracket: Token;
997
        key: Expression;
998
        rightBracket: Token;
999
        colon: Token;
1000
        /** The expression evaluated to determine the member's initial value. */
1001
        value: Expression;
1002
    }) {
1003
        super();
33✔
1004
        this.key = options.key;
33✔
1005
        this.tokens = {
33✔
1006
            leftBracket: options.leftBracket,
1007
            rightBracket: options.rightBracket,
1008
            colon: options.colon
1009
        };
1010
        this.value = options.value;
33✔
1011
        this.range = util.createBoundingRange(this.tokens.leftBracket, this.key, this.tokens.rightBracket, this.tokens.colon, this.value);
33✔
1012
    }
1013

1014
    public readonly tokens: {
1015
        readonly leftBracket: Token;
1016
        readonly rightBracket: Token;
1017
        readonly colon: Token;
1018
    };
1019

1020
    public key: Expression;
1021
    /** The expression evaluated to determine the member's initial value. */
1022
    public value: Expression;
1023

1024
    public range: Range | undefined;
1025
    public commaToken?: Token;
1026

1027
    transpile(state: BrsTranspileState) {
1028
        return [];
×
1029
    }
1030

1031
    walk(visitor: WalkVisitor, options: WalkOptions) {
1032
        walk(this, 'key', visitor, options);
91✔
1033
        walk(this, 'value', visitor, options);
91✔
1034
    }
1035

1036
    public clone() {
1037
        return this.finalizeClone(
2✔
1038
            new AAIndexedMemberExpression({
1039
                leftBracket: util.cloneToken(this.tokens.leftBracket),
1040
                key: this.key?.clone(),
6!
1041
                rightBracket: util.cloneToken(this.tokens.rightBracket),
1042
                colon: util.cloneToken(this.tokens.colon),
1043
                value: this.value?.clone()
6✔
1044
            }),
1045
            ['key', 'value']
1046
        );
1047
    }
1048
}
1049

1050
export class AALiteralExpression extends Expression {
1✔
1051
    constructor(
1052
        readonly elements: Array<AAMemberExpression | AAIndexedMemberExpression | CommentStatement>,
263✔
1053
        readonly open: Token,
263✔
1054
        readonly close: Token
263✔
1055
    ) {
1056
        super();
263✔
1057
        this.range = util.createBoundingRange(this.open, ...this.elements ?? [], this.close);
263✔
1058
    }
1059

1060
    public readonly range: Range | undefined;
1061

1062
    transpile(state: BrsTranspileState) {
1063
        let result = [] as TranspileResult;
79✔
1064
        //open curly
1065
        result.push(
79✔
1066
            state.transpileToken(this.open)
1067
        );
1068
        let hasChildren = this.elements.length > 0;
79✔
1069
        //add newline if the object has children and the first child isn't a comment starting on the same line as opening curly
1070
        if (hasChildren && (isCommentStatement(this.elements[0]) === false || !util.linesTouch(this.elements[0], this.open))) {
79✔
1071
            result.push('\n');
47✔
1072
        }
1073
        state.blockDepth++;
79✔
1074
        for (let i = 0; i < this.elements.length; i++) {
79✔
1075
            let element = this.elements[i];
84✔
1076
            let previousElement = this.elements[i - 1];
84✔
1077
            let nextElement = this.elements[i + 1];
84✔
1078

1079
            //don't indent if comment is same-line
1080
            if (isCommentStatement(element as any) &&
84✔
1081
                (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element))
1082
            ) {
1083
                result.push(' ');
10✔
1084

1085
                //indent line
1086
            } else {
1087
                result.push(state.indent());
74✔
1088
            }
1089

1090
            //render comments
1091
            if (isCommentStatement(element)) {
84✔
1092
                result.push(...element.transpile(state));
14✔
1093
            } else {
1094
                //key
1095
                if ('tokens' in element) {
70✔
1096
                    //computed key: transpile the resolved expression (pre-transpile overrides it to a literal)
1097
                    result.push(...element.key.transpile(state));
10✔
1098
                } else {
1099
                    result.push(
60✔
1100
                        state.transpileToken(element.keyToken)
1101
                    );
1102
                }
1103
                //colon
1104
                result.push(
70✔
1105
                    state.transpileToken('tokens' in element ? element.tokens.colon : element.colonToken),
70✔
1106
                    ' '
1107
                );
1108

1109
                //value
1110
                result.push(...element.value.transpile(state));
70✔
1111
            }
1112

1113

1114
            //if next element is a same-line comment, skip the newline
1115
            if (nextElement && isCommentStatement(nextElement) && nextElement.range?.start.line === element.range?.start.line) {
84!
1116

1117
                //add a newline between statements
1118
            } else {
1119
                result.push('\n');
76✔
1120
            }
1121
        }
1122
        state.blockDepth--;
79✔
1123

1124
        //only indent the closing curly if we have children
1125
        if (hasChildren) {
79✔
1126
            result.push(state.indent());
49✔
1127
        }
1128
        //close curly
1129
        if (this.close) {
79!
1130
            result.push(
79✔
1131
                state.transpileToken(this.close)
1132
            );
1133
        }
1134
        return result;
79✔
1135
    }
1136

1137
    walk(visitor: WalkVisitor, options: WalkOptions) {
1138
        if (options.walkMode & InternalWalkMode.walkExpressions) {
621!
1139
            walkArray(this.elements, visitor, options, this);
621✔
1140
        }
1141
    }
1142

1143
    public clone() {
1144
        return this.finalizeClone(
8✔
1145
            new AALiteralExpression(
1146
                this.elements?.map(e => e?.clone()),
7✔
1147
                util.cloneToken(this.open),
1148
                util.cloneToken(this.close)
1149
            ),
1150
            ['elements']
1151
        );
1152
    }
1153
}
1154

1155
export class UnaryExpression extends Expression {
1✔
1156
    constructor(
1157
        public operator: Token,
41✔
1158
        public right: Expression
41✔
1159
    ) {
1160
        super();
41✔
1161
        this.range = util.createBoundingRange(this.operator, this.right);
41✔
1162
    }
1163

1164
    public readonly range: Range | undefined;
1165

1166
    transpile(state: BrsTranspileState) {
1167
        let separatingWhitespace: string | undefined;
1168
        if (isVariableExpression(this.right)) {
14✔
1169
            separatingWhitespace = this.right.name.leadingWhitespace;
6✔
1170
        } else if (isLiteralExpression(this.right)) {
8✔
1171
            separatingWhitespace = this.right.token.leadingWhitespace;
3✔
1172
        }
1173

1174
        return [
14✔
1175
            state.transpileToken(this.operator),
1176
            separatingWhitespace ?? ' ',
42✔
1177
            ...this.right.transpile(state)
1178
        ];
1179
    }
1180

1181
    walk(visitor: WalkVisitor, options: WalkOptions) {
1182
        if (options.walkMode & InternalWalkMode.walkExpressions) {
91!
1183
            walk(this, 'right', visitor, options);
91✔
1184
        }
1185
    }
1186

1187
    public clone() {
1188
        return this.finalizeClone(
2✔
1189
            new UnaryExpression(
1190
                util.cloneToken(this.operator),
1191
                this.right?.clone()
6✔
1192
            ),
1193
            ['right']
1194
        );
1195
    }
1196
}
1197

1198
export class VariableExpression extends Expression {
1✔
1199
    constructor(
1200
        readonly name: Identifier
2,371✔
1201
    ) {
1202
        super();
2,371✔
1203
        this.range = this.name?.range;
2,371!
1204
    }
1205

1206
    public readonly range: Range;
1207

1208
    public getName(parseMode: ParseMode) {
1209
        return this.name.text;
32✔
1210
    }
1211

1212
    transpile(state: BrsTranspileState) {
1213
        let result = [] as TranspileResult;
432✔
1214
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
432✔
1215
        //if the callee is the name of a known namespace function
1216
        if (namespace && state.file.calleeIsKnownNamespaceFunction(this, namespace.getName(ParseMode.BrighterScript))) {
432✔
1217
            result.push(
5✔
1218
                state.sourceNode(this, [
1219
                    namespace.getName(ParseMode.BrightScript),
1220
                    '_',
1221
                    this.getName(ParseMode.BrightScript)
1222
                ])
1223
            );
1224

1225
            //transpile  normally
1226
        } else {
1227
            result.push(
427✔
1228
                state.transpileToken(this.name)
1229
            );
1230
        }
1231
        return result;
432✔
1232
    }
1233

1234
    getTypedef(state: BrsTranspileState) {
1235
        return [
4✔
1236
            state.transpileToken(this.name)
1237
        ];
1238
    }
1239

1240
    walk(visitor: WalkVisitor, options: WalkOptions) {
1241
        //nothing to walk
1242
    }
1243

1244
    public clone() {
1245
        return this.finalizeClone(
42✔
1246
            new VariableExpression(
1247
                util.cloneToken(this.name)
1248
            )
1249
        );
1250
    }
1251
}
1252

1253
export class SourceLiteralExpression extends Expression {
1✔
1254
    constructor(
1255
        readonly token: Token
37✔
1256
    ) {
1257
        super();
37✔
1258
        this.range = token?.range;
37!
1259
    }
1260

1261
    public readonly range: Range;
1262

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

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

1292
    transpile(state: BrsTranspileState) {
1293
        let text: string;
1294
        switch (this.token.kind) {
31✔
1295
            case TokenKind.SourceFilePathLiteral:
40!
1296
                const pathUrl = fileUrl(state.srcPath);
3✔
1297
                text = `"${pathUrl.substring(0, 4)}" + "${pathUrl.substring(4)}"`;
3✔
1298
                break;
3✔
1299
            case TokenKind.SourceLineNumLiteral:
1300
                //TODO find first parent that has range, or default to -1
1301
                text = `${this.getClosestLineNumber()}`;
4✔
1302
                break;
4✔
1303
            case TokenKind.FunctionNameLiteral:
1304
                text = `"${this.getFunctionName(state, ParseMode.BrightScript)}"`;
4✔
1305
                break;
4✔
1306
            case TokenKind.SourceFunctionNameLiteral:
1307
                text = `"${this.getFunctionName(state, ParseMode.BrighterScript)}"`;
4✔
1308
                break;
4✔
1309
            case TokenKind.SourceNamespaceNameLiteral:
1310
                let namespaceParts = this.getFunctionName(state, ParseMode.BrighterScript).split('.');
×
1311
                namespaceParts.pop(); // remove the function name
×
1312

1313
                text = `"${namespaceParts.join('.')}"`;
×
1314
                break;
×
1315
            case TokenKind.SourceNamespaceRootNameLiteral:
1316
                let namespaceRootParts = this.getFunctionName(state, ParseMode.BrighterScript).split('.');
×
1317
                namespaceRootParts.pop(); // remove the function name
×
1318

1319
                let rootNamespace = namespaceRootParts.shift() ?? '';
×
1320
                text = `"${rootNamespace}"`;
×
1321
                break;
×
1322
            case TokenKind.SourceLocationLiteral:
1323
                const locationUrl = fileUrl(state.srcPath);
3✔
1324
                //TODO find first parent that has range, or default to -1
1325
                text = `"${locationUrl.substring(0, 4)}" + "${locationUrl.substring(4)}:${this.getClosestLineNumber()}"`;
3✔
1326
                break;
3✔
1327
            case TokenKind.PkgPathLiteral:
1328
                let pkgPath1 = `pkg:/${state.file.pkgPath}`
2✔
1329
                    .replace(/\\/g, '/')
1330
                    .replace(/\.bs$/i, '.brs');
1331

1332
                text = `"${pkgPath1}"`;
2✔
1333
                break;
2✔
1334
            case TokenKind.PkgLocationLiteral:
1335
                let pkgPath2 = `pkg:/${state.file.pkgPath}`
2✔
1336
                    .replace(/\\/g, '/')
1337
                    .replace(/\.bs$/i, '.brs');
1338

1339
                text = `"${pkgPath2}:" + str(LINE_NUM)`;
2✔
1340
                break;
2✔
1341
            case TokenKind.LineNumLiteral:
1342
            default:
1343
                //use the original text (because it looks like a variable)
1344
                text = this.token.text;
9✔
1345
                break;
9✔
1346

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

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

1357
    public clone() {
1358
        return this.finalizeClone(
1✔
1359
            new SourceLiteralExpression(
1360
                util.cloneToken(this.token)
1361
            )
1362
        );
1363
    }
1364
}
1365

1366
/**
1367
 * This expression transpiles and acts exactly like a CallExpression,
1368
 * except we need to uniquely identify these statements so we can
1369
 * do more type checking.
1370
 */
1371
export class NewExpression extends Expression {
1✔
1372
    constructor(
1373
        readonly newKeyword: Token,
44✔
1374
        readonly call: CallExpression
44✔
1375
    ) {
1376
        super();
44✔
1377
        this.range = util.createBoundingRange(this.newKeyword, this.call);
44✔
1378
    }
1379

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

1389
    public readonly range: Range | undefined;
1390

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

1402
    walk(visitor: WalkVisitor, options: WalkOptions) {
1403
        if (options.walkMode & InternalWalkMode.walkExpressions) {
185!
1404
            walk(this, 'call', visitor, options);
185✔
1405
        }
1406
    }
1407

1408
    public clone() {
1409
        return this.finalizeClone(
2✔
1410
            new NewExpression(
1411
                util.cloneToken(this.newKeyword),
1412
                this.call?.clone()
6✔
1413
            ),
1414
            ['call']
1415
        );
1416
    }
1417
}
1418

1419
export class CallfuncExpression extends Expression {
1✔
1420
    constructor(
1421
        readonly callee: Expression,
28✔
1422
        readonly operator: Token,
28✔
1423
        readonly methodName: Identifier,
28✔
1424
        readonly openingParen: Token,
28✔
1425
        readonly args: Expression[],
28✔
1426
        readonly closingParen: Token
28✔
1427
    ) {
1428
        super();
28✔
1429
        this.range = util.createBoundingRange(
28✔
1430
            callee,
1431
            operator,
1432
            methodName,
1433
            openingParen,
1434
            ...args ?? [],
84✔
1435
            closingParen
1436
        );
1437
    }
1438

1439
    public readonly range: Range | undefined;
1440

1441
    /**
1442
     * Get the name of the wrapping namespace (if it exists)
1443
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
1444
     */
1445
    public get namespaceName() {
1446
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
1447
    }
1448

1449
    public transpile(state: BrsTranspileState) {
1450
        let result = [] as TranspileResult;
8✔
1451
        result.push(
8✔
1452
            ...this.callee.transpile(state),
1453
            state.sourceNode(this.operator, '.callfunc'),
1454
            state.transpileToken(this.openingParen),
1455
            //the name of the function
1456
            state.sourceNode(this.methodName, ['"', this.methodName.text, '"']),
1457
            ', '
1458
        );
1459
        //transpile args
1460
        //callfunc with zero args never gets called, so pass invalid as the first parameter if there are no args
1461
        if (this.args.length === 0) {
8✔
1462
            result.push('invalid');
5✔
1463
        } else {
1464
            for (let i = 0; i < this.args.length; i++) {
3✔
1465
                //add comma between args
1466
                if (i > 0) {
6✔
1467
                    result.push(', ');
3✔
1468
                }
1469
                let arg = this.args[i];
6✔
1470
                result.push(...arg.transpile(state));
6✔
1471
            }
1472
        }
1473
        result.push(
8✔
1474
            state.transpileToken(this.closingParen)
1475
        );
1476
        return result;
8✔
1477
    }
1478

1479
    walk(visitor: WalkVisitor, options: WalkOptions) {
1480
        if (options.walkMode & InternalWalkMode.walkExpressions) {
65!
1481
            walk(this, 'callee', visitor, options);
65✔
1482
            walkArray(this.args, visitor, options, this);
65✔
1483
        }
1484
    }
1485

1486
    public clone() {
1487
        return this.finalizeClone(
3✔
1488
            new CallfuncExpression(
1489
                this.callee?.clone(),
9✔
1490
                util.cloneToken(this.operator),
1491
                util.cloneToken(this.methodName),
1492
                util.cloneToken(this.openingParen),
1493
                this.args?.map(e => e?.clone()),
2✔
1494
                util.cloneToken(this.closingParen)
1495
            ),
1496
            ['callee', 'args']
1497
        );
1498
    }
1499
}
1500

1501
/**
1502
 * Since template strings can contain newlines, we need to concatenate multiple strings together with chr() calls.
1503
 * This is a single expression that represents the string contatenation of all parts of a single quasi.
1504
 */
1505
export class TemplateStringQuasiExpression extends Expression {
1✔
1506
    constructor(
1507
        readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>
114✔
1508
    ) {
1509
        super();
114✔
1510
        this.range = util.createBoundingRange(
114✔
1511
            ...expressions ?? []
342✔
1512
        );
1513
    }
1514
    readonly range: Range | undefined;
1515

1516
    transpile(state: BrsTranspileState, skipEmptyStrings = true) {
41✔
1517
        let result = [] as TranspileResult;
49✔
1518
        let plus = '';
49✔
1519
        for (let expression of this.expressions) {
49✔
1520
            //skip empty strings
1521
            //TODO what does an empty string literal expression look like?
1522
            if (expression.token.text === '' && skipEmptyStrings === true) {
78✔
1523
                continue;
28✔
1524
            }
1525
            result.push(
50✔
1526
                plus,
1527
                ...expression.transpile(state)
1528
            );
1529
            plus = ' + ';
50✔
1530
        }
1531
        return result;
49✔
1532
    }
1533

1534
    walk(visitor: WalkVisitor, options: WalkOptions) {
1535
        if (options.walkMode & InternalWalkMode.walkExpressions) {
263!
1536
            walkArray(this.expressions, visitor, options, this);
263✔
1537
        }
1538
    }
1539

1540
    public clone() {
1541
        return this.finalizeClone(
15✔
1542
            new TemplateStringQuasiExpression(
1543
                this.expressions?.map(e => e?.clone())
20✔
1544
            ),
1545
            ['expressions']
1546
        );
1547
    }
1548
}
1549

1550
export class TemplateStringExpression extends Expression {
1✔
1551
    constructor(
1552
        readonly openingBacktick: Token,
53✔
1553
        readonly quasis: TemplateStringQuasiExpression[],
53✔
1554
        readonly expressions: Expression[],
53✔
1555
        readonly closingBacktick: Token
53✔
1556
    ) {
1557
        super();
53✔
1558
        this.range = util.createBoundingRange(
53✔
1559
            openingBacktick,
1560
            quasis?.[0],
159✔
1561
            quasis?.[quasis?.length - 1],
315!
1562
            closingBacktick
1563
        );
1564
    }
1565

1566
    public readonly range: Range | undefined;
1567

1568
    transpile(state: BrsTranspileState) {
1569
        //if this is essentially just a normal brightscript string but with backticks, transpile it as a normal string without parens
1570
        if (this.expressions.length === 0 && this.quasis.length === 1 && this.quasis[0].expressions.length === 1) {
24✔
1571
            return this.quasis[0].transpile(state);
6✔
1572
        }
1573
        let result = ['('];
18✔
1574
        let plus = '';
18✔
1575
        //helper function to figure out when to include the plus
1576
        function add(...items) {
1577
            if (items.length > 0) {
52✔
1578
                result.push(
40✔
1579
                    plus,
1580
                    ...items
1581
                );
1582
            }
1583
            //set the plus after the first occurance of a nonzero length set of items
1584
            if (plus === '' && items.length > 0) {
52✔
1585
                plus = ' + ';
18✔
1586
            }
1587
        }
1588

1589
        for (let i = 0; i < this.quasis.length; i++) {
18✔
1590
            let quasi = this.quasis[i];
35✔
1591
            let expression = this.expressions[i];
35✔
1592

1593
            add(
35✔
1594
                ...quasi.transpile(state)
1595
            );
1596
            if (expression) {
35✔
1597
                //skip the toString wrapper around certain expressions
1598
                if (
17✔
1599
                    isEscapedCharCodeLiteralExpression(expression) ||
39✔
1600
                    (isLiteralExpression(expression) && isStringType(expression.type))
1601
                ) {
1602
                    add(
3✔
1603
                        ...expression.transpile(state)
1604
                    );
1605

1606
                    //wrap all other expressions with a bslib_toString call to prevent runtime type mismatch errors
1607
                } else {
1608
                    add(
14✔
1609
                        state.bslibPrefix + '_toString(',
1610
                        ...expression.transpile(state),
1611
                        ')'
1612
                    );
1613
                }
1614
            }
1615
        }
1616
        //the expression should be wrapped in parens so it can be used line a single expression at runtime
1617
        result.push(')');
18✔
1618

1619
        return result;
18✔
1620
    }
1621

1622
    walk(visitor: WalkVisitor, options: WalkOptions) {
1623
        if (options.walkMode & InternalWalkMode.walkExpressions) {
129!
1624
            //walk the quasis and expressions in left-to-right order
1625
            for (let i = 0; i < this.quasis?.length; i++) {
129✔
1626
                walk(this.quasis, i, visitor, options, this);
221✔
1627

1628
                //this skips the final loop iteration since we'll always have one more quasi than expression
1629
                if (this.expressions[i]) {
221✔
1630
                    walk(this.expressions, i, visitor, options, this);
92✔
1631
                }
1632
            }
1633
        }
1634
    }
1635

1636
    public clone() {
1637
        return this.finalizeClone(
7✔
1638
            new TemplateStringExpression(
1639
                util.cloneToken(this.openingBacktick),
1640
                this.quasis?.map(e => e?.clone()),
12✔
1641
                this.expressions?.map(e => e?.clone()),
6✔
1642
                util.cloneToken(this.closingBacktick)
1643
            ),
1644
            ['quasis', 'expressions']
1645
        );
1646
    }
1647
}
1648

1649
export class TaggedTemplateStringExpression extends Expression {
1✔
1650
    constructor(
1651
        readonly tagName: Identifier,
12✔
1652
        readonly openingBacktick: Token,
12✔
1653
        readonly quasis: TemplateStringQuasiExpression[],
12✔
1654
        readonly expressions: Expression[],
12✔
1655
        readonly closingBacktick: Token
12✔
1656
    ) {
1657
        super();
12✔
1658
        this.range = util.createBoundingRange(
12✔
1659
            tagName,
1660
            openingBacktick,
1661
            quasis?.[0],
36✔
1662
            quasis?.[quasis?.length - 1],
69!
1663
            closingBacktick
1664
        );
1665
    }
1666

1667
    public readonly range: Range | undefined;
1668

1669
    transpile(state: BrsTranspileState) {
1670
        let result = [] as TranspileResult;
3✔
1671
        result.push(
3✔
1672
            state.transpileToken(this.tagName),
1673
            '(['
1674
        );
1675

1676
        //add quasis as the first array
1677
        for (let i = 0; i < this.quasis.length; i++) {
3✔
1678
            let quasi = this.quasis[i];
8✔
1679
            //separate items with a comma
1680
            if (i > 0) {
8✔
1681
                result.push(
5✔
1682
                    ', '
1683
                );
1684
            }
1685
            result.push(
8✔
1686
                ...quasi.transpile(state, false)
1687
            );
1688
        }
1689
        result.push(
3✔
1690
            '], ['
1691
        );
1692

1693
        //add expressions as the second array
1694
        for (let i = 0; i < this.expressions.length; i++) {
3✔
1695
            let expression = this.expressions[i];
5✔
1696
            if (i > 0) {
5✔
1697
                result.push(
2✔
1698
                    ', '
1699
                );
1700
            }
1701
            result.push(
5✔
1702
                ...expression.transpile(state)
1703
            );
1704
        }
1705
        result.push(
3✔
1706
            state.sourceNode(this.closingBacktick, '])')
1707
        );
1708
        return result;
3✔
1709
    }
1710

1711
    walk(visitor: WalkVisitor, options: WalkOptions) {
1712
        if (options.walkMode & InternalWalkMode.walkExpressions) {
21!
1713
            //walk the quasis and expressions in left-to-right order
1714
            for (let i = 0; i < this.quasis?.length; i++) {
21✔
1715
                walk(this.quasis, i, visitor, options, this);
48✔
1716

1717
                //this skips the final loop iteration since we'll always have one more quasi than expression
1718
                if (this.expressions[i]) {
48✔
1719
                    walk(this.expressions, i, visitor, options, this);
27✔
1720
                }
1721
            }
1722
        }
1723
    }
1724

1725
    public clone() {
1726
        return this.finalizeClone(
3✔
1727
            new TaggedTemplateStringExpression(
1728
                util.cloneToken(this.tagName),
1729
                util.cloneToken(this.openingBacktick),
1730
                this.quasis?.map(e => e?.clone()),
5✔
1731
                this.expressions?.map(e => e?.clone()),
3✔
1732
                util.cloneToken(this.closingBacktick)
1733
            ),
1734
            ['quasis', 'expressions']
1735
        );
1736
    }
1737
}
1738

1739
export class AnnotationExpression extends Expression {
1✔
1740
    constructor(
1741
        readonly atToken: Token,
70✔
1742
        readonly nameToken: Token
70✔
1743
    ) {
1744
        super();
70✔
1745
        this.name = nameToken.text;
70✔
1746
    }
1747

1748
    public get range() {
1749
        return util.createBoundingRange(
32✔
1750
            this.atToken,
1751
            this.nameToken,
1752
            this.call
1753
        );
1754
    }
1755

1756
    public name: string;
1757
    public call: CallExpression | undefined;
1758

1759
    /**
1760
     * Convert annotation arguments to JavaScript types
1761
     * @param strict If false, keep Expression objects not corresponding to JS types
1762
     */
1763
    getArguments(strict = true): ExpressionValue[] {
10✔
1764
        if (!this.call) {
11✔
1765
            return [];
1✔
1766
        }
1767
        return this.call.args.map(e => expressionToValue(e, strict));
20✔
1768
    }
1769

1770
    transpile(state: BrsTranspileState) {
1771
        return [];
3✔
1772
    }
1773

1774
    walk(visitor: WalkVisitor, options: WalkOptions) {
1775
        //nothing to walk
1776
    }
1777
    getTypedef(state: BrsTranspileState) {
1778
        return [
9✔
1779
            '@',
1780
            this.name,
1781
            ...(this.call?.transpile(state) ?? [])
54✔
1782
        ];
1783
    }
1784

1785
    public clone() {
1786
        const clone = this.finalizeClone(
8✔
1787
            new AnnotationExpression(
1788
                util.cloneToken(this.atToken),
1789
                util.cloneToken(this.nameToken)
1790
            )
1791
        );
1792
        return clone;
8✔
1793
    }
1794
}
1795

1796
export class TernaryExpression extends Expression {
1✔
1797
    constructor(
1798
        readonly test: Expression,
96✔
1799
        readonly questionMarkToken: Token,
96✔
1800
        readonly consequent?: Expression,
96✔
1801
        readonly colonToken?: Token,
96✔
1802
        readonly alternate?: Expression
96✔
1803
    ) {
1804
        super();
96✔
1805
        this.range = util.createBoundingRange(
96✔
1806
            test,
1807
            questionMarkToken,
1808
            consequent,
1809
            colonToken,
1810
            alternate
1811
        );
1812
    }
1813

1814
    public range: Range | undefined;
1815

1816
    transpile(state: BrsTranspileState) {
1817
        let result = [] as TranspileResult;
18✔
1818
        const file = state.file;
18✔
1819
        let consequentInfo = util.getExpressionInfo(this.consequent!, file);
18✔
1820
        let alternateInfo = util.getExpressionInfo(this.alternate!, file);
18✔
1821

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

1829
        let mutatingExpressions = [
18✔
1830
            ...consequentInfo.expressions,
1831
            ...alternateInfo.expressions
1832
        ].filter(e => e instanceof CallExpression || e instanceof CallfuncExpression || e instanceof DottedGetExpression);
120✔
1833

1834
        if (mutatingExpressions.length > 0) {
18✔
1835
            result.push(
10✔
1836
                state.sourceNode(
1837
                    this.questionMarkToken,
1838
                    //write all the scope variables as parameters.
1839
                    //TODO handle when there are more than 31 parameters
1840
                    `(function(${['__bsCondition', ...allUniqueVarNames].join(', ')})`
1841
                ),
1842
                state.newline,
1843
                //double indent so our `end function` line is still indented one at the end
1844
                state.indent(2),
1845
                state.sourceNode(this.test, `if __bsCondition then`),
1846
                state.newline,
1847
                state.indent(1),
1848
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
30!
1849
                ...this.consequent?.transpile(state) ?? [state.sourceNode(this.questionMarkToken, 'invalid')],
60!
1850
                state.newline,
1851
                state.indent(-1),
1852
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'else'),
30!
1853
                state.newline,
1854
                state.indent(1),
1855
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
30!
1856
                ...this.alternate?.transpile(state) ?? [state.sourceNode(this.consequent ?? this.questionMarkToken, 'invalid')],
60!
1857
                state.newline,
1858
                state.indent(-1),
1859
                state.sourceNode(this.questionMarkToken, 'end if'),
1860
                state.newline,
1861
                state.indent(-1),
1862
                state.sourceNode(this.questionMarkToken, 'end function)('),
1863
                ...this.test.transpile(state),
1864
                state.sourceNode(this.questionMarkToken, `${['', ...allUniqueVarNames].join(', ')})`)
1865
            );
1866
            state.blockDepth--;
10✔
1867
        } else {
1868
            result.push(
8✔
1869
                state.sourceNode(this.test, state.bslibPrefix + `_ternary(`),
1870
                ...this.test.transpile(state),
1871
                state.sourceNode(this.test, `, `),
1872
                ...this.consequent?.transpile(state) ?? ['invalid'],
48✔
1873
                `, `,
1874
                ...this.alternate?.transpile(state) ?? ['invalid'],
48✔
1875
                `)`
1876
            );
1877
        }
1878
        return result;
18✔
1879
    }
1880

1881
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1882
        if (options.walkMode & InternalWalkMode.walkExpressions) {
236!
1883
            walk(this, 'test', visitor, options);
236✔
1884
            walk(this, 'consequent', visitor, options);
236✔
1885
            walk(this, 'alternate', visitor, options);
236✔
1886
        }
1887
    }
1888

1889
    public clone() {
1890
        return this.finalizeClone(
2✔
1891
            new TernaryExpression(
1892
                this.test?.clone(),
6✔
1893
                util.cloneToken(this.questionMarkToken),
1894
                this.consequent?.clone(),
6✔
1895
                util.cloneToken(this.colonToken),
1896
                this.alternate?.clone()
6✔
1897
            ),
1898
            ['test', 'consequent', 'alternate']
1899
        );
1900
    }
1901
}
1902

1903
export class NullCoalescingExpression extends Expression {
1✔
1904
    constructor(
1905
        public consequent: Expression,
32✔
1906
        public questionQuestionToken: Token,
32✔
1907
        public alternate: Expression
32✔
1908
    ) {
1909
        super();
32✔
1910
        this.range = util.createBoundingRange(
32✔
1911
            consequent,
1912
            questionQuestionToken,
1913
            alternate
1914
        );
1915
    }
1916
    public readonly range: Range | undefined;
1917

1918
    transpile(state: BrsTranspileState) {
1919
        let result = [] as TranspileResult;
11✔
1920
        let consequentInfo = util.getExpressionInfo(this.consequent, state.file);
11✔
1921
        let alternateInfo = util.getExpressionInfo(this.alternate, state.file);
11✔
1922

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

1930
        let hasMutatingExpression = [
11✔
1931
            ...consequentInfo.expressions,
1932
            ...alternateInfo.expressions
1933
        ].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e));
30✔
1934

1935
        if (hasMutatingExpression) {
11✔
1936
            result.push(
7✔
1937
                `(function(`,
1938
                //write all the scope variables as parameters.
1939
                //TODO handle when there are more than 31 parameters
1940
                allUniqueVarNames.join(', '),
1941
                ')',
1942
                state.newline,
1943
                //double indent so our `end function` line is still indented one at the end
1944
                state.indent(2),
1945
                //evaluate the consequent exactly once, and then use it in the following condition
1946
                `__bsConsequent = `,
1947
                ...this.consequent.transpile(state),
1948
                state.newline,
1949
                state.indent(),
1950
                `if __bsConsequent <> invalid then`,
1951
                state.newline,
1952
                state.indent(1),
1953
                'return __bsConsequent',
1954
                state.newline,
1955
                state.indent(-1),
1956
                'else',
1957
                state.newline,
1958
                state.indent(1),
1959
                'return ',
1960
                ...this.alternate.transpile(state),
1961
                state.newline,
1962
                state.indent(-1),
1963
                'end if',
1964
                state.newline,
1965
                state.indent(-1),
1966
                'end function)(',
1967
                allUniqueVarNames.join(', '),
1968
                ')'
1969
            );
1970
            state.blockDepth--;
7✔
1971
        } else {
1972
            result.push(
4✔
1973
                state.bslibPrefix + `_coalesce(`,
1974
                ...this.consequent.transpile(state),
1975
                ', ',
1976
                ...this.alternate.transpile(state),
1977
                ')'
1978
            );
1979
        }
1980
        return result;
11✔
1981
    }
1982

1983
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1984
        if (options.walkMode & InternalWalkMode.walkExpressions) {
64!
1985
            walk(this, 'consequent', visitor, options);
64✔
1986
            walk(this, 'alternate', visitor, options);
64✔
1987
        }
1988
    }
1989

1990
    public clone() {
1991
        return this.finalizeClone(
2✔
1992
            new NullCoalescingExpression(
1993
                this.consequent?.clone(),
6✔
1994
                util.cloneToken(this.questionQuestionToken),
1995
                this.alternate?.clone()
6✔
1996
            ),
1997
            ['consequent', 'alternate']
1998
        );
1999
    }
2000
}
2001

2002
export class RegexLiteralExpression extends Expression {
1✔
2003
    public constructor(
2004
        public tokens: {
46✔
2005
            regexLiteral: Token;
2006
        }
2007
    ) {
2008
        super();
46✔
2009
    }
2010

2011
    public get range() {
2012
        return this.tokens?.regexLiteral?.range;
55!
2013
    }
2014

2015
    public transpile(state: BrsTranspileState): TranspileResult {
2016
        let text = this.tokens.regexLiteral?.text ?? '';
42!
2017
        let flags = '';
42✔
2018
        //get any flags from the end
2019
        const flagMatch = /\/([a-z]+)$/i.exec(text);
42✔
2020
        if (flagMatch) {
42✔
2021
            text = text.substring(0, flagMatch.index + 1);
2✔
2022
            flags = flagMatch[1];
2✔
2023
        }
2024
        let pattern = text
42✔
2025
            //remove leading and trailing slashes
2026
            .substring(1, text.length - 1)
2027
            //escape quotemarks
2028
            .split('"').join('" + chr(34) + "');
2029

2030
        return [
42✔
2031
            state.sourceNode(this.tokens.regexLiteral, [
2032
                'CreateObject("roRegex", ',
2033
                `"${pattern}", `,
2034
                `"${flags}"`,
2035
                ')'
2036
            ])
2037
        ];
2038
    }
2039

2040
    walk(visitor: WalkVisitor, options: WalkOptions) {
2041
        //nothing to walk
2042
    }
2043

2044
    public clone() {
2045
        return this.finalizeClone(
1✔
2046
            new RegexLiteralExpression({
2047
                regexLiteral: util.cloneToken(this.tokens.regexLiteral)
2048
            })
2049
        );
2050
    }
2051
}
2052

2053

2054
export class TypeCastExpression extends Expression {
1✔
2055
    constructor(
2056
        public obj: Expression,
19✔
2057
        public asToken: Token,
19✔
2058
        public typeToken: Token
19✔
2059
    ) {
2060
        super();
19✔
2061
        this.range = util.createBoundingRange(
19✔
2062
            this.obj,
2063
            this.asToken,
2064
            this.typeToken
2065
        );
2066
    }
2067

2068
    public range: Range;
2069

2070
    public transpile(state: BrsTranspileState): TranspileResult {
2071
        return this.obj.transpile(state);
11✔
2072
    }
2073
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2074
        if (options.walkMode & InternalWalkMode.walkExpressions) {
51!
2075
            walk(this, 'obj', visitor, options);
51✔
2076
        }
2077
    }
2078

2079
    public clone() {
2080
        return this.finalizeClone(
2✔
2081
            new TypeCastExpression(
2082
                this.obj?.clone(),
6✔
2083
                util.cloneToken(this.asToken),
2084
                util.cloneToken(this.typeToken)
2085
            ),
2086
            ['obj']
2087
        );
2088
    }
2089
}
2090

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

2094
function expressionToValue(expr: Expression, strict: boolean): ExpressionValue {
2095
    if (!expr) {
30!
2096
        return null;
×
2097
    }
2098
    if (isUnaryExpression(expr) && isLiteralNumber(expr.right)) {
30✔
2099
        return numberExpressionToValue(expr.right, expr.operator.text);
1✔
2100
    }
2101
    if (isLiteralString(expr)) {
29✔
2102
        //remove leading and trailing quotes
2103
        return expr.token.text.replace(/^"/, '').replace(/"$/, '');
5✔
2104
    }
2105
    if (isLiteralNumber(expr)) {
24✔
2106
        return numberExpressionToValue(expr);
11✔
2107
    }
2108

2109
    if (isLiteralBoolean(expr)) {
13✔
2110
        return expr.token.text.toLowerCase() === 'true';
3✔
2111
    }
2112
    if (isArrayLiteralExpression(expr)) {
10✔
2113
        return expr.elements
3✔
2114
            .filter(e => !isCommentStatement(e))
7✔
2115
            .map(e => expressionToValue(e, strict));
7✔
2116
    }
2117
    if (isAALiteralExpression(expr)) {
7✔
2118
        return expr.elements.reduce((acc, e) => {
3✔
2119
            if (!isCommentStatement(e) && !(isAAIndexedMemberExpression(e))) {
3!
2120
                acc[e.keyToken.text] = expressionToValue(e.value, strict);
3✔
2121
            }
2122
            return acc;
3✔
2123
        }, {});
2124
    }
2125
    //for annotations, we only support serializing pure string values
2126
    if (isTemplateStringExpression(expr)) {
4✔
2127
        if (expr.quasis?.length === 1 && expr.expressions.length === 0) {
2!
2128
            return expr.quasis[0].expressions.map(x => x.token.text).join('');
10✔
2129
        }
2130
    }
2131
    return strict ? null : expr;
2✔
2132
}
2133

2134
function numberExpressionToValue(expr: LiteralExpression, operator = '') {
11✔
2135
    if (isIntegerType(expr.type) || isLongIntegerType(expr.type)) {
12!
2136
        return parseInt(operator + expr.token.text);
12✔
2137
    } else {
2138
        return parseFloat(operator + expr.token.text);
×
2139
    }
2140
}
2141

2142
/**
2143
 * A list of names of functions that are restricted from being stored to a
2144
 * variable, property, or passed as an argument. (i.e. `type` or `createobject`).
2145
 * Names are stored in lower case.
2146
 */
2147
const nonReferenceableFunctions = [
1✔
2148
    'createobject',
2149
    'type',
2150
    'getglobalaa',
2151
    'box',
2152
    'run',
2153
    'eval',
2154
    'getlastruncompileerror',
2155
    'getlastrunruntimeerror',
2156
    'tab',
2157
    'pos'
2158
];
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc