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

rokucommunity / brighterscript / #13903

11 Oct 2024 04:57PM UTC coverage: 88.99% (+0.8%) from 88.149%
#13903

push

TwitchBronBron
Better settings reload functionality

7194 of 8520 branches covered (84.44%)

Branch coverage included in aggregate %.

34 of 35 new or added lines in 2 files covered. (97.14%)

274 existing lines in 18 files now uncovered.

9601 of 10353 relevant lines covered (92.74%)

1780.31 hits per line

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

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

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

26
export class BinaryExpression extends Expression {
1✔
27
    constructor(
28
        public left: Expression,
352✔
29
        public operator: Token,
352✔
30
        public right: Expression
352✔
31
    ) {
32
        super();
352✔
33
        this.range = util.createBoundingRange(this.left, this.operator, this.right);
352✔
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) {
539!
50
            walk(this, 'left', visitor, options);
539✔
51
            walk(this, 'right', visitor, options);
539✔
52
        }
53
    }
54

55
    public clone() {
56
        return this.finalizeClone(
3✔
57
            new BinaryExpression(
58
                this.left?.clone(),
9✔
59
                util.cloneToken(this.operator),
60
                this.right?.clone()
9✔
61
            ),
62
            ['left', 'right']
63
        );
64
    }
65
}
66

67
export class CallExpression extends Expression {
1✔
68
    /**
69
     * Number of parameters that can be defined on a function
70
     *
71
     * Prior to Roku OS 11.5, this was 32
72
     * As of Roku OS 11.5, this is 63
73
     */
74
    static MaximumArguments = 63;
1✔
75

76
    constructor(
77
        readonly callee: Expression,
648✔
78
        /**
79
         * Can either be `(`, or `?(` for optional chaining
80
         */
81
        readonly openingParen: Token,
648✔
82
        readonly closingParen: Token,
648✔
83
        readonly args: Expression[],
648✔
84
        unused?: any
85
    ) {
86
        super();
648✔
87
        this.range = util.createBoundingRange(this.callee, this.openingParen, ...args ?? [], this.closingParen);
648✔
88
    }
89

90
    public readonly range: Range | undefined;
91

92
    /**
93
     * Get the name of the wrapping namespace (if it exists)
94
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
95
     */
96
    public get namespaceName() {
97
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
98
    }
99

100
    transpile(state: BrsTranspileState, nameOverride?: string) {
101
        let result: TranspileResult = [];
222✔
102

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

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

129
    walk(visitor: WalkVisitor, options: WalkOptions) {
130
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,380!
131
            walk(this, 'callee', visitor, options);
1,380✔
132
            walkArray(this.args, visitor, options, this);
1,380✔
133
        }
134
    }
135

136
    public clone() {
137
        return this.finalizeClone(
7✔
138
            new CallExpression(
139
                this.callee?.clone(),
21✔
140
                util.cloneToken(this.openingParen),
141
                util.cloneToken(this.closingParen),
142
                this.args?.map(e => e?.clone())
6✔
143
            ),
144
            ['callee', 'args']
145
        );
146
    }
147
}
148

149
export class FunctionExpression extends Expression implements TypedefProvider {
1✔
150
    constructor(
151
        readonly parameters: FunctionParameterExpression[],
1,821✔
152
        public body: Block,
1,821✔
153
        readonly functionType: Token | null,
1,821✔
154
        public end: Token,
1,821✔
155
        readonly leftParen: Token,
1,821✔
156
        readonly rightParen: Token,
1,821✔
157
        readonly asToken?: Token,
1,821✔
158
        readonly returnTypeToken?: Token
1,821✔
159
    ) {
160
        super();
1,821✔
161
        if (this.returnTypeToken) {
1,821✔
162
            this.returnType = util.tokenToBscType(this.returnTypeToken);
76✔
163
        } else if (this.functionType?.text.toLowerCase() === 'sub') {
1,745!
164
            this.returnType = new VoidType();
1,385✔
165
        } else {
166
            this.returnType = DynamicType.instance;
360✔
167
        }
168

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

389
    public type: BscType;
390

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

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

418
        return result;
110✔
419
    }
420

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

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

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

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

438
        return results;
7✔
439
    }
440

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

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

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

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

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

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

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

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

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

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

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

531
    public readonly range: Range | undefined;
532

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

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

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

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

577
    public readonly range: Range | undefined;
