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

rokucommunity / brighterscript / #13350

25 Nov 2024 08:44PM UTC coverage: 89.053%. Remained the same
#13350

push

web-flow
Merge 961502182 into c5674f5d8

7359 of 8712 branches covered (84.47%)

Branch coverage included in aggregate %.

55 of 64 new or added lines in 9 files covered. (85.94%)

63 existing lines in 7 files now uncovered.

9724 of 10471 relevant lines covered (92.87%)

1825.48 hits per line

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

92.12
/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 { isAALiteralExpression, isArrayLiteralExpression, isCallExpression, isCallfuncExpression, isCommentStatement, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isIntegerType, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isNewExpression, isStringType, isTemplateStringExpression, isTypeCastExpression, isUnaryExpression, isVariableExpression } 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,
362✔
29
        public operator: Token,
362✔
30
        public right: Expression
362✔
31
    ) {
32
        super();
362✔
33
        this.range = util.createBoundingRange(this.left, this.operator, this.right);
362✔
34
    }
35

36
    public readonly range: Range | undefined;
37

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

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

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

129
    walk(visitor: WalkVisitor, options: WalkOptions) {
130
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,586!
131
            walk(this, 'callee', visitor, options);
1,586✔
132
            walkArray(this.args, visitor, options, this);
1,586✔
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[],
1,843✔
152
        public body: Block,
1,843✔
153
        readonly functionType: Token | null,
1,843✔
154
        public end: Token,
1,843✔
155
        readonly leftParen: Token,
1,843✔
156
        readonly rightParen: Token,
1,843✔
157
        readonly asToken?: Token,
1,843✔
158
        readonly returnTypeToken?: Token
1,843✔
159
    ) {
160
        super();
1,843✔
161
        if (this.returnTypeToken) {
1,843✔
162
            this.returnType = util.tokenToBscType(this.returnTypeToken);
76✔
163
        } else if (this.functionType?.text.toLowerCase() === 'sub') {
1,767!
164
            this.returnType = new VoidType();
1,404✔
165
        } else {
166
            this.returnType = DynamicType.instance;
363✔
167
        }
168

169
        //if there's a body, and it doesn't have a SymbolTable, assign one
170
        if (this.body && !this.body.symbolTable) {
1,843✔
171
            this.body.symbolTable = new SymbolTable(`Function Body`);
142✔
172
        }
173
        this.symbolTable = new SymbolTable('FunctionExpression', () => this.parent?.getSymbolTable());
1,843!
174
    }
175

176
    /**
177
     * The type this function returns
178
     */
179
    public returnType: BscType;
180

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

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

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

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

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

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

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

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

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

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

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

349
    public clone() {
350
        const clone = this.finalizeClone(
106✔
351
            new FunctionExpression(
352
                this.parameters?.map(e => e?.clone()),
7✔
353
                this.body?.clone(),
318✔
354
                util.cloneToken(this.functionType),
355
                util.cloneToken(this.end),
356
                util.cloneToken(this.leftParen),
357
                util.cloneToken(this.rightParen),
358
                util.cloneToken(this.asToken),
359
                util.cloneToken(this.returnTypeToken)
360
            ),
361
            ['body']
362
        );
363

364
        //rebuild the .callExpressions list in the clone
365
        clone.body?.walk?.((node) => {
106✔
366
            if (isCallExpression(node) && !isNewExpression(node.parent)) {
201✔
367
                clone.callExpressions.push(node);
6✔
368
            }
369
        }, { walkMode: WalkMode.visitExpressions });
370
        return clone;
106✔
371
    }
372
}
373

374
export class FunctionParameterExpression extends Expression {
1✔
375
    constructor(
376
        public name: Identifier,
627✔
377
        public typeToken?: Token,
627✔
378
        public defaultValue?: Expression,
627✔
379
        public asToken?: Token
627✔
380
    ) {
381
        super();
627✔
382
        if (typeToken) {
627✔
383
            this.type = util.tokenToBscType(typeToken);
277✔
384
        } else {
385
            this.type = new DynamicType();
350✔
386
        }
387
    }
388

389
    public type: BscType;
390

391
    public get range(): Range | undefined {
392
        return util.createBoundingRange(
422✔
393
            this.name,
394
            this.asToken,
395
            this.typeToken,
396
            this.defaultValue
397
        );
398
    }
399

400
    public transpile(state: BrsTranspileState) {
401
        let result = [
110✔
402
            //name
403
            state.transpileToken(this.name)
404
        ] as any[];
405
        //default value
406
        if (this.defaultValue) {
110✔
407
            result.push(' = ');
9✔
408
            result.push(this.defaultValue.transpile(state));
9✔
409
        }
410
        //type declaration
411
        if (this.asToken && !state.options.removeParameterTypes) {
110✔
412
            result.push(' ');
74✔
413
            result.push(state.transpileToken(this.asToken));
74✔
414
            result.push(' ');
74✔
415
            result.push(state.sourceNode(this.typeToken!, this.type.toTypeString()));
74✔
416
        }
417

418
        return result;
110✔
419
    }
420

421
    public getTypedef(state: BrsTranspileState): TranspileResult {
422
        const results = [this.name.text] as TranspileResult;
7✔
423

424
        if (this.defaultValue) {
7!
425
            results.push(' = ', ...this.defaultValue.transpile(state));
×
426
        }
427

428
        if (this.asToken) {
7✔
429
            results.push(' as ');
6✔
430

431
            // TODO: Is this conditional needed? Will typeToken always exist
432
            // so long as `asToken` exists?
433
            if (this.typeToken) {
6!
434
                results.push(this.typeToken.text);
6✔
435
            }
436
        }
437

438
        return results;
7✔
439
    }
440

441
    walk(visitor: WalkVisitor, options: WalkOptions) {
442
        // eslint-disable-next-line no-bitwise
443
        if (this.defaultValue && options.walkMode & InternalWalkMode.walkExpressions) {
1,499✔
444
            walk(this, 'defaultValue', visitor, options);
472✔
445
        }
446
    }
447

448
    public clone() {
449
        return this.finalizeClone(
8✔
450
            new FunctionParameterExpression(
451
                util.cloneToken(this.name),
452
                util.cloneToken(this.typeToken),
453
                this.defaultValue?.clone(),
24✔
454
                util.cloneToken(this.asToken)
455
            ),
456
            ['defaultValue']
457
        );
458
    }
459
}
460

461
export class NamespacedVariableNameExpression extends Expression {
1✔
462
    constructor(
463
        //if this is a `DottedGetExpression`, it must be comprised only of `VariableExpression`s
464
        readonly expression: DottedGetExpression | VariableExpression
448✔
465
    ) {
466
        super();
448✔
467
        this.range = expression?.range;
448✔
468
    }
469
    range: Range | undefined;
470

471
    transpile(state: BrsTranspileState) {
472
        return [
4✔
473
            state.sourceNode(this, this.getName(ParseMode.BrightScript))
474
        ];
475
    }
476

477
    public getNameParts() {
478
        let parts = [] as string[];
3,134✔
479
        if (isVariableExpression(this.expression)) {
3,134✔
480
            parts.push(this.expression.name.text);
2,204✔
481
        } else {
482
            let expr = this.expression;
930✔
483

484
            parts.push(expr.name.text);
930✔
485

486
            while (isVariableExpression(expr) === false) {
930✔
487
                expr = expr.obj as DottedGetExpression;
1,090✔
488
                parts.unshift(expr.name.text);
1,090✔
489
            }
490
        }
491
        return parts;
3,134✔
492
    }
493

494
    getName(parseMode: ParseMode) {
495
        if (parseMode === ParseMode.BrighterScript) {
3,086✔
496
            return this.getNameParts().join('.');
2,841✔
497
        } else {
498
            return this.getNameParts().join('_');
245✔
499
        }
500
    }
501

502
    walk(visitor: WalkVisitor, options: WalkOptions) {
503
        this.expression?.link();
815!
504
        if (options.walkMode & InternalWalkMode.walkExpressions) {
815✔
505
            walk(this, 'expression', visitor, options);
791✔
506
        }
507
    }
508

509
    public clone() {
510
        return this.finalizeClone(
6✔
511
            new NamespacedVariableNameExpression(
512
                this.expression?.clone()
18✔
513
            )
514
        );
515
    }
516
}
517

