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

rokucommunity / brighterscript / #13787

31 Jan 2024 09:27PM UTC coverage: 88.178% (-0.2%) from 88.331%
#13787

push

TwitchBronBron
0.65.21

5869 of 7140 branches covered (82.2%)

Branch coverage included in aggregate %.

8698 of 9380 relevant lines covered (92.73%)

1683.86 hits per line

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

91.71
/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 { Expression } from './AstNode';
1✔
20
import { SymbolTable } from '../SymbolTable';
1✔
21
import { SourceNode } from 'source-map';
1✔
22

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

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

35
    public readonly range: Range;
36

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

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

55
export class CallExpression extends Expression {
1✔
56
    static MaximumArguments = 32;
1✔
57

58
    constructor(
59
        readonly callee: Expression,
569✔
60
        /**
61
         * Can either be `(`, or `?(` for optional chaining
62
         */
63
        readonly openingParen: Token,
569✔
64
        readonly closingParen: Token,
569✔
65
        readonly args: Expression[],
569✔
66
        unused?: any
67
    ) {
68
        super();
569✔
69
        this.range = util.createBoundingRange(this.callee, this.openingParen, ...args, this.closingParen);
569✔
70
    }
71

72
    public readonly range: Range;
73

74
    /**
75
     * Get the name of the wrapping namespace (if it exists)
76
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
77
     */
78
    public get namespaceName() {
79
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
80
    }
81

82
    transpile(state: BrsTranspileState, nameOverride?: string) {
83
        let result: Array<string | SourceNode> = [];
218✔
84

85
        //transpile the name
86
        if (nameOverride) {
218✔
87
            result.push(state.sourceNode(this.callee, nameOverride));
8✔
88
        } else {
89
            result.push(...this.callee.transpile(state));
210✔
90
        }
91

92
        result.push(
218✔
93
            state.transpileToken(this.openingParen)
94
        );
95
        for (let i = 0; i < this.args.length; i++) {
218✔
96
            //add comma between args
97
            if (i > 0) {
112✔
98
                result.push(', ');
10✔
99
            }
100
            let arg = this.args[i];
112✔
101
            result.push(...arg.transpile(state));
112✔
102
        }
103
        if (this.closingParen) {
218!
104
            result.push(
218✔
105
                state.transpileToken(this.closingParen)
106
            );
107
        }
108
        return result;
218✔
109
    }
110

111
    walk(visitor: WalkVisitor, options: WalkOptions) {
112
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,278!
113
            walk(this, 'callee', visitor, options);
1,278✔
114
            walkArray(this.args, visitor, options, this);
1,278✔
115
        }
116
    }
117
}
118

119
export class FunctionExpression extends Expression implements TypedefProvider {
1✔
120
    constructor(
121
        readonly parameters: FunctionParameterExpression[],
1,484✔
122
        public body: Block,
1,484✔
123
        readonly functionType: Token | null,
1,484✔
124
        public end: Token,
1,484✔
125
        readonly leftParen: Token,
1,484✔
126
        readonly rightParen: Token,
1,484✔
127
        readonly asToken?: Token,
1,484✔
128
        readonly returnTypeToken?: Token
1,484✔
129
    ) {
130
        super();
1,484✔
131
        if (this.returnTypeToken) {
1,484✔
132
            this.returnType = util.tokenToBscType(this.returnTypeToken);
73✔
133
        } else if (this.functionType?.text.toLowerCase() === 'sub') {
1,411!
134
            this.returnType = new VoidType();
1,112✔
135
        } else {
136
            this.returnType = DynamicType.instance;
299✔
137
        }
138

139
        //if there's a body, and it doesn't have a SymbolTable, assign one
140
        if (this.body && !this.body.symbolTable) {
1,484✔
141
            this.body.symbolTable = new SymbolTable(`Function Body`);
37✔
142
        }
143
        this.symbolTable = new SymbolTable('FunctionExpression', () => this.parent?.getSymbolTable());
1,484!
144
    }
145

146
    /**
147
     * The type this function returns
148
     */
149
    public returnType: BscType;
150

151
    /**
152
     * Get the name of the wrapping namespace (if it exists)
153
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
154
     */
155
    public get namespaceName() {
156
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
157
    }
158

159
    /**
160
     * Get the name of the wrapping namespace (if it exists)
161
     * @deprecated use `.findAncestor(isFunctionExpression)` instead.
162
     */
163
    public get parentFunction() {
164
        return this.findAncestor<FunctionExpression>(isFunctionExpression);
917✔
165
    }
166

167
    /**
168
     * The list of function calls that are declared within this function scope. This excludes CallExpressions
169
     * declared in child functions
170
     */
171
    public callExpressions = [] as CallExpression[];
1,484✔
172

173
    /**
174
     * If this function is part of a FunctionStatement, this will be set. Otherwise this will be undefined
175
     */
176
    public functionStatement?: FunctionStatement;
177

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

194
    /**
195
     * The range of the function, starting at the 'f' in function or 's' in sub (or the open paren if the keyword is missing),
196
     * and ending with the last n' in 'end function' or 'b' in 'end sub'
197
     */
198
    public get range() {
199
        return util.createBoundingRange(
4,190✔
200
            this.functionType, this.leftParen,
201
            ...this.parameters,
202
            this.rightParen,
203
            this.asToken,
204
            this.returnTypeToken,
205
            this.end
206
        );
207
    }
208

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

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

299
    walk(visitor: WalkVisitor, options: WalkOptions) {
300
        if (options.walkMode & InternalWalkMode.walkExpressions) {
2,600!
301
            walkArray(this.parameters, visitor, options, this);
2,600✔
302

303
            //This is the core of full-program walking...it allows us to step into sub functions
304
            if (options.walkMode & InternalWalkMode.recurseChildFunctions) {
2,600!
305
                walk(this, 'body', visitor, options);
2,600✔
306
            }
307
        }
308
    }
309

310
    getFunctionType(): FunctionType {
311
        let functionType = new FunctionType(this.returnType);
1,447✔
312
        functionType.isSub = this.functionType.text === 'sub';
1,447✔
313
        for (let param of this.parameters) {
1,447✔
314
            functionType.addParameter(param.name.text, param.type, !!param.typeToken);
388✔
315
        }
316
        return functionType;
1,447✔
317
    }
318
}
319