578

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

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

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

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

625
    public readonly range: Range | undefined;
626

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

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

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

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

686
    public readonly range: Range | undefined;
687

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

813
    public readonly range: Range | undefined;
814

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

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

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

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

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

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

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

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

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

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

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

916
}
917

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

928
    public readonly range: Range | undefined;
929

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

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

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

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

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

976

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

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

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

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

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

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

1027
    public readonly range: Range | undefined;
1028

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

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

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

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

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

1069
    public readonly range: Range;
1070

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

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

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

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

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

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

1118
    public readonly range: Range;
1119

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

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

1149
    transpile(state: BrsTranspileState) {
1150
        let text: string;
1151
        switch (this.token.kind) {
31✔
1152
            case TokenKind.SourceFilePathLiteral:
40✔
1153
                const pathUrl = fileUrl(state.srcPath);
3✔
1154
                text = `"${pathUrl.substring(0, 4)}" + "${pathUrl.substring(4)}"`;
3✔
1155
                break;
3✔
1156
            case TokenKind.SourceLineNumLiteral:
1157
                //TODO find first parent that has range, or default to -1
1158
                text = `${this.getClosestLineNumber()}`;
4✔
1159
                break;
4✔
1160
            case TokenKind.FunctionNameLiteral:
1161
                text = `"${this.getFunctionName(state, ParseMode.BrightScript)}"`;
4✔
1162
                break;
4✔
1163
            case TokenKind.SourceFunctionNameLiteral:
1164
                text = `"${this.getFunctionName(state, ParseMode.BrighterScript)}"`;
4✔
1165
                break;
4✔
1166
            case TokenKind.SourceLocationLiteral:
1167
                const locationUrl = fileUrl(state.srcPath);
3✔
1168
                //TODO find first parent that has range, or default to -1
1169
                text = `"${locationUrl.substring(0, 4)}" + "${locationUrl.substring(4)}:${this.getClosestLineNumber()}"`;
3✔
1170
                break;
3✔
1171
            case TokenKind.PkgPathLiteral:
1172
                let pkgPath1 = `pkg:/${state.file.pkgPath}`
2✔
1173
                    .replace(/\\/g, '/')
1174
                    .replace(/\.bs$/i, '.brs');
1175

1176
                text = `"${pkgPath1}"`;
2✔
1177
                break;
2✔
1178
            case TokenKind.PkgLocationLiteral:
1179
                let pkgPath2 = `pkg:/${state.file.pkgPath}`
2✔
1180
                    .replace(/\\/g, '/')
1181
                    .replace(/\.bs$/i, '.brs');
1182

1183
                text = `"${pkgPath2}:" + str(LINE_NUM)`;
2✔
1184
                break;
2✔
1185
            case TokenKind.LineNumLiteral:
1186
            default:
1187
                //use the original text (because it looks like a variable)
1188
                text = this.token.text;
9✔
1189
                break;
9✔
1190

1191
        }
1192
        return [
31✔
1193
            state.sourceNode(this, text)
1194
        ];
1195
    }
1196

1197
    walk(visitor: WalkVisitor, options: WalkOptions) {
1198
        //nothing to walk
1199
    }
1200

1201
    public clone() {
1202
        return this.finalizeClone(
1✔
1203
            new SourceLiteralExpression(
1204
                util.cloneToken(this.token)
1205
            )
1206
        );
1207
    }
1208
}
1209

1210
/**
1211
 * This expression transpiles and acts exactly like a CallExpression,
1212
 * except we need to uniquely identify these statements so we can
1213
 * do more type checking.
1214
 */
