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

rokucommunity / brighterscript / #13786

13 Mar 2024 03:54PM UTC coverage: 88.331% (+0.2%) from 88.178%
#13786

push

TwitchBronBron
0.65.26

6007 of 7278 branches covered (82.54%)

Branch coverage included in aggregate %.

8784 of 9467 relevant lines covered (92.79%)

1715.21 hits per line

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

91.53
/src/parser/Expression.ts
1
/* eslint-disable no-bitwise */
2
import type { Token, Identifier } from '../lexer/Token';
3
import { TokenKind } from '../lexer/TokenKind';
1✔
4
import type { Block, CommentStatement, FunctionStatement, NamespaceStatement } from './Statement';
5
import type { Range } from 'vscode-languageserver';
6
import util from '../util';
1✔
7
import type { BrsTranspileState } from './BrsTranspileState';
8
import { ParseMode } from './Parser';
1✔
9
import * as fileUrl from 'file-url';
1✔
10
import type { WalkOptions, WalkVisitor } from '../astUtils/visitors';
11
import { createVisitor, WalkMode } from '../astUtils/visitors';
1✔
12
import { walk, InternalWalkMode, walkArray } from '../astUtils/visitors';
1✔
13
import { isAALiteralExpression, isArrayLiteralExpression, isCallExpression, isCallfuncExpression, isCommentStatement, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isIntegerType, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isStringType, isTypeCastExpression, isUnaryExpression, isVariableExpression } from '../astUtils/reflection';
1✔
14
import type { TranspileResult, TypedefProvider } from '../interfaces';
15
import { VoidType } from '../types/VoidType';
1✔
16
import { DynamicType } from '../types/DynamicType';
1✔
17
import type { BscType } from '../types/BscType';
18
import { FunctionType } from '../types/FunctionType';
1✔
19
import type { AstNode } from './AstNode';
20
import { Expression } from './AstNode';
1✔
21
import { SymbolTable } from '../SymbolTable';
1✔
22
import { SourceNode } from 'source-map';
1✔
23

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

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

36
    public readonly range: Range | undefined;
37

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

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

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

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

73
    public readonly range: Range | undefined;
74

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

83
    transpile(state: BrsTranspileState, nameOverride?: string) {
84
        let result: TranspileResult = [];
222✔
85

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

336
    public type: BscType;
337

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

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

365
        return result;
110✔
366
    }
367

368
    public getTypedef(state: BrsTranspileState): TranspileResult {
369
        const results = [this.name.text] as TranspileResult;
7✔
370

371
        if (this.defaultValue) {
7!
372
            results.push(' = ', ...this.defaultValue.transpile(state));
×
373
        }
374

375
        if (this.asToken) {
7✔
376
            results.push(' as ');
6✔
377

378
            // TODO: Is this conditional needed? Will typeToken always exist
379
            // so long as `asToken` exists?
380
            if (this.typeToken) {
6!
381
                results.push(this.typeToken.text);
6✔
382
            }
383
        }
384

385
        return results;
7✔
386
    }
387

388
    walk(visitor: WalkVisitor, options: WalkOptions) {
389
        // eslint-disable-next-line no-bitwise
390
        if (this.defaultValue && options.walkMode & InternalWalkMode.walkExpressions) {
1,006✔
391
            walk(this, 'defaultValue', visitor, options);
72✔
392
        }
393
    }
394
}
395

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

406
    transpile(state: BrsTranspileState) {
407
        return [
4✔
408
            state.sourceNode(this, this.getName(ParseMode.BrightScript))
409
        ];
410
    }
411

412
    public getNameParts() {
413
        let parts = [] as string[];
2,558✔
414
        if (isVariableExpression(this.expression)) {
2,558✔
415
            parts.push(this.expression.name.text);
1,789✔
416
        } else {
417
            let expr = this.expression;
769✔
418

419
            parts.push(expr.name.text);
769✔
420

421
            while (isVariableExpression(expr) === false) {
769✔
422
                expr = expr.obj as DottedGetExpression;
906✔
423
                parts.unshift(expr.name.text);
906✔
424
            }
425
        }
426
        return parts;
2,558✔
427
    }
428

429
    getName(parseMode: ParseMode) {
430
        if (parseMode === ParseMode.BrighterScript) {
2,532✔
431
            return this.getNameParts().join('.');
2,305✔
432
        } else {
433
            return this.getNameParts().join('_');
227✔
434
        }
435
    }