320
export class FunctionParameterExpression extends Expression {
1✔
321
    constructor(
322
        public name: Identifier,
405✔
323
        public typeToken?: Token,
405✔
324
        public defaultValue?: Expression,
405✔
325
        public asToken?: Token
405✔
326
    ) {
327
        super();
405✔
328
        if (typeToken) {
405✔
329
            this.type = util.tokenToBscType(typeToken);
264✔
330
        } else {
331
            this.type = new DynamicType();
141✔
332
        }
333
    }
334

335
    public type: BscType;
336

337
    public get range(): Range {
338
        return util.createBoundingRange(
280✔
339
            this.name,
340
            this.asToken,
341
            this.typeToken,
342
            this.defaultValue
343
        );
344
    }
345

346
    public transpile(state: BrsTranspileState) {
347
        let result = [
103✔
348
            //name
349
            state.transpileToken(this.name)
350
        ] as any[];
351
        //default value
352
        if (this.defaultValue) {
103✔
353
            result.push(' = ');
8✔
354
            result.push(this.defaultValue.transpile(state));
8✔
355
        }
356
        //type declaration
357
        if (this.asToken && !state.options.removeParameterTypes) {
103✔
358
            result.push(' ');
72✔
359
            result.push(state.transpileToken(this.asToken));
72✔
360
            result.push(' ');
72✔
361
            result.push(state.sourceNode(this.typeToken, this.type.toTypeString()));
72✔
362
        }
363

364
        return result;
103✔
365
    }
366

367
    public getTypedef(state: BrsTranspileState): TranspileResult {
368
        return [
7✔
369
            //name
370
            this.name.text,
371
            //default value
372
            ...(this.defaultValue ? [
7!
373
                ' = ',
374
                ...this.defaultValue.transpile(state)
375
            ] : []),
376
            //type declaration
377
            ...(this.asToken ? [
7✔
378
                ' as ',
379
                this.typeToken?.text
18!
380
            ] : [])
381
        ];
382
    }
383

384
    walk(visitor: WalkVisitor, options: WalkOptions) {
385
        // eslint-disable-next-line no-bitwise
386
        if (this.defaultValue && options.walkMode & InternalWalkMode.walkExpressions) {
990✔
387
            walk(this, 'defaultValue', visitor, options);
70✔
388
        }
389
    }
390
}
391

392
export class NamespacedVariableNameExpression extends Expression {
1✔
393
    constructor(
394
        //if this is a `DottedGetExpression`, it must be comprised only of `VariableExpression`s
395
        readonly expression: DottedGetExpression | VariableExpression
394✔
396
    ) {
397
        super();
394✔
398
        this.range = expression.range;
394✔
399
    }
400
    range: Range;
401

402
    transpile(state: BrsTranspileState) {
403
        return [
4✔
404
            state.sourceNode(this, this.getName(ParseMode.BrightScript))
405
        ];
406
    }
407

408
    public getNameParts() {
409
        let parts = [] as string[];
2,538✔
410
        if (isVariableExpression(this.expression)) {
2,538✔
411
            parts.push(this.expression.name.text);
1,769✔
412
        } else {
413
            let expr = this.expression;
769✔
414

415
            parts.push(expr.name.text);
769✔
416

417
            while (isVariableExpression(expr) === false) {
769✔
418
                expr = expr.obj as DottedGetExpression;
906✔
419
                parts.unshift(expr.name.text);
906✔
420
            }
421
        }
422
        return parts;
2,538✔
423
    }
424

425
    getName(parseMode: ParseMode) {
426
        if (parseMode === ParseMode.BrighterScript) {
2,512✔
427
            return this.getNameParts().join('.');
2,289✔
428
        } else {
429
            return this.getNameParts().join('_');
223✔
430
        }
431
    }
432

433
    walk(visitor: WalkVisitor, options: WalkOptions) {
434
        this.expression?.link();
682!
435
        if (options.walkMode & InternalWalkMode.walkExpressions) {
682✔
436
            walk(this, 'expression', visitor, options);
658✔
437
        }
438
    }
439
}
440

441
export class DottedGetExpression extends Expression {
1✔
442
    constructor(
443
        readonly obj: Expression,
1,002✔
444
        readonly name: Identifier,
1,002✔
445
        /**
446
         * Can either be `.`, or `?.` for optional chaining
447
         */
448
        readonly dot: Token
1,002✔
449
    ) {
450
        super();
1,002✔
451
        this.range = util.createBoundingRange(this.obj, this.dot, this.name);
1,002✔
452
    }
453

454
    public readonly range: Range;
455

456
    transpile(state: BrsTranspileState) {
457
        //if the callee starts with a namespace name, transpile the name
458
        if (state.file.calleeStartsWithNamespace(this)) {
223✔
459
            return new NamespacedVariableNameExpression(this as DottedGetExpression | VariableExpression).transpile(state);
3✔
460
        } else {
461
            return [
220✔
462
                ...this.obj.transpile(state),
463
                state.transpileToken(this.dot),
464
                state.transpileToken(this.name)
465
            ];
466
        }
467
    }
468

469
    walk(visitor: WalkVisitor, options: WalkOptions) {
470
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,740!
471
            walk(this, 'obj', visitor, options);
1,740✔
472
        }
473
    }
474

475
}
476

477
export class XmlAttributeGetExpression extends Expression {
1✔
478
    constructor(
479
        readonly obj: Expression,
9✔
480
        readonly name: Identifier,
9✔
481
        /**
482
         * Can either be `@`, or `?@` for optional chaining
483
         */
484
        readonly at: Token
9✔
485
    ) {
486
        super();
9✔
487
        this.range = util.createBoundingRange(this.obj, this.at, this.name);
9✔
488
    }
489

490
    public readonly range: Range;
491

492
    transpile(state: BrsTranspileState) {
493
        return [
2✔
494
            ...this.obj.transpile(state),
495
            state.transpileToken(this.at),
496
            state.transpileToken(this.name)
497
        ];
498
    }
499

500
    walk(visitor: WalkVisitor, options: WalkOptions) {
501
        if (options.walkMode & InternalWalkMode.walkExpressions) {
12!
502
            walk(this, 'obj', visitor, options);
12✔
503
        }
504
    }
505
}
506