1215
export class NewExpression extends Expression {
1✔
1216
    constructor(
1217
        readonly newKeyword: Token,
43✔
1218
        readonly call: CallExpression
43✔
1219
    ) {
1220
        super();
43✔
1221
        this.range = util.createBoundingRange(this.newKeyword, this.call);
43✔
1222
    }
1223

1224
    /**
1225
     * The name of the class to initialize (with optional namespace prefixed)
1226
     */
1227
    public get className() {
1228
        //the parser guarantees the callee of a new statement's call object will be
1229
        //a NamespacedVariableNameExpression
1230
        return this.call.callee as NamespacedVariableNameExpression;
104✔
1231
    }
1232

1233
    public readonly range: Range | undefined;
1234

1235
    public transpile(state: BrsTranspileState) {
1236
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
10✔
1237
        const cls = state.file.getClassFileLink(
10✔
1238
            this.className.getName(ParseMode.BrighterScript),
1239
            namespace?.getName(ParseMode.BrighterScript)
30✔
1240
        )?.item;
10✔
1241
        //new statements within a namespace block can omit the leading namespace if the class resides in that same namespace.
1242
        //So we need to figure out if this is a namespace-omitted class, or if this class exists without a namespace.
1243
        return this.call.transpile(state, cls?.getName(ParseMode.BrightScript));
10✔
1244
    }
1245

1246
    walk(visitor: WalkVisitor, options: WalkOptions) {
1247
        if (options.walkMode & InternalWalkMode.walkExpressions) {
141!
1248
            walk(this, 'call', visitor, options);
141✔
1249
        }
1250
    }
1251

1252
    public clone() {
1253
        return this.finalizeClone(
2✔
1254
            new NewExpression(
1255
                util.cloneToken(this.newKeyword),
1256
                this.call?.clone()
6✔
1257
            ),
1258
            ['call']
1259
        );
1260
    }
1261
}
1262

1263
export class CallfuncExpression extends Expression {
1✔
1264
    constructor(
1265
        readonly callee: Expression,
28✔
1266
        readonly operator: Token,
28✔
1267
        readonly methodName: Identifier,
28✔
1268
        readonly openingParen: Token,
28✔
1269
        readonly args: Expression[],
28✔
1270
        readonly closingParen: Token
28✔
1271
    ) {
1272
        super();
28✔
1273
        this.range = util.createBoundingRange(
28✔
1274
            callee,
1275
            operator,
1276
            methodName,
1277
            openingParen,
1278
            ...args ?? [],
84✔
1279
            closingParen
1280
        );
1281
    }
1282

1283
    public readonly range: Range | undefined;
1284

1285
    /**
1286
     * Get the name of the wrapping namespace (if it exists)
1287
     * @deprecated use `.findAncestor(isNamespaceStatement)` instead.
1288
     */
1289
    public get namespaceName() {
UNCOV
1290
        return this.findAncestor<NamespaceStatement>(isNamespaceStatement)?.nameExpression;
×
1291
    }
1292

1293
    public transpile(state: BrsTranspileState) {
1294
        let result = [] as TranspileResult;
8✔
1295
        result.push(
8✔
1296
            ...this.callee.transpile(state),
1297
            state.sourceNode(this.operator, '.callfunc'),
1298
            state.transpileToken(this.openingParen),
1299
            //the name of the function
1300
            state.sourceNode(this.methodName, ['"', this.methodName.text, '"']),
1301
            ', '
1302
        );
1303
        //transpile args
1304
        //callfunc with zero args never gets called, so pass invalid as the first parameter if there are no args
1305
        if (this.args.length === 0) {
8✔
1306
            result.push('invalid');
5✔
1307
        } else {
1308
            for (let i = 0; i < this.args.length; i++) {
3✔
1309
                //add comma between args
1310
                if (i > 0) {
6✔
1311
                    result.push(', ');
3✔
1312
                }
1313
                let arg = this.args[i];
6✔
1314
                result.push(...arg.transpile(state));
6✔
1315
            }
1316
        }
1317
        result.push(
8✔
1318
            state.transpileToken(this.closingParen)
1319
        );
1320
        return result;
8✔
1321
    }
1322

1323
    walk(visitor: WalkVisitor, options: WalkOptions) {
1324
        if (options.walkMode & InternalWalkMode.walkExpressions) {
47!
1325
            walk(this, 'callee', visitor, options);
47✔
1326
            walkArray(this.args, visitor, options, this);
47✔
1327
        }
1328
    }
1329

1330
    public clone() {
1331
        return this.finalizeClone(
3✔
1332
            new CallfuncExpression(
1333
                this.callee?.clone(),
9✔
1334
                util.cloneToken(this.operator),
1335
                util.cloneToken(this.methodName),
1336
                util.cloneToken(this.openingParen),
1337
                this.args?.map(e => e?.clone()),
2✔
1338
                util.cloneToken(this.closingParen)
1339
            ),
1340
            ['callee', 'args']
1341
        );
1342
    }
1343
}
1344