436

437
    walk(visitor: WalkVisitor, options: WalkOptions) {
438
        this.expression?.link();
686!
439
        if (options.walkMode & InternalWalkMode.walkExpressions) {
686✔
440
            walk(this, 'expression', visitor, options);
662✔
441
        }
442
    }
443
}
444

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

458
    public readonly range: Range | undefined;
459

460
    transpile(state: BrsTranspileState) {
461
        //if the callee starts with a namespace name, transpile the name
462
        if (state.file.calleeStartsWithNamespace(this)) {
236✔
463
            return new NamespacedVariableNameExpression(this as DottedGetExpression | VariableExpression).transpile(state);
3✔
464
        } else {
465
            return [
233✔
466
                ...this.obj.transpile(state),
467
                state.transpileToken(this.dot),
468
                state.transpileToken(this.name)
469
            ];
470
        }
471
    }
472

473
    walk(visitor: WalkVisitor, options: WalkOptions) {
474
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,805!
475
            walk(this, 'obj', visitor, options);
1,805✔
476
        }
477
    }
478

479
}
480

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

494
    public readonly range: Range | undefined;
495

496
    transpile(state: BrsTranspileState) {
497
        return [
3✔
498
            ...this.obj.transpile(state),
499
            state.transpileToken(this.at),
500
            state.transpileToken(this.name)
501
        ];
502
    }
503

504
    walk(visitor: WalkVisitor, options: WalkOptions) {
505
        if (options.walkMode & InternalWalkMode.walkExpressions) {
14!
506
            walk(this, 'obj', visitor, options);
14✔
507
        }
508
    }
509
}
510

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

531
    public readonly range: Range | undefined;
532

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

557
    walk(visitor: WalkVisitor, options: WalkOptions) {
558
        if (options.walkMode & InternalWalkMode.walkExpressions) {
175!
559
            walk(this, 'obj', visitor, options);
175✔
560
            walk(this, 'index', visitor, options);
175✔
561
            walkArray(this.additionalIndexes, visitor, options, this);
175✔
562
        }
563
    }
564
}
565

566
export class GroupingExpression extends Expression {
1✔
567
    constructor(
568
        readonly tokens: {
32✔
569
            left: Token;
570
            right: Token;
571
        },
572
        public expression: Expression
32✔
573
    ) {
574
        super();
32✔
575
        this.range = util.createBoundingRange(this.tokens.left, this.expression, this.tokens.right);
32✔
576
    }
577

578
    public readonly range: Range | undefined;
579

580
    transpile(state: BrsTranspileState) {
581
        if (isTypeCastExpression(this.expression)) {
12✔
582
            return this.expression.transpile(state);
7✔
583
        }
584
        return [
5✔
585
            state.transpileToken(this.tokens.left),
586
            ...this.expression.transpile(state),
587
            state.transpileToken(this.tokens.right)
588
        ];
589
    }
590

591
    walk(visitor: WalkVisitor, options: WalkOptions) {
592
        if (options.walkMode & InternalWalkMode.walkExpressions) {
45!
593
            walk(this, 'expression', visitor, options);
45✔
594
        }
595
    }
596
}
597

598
export class LiteralExpression extends Expression {
1✔
599
    constructor(
600
        public token: Token
2,736✔
601
    ) {
602
        super();
2,736✔
603
        this.type = util.tokenToBscType(token);
2,736✔
604
    }
605

606
    public get range() {
607
        return this.token.range;
6,956✔
608
    }
609

610
    /**
611
     * The (data) type of this expression
612
     */
613
    public type: BscType;
614

615
    transpile(state: BrsTranspileState) {
616
        let text: string;
617
        if (this.token.kind === TokenKind.TemplateStringQuasi) {
755✔
618
            //wrap quasis with quotes (and escape inner quotemarks)
619
            text = `"${this.token.text.replace(/"/g, '""')}"`;
28✔
620

621
        } else if (isStringType(this.type)) {
727✔
622
            text = this.token.text;
286✔
623
            //add trailing quotemark if it's missing. We will have already generated a diagnostic for this.
624
            if (text.endsWith('"') === false) {
286✔
625
                text += '"';
1✔
626
            }
627
        } else {
628
            text = this.token.text;
441✔
629
        }
630

631
        return [
755✔
632
            state.sourceNode(this, text)
633
        ];
634
    }
635

636
    walk(visitor: WalkVisitor, options: WalkOptions) {
637
        //nothing to walk
638
    }
639
}
640

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