507
export class IndexedGetExpression extends Expression {
1✔
508
    constructor(
509
        public obj: Expression,
122✔
510
        public index: Expression,
122✔
511
        /**
512
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.`
513
         */
514
        public openingSquare: Token,
122✔
515
        public closingSquare: Token,
122✔
516
        public questionDotToken?: Token, //  ? or ?.
122✔
517
        /**
518
         * More indexes, separated by commas
519
         */
520
        public additionalIndexes?: Expression[]
122✔
521
    ) {
522
        super();
122✔
523
        this.range = util.createBoundingRange(this.obj, this.openingSquare, this.questionDotToken, this.openingSquare, this.index, this.closingSquare);
122✔
524
        this.additionalIndexes ??= [];
122✔
525
    }
526

527
    public readonly range: Range;
528

529
    transpile(state: BrsTranspileState) {
530
        const result = [];
61✔
531
        result.push(
61✔
532
            ...this.obj.transpile(state),
533
            this.questionDotToken ? state.transpileToken(this.questionDotToken) : '',
61✔
534
            state.transpileToken(this.openingSquare)
535
        );
536
        const indexes = [this.index, ...this.additionalIndexes ?? []];
61!
537
        for (let i = 0; i < indexes.length; i++) {
61✔
538
            //add comma between indexes
539
            if (i > 0) {
69✔
540
                result.push(', ');
8✔
541
            }
542
            let index = indexes[i];
69✔
543
            result.push(
69✔
544
                ...(index?.transpile(state) ?? [])
414!
545
            );
546
        }
547
        result.push(
61✔
548
            this.closingSquare ? state.transpileToken(this.closingSquare) : ''
61!
549
        );
550
        return result;
61✔
551
    }
552

553
    walk(visitor: WalkVisitor, options: WalkOptions) {
554
        if (options.walkMode & InternalWalkMode.walkExpressions) {
171!
555
            walk(this, 'obj', visitor, options);
171✔
556
            walk(this, 'index', visitor, options);
171✔
557
            walkArray(this.additionalIndexes, visitor, options, this);
171✔
558
        }
559
    }
560
}
561

562
export class GroupingExpression extends Expression {
1✔
563
    constructor(
564
        readonly tokens: {
30✔
565
            left: Token;
566
            right: Token;
567
        },
568
        public expression: Expression
30✔
569
    ) {
570
        super();
30✔
571
        this.range = util.createBoundingRange(this.tokens.left, this.expression, this.tokens.right);
30✔
572
    }
573

574
    public readonly range: Range;
575

576
    transpile(state: BrsTranspileState) {
577
        if (isTypeCastExpression(this.expression)) {
10✔
578
            return this.expression.transpile(state);
6✔
579
        }
580
        return [
4✔
581
            state.transpileToken(this.tokens.left),
582
            ...this.expression.transpile(state),
583
            state.transpileToken(this.tokens.right)
584
        ];
585
    }
586

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

594
export class LiteralExpression extends Expression {
1✔
595
    constructor(
596
        public token: Token
2,652✔
597
    ) {
598
        super();
2,652✔
599
        this.type = util.tokenToBscType(token);
2,652✔
600
    }
601

602
    public get range() {
603
        return this.token.range;
6,397✔
604
    }
605

606
    /**
607
     * The (data) type of this expression
608
     */
609
    public type: BscType;
610

611
    transpile(state: BrsTranspileState) {
612
        let text: string;
613
        if (this.token.kind === TokenKind.TemplateStringQuasi) {
687✔
614
            //wrap quasis with quotes (and escape inner quotemarks)
615
            text = `"${this.token.text.replace(/"/g, '""')}"`;
24✔
616

617
        } else if (isStringType(this.type)) {
663✔
618
            text = this.token.text;
273✔
619
            //add trailing quotemark if it's missing. We will have already generated a diagnostic for this.
620
            if (text.endsWith('"') === false) {
273✔
621
                text += '"';
1✔
622
            }
623
        } else {
624
            text = this.token.text;
390✔
625
        }
626

627
        return [
687✔
628
            state.sourceNode(this, text)
629
        ];
630
    }
631

632
    walk(visitor: WalkVisitor, options: WalkOptions) {
633
        //nothing to walk
634
    }
635
}
636

637
/**
638
 * This is a special expression only used within template strings. It exists so we can prevent producing lots of empty strings
639
 * during template string transpile by identifying these expressions explicitly and skipping the bslib_toString around them
640
 */
641
export class EscapedCharCodeLiteralExpression extends Expression {
1✔
642
    constructor(
643
        readonly token: Token & { charCode: number }
24✔
644
    ) {
645
        super();
24✔
646
        this.range = token.range;
24✔
647
    }
648
    readonly range: Range;
649

650
    transpile(state: BrsTranspileState) {
651
        return [
12✔
652
            state.sourceNode(this, `chr(${this.token.charCode})`)
653
        ];
654
    }
655

656
    walk(visitor: WalkVisitor, options: WalkOptions) {
657
        //nothing to walk
658
    }
659
}
660

661
export class ArrayLiteralExpression extends Expression {
1✔
662
    constructor(
663
        readonly elements: Array<Expression | CommentStatement>,
109✔
664
        readonly open: Token,
109✔
665
        readonly close: Token,
109✔
666
        readonly hasSpread = false
109✔
667
    ) {
668
        super();
109✔
669
        this.range = util.createBoundingRange(this.open, ...this.elements, this.close);
109✔
670
    }
671

672
    public readonly range: Range;
673

674
    transpile(state: BrsTranspileState) {
675
        let result = [];
46✔
676
        result.push(
46✔
677
            state.transpileToken(this.open)
678
        );
679
        let hasChildren = this.elements.length > 0;
46✔
680
        state.blockDepth++;
46✔
681

682
        for (let i = 0; i < this.elements.length; i++) {
46✔
683
            let previousElement = this.elements[i - 1];
52✔
684
            let element = this.elements[i];
52✔
685

686
            if (isCommentStatement(element)) {
52✔
687
                //if the comment is on the same line as opening square or previous statement, don't add newline
688
                if (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element)) {
5✔
689
                    result.push(' ');
4✔
690
                } else {
691
                    result.push(
1✔
692
                        '\n',
693
                        state.indent()
694
                    );
695
                }
696
                state.lineage.unshift(this);
5✔
697
                result.push(element.transpile(state));
5✔
698
                state.lineage.shift();
5✔
699
            } else {
700
                result.push('\n');
47✔
701

702
                result.push(
47✔
703
                    state.indent(),
704
                    ...element.transpile(state)
705
                );
706
            }
707
        }
708
        state.blockDepth--;
46✔
709
        //add a newline between open and close if there are elements
710
        if (hasChildren) {
46✔
711
            result.push('\n');
24✔
712
            result.push(state.indent());
24✔
713
        }
714
        if (this.close) {
46!
715
            result.push(
46✔
716
                state.transpileToken(this.close)
717
            );
718
        }
719
        return result;
46✔
720
    }
721

722
    walk(visitor: WalkVisitor, options: WalkOptions) {
723
        if (options.walkMode & InternalWalkMode.walkExpressions) {
214!
724
            walkArray(this.elements, visitor, options, this);
214✔
725
        }
726
    }
727
}
728

