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

rokucommunity / brighterscript / #12837

24 Jul 2024 05:52PM UTC coverage: 87.936% (+2.3%) from 85.65%
#12837

push

TwitchBronBron
0.67.4

6069 of 7376 branches covered (82.28%)

Branch coverage included in aggregate %.

8793 of 9525 relevant lines covered (92.31%)

1741.63 hits per line

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

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

36
    public readonly range: Range | undefined;
37

38
    transpile(state: BrsTranspileState) {
39
        return [
156✔
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) {
532!
50
            walk(this, 'left', visitor, options);
532✔
51
            walk(this, 'right', visitor, options);
532✔
52
        }
53
    }
54
}
55

56
export class CallExpression extends Expression {
1✔
57
    /**
58
     * Number of parameters that can be defined on a function
59
     *
60
     * Prior to Roku OS 11.5, this was 32
61
     * As of Roku OS 11.5, this is 63
62
     */
63
    static MaximumArguments = 63;
1✔
64

65
    constructor(
66
        readonly callee: Expression,
580✔
67
        /**
68
         * Can either be `(`, or `?(` for optional chaining
69
         */
70
        readonly openingParen: Token,
580✔
71
        readonly closingParen: Token,
580✔
72
        readonly args: Expression[],
580✔
73
        unused?: any
74
    ) {
75
        super();
580✔
76
        this.range = util.createBoundingRange(this.callee, this.openingParen, ...args, this.closingParen);
580✔
77
    }
78

79
    public readonly range: Range | undefined;
80

81
    /**
82
     * Get the name of the wrapping namespace (if it exists)
83
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
84
     */
85
    public get namespaceName() {
86
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
87
    }
88

89
    transpile(state: BrsTranspileState, nameOverride?: string) {
90
        let result: TranspileResult = [];
222✔
91

92
        //transpile the name
93
        if (nameOverride) {
222✔
94
            result.push(state.sourceNode(this.callee, nameOverride));
9✔
95
        } else {
96
            result.push(...this.callee.transpile(state));
213✔
97
        }
98

99
        result.push(
222✔
100
            state.transpileToken(this.openingParen)
101
        );
102
        for (let i = 0; i < this.args.length; i++) {
222✔
103
            //add comma between args
104
            if (i > 0) {
114✔
105
                result.push(', ');
10✔
106
            }
107
            let arg = this.args[i];
114✔
108
            result.push(...arg.transpile(state));
114✔
109
        }
110
        if (this.closingParen) {
222!
111
            result.push(
222✔
112
                state.transpileToken(this.closingParen)
113
            );
114
        }
115
        return result;
222✔
116
    }
117

118
    walk(visitor: WalkVisitor, options: WalkOptions) {
119
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,296!
120
            walk(this, 'callee', visitor, options);
1,296✔
121
            walkArray(this.args, visitor, options, this);
1,296✔
122
        }
123
    }
124
}
125

126
export class FunctionExpression extends Expression implements TypedefProvider {
1✔
127
    constructor(
128
        readonly parameters: FunctionParameterExpression[],
1,520✔
129
        public body: Block,
1,520✔
130
        readonly functionType: Token | null,
1,520✔
131
        public end: Token,
1,520✔
132
        readonly leftParen: Token,
1,520✔
133
        readonly rightParen: Token,
1,520✔
134
        readonly asToken?: Token,
1,520✔
135
        readonly returnTypeToken?: Token
1,520✔
136
    ) {
137
        super();
1,520✔
138
        if (this.returnTypeToken) {
1,520✔
139
            this.returnType = util.tokenToBscType(this.returnTypeToken);
74✔
140
        } else if (this.functionType?.text.toLowerCase() === 'sub') {
1,446!
141
            this.returnType = new VoidType();
1,137✔
142
        } else {
143
            this.returnType = DynamicType.instance;
309✔
144
        }
145

146
        //if there's a body, and it doesn't have a SymbolTable, assign one
147
        if (this.body && !this.body.symbolTable) {
1,520✔
148
            this.body.symbolTable = new SymbolTable(`Function Body`);
37✔
149
        }
150
        this.symbolTable = new SymbolTable('FunctionExpression', () => this.parent?.getSymbolTable());
1,520!
151
    }
152

153
    /**
154
     * The type this function returns
155
     */
156
    public returnType: BscType;
157

158
    /**
159
     * Get the name of the wrapping namespace (if it exists)
160
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
161
     */
162
    public get namespaceName() {
163
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
164
    }
165

166
    /**
167
     * Get the name of the wrapping namespace (if it exists)
168
     * @deprecated use `.findAncestor(isFunctionExpression)` instead.
169
     */
170
    public get parentFunction() {
171
        return this.findAncestor<FunctionExpression>(isFunctionExpression);
941✔
172
    }
173

174
    /**
175
     * The list of function calls that are declared within this function scope. This excludes CallExpressions
176
     * declared in child functions
177
     */
178
    public callExpressions = [] as CallExpression[];
1,520✔
179

180
    /**
181
     * If this function is part of a FunctionStatement, this will be set. Otherwise this will be undefined
182
     */
183
    public functionStatement?: FunctionStatement;
184

185
    /**
186
     * A list of all child functions declared directly within this function
187
     * @deprecated use `.walk(createVisitor({ FunctionExpression: ()=>{}), { walkMode: WalkMode.visitAllRecursive })` instead
188
     */
189
    public get childFunctionExpressions() {
190
        const expressions = [] as FunctionExpression[];
4✔
191
        this.walk(createVisitor({
4✔
192
            FunctionExpression: (expression) => {
193
                expressions.push(expression);
6✔
194
            }
195
        }), {
196
            walkMode: WalkMode.visitAllRecursive
197
        });
198
        return expressions;
4✔
199
    }
200

201
    /**
202
     * The range of the function, starting at the 'f' in function or 's' in sub (or the open paren if the keyword is missing),
203
     * and ending with the last n' in 'end function' or 'b' in 'end sub'
204
     */
205
    public get range() {
206
        return util.createBoundingRange(
4,323✔
207
            this.functionType, this.leftParen,
208
            ...this.parameters,
209
            this.rightParen,
210
            this.asToken,
211
            this.returnTypeToken,
212
            this.end
213
        );
214
    }
215

216
    transpile(state: BrsTranspileState, name?: Identifier, includeBody = true) {
325✔
217
        let results = [] as TranspileResult;
325✔
218
        //'function'|'sub'
219
        results.push(
325✔
220
            state.transpileToken(this.functionType!)
221
        );
222
        //functionName?
223
        if (name) {
325✔
224
            results.push(
264✔
225
                ' ',
226
                state.transpileToken(name)
227
            );
228
        }
229
        //leftParen
230
        results.push(
325✔
231
            state.transpileToken(this.leftParen)
232
        );
233
        //parameters
234
        for (let i = 0; i < this.parameters.length; i++) {
325✔
235
            let param = this.parameters[i];
102✔
236
            //add commas
237
            if (i > 0) {
102✔
238
                results.push(', ');
46✔
239
            }
240
            //add parameter
241
            results.push(param.transpile(state));
102✔
242
        }
243
        //right paren
244
        results.push(
325✔
245
            state.transpileToken(this.rightParen)
246
        );
247
        //as [Type]
248
        if (this.asToken && !state.options.removeParameterTypes) {
325✔
249
            results.push(
34✔
250
                ' ',
251
                //as
252
                state.transpileToken(this.asToken),
253
                ' ',
254
                //return type
255
                state.sourceNode(this.returnTypeToken!, this.returnType.toTypeString())
256
            );
257
        }
258
        if (includeBody) {
325!
259
            state.lineage.unshift(this);
325✔
260
            let body = this.body.transpile(state);
325✔
261
            state.lineage.shift();
325✔
262
            results.push(...body);
325✔
263
        }
264
        results.push('\n');
325✔
265
        //'end sub'|'end function'
266
        results.push(
325✔
267
            state.indent(),
268
            state.transpileToken(this.end)
269
        );
270
        return results;
325✔
271
    }
272

273
    getTypedef(state: BrsTranspileState) {
274
        let results = [
29✔
275
            new SourceNode(1, 0, null, [
276
                //'function'|'sub'
277
                this.functionType?.text,
87!
278
                //functionName?
279
                ...(isFunctionStatement(this.parent) || isMethodStatement(this.parent) ? [' ', this.parent.name?.text ?? ''] : []),
255!
280
                //leftParen
281
                '(',
282
                //parameters
283
                ...(
284
                    this.parameters?.map((param, i) => ([
7!
285
                        //separating comma
286
                        i > 0 ? ', ' : '',
7!
287
                        ...param.getTypedef(state)
288
                    ])) ?? []
29!
289
                ) as any,
290
                //right paren
291
                ')',
292
                //as <ReturnType>
293
                ...(this.asToken ? [
29✔
294
                    ' as ',
295
                    this.returnTypeToken?.text
9!
296
                ] : []),
297
                '\n',
298
                state.indent(),
299
                //'end sub'|'end function'
300
                this.end.text
301
            ])
302
        ];
303
        return results;
29✔
304
    }
305

306
    walk(visitor: WalkVisitor, options: WalkOptions) {
307
        if (options.walkMode & InternalWalkMode.walkExpressions) {
2,698!
308
            walkArray(this.parameters, visitor, options, this);
2,698✔
309

310
            //This is the core of full-program walking...it allows us to step into sub functions
311
            if (options.walkMode & InternalWalkMode.recurseChildFunctions) {
2,698!
312
                walk(this, 'body', visitor, options);
2,698✔
313
            }
314
        }
315
    }
316

317
    getFunctionType(): FunctionType {
318
        let functionType = new FunctionType(this.returnType);
1,485✔
319
        functionType.isSub = this.functionType?.text === 'sub';
1,485!
320
        for (let param of this.parameters) {
1,485✔
321
            functionType.addParameter(param.name.text, param.type, !!param.typeToken);
588✔
322
        }
323
        return functionType;
1,485✔
324
    }
325
}
326

