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

rokucommunity / brighterscript / #13768

16 Jan 2024 07:10PM UTC coverage: 88.101% (+0.05%) from 88.056%
#13768

push

TwitchBronBron
0.65.17

5759 of 7011 branches covered (82.14%)

Branch coverage included in aggregate %.

8612 of 9301 relevant lines covered (92.59%)

1666.09 hits per line

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

91.59
/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, 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,
562✔
60
        /**
61
         * Can either be `(`, or `?(` for optional chaining
62
         */
63
        readonly openingParen: Token,
562✔
64
        readonly closingParen: Token,
562✔
65
        readonly args: Expression[],
562✔
66
        unused?: any
67
    ) {
68
        super();
562✔
69
        this.range = util.createBoundingRange(this.callee, this.openingParen, ...args, this.closingParen);
562✔
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> = [];
213✔
84

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

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

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

119
export class FunctionExpression extends Expression implements TypedefProvider {
1✔
120
    constructor(
121
        readonly parameters: FunctionParameterExpression[],
1,466✔
122
        public body: Block,
1,466✔
123
        readonly functionType: Token | null,
1,466✔
124
        public end: Token,
1,466✔
125
        readonly leftParen: Token,
1,466✔
126
        readonly rightParen: Token,
1,466✔
127
        readonly asToken?: Token,
1,466✔
128
        readonly returnTypeToken?: Token
1,466✔
129
    ) {
130
        super();
1,466✔
131
        if (this.returnTypeToken) {
1,466✔
132
            this.returnType = util.tokenToBscType(this.returnTypeToken);
70✔
133
        } else if (this.functionType?.text.toLowerCase() === 'sub') {
1,396!
134
            this.returnType = new VoidType();
1,098✔
135
        } else {
136
            this.returnType = DynamicType.instance;
298✔
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,466✔
141
            this.body.symbolTable = new SymbolTable(`Function Body`);
37✔
142
        }
143
        this.symbolTable = new SymbolTable('FunctionExpression', () => this.parent?.getSymbolTable());
1,466!
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);
902✔
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,466✔
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,149✔
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) {
293✔
210
        let results = [];
293✔
211
        //'function'|'sub'
212
        results.push(
293✔
213
            state.transpileToken(this.functionType)
214
        );
215
        //functionName?
216
        if (name) {
293✔
217
            results.push(
234✔
218
                ' ',
219
                state.transpileToken(name)
220
            );
221
        }
222
        //leftParen
223
        results.push(
293✔
224
            state.transpileToken(this.leftParen)
225
        );
226
        //parameters
227
        for (let i = 0; i < this.parameters.length; i++) {
293✔
228
            let param = this.parameters[i];
77✔
229
            //add commas
230
            if (i > 0) {
77✔
231
                results.push(', ');
36✔
232
            }
233
            //add parameter
234
            results.push(param.transpile(state));
77✔
235
        }
236
        //right paren
237
        results.push(
293✔
238
            state.transpileToken(this.rightParen)
239
        );
240
        //as [Type]
241
        if (this.asToken && !state.options.removeParameterTypes) {
293✔
242
            results.push(
31✔
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) {
293!
252
            state.lineage.unshift(this);
293✔
253
            let body = this.body.transpile(state);
293✔
254
            state.lineage.shift();
293✔
255
            results.push(...body);
293✔
256
        }
257
        results.push('\n');
293✔
258
        //'end sub'|'end function'
259
        results.push(
293✔
260
            state.indent(),
261
            state.transpileToken(this.end)
262
        );
263
        return results;
293✔
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,565!
301
            walkArray(this.parameters, visitor, options, this);
2,565✔
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,565!
305
                walk(this, 'body', visitor, options);
2,565✔
306
            }
307
        }
308
    }
309

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