518
export class DottedGetExpression extends Expression {
1✔
519
    constructor(
520
        readonly obj: Expression,
1,083✔
521
        readonly name: Identifier,
1,083✔
522
        /**
523
         * Can either be `.`, or `?.` for optional chaining
524
         */
525
        readonly dot: Token
1,083✔
526
    ) {
527
        super();
1,083✔
528
        this.range = util.createBoundingRange(this.obj, this.dot, this.name);
1,083✔
529
    }
530

531
    public readonly range: Range | undefined;
532

533
    transpile(state: BrsTranspileState) {
534
        //if the callee starts with a namespace name, transpile the name
535
        if (state.file.calleeStartsWithNamespace(this)) {
237✔
536
            return new NamespacedVariableNameExpression(this as DottedGetExpression | VariableExpression).transpile(state);
3✔
537
        } else {
538
            return [
234✔
539
                ...this.obj.transpile(state),
540
                state.transpileToken(this.dot),
541
                state.transpileToken(this.name)
542
            ];
543
        }
544
    }
545

546
    walk(visitor: WalkVisitor, options: WalkOptions) {
547
        if (options.walkMode & InternalWalkMode.walkExpressions) {
2,187!
548
            walk(this, 'obj', visitor, options);
2,187✔
549
        }
550
    }
551

552
    public clone() {
553
        return this.finalizeClone(
6✔
554
            new DottedGetExpression(
555
                this.obj?.clone(),
18✔
556
                util.cloneToken(this.name),
557
                util.cloneToken(this.dot)
558
            ),
559
            ['obj']
560
        );
561
    }
562
}
563

564
export class XmlAttributeGetExpression extends Expression {
1✔
565
    constructor(
566
        readonly obj: Expression,
14✔
567
        readonly name: Identifier,
14✔
568
        /**
569
         * Can either be `@`, or `?@` for optional chaining
570
         */
571
        readonly at: Token
14✔
572
    ) {
573
        super();
14✔
574
        this.range = util.createBoundingRange(this.obj, this.at, this.name);
14✔
575
    }
576

577
    public readonly range: Range | undefined;
578

579
    transpile(state: BrsTranspileState) {
580
        return [
3✔
581
            ...this.obj.transpile(state),
582
            state.transpileToken(this.at),
583
            state.transpileToken(this.name)
584
        ];
585
    }
586

587
    walk(visitor: WalkVisitor, options: WalkOptions) {
588
        if (options.walkMode & InternalWalkMode.walkExpressions) {
20!
589
            walk(this, 'obj', visitor, options);
20✔
590
        }
591
    }
592

593
    public clone() {
594
        return this.finalizeClone(
2✔
595
            new XmlAttributeGetExpression(
596
                this.obj?.clone(),
6✔
597
                util.cloneToken(this.name),
598
                util.cloneToken(this.at)
599
            ),
600
            ['obj']
601
        );
602
    }
603
}
604

605
export class IndexedGetExpression extends Expression {
1✔
606
    constructor(
607
        public obj: Expression,
146✔
608
        public index: Expression,
146✔
609
        /**
610
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.`
611
         */
612
        public openingSquare: Token,
146✔
613
        public closingSquare: Token,
146✔
614
        public questionDotToken?: Token, //  ? or ?.
146✔
615
        /**
616
         * More indexes, separated by commas
617
         */
618
        public additionalIndexes?: Expression[]
146✔
619
    ) {
620
        super();
146✔
621
        this.range = util.createBoundingRange(this.obj, this.openingSquare, this.questionDotToken, this.openingSquare, this.index, this.closingSquare);
146✔
622
        this.additionalIndexes ??= [];
146✔
623
    }
624

625
    public readonly range: Range | undefined;
626

627
    transpile(state: BrsTranspileState) {
628
        const result = [];
62✔
629
        result.push(
62✔
630
            ...this.obj.transpile(state),
631
            this.questionDotToken ? state.transpileToken(this.questionDotToken) : '',
62✔
632
            state.transpileToken(this.openingSquare)
633
        );
634
        const indexes = [this.index, ...this.additionalIndexes ?? []];
62!
635
        for (let i = 0; i < indexes.length; i++) {
62✔
636
            //add comma between indexes
637
            if (i > 0) {
70✔
638
                result.push(', ');
8✔
639
            }
640
            let index = indexes[i];
70✔
641
            result.push(
70✔
642
                ...(index?.transpile(state) ?? [])
420!
643
            );
644
        }
645
        result.push(
62✔
646
            this.closingSquare ? state.transpileToken(this.closingSquare) : ''
62!
647
        );
648
        return result;
62✔
649
    }
650

651
    walk(visitor: WalkVisitor, options: WalkOptions) {
652
        if (options.walkMode & InternalWalkMode.walkExpressions) {
254!
653
            walk(this, 'obj', visitor, options);
254✔
654
            walk(this, 'index', visitor, options);
254✔
655
            walkArray(this.additionalIndexes, visitor, options, this);
254✔
656
        }
657
    }
658

659
    public clone() {
660
        return this.finalizeClone(
5✔
661
            new IndexedGetExpression(
662
                this.obj?.clone(),
15✔
663
                this.index?.clone(),
15✔
664
                util.cloneToken(this.openingSquare),
665
                util.cloneToken(this.closingSquare),
666
                util.cloneToken(this.questionDotToken),
667
                this.additionalIndexes?.map(e => e?.clone())
2✔
668
            ),
669
            ['obj', 'index', 'additionalIndexes']
670
        );
671
    }
672
}
673

674
export class GroupingExpression extends Expression {
1✔
675
    constructor(
676
        readonly tokens: {
39✔
677
            left: Token;
678
            right: Token;
679
        },
680
        public expression: Expression
39✔
681
    ) {
682
        super();
39✔
683
        this.range = util.createBoundingRange(this.tokens.left, this.expression, this.tokens.right);
39✔
684
    }
685

686
    public readonly range: Range | undefined;
687

688
    transpile(state: BrsTranspileState) {
689
        if (isTypeCastExpression(this.expression)) {
12✔
690
            return this.expression.transpile(state);
7✔
691
        }
692
        return [
5✔
693
            state.transpileToken(this.tokens.left),
694
            ...this.expression.transpile(state),
695
            state.transpileToken(this.tokens.right)
696
        ];
697
    }
698

699
    walk(visitor: WalkVisitor, options: WalkOptions) {
700
        if (options.walkMode & InternalWalkMode.walkExpressions) {
71!
701
            walk(this, 'expression', visitor, options);
71✔
702
        }
703
    }
704

705
    public clone() {
706
        return this.finalizeClone(
2✔
707
            new GroupingExpression(
708
                {
709
                    left: util.cloneToken(this.tokens.left),
710
                    right: util.cloneToken(this.tokens.right)
711
                },
712
                this.expression?.clone()
6✔
713
            ),
714
            ['expression']
715
        );
716
    }
717
}
718