327
export class FunctionParameterExpression extends Expression {
1✔
328
    constructor(
329
        public name: Identifier,
606✔
330
        public typeToken?: Token,
606✔
331
        public defaultValue?: Expression,
606✔
332
        public asToken?: Token
606✔
333
    ) {
334
        super();
606✔
335
        if (typeToken) {
606✔
336
            this.type = util.tokenToBscType(typeToken);
268✔
337
        } else {
338
            this.type = new DynamicType();
338✔
339
        }
340
    }
341

342
    public type: BscType;
343

344
    public get range(): Range | undefined {
345
        return util.createBoundingRange(
420✔
346
            this.name,
347
            this.asToken,
348
            this.typeToken,
349
            this.defaultValue
350
        );
351
    }
352

353
    public transpile(state: BrsTranspileState) {
354
        let result = [
110✔
355
            //name
356
            state.transpileToken(this.name)
357
        ] as any[];
358
        //default value
359
        if (this.defaultValue) {
110✔
360
            result.push(' = ');
9✔
361
            result.push(this.defaultValue.transpile(state));
9✔
362
        }
363
        //type declaration
364
        if (this.asToken && !state.options.removeParameterTypes) {
110✔
365
            result.push(' ');
74✔
366
            result.push(state.transpileToken(this.asToken));
74✔
367
            result.push(' ');
74✔
368
            result.push(state.sourceNode(this.typeToken!, this.type.toTypeString()));
74✔
369
        }
370

371
        return result;
110✔
372
    }
373

374
    public getTypedef(state: BrsTranspileState): TranspileResult {
375
        const results = [this.name.text] as TranspileResult;
7✔
376

377
        if (this.defaultValue) {
7!
378
            results.push(' = ', ...this.defaultValue.transpile(state));
×
379
        }
380

381
        if (this.asToken) {
7✔
382
            results.push(' as ');
6✔
383

384
            // TODO: Is this conditional needed? Will typeToken always exist
385
            // so long as `asToken` exists?
386
            if (this.typeToken) {
6!
387
                results.push(this.typeToken.text);
6✔
388
            }
389
        }
390

391
        return results;
7✔
392
    }
393

394
    walk(visitor: WalkVisitor, options: WalkOptions) {
395
        // eslint-disable-next-line no-bitwise
396
        if (this.defaultValue && options.walkMode & InternalWalkMode.walkExpressions) {
1,390✔
397
            walk(this, 'defaultValue', visitor, options);
456✔
398
        }
399
    }
400
}
401

402
export class NamespacedVariableNameExpression extends Expression {
1✔
403
    constructor(
404
        //if this is a `DottedGetExpression`, it must be comprised only of `VariableExpression`s
405
        readonly expression: DottedGetExpression | VariableExpression
422✔
406
    ) {
407
        super();
422✔
408
        this.range = expression.range;
422✔
409
    }
410
    range: Range | undefined;
411

412
    transpile(state: BrsTranspileState) {
413
        return [
4✔
414
            state.sourceNode(this, this.getName(ParseMode.BrightScript))
415
        ];
416
    }
417

418
    public getNameParts() {
419
        let parts = [] as string[];
2,585✔
420
        if (isVariableExpression(this.expression)) {
2,585✔
421
            parts.push(this.expression.name.text);
1,816✔
422
        } else {
423
            let expr = this.expression;
769✔
424

425
            parts.push(expr.name.text);
769✔
426

427
            while (isVariableExpression(expr) === false) {
769✔
428
                expr = expr.obj as DottedGetExpression;
906✔
429
                parts.unshift(expr.name.text);
906✔
430
            }
431
        }
432
        return parts;
2,585✔
433
    }
434

435
    getName(parseMode: ParseMode) {
436
        if (parseMode === ParseMode.BrighterScript) {
2,537✔
437
            return this.getNameParts().join('.');
2,310✔
438
        } else {
439
            return this.getNameParts().join('_');
227✔
440
        }
441
    }
442

443
    walk(visitor: WalkVisitor, options: WalkOptions) {
444
        this.expression?.link();
726!
445
        if (options.walkMode & InternalWalkMode.walkExpressions) {
726✔
446
            walk(this, 'expression', visitor, options);
702✔
447
        }
448
    }
449
}
450