654
    transpile(state: BrsTranspileState) {
655
        return [
13✔
656
            state.sourceNode(this, `chr(${this.token.charCode})`)
657
        ];
658
    }
659

660
    walk(visitor: WalkVisitor, options: WalkOptions) {
661
        //nothing to walk
662
    }
663
}
664

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

676
    public readonly range: Range | undefined;
677

678
    transpile(state: BrsTranspileState) {
679
        let result = [] as TranspileResult;
48✔
680
        result.push(
48✔
681
            state.transpileToken(this.open)
682
        );
683
        let hasChildren = this.elements.length > 0;
48✔
684
        state.blockDepth++;
48✔
685

686
        for (let i = 0; i < this.elements.length; i++) {
48✔
687
            let previousElement = this.elements[i - 1];
59✔
688
            let element = this.elements[i];
59✔
689

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

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

726
    walk(visitor: WalkVisitor, options: WalkOptions) {
727
        if (options.walkMode & InternalWalkMode.walkExpressions) {
218!
728
            walkArray(this.elements, visitor, options, this);
218✔
729
        }
730
    }
731
}
732

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

744
    public range: Range | undefined;
745
    public commaToken?: Token;
746

747
    transpile(state: BrsTranspileState) {
748
        //TODO move the logic from AALiteralExpression loop into this function
749
        return [];
×
750
    }
751

752
    walk(visitor: WalkVisitor, options: WalkOptions) {
753
        walk(this, 'value', visitor, options);
245✔
754
    }
755

756
}
757

758
export class AALiteralExpression extends Expression {
1✔
759
    constructor(
760
        readonly elements: Array<AAMemberExpression | CommentStatement>,
185✔
761
        readonly open: Token,
185✔
762
        readonly close: Token
185✔
763
    ) {
764
        super();
185✔
765
        this.range = util.createBoundingRange(this.open, ...this.elements, this.close);
185✔
766
    }
767

768
    public readonly range: Range | undefined;
769

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

787
            //don't indent if comment is same-line
788
            if (isCommentStatement(element as any) &&
47✔
789
                (util.linesTouch(this.open, element) || util.linesTouch(previousElement, element))
790
            ) {
791
                result.push(' ');
10✔
792

793
                //indent line
794
            } else {
795
                result.push(state.indent());
37✔
796
            }
797

798
            //render comments
799
            if (isCommentStatement(element)) {
47✔
800
                result.push(...element.transpile(state));
14✔
801
            } else {
802
                //key
803
                result.push(
33✔
804
                    state.transpileToken(element.keyToken)
805
                );
806
                //colon
807
                result.push(
33✔
808
                    state.transpileToken(element.colonToken),
809
                    ' '
810
                );
811

812
                //value
813
                result.push(...element.value.transpile(state));
33✔
814
            }
815

816

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

820
                //add a newline between statements
821
            } else {
822
                result.push('\n');
39✔
823
            }
824
        }
825
        state.blockDepth--;
51✔
826

827
        //only indent the closing curly if we have children
828
        if (hasChildren) {
51✔
829
            result.push(state.indent());
23✔
830
        }
831
        //close curly
832
        if (this.close) {
51!
833
            result.push(
51✔
834
                state.transpileToken(this.close)
835
            );
836
        }
837
        return result;
51✔
838
    }
839

840
    walk(visitor: WalkVisitor, options: WalkOptions) {
841
        if (options.walkMode & InternalWalkMode.walkExpressions) {
256!
842
            walkArray(this.elements, visitor, options, this);
256✔
843
        }
844
    }
845
}
846