719
export class LiteralExpression extends Expression {
1✔
720
    constructor(
721
        public token: Token
3,335✔
722
    ) {
723
        super();
3,335✔
724
        this.type = util.tokenToBscType(token);
3,335✔
725
    }
726

727
    public get range() {
728
        return this.token.range;
7,535✔
729
    }
730

731
    /**
732
     * The (data) type of this expression
733
     */
734
    public type: BscType;
735

736
    transpile(state: BrsTranspileState) {
737
        let text: string;
738
        if (this.token.kind === TokenKind.TemplateStringQuasi) {
801✔
739
            //wrap quasis with quotes (and escape inner quotemarks)
740
            text = `"${this.token.text.replace(/"/g, '""')}"`;
28✔
741

742
        } else if (isStringType(this.type)) {
773✔
743
            text = this.token.text;
303✔
744
            //add trailing quotemark if it's missing. We will have already generated a diagnostic for this.
745
            if (text.endsWith('"') === false) {
303✔
746
                text += '"';
1✔
747
            }
748
        } else {
749
            text = this.token.text;
470✔
750
        }
751

752
        return [
801✔
753
            state.sourceNode(this, text)
754
        ];
755
    }
756

757
    walk(visitor: WalkVisitor, options: WalkOptions) {
758
        //nothing to walk
759
    }
760

761
    public clone() {
762
        return this.finalizeClone(
100✔
763
            new LiteralExpression(
764
                util.cloneToken(this.token)
765
            )
766
        );
767
    }
768
}
769

770
/**
771
 * This is a special expression only used within template strings. It exists so we can prevent producing lots of empty strings
772
 * during template string transpile by identifying these expressions explicitly and skipping the bslib_toString around them
773
 */
774
export class EscapedCharCodeLiteralExpression extends Expression {
1✔
775
    constructor(
776
        readonly token: Token & { charCode: number }
35✔
777
    ) {
778
        super();
35✔
779
        this.range = token.range;
35✔
780
    }
781
    readonly range: Range;
782

783
    transpile(state: BrsTranspileState) {
784
        return [
13✔
785
            state.sourceNode(this, `chr(${this.token.charCode})`)
786
        ];
787
    }
788

789
    walk(visitor: WalkVisitor, options: WalkOptions) {
790
        //nothing to walk
791
    }
792

793
    public clone() {
794
        return this.finalizeClone(
3✔
795
            new EscapedCharCodeLiteralExpression(
796
                util.cloneToken(this.token)
797
            )
798
        );
799
    }
800
}
801

802
export class ArrayLiteralExpression extends Expression {
1✔
803
    constructor(
804
        readonly elements: Array<Expression | CommentStatement>,
130✔
805
        readonly open: Token,
130✔
806
        readonly close: Token,
130✔
807
        readonly hasSpread = false
130✔
808
    ) {
809
        super();
130✔
810
        this.range = util.createBoundingRange(this.open, ...this.elements ?? [], this.close);
130✔
811
    }
812

813
    public readonly range: Range | undefined;
814

815
    transpile(state: BrsTranspileState) {
816
        let result = [] as TranspileResult;
57✔
817
        result.push(
57✔
818
            state.transpileToken(this.open)
819
        );
820
        let hasChildren = this.elements.length > 0;
57✔
821
        state.blockDepth++;
57✔
822

823
        for (let i = 0; i < this.elements.length; i++) {
57✔
824
            let previousElement = this.elements[i - 1];
69✔
825
            let element = this.elements[i];
69✔
826

827
            if (isCommentStatement(element)) {
69✔
828
                //if the comment is on the same line as opening square or previous statement, don't add newline
829
                if (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element)) {
7✔
830
                    result.push(' ');
4✔
831
                } else {
832
                    result.push(
3✔
833
                        '\n',
834
                        state.indent()
835
                    );
836
                }
837
                state.lineage.unshift(this);
7✔
838
                result.push(element.transpile(state));
7✔
839
                state.lineage.shift();
7✔
840
            } else {
841
                result.push('\n');
62✔
842

843
                result.push(
62✔
844
                    state.indent(),
845
                    ...element.transpile(state)
846
                );
847
            }
848
        }
849
        state.blockDepth--;
57✔
850
        //add a newline between open and close if there are elements
851
        if (hasChildren) {
57✔
852
            result.push('\n');
35✔
853
            result.push(state.indent());
35✔
854
        }
855
        if (this.close) {
57!
856
            result.push(
57✔
857
                state.transpileToken(this.close)
858
            );
859
        }
860
        return result;
57✔
861
    }
862

863
    walk(visitor: WalkVisitor, options: WalkOptions) {
864
        if (options.walkMode & InternalWalkMode.walkExpressions) {
299!
865
            walkArray(this.elements, visitor, options, this);
299✔
866
        }
867
    }
868

869
    public clone() {
870
        return this.finalizeClone(
4✔
871
            new ArrayLiteralExpression(
872
                this.elements?.map(e => e?.clone()),
6✔
873
                util.cloneToken(this.open),
874
                util.cloneToken(this.close),
875
                this.hasSpread
876
            ),
877
            ['elements']
878
        );
879
    }
880
}
881

882
export class AAMemberExpression extends Expression {
1✔
883
    constructor(
884
        public keyToken: Token,
194✔
885
        public colonToken: Token,
194✔
886
        /** The expression evaluated to determine the member's initial value. */
887
        public value: Expression
194✔
888
    ) {
889
        super();
194✔
890
        this.range = util.createBoundingRange(this.keyToken, this.colonToken, this.value);
194✔
891
    }
892

893
    public range: Range | undefined;
894
    public commaToken?: Token;
895

896
    transpile(state: BrsTranspileState) {
897
        //TODO move the logic from AALiteralExpression loop into this function
898
        return [];
×
899
    }
900

901
    walk(visitor: WalkVisitor, options: WalkOptions) {
902
        walk(this, 'value', visitor, options);
284✔
903
    }
904

905
    public clone() {
906
        return this.finalizeClone(
4✔
907
            new AAMemberExpression(
908
                util.cloneToken(this.keyToken),
909
                util.cloneToken(this.colonToken),
910
                this.value?.clone()
12✔
911
            ),
912
            ['value']
913
        );
914
    }
915

916
}
917