451
export class DottedGetExpression extends Expression {
1✔
452
    constructor(
453
        readonly obj: Expression,
1,036✔
454
        readonly name: Identifier,
1,036✔
455
        /**
456
         * Can either be `.`, or `?.` for optional chaining
457
         */
458
        readonly dot: Token
1,036✔
459
    ) {
460
        super();
1,036✔
461
        this.range = util.createBoundingRange(this.obj, this.dot, this.name);
1,036✔
462
    }
463

464
    public readonly range: Range | undefined;
465

466
    transpile(state: BrsTranspileState) {
467
        //if the callee starts with a namespace name, transpile the name
468
        if (state.file.calleeStartsWithNamespace(this)) {
236✔
469
            return new NamespacedVariableNameExpression(this as DottedGetExpression | VariableExpression).transpile(state);
3✔
470
        } else {
471
            return [
233✔
472
                ...this.obj.transpile(state),
473
                state.transpileToken(this.dot),
474
                state.transpileToken(this.name)
475
            ];
476
        }
477
    }
478

479
    walk(visitor: WalkVisitor, options: WalkOptions) {
480
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,815!
481
            walk(this, 'obj', visitor, options);
1,815✔
482
        }
483
    }
484

485
}
486

487
export class XmlAttributeGetExpression extends Expression {
1✔
488
    constructor(
489
        readonly obj: Expression,
10✔
490
        readonly name: Identifier,
10✔
491
        /**
492
         * Can either be `@`, or `?@` for optional chaining
493
         */
494
        readonly at: Token
10✔
495
    ) {
496
        super();
10✔
497
        this.range = util.createBoundingRange(this.obj, this.at, this.name);
10✔
498
    }
499

500
    public readonly range: Range | undefined;
501

502
    transpile(state: BrsTranspileState) {
503
        return [
3✔
504
            ...this.obj.transpile(state),
505
            state.transpileToken(this.at),
506
            state.transpileToken(this.name)
507
        ];
508
    }
509

510
    walk(visitor: WalkVisitor, options: WalkOptions) {
511
        if (options.walkMode & InternalWalkMode.walkExpressions) {
14!
512
            walk(this, 'obj', visitor, options);
14✔
513
        }
514
    }
515
}
516

517
export class IndexedGetExpression extends Expression {
1✔
518
    constructor(
519
        public obj: Expression,
127✔
520
        public index: Expression,
127✔
521
        /**
522
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.`
523
         */
524
        public openingSquare: Token,
127✔
525
        public closingSquare: Token,
127✔
526
        public questionDotToken?: Token, //  ? or ?.
127✔
527
        /**
528
         * More indexes, separated by commas
529
         */
530
        public additionalIndexes?: Expression[]
127✔
531
    ) {
532
        super();
127✔
533
        this.range = util.createBoundingRange(this.obj, this.openingSquare, this.questionDotToken, this.openingSquare, this.index, this.closingSquare);
127✔
534
        this.additionalIndexes ??= [];
127✔
535
    }
536

537
    public readonly range: Range | undefined;
538

539
    transpile(state: BrsTranspileState) {
540
        const result = [];
62✔
541
        result.push(
62✔
542
            ...this.obj.transpile(state),
543
            this.questionDotToken ? state.transpileToken(this.questionDotToken) : '',
62✔
544
            state.transpileToken(this.openingSquare)
545
        );
546
        const indexes = [this.index, ...this.additionalIndexes ?? []];
62!
547
        for (let i = 0; i < indexes.length; i++) {
62✔
548
            //add comma between indexes
549
            if (i > 0) {
70✔
550
                result.push(', ');
8✔
551
            }
552
            let index = indexes[i];
70✔
553
            result.push(
70✔
554
                ...(index?.transpile(state) ?? [])
420!
555
            );
556
        }
557
        result.push(
62✔
558
            this.closingSquare ? state.transpileToken(this.closingSquare) : ''
62!
559
        );
560
        return result;
62✔
561
    }
562

563
    walk(visitor: WalkVisitor, options: WalkOptions) {
564
        if (options.walkMode & InternalWalkMode.walkExpressions) {
178!
565
            walk(this, 'obj', visitor, options);
178✔
566
            walk(this, 'index', visitor, options);
178✔
567
            walkArray(this.additionalIndexes, visitor, options, this);
178✔
568
        }
569
    }
570
}
571

572
export class GroupingExpression extends Expression {
1✔
573
    constructor(
574
        readonly tokens: {
32✔
575
            left: Token;
576
            right: Token;
577
        },
578
        public expression: Expression
32✔
579
    ) {
580
        super();
32✔
581
        this.range = util.createBoundingRange(this.tokens.left, this.expression, this.tokens.right);
32✔
582
    }
583

584
    public readonly range: Range | undefined;
585

586
    transpile(state: BrsTranspileState) {
587
        if (isTypeCastExpression(this.expression)) {
12✔
588
            return this.expression.transpile(state);
7✔
589
        }
590
        return [
5✔
591
            state.transpileToken(this.tokens.left),
592
            ...this.expression.transpile(state),
593
            state.transpileToken(this.tokens.right)
594
        ];
595
    }
596

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

604
export class LiteralExpression extends Expression {
1✔
605
    constructor(
606
        public token: Token
2,950✔
607
    ) {
608
        super();
2,950✔
609
        this.type = util.tokenToBscType(token);
2,950✔
610
    }
611

612
    public get range() {
613
        return this.token.range;
6,977✔
614
    }
615

616
    /**
617
     * The (data) type of this expression
618
     */
619
    public type: BscType;
620

621
    transpile(state: BrsTranspileState) {
622
        let text: string;
623
        if (this.token.kind === TokenKind.TemplateStringQuasi) {
755✔
624
            //wrap quasis with quotes (and escape inner quotemarks)
625
            text = `"${this.token.text.replace(/"/g, '""')}"`;
28✔
626

627
        } else if (isStringType(this.type)) {
727✔
628
            text = this.token.text;
286✔
629
            //add trailing quotemark if it's missing. We will have already generated a diagnostic for this.
630
            if (text.endsWith('"') === false) {
286✔
631
                text += '"';
1✔
632
            }
633
        } else {
634
            text = this.token.text;
441✔
635
        }
636

637
        return [
755✔
638
            state.sourceNode(this, text)
639
        ];
640
    }
641

642
    walk(visitor: WalkVisitor, options: WalkOptions) {
643
        //nothing to walk
644
    }
645
}
646

647
/**
648
 * This is a special expression only used within template strings. It exists so we can prevent producing lots of empty strings
649
 * during template string transpile by identifying these expressions explicitly and skipping the bslib_toString around them
650
 */
651
export class EscapedCharCodeLiteralExpression extends Expression {
1✔
652
    constructor(
653
        readonly token: Token & { charCode: number }
25✔
654
    ) {
655
        super();
25✔
656
        this.range = token.range;
25✔
657
    }
658
    readonly range: Range;
659

660
    transpile(state: BrsTranspileState) {
661
        return [
13✔
662
            state.sourceNode(this, `chr(${this.token.charCode})`)
663
        ];
664
    }
665

666
    walk(visitor: WalkVisitor, options: WalkOptions) {
667
        //nothing to walk
668
    }
669
}
670

671
export class ArrayLiteralExpression extends Expression {
1✔
672
    constructor(
673
        readonly elements: Array<Expression | CommentStatement>,
111✔
674
        readonly open: Token,
111✔
675
        readonly close: Token,
111✔
676
        readonly hasSpread = false
111✔
677
    ) {
678
        super();
111✔
679
        this.range = util.createBoundingRange(this.open, ...this.elements, this.close);
111✔
680
    }
681

682
    public readonly range: Range | undefined;
683

684
    transpile(state: BrsTranspileState) {
685
        let result = [] as TranspileResult;
48✔
686
        result.push(
48✔
687
            state.transpileToken(this.open)
688
        );
689
        let hasChildren = this.elements.length > 0;
48✔
690
        state.blockDepth++;
48✔
691

692
        for (let i = 0; i < this.elements.length; i++) {
48✔
693
            let previousElement = this.elements[i - 1];
59✔
694
            let element = this.elements[i];
59✔
695

696
            if (isCommentStatement(element)) {
59✔
697
                //if the comment is on the same line as opening square or previous statement, don't add newline
698
                if (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element)) {
7✔
699
                    result.push(' ');
4✔
700
                } else {
701
                    result.push(
3✔
702
                        '\n',
703
                        state.indent()
704
                    );
705
                }
706
                state.lineage.unshift(this);
7✔
707
                result.push(element.transpile(state));
7✔
708
                state.lineage.shift();
7✔
709
            } else {
710
                result.push('\n');
52✔
711

712
                result.push(
52✔
713
                    state.indent(),
714
                    ...element.transpile(state)
715
                );
716
            }
717
        }
718
        state.blockDepth--;
48✔
719
        //add a newline between open and close if there are elements
720
        if (hasChildren) {
48✔
721
            result.push('\n');
26✔
722
            result.push(state.indent());
26✔
723
        }
724
        if (this.close) {
48!
725
            result.push(
48✔
726
                state.transpileToken(this.close)
727
            );
728
        }
729
        return result;
48✔
730
    }
731

732
    walk(visitor: WalkVisitor, options: WalkOptions) {
733
        if (options.walkMode & InternalWalkMode.walkExpressions) {
218!
734
            walkArray(this.elements, visitor, options, this);
218✔
735
        }
736
    }
737
}
738