729
export class AAMemberExpression extends Expression {
1✔
730
    constructor(
731
        public keyToken: Token,
182✔
732
        public colonToken: Token,
182✔
733
        /** The expression evaluated to determine the member's initial value. */
734
        public value: Expression
182✔
735
    ) {
736
        super();
182✔
737
        this.range = util.createBoundingRange(this.keyToken, this.colonToken, this.value);
182✔
738
    }
739

740
    public range: Range;
741
    public commaToken?: Token;
742

743
    transpile(state: BrsTranspileState) {
744
        //TODO move the logic from AALiteralExpression loop into this function
745
        return [];
×
746
    }
747

748
    walk(visitor: WalkVisitor, options: WalkOptions) {
749
        walk(this, 'value', visitor, options);
241✔
750
    }
751

752
}
753

754
export class AALiteralExpression extends Expression {
1✔
755
    constructor(
756
        readonly elements: Array<AAMemberExpression | CommentStatement>,
183✔
757
        readonly open: Token,
183✔
758
        readonly close: Token
183✔
759
    ) {
760
        super();
183✔
761
        this.range = util.createBoundingRange(this.open, ...this.elements, this.close);
183✔
762
    }
763

764
    public readonly range: Range;
765

766
    transpile(state: BrsTranspileState) {
767
        let result = [];
49✔
768
        //open curly
769
        result.push(
49✔
770
            state.transpileToken(this.open)
771
        );
772
        let hasChildren = this.elements.length > 0;
49✔
773
        //add newline if the object has children and the first child isn't a comment starting on the same line as opening curly
774
        if (hasChildren && (isCommentStatement(this.elements[0]) === false || !util.linesTouch(this.elements[0], this.open))) {
49✔
775
            result.push('\n');
20✔
776
        }
777
        state.blockDepth++;
49✔
778
        for (let i = 0; i < this.elements.length; i++) {
49✔
779
            let element = this.elements[i];
44✔
780
            let previousElement = this.elements[i - 1];
44✔
781
            let nextElement = this.elements[i + 1];
44✔
782

783
            //don't indent if comment is same-line
784
            if (isCommentStatement(element as any) &&
44✔
785
                (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element))
786
            ) {
787
                result.push(' ');
10✔
788

789
                //indent line
790
            } else {
791
                result.push(state.indent());
34✔
792
            }
793

794
            //render comments
795
            if (isCommentStatement(element)) {
44✔
796
                result.push(...element.transpile(state));
13✔
797
            } else {
798
                //key
799
                result.push(
31✔
800
                    state.transpileToken(element.keyToken)
801
                );
802
                //colon
803
                result.push(
31✔
804
                    state.transpileToken(element.colonToken),
805
                    ' '
806
                );
807

808
                //value
809
                result.push(...element.value.transpile(state));
31✔
810
            }
811

812

813
            //if next element is a same-line comment, skip the newline
814
            if (nextElement && isCommentStatement(nextElement) && nextElement.range.start.line === element.range.start.line) {
44✔
815

816
                //add a newline between statements
817
            } else {
818
                result.push('\n');
36✔
819
            }
820
        }
821
        state.blockDepth--;
49✔
822

823
        //only indent the closing curly if we have children
824
        if (hasChildren) {
49✔
825
            result.push(state.indent());
22✔
826
        }
827
        //close curly
828
        if (this.close) {
49!
829
            result.push(
49✔
830
                state.transpileToken(this.close)
831
            );
832
        }
833
        return result;
49✔
834
    }
835

836
    walk(visitor: WalkVisitor, options: WalkOptions) {
837
        if (options.walkMode & InternalWalkMode.walkExpressions) {
252!
838
            walkArray(this.elements, visitor, options, this);
252✔
839
        }
840
    }
841
}
842

843
export class UnaryExpression extends Expression {
1✔
844
    constructor(
845
        public operator: Token,
35✔
846
        public right: Expression
35✔
847
    ) {
848
        super();
35✔
849
        this.range = util.createBoundingRange(this.operator, this.right);
35✔
850
    }
851

852
    public readonly range: Range;
853

854
    transpile(state: BrsTranspileState) {
855
        let separatingWhitespace: string;
856
        if (isVariableExpression(this.right)) {
12✔
857
            separatingWhitespace = this.right.name.leadingWhitespace;
5✔
858
        } else if (isLiteralExpression(this.right)) {
7✔
859
            separatingWhitespace = this.right.token.leadingWhitespace;
2✔
860
        } else {
861
            separatingWhitespace = ' ';
5✔
862
        }
863
        return [
12✔
864
            state.transpileToken(this.operator),
865
            separatingWhitespace,
866
            ...this.right.transpile(state)
867
        ];
868
    }
869

870
    walk(visitor: WalkVisitor, options: WalkOptions) {
871
        if (options.walkMode & InternalWalkMode.walkExpressions) {
52!
872
            walk(this, 'right', visitor, options);
52✔
873
        }
874
    }
875
}
876

877
export class VariableExpression extends Expression {
1✔
878
    constructor(
879
        readonly name: Identifier
1,738✔
880
    ) {
881
        super();
1,738✔
882
        this.range = this.name?.range;
1,738!
883
    }
884

885
    public readonly range: Range;
886

887
    public getName(parseMode: ParseMode) {
888
        return this.name.text;
27✔
889
    }
890

891
    transpile(state: BrsTranspileState) {
892
        let result = [];
324✔
893
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
324✔
894
        //if the callee is the name of a known namespace function
895
        if (state.file.calleeIsKnownNamespaceFunction(this, namespace?.getName(ParseMode.BrighterScript))) {
324✔
896
            result.push(
3✔
897
                state.sourceNode(this, [
898
                    namespace.getName(ParseMode.BrightScript),
899
                    '_',
900
                    this.getName(ParseMode.BrightScript)
901
                ])
902
            );
903

904
            //transpile  normally
905
        } else {
906
            result.push(
321✔
907
                state.transpileToken(this.name)
908
            );
909
        }
910
        return result;
324✔
911
    }
912

913
    walk(visitor: WalkVisitor, options: WalkOptions) {
914
        //nothing to walk
915
    }
916
}
917