918
export class AALiteralExpression extends Expression {
1✔
919
    constructor(
920
        readonly elements: Array<AAMemberExpression | CommentStatement>,
200✔
921
        readonly open: Token,
200✔
922
        readonly close: Token
200✔
923
    ) {
924
        super();
200✔
925
        this.range = util.createBoundingRange(this.open, ...this.elements ?? [], this.close);
200✔
926
    }
927

928
    public readonly range: Range | undefined;
929

930
    transpile(state: BrsTranspileState) {
931
        let result = [] as TranspileResult;
53✔
932
        //open curly
933
        result.push(
53✔
934
            state.transpileToken(this.open)
935
        );
936
        let hasChildren = this.elements.length > 0;
53✔
937
        //add newline if the object has children and the first child isn't a comment starting on the same line as opening curly
938
        if (hasChildren && (isCommentStatement(this.elements[0]) === false || !util.linesTouch(this.elements[0], this.open))) {
53✔
939
            result.push('\n');
21✔
940
        }
941
        state.blockDepth++;
53✔
942
        for (let i = 0; i < this.elements.length; i++) {
53✔
943
            let element = this.elements[i];
47✔
944
            let previousElement = this.elements[i - 1];
47✔
945
            let nextElement = this.elements[i + 1];
47✔
946

947
            //don't indent if comment is same-line
948
            if (isCommentStatement(element as any) &&
47✔
949
                (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element))
950
            ) {
951
                result.push(' ');
10✔
952

953
                //indent line
954
            } else {
955
                result.push(state.indent());
37✔
956
            }
957

958
            //render comments
959
            if (isCommentStatement(element)) {
47✔
960
                result.push(...element.transpile(state));
14✔
961
            } else {
962
                //key
963
                result.push(
33✔
964
                    state.transpileToken(element.keyToken)
965
                );
966
                //colon
967
                result.push(
33✔
968
                    state.transpileToken(element.colonToken),
969
                    ' '
970
                );
971

972
                //value
973
                result.push(...element.value.transpile(state));
33✔
974
            }
975

976

977
            //if next element is a same-line comment, skip the newline
978
            if (nextElement && isCommentStatement(nextElement) && nextElement.range?.start.line === element.range?.start.line) {
47!
979

980
                //add a newline between statements
981
            } else {
982
                result.push('\n');
39✔
983
            }
984
        }
985
        state.blockDepth--;
53✔
986

987
        //only indent the closing curly if we have children
988
        if (hasChildren) {
53✔
989
            result.push(state.indent());
23✔
990
        }
991
        //close curly
992
        if (this.close) {
53!
993
            result.push(
53✔
994
                state.transpileToken(this.close)
995
            );
996
        }
997
        return result;
53✔
998
    }
999

1000
    walk(visitor: WalkVisitor, options: WalkOptions) {
1001
        if (options.walkMode & InternalWalkMode.walkExpressions) {
325!
1002
            walkArray(this.elements, visitor, options, this);
325✔
1003
        }
1004
    }
1005

1006
    public clone() {
1007
        return this.finalizeClone(
6✔
1008
            new AALiteralExpression(
1009
                this.elements?.map(e => e?.clone()),
5✔
1010
                util.cloneToken(this.open),
1011
                util.cloneToken(this.close)
1012
            ),
1013
            ['elements']
1014
        );
1015
    }
1016
}
1017

1018
export class UnaryExpression extends Expression {
1✔
1019
    constructor(
1020
        public operator: Token,
41✔
1021
        public right: Expression
41✔
1022
    ) {
1023
        super();
41✔
1024
        this.range = util.createBoundingRange(this.operator, this.right);
41✔
1025
    }
1026

1027
    public readonly range: Range | undefined;
1028

1029
    transpile(state: BrsTranspileState) {
1030
        let separatingWhitespace: string | undefined;
1031
        if (isVariableExpression(this.right)) {
14✔
1032
            separatingWhitespace = this.right.name.leadingWhitespace;
6✔
1033
        } else if (isLiteralExpression(this.right)) {
8✔
1034
            separatingWhitespace = this.right.token.leadingWhitespace;
3✔
1035
        }
1036

1037
        return [
14✔
1038
            state.transpileToken(this.operator),
1039
            separatingWhitespace ?? ' ',
42✔
1040
            ...this.right.transpile(state)
1041
        ];
1042
    }
1043

1044
    walk(visitor: WalkVisitor, options: WalkOptions) {
1045
        if (options.walkMode & InternalWalkMode.walkExpressions) {
73!
1046
            walk(this, 'right', visitor, options);
73✔
1047
        }
1048
    }
1049

1050
    public clone() {
1051
        return this.finalizeClone(
2✔
1052
            new UnaryExpression(
1053
                util.cloneToken(this.operator),
1054
                this.right?.clone()
6✔
1055
            ),
1056
            ['right']
1057
        );
1058
    }
1059
}
1060

1061
export class VariableExpression extends Expression {
1✔
1062
    constructor(
1063
        readonly name: Identifier
2,018✔
1064
    ) {
1065
        super();
2,018✔
1066
        this.range = this.name?.range;
2,018!
1067
    }
1068

1069
    public readonly range: Range;
1070

1071
    public getName(parseMode: ParseMode) {
1072
        return this.name.text;
30✔
1073
    }
1074

1075
    transpile(state: BrsTranspileState) {
1076
        let result = [] as TranspileResult;
377✔
1077
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
377✔
1078
        //if the callee is the name of a known namespace function
1079
        if (namespace && state.file.calleeIsKnownNamespaceFunction(this, namespace.getName(ParseMode.BrighterScript))) {
377✔
1080
            result.push(
5✔
1081
                state.sourceNode(this, [
1082
                    namespace.getName(ParseMode.BrightScript),
1083
                    '_',
1084
                    this.getName(ParseMode.BrightScript)
1085
                ])
1086
            );
1087

1088
            //transpile  normally
1089
        } else {
1090
            result.push(
372✔
1091
                state.transpileToken(this.name)
1092
            );
1093
        }
1094
        return result;
377✔
1095
    }
1096

1097
    walk(visitor: WalkVisitor, options: WalkOptions) {
1098
        //nothing to walk
1099
    }
1100

1101
    public clone() {
1102
        return this.finalizeClone(
40✔
1103
            new VariableExpression(
1104
                util.cloneToken(this.name)
1105
            )
1106
        );
1107
    }
1108
}
1109

1110
export class SourceLiteralExpression extends Expression {
1✔
1111
    constructor(
1112
        readonly token: Token
37✔
1113
    ) {
1114
        super();
37✔
1115
        this.range = token?.range;
37!
1116
    }
1117

1118
    public readonly range: Range;
1119

1120
    private getFunctionName(state: BrsTranspileState, parseMode: ParseMode) {
1121
        let func = this.findAncestor<FunctionExpression>(isFunctionExpression);
8✔
1122
        let nameParts = [] as TranspileResult;
8✔
1123
        while (func.parentFunction) {
8✔
1124
            let index = func.parentFunction.childFunctionExpressions.indexOf(func);
4✔
1125
            nameParts.unshift(`anon${index}`);
4✔
1126
            func = func.parentFunction;
4✔
1127
        }
1128
        //get the index of this function in its parent
1129
        nameParts.unshift(
8✔
1130
            func.functionStatement!.getName(parseMode)
1131
        );
1132
        return nameParts.join('$');
8✔
1133
    }
1134

1135
    /**
1136
     * Get the line number from our token or from the closest ancestor that has a range
1137
     */
1138
    private getClosestLineNumber() {
1139
        let node: AstNode = this;
7✔
1140
        while (node) {
7✔
1141
            if (node.range) {
17✔
1142
                return node.range.start.line + 1;
5✔
1143
            }
1144
            node = node.parent;
12✔
1145
        }
1146
        return -1;
2✔
1147
    }
1148

1149
    transpile(state: BrsTranspileState) {
1150
        let text: string;
1151
        switch (this.token.kind) {
31✔
1152
            case TokenKind.SourceFilePathLiteral:
40!
1153
                const pathUrl = fileUrl(state.srcPath);
3✔
1154
                text = `"${pathUrl.substring(0, 4)}" + "${pathUrl.substring(4)}"`;
3✔
1155
                break;
3✔
1156
            case TokenKind.SourceLineNumLiteral:
1157
                //TODO find first parent that has range, or default to -1
1158
                text = `${this.getClosestLineNumber()}`;
4✔
1159
                break;
4✔
1160
            case TokenKind.FunctionNameLiteral:
1161
                text = `"${this.getFunctionName(state, ParseMode.BrightScript)}"`;
4✔
1162
                break;
4✔
1163
            case TokenKind.SourceFunctionNameLiteral:
1164
                text = `"${this.getFunctionName(state, ParseMode.BrighterScript)}"`;
4✔
1165
                break;
4✔
1166
            case TokenKind.SourceNamespaceNameLiteral:
NEW
1167
                let namespaceParts = this.getFunctionName(state, ParseMode.BrighterScript).split('.');
×
NEW
1168
                namespaceParts.pop(); // remove the function name
×
1169

NEW
1170
                text = `"${namespaceParts.join('.')}"`;
×
NEW
1171
                break;
×
1172
            case TokenKind.SourceNamespaceRootNameLiteral:
NEW
1173
                let namespaceRootParts = this.getFunctionName(state, ParseMode.BrighterScript).split('.');
×
NEW
1174
                namespaceRootParts.pop(); // remove the function name
×
1175

NEW
1176
                let rootNamespace = namespaceRootParts.shift() ?? '';
×
NEW
1177
                text = `"${rootNamespace}"`;
×
NEW
1178
                break;
×
1179
            case TokenKind.SourceLocationLiteral:
1180
                const locationUrl = fileUrl(state.srcPath);
3✔
1181
                //TODO find first parent that has range, or default to -1
1182
                text = `"${locationUrl.substring(0, 4)}" + "${locationUrl.substring(4)}:${this.getClosestLineNumber()}"`;
3✔
1183
                break;
3✔
1184
            case TokenKind.PkgPathLiteral:
1185
                let pkgPath1 = `pkg:/${state.file.pkgPath}`
2✔
1186
                    .replace(/\\/g, '/')
1187
                    .replace(/\.bs$/i, '.brs');
1188

1189
                text = `"${pkgPath1}"`;
2✔
1190
                break;
2✔
1191
            case TokenKind.PkgLocationLiteral:
1192
                let pkgPath2 = `pkg:/${state.file.pkgPath}`
2✔
1193
                    .replace(/\\/g, '/')
1194
                    .replace(/\.bs$/i, '.brs');
1195

1196
                text = `"${pkgPath2}:" + str(LINE_NUM)`;
2✔
1197
                break;
2✔
1198
            case TokenKind.LineNumLiteral:
1199
            default:
1200
                //use the original text (because it looks like a variable)
1201
                text = this.token.text;
9✔
1202
                break;
9✔
1203

1204
        }
1205
        return [
31✔
1206
            state.sourceNode(this, text)
1207
        ];
1208
    }
1209

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

1214
    public clone() {
1215
        return this.finalizeClone(
1✔
1216
            new SourceLiteralExpression(
1217
                util.cloneToken(this.token)
1218
            )
1219
        );
1220
    }
1221
}
1222