739
export class AAMemberExpression extends Expression {
1✔
740
    constructor(
741
        public keyToken: Token,
184✔
742
        public colonToken: Token,
184✔
743
        /** The expression evaluated to determine the member's initial value. */
744
        public value: Expression
184✔
745
    ) {
746
        super();
184✔
747
        this.range = util.createBoundingRange(this.keyToken, this.colonToken, this.value);
184✔
748
    }
749

750
    public range: Range | undefined;
751
    public commaToken?: Token;
752

753
    transpile(state: BrsTranspileState) {
754
        //TODO move the logic from AALiteralExpression loop into this function
755
        return [];
×
756
    }
757

758
    walk(visitor: WalkVisitor, options: WalkOptions) {
759
        walk(this, 'value', visitor, options);
245✔
760
    }
761

762
}
763

764
export class AALiteralExpression extends Expression {
1✔
765
    constructor(
766
        readonly elements: Array<AAMemberExpression | CommentStatement>,
185✔
767
        readonly open: Token,
185✔
768
        readonly close: Token
185✔
769
    ) {
770
        super();
185✔
771
        this.range = util.createBoundingRange(this.open, ...this.elements, this.close);
185✔
772
    }
773

774
    public readonly range: Range | undefined;
775

776
    transpile(state: BrsTranspileState) {
777
        let result = [] as TranspileResult;
51✔
778
        //open curly
779
        result.push(
51✔
780
            state.transpileToken(this.open)
781
        );
782
        let hasChildren = this.elements.length > 0;
51✔
783
        //add newline if the object has children and the first child isn't a comment starting on the same line as opening curly
784
        if (hasChildren && (isCommentStatement(this.elements[0]) === false || !util.linesTouch(this.elements[0], this.open))) {
51✔
785
            result.push('\n');
21✔
786
        }
787
        state.blockDepth++;
51✔
788
        for (let i = 0; i < this.elements.length; i++) {
51✔
789
            let element = this.elements[i];
47✔
790
            let previousElement = this.elements[i - 1];
47✔
791
            let nextElement = this.elements[i + 1];
47✔
792

793
            //don't indent if comment is same-line
794
            if (isCommentStatement(element as any) &&
47✔
795
                (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element))
796
            ) {
797
                result.push(' ');
10✔
798

799
                //indent line
800
            } else {
801
                result.push(state.indent());
37✔
802
            }
803

804
            //render comments
805
            if (isCommentStatement(element)) {
47✔
806
                result.push(...element.transpile(state));
14✔
807
            } else {
808
                //key
809
                result.push(
33✔
810
                    state.transpileToken(element.keyToken)
811
                );
812
                //colon
813
                result.push(
33✔
814
                    state.transpileToken(element.colonToken),
815
                    ' '
816
                );
817

818
                //value
819
                result.push(...element.value.transpile(state));
33✔
820
            }
821

822

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

826
                //add a newline between statements
827
            } else {
828
                result.push('\n');
39✔
829
            }
830
        }
831
        state.blockDepth--;
51✔
832

833
        //only indent the closing curly if we have children
834
        if (hasChildren) {
51✔
835
            result.push(state.indent());
23✔
836
        }
837
        //close curly
838
        if (this.close) {
51!
839
            result.push(
51✔
840
                state.transpileToken(this.close)
841
            );
842
        }
843
        return result;
51✔
844
    }
845

846
    walk(visitor: WalkVisitor, options: WalkOptions) {
847
        if (options.walkMode & InternalWalkMode.walkExpressions) {
256!
848
            walkArray(this.elements, visitor, options, this);
256✔
849
        }
850
    }
851
}
852

853
export class UnaryExpression extends Expression {
1✔
854
    constructor(
855
        public operator: Token,
37✔
856
        public right: Expression
37✔
857
    ) {
858
        super();
37✔
859
        this.range = util.createBoundingRange(this.operator, this.right);
37✔
860
    }
861

862
    public readonly range: Range | undefined;
863

864
    transpile(state: BrsTranspileState) {
865
        let separatingWhitespace: string | undefined;
866
        if (isVariableExpression(this.right)) {
14✔
867
            separatingWhitespace = this.right.name.leadingWhitespace;
6✔
868
        } else if (isLiteralExpression(this.right)) {
8✔
869
            separatingWhitespace = this.right.token.leadingWhitespace;
3✔
870
        }
871

872
        return [
14✔
873
            state.transpileToken(this.operator),
874
            separatingWhitespace ?? ' ',
42✔
875
            ...this.right.transpile(state)
876
        ];
877
    }
878

879
    walk(visitor: WalkVisitor, options: WalkOptions) {
880
        if (options.walkMode & InternalWalkMode.walkExpressions) {
56!
881
            walk(this, 'right', visitor, options);
56✔
882
        }
883
    }
884
}
885