1345
/**
1346
 * Since template strings can contain newlines, we need to concatenate multiple strings together with chr() calls.
1347
 * This is a single expression that represents the string contatenation of all parts of a single quasi.
1348
 */
1349
export class TemplateStringQuasiExpression extends Expression {
1✔
1350
    constructor(
1351
        readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>
108✔
1352
    ) {
1353
        super();
108✔
1354
        this.range = util.createBoundingRange(
108✔
1355
            ...expressions ?? []
324✔
1356
        );
1357
    }
1358
    readonly range: Range | undefined;
1359

1360
    transpile(state: BrsTranspileState, skipEmptyStrings = true) {
35✔
1361
        let result = [] as TranspileResult;
43✔
1362
        let plus = '';
43✔
1363
        for (let expression of this.expressions) {
43✔
1364
            //skip empty strings
1365
            //TODO what does an empty string literal expression look like?
1366
            if (expression.token.text === '' && skipEmptyStrings === true) {
68✔
1367
                continue;
27✔
1368
            }
1369
            result.push(
41✔
1370
                plus,
1371
                ...expression.transpile(state)
1372
            );
1373
            plus = ' + ';
41✔
1374
        }
1375
        return result;
43✔
1376
    }
1377

1378
    walk(visitor: WalkVisitor, options: WalkOptions) {
1379
        if (options.walkMode & InternalWalkMode.walkExpressions) {
158!
1380
            walkArray(this.expressions, visitor, options, this);
158✔
1381
        }
1382
    }
1383

1384
    public clone() {
1385
        return this.finalizeClone(
15✔
1386
            new TemplateStringQuasiExpression(
1387
                this.expressions?.map(e => e?.clone())
20✔
1388
            ),
1389
            ['expressions']
1390
        );
1391
    }
1392
}
1393

1394
export class TemplateStringExpression extends Expression {
1✔
1395
    constructor(
1396
        readonly openingBacktick: Token,
49✔
1397
        readonly quasis: TemplateStringQuasiExpression[],
49✔
1398
        readonly expressions: Expression[],
49✔
1399
        readonly closingBacktick: Token
49✔
1400
    ) {
1401
        super();
49✔
1402
        this.range = util.createBoundingRange(
49✔
1403
            openingBacktick,
1404
            quasis?.[0],
147✔
1405
            quasis?.[quasis?.length - 1],
291!
1406
            closingBacktick
1407
        );
1408
    }
1409

1410
    public readonly range: Range | undefined;
1411

1412
    transpile(state: BrsTranspileState) {
1413
        if (this.quasis.length === 1 && this.expressions.length === 0) {
20✔
1414
            return this.quasis[0].transpile(state);
10✔
1415
        }
1416
        let result = ['('];
10✔
1417
        let plus = '';
10✔
1418
        //helper function to figure out when to include the plus
1419
        function add(...items) {
1420
            if (items.length > 0) {
40✔
1421
                result.push(
29✔
1422
                    plus,
1423
                    ...items
1424
                );
1425
            }
1426
            //set the plus after the first occurance of a nonzero length set of items
1427
            if (plus === '' && items.length > 0) {
40✔
1428
                plus = ' + ';
10✔
1429
            }
1430
        }
1431

1432
        for (let i = 0; i < this.quasis.length; i++) {
10✔
1433
            let quasi = this.quasis[i];
25✔
1434
            let expression = this.expressions[i];
25✔
1435

1436
            add(
25✔
1437
                ...quasi.transpile(state)
1438
            );
1439
            if (expression) {
25✔
1440
                //skip the toString wrapper around certain expressions
1441
                if (
15✔
1442
                    isEscapedCharCodeLiteralExpression(expression) ||
34✔
1443
                    (isLiteralExpression(expression) && isStringType(expression.type))
1444
                ) {
1445
                    add(
3✔
1446
                        ...expression.transpile(state)
1447
                    );
1448

1449
                    //wrap all other expressions with a bslib_toString call to prevent runtime type mismatch errors
1450
                } else {
1451
                    add(
12✔
1452
                        state.bslibPrefix + '_toString(',
1453
                        ...expression.transpile(state),
1454
                        ')'
1455
                    );
1456
                }
1457
            }
1458
        }
1459
        //the expression should be wrapped in parens so it can be used line a single expression at runtime
1460
        result.push(')');
10✔
1461

1462
        return result;
10✔
1463
    }
1464

1465
    walk(visitor: WalkVisitor, options: WalkOptions) {
1466
        if (options.walkMode & InternalWalkMode.walkExpressions) {
73!
1467
            //walk the quasis and expressions in left-to-right order
1468
            for (let i = 0; i < this.quasis?.length; i++) {
73✔
1469
                walk(this.quasis, i, visitor, options, this);
129✔
1470

1471
                //this skips the final loop iteration since we'll always have one more quasi than expression
1472
                if (this.expressions[i]) {
129✔
1473
                    walk(this.expressions, i, visitor, options, this);
56✔
1474
                }
1475
            }
1476
        }
1477
    }
1478

1479
    public clone() {
1480
        return this.finalizeClone(
7✔
1481
            new TemplateStringExpression(
1482
                util.cloneToken(this.openingBacktick),
1483
                this.quasis?.map(e => e?.clone()),
12✔
1484
                this.expressions?.map(e => e?.clone()),
6✔
1485
                util.cloneToken(this.closingBacktick)
1486
            ),
1487
            ['quasis', 'expressions']
1488
        );
1489
    }
1490
}
1491