320
export class FunctionParameterExpression extends Expression {
1✔
321
    constructor(
322
        public name: Identifier,
387✔
323
        public typeToken?: Token,
387✔
324
        public defaultValue?: Expression,
387✔
325
        public asToken?: Token
387✔
326
    ) {
327
        super();
387✔
328
        if (typeToken) {
387✔
329
            this.type = util.tokenToBscType(typeToken);
249✔
330
        } else {
331
            this.type = new DynamicType();
138✔
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 = [
85✔
348
            //name
349
            state.transpileToken(this.name)
350
        ] as any[];
351
        //default value
352
        if (this.defaultValue) {
85✔
353
            result.push(' = ');
8✔
354
            result.push(this.defaultValue.transpile(state));
8✔
355
        }
356
        //type declaration
357
        if (this.asToken && !state.options.removeParameterTypes) {
85✔
358
            result.push(' ');
57✔
359
            result.push(state.transpileToken(this.asToken));
57✔
360
            result.push(' ');
57✔
361
            result.push(state.sourceNode(this.typeToken, this.type.toTypeString()));
57✔
362
        }
363

364
        return result;
85✔
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) {
954✔
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
369✔
396
    ) {
397
        super();
369✔
398
        this.range = expression.range;
369✔
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,504✔
410
        if (isVariableExpression(this.expression)) {
2,504✔
411
            parts.push(this.expression.name.text);
1,751✔
412
        } else {
413
            let expr = this.expression;
753✔
414

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

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

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

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

441
export class DottedGetExpression extends Expression {
1✔
442
    constructor(
443
        readonly obj: Expression,
982✔
444
        readonly name: Identifier,
982✔
445
        /**
446
         * Can either be `.`, or `?.` for optional chaining
447
         */
448
        readonly dot: Token
982✔
449
    ) {
450
        super();
982✔
451
        this.range = util.createBoundingRange(this.obj, this.dot, this.name);
982✔
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)) {
216✔
459
            return new NamespacedVariableNameExpression(this as DottedGetExpression | VariableExpression).transpile(state);
3✔
460
        } else {
461
            return [
213✔
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,714!
471
            walk(this, 'obj', visitor, options);
1,714✔
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,
110✔
510
        public index: Expression,
110✔
511
        /**
512
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.`
513
         */
514
        public openingSquare: Token,
110✔
515
        public closingSquare: Token,
110✔
516
        public questionDotToken?: Token //  ? or ?.
110✔
517
    ) {
518
        super();
110✔
519
        this.range = util.createBoundingRange(this.obj, this.openingSquare, this.questionDotToken, this.openingSquare, this.index, this.closingSquare);
110✔
520
    }
521

522
    public readonly range: Range;
523

524
    transpile(state: BrsTranspileState) {
525
        return [
52✔
526
            ...this.obj.transpile(state),
527
            this.questionDotToken ? state.transpileToken(this.questionDotToken) : '',
52✔
528
            state.transpileToken(this.openingSquare),
529
            ...(this.index?.transpile(state) ?? []),
312!
530
            this.closingSquare ? state.transpileToken(this.closingSquare) : ''
52!
531
        ];
532
    }
533

534
    walk(visitor: WalkVisitor, options: WalkOptions) {
535
        if (options.walkMode & InternalWalkMode.walkExpressions) {
151!
536
            walk(this, 'obj', visitor, options);
151✔
537
            walk(this, 'index', visitor, options);
151✔
538
        }
539
    }
540
}
541

542
export class GroupingExpression extends Expression {
1✔
543
    constructor(
544
        readonly tokens: {
24✔
545
            left: Token;
546
            right: Token;
547
        },
548
        public expression: Expression
24✔
549
    ) {
550
        super();
24✔
551
        this.range = util.createBoundingRange(this.tokens.left, this.expression, this.tokens.right);
24✔
552
    }
553

554
    public readonly range: Range;
555

556
    transpile(state: BrsTranspileState) {
557
        return [
4✔
558
            state.transpileToken(this.tokens.left),
559
            ...this.expression.transpile(state),
560
            state.transpileToken(this.tokens.right)
561
        ];
562
    }
563

564
    walk(visitor: WalkVisitor, options: WalkOptions) {
565
        if (options.walkMode & InternalWalkMode.walkExpressions) {
29!
566
            walk(this, 'expression', visitor, options);
29✔
567
        }
568
    }
569
}
570

571
export class LiteralExpression extends Expression {
1✔
572
    constructor(
573
        public token: Token
2,621✔
574
    ) {
575
        super();
2,621✔
576
        this.type = util.tokenToBscType(token);
2,621✔
577
    }
578

579
    public get range() {
580
        return this.token.range;
6,276✔
581
    }
582

583
    /**
584
     * The (data) type of this expression
585
     */
586
    public type: BscType;
587

588
    transpile(state: BrsTranspileState) {
589
        let text: string;
590
        if (this.token.kind === TokenKind.TemplateStringQuasi) {
664✔
591
            //wrap quasis with quotes (and escape inner quotemarks)
592
            text = `"${this.token.text.replace(/"/g, '""')}"`;
24✔
593

594
        } else if (isStringType(this.type)) {
640✔
595
            text = this.token.text;
269✔
596
            //add trailing quotemark if it's missing. We will have already generated a diagnostic for this.
597
            if (text.endsWith('"') === false) {
269✔
598
                text += '"';
1✔
599
            }
600
        } else {
601
            text = this.token.text;
371✔
602
        }
603

604
        return [
664✔
605
            state.sourceNode(this, text)
606
        ];
607
    }
608

609
    walk(visitor: WalkVisitor, options: WalkOptions) {
610
        //nothing to walk
611
    }
612
}
613

614
/**
615
 * This is a special expression only used within template strings. It exists so we can prevent producing lots of empty strings
616
 * during template string transpile by identifying these expressions explicitly and skipping the bslib_toString around them
617
 */
618
export class EscapedCharCodeLiteralExpression extends Expression {
1✔
619
    constructor(
620
        readonly token: Token & { charCode: number }
24✔
621
    ) {
622
        super();
24✔
623
        this.range = token.range;
24✔
624
    }
625
    readonly range: Range;
626

627
    transpile(state: BrsTranspileState) {
628
        return [
12✔
629
            state.sourceNode(this, `chr(${this.token.charCode})`)
630
        ];
631
    }
632

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

638
export class ArrayLiteralExpression extends Expression {
1✔
639
    constructor(
640
        readonly elements: Array<Expression | CommentStatement>,
98✔
641
        readonly open: Token,
98✔
642
        readonly close: Token,
98✔
643
        readonly hasSpread = false
98✔
644
    ) {
645
        super();
98✔
646
        this.range = util.createBoundingRange(this.open, ...this.elements, this.close);
98✔
647
    }
648

649
    public readonly range: Range;
650

651
    transpile(state: BrsTranspileState) {
652
        let result = [];
35✔
653
        result.push(
35✔
654
            state.transpileToken(this.open)
655
        );
656
        let hasChildren = this.elements.length > 0;
35✔
657
        state.blockDepth++;
35✔
658

659
        for (let i = 0; i < this.elements.length; i++) {
35✔
660
            let previousElement = this.elements[i - 1];
43✔
661
            let element = this.elements[i];
43✔
662

663
            if (isCommentStatement(element)) {
43✔
664
                //if the comment is on the same line as opening square or previous statement, don't add newline
665
                if (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element)) {
5✔
666
                    result.push(' ');
4✔
667
                } else {
668
                    result.push(
1✔
669
                        '\n',
670
                        state.indent()
671
                    );
672
                }
673
                state.lineage.unshift(this);
5✔
674
                result.push(element.transpile(state));
5✔
675
                state.lineage.shift();
5✔
676
            } else {
677
                result.push('\n');
38✔
678

679
                result.push(
38✔
680
                    state.indent(),
681
                    ...element.transpile(state)
682
                );
683
            }
684
        }
685
        state.blockDepth--;
35✔
686
        //add a newline between open and close if there are elements
687
        if (hasChildren) {
35✔
688
            result.push('\n');
15✔
689
            result.push(state.indent());
15✔
690
        }
691
        if (this.close) {
35!
692
            result.push(
35✔
693
                state.transpileToken(this.close)
694
            );
695
        }
696
        return result;
35✔
697
    }
698

699
    walk(visitor: WalkVisitor, options: WalkOptions) {
700
        if (options.walkMode & InternalWalkMode.walkExpressions) {
192!
701
            walkArray(this.elements, visitor, options, this);
192✔
702
        }
703
    }
704
}
705

706
export class AAMemberExpression extends Expression {
1✔
707
    constructor(
708
        public keyToken: Token,
182✔
709
        public colonToken: Token,
182✔
710
        /** The expression evaluated to determine the member's initial value. */
711
        public value: Expression
182✔
712
    ) {
713
        super();
182✔
714
        this.range = util.createBoundingRange(this.keyToken, this.colonToken, this.value);
182✔
715
    }
716

717
    public range: Range;
718
    public commaToken?: Token;
719

720
    transpile(state: BrsTranspileState) {
721
        //TODO move the logic from AALiteralExpression loop into this function
722
        return [];
×
723
    }
724

725
    walk(visitor: WalkVisitor, options: WalkOptions) {
726
        walk(this, 'value', visitor, options);
241✔
727
    }
728

729
}
730

731
export class AALiteralExpression extends Expression {
1✔
732
    constructor(
733
        readonly elements: Array<AAMemberExpression | CommentStatement>,
183✔
734
        readonly open: Token,
183✔
735
        readonly close: Token
183✔
736
    ) {
737
        super();
183✔
738
        this.range = util.createBoundingRange(this.open, ...this.elements, this.close);
183✔
739
    }
740

741
    public readonly range: Range;
742

743
    transpile(state: BrsTranspileState) {
744
        let result = [];
49✔
745
        //open curly
746
        result.push(
49✔
747
            state.transpileToken(this.open)
748
        );
749
        let hasChildren = this.elements.length > 0;
49✔
750
        //add newline if the object has children and the first child isn't a comment starting on the same line as opening curly
751
        if (hasChildren && (isCommentStatement(this.elements[0]) === false || !util.linesTouch(this.elements[0], this.open))) {
49✔
752
            result.push('\n');
20✔
753
        }
754
        state.blockDepth++;
49✔
755
        for (let i = 0; i < this.elements.length; i++) {
49✔
756
            let element = this.elements[i];
44✔
757
            let previousElement = this.elements[i - 1];
44✔
758
            let nextElement = this.elements[i + 1];
44✔
759

760
            //don't indent if comment is same-line
761
            if (isCommentStatement(element as any) &&
44✔
762
                (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element))
763
            ) {
764
                result.push(' ');
10✔
765

766
                //indent line
767
            } else {
768
                result.push(state.indent());
34✔
769
            }
770

771
            //render comments
772
            if (isCommentStatement(element)) {
44✔
773
                result.push(...element.transpile(state));
13✔
774
            } else {
775
                //key
776
                result.push(
31✔
777
                    state.transpileToken(element.keyToken)
778
                );
779
                //colon
780
                result.push(
31✔
781
                    state.transpileToken(element.colonToken),
782
                    ' '
783
                );
784

785
                //value
786
                result.push(...element.value.transpile(state));
31✔
787
            }
788

789

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

793
                //add a newline between statements
794
            } else {
795
                result.push('\n');
36✔
796
            }
797
        }
798
        state.blockDepth--;
49✔
799

800
        //only indent the closing curly if we have children
801
        if (hasChildren) {
49✔
802
            result.push(state.indent());
22✔
803
        }
804
        //close curly
805
        if (this.close) {
49!
806
            result.push(
49✔
807
                state.transpileToken(this.close)
808
            );
809
        }
810
        return result;
49✔
811
    }
812

813
    walk(visitor: WalkVisitor, options: WalkOptions) {
814
        if (options.walkMode & InternalWalkMode.walkExpressions) {
252!
815
            walkArray(this.elements, visitor, options, this);
252✔
816
        }
817
    }
818
}
819

820
export class UnaryExpression extends Expression {
1✔
821
    constructor(
822
        public operator: Token,
35✔
823
        public right: Expression
35✔
824
    ) {
825
        super();
35✔
826
        this.range = util.createBoundingRange(this.operator, this.right);
35✔
827
    }
828

829
    public readonly range: Range;
830

831
    transpile(state: BrsTranspileState) {
832
        let separatingWhitespace: string;
833
        if (isVariableExpression(this.right)) {
12✔
834
            separatingWhitespace = this.right.name.leadingWhitespace;
5✔
835
        } else if (isLiteralExpression(this.right)) {
7✔
836
            separatingWhitespace = this.right.token.leadingWhitespace;
2✔
837
        } else {
838
            separatingWhitespace = ' ';
5✔
839
        }
840
        return [
12✔
841
            state.transpileToken(this.operator),
842
            separatingWhitespace,
843
            ...this.right.transpile(state)
844
        ];
845
    }
846

847
    walk(visitor: WalkVisitor, options: WalkOptions) {
848
        if (options.walkMode & InternalWalkMode.walkExpressions) {
52!
849
            walk(this, 'right', visitor, options);
52✔
850
        }
851
    }
852
}
853

854
export class VariableExpression extends Expression {
1✔
855
    constructor(
856
        readonly name: Identifier
1,697✔
857
    ) {
858
        super();
1,697✔
859
        this.range = this.name?.range;
1,697!
860
    }
861

862
    public readonly range: Range;
863

864
    public getName(parseMode: ParseMode) {
865
        return this.name.text;
27✔
866
    }
867

868
    transpile(state: BrsTranspileState) {
869
        let result = [];
312✔
870
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
312✔
871
        //if the callee is the name of a known namespace function
872
        if (state.file.calleeIsKnownNamespaceFunction(this, namespace?.getName(ParseMode.BrighterScript))) {
312✔
873
            result.push(
3✔
874
                state.sourceNode(this, [
875
                    namespace.getName(ParseMode.BrightScript),
876
                    '_',
877
                    this.getName(ParseMode.BrightScript)
878
                ])
879
            );
880

881
            //transpile  normally
882
        } else {
883
            result.push(
309✔
884
                state.transpileToken(this.name)
885
            );
886
        }
887
        return result;
312✔
888
    }
889

890
    walk(visitor: WalkVisitor, options: WalkOptions) {
891
        //nothing to walk
892
    }
893
}
894

895
export class SourceLiteralExpression extends Expression {
1✔
896
    constructor(
897
        readonly token: Token
25✔
898
    ) {
899
        super();
25✔
900
        this.range = token?.range;
25!
901
    }
902

903
    public readonly range: Range;
904

905
    private getFunctionName(state: BrsTranspileState, parseMode: ParseMode) {
906
        let func = state.file.getFunctionScopeAtPosition(this.token.range.start).func;
6✔
907
        let nameParts = [];
6✔
908
        while (func.parentFunction) {
6✔
909
            let index = func.parentFunction.childFunctionExpressions.indexOf(func);
4✔
910
            nameParts.unshift(`anon${index}`);
4✔
911
            func = func.parentFunction;
4✔
912
        }
913
        //get the index of this function in its parent
914
        nameParts.unshift(
6✔
915
            func.functionStatement.getName(parseMode)
916
        );
917
        return nameParts.join('$');
6✔
918
    }
919

920
    transpile(state: BrsTranspileState) {
921
        let text: string;
922
        switch (this.token.kind) {
21✔
923
            case TokenKind.SourceFilePathLiteral:
27✔
924
                const pathUrl = fileUrl(state.srcPath);
2✔
925
                text = `"${pathUrl.substring(0, 4)}" + "${pathUrl.substring(4)}"`;
2✔
926
                break;
2✔
927
            case TokenKind.SourceLineNumLiteral:
928
                text = `${this.token.range.start.line + 1}`;
3✔
929
                break;
3✔
930
            case TokenKind.FunctionNameLiteral:
931
                text = `"${this.getFunctionName(state, ParseMode.BrightScript)}"`;
3✔
932
                break;
3✔
933
            case TokenKind.SourceFunctionNameLiteral:
934
                text = `"${this.getFunctionName(state, ParseMode.BrighterScript)}"`;
3✔
935
                break;
3✔
936
            case TokenKind.SourceLocationLiteral:
937
                const locationUrl = fileUrl(state.srcPath);
2✔
938
                text = `"${locationUrl.substring(0, 4)}" + "${locationUrl.substring(4)}:${this.token.range.start.line + 1}"`;
2✔
939
                break;
2✔
940
            case TokenKind.PkgPathLiteral:
941
                let pkgPath1 = `pkg:/${state.file.pkgPath}`
1✔
942
                    .replace(/\\/g, '/')
943
                    .replace(/\.bs$/i, '.brs');
944

945
                text = `"${pkgPath1}"`;
1✔
946
                break;
1✔
947
            case TokenKind.PkgLocationLiteral:
948
                let pkgPath2 = `pkg:/${state.file.pkgPath}`
1✔
949
                    .replace(/\\/g, '/')
950
                    .replace(/\.bs$/i, '.brs');
951

952
                text = `"${pkgPath2}:" + str(LINE_NUM)`;
1✔
953
                break;
1✔
954
            case TokenKind.LineNumLiteral:
955
            default:
956
                //use the original text (because it looks like a variable)
957
                text = this.token.text;
6✔
958
                break;
6✔
959

960
        }
961
        return [
21✔
962
            state.sourceNode(this, text)
963
        ];
964
    }
965

966
    walk(visitor: WalkVisitor, options: WalkOptions) {
967
        //nothing to walk
968
    }
969
}
970

971
/**
972
 * This expression transpiles and acts exactly like a CallExpression,
973
 * except we need to uniquely identify these statements so we can
974
 * do more type checking.
975
 */
976
export class NewExpression extends Expression {
1✔
977
    constructor(
978
        readonly newKeyword: Token,
37✔
979
        readonly call: CallExpression
37✔
980
    ) {
981
        super();
37✔
982
        this.range = util.createBoundingRange(this.newKeyword, this.call);
37✔
983
    }
984

985
    /**
986
     * The name of the class to initialize (with optional namespace prefixed)
987
     */
988
    public get className() {
989
        //the parser guarantees the callee of a new statement's call object will be
990
        //a NamespacedVariableNameExpression
991
        return this.call.callee as NamespacedVariableNameExpression;
102✔
992
    }
993

994
    public readonly range: Range;
995

996
    public transpile(state: BrsTranspileState) {
997
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
9✔
998
        const cls = state.file.getClassFileLink(
9✔
999
            this.className.getName(ParseMode.BrighterScript),
1000
            namespace?.getName(ParseMode.BrighterScript)
27✔
1001
        )?.item;
9✔
1002
        //new statements within a namespace block can omit the leading namespace if the class resides in that same namespace.
1003
        //So we need to figure out if this is a namespace-omitted class, or if this class exists without a namespace.
1004
        return this.call.transpile(state, cls?.getName(ParseMode.BrightScript));
9✔
1005
    }
1006

1007
    walk(visitor: WalkVisitor, options: WalkOptions) {
1008
        if (options.walkMode & InternalWalkMode.walkExpressions) {
133!
1009
            walk(this, 'call', visitor, options);
133✔
1010
        }
1011
    }
1012
}
1013

1014
export class CallfuncExpression extends Expression {
1✔
1015
    constructor(
1016
        readonly callee: Expression,
18✔
1017
        readonly operator: Token,
18✔
1018
        readonly methodName: Identifier,
18✔
1019
        readonly openingParen: Token,
18✔
1020
        readonly args: Expression[],
18✔
1021
        readonly closingParen: Token
18✔
1022
    ) {
1023
        super();
18✔
1024
        this.range = util.createBoundingRange(
18✔
1025
            callee,
1026
            operator,
1027
            methodName,
1028
            openingParen,
1029
            ...args,
1030
            closingParen
1031
        );
1032
    }
1033

1034
    public readonly range: Range;
1035

1036
    /**
1037
     * Get the name of the wrapping namespace (if it exists)
1038
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
1039
     */
1040
    public get namespaceName() {
1041
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
1042
    }
1043

1044
    public transpile(state: BrsTranspileState) {
1045
        let result = [];
6✔
1046
        result.push(
6✔
1047
            ...this.callee.transpile(state),
1048
            state.sourceNode(this.operator, '.callfunc'),
1049
            state.transpileToken(this.openingParen),
1050
            //the name of the function
1051
            state.sourceNode(this.methodName, ['"', this.methodName.text, '"']),
1052
            ', '
1053
        );
1054
        //transpile args
1055
        //callfunc with zero args never gets called, so pass invalid as the first parameter if there are no args
1056
        if (this.args.length === 0) {
6✔
1057
            result.push('invalid');
4✔
1058
        } else {
1059
            for (let i = 0; i < this.args.length; i++) {
2✔
1060
                //add comma between args
1061
                if (i > 0) {
4✔
1062
                    result.push(', ');
2✔
1063
                }
1064
                let arg = this.args[i];
4✔
1065
                result.push(...arg.transpile(state));
4✔
1066
            }
1067
        }
1068
        result.push(
6✔
1069
            state.transpileToken(this.closingParen)
1070
        );
1071
        return result;
6✔
1072
    }
1073

1074
    walk(visitor: WalkVisitor, options: WalkOptions) {
1075
        if (options.walkMode & InternalWalkMode.walkExpressions) {
33!
1076
            walk(this, 'callee', visitor, options);
33✔
1077
            walkArray(this.args, visitor, options, this);
33✔
1078
        }
1079
    }
1080
}
1081

1082
/**
1083
 * Since template strings can contain newlines, we need to concatenate multiple strings together with chr() calls.
1084
 * This is a single expression that represents the string contatenation of all parts of a single quasi.
1085
 */
1086
export class TemplateStringQuasiExpression extends Expression {
1✔
1087
    constructor(
1088
        readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>
66✔
1089
    ) {
1090
        super();
66✔
1091
        this.range = util.createBoundingRange(
66✔
1092
            ...expressions
1093
        );
1094
    }
1095
    readonly range: Range;
1096

1097
    transpile(state: BrsTranspileState, skipEmptyStrings = true) {
32✔
1098
        let result = [];
37✔
1099
        let plus = '';
37✔
1100
        for (let expression of this.expressions) {
37✔
1101
            //skip empty strings
1102
            //TODO what does an empty string literal expression look like?
1103
            if (expression.token.text === '' && skipEmptyStrings === true) {
60✔
1104
                continue;
24✔
1105
            }
1106
            result.push(
36✔
1107
                plus,
1108
                ...expression.transpile(state)
1109
            );
1110
            plus = ' + ';
36✔
1111
        }
1112
        return result;
37✔
1113
    }
1114

1115
    walk(visitor: WalkVisitor, options: WalkOptions) {
1116
        if (options.walkMode & InternalWalkMode.walkExpressions) {
112!
1117
            walkArray(this.expressions, visitor, options, this);
112✔
1118
        }
1119
    }
1120
}
1121

1122
export class TemplateStringExpression extends Expression {
1✔
1123
    constructor(
1124
        readonly openingBacktick: Token,
32✔
1125
        readonly quasis: TemplateStringQuasiExpression[],
32✔
1126
        readonly expressions: Expression[],
32✔
1127
        readonly closingBacktick: Token
32✔
1128
    ) {
1129
        super();
32✔
1130
        this.range = util.createBoundingRange(
32✔
1131
            openingBacktick,
1132
            quasis[0],
1133
            quasis[quasis.length - 1],
1134
            closingBacktick
1135
        );
1136
    }
1137

1138
    public readonly range: Range;
1139

1140
    transpile(state: BrsTranspileState) {
1141
        if (this.quasis.length === 1 && this.expressions.length === 0) {
19✔
1142
            return this.quasis[0].transpile(state);
10✔
1143
        }
1144
        let result = ['('];
9✔
1145
        let plus = '';
9✔
1146
        //helper function to figure out when to include the plus
1147
        function add(...items) {
1148
            if (items.length > 0) {
35✔
1149
                result.push(
25✔
1150
                    plus,
1151
                    ...items
1152
                );
1153
            }
1154
            //set the plus after the first occurance of a nonzero length set of items
1155
            if (plus === '' && items.length > 0) {
35✔
1156
                plus = ' + ';
9✔
1157
            }
1158
        }
1159

1160
        for (let i = 0; i < this.quasis.length; i++) {
9✔
1161
            let quasi = this.quasis[i];
22✔
1162
            let expression = this.expressions[i];
22✔
1163

1164
            add(
22✔
1165
                ...quasi.transpile(state)
1166
            );
1167
            if (expression) {
22✔
1168
                //skip the toString wrapper around certain expressions
1169
                if (
13✔
1170
                    isEscapedCharCodeLiteralExpression(expression) ||
29✔
1171
                    (isLiteralExpression(expression) && isStringType(expression.type))
1172
                ) {
1173
                    add(
3✔
1174
                        ...expression.transpile(state)
1175
                    );
1176

1177
                    //wrap all other expressions with a bslib_toString call to prevent runtime type mismatch errors
1178
                } else {
1179
                    add(
10✔
1180
                        state.bslibPrefix + '_toString(',
1181
                        ...expression.transpile(state),
1182
                        ')'
1183
                    );
1184
                }
1185
            }
1186
        }
1187
        //the expression should be wrapped in parens so it can be used line a single expression at runtime
1188
        result.push(')');
9✔
1189

1190
        return result;
9✔
1191
    }
1192

1193
    walk(visitor: WalkVisitor, options: WalkOptions) {
1194
        if (options.walkMode & InternalWalkMode.walkExpressions) {
55!
1195
            //walk the quasis and expressions in left-to-right order
1196
            for (let i = 0; i < this.quasis.length; i++) {
55✔
1197
                walk(this.quasis, i, visitor, options, this);
94✔
1198

1199
                //this skips the final loop iteration since we'll always have one more quasi than expression
1200
                if (this.expressions[i]) {
94✔
1201
                    walk(this.expressions, i, visitor, options, this);
39✔
1202
                }
1203
            }
1204
        }
1205
    }
1206
}
1207

1208
export class TaggedTemplateStringExpression extends Expression {
1✔
1209
    constructor(
1210
        readonly tagName: Identifier,
5✔
1211
        readonly openingBacktick: Token,
5✔
1212
        readonly quasis: TemplateStringQuasiExpression[],
5✔
1213
        readonly expressions: Expression[],
5✔
1214
        readonly closingBacktick: Token
5✔
1215
    ) {
1216
        super();
5✔
1217
        this.range = util.createBoundingRange(
5✔
1218
            tagName,
1219
            openingBacktick,
1220
            quasis[0],
1221
            quasis[quasis.length - 1],
1222
            closingBacktick
1223
        );
1224
    }
1225

1226
    public readonly range: Range;
1227

1228
    transpile(state: BrsTranspileState) {
1229
        let result = [];
2✔
1230
        result.push(
2✔
1231
            state.transpileToken(this.tagName),
1232
            '(['
1233
        );
1234

1235
        //add quasis as the first array
1236
        for (let i = 0; i < this.quasis.length; i++) {
2✔
1237
            let quasi = this.quasis[i];
5✔
1238
            //separate items with a comma
1239
            if (i > 0) {
5✔
1240
                result.push(
3✔
1241
                    ', '
1242
                );
1243
            }
1244
            result.push(
5✔
1245
                ...quasi.transpile(state, false)
1246
            );
1247
        }
1248
        result.push(
2✔
1249
            '], ['
1250
        );
1251

1252
        //add expressions as the second array
1253
        for (let i = 0; i < this.expressions.length; i++) {
2✔
1254
            let expression = this.expressions[i];
3✔
1255
            if (i > 0) {
3✔
1256
                result.push(
1✔
1257
                    ', '
1258
                );
1259
            }
1260
            result.push(
3✔
1261
                ...expression.transpile(state)
1262
            );
1263
        }
1264
        result.push(
2✔
1265
            state.sourceNode(this.closingBacktick, '])')
1266
        );
1267
        return result;
2✔
1268
    }
1269

1270
    walk(visitor: WalkVisitor, options: WalkOptions) {
1271
        if (options.walkMode & InternalWalkMode.walkExpressions) {
8!
1272
            //walk the quasis and expressions in left-to-right order
1273
            for (let i = 0; i < this.quasis.length; i++) {
8✔
1274
                walk(this.quasis, i, visitor, options, this);
18✔
1275

1276
                //this skips the final loop iteration since we'll always have one more quasi than expression
1277
                if (this.expressions[i]) {
18✔
1278
                    walk(this.expressions, i, visitor, options, this);
10✔
1279
                }
1280
            }
1281
        }
1282
    }
1283
}
1284

1285
export class AnnotationExpression extends Expression {
1✔
1286
    constructor(
1287
        readonly atToken: Token,
46✔
1288
        readonly nameToken: Token
46✔
1289
    ) {
1290
        super();
46✔
1291
        this.name = nameToken.text;
46✔
1292
    }
1293

1294
    public get range() {
1295
        return util.createBoundingRange(
15✔
1296
            this.atToken,
1297
            this.nameToken,
1298
            this.call
1299
        );
1300
    }
1301

1302
    public name: string;
1303
    public call: CallExpression;
1304

1305
    /**
1306
     * Convert annotation arguments to JavaScript types
1307
     * @param strict If false, keep Expression objects not corresponding to JS types
1308
     */
1309
    getArguments(strict = true): ExpressionValue[] {
3✔
1310
        if (!this.call) {
4✔
1311
            return [];
1✔
1312
        }
1313
        return this.call.args.map(e => expressionToValue(e, strict));
13✔
1314
    }
1315

1316
    transpile(state: BrsTranspileState) {
1317
        return [];
3✔
1318
    }
1319

1320
    walk(visitor: WalkVisitor, options: WalkOptions) {
1321
        //nothing to walk
1322
    }
1323
    getTypedef(state: BrsTranspileState) {
1324
        return [
9✔
1325
            '@',
1326
            this.name,
1327
            ...(this.call?.transpile(state) ?? [])
54✔
1328
        ];
1329
    }
1330
}
1331

1332
export class TernaryExpression extends Expression {
1✔
1333
    constructor(
1334
        readonly test: Expression,
70✔
1335
        readonly questionMarkToken: Token,
70✔
1336
        readonly consequent?: Expression,
70✔
1337
        readonly colonToken?: Token,
70✔
1338
        readonly alternate?: Expression
70✔
1339
    ) {
1340
        super();
70✔
1341
        this.range = util.createBoundingRange(
70✔
1342
            test,
1343
            questionMarkToken,
1344
            consequent,
1345
            colonToken,
1346
            alternate
1347
        );
1348
    }
1349

1350
    public range: Range;
1351

1352
    transpile(state: BrsTranspileState) {
1353
        let result = [];
23✔
1354
        let consequentInfo = util.getExpressionInfo(this.consequent);
23✔
1355
        let alternateInfo = util.getExpressionInfo(this.alternate);
23✔
1356

1357
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1358
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
23✔
1359
        let mutatingExpressions = [
23✔
1360
            ...consequentInfo.expressions,
1361
            ...alternateInfo.expressions
1362
        ].filter(e => e instanceof CallExpression || e instanceof CallfuncExpression || e instanceof DottedGetExpression);
106✔
1363

1364
        if (mutatingExpressions.length > 0) {
23✔
1365
            result.push(
5✔
1366
                state.sourceNode(
1367
                    this.questionMarkToken,
1368
                    //write all the scope variables as parameters.
1369
                    //TODO handle when there are more than 31 parameters
1370
                    `(function(__bsCondition, ${allUniqueVarNames.join(', ')})`
1371
                ),
1372
                state.newline,
1373
                //double indent so our `end function` line is still indented one at the end
1374
                state.indent(2),
1375
                state.sourceNode(this.test, `if __bsCondition then`),
1376
                state.newline,
1377
                state.indent(1),
1378
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
15!
1379
                ...this.consequent?.transpile(state) ?? [state.sourceNode(this.questionMarkToken, 'invalid')],
30!
1380
                state.newline,
1381
                state.indent(-1),
1382
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'else'),
15!
1383
                state.newline,
1384
                state.indent(1),
1385
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
15!
1386
                ...this.alternate?.transpile(state) ?? [state.sourceNode(this.consequent ?? this.questionMarkToken, 'invalid')],
30!
1387
                state.newline,
1388
                state.indent(-1),
1389
                state.sourceNode(this.questionMarkToken, 'end if'),
1390
                state.newline,
1391
                state.indent(-1),
1392
                state.sourceNode(this.questionMarkToken, 'end function)('),
1393
                ...this.test.transpile(state),
1394
                state.sourceNode(this.questionMarkToken, `, ${allUniqueVarNames.join(', ')})`)
1395
            );
1396
            state.blockDepth--;
5✔
1397
        } else {
1398
            result.push(
18✔
1399
                state.sourceNode(this.test, state.bslibPrefix + `_ternary(`),
1400
                ...this.test.transpile(state),
1401
                state.sourceNode(this.test, `, `),
1402
                ...this.consequent?.transpile(state) ?? ['invalid'],
108✔
1403
                `, `,
1404
                ...this.alternate?.transpile(state) ?? ['invalid'],
108✔
1405
                `)`
1406
            );
1407
        }
1408
        return result;
23✔
1409
    }
1410

1411
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1412
        if (options.walkMode & InternalWalkMode.walkExpressions) {
95!
1413
            walk(this, 'test', visitor, options);
95✔
1414
            walk(this, 'consequent', visitor, options);
95✔
1415
            walk(this, 'alternate', visitor, options);
95✔
1416
        }
1417
    }
1418
}
1419