886
export class VariableExpression extends Expression {
1✔
887
    constructor(
888
        readonly name: Identifier
1,819✔
889
    ) {
890
        super();
1,819✔
891
        this.range = this.name?.range;
1,819!
892
    }
893

894
    public readonly range: Range;
895

896
    public getName(parseMode: ParseMode) {
897
        return this.name.text;
28✔
898
    }
899

900
    transpile(state: BrsTranspileState) {
901
        let result = [] as TranspileResult;
356✔
902
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
356✔
903
        //if the callee is the name of a known namespace function
904
        if (namespace && state.file.calleeIsKnownNamespaceFunction(this, namespace.getName(ParseMode.BrighterScript))) {
356✔
905
            result.push(
3✔
906
                state.sourceNode(this, [
907
                    namespace.getName(ParseMode.BrightScript),
908
                    '_',
909
                    this.getName(ParseMode.BrightScript)
910
                ])
911
            );
912

913
            //transpile  normally
914
        } else {
915
            result.push(
353✔
916
                state.transpileToken(this.name)
917
            );
918
        }
919
        return result;
356✔
920
    }
921

922
    walk(visitor: WalkVisitor, options: WalkOptions) {
923
        //nothing to walk
924
    }
925
}
926

927
export class SourceLiteralExpression extends Expression {
1✔
928
    constructor(
929
        readonly token: Token
35✔
930
    ) {
931
        super();
35✔
932
        this.range = token?.range;
35!
933
    }
934

935
    public readonly range: Range;
936

937
    private getFunctionName(state: BrsTranspileState, parseMode: ParseMode) {
938
        let func = this.findAncestor<FunctionExpression>(isFunctionExpression);
8✔
939
        let nameParts = [] as TranspileResult;
8✔
940
        while (func.parentFunction) {
8✔
941
            let index = func.parentFunction.childFunctionExpressions.indexOf(func);
4✔
942
            nameParts.unshift(`anon${index}`);
4✔
943
            func = func.parentFunction;
4✔
944
        }
945
        //get the index of this function in its parent
946
        nameParts.unshift(
8✔
947
            func.functionStatement!.getName(parseMode)
948
        );
949
        return nameParts.join('$');
8✔
950
    }
951

952
    /**
953
     * Get the line number from our token or from the closest ancestor that has a range
954
     */
955
    private getClosestLineNumber() {
956
        let node: AstNode = this;
7✔
957
        while (node) {
7✔
958
            if (node.range) {
17✔
959
                return node.range.start.line + 1;
5✔
960
            }
961
            node = node.parent;
12✔
962
        }
963
        return -1;
2✔
964
    }
965

966
    transpile(state: BrsTranspileState) {
967
        let text: string;
968
        switch (this.token.kind) {
31✔
969
            case TokenKind.SourceFilePathLiteral:
40✔
970
                const pathUrl = fileUrl(state.srcPath);
3✔
971
                text = `"${pathUrl.substring(0, 4)}" + "${pathUrl.substring(4)}"`;
3✔
972
                break;
3✔
973
            case TokenKind.SourceLineNumLiteral:
974
                //TODO find first parent that has range, or default to -1
975
                text = `${this.getClosestLineNumber()}`;
4✔
976
                break;
4✔
977
            case TokenKind.FunctionNameLiteral:
978
                text = `"${this.getFunctionName(state, ParseMode.BrightScript)}"`;
4✔
979
                break;
4✔
980
            case TokenKind.SourceFunctionNameLiteral:
981
                text = `"${this.getFunctionName(state, ParseMode.BrighterScript)}"`;
4✔
982
                break;
4✔
983
            case TokenKind.SourceLocationLiteral:
984
                const locationUrl = fileUrl(state.srcPath);
3✔
985
                //TODO find first parent that has range, or default to -1
986
                text = `"${locationUrl.substring(0, 4)}" + "${locationUrl.substring(4)}:${this.getClosestLineNumber()}"`;
3✔
987
                break;
3✔
988
            case TokenKind.PkgPathLiteral:
989
                let pkgPath1 = `pkg:/${state.file.pkgPath}`
2✔
990
                    .replace(/\\/g, '/')
991
                    .replace(/\.bs$/i, '.brs');
992

993
                text = `"${pkgPath1}"`;
2✔
994
                break;
2✔
995
            case TokenKind.PkgLocationLiteral:
996
                let pkgPath2 = `pkg:/${state.file.pkgPath}`
2✔
997
                    .replace(/\\/g, '/')
998
                    .replace(/\.bs$/i, '.brs');
999

1000
                text = `"${pkgPath2}:" + str(LINE_NUM)`;
2✔
1001
                break;
2✔
1002
            case TokenKind.LineNumLiteral:
1003
            default:
1004
                //use the original text (because it looks like a variable)
1005
                text = this.token.text;
9✔
1006
                break;
9✔
1007

1008
        }
1009
        return [
31✔
1010
            state.sourceNode(this, text)
1011
        ];
1012
    }
1013

1014
    walk(visitor: WalkVisitor, options: WalkOptions) {
1015
        //nothing to walk
1016
    }
1017
}
1018

1019
/**
1020
 * This expression transpiles and acts exactly like a CallExpression,
1021
 * except we need to uniquely identify these statements so we can
1022
 * do more type checking.
1023
 */
1024
export class NewExpression extends Expression {
1✔
1025
    constructor(
1026
        readonly newKeyword: Token,
38✔
1027
        readonly call: CallExpression
38✔
1028
    ) {
1029
        super();
38✔
1030
        this.range = util.createBoundingRange(this.newKeyword, this.call);
38✔
1031
    }
1032

1033
    /**
1034
     * The name of the class to initialize (with optional namespace prefixed)
1035
     */
1036
    public get className() {
1037
        //the parser guarantees the callee of a new statement's call object will be
1038
        //a NamespacedVariableNameExpression
1039
        return this.call.callee as NamespacedVariableNameExpression;
104✔
1040
    }
1041

1042
    public readonly range: Range | undefined;
1043

1044
    public transpile(state: BrsTranspileState) {
1045
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
10✔
1046
        const cls = state.file.getClassFileLink(
10✔
1047
            this.className.getName(ParseMode.BrighterScript),
1048
            namespace?.getName(ParseMode.BrighterScript)
30✔
1049
        )?.item;
10✔
1050
        //new statements within a namespace block can omit the leading namespace if the class resides in that same namespace.
1051
        //So we need to figure out if this is a namespace-omitted class, or if this class exists without a namespace.
1052
        return this.call.transpile(state, cls?.getName(ParseMode.BrightScript));
10✔
1053
    }
1054

1055
    walk(visitor: WalkVisitor, options: WalkOptions) {
1056
        if (options.walkMode & InternalWalkMode.walkExpressions) {
135!
1057
            walk(this, 'call', visitor, options);
135✔
1058
        }
1059
    }
1060
}
1061