1492
export class TaggedTemplateStringExpression extends Expression {
1✔
1493
    constructor(
1494
        readonly tagName: Identifier,
12✔
1495
        readonly openingBacktick: Token,
12✔
1496
        readonly quasis: TemplateStringQuasiExpression[],
12✔
1497
        readonly expressions: Expression[],
12✔
1498
        readonly closingBacktick: Token
12✔
1499
    ) {
1500
        super();
12✔
1501
        this.range = util.createBoundingRange(
12✔
1502
            tagName,
1503
            openingBacktick,
1504
            quasis?.[0],
36✔
1505
            quasis?.[quasis?.length - 1],
69!
1506
            closingBacktick
1507
        );
1508
    }
1509

1510
    public readonly range: Range | undefined;
1511

1512
    transpile(state: BrsTranspileState) {
1513
        let result = [] as TranspileResult;
3✔
1514
        result.push(
3✔
1515
            state.transpileToken(this.tagName),
1516
            '(['
1517
        );
1518

1519
        //add quasis as the first array
1520
        for (let i = 0; i < this.quasis.length; i++) {
3✔
1521
            let quasi = this.quasis[i];
8✔
1522
            //separate items with a comma
1523
            if (i > 0) {
8✔
1524
                result.push(
5✔
1525
                    ', '
1526
                );
1527
            }
1528
            result.push(
8✔
1529
                ...quasi.transpile(state, false)
1530
            );
1531
        }
1532
        result.push(
3✔
1533
            '], ['
1534
        );
1535

1536
        //add expressions as the second array
1537
        for (let i = 0; i < this.expressions.length; i++) {
3✔
1538
            let expression = this.expressions[i];
5✔
1539
            if (i > 0) {
5✔
1540
                result.push(
2✔
1541
                    ', '
1542
                );
1543
            }
1544
            result.push(
5✔
1545
                ...expression.transpile(state)
1546
            );
1547
        }
1548
        result.push(
3✔
1549
            state.sourceNode(this.closingBacktick, '])')
1550
        );
1551
        return result;
3✔
1552
    }
1553

1554
    walk(visitor: WalkVisitor, options: WalkOptions) {
1555
        if (options.walkMode & InternalWalkMode.walkExpressions) {
16!
1556
            //walk the quasis and expressions in left-to-right order
1557
            for (let i = 0; i < this.quasis?.length; i++) {
16✔
1558
                walk(this.quasis, i, visitor, options, this);
35✔
1559

1560
                //this skips the final loop iteration since we'll always have one more quasi than expression
1561
                if (this.expressions[i]) {
35✔
1562
                    walk(this.expressions, i, visitor, options, this);
19✔
1563
                }
1564
            }
1565
        }
1566
    }
1567

1568
    public clone() {
1569
        return this.finalizeClone(
3✔
1570
            new TaggedTemplateStringExpression(
1571
                util.cloneToken(this.tagName),
1572
                util.cloneToken(this.openingBacktick),
1573
                this.quasis?.map(e => e?.clone()),
5✔
1574
                this.expressions?.map(e => e?.clone()),
3✔
1575
                util.cloneToken(this.closingBacktick)
1576
            ),
1577
            ['quasis', 'expressions']
1578
        );
1579
    }
1580
}
1581