1223
/**
1224
 * This expression transpiles and acts exactly like a CallExpression,
1225
 * except we need to uniquely identify these statements so we can
1226
 * do more type checking.
1227
 */
1228
export class NewExpression extends Expression {
1✔
1229
    constructor(
1230
        readonly newKeyword: Token,
43✔
1231
        readonly call: CallExpression
43✔
1232
    ) {
1233
        super();
43✔
1234
        this.range = util.createBoundingRange(this.newKeyword, this.call);
43✔
1235
    }
1236

1237
    /**
1238
     * The name of the class to initialize (with optional namespace prefixed)
1239
     */
1240
    public get className() {
1241
        //the parser guarantees the callee of a new statement's call object will be
1242
        //a NamespacedVariableNameExpression
1243
        return this.call.callee as NamespacedVariableNameExpression;
104✔
1244
    }
1245

1246
    public readonly range: Range | undefined;
1247

1248
    public transpile(state: BrsTranspileState) {
1249
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
10✔
1250
        const cls = state.file.getClassFileLink(
10✔
1251
            this.className.getName(ParseMode.BrighterScript),
1252
            namespace?.getName(ParseMode.BrighterScript)
30✔
1253
        )?.item;
10✔
1254
        //new statements within a namespace block can omit the leading namespace if the class resides in that same namespace.
1255
        //So we need to figure out if this is a namespace-omitted class, or if this class exists without a namespace.
1256
        return this.call.transpile(state, cls?.getName(ParseMode.BrightScript));
10✔
1257
    }
1258

1259
    walk(visitor: WalkVisitor, options: WalkOptions) {
1260
        if (options.walkMode & InternalWalkMode.walkExpressions) {
150!
1261
            walk(this, 'call', visitor, options);
150✔
1262
        }
1263
    }
1264

1265
    public clone() {
1266
        return this.finalizeClone(
2✔
1267
            new NewExpression(
1268
                util.cloneToken(this.newKeyword),
1269
                this.call?.clone()
6✔
1270
            ),
1271
            ['call']
1272
        );
1273
    }
1274
}
1275

1276
export class CallfuncExpression extends Expression {
1✔
1277
    constructor(
1278
        readonly callee: Expression,
28✔
1279
        readonly operator: Token,
28✔
1280
        readonly methodName: Identifier,
28✔
1281
        readonly openingParen: Token,
28✔
1282
        readonly args: Expression[],
28✔
1283
        readonly closingParen: Token
28✔
1284
    ) {
1285
        super();
28✔
1286
        this.range = util.createBoundingRange(
28✔
1287
            callee,
1288
            operator,
1289
            methodName,
1290
            openingParen,
1291
            ...args ?? [],
84✔
1292
            closingParen
1293
        );
1294
    }
1295

1296
    public readonly range: Range | undefined;
1297

1298
    /**
1299
     * Get the name of the wrapping namespace (if it exists)
1300
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
1301
     */
1302
    public get namespaceName() {
UNCOV
1303
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
1304
    }
1305

1306
    public transpile(state: BrsTranspileState) {
1307
        let result = [] as TranspileResult;
8✔
1308
        result.push(
8✔
1309
            ...this.callee.transpile(state),
1310
            state.sourceNode(this.operator, '.callfunc'),
1311
            state.transpileToken(this.openingParen),
1312
            //the name of the function
1313
            state.sourceNode(this.methodName, ['"', this.methodName.text, '"']),
1314
            ', '
1315
        );
1316
        //transpile args
1317
        //callfunc with zero args never gets called, so pass invalid as the first parameter if there are no args
1318
        if (this.args.length === 0) {
8✔
1319
            result.push('invalid');
5✔
1320
        } else {
1321
            for (let i = 0; i < this.args.length; i++) {
3✔
1322
                //add comma between args
1323
                if (i > 0) {
6✔
1324
                    result.push(', ');
3✔
1325
                }
1326
                let arg = this.args[i];
6✔
1327
                result.push(...arg.transpile(state));
6✔
1328
            }
1329
        }
1330
        result.push(
8✔
1331
            state.transpileToken(this.closingParen)
1332
        );
1333
        return result;
8✔
1334
    }
1335

1336
    walk(visitor: WalkVisitor, options: WalkOptions) {
1337
        if (options.walkMode & InternalWalkMode.walkExpressions) {
51!
1338
            walk(this, 'callee', visitor, options);
51✔
1339
            walkArray(this.args, visitor, options, this);
51✔
1340
        }
1341
    }
1342

1343
    public clone() {
1344
        return this.finalizeClone(
3✔
1345
            new CallfuncExpression(
1346
                this.callee?.clone(),
9✔
1347
                util.cloneToken(this.operator),
1348
                util.cloneToken(this.methodName),
1349
                util.cloneToken(this.openingParen),
1350
                this.args?.map(e => e?.clone()),
2✔
1351
                util.cloneToken(this.closingParen)
1352
            ),
1353
            ['callee', 'args']
1354
        );
1355
    }
1356
}
1357

1358
/**
1359
 * Since template strings can contain newlines, we need to concatenate multiple strings together with chr() calls.
1360
 * This is a single expression that represents the string contatenation of all parts of a single quasi.
1361
 */