847
export class UnaryExpression extends Expression {
1✔
848
    constructor(
849
        public operator: Token,
37✔
850
        public right: Expression
37✔
851
    ) {
852
        super();
37✔
853
        this.range = util.createBoundingRange(this.operator, this.right);
37✔
854
    }
855

856
    public readonly range: Range | undefined;
857

858
    transpile(state: BrsTranspileState) {
859
        let separatingWhitespace: string | undefined;
860
        if (isVariableExpression(this.right)) {
14✔
861
            separatingWhitespace = this.right.name.leadingWhitespace;
6✔
862
        } else if (isLiteralExpression(this.right)) {
8✔
863
            separatingWhitespace = this.right.token.leadingWhitespace;
3✔
864
        }
865

866
        return [
14✔
867
            state.transpileToken(this.operator),
868
            separatingWhitespace ?? ' ',
42✔
869
            ...this.right.transpile(state)
870
        ];
871
    }
872

873
    walk(visitor: WalkVisitor, options: WalkOptions) {
874
        if (options.walkMode & InternalWalkMode.walkExpressions) {
56!
875
            walk(this, 'right', visitor, options);
56✔
876
        }
877
    }
878
}
879

880
export class VariableExpression extends Expression {
1✔
881
    constructor(
882
        readonly name: Identifier
1,798✔
883
    ) {
884
        super();
1,798✔
885
        this.range = this.name?.range;
1,798!
886
    }
887

888
    public readonly range: Range;
889

890
    public getName(parseMode: ParseMode) {
891
        return this.name.text;
28✔
892
    }
893

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

907
            //transpile  normally
908
        } else {
909
            result.push(
353✔
910
                state.transpileToken(this.name)
911
            );
912
        }
913
        return result;
356✔
914
    }
915

916
    walk(visitor: WalkVisitor, options: WalkOptions) {
917
        //nothing to walk
918
    }
919
}
920

921
export class SourceLiteralExpression extends Expression {
1✔
922
    constructor(
923
        readonly token: Token
35✔
924
    ) {
925
        super();
35✔
926
        this.range = token?.range;
35!
927
    }
928

929
    public readonly range: Range;
930

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

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

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

987
                text = `"${pkgPath1}"`;
2✔
988
                break;
2✔
989
            case TokenKind.PkgLocationLiteral:
990
                let pkgPath2 = `pkg:/${state.file.pkgPath}`
2✔
991
                    .replace(/\\/g, '/')
992
                    .replace(/\.bs$/i, '.brs');
993

994
                text = `"${pkgPath2}:" + str(LINE_NUM)`;
2✔
995
                break;
2✔
996
            case TokenKind.LineNumLiteral:
997
            default:
998
                //use the original text (because it looks like a variable)
999
                text = this.token.text;
9✔
1000
                break;
9✔
1001

1002
        }
1003
        return [
31✔
1004
            state.sourceNode(this, text)
1005
        ];
1006
    }
1007

1008
    walk(visitor: WalkVisitor, options: WalkOptions) {
1009
        //nothing to walk
1010
    }
1011
}
1012

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

1027
    /**
1028
     * The name of the class to initialize (with optional namespace prefixed)
1029
     */
1030
    public get className() {
1031
        //the parser guarantees the callee of a new statement's call object will be
1032
        //a NamespacedVariableNameExpression
1033
        return this.call.callee as NamespacedVariableNameExpression;
104✔
1034
    }
1035

1036
    public readonly range: Range | undefined;
1037

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

1049
    walk(visitor: WalkVisitor, options: WalkOptions) {
1050
        if (options.walkMode & InternalWalkMode.walkExpressions) {
135!
1051
            walk(this, 'call', visitor, options);
135✔
1052
        }
1053
    }
1054
}
1055

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

1076
    public readonly range: Range | undefined;
1077

1078
    /**
1079
     * Get the name of the wrapping namespace (if it exists)
1080
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
1081
     */
1082
    public get namespaceName() {
1083
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
1084
    }
1085

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

1116
    walk(visitor: WalkVisitor, options: WalkOptions) {
1117
        if (options.walkMode & InternalWalkMode.walkExpressions) {
41!
1118
            walk(this, 'callee', visitor, options);
41✔
1119
            walkArray(this.args, visitor, options, this);
41✔
1120
        }
1121
    }
1122
}
1123

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

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

1157
    walk(visitor: WalkVisitor, options: WalkOptions) {
1158
        if (options.walkMode & InternalWalkMode.walkExpressions) {
124!
1159
            walkArray(this.expressions, visitor, options, this);
124✔
1160
        }
1161
    }
1162
}
1163