918
export class SourceLiteralExpression extends Expression {
1✔
919
    constructor(
920
        readonly token: Token
25✔
921
    ) {
922
        super();
25✔
923
        this.range = token?.range;
25!
924
    }
925

926
    public readonly range: Range;
927

928
    private getFunctionName(state: BrsTranspileState, parseMode: ParseMode) {
929
        let func = state.file.getFunctionScopeAtPosition(this.token.range.start).func;
6✔
930
        let nameParts = [];
6✔
931
        while (func.parentFunction) {
6✔
932
            let index = func.parentFunction.childFunctionExpressions.indexOf(func);
4✔
933
            nameParts.unshift(`anon${index}`);
4✔
934
            func = func.parentFunction;
4✔
935
        }
936
        //get the index of this function in its parent
937
        nameParts.unshift(
6✔
938
            func.functionStatement.getName(parseMode)
939
        );
940
        return nameParts.join('$');
6✔
941
    }
942

943
    transpile(state: BrsTranspileState) {
944
        let text: string;
945
        switch (this.token.kind) {
21✔
946
            case TokenKind.SourceFilePathLiteral:
27✔
947
                const pathUrl = fileUrl(state.srcPath);
2✔
948
                text = `"${pathUrl.substring(0, 4)}" + "${pathUrl.substring(4)}"`;
2✔
949
                break;
2✔
950
            case TokenKind.SourceLineNumLiteral:
951
                text = `${this.token.range.start.line + 1}`;
3✔
952
                break;
3✔
953
            case TokenKind.FunctionNameLiteral:
954
                text = `"${this.getFunctionName(state, ParseMode.BrightScript)}"`;
3✔
955
                break;
3✔
956
            case TokenKind.SourceFunctionNameLiteral:
957
                text = `"${this.getFunctionName(state, ParseMode.BrighterScript)}"`;
3✔
958
                break;
3✔
959
            case TokenKind.SourceLocationLiteral:
960
                const locationUrl = fileUrl(state.srcPath);
2✔
961
                text = `"${locationUrl.substring(0, 4)}" + "${locationUrl.substring(4)}:${this.token.range.start.line + 1}"`;
2✔
962
                break;
2✔
963
            case TokenKind.PkgPathLiteral:
964
                let pkgPath1 = `pkg:/${state.file.pkgPath}`
1✔
965
                    .replace(/\\/g, '/')
966
                    .replace(/\.bs$/i, '.brs');
967

968
                text = `"${pkgPath1}"`;
1✔
969
                break;
1✔
970
            case TokenKind.PkgLocationLiteral:
971
                let pkgPath2 = `pkg:/${state.file.pkgPath}`
1✔
972
                    .replace(/\\/g, '/')
973
                    .replace(/\.bs$/i, '.brs');
974

975
                text = `"${pkgPath2}:" + str(LINE_NUM)`;
1✔
976
                break;
1✔
977
            case TokenKind.LineNumLiteral:
978
            default:
979
                //use the original text (because it looks like a variable)
980
                text = this.token.text;
6✔
981
                break;
6✔
982

983
        }
984
        return [
21✔
985
            state.sourceNode(this, text)
986
        ];
987
    }
988

989
    walk(visitor: WalkVisitor, options: WalkOptions) {
990
        //nothing to walk
991
    }
992
}
993

994
/**
995
 * This expression transpiles and acts exactly like a CallExpression,
996
 * except we need to uniquely identify these statements so we can
997
 * do more type checking.
998
 */
999
export class NewExpression extends Expression {
1✔
1000
    constructor(
1001
        readonly newKeyword: Token,
37✔
1002
        readonly call: CallExpression
37✔
1003
    ) {
1004
        super();
37✔
1005
        this.range = util.createBoundingRange(this.newKeyword, this.call);
37✔
1006
    }
1007

1008
    /**
1009
     * The name of the class to initialize (with optional namespace prefixed)
1010
     */
1011
    public get className() {
1012
        //the parser guarantees the callee of a new statement's call object will be
1013
        //a NamespacedVariableNameExpression
1014
        return this.call.callee as NamespacedVariableNameExpression;
102✔
1015
    }
1016

1017
    public readonly range: Range;
1018

1019
    public transpile(state: BrsTranspileState) {
1020
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
9✔
1021
        const cls = state.file.getClassFileLink(
9✔
1022
            this.className.getName(ParseMode.BrighterScript),
1023
            namespace?.getName(ParseMode.BrighterScript)
27✔
1024
        )?.item;
9✔
1025
        //new statements within a namespace block can omit the leading namespace if the class resides in that same namespace.
1026
        //So we need to figure out if this is a namespace-omitted class, or if this class exists without a namespace.
1027
        return this.call.transpile(state, cls?.getName(ParseMode.BrightScript));
9✔
1028
    }
1029

1030
    walk(visitor: WalkVisitor, options: WalkOptions) {
1031
        if (options.walkMode & InternalWalkMode.walkExpressions) {
133!
1032
            walk(this, 'call', visitor, options);
133✔
1033
        }
1034
    }
1035
}
1036

1037
export class CallfuncExpression extends Expression {
1✔
1038
    constructor(
1039
        readonly callee: Expression,
20✔
1040
        readonly operator: Token,
20✔
1041
        readonly methodName: Identifier,
20✔
1042
        readonly openingParen: Token,
20✔
1043
        readonly args: Expression[],
20✔
1044
        readonly closingParen: Token
20✔
1045
    ) {
1046
        super();
20✔
1047
        this.range = util.createBoundingRange(
20✔
1048
            callee,
1049
            operator,
1050
            methodName,
1051
            openingParen,
1052
            ...args,
1053
            closingParen
1054
        );
1055
    }
1056

1057
    public readonly range: Range;
1058

1059
    /**
1060
     * Get the name of the wrapping namespace (if it exists)
1061
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
1062
     */
1063
    public get namespaceName() {
1064
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
1065
    }
1066

1067
    public transpile(state: BrsTranspileState) {
1068
        let result = [];
6✔
1069
        result.push(
6✔
1070
            ...this.callee.transpile(state),
1071
            state.sourceNode(this.operator, '.callfunc'),
1072
            state.transpileToken(this.openingParen),
1073
            //the name of the function
1074
            state.sourceNode(this.methodName, ['"', this.methodName.text, '"']),
1075
            ', '
1076
        );
1077
        //transpile args
1078
        //callfunc with zero args never gets called, so pass invalid as the first parameter if there are no args
1079
        if (this.args.length === 0) {
6✔
1080
            result.push('invalid');
4✔
1081
        } else {
1082
            for (let i = 0; i < this.args.length; i++) {
2✔
1083
                //add comma between args
1084
                if (i > 0) {
4✔
1085
                    result.push(', ');
2✔
1086
                }
1087
                let arg = this.args[i];
4✔
1088
                result.push(...arg.transpile(state));
4✔
1089
            }
1090
        }
1091
        result.push(
6✔
1092
            state.transpileToken(this.closingParen)
1093
        );
1094
        return result;
6✔
1095
    }
1096

1097
    walk(visitor: WalkVisitor, options: WalkOptions) {
1098
        if (options.walkMode & InternalWalkMode.walkExpressions) {
37!
1099
            walk(this, 'callee', visitor, options);
37✔
1100
            walkArray(this.args, visitor, options, this);
37✔
1101
        }
1102
    }
1103
}
1104