1362
export class TemplateStringQuasiExpression extends Expression {
1✔
1363
    constructor(
1364
        readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>
108✔
1365
    ) {
1366
        super();
108✔
1367
        this.range = util.createBoundingRange(
108✔
1368
            ...expressions ?? []
324✔
1369
        );
1370
    }
1371
    readonly range: Range | undefined;
1372

1373
    transpile(state: BrsTranspileState, skipEmptyStrings = true) {
35✔
1374
        let result = [] as TranspileResult;
43✔
1375
        let plus = '';
43✔
1376
        for (let expression of this.expressions) {
43✔
1377
            //skip empty strings
1378
            //TODO what does an empty string literal expression look like?
1379
            if (expression.token.text === '' && skipEmptyStrings === true) {
68✔
1380
                continue;
27✔
1381
            }
1382
            result.push(
41✔
1383
                plus,
1384
                ...expression.transpile(state)
1385
            );
1386
            plus = ' + ';
41✔
1387
        }
1388
        return result;
43✔
1389
    }
1390

1391
    walk(visitor: WalkVisitor, options: WalkOptions) {
1392
        if (options.walkMode & InternalWalkMode.walkExpressions) {
195!
1393
            walkArray(this.expressions, visitor, options, this);
195✔
1394
        }
1395
    }
1396

1397
    public clone() {
1398
        return this.finalizeClone(
15✔
1399
            new TemplateStringQuasiExpression(
1400
                this.expressions?.map(e => e?.clone())
20✔
1401
            ),
1402
            ['expressions']
1403
        );
1404
    }
1405
}
1406

1407
export class TemplateStringExpression extends Expression {
1✔
1408
    constructor(
1409
        readonly openingBacktick: Token,
49✔
1410
        readonly quasis: TemplateStringQuasiExpression[],
49✔
1411
        readonly expressions: Expression[],
49✔
1412
        readonly closingBacktick: Token
49✔
1413
    ) {
1414
        super();
49✔
1415
        this.range = util.createBoundingRange(
49✔
1416
            openingBacktick,
1417
            quasis?.[0],
147✔
1418
            quasis?.[quasis?.length - 1],
291!
1419
            closingBacktick
1420
        );
1421
    }
1422

1423
    public readonly range: Range | undefined;
1424

1425
    transpile(state: BrsTranspileState) {
1426
        if (this.quasis.length === 1 && this.expressions.length === 0) {
20✔
1427
            return this.quasis[0].transpile(state);
10✔
1428
        }
1429
        let result = ['('];
10✔
1430
        let plus = '';
10✔
1431
        //helper function to figure out when to include the plus
1432
        function add(...items) {
1433
            if (items.length > 0) {
40✔
1434
                result.push(
29✔
1435
                    plus,
1436
                    ...items
1437
                );
1438
            }
1439
            //set the plus after the first occurance of a nonzero length set of items
1440
            if (plus === '' && items.length > 0) {
40✔
1441
                plus = ' + ';
10✔
1442
            }
1443
        }
1444

1445
        for (let i = 0; i < this.quasis.length; i++) {
10✔
1446
            let quasi = this.quasis[i];
25✔
1447
            let expression = this.expressions[i];
25✔
1448

1449
            add(
25✔
1450
                ...quasi.transpile(state)
1451
            );
1452
            if (expression) {
25✔
1453
                //skip the toString wrapper around certain expressions
1454
                if (
15✔
1455
                    isEscapedCharCodeLiteralExpression(expression) ||
34✔
1456
                    (isLiteralExpression(expression) && isStringType(expression.type))
1457
                ) {
1458
                    add(
3✔
1459
                        ...expression.transpile(state)
1460
                    );
1461

1462
                    //wrap all other expressions with a bslib_toString call to prevent runtime type mismatch errors
1463
                } else {
1464
                    add(
12✔
1465
                        state.bslibPrefix + '_toString(',
1466
                        ...expression.transpile(state),
1467
                        ')'
1468
                    );
1469
                }
1470
            }
1471
        }
1472
        //the expression should be wrapped in parens so it can be used line a single expression at runtime
1473
        result.push(')');
10✔
1474

1475
        return result;
10✔
1476
    }
1477

1478
    walk(visitor: WalkVisitor, options: WalkOptions) {
1479
        if (options.walkMode & InternalWalkMode.walkExpressions) {
92!
1480
            //walk the quasis and expressions in left-to-right order
1481
            for (let i = 0; i < this.quasis?.length; i++) {
92✔
1482
                walk(this.quasis, i, visitor, options, this);
161✔
1483

1484
                //this skips the final loop iteration since we'll always have one more quasi than expression
1485
                if (this.expressions[i]) {
161✔
1486
                    walk(this.expressions, i, visitor, options, this);
69✔
1487
                }
1488
            }
1489
        }
1490
    }
1491

1492
    public clone() {
1493
        return this.finalizeClone(
7✔
1494
            new TemplateStringExpression(
1495
                util.cloneToken(this.openingBacktick),
1496
                this.quasis?.map(e => e?.clone()),
12✔
1497
                this.expressions?.map(e => e?.clone()),
6✔
1498
                util.cloneToken(this.closingBacktick)
1499
            ),
1500
            ['quasis', 'expressions']
1501
        );
1502
    }
1503
}
1504

1505
export class TaggedTemplateStringExpression extends Expression {
1✔
1506
    constructor(
1507
        readonly tagName: Identifier,
12✔
1508
        readonly openingBacktick: Token,
12✔
1509
        readonly quasis: TemplateStringQuasiExpression[],
12✔
1510
        readonly expressions: Expression[],
12✔
1511
        readonly closingBacktick: Token
12✔
1512
    ) {
1513
        super();
12✔
1514
        this.range = util.createBoundingRange(
12✔
1515
            tagName,
1516
            openingBacktick,
1517
            quasis?.[0],
36✔
1518
            quasis?.[quasis?.length - 1],
69!
1519
            closingBacktick
1520
        );
1521
    }
1522

1523
    public readonly range: Range | undefined;
1524

1525
    transpile(state: BrsTranspileState) {
1526
        let result = [] as TranspileResult;
3✔
1527
        result.push(
3✔
1528
            state.transpileToken(this.tagName),
1529
            '(['
1530
        );
1531

1532
        //add quasis as the first array
1533
        for (let i = 0; i < this.quasis.length; i++) {
3✔
1534
            let quasi = this.quasis[i];
8✔
1535
            //separate items with a comma
1536
            if (i > 0) {
8✔
1537
                result.push(
5✔
1538
                    ', '
1539
                );
1540
            }
1541
            result.push(
8✔
1542
                ...quasi.transpile(state, false)
1543
            );
1544
        }
1545
        result.push(
3✔
1546
            '], ['
1547
        );
1548

1549
        //add expressions as the second array
1550
        for (let i = 0; i < this.expressions.length; i++) {
3✔
1551
            let expression = this.expressions[i];
5✔
1552
            if (i > 0) {
5✔
1553
                result.push(
2✔
1554
                    ', '
1555
                );
1556
            }
1557
            result.push(
5✔
1558
                ...expression.transpile(state)
1559
            );
1560
        }
1561
        result.push(
3✔
1562
            state.sourceNode(this.closingBacktick, '])')
1563
        );
1564
        return result;
3✔
1565
    }
1566

1567
    walk(visitor: WalkVisitor, options: WalkOptions) {
1568
        if (options.walkMode & InternalWalkMode.walkExpressions) {
18!
1569
            //walk the quasis and expressions in left-to-right order
1570
            for (let i = 0; i < this.quasis?.length; i++) {
18✔
1571
                walk(this.quasis, i, visitor, options, this);
40✔
1572

1573
                //this skips the final loop iteration since we'll always have one more quasi than expression
1574
                if (this.expressions[i]) {
40✔
1575
                    walk(this.expressions, i, visitor, options, this);
22✔
1576
                }
1577
            }
1578
        }
1579
    }
1580

1581
    public clone() {
1582
        return this.finalizeClone(
3✔
1583
            new TaggedTemplateStringExpression(
1584
                util.cloneToken(this.tagName),
1585
                util.cloneToken(this.openingBacktick),
1586
                this.quasis?.map(e => e?.clone()),
5✔
1587
                this.expressions?.map(e => e?.clone()),
3✔
1588
                util.cloneToken(this.closingBacktick)
1589
            ),
1590
            ['quasis', 'expressions']
1591
        );
1592
    }
1593
}
1594