1062
export class CallfuncExpression extends Expression {
1✔
1063
    constructor(
1064
        readonly callee: Expression,
22✔
1065
        readonly operator: Token,
22✔
1066
        readonly methodName: Identifier,
22✔
1067
        readonly openingParen: Token,
22✔
1068
        readonly args: Expression[],
22✔
1069
        readonly closingParen: Token
22✔
1070
    ) {
1071
        super();
22✔
1072
        this.range = util.createBoundingRange(
22✔
1073
            callee,
1074
            operator,
1075
            methodName,
1076
            openingParen,
1077
            ...args,
1078
            closingParen
1079
        );
1080
    }
1081

1082
    public readonly range: Range | undefined;
1083

1084
    /**
1085
     * Get the name of the wrapping namespace (if it exists)
1086
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
1087
     */
1088
    public get namespaceName() {
1089
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
1090
    }
1091

1092
    public transpile(state: BrsTranspileState) {
1093
        let result = [] as TranspileResult;
8✔
1094
        result.push(
8✔
1095
            ...this.callee.transpile(state),
1096
            state.sourceNode(this.operator, '.callfunc'),
1097
            state.transpileToken(this.openingParen),
1098
            //the name of the function
1099
            state.sourceNode(this.methodName, ['"', this.methodName.text, '"']),
1100
            ', '
1101
        );
1102
        //transpile args
1103
        //callfunc with zero args never gets called, so pass invalid as the first parameter if there are no args
1104
        if (this.args.length === 0) {
8✔
1105
            result.push('invalid');
5✔
1106
        } else {
1107
            for (let i = 0; i < this.args.length; i++) {
3✔
1108
                //add comma between args
1109
                if (i > 0) {
6✔
1110
                    result.push(', ');
3✔
1111
                }
1112
                let arg = this.args[i];
6✔
1113
                result.push(...arg.transpile(state));
6✔
1114
            }
1115
        }
1116
        result.push(
8✔
1117
            state.transpileToken(this.closingParen)
1118
        );
1119
        return result;
8✔
1120
    }
1121

1122
    walk(visitor: WalkVisitor, options: WalkOptions) {
1123
        if (options.walkMode & InternalWalkMode.walkExpressions) {
41!
1124
            walk(this, 'callee', visitor, options);
41✔
1125
            walkArray(this.args, visitor, options, this);
41✔
1126
        }
1127
    }
1128
}
1129

1130
/**
1131
 * Since template strings can contain newlines, we need to concatenate multiple strings together with chr() calls.
1132
 * This is a single expression that represents the string contatenation of all parts of a single quasi.
1133
 */
1134
export class TemplateStringQuasiExpression extends Expression {
1✔
1135
    constructor(
1136
        readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>
72✔
1137
    ) {
1138
        super();
72✔
1139
        this.range = util.createBoundingRange(
72✔
1140
            ...expressions
1141
        );
1142
    }
1143
    readonly range: Range | undefined;
1144

1145
    transpile(state: BrsTranspileState, skipEmptyStrings = true) {
35✔
1146
        let result = [] as TranspileResult;
43✔
1147
        let plus = '';
43✔
1148
        for (let expression of this.expressions) {
43✔
1149
            //skip empty strings
1150
            //TODO what does an empty string literal expression look like?
1151
            if (expression.token.text === '' && skipEmptyStrings === true) {
68✔
1152
                continue;
27✔
1153
            }
1154
            result.push(
41✔
1155
                plus,
1156
                ...expression.transpile(state)
1157
            );
1158
            plus = ' + ';
41✔
1159
        }
1160
        return result;
43✔
1161
    }
1162

1163
    walk(visitor: WalkVisitor, options: WalkOptions) {
1164
        if (options.walkMode & InternalWalkMode.walkExpressions) {
124!
1165
            walkArray(this.expressions, visitor, options, this);
124✔
1166
        }
1167
    }
1168
}
1169

1170
export class TemplateStringExpression extends Expression {
1✔
1171
    constructor(
1172
        readonly openingBacktick: Token,
33✔
1173
        readonly quasis: TemplateStringQuasiExpression[],
33✔
1174
        readonly expressions: Expression[],
33✔
1175
        readonly closingBacktick: Token
33✔
1176
    ) {
1177
        super();
33✔
1178
        this.range = util.createBoundingRange(
33✔
1179
            openingBacktick,
1180
            quasis[0],
1181
            quasis[quasis.length - 1],
1182
            closingBacktick
1183
        );
1184
    }
1185

1186
    public readonly range: Range | undefined;
1187

1188
    transpile(state: BrsTranspileState) {
1189
        if (this.quasis.length === 1 && this.expressions.length === 0) {
20✔
1190
            return this.quasis[0].transpile(state);
10✔
1191
        }
1192
        let result = ['('];
10✔
1193
        let plus = '';
10✔
1194
        //helper function to figure out when to include the plus
1195
        function add(...items) {
1196
            if (items.length > 0) {
40✔
1197
                result.push(
29✔
1198
                    plus,
1199
                    ...items
1200
                );
1201
            }
1202
            //set the plus after the first occurance of a nonzero length set of items
1203
            if (plus === '' && items.length > 0) {
40✔
1204
                plus = ' + ';
10✔
1205
            }
1206
        }
1207

1208
        for (let i = 0; i < this.quasis.length; i++) {
10✔
1209
            let quasi = this.quasis[i];
25✔
1210
            let expression = this.expressions[i];
25✔
1211

1212
            add(
25✔
1213
                ...quasi.transpile(state)
1214
            );
1215
            if (expression) {
25✔
1216
                //skip the toString wrapper around certain expressions
1217
                if (
15✔
1218
                    isEscapedCharCodeLiteralExpression(expression) ||
34✔
1219
                    (isLiteralExpression(expression) && isStringType(expression.type))
1220
                ) {
1221
                    add(
3✔
1222
                        ...expression.transpile(state)
1223
                    );
1224

1225
                    //wrap all other expressions with a bslib_toString call to prevent runtime type mismatch errors
1226
                } else {
1227
                    add(
12✔
1228
                        state.bslibPrefix + '_toString(',
1229
                        ...expression.transpile(state),
1230
                        ')'
1231
                    );
1232
                }
1233
            }
1234
        }
1235
        //the expression should be wrapped in parens so it can be used line a single expression at runtime
1236
        result.push(')');
10✔
1237

1238
        return result;
10✔
1239
    }
1240

1241
    walk(visitor: WalkVisitor, options: WalkOptions) {
1242
        if (options.walkMode & InternalWalkMode.walkExpressions) {
57!
1243
            //walk the quasis and expressions in left-to-right order
1244
            for (let i = 0; i < this.quasis.length; i++) {
57✔
1245
                walk(this.quasis, i, visitor, options, this);
100✔
1246

1247
                //this skips the final loop iteration since we'll always have one more quasi than expression
1248
                if (this.expressions[i]) {
100✔
1249
                    walk(this.expressions, i, visitor, options, this);
43✔
1250
                }
1251
            }
1252
        }
1253
    }
1254
}
1255