1420
export class NullCoalescingExpression extends Expression {
1✔
1421
    constructor(
1422
        public consequent: Expression,
23✔
1423
        public questionQuestionToken: Token,
23✔
1424
        public alternate: Expression
23✔
1425
    ) {
1426
        super();
23✔
1427
        this.range = util.createBoundingRange(
23✔
1428
            consequent,
1429
            questionQuestionToken,
1430
            alternate
1431
        );
1432
    }
1433
    public readonly range: Range;
1434

1435
    transpile(state: BrsTranspileState) {
1436
        let result = [];
6✔
1437
        let consequentInfo = util.getExpressionInfo(this.consequent);
6✔
1438
        let alternateInfo = util.getExpressionInfo(this.alternate);
6✔
1439

1440
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1441
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
6✔
1442
        let hasMutatingExpression = [
6✔
1443
            ...consequentInfo.expressions,
1444
            ...alternateInfo.expressions
1445
        ].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e));
19✔
1446

1447
        if (hasMutatingExpression) {
6✔
1448
            result.push(
3✔
1449
                `(function(`,
1450
                //write all the scope variables as parameters.
1451
                //TODO handle when there are more than 31 parameters
1452
                allUniqueVarNames.join(', '),
1453
                ')',
1454
                state.newline,
1455
                //double indent so our `end function` line is still indented one at the end
1456
                state.indent(2),
1457
                //evaluate the consequent exactly once, and then use it in the following condition
1458
                `__bsConsequent = `,
1459
                ...this.consequent.transpile(state),
1460
                state.newline,
1461
                state.indent(),
1462
                `if __bsConsequent <> invalid then`,
1463
                state.newline,
1464
                state.indent(1),
1465
                'return __bsConsequent',
1466
                state.newline,
1467
                state.indent(-1),
1468
                'else',
1469
                state.newline,
1470
                state.indent(1),
1471
                'return ',
1472
                ...this.alternate.transpile(state),
1473
                state.newline,
1474
                state.indent(-1),
1475
                'end if',
1476
                state.newline,
1477
                state.indent(-1),
1478
                'end function)(',
1479
                allUniqueVarNames.join(', '),
1480
                ')'
1481
            );
1482
            state.blockDepth--;
3✔
1483
        } else {
1484
            result.push(
3✔
1485
                state.bslibPrefix + `_coalesce(`,
1486
                ...this.consequent.transpile(state),
1487
                ', ',
1488
                ...this.alternate.transpile(state),
1489
                ')'
1490
            );
1491
        }