1105
/**
1106
 * Since template strings can contain newlines, we need to concatenate multiple strings together with chr() calls.
1107
 * This is a single expression that represents the string contatenation of all parts of a single quasi.
1108
 */
1109
export class TemplateStringQuasiExpression extends Expression {
1✔
1110
    constructor(
1111
        readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>
66✔
1112
    ) {
1113
        super();
66✔
1114
        this.range = util.createBoundingRange(
66✔
1115
            ...expressions
1116
        );
1117
    }
1118
    readonly range: Range;
1119

1120
    transpile(state: BrsTranspileState, skipEmptyStrings = true) {
32✔
1121
        let result = [];
37✔
1122
        let plus = '';
37✔
1123
        for (let expression of this.expressions) {
37✔
1124
            //skip empty strings
1125
            //TODO what does an empty string literal expression look like?
1126
            if (expression.token.text === '' && skipEmptyStrings === true) {
60✔
1127
                continue;
24✔
1128
            }
1129
            result.push(
36✔
1130
                plus,
1131
                ...expression.transpile(state)
1132
            );
1133
            plus = ' + ';
36✔
1134
        }
1135
        return result;
37✔
1136
    }
1137

1138
    walk(visitor: WalkVisitor, options: WalkOptions) {
1139
        if (options.walkMode & InternalWalkMode.walkExpressions) {
112!
1140
            walkArray(this.expressions, visitor, options, this);
112✔
1141
        }
1142
    }
1143
}
1144

1145
export class TemplateStringExpression extends Expression {
1✔
1146
    constructor(
1147
        readonly openingBacktick: Token,
32✔
1148
        readonly quasis: TemplateStringQuasiExpression[],
32✔
1149
        readonly expressions: Expression[],
32✔
1150
        readonly closingBacktick: Token
32✔
1151
    ) {
1152
        super();
32✔
1153
        this.range = util.createBoundingRange(
32✔
1154
            openingBacktick,
1155
            quasis[0],
1156
            quasis[quasis.length - 1],
1157
            closingBacktick
1158
        );
1159
    }
1160

1161
    public readonly range: Range;
1162

1163
    transpile(state: BrsTranspileState) {
1164
        if (this.quasis.length === 1 && this.expressions.length === 0) {
19✔
1165
            return this.quasis[0].transpile(state);
10✔
1166
        }
1167
        let result = ['('];
9✔
1168
        let plus = '';
9✔
1169
        //helper function to figure out when to include the plus
1170
        function add(...items) {
1171
            if (items.length > 0) {
35✔
1172
                result.push(
25✔
1173
                    plus,
1174
                    ...items
1175
                );
1176
            }
1177
            //set the plus after the first occurance of a nonzero length set of items
1178
            if (plus === '' && items.length > 0) {
35✔
1179
                plus = ' + ';
9✔
1180
            }
1181
        }
1182

1183
        for (let i = 0; i < this.quasis.length; i++) {
9✔
1184
            let quasi = this.quasis[i];
22✔
1185
            let expression = this.expressions[i];
22✔
1186

1187
            add(
22✔
1188
                ...quasi.transpile(state)
1189
            );
1190
            if (expression) {
22✔
1191
                //skip the toString wrapper around certain expressions
1192
                if (
13✔
1193
                    isEscapedCharCodeLiteralExpression(expression) ||
29✔
1194
                    (isLiteralExpression(expression) && isStringType(expression.type))
1195
                ) {
1196
                    add(
3✔
1197
                        ...expression.transpile(state)
1198
                    );
1199

1200
                    //wrap all other expressions with a bslib_toString call to prevent runtime type mismatch errors
1201
                } else {
1202
                    add(
10✔
1203
                        state.bslibPrefix + '_toString(',
1204
                        ...expression.transpile(state),
1205
                        ')'
1206
                    );
1207
                }
1208
            }
1209
        }
1210
        //the expression should be wrapped in parens so it can be used line a single expression at runtime
1211
        result.push(')');
9✔
1212

1213
        return result;
9✔
1214
    }
1215

1216
    walk(visitor: WalkVisitor, options: WalkOptions) {
1217
        if (options.walkMode & InternalWalkMode.walkExpressions) {
55!
1218
            //walk the quasis and expressions in left-to-right order
1219
            for (let i = 0; i < this.quasis.length; i++) {
55✔
1220
                walk(this.quasis, i, visitor, options, this);
94✔
1221

1222
                //this skips the final loop iteration since we'll always have one more quasi than expression
1223
                if (this.expressions[i]) {
94✔
1224
                    walk(this.expressions, i, visitor, options, this);
39✔
1225
                }
1226
            }
1227
        }
1228
    }
1229
}
1230

1231
export class TaggedTemplateStringExpression extends Expression {
1✔
1232
    constructor(
1233
        readonly tagName: Identifier,
5✔
1234
        readonly openingBacktick: Token,
5✔
1235
        readonly quasis: TemplateStringQuasiExpression[],
5✔
1236
        readonly expressions: Expression[],
5✔
1237
        readonly closingBacktick: Token
5✔
1238
    ) {
1239
        super();
5✔
1240
        this.range = util.createBoundingRange(
5✔
1241
            tagName,
1242
            openingBacktick,
1243
            quasis[0],
1244
            quasis[quasis.length - 1],
1245
            closingBacktick
1246
        );
1247
    }
1248

1249
    public readonly range: Range;
1250

1251
    transpile(state: BrsTranspileState) {
1252
        let result = [];
2✔
1253
        result.push(
2✔
1254
            state.transpileToken(this.tagName),
1255
            '(['
1256
        );
1257

1258
        //add quasis as the first array
1259
        for (let i = 0; i < this.quasis.length; i++) {
2✔
1260
            let quasi = this.quasis[i];
5✔
1261
            //separate items with a comma
1262
            if (i > 0) {
5✔
1263
                result.push(
3✔
1264
                    ', '
1265
                );
1266
            }
1267
            result.push(
5✔
1268
                ...quasi.transpile(state, false)
1269
            );
1270
        }
1271
        result.push(
2✔
1272
            '], ['
1273
        );
1274

1275
        //add expressions as the second array
1276
        for (let i = 0; i < this.expressions.length; i++) {
2✔
1277
            let expression = this.expressions[i];
3✔
1278
            if (i > 0) {
3✔
1279
                result.push(
1✔
1280
                    ', '
1281
                );
1282
            }
1283
            result.push(
3✔
1284
                ...expression.transpile(state)
1285
            );
1286
        }
1287
        result.push(
2✔
1288
            state.sourceNode(this.closingBacktick, '])')
1289
        );
1290
        return result;
2✔
1291
    }
1292

1293
    walk(visitor: WalkVisitor, options: WalkOptions) {
1294
        if (options.walkMode & InternalWalkMode.walkExpressions) {
8!
1295
            //walk the quasis and expressions in left-to-right order
1296
            for (let i = 0; i < this.quasis.length; i++) {
8✔
1297
                walk(this.quasis, i, visitor, options, this);
18✔
1298

1299
                //this skips the final loop iteration since we'll always have one more quasi than expression
1300
                if (this.expressions[i]) {
18✔
1301
                    walk(this.expressions, i, visitor, options, this);
10✔
1302
                }
1303
            }
1304
        }
1305
    }
1306
}
1307