1256
export class TaggedTemplateStringExpression extends Expression {
1✔
1257
    constructor(
1258
        readonly tagName: Identifier,
6✔
1259
        readonly openingBacktick: Token,
6✔
1260
        readonly quasis: TemplateStringQuasiExpression[],
6✔
1261
        readonly expressions: Expression[],
6✔
1262
        readonly closingBacktick: Token
6✔
1263
    ) {
1264
        super();
6✔
1265
        this.range = util.createBoundingRange(
6✔
1266
            tagName,
1267
            openingBacktick,
1268
            quasis[0],
1269
            quasis[quasis.length - 1],
1270
            closingBacktick
1271
        );
1272
    }
1273

1274
    public readonly range: Range | undefined;
1275

1276
    transpile(state: BrsTranspileState) {
1277
        let result = [] as TranspileResult;
3✔
1278
        result.push(
3✔
1279
            state.transpileToken(this.tagName),
1280
            '(['
1281
        );
1282

1283
        //add quasis as the first array
1284
        for (let i = 0; i < this.quasis.length; i++) {
3✔
1285
            let quasi = this.quasis[i];
8✔
1286
            //separate items with a comma
1287
            if (i > 0) {
8✔
1288
                result.push(
5✔
1289
                    ', '
1290
                );
1291
            }
1292
            result.push(
8✔
1293
                ...quasi.transpile(state, false)
1294
            );
1295
        }
1296
        result.push(
3✔
1297
            '], ['
1298
        );
1299

1300
        //add expressions as the second array
1301
        for (let i = 0; i < this.expressions.length; i++) {
3✔
1302
            let expression = this.expressions[i];
5✔
1303
            if (i > 0) {
5✔
1304
                result.push(
2✔
1305
                    ', '
1306
                );
1307
            }
1308
            result.push(
5✔
1309
                ...expression.transpile(state)
1310
            );
1311
        }
1312
        result.push(
3✔
1313
            state.sourceNode(this.closingBacktick, '])')
1314
        );
1315
        return result;
3✔
1316
    }
1317

1318
    walk(visitor: WalkVisitor, options: WalkOptions) {
1319
        if (options.walkMode & InternalWalkMode.walkExpressions) {
10!
1320
            //walk the quasis and expressions in left-to-right order
1321
            for (let i = 0; i < this.quasis.length; i++) {
10✔
1322
                walk(this.quasis, i, visitor, options, this);
24✔
1323

1324
                //this skips the final loop iteration since we'll always have one more quasi than expression
1325
                if (this.expressions[i]) {
24✔
1326
                    walk(this.expressions, i, visitor, options, this);
14✔
1327
                }
1328
            }
1329
        }
1330
    }
1331
}
1332

1333
export class AnnotationExpression extends Expression {
1✔
1334
    constructor(
1335
        readonly atToken: Token,
47✔
1336
        readonly nameToken: Token
47✔
1337
    ) {
1338
        super();
47✔
1339
        this.name = nameToken.text;
47✔
1340
    }
1341

1342
    public get range() {
1343
        return util.createBoundingRange(
17✔
1344
            this.atToken,
1345
            this.nameToken,
1346
            this.call
1347
        );
1348
    }
1349

1350
    public name: string;
1351
    public call: CallExpression | undefined;
1352

1353
    /**
1354
     * Convert annotation arguments to JavaScript types
1355
     * @param strict If false, keep Expression objects not corresponding to JS types
1356
     */
1357
    getArguments(strict = true): ExpressionValue[] {
3✔
1358
        if (!this.call) {
4✔
1359
            return [];
1✔
1360
        }
1361
        return this.call.args.map(e => expressionToValue(e, strict));
13✔
1362
    }
1363

1364
    transpile(state: BrsTranspileState) {
1365
        return [];
3✔
1366
    }
1367

1368
    walk(visitor: WalkVisitor, options: WalkOptions) {
1369
        //nothing to walk
1370
    }
1371
    getTypedef(state: BrsTranspileState) {
1372
        return [
9✔
1373
            '@',
1374
            this.name,
1375
            ...(this.call?.transpile(state) ?? [])
54✔
1376
        ];
1377
    }
1378
}
1379

1380
export class TernaryExpression extends Expression {
1✔
1381
    constructor(
1382
        readonly test: Expression,
74✔
1383
        readonly questionMarkToken: Token,
74✔
1384
        readonly consequent?: Expression,
74✔
1385
        readonly colonToken?: Token,
74✔
1386
        readonly alternate?: Expression
74✔
1387
    ) {
1388
        super();
74✔
1389
        this.range = util.createBoundingRange(
74✔
1390
            test,
1391
            questionMarkToken,
1392
            consequent,
1393
            colonToken,
1394
            alternate
1395
        );
1396
    }
1397

1398
    public range: Range | undefined;
1399

1400
    transpile(state: BrsTranspileState) {
1401
        let result = [] as TranspileResult;
27✔
1402
        const file = state.file;
27✔
1403
        let consequentInfo = util.getExpressionInfo(this.consequent!, file);
27✔
1404
        let alternateInfo = util.getExpressionInfo(this.alternate!, file);
27✔
1405

1406
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1407
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
27✔
1408
        let mutatingExpressions = [
27✔
1409
            ...consequentInfo.expressions,
1410
            ...alternateInfo.expressions
1411
        ].filter(e => e instanceof CallExpression || e instanceof CallfuncExpression || e instanceof DottedGetExpression);
126✔
1412

1413
        if (mutatingExpressions.length > 0) {
27✔
1414
            result.push(
8✔
1415
                state.sourceNode(
1416
                    this.questionMarkToken,
1417
                    //write all the scope variables as parameters.
1418
                    //TODO handle when there are more than 31 parameters
1419
                    `(function(${['__bsCondition', ...allUniqueVarNames].join(', ')})`
1420
                ),
1421
                state.newline,
1422
                //double indent so our `end function` line is still indented one at the end
1423
                state.indent(2),
1424
                state.sourceNode(this.test, `if __bsCondition then`),
1425
                state.newline,
1426
                state.indent(1),
1427
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
24!
1428
                ...this.consequent?.transpile(state) ?? [state.sourceNode(this.questionMarkToken, 'invalid')],
48!
1429
                state.newline,
1430
                state.indent(-1),
1431
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'else'),
24!
1432
                state.newline,
1433
                state.indent(1),
1434
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
24!
1435
                ...this.alternate?.transpile(state) ?? [state.sourceNode(this.consequent ?? this.questionMarkToken, 'invalid')],
48!
1436
                state.newline,
1437
                state.indent(-1),
1438
                state.sourceNode(this.questionMarkToken, 'end if'),
1439
                state.newline,
1440
                state.indent(-1),
1441
                state.sourceNode(this.questionMarkToken, 'end function)('),
1442
                ...this.test.transpile(state),
1443
                state.sourceNode(this.questionMarkToken, `${['', ...allUniqueVarNames].join(', ')})`)
1444
            );
1445
            state.blockDepth--;
8✔
1446
        } else {
1447
            result.push(
19✔
1448
                state.sourceNode(this.test, state.bslibPrefix + `_ternary(`),
1449
                ...this.test.transpile(state),
1450
                state.sourceNode(this.test, `, `),
1451
                ...this.consequent?.transpile(state) ?? ['invalid'],
114✔
1452
                `, `,
1453
                ...this.alternate?.transpile(state) ?? ['invalid'],
114✔
1454
                `)`
1455
            );
1456
        }
1457
        return result;
27✔
1458
    }
1459

1460
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1461
        if (options.walkMode & InternalWalkMode.walkExpressions) {
103!
1462
            walk(this, 'test', visitor, options);
103✔
1463
            walk(this, 'consequent', visitor, options);
103✔
1464
            walk(this, 'alternate', visitor, options);
103✔
1465
        }