1595
export class AnnotationExpression extends Expression {
1✔
1596
    constructor(
1597
        readonly atToken: Token,
70✔
1598
        readonly nameToken: Token
70✔
1599
    ) {
1600
        super();
70✔
1601
        this.name = nameToken.text;
70✔
1602
    }
1603

1604
    public get range() {
1605
        return util.createBoundingRange(
32✔
1606
            this.atToken,
1607
            this.nameToken,
1608
            this.call
1609
        );
1610
    }
1611

1612
    public name: string;
1613
    public call: CallExpression | undefined;
1614

1615
    /**
1616
     * Convert annotation arguments to JavaScript types
1617
     * @param strict If false, keep Expression objects not corresponding to JS types
1618
     */
1619
    getArguments(strict = true): ExpressionValue[] {
10✔
1620
        if (!this.call) {
11✔
1621
            return [];
1✔
1622
        }
1623
        return this.call.args.map(e => expressionToValue(e, strict));
20✔
1624
    }
1625

1626
    transpile(state: BrsTranspileState) {
1627
        return [];
3✔
1628
    }
1629

1630
    walk(visitor: WalkVisitor, options: WalkOptions) {
1631
        //nothing to walk
1632
    }
1633
    getTypedef(state: BrsTranspileState) {
1634
        return [
9✔
1635
            '@',
1636
            this.name,
1637
            ...(this.call?.transpile(state) ?? [])
54✔
1638
        ];
1639
    }
1640

1641
    public clone() {
1642
        const clone = this.finalizeClone(
8✔
1643
            new AnnotationExpression(
1644
                util.cloneToken(this.atToken),
1645
                util.cloneToken(this.nameToken)
1646
            )
1647
        );
1648
        return clone;
8✔
1649
    }
1650
}
1651

1652
export class TernaryExpression extends Expression {
1✔
1653
    constructor(
1654
        readonly test: Expression,
93✔
1655
        readonly questionMarkToken: Token,
93✔
1656
        readonly consequent?: Expression,
93✔
1657
        readonly colonToken?: Token,
93✔
1658
        readonly alternate?: Expression
93✔
1659
    ) {
1660
        super();
93✔
1661
        this.range = util.createBoundingRange(
93✔
1662
            test,
1663
            questionMarkToken,
1664
            consequent,
1665
            colonToken,
1666
            alternate
1667
        );
1668
    }
1669

1670
    public range: Range | undefined;
1671

1672
    transpile(state: BrsTranspileState) {
1673
        let result = [] as TranspileResult;
15✔
1674
        const file = state.file;
15✔
1675
        let consequentInfo = util.getExpressionInfo(this.consequent!, file);
15✔
1676
        let alternateInfo = util.getExpressionInfo(this.alternate!, file);
15✔
1677

1678
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1679
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
15✔
1680
        let mutatingExpressions = [
15✔
1681
            ...consequentInfo.expressions,
1682
            ...alternateInfo.expressions
1683
        ].filter(e => e instanceof CallExpression || e instanceof CallfuncExpression || e instanceof DottedGetExpression);
78✔
1684

1685
        if (mutatingExpressions.length > 0) {
15✔
1686
            result.push(
8✔
1687
                state.sourceNode(
1688
                    this.questionMarkToken,
1689
                    //write all the scope variables as parameters.
1690
                    //TODO handle when there are more than 31 parameters
1691
                    `(function(${['__bsCondition', ...allUniqueVarNames].join(', ')})`
1692
                ),
1693
                state.newline,
1694
                //double indent so our `end function` line is still indented one at the end
1695
                state.indent(2),
1696
                state.sourceNode(this.test, `if __bsCondition then`),
1697
                state.newline,
1698
                state.indent(1),
1699
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
24!
1700
                ...this.consequent?.transpile(state) ?? [state.sourceNode(this.questionMarkToken, 'invalid')],
48!
1701
                state.newline,
1702
                state.indent(-1),
1703
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'else'),
24!
1704
                state.newline,
1705
                state.indent(1),
1706
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
24!
1707
                ...this.alternate?.transpile(state) ?? [state.sourceNode(this.consequent ?? this.questionMarkToken, 'invalid')],
48!
1708
                state.newline,
1709
                state.indent(-1),
1710
                state.sourceNode(this.questionMarkToken, 'end if'),
1711
                state.newline,
1712
                state.indent(-1),
1713
                state.sourceNode(this.questionMarkToken, 'end function)('),
1714
                ...this.test.transpile(state),
1715
                state.sourceNode(this.questionMarkToken, `${['', ...allUniqueVarNames].join(', ')})`)
1716
            );
1717
            state.blockDepth--;
8✔
1718
        } else {
1719
            result.push(
7✔
1720
                state.sourceNode(this.test, state.bslibPrefix + `_ternary(`),
1721
                ...this.test.transpile(state),
1722
                state.sourceNode(this.test, `, `),
1723
                ...this.consequent?.transpile(state) ?? ['invalid'],
42✔
1724
                `, `,
1725
                ...this.alternate?.transpile(state) ?? ['invalid'],
42✔
1726
                `)`
1727
            );
1728
        }
1729
        return result;
15✔
1730
    }
1731

1732
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1733
        if (options.walkMode & InternalWalkMode.walkExpressions) {
182!
1734
            walk(this, 'test', visitor, options);
182✔
1735
            walk(this, 'consequent', visitor, options);
182✔
1736
            walk(this, 'alternate', visitor, options);
182✔
1737
        }
1738
    }
1739

1740
    public clone() {
1741
        return this.finalizeClone(
2✔
1742
            new TernaryExpression(
1743
                this.test?.clone(),
6✔
1744
                util.cloneToken(this.questionMarkToken),
1745
                this.consequent?.clone(),
6✔
1746
                util.cloneToken(this.colonToken),
1747
                this.alternate?.clone()
6✔
1748
            ),
1749
            ['test', 'consequent', 'alternate']
1750
        );
1751
    }
1752
}
1753

1754
export class NullCoalescingExpression extends Expression {
1✔
1755
    constructor(
1756
        public consequent: Expression,
31✔
1757
        public questionQuestionToken: Token,
31✔
1758
        public alternate: Expression
31✔
1759
    ) {
1760
        super();
31✔
1761
        this.range = util.createBoundingRange(
31✔
1762
            consequent,
1763
            questionQuestionToken,
1764
            alternate
1765
        );
1766
    }
1767
    public readonly range: Range | undefined;
1768

1769
    transpile(state: BrsTranspileState) {
1770
        let result = [] as TranspileResult;
10✔
1771
        let consequentInfo = util.getExpressionInfo(this.consequent, state.file);
10✔
1772
        let alternateInfo = util.getExpressionInfo(this.alternate, state.file);
10✔
1773

1774
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1775
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
10✔
1776
        let hasMutatingExpression = [
10✔
1777
            ...consequentInfo.expressions,
1778
            ...alternateInfo.expressions
1779
        ].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e));