1582
export class AnnotationExpression extends Expression {
1✔
1583
    constructor(
1584
        readonly atToken: Token,
70✔
1585
        readonly nameToken: Token
70✔
1586
    ) {
1587
        super();
70✔
1588
        this.name = nameToken.text;
70✔
1589
    }
1590

1591
    public get range() {
1592
        return util.createBoundingRange(
32✔
1593
            this.atToken,
1594
            this.nameToken,
1595
            this.call
1596
        );
1597
    }
1598

1599
    public name: string;
1600
    public call: CallExpression | undefined;
1601

1602
    /**
1603
     * Convert annotation arguments to JavaScript types
1604
     * @param strict If false, keep Expression objects not corresponding to JS types
1605
     */
1606
    getArguments(strict = true): ExpressionValue[] {
10✔
1607
        if (!this.call) {
11✔
1608
            return [];
1✔
1609
        }
1610
        return this.call.args.map(e => expressionToValue(e, strict));
20✔
1611
    }
1612

1613
    transpile(state: BrsTranspileState) {
1614
        return [];
3✔
1615
    }
1616

1617
    walk(visitor: WalkVisitor, options: WalkOptions) {
1618
        //nothing to walk
1619
    }
1620
    getTypedef(state: BrsTranspileState) {
1621
        return [
9✔
1622
            '@',
1623
            this.name,
1624
            ...(this.call?.transpile(state) ?? [])
54✔
1625
        ];
1626
    }
1627

1628
    public clone() {
1629
        const clone = this.finalizeClone(
8✔
1630
            new AnnotationExpression(
1631
                util.cloneToken(this.atToken),
1632
                util.cloneToken(this.nameToken)
1633
            )
1634
        );
1635
        return clone;
8✔
1636
    }
1637
}
1638

1639
export class TernaryExpression extends Expression {
1✔
1640
    constructor(
1641
        readonly test: Expression,
78✔
1642
        readonly questionMarkToken: Token,
78✔
1643
        readonly consequent?: Expression,
78✔
1644
        readonly colonToken?: Token,
78✔
1645
        readonly alternate?: Expression
78✔
1646
    ) {
1647
        super();
78✔
1648
        this.range = util.createBoundingRange(
78✔
1649
            test,
1650
            questionMarkToken,
1651
            consequent,
1652
            colonToken,
1653
            alternate
1654
        );
1655
    }
1656

1657
    public range: Range | undefined;
1658

1659
    transpile(state: BrsTranspileState) {
1660
        let result = [] as TranspileResult;
27✔
1661
        const file = state.file;
27✔
1662
        let consequentInfo = util.getExpressionInfo(this.consequent!, file);
27✔
1663
        let alternateInfo = util.getExpressionInfo(this.alternate!, file);
27✔
1664

1665
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1666
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
27✔
1667
        let mutatingExpressions = [
27✔
1668
            ...consequentInfo.expressions,
1669
            ...alternateInfo.expressions
1670
        ].filter(e => e instanceof CallExpression || e instanceof CallfuncExpression || e instanceof DottedGetExpression);
126✔
1671

1672
        if (mutatingExpressions.length > 0) {
27✔
1673
            result.push(
8✔
1674
                state.sourceNode(
1675
                    this.questionMarkToken,
1676
                    //write all the scope variables as parameters.
1677
                    //TODO handle when there are more than 31 parameters
1678
                    `(function(${['__bsCondition', ...allUniqueVarNames].join(', ')})`
1679
                ),
1680
                state.newline,
1681
                //double indent so our `end function` line is still indented one at the end
1682
                state.indent(2),
1683
                state.sourceNode(this.test, `if __bsCondition then`),
1684
                state.newline,
1685
                state.indent(1),
1686
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
24!
1687
                ...this.consequent?.transpile(state) ?? [state.sourceNode(this.questionMarkToken, 'invalid')],
48!
1688
                state.newline,
1689
                state.indent(-1),
1690
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'else'),
24!
1691
                state.newline,
1692
                state.indent(1),
1693
                state.sourceNode(this.consequent ?? this.questionMarkToken, 'return '),
24!
1694
                ...this.alternate?.transpile(state) ?? [state.sourceNode(this.consequent ?? this.questionMarkToken, 'invalid')],
48!
1695
                state.newline,
1696
                state.indent(-1),
1697
                state.sourceNode(this.questionMarkToken, 'end if'),
1698
                state.newline,
1699
                state.indent(-1),
1700
                state.sourceNode(this.questionMarkToken, 'end function)('),
1701
                ...this.test.transpile(state),
1702
                state.sourceNode(this.questionMarkToken, `${['', ...allUniqueVarNames].join(', ')})`)
1703
            );
1704
            state.blockDepth--;
8✔
1705
        } else {
1706
            result.push(
19✔
1707
                state.sourceNode(this.test, state.bslibPrefix + `_ternary(`),
1708
                ...this.test.transpile(state),
1709
                state.sourceNode(this.test, `, `),
1710
                ...this.consequent?.transpile(state) ?? ['invalid'],
114✔
1711
                `, `,
1712
                ...this.alternate?.transpile(state) ?? ['invalid'],
114✔
1713
                `)`
1714
            );
1715
        }
1716
        return result;
27✔
1717
    }