1492
        return result;
6✔
1493
    }
1494

1495
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1496
        if (options.walkMode & InternalWalkMode.walkExpressions) {
30!
1497
            walk(this, 'consequent', visitor, options);
30✔
1498
            walk(this, 'alternate', visitor, options);
30✔
1499
        }
1500
    }
1501
}
1502

1503
export class RegexLiteralExpression extends Expression {
1✔
1504
    public constructor(
1505
        public tokens: {
43✔
1506
            regexLiteral: Token;
1507
        }
1508
    ) {
1509
        super();
43✔
1510
    }
1511

1512
    public get range() {
1513
        return this.tokens?.regexLiteral?.range;
51!
1514
    }
1515

1516
    public transpile(state: BrsTranspileState): TranspileResult {
1517
        let text = this.tokens.regexLiteral?.text ?? '';
41!
1518
        let flags = '';
41✔
1519
        //get any flags from the end
1520
        const flagMatch = /\/([a-z]+)$/i.exec(text);
41✔
1521
        if (flagMatch) {
41✔
1522
            text = text.substring(0, flagMatch.index + 1);
1✔
1523
            flags = flagMatch[1];
1✔
1524
        }
1525
        let pattern = text
41✔
1526
            //remove leading and trailing slashes
1527
            .substring(1, text.length - 1)
1528
            //escape quotemarks
1529
            .split('"').join('" + chr(34) + "');
1530

1531
        return [
41✔
1532
            state.sourceNode(this.tokens.regexLiteral, [
1533
                'CreateObject("roRegex", ',
1534
                `"${pattern}", `,
1535
                `"${flags}"`,
1536
                ')'
1537
            ])
1538
        ];
1539
    }
1540

1541
    walk(visitor: WalkVisitor, options: WalkOptions) {
1542
        //nothing to walk
1543
    }
1544
}
1545

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