1466
    }
1467
}
1468

1469
export class NullCoalescingExpression extends Expression {
1✔
1470
    constructor(
1471
        public consequent: Expression,
27✔
1472
        public questionQuestionToken: Token,
27✔
1473
        public alternate: Expression
27✔
1474
    ) {
1475
        super();
27✔
1476
        this.range = util.createBoundingRange(
27✔
1477
            consequent,
1478
            questionQuestionToken,
1479
            alternate
1480
        );
1481
    }
1482
    public readonly range: Range | undefined;
1483

1484
    transpile(state: BrsTranspileState) {
1485
        let result = [] as TranspileResult;
10✔
1486
        let consequentInfo = util.getExpressionInfo(this.consequent, state.file);
10✔
1487
        let alternateInfo = util.getExpressionInfo(this.alternate, state.file);
10✔
1488

1489
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1490
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
10✔
1491
        let hasMutatingExpression = [
10✔
1492
            ...consequentInfo.expressions,
1493
            ...alternateInfo.expressions
1494
        ].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e));
28✔
1495

1496
        if (hasMutatingExpression) {
10✔
1497
            result.push(
6✔
1498
                `(function(`,
1499
                //write all the scope variables as parameters.
1500
                //TODO handle when there are more than 31 parameters
1501
                allUniqueVarNames.join(', '),
1502
                ')',
1503
                state.newline,
1504
                //double indent so our `end function` line is still indented one at the end
1505
                state.indent(2),
1506
                //evaluate the consequent exactly once, and then use it in the following condition
1507
                `__bsConsequent = `,
1508
                ...this.consequent.transpile(state),
1509
                state.newline,
1510
                state.indent(),
1511
                `if __bsConsequent <> invalid then`,
1512
                state.newline,
1513
                state.indent(1),
1514
                'return __bsConsequent',
1515
                state.newline,
1516
                state.indent(-1),
1517
                'else',
1518
                state.newline,
1519
                state.indent(1),
1520
                'return ',
1521
                ...this.alternate.transpile(state),
1522
                state.newline,
1523
                state.indent(-1),
1524
                'end if',
1525
                state.newline,
1526
                state.indent(-1),
1527
                'end function)(',
1528
                allUniqueVarNames.join(', '),
1529
                ')'
1530
            );
1531
            state.blockDepth--;
6✔
1532
        } else {
1533
            result.push(
4✔
1534
                state.bslibPrefix + `_coalesce(`,
1535
                ...this.consequent.transpile(state),
1536
                ', ',
1537
                ...this.alternate.transpile(state),
1538
                ')'
1539
            );
1540
        }
1541
        return result;
10✔
1542
    }
1543

1544
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1545
        if (options.walkMode & InternalWalkMode.walkExpressions) {
38!
1546
            walk(this, 'consequent', visitor, options);
38✔
1547
            walk(this, 'alternate', visitor, options);
38✔
1548
        }
1549
    }
1550
}
1551

1552
export class RegexLiteralExpression extends Expression {
1✔
1553
    public constructor(
1554
        public tokens: {
44✔
1555
            regexLiteral: Token;
1556
        }
1557
    ) {
1558
        super();
44✔
1559
    }
1560

1561
    public get range() {
1562
        return this.tokens?.regexLiteral?.range;
53!
1563
    }
1564

1565
    public transpile(state: BrsTranspileState): TranspileResult {
1566
        let text = this.tokens.regexLiteral?.text ?? '';
42!
1567
        let flags = '';
42✔
1568
        //get any flags from the end
1569
        const flagMatch = /\/([a-z]+)$/i.exec(text);
42✔
1570
        if (flagMatch) {
42✔
1571
            text = text.substring(0, flagMatch.index + 1);
2✔
1572
            flags = flagMatch[1];
2✔
1573
        }
1574
        let pattern = text
42✔
1575
            //remove leading and trailing slashes
1576
            .substring(1, text.length - 1)
1577
            //escape quotemarks
1578
            .split('"').join('" + chr(34) + "');
1579

1580
        return [
42✔
1581
            state.sourceNode(this.tokens.regexLiteral, [
1582
                'CreateObject("roRegex", ',
1583
                `"${pattern}", `,
1584
                `"${flags}"`,
1585
                ')'
1586
            ])
1587
        ];
1588
    }
1589

1590
    walk(visitor: WalkVisitor, options: WalkOptions) {
1591
        //nothing to walk
1592
    }
1593
}
1594

1595

1596
export class TypeCastExpression extends Expression {
1✔
1597
    constructor(
1598
        public obj: Expression,
11✔
1599
        public asToken: Token,
11✔
1600
        public typeToken: Token
11✔
1601
    ) {
1602
        super();
11✔
1603
        this.range = util.createBoundingRange(
11✔
1604
            this.obj,
1605
            this.asToken,
1606
            this.typeToken
1607
        );
1608
    }
1609

1610
    public range: Range;
1611

1612
    public transpile(state: BrsTranspileState): TranspileResult {
1613
        return this.obj.transpile(state);
11✔
1614
    }
1615
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1616
        if (options.walkMode & InternalWalkMode.walkExpressions) {
22!
1617
            walk(this, 'obj', visitor, options);
22✔
1618
        }
1619
    }
1620
}
1621

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

1625
function expressionToValue(expr: Expression, strict: boolean): ExpressionValue {
1626
    if (!expr) {
19!
1627
        return null;
×
1628
    }
1629
    if (isUnaryExpression(expr) && isLiteralNumber(expr.right)) {
19✔
1630
        return numberExpressionToValue(expr.right, expr.operator.text);
1✔
1631
    }
1632
    if (isLiteralString(expr)) {
18✔
1633
        //remove leading and trailing quotes
1634
        return expr.token.text.replace(/^"/, '').replace(/"$/, '');
4✔
1635
    }
1636
    if (isLiteralNumber(expr)) {
14✔
1637
        return numberExpressionToValue(expr);
6✔
1638
    }
1639

1640
    if (isLiteralBoolean(expr)) {
8✔
1641
        return expr.token.text.toLowerCase() === 'true';
2✔
1642
    }
1643
    if (isArrayLiteralExpression(expr)) {
6✔
1644
        return expr.elements
2✔
1645
            .filter(e => !isCommentStatement(e))
4✔
1646
            .map(e => expressionToValue(e, strict));
4✔
1647
    }
1648
    if (isAALiteralExpression(expr)) {
4✔
1649
        return expr.elements.reduce((acc, e) => {
2✔
1650
            if (!isCommentStatement(e)) {
2!
1651
                acc[e.keyToken.text] = expressionToValue(e.value, strict);
2✔
1652
            }
1653
            return acc;
2✔
1654
        }, {});
1655
    }
1656
    return strict ? null : expr;
2✔
1657
}
1658

1659
function numberExpressionToValue(expr: LiteralExpression, operator = '') {
6✔
1660
    if (isIntegerType(expr.type) || isLongIntegerType(expr.type)) {
7!
1661
        return parseInt(operator + expr.token.text);
7✔
1662
    } else {
1663
        return parseFloat(operator + expr.token.text);
×
1664
    }
1665
}
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