1718

1719
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1720
        if (options.walkMode & InternalWalkMode.walkExpressions) {
107!
1721
            walk(this, 'test', visitor, options);
107✔
1722
            walk(this, 'consequent', visitor, options);
107✔
1723
            walk(this, 'alternate', visitor, options);
107✔
1724
        }
1725
    }
1726

1727
    public clone() {
1728
        return this.finalizeClone(
2✔
1729
            new TernaryExpression(
1730
                this.test?.clone(),
6✔
1731
                util.cloneToken(this.questionMarkToken),
1732
                this.consequent?.clone(),
6✔
1733
                util.cloneToken(this.colonToken),
1734
                this.alternate?.clone()
6✔
1735
            ),
1736
            ['test', 'consequent', 'alternate']
1737
        );
1738
    }
1739
}
1740

1741
export class NullCoalescingExpression extends Expression {
1✔
1742
    constructor(
1743
        public consequent: Expression,
31✔
1744
        public questionQuestionToken: Token,
31✔
1745
        public alternate: Expression
31✔
1746
    ) {
1747
        super();
31✔
1748
        this.range = util.createBoundingRange(
31✔
1749
            consequent,
1750
            questionQuestionToken,
1751
            alternate
1752
        );
1753
    }
1754
    public readonly range: Range | undefined;
1755

1756
    transpile(state: BrsTranspileState) {
1757
        let result = [] as TranspileResult;
10✔
1758
        let consequentInfo = util.getExpressionInfo(this.consequent, state.file);
10✔
1759
        let alternateInfo = util.getExpressionInfo(this.alternate, state.file);
10✔
1760

1761
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
1762
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
10✔
1763
        let hasMutatingExpression = [
10✔
1764
            ...consequentInfo.expressions,
1765
            ...alternateInfo.expressions
1766
        ].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e));
28✔
1767

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

1816
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1817
        if (options.walkMode & InternalWalkMode.walkExpressions) {
42!
1818
            walk(this, 'consequent', visitor, options);
42✔
1819
            walk(this, 'alternate', visitor, options);
42✔
1820
        }
1821
    }
1822

1823
    public clone() {
1824
        return this.finalizeClone(
2✔
1825
            new NullCoalescingExpression(
1826
                this.consequent?.clone(),
6✔
1827
                util.cloneToken(this.questionQuestionToken),
1828
                this.alternate?.clone()
6✔
1829
            ),
1830
            ['consequent', 'alternate']
1831
        );
1832
    }
1833
}
1834

1835
export class RegexLiteralExpression extends Expression {
1✔
1836
    public constructor(
1837
        public tokens: {
46✔
1838
            regexLiteral: Token;
1839
        }
1840
    ) {
1841
        super();
46✔
1842
    }
1843

1844
    public get range() {
1845
        return this.tokens?.regexLiteral?.range;
55!
1846
    }
1847

1848
    public transpile(state: BrsTranspileState): TranspileResult {
1849
        let text = this.tokens.regexLiteral?.text ?? '';
42!
1850
        let flags = '';
42✔
1851
        //get any flags from the end
1852
        const flagMatch = /\/([a-z]+)$/i.exec(text);
42✔
1853
        if (flagMatch) {
42✔
1854
            text = text.substring(0, flagMatch.index + 1);
2✔
1855
            flags = flagMatch[1];
2✔
1856
        }
1857
        let pattern = text
42✔
1858
            //remove leading and trailing slashes
1859
            .substring(1, text.length - 1)
1860
            //escape quotemarks
1861
            .split('"').join('" + chr(34) + "');
1862

1863
        return [
42✔
1864
            state.sourceNode(this.tokens.regexLiteral, [
1865
                'CreateObject("roRegex", ',
1866
                `"${pattern}", `,
1867
                `"${flags}"`,
1868
                ')'
1869
            ])
1870
        ];
1871
    }
1872

1873
    walk(visitor: WalkVisitor, options: WalkOptions) {
1874
        //nothing to walk
1875
    }
1876

1877
    public clone() {
1878
        return this.finalizeClone(
1✔
1879
            new RegexLiteralExpression({
1880
                regexLiteral: util.cloneToken(this.tokens.regexLiteral)
1881
            })
1882
        );
1883
    }
1884
}
1885