1549
function expressionToValue(expr: Expression, strict: boolean): ExpressionValue {
1550
    if (!expr) {
19!
1551
        return null;
×
1552
    }
1553
    if (isUnaryExpression(expr) && isLiteralNumber(expr.right)) {
19✔
1554
        return numberExpressionToValue(expr.right, expr.operator.text);
1✔
1555
    }
1556
    if (isLiteralString(expr)) {
18✔
1557
        //remove leading and trailing quotes
1558
        return expr.token.text.replace(/^"/, '').replace(/"$/, '');
4✔
1559
    }
1560
    if (isLiteralNumber(expr)) {
14✔
1561
        return numberExpressionToValue(expr);
6✔
1562
    }
1563

1564
    if (isLiteralBoolean(expr)) {
8✔
1565
        return expr.token.text.toLowerCase() === 'true';
2✔
1566
    }
1567
    if (isArrayLiteralExpression(expr)) {
6✔
1568
        return expr.elements
2✔
1569
            .filter(e => !isCommentStatement(e))
4✔
1570
            .map(e => expressionToValue(e, strict));
4✔
1571
    }
1572
    if (isAALiteralExpression(expr)) {
4✔
1573
        return expr.elements.reduce((acc, e) => {
2✔
1574
            if (!isCommentStatement(e)) {
2!
1575
                acc[e.keyToken.text] = expressionToValue(e.value, strict);
2✔
1576
            }
1577
            return acc;
2✔
1578
        }, {});
1579
    }
1580
    return strict ? null : expr;
2✔
1581
}
1582

1583
function numberExpressionToValue(expr: LiteralExpression, operator = '') {
6✔
1584
    if (isIntegerType(expr.type) || isLongIntegerType(expr.type)) {
7!
1585
        return parseInt(operator + expr.token.text);
7✔
1586
    } else {
1587
        return parseFloat(operator + expr.token.text);
×
1588
    }
1589
}
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