28✔
1780

1781
        if (hasMutatingExpression) {
10✔
1782
            result.push(
6✔
1783
                `(function(`,
1784
                //write all the scope variables as parameters.
1785
                //TODO handle when there are more than 31 parameters
1786
                allUniqueVarNames.join(', '),
1787
                ')',
1788
                state.newline,
1789
                //double indent so our `end function` line is still indented one at the end
1790
                state.indent(2),
1791
                //evaluate the consequent exactly once, and then use it in the following condition
1792
                `__bsConsequent = `,
1793
                ...this.consequent.transpile(state),
1794
                state.newline,
1795
                state.indent(),
1796
                `if __bsConsequent <> invalid then`,
1797
                state.newline,
1798
                state.indent(1),
1799
                'return __bsConsequent',
1800
                state.newline,
1801
                state.indent(-1),
1802
                'else',
1803
                state.newline,
1804
                state.indent(1),
1805
                'return ',
1806
                ...this.alternate.transpile(state),
1807
                state.newline,
1808
                state.indent(-1),
1809
                'end if',
1810
                state.newline,
1811
                state.indent(-1),
1812
                'end function)(',
1813
                allUniqueVarNames.join(', '),
1814
                ')'
1815
            );
1816
            state.blockDepth--;
6✔
1817
        } else {
1818
            result.push(
4✔
1819
                state.bslibPrefix + `_coalesce(`,
1820
                ...this.consequent.transpile(state),
1821
                ', ',
1822
                ...this.alternate.transpile(state),
1823
                ')'
1824
            );
1825
        }
1826
        return result;
10✔
1827
    }
1828

1829
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1830
        if (options.walkMode & InternalWalkMode.walkExpressions) {
50!
1831
            walk(this, 'consequent', visitor, options);
50✔
1832
            walk(this, 'alternate', visitor, options);
50✔
1833
        }
1834
    }
1835

1836
    public clone() {
1837
        return this.finalizeClone(
2✔
1838
            new NullCoalescingExpression(
1839
                this.consequent?.clone(),
6✔
1840
                util.cloneToken(this.questionQuestionToken),
1841
                this.alternate?.clone()
6✔
1842
            ),
1843
            ['consequent', 'alternate']
1844
        );
1845
    }
1846
}
1847

1848
export class RegexLiteralExpression extends Expression {
1✔
1849
    public constructor(
1850
        public tokens: {
46✔
1851
            regexLiteral: Token;
1852
        }
1853
    ) {
1854
        super();
46✔
1855
    }
1856

1857
    public get range() {
1858
        return this.tokens?.regexLiteral?.range;
55!
1859
    }
1860

1861
    public transpile(state: BrsTranspileState): TranspileResult {
1862
        let text = this.tokens.regexLiteral?.text ?? '';
42!
1863
        let flags = '';
42✔
1864
        //get any flags from the end
1865
        const flagMatch = /\/([a-z]+)$/i.exec(text);
42✔
1866
        if (flagMatch) {
42✔
1867
            text = text.substring(0, flagMatch.index + 1);
2✔
1868
            flags = flagMatch[1];
2✔
1869
        }
1870
        let pattern = text
42✔
1871
            //remove leading and trailing slashes
1872
            .substring(1, text.length - 1)
1873
            //escape quotemarks
1874
            .split('"').join('" + chr(34) + "');
1875

1876
        return [
42✔
1877
            state.sourceNode(this.tokens.regexLiteral, [
1878
                'CreateObject("roRegex", ',
1879
                `"${pattern}", `,
1880
                `"${flags}"`,
1881
                ')'
1882
            ])
1883
        ];
1884
    }
1885

1886
    walk(visitor: WalkVisitor, options: WalkOptions) {
1887
        //nothing to walk
1888
    }
1889

1890
    public clone() {
1891
        return this.finalizeClone(
1✔
1892
            new RegexLiteralExpression({
1893
                regexLiteral: util.cloneToken(this.tokens.regexLiteral)
1894
            })
1895
        );
1896
    }
1897
}
1898

1899

1900
export class TypeCastExpression extends Expression {
1✔
1901
    constructor(
1902
        public obj: Expression,
15✔
1903
        public asToken: Token,
15✔
1904
        public typeToken: Token
15✔
1905
    ) {
1906
        super();
15✔
1907
        this.range = util.createBoundingRange(
15✔
1908
            this.obj,
1909
            this.asToken,
1910
            this.typeToken
1911
        );
1912
    }
1913

1914
    public range: Range;
1915

1916
    public transpile(state: BrsTranspileState): TranspileResult {
1917
        return this.obj.transpile(state);
11✔
1918
    }
1919
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1920
        if (options.walkMode & InternalWalkMode.walkExpressions) {
36!
1921
            walk(this, 'obj', visitor, options);
36✔
1922
        }
1923
    }
1924

1925
    public clone() {
1926
        return this.finalizeClone(
2✔
1927
            new TypeCastExpression(
1928
                this.obj?.clone(),
6✔
1929
                util.cloneToken(this.asToken),
1930
                util.cloneToken(this.typeToken)
1931
            ),
1932
            ['obj']
1933
        );
1934
    }
1935
}
1936

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

1940
function expressionToValue(expr: Expression, strict: boolean): ExpressionValue {
1941
    if (!expr) {
30!
UNCOV
1942
        return null;
×
1943
    }
1944
    if (isUnaryExpression(expr) && isLiteralNumber(expr.right)) {
30✔
1945
        return numberExpressionToValue(expr.right, expr.operator.text);
1✔
1946
    }
1947
    if (isLiteralString(expr)) {
29✔
1948
        //remove leading and trailing quotes
1949
        return expr.token.text.replace(/^"/, '').replace(/"$/, '');
5✔
1950
    }
1951
    if (isLiteralNumber(expr)) {
24✔
1952
        return numberExpressionToValue(expr);
11✔
1953
    }
1954

1955
    if (isLiteralBoolean(expr)) {
13✔
1956
        return expr.token.text.toLowerCase() === 'true';
3✔
1957
    }
1958
    if (isArrayLiteralExpression(expr)) {
10✔
1959
        return expr.elements
3✔
1960
            .filter(e => !isCommentStatement(e))
7✔
1961
            .map(e => expressionToValue(e, strict));
7✔
1962
    }
1963
    if (isAALiteralExpression(expr)) {
7✔
1964
        return expr.elements.reduce((acc, e) => {
3✔
1965
            if (!isCommentStatement(e)) {
3!
1966
                acc[e.keyToken.text] = expressionToValue(e.value, strict);
3✔
1967
            }
1968
            return acc;
3✔
1969
        }, {});
1970
    }
1971
    //for annotations, we only support serializing pure string values
1972
    if (isTemplateStringExpression(expr)) {
4✔
1973
        if (expr.quasis?.length === 1 && expr.expressions.length === 0) {
2!
1974
            return expr.quasis[0].expressions.map(x => x.token.text).join('');
10✔
1975
        }
1976
    }
1977
    return strict ? null : expr;
2✔
1978
}
1979

1980
function numberExpressionToValue(expr: LiteralExpression, operator = '') {
11✔
1981
    if (isIntegerType(expr.type) || isLongIntegerType(expr.type)) {
12!
1982
        return parseInt(operator + expr.token.text);
12✔
1983
    } else {
UNCOV
1984
        return parseFloat(operator + expr.token.text);
×
1985
    }
1986
}
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