1308
export class AnnotationExpression extends Expression {
1✔
1309
    constructor(
1310
        readonly atToken: Token,
46✔
1311
        readonly nameToken: Token
46✔
1312
    ) {
1313
        super();
46✔
1314
        this.name = nameToken.text;
46✔
1315
    }
1316

1317
    public get range() {
1318
        return util.createBoundingRange(
15✔
1319
            this.atToken,
1320
            this.nameToken,
1321
            this.call
1322
        );
1323
    }
1324

1325
    public name: string;
1326
    public call: CallExpression;
1327

1328
    /**
1329
     * Convert annotation arguments to JavaScript types
1330
     * @param strict If false, keep Expression objects not corresponding to JS types
1331
     */
1332
    getArguments(strict = true): ExpressionValue[] {
3✔
1333
        if (!this.call) {
4✔
1334
            return [];
1✔
1335
        }
1336
        return this.call.args.map(e => expressionToValue(e, strict));
13✔
1337
    }
1338

1339
    transpile(state: BrsTranspileState) {
1340
        return [];
3✔
1341
    }
1342

1343
    walk(visitor: WalkVisitor, options: WalkOptions) {
1344
        //nothing to walk
1345
    }
1346
    getTypedef(state: BrsTranspileState) {
1347
        return [
9✔
1348
            '@',
1349
            this.name,
1350
            ...(this.call?.transpile(state) ?? [])
54✔
1351
        ];
1352
    }
1353
}
1354

1355
export class TernaryExpression extends Expression {
1✔
1356
    constructor(
1357
        readonly test: Expression,
70✔
1358
        readonly questionMarkToken: Token,
70✔
1359
        readonly consequent?: Expression,
70✔
1360
        readonly colonToken?: Token,
70✔
1361
        readonly alternate?: Expression
70✔
1362
    ) {
1363
        super();
70✔
1364
        this.range = util.createBoundingRange(
70✔
1365
            test,
1366
            questionMarkToken,
1367
            consequent,
1368
            colonToken,
1369
            alternate
1370
        );
1371
    }
1372

1373
    public range: Range;
1374

1375
    transpile(state: BrsTranspileState) {
1376
        let result = [];
23✔
1377
        let consequentInfo = util.getExpressionInfo(this.consequent);
23✔
1378
        let alternateInfo = util.getExpressionInfo(this.alternate);
23✔
1379

1380
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1381
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
23✔
1382
        let mutatingExpressions = [
23✔
1383
            ...consequentInfo.expressions,
1384
            ...alternateInfo.expressions
1385
        ].filter(e => e instanceof CallExpression || e instanceof CallfuncExpression || e instanceof DottedGetExpression);
106✔
1386

1387
        if (mutatingExpressions.length > 0) {
23✔
1388
            result.push(
5✔
1389
                state.sourceNode(
1390
                    this.questionMarkToken,
1391
                    //write all the scope variables as parameters.
1392
                    //TODO handle when there are more than 31 parameters
1393
                    `(function(__bsCondition, ${allUniqueVarNames.join(', ')})`
1394
                ),
1395
                state.newline,
1396
                //double indent so our `end function` line is still indented one at the end
1397
                state.indent(2),
1398
                state.sourceNode(this.test, `if __bsCondition then`),
1399
                state.newline,
1400
                state.indent(1),
1401
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
15!
1402
                ...this.consequent?.transpile(state) ?? [state.sourceNode(this.questionMarkToken, 'invalid')],
30!
1403
                state.newline,
1404
                state.indent(-1),
1405
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'else'),
15!
1406
                state.newline,
1407
                state.indent(1),
1408
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
15!
1409
                ...this.alternate?.transpile(state) ?? [state.sourceNode(this.consequent ?? this.questionMarkToken, 'invalid')],
30!
1410
                state.newline,
1411
                state.indent(-1),
1412
                state.sourceNode(this.questionMarkToken, 'end if'),
1413
                state.newline,
1414
                state.indent(-1),
1415
                state.sourceNode(this.questionMarkToken, 'end function)('),
1416
                ...this.test.transpile(state),
1417
                state.sourceNode(this.questionMarkToken, `, ${allUniqueVarNames.join(', ')})`)
1418
            );
1419
            state.blockDepth--;
5✔
1420
        } else {
1421
            result.push(
18✔
1422
                state.sourceNode(this.test, state.bslibPrefix + `_ternary(`),
1423
                ...this.test.transpile(state),
1424
                state.sourceNode(this.test, `, `),
1425
                ...this.consequent?.transpile(state) ?? ['invalid'],
108✔
1426
                `, `,
1427
                ...this.alternate?.transpile(state) ?? ['invalid'],
108✔
1428
                `)`
1429
            );
1430
        }
1431
        return result;
23✔
1432
    }
1433

1434
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1435
        if (options.walkMode & InternalWalkMode.walkExpressions) {
95!
1436
            walk(this, 'test', visitor, options);
95✔
1437
            walk(this, 'consequent', visitor, options);
95✔
1438
            walk(this, 'alternate', visitor, options);
95✔
1439
        }
1440
    }
1441
}
1442

1443
export class NullCoalescingExpression extends Expression {
1✔
1444
    constructor(
1445
        public consequent: Expression,
23✔
1446
        public questionQuestionToken: Token,
23✔
1447
        public alternate: Expression
23✔
1448
    ) {
1449
        super();
23✔
1450
        this.range = util.createBoundingRange(
23✔
1451
            consequent,
1452
            questionQuestionToken,
1453
            alternate
1454
        );
1455
    }
1456
    public readonly range: Range;
1457

1458
    transpile(state: BrsTranspileState) {
1459
        let result = [];
6✔
1460
        let consequentInfo = util.getExpressionInfo(this.consequent);
6✔
1461
        let alternateInfo = util.getExpressionInfo(this.alternate);
6✔
1462

1463
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1464
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
6✔
1465
        let hasMutatingExpression = [
6✔
1466
            ...consequentInfo.expressions,
1467
            ...alternateInfo.expressions
1468
        ].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e));