1164
export class TemplateStringExpression extends Expression {
1✔
1165
    constructor(
1166
        readonly openingBacktick: Token,
33✔
1167
        readonly quasis: TemplateStringQuasiExpression[],
33✔
1168
        readonly expressions: Expression[],
33✔
1169
        readonly closingBacktick: Token
33✔
1170
    ) {
1171
        super();
33✔
1172
        this.range = util.createBoundingRange(
33✔
1173
            openingBacktick,
1174
            quasis[0],
1175
            quasis[quasis.length - 1],
1176
            closingBacktick
1177
        );
1178
    }
1179

1180
    public readonly range: Range | undefined;
1181

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

1202
        for (let i = 0; i < this.quasis.length; i++) {
10✔
1203
            let quasi = this.quasis[i];
25✔
1204
            let expression = this.expressions[i];
25✔
1205

1206
            add(
25✔
1207
                ...quasi.transpile(state)
1208
            );
1209
            if (expression) {
25✔
1210
                //skip the toString wrapper around certain expressions
1211
                if (
15✔
1212
                    isEscapedCharCodeLiteralExpression(expression) ||
34✔
1213
                    (isLiteralExpression(expression) && isStringType(expression.type))
1214
                ) {
1215
                    add(
3✔
1216
                        ...expression.transpile(state)
1217
                    );
1218

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

1232
        return result;
10✔
1233
    }
1234

1235
    walk(visitor: WalkVisitor, options: WalkOptions) {
1236
        if (options.walkMode & InternalWalkMode.walkExpressions) {
57!
1237
            //walk the quasis and expressions in left-to-right order
1238
            for (let i = 0; i < this.quasis.length; i++) {
57✔
1239
                walk(this.quasis, i, visitor, options, this);
100✔
1240

1241
                //this skips the final loop iteration since we'll always have one more quasi than expression
1242
                if (this.expressions[i]) {
100✔
1243
                    walk(this.expressions, i, visitor, options, this);
43✔
1244
                }
1245
            }
1246
        }
1247
    }
1248
}
1249

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

1268
    public readonly range: Range | undefined;
1269

1270
    transpile(state: BrsTranspileState) {
1271
        let result = [] as TranspileResult;
3✔
1272
        result.push(
3✔
1273
            state.transpileToken(this.tagName),
1274
            '(['
1275
        );
1276

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

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

1312
    walk(visitor: WalkVisitor, options: WalkOptions) {
1313
        if (options.walkMode & InternalWalkMode.walkExpressions) {
10!
1314
            //walk the quasis and expressions in left-to-right order
1315
            for (let i = 0; i < this.quasis.length; i++) {
10✔
1316
                walk(this.quasis, i, visitor, options, this);
24✔
1317

1318
                //this skips the final loop iteration since we'll always have one more quasi than expression
1319
                if (this.expressions[i]) {
24✔
1320
                    walk(this.expressions, i, visitor, options, this);
14✔
1321
                }
1322
            }
1323
        }
1324
    }
1325
}
1326

1327
export class AnnotationExpression extends Expression {
1✔
1328
    constructor(
1329
        readonly atToken: Token,
47✔
1330
        readonly nameToken: Token
47✔
1331
    ) {
1332
        super();
47✔
1333
        this.name = nameToken.text;
47✔
1334
    }
1335

1336
    public get range() {
1337
        return util.createBoundingRange(
17✔
1338
            this.atToken,
1339
            this.nameToken,
1340
            this.call
1341
        );
1342
    }
1343

1344
    public name: string;
1345
    public call: CallExpression | undefined;
1346

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

1358
    transpile(state: BrsTranspileState) {
1359
        return [];
3✔
1360
    }
1361

1362
    walk(visitor: WalkVisitor, options: WalkOptions) {
1363
        //nothing to walk
1364
    }
1365
    getTypedef(state: BrsTranspileState) {
1366
        return [
9✔
1367
            '@',
1368
            this.name,
1369
            ...(this.call?.transpile(state) ?? [])
54✔
1370
        ];
1371
    }
1372
}
1373

1374
export class TernaryExpression extends Expression {
1✔
1375
    constructor(
1376
        readonly test: Expression,
74✔
1377
        readonly questionMarkToken: Token,
74✔
1378
        readonly consequent?: Expression,
74✔
1379
        readonly colonToken?: Token,
74✔
1380
        readonly alternate?: Expression
74✔
1381
    ) {
1382
        super();
74✔
1383
        this.range = util.createBoundingRange(
74✔
1384
            test,
1385
            questionMarkToken,
1386
            consequent,
1387
            colonToken,
1388
            alternate
1389
        );
1390
    }
1391

1392
    public range: Range | undefined;
1393

1394
    transpile(state: BrsTranspileState) {
1395
        let result = [] as TranspileResult;
27✔
1396
        const file = state.file;
27✔
1397
        let consequentInfo = util.getExpressionInfo(this.consequent!, file);
27✔
1398
        let alternateInfo = util.getExpressionInfo(this.alternate!, file);
27✔
1399

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

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

1454
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1455
        if (options.walkMode & InternalWalkMode.walkExpressions) {
103!
1456
            walk(this, 'test', visitor, options);
103✔
1457
            walk(this, 'consequent', visitor, options);
103✔
1458
            walk(this, 'alternate', visitor, options);
103✔
1459
        }
1460
    }
1461
}
1462

1463
export class NullCoalescingExpression extends Expression {
1✔
1464
    constructor(
1465
        public consequent: Expression,
27✔
1466
        public questionQuestionToken: Token,
27✔
1467
        public alternate: Expression
27✔
1468
    ) {
1469
        super();
27✔
1470
        this.range = util.createBoundingRange(
27✔
1471
            consequent,
1472
            questionQuestionToken,
1473
            alternate
1474
        );
1475
    }
1476
    public readonly range: Range | undefined;
1477

1478
    transpile(state: BrsTranspileState) {
1479
        let result = [] as TranspileResult;
10✔
1480
        let consequentInfo = util.getExpressionInfo(this.consequent, state.file);
10✔
1481
        let alternateInfo = util.getExpressionInfo(this.alternate, state.file);
10✔
1482

1483
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1484
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
10✔
1485
        let hasMutatingExpression = [
10✔
1486
            ...consequentInfo.expressions,
1487
            ...alternateInfo.expressions
1488
        ].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e));
28✔
1489

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

1538
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1539
        if (options.walkMode & InternalWalkMode.walkExpressions) {
38!
1540
            walk(this, 'consequent', visitor, options);
38✔
1541
            walk(this, 'alternate', visitor, options);
38✔
1542
        }
1543
    }
1544
}
1545