1886

1887
export class TypeCastExpression extends Expression {
1✔
1888
    constructor(
1889
        public obj: Expression,
15✔
1890
        public asToken: Token,
15✔
1891
        public typeToken: Token
15✔
1892
    ) {
1893
        super();
15✔
1894
        this.range = util.createBoundingRange(
15✔
1895
            this.obj,
1896
            this.asToken,
1897
            this.typeToken
1898
        );
1899
    }
1900

1901
    public range: Range;
1902

1903
    public transpile(state: BrsTranspileState): TranspileResult {
1904
        return this.obj.transpile(state);
11✔
1905
    }
1906
    public walk(visitor: WalkVisitor, options: WalkOptions) {
1907
        if (options.walkMode & InternalWalkMode.walkExpressions) {
26!
1908
            walk(this, 'obj', visitor, options);
26✔
1909
        }
1910
    }
1911

1912
    public clone() {
1913
        return this.finalizeClone(
2✔
1914
            new TypeCastExpression(
1915
                this.obj?.clone(),
6✔
1916
                util.cloneToken(this.asToken),
1917
                util.cloneToken(this.typeToken)
1918
            ),
1919
            ['obj']
1920
        );
1921
    }
1922
}
1923

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

1927
function expressionToValue(expr: Expression, strict: boolean): ExpressionValue {
1928
    if (!expr) {
30!
UNCOV
1929
        return null;
×
1930
    }
1931
    if (isUnaryExpression(expr) && isLiteralNumber(expr.right)) {
30✔
1932
        return numberExpressionToValue(expr.right, expr.operator.text);
1✔
1933
    }
1934
    if (isLiteralString(expr)) {
29✔
1935
        //remove leading and trailing quotes
1936
        return expr.token.text.replace(/^"/, '').replace(/"$/, '');
5✔
1937
    }
1938
    if (isLiteralNumber(expr)) {
24✔
1939
        return numberExpressionToValue(expr);
11✔
1940
    }
1941

1942
    if (isLiteralBoolean(expr)) {
13✔
1943
        return expr.token.text.toLowerCase() === 'true';
3✔
1944
    }
1945
    if (isArrayLiteralExpression(expr)) {
10✔
1946
        return expr.elements
3✔
1947
            .filter(e => !isCommentStatement(e))
7✔
1948
            .map(e => expressionToValue(e, strict));
7✔
1949
    }
1950
    if (isAALiteralExpression(expr)) {
7✔
1951
        return expr.elements.reduce((acc, e) => {
3✔
1952
            if (!isCommentStatement(e)) {
3!
1953
                acc[e.keyToken.text] = expressionToValue(e.value, strict);
3✔
1954
            }
1955
            return acc;
3✔
1956
        }, {});
1957
    }
1958
    //for annotations, we only support serializing pure string values
1959
    if (isTemplateStringExpression(expr)) {
4✔
1960
        if (expr.quasis?.length === 1 && expr.expressions.length === 0) {
2!
1961
            return expr.quasis[0].expressions.map(x => x.token.text).join('');
10✔
1962
        }
1963
    }
1964
    return strict ? null : expr;
2✔
1965
}
1966

1967
function numberExpressionToValue(expr: LiteralExpression, operator = '') {
11✔
1968
    if (isIntegerType(expr.type) || isLongIntegerType(expr.type)) {
12!
1969
        return parseInt(operator + expr.token.text);
12✔
1970
    } else {
UNCOV
1971
        return parseFloat(operator + expr.token.text);
×
1972
    }
1973
}
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