19✔
1469

1470
        if (hasMutatingExpression) {
6✔
1471
            result.push(
3✔
1472
                `(function(`,
1473
                //write all the scope variables as parameters.
1474
                //TODO handle when there are more than 31 parameters
1475
                allUniqueVarNames.join(', '),
1476
                ')',
1477
                state.newline,
1478
                //double indent so our `end function` line is still indented one at the end
1479
                state.indent(2),
1480
                //evaluate the consequent exactly once, and then use it in the following condition
1481
                `__bsConsequent = `,
1482
                ...this.consequent.transpile(state),
1483
                state.newline,
1484
                state.indent(),
1485
                `if __bsConsequent <> invalid then`,
1486
                state.newline,
1487
                state.indent(1),
1488
                'return __bsConsequent',
1489
                state.newline,
1490
                state.indent(-1),
1491
                'else',
1492
                state.newline,
1493
                state.indent(1),
1494
                'return ',
1495
                ...this.alternate.transpile(state),
1496
                state.newline,
1497
                state.indent(-1),
1498
                'end if',
1499
                state.newline,
1500
                state.indent(-1),
1501
                'end function)(',
1502
                allUniqueVarNames.join(', '),
1503
                ')'
1504
            );
1505
            state.blockDepth--;
3✔
1506
        } else {
1507
            result.push(
3✔
1508
                state.bslibPrefix + `_coalesce(`,
1509
                ...this.consequent.transpile(state),
1510
                ', ',
1511
                ...this.alternate.transpile(state),
1512
                ')'
1513
            );
1514
        }
1515
        return result;
6✔
1516
    }
1517

1518
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1519
        if (options.walkMode & InternalWalkMode.walkExpressions) {
30!
1520
            walk(this, 'consequent', visitor, options);
30✔
1521
            walk(this, 'alternate', visitor, options);
30✔
1522
        }
1523
    }
1524
}
1525

1526
export class RegexLiteralExpression extends Expression {
1✔
1527
    public constructor(
1528
        public tokens: {
43✔
1529
            regexLiteral: Token;
1530
        }
1531
    ) {
1532
        super();
43✔
1533
    }
1534

1535
    public get range() {
1536
        return this.tokens?.regexLiteral?.range;
51!
1537
    }
1538

1539
    public transpile(state: BrsTranspileState): TranspileResult {
1540
        let text = this.tokens.regexLiteral?.text ?? '';
41!
1541
        let flags = '';
41✔
1542
        //get any flags from the end
1543
        const flagMatch = /\/([a-z]+)$/i.exec(text);
41✔
1544
        if (flagMatch) {
41✔
1545
            text = text.substring(0, flagMatch.index + 1);
1✔
1546
            flags = flagMatch[1];
1✔
1547
        }
1548
        let pattern = text
41✔
1549
            //remove leading and trailing slashes
1550
            .substring(1, text.length - 1)
1551
            //escape quotemarks
1552
            .split('"').join('" + chr(34) + "');
1553

1554
        return [
41✔
1555
            state.sourceNode(this.tokens.regexLiteral, [
1556
                'CreateObject("roRegex", ',
1557
                `"${pattern}", `,
1558
                `"${flags}"`,
1559
                ')'
1560
            ])
1561
        ];
1562
    }
1563

1564
    walk(visitor: WalkVisitor, options: WalkOptions) {
1565
        //nothing to walk
1566
    }
1567
}
1568

1569

1570
export class TypeCastExpression extends Expression {
1✔
1571
    constructor(
1572
        public obj: Expression,
10✔
1573
        public asToken: Token,
10✔
1574
        public typeToken: Token
10✔
1575
    ) {
1576
        super();
10✔
1577
        this.range = util.createBoundingRange(
10✔
1578
            this.obj,
1579
            this.asToken,
1580
            this.typeToken
1581
        );
1582
    }
1583

1584
    public range: Range;
1585

1586
    public transpile(state: BrsTranspileState): TranspileResult {
1587
        return this.obj.transpile(state);
10✔
1588
    }
1589
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1590
        if (options.walkMode & InternalWalkMode.walkExpressions) {
20!
1591
            walk(this, 'obj', visitor, options);
20✔
1592
        }
1593
    }
1594
}
1595

1596
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
1597
type ExpressionValue = string | number | boolean | Expression | ExpressionValue[] | { [key: string]: ExpressionValue };
1598

1599
function expressionToValue(expr: Expression, strict: boolean): ExpressionValue {
1600
    if (!expr) {
19!
1601
        return null;
×
1602
    }
1603
    if (isUnaryExpression(expr) && isLiteralNumber(expr.right)) {
19✔
1604
        return numberExpressionToValue(expr.right, expr.operator.text);
1✔
1605
    }
1606
    if (isLiteralString(expr)) {
18✔
1607
        //remove leading and trailing quotes
1608
        return expr.token.text.replace(/^"/, '').replace(/"$/, '');
4✔
1609
    }
1610
    if (isLiteralNumber(expr)) {
14✔
1611
        return numberExpressionToValue(expr);
6✔
1612
    }
1613

1614
    if (isLiteralBoolean(expr)) {
8✔
1615
        return expr.token.text.toLowerCase() === 'true';
2✔
1616
    }
1617
    if (isArrayLiteralExpression(expr)) {
6✔
1618
        return expr.elements
2✔
1619
            .filter(e => !isCommentStatement(e))
4✔
1620
            .map(e => expressionToValue(e, strict));
4✔
1621
    }
1622
    if (isAALiteralExpression(expr)) {
4✔
1623
        return expr.elements.reduce((acc, e) => {
2✔
1624
            if (!isCommentStatement(e)) {
2!
1625
                acc[e.keyToken.text] = expressionToValue(e.value, strict);
2✔
1626
            }
1627
            return acc;
2✔
1628
        }, {});
1629
    }
1630
    return strict ? null : expr;
2✔
1631
}
1632

1633
function numberExpressionToValue(expr: LiteralExpression, operator = '') {
6✔
1634
    if (isIntegerType(expr.type) || isLongIntegerType(expr.type)) {
7!
1635
        return parseInt(operator + expr.token.text);
7✔
1636
    } else {
1637
        return parseFloat(operator + expr.token.text);
×
1638
    }
1639
}
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