1546
export class RegexLiteralExpression extends Expression {
1✔
1547
    public constructor(
1548
        public tokens: {
44✔
1549
            regexLiteral: Token;
1550
        }
1551
    ) {
1552
        super();
44✔
1553
    }
1554

1555
    public get range() {
1556
        return this.tokens?.regexLiteral?.range;
53!
1557
    }
1558

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

1574
        return [
42✔
1575
            state.sourceNode(this.tokens.regexLiteral, [
1576
                'CreateObject("roRegex", ',
1577
                `"${pattern}", `,
1578
                `"${flags}"`,
1579
                ')'
1580
            ])
1581
        ];
1582
    }
1583

1584
    walk(visitor: WalkVisitor, options: WalkOptions) {
1585
        //nothing to walk
1586
    }
1587
}
1588

1589

1590
export class TypeCastExpression extends Expression {
1✔
1591
    constructor(
1592
        public obj: Expression,
11✔
1593
        public asToken: Token,
11✔
1594
        public typeToken: Token
11✔
1595
    ) {
1596
        super();
11✔
1597
        this.range = util.createBoundingRange(
11✔
1598
            this.obj,
1599
            this.asToken,
1600
            this.typeToken
1601
        );
1602
    }
1603

1604
    public range: Range;
1605

1606
    public transpile(state: BrsTranspileState): TranspileResult {
1607
        return this.obj.transpile(state);
11✔
1608
    }
1609
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1610
        if (options.walkMode & InternalWalkMode.walkExpressions) {
22!
1611
            walk(this, 'obj', visitor, options);
22✔
1612
        }
1613
    }
1614
}
1615

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

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

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

1653
function numberExpressionToValue(expr: LiteralExpression, operator = '') {
6✔
1654
    if (isIntegerType(expr.type) || isLongIntegerType(expr.type)) {
7!
1655
        return parseInt(operator + expr.token.text);
7✔
1656
    } else {
1657
        return parseFloat(operator + expr.token.text);
×
1658
    }
1659
}
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