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

rokucommunity / brighterscript / #13308

22 Nov 2024 02:25PM UTC coverage: 86.801%. Remained the same
#13308

push

web-flow
Merge 332332a1f into 2a6afd921

11833 of 14419 branches covered (82.07%)

Branch coverage included in aggregate %.

191 of 205 new or added lines in 26 files covered. (93.17%)

201 existing lines in 18 files now uncovered.

12868 of 14038 relevant lines covered (91.67%)

32022.22 hits per line

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

92.84
/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, NamespaceStatement } from './Statement';
5
import type { Location } 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 { WalkMode } from '../astUtils/visitors';
1✔
12
import { walk, InternalWalkMode, walkArray } from '../astUtils/visitors';
1✔
13
import { isAALiteralExpression, isAAMemberExpression, isArrayLiteralExpression, isArrayType, isCallExpression, isCallableType, isCallfuncExpression, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isIntegerType, isInterfaceMethodStatement, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isNewExpression, isPrimitiveType, isReferenceType, isStringType, isTemplateStringExpression, isTypecastExpression, isUnaryExpression, isVariableExpression } from '../astUtils/reflection';
1✔
14
import type { GetTypeOptions, TranspileResult, TypedefProvider } from '../interfaces';
15
import { TypeChainEntry } from '../interfaces';
1✔
16
import { VoidType } from '../types/VoidType';
1✔
17
import { DynamicType } from '../types/DynamicType';
1✔
18
import type { BscType } from '../types/BscType';
19
import type { AstNode } from './AstNode';
20
import { AstNodeKind, Expression } from './AstNode';
1✔
21
import { SymbolTable } from '../SymbolTable';
1✔
22
import { SourceNode } from 'source-map';
1✔
23
import type { TranspileState } from './TranspileState';
24
import { StringType } from '../types/StringType';
1✔
25
import { TypePropertyReferenceType } from '../types/ReferenceType';
1✔
26
import { UnionType } from '../types/UnionType';
1✔
27
import { ArrayType } from '../types/ArrayType';
1✔
28
import { AssociativeArrayType } from '../types/AssociativeArrayType';
1✔
29
import { TypedFunctionType } from '../types';
1✔
30
import { SymbolTypeFlag } from '../SymbolTypeFlag';
1✔
31
import { FunctionType } from '../types/FunctionType';
1✔
32
import type { BaseFunctionType } from '../types/BaseFunctionType';
33
import { brsDocParser } from './BrightScriptDocParser';
1✔
34

35
export type ExpressionVisitor = (expression: Expression, parent: Expression) => void;
36

37
export class BinaryExpression extends Expression {
1✔
38
    constructor(options: {
39
        left: Expression;
40
        operator: Token;
41
        right: Expression;
42
    }) {
43
        super();
3,178✔
44
        this.tokens = {
3,178✔
45
            operator: options.operator
46
        };
47
        this.left = options.left;
3,178✔
48
        this.right = options.right;
3,178✔
49
        this.location = util.createBoundingLocation(this.left, this.tokens.operator, this.right);
3,178✔
50
    }
51
    readonly tokens: {
52
        readonly operator: Token;
53
    };
54

55
    public readonly left: Expression;
56
    public readonly right: Expression;
57

58
    public readonly kind = AstNodeKind.BinaryExpression;
3,178✔
59

60
    public readonly location: Location | undefined;
61

62
    transpile(state: BrsTranspileState): TranspileResult {
63
        return [
3,184✔
64
            state.sourceNode(this.left, this.left.transpile(state)),
65
            ' ',
66
            state.transpileToken(this.tokens.operator),
67
            ' ',
68
            state.sourceNode(this.right, this.right.transpile(state))
69
        ];
70
    }
71

72
    walk(visitor: WalkVisitor, options: WalkOptions) {
73
        if (options.walkMode & InternalWalkMode.walkExpressions) {
10,380!
74
            walk(this, 'left', visitor, options);
10,380✔
75
            walk(this, 'right', visitor, options);
10,380✔
76
        }
77
    }
78

79

80
    public getType(options: GetTypeOptions): BscType {
81
        const operatorKind = this.tokens.operator.kind;
352✔
82
        if (options.flags & SymbolTypeFlag.typetime) {
352✔
83
            // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
84
            switch (operatorKind) {
79✔
85
                case TokenKind.Or:
78✔
86
                    return new UnionType([this.left.getType(options), this.right.getType(options)]);
78✔
87
                //TODO: Intersection Types?, eg. case TokenKind.And:
88
            }
89
        } else if (options.flags & SymbolTypeFlag.runtime) {
273!
90
            return util.binaryOperatorResultType(
273✔
91
                this.left.getType(options),
92
                this.tokens.operator,
93
                this.right.getType(options));
94
        }
95
        return DynamicType.instance;
1✔
96
    }
97

98
    get leadingTrivia(): Token[] {
99
        return this.left.leadingTrivia;
1,629✔
100
    }
101

102
    public clone() {
103
        return this.finalizeClone(
3✔
104
            new BinaryExpression({
105
                left: this.left?.clone(),
9✔
106
                operator: util.cloneToken(this.tokens.operator),
107
                right: this.right?.clone()
9✔
108
            }),
109
            ['left', 'right']
110
        );
111
    }
112
}
113

114

115
export class CallExpression extends Expression {
1✔
116
    /**
117
     * Number of parameters that can be defined on a function
118
     *
119
     * Prior to Roku OS 11.5, this was 32
120
     * As of Roku OS 11.5, this is 63
121
     */
122
    static MaximumArguments = 63;
1✔
123

124
    constructor(options: {
125
        callee: Expression;
126
        openingParen?: Token;
127
        args?: Expression[];
128
        closingParen?: Token;
129
    }) {
130
        super();
2,381✔
131
        this.tokens = {
2,381✔
132
            openingParen: options.openingParen,
133
            closingParen: options.closingParen
134
        };
135
        this.callee = options.callee;
2,381✔
136
        this.args = options.args ?? [];
2,381✔
137
        this.location = util.createBoundingLocation(this.callee, this.tokens.openingParen, ...this.args ?? [], this.tokens.closingParen);
2,381!
138
    }
139

140
    readonly callee: Expression;
141
    readonly args: Expression[];
142
    readonly tokens: {
143
        /**
144
         * Can either be `(`, or `?(` for optional chaining - defaults to '('
145
         */
146
        readonly openingParen?: Token;
147
        readonly closingParen?: Token;
148
    };
149

150
    public readonly kind = AstNodeKind.CallExpression;
2,381✔
151

152
    public readonly location: Location | undefined;
153

154
    transpile(state: BrsTranspileState, nameOverride?: string) {
155
        let result: TranspileResult = [];
1,594✔
156

157
        //transpile the name
158
        if (nameOverride) {
1,594✔
159
            result.push(
12✔
160
                //transpile leading comments since we're bypassing callee.transpile (which would normally do this)
161
                ...state.transpileLeadingCommentsForAstNode(this),
162
                state.sourceNode(this.callee, nameOverride)
163
            );
164
        } else {
165
            result.push(...this.callee.transpile(state));
1,582✔
166
        }
167

168
        result.push(
1,594✔
169
            state.transpileToken(this.tokens.openingParen, '(')
170
        );
171
        for (let i = 0; i < this.args.length; i++) {
1,594✔
172
            //add comma between args
173
            if (i > 0) {
1,147✔
174
                result.push(', ');
353✔
175
            }
176
            let arg = this.args[i];
1,147✔
177
            result.push(...arg.transpile(state));
1,147✔
178
        }
179
        result.push(
1,594✔
180
            state.transpileToken(this.tokens.closingParen, ')')
181
        );
182
        return result;
1,594✔
183
    }
184

185
    walk(visitor: WalkVisitor, options: WalkOptions) {
186
        if (options.walkMode & InternalWalkMode.walkExpressions) {
9,955!
187
            walk(this, 'callee', visitor, options);
9,955✔
188
            walkArray(this.args, visitor, options, this);
9,955✔
189
        }
190
    }
191

192
    getType(options: GetTypeOptions) {
193
        const calleeType = this.callee.getType(options);
980✔
194
        if (options.ignoreCall) {
980✔
195
            return calleeType;
1✔
196
        }
197
        if (isNewExpression(this.parent)) {
979✔
198
            return calleeType;
322✔
199
        }
200
        const specialCaseReturnType = util.getSpecialCaseCallExpressionReturnType(this, options);
657✔
201
        if (specialCaseReturnType) {
657✔
202
            return specialCaseReturnType;
112✔
203
        }
204
        if (isCallableType(calleeType) && (!isReferenceType(calleeType.returnType) || calleeType.returnType?.isResolvable())) {
545!
205
            return calleeType.returnType;
244✔
206
        }
207
        if (!isReferenceType(calleeType) && (calleeType as BaseFunctionType)?.returnType?.isResolvable()) {
301✔
208
            return (calleeType as BaseFunctionType).returnType;
257✔
209
        }
210
        return new TypePropertyReferenceType(calleeType, 'returnType');
44✔
211
    }
212

213
    get leadingTrivia(): Token[] {
214
        return this.callee.leadingTrivia;
8,113✔
215
    }
216

217
    public clone() {
218
        return this.finalizeClone(
7✔
219
            new CallExpression({
220
                callee: this.callee?.clone(),
21✔
221
                openingParen: util.cloneToken(this.tokens.openingParen),
222
                closingParen: util.cloneToken(this.tokens.closingParen),
223
                args: this.args?.map(e => e?.clone())
6✔
224
            }),
225
            ['callee', 'args']
226
        );
227
    }
228
}
229

230
export class FunctionExpression extends Expression implements TypedefProvider {
1✔
231
    constructor(options: {
232
        functionType?: Token;
233
        leftParen?: Token;
234
        parameters?: FunctionParameterExpression[];
235
        rightParen?: Token;
236
        as?: Token;
237
        returnTypeExpression?: TypeExpression;
238
        body: Block;
239
        endFunctionType?: Token;
240
    }) {
241
        super();
3,780✔
242
        this.tokens = {
3,780✔
243
            functionType: options.functionType,
244
            leftParen: options.leftParen,
245
            rightParen: options.rightParen,
246
            as: options.as,
247
            endFunctionType: options.endFunctionType
248
        };
249
        this.parameters = options.parameters ?? [];
3,780✔
250
        this.body = options.body;
3,780✔
251
        this.returnTypeExpression = options.returnTypeExpression;
3,780✔
252
        //if there's a body, and it doesn't have a SymbolTable, assign one
253
        if (this.body) {
3,780✔
254
            if (!this.body.symbolTable) {
3,779!
255
                this.body.symbolTable = new SymbolTable(`Block`, () => this.getSymbolTable());
27,309✔
256
            } else {
UNCOV
257
                this.body.symbolTable.pushParentProvider(() => this.getSymbolTable());
×
258
            }
259
            this.body.parent = this;
3,779✔
260
        }
261
        this.symbolTable = new SymbolTable('FunctionExpression', () => this.parent?.getSymbolTable());
33,753!
262
    }
263

264
    public readonly kind = AstNodeKind.FunctionExpression;
3,780✔
265

266
    readonly parameters: FunctionParameterExpression[];
267
    public readonly body: Block;
268
    public readonly returnTypeExpression?: TypeExpression;
269

270
    readonly tokens: {
271
        readonly functionType?: Token;
272
        readonly endFunctionType?: Token;
273
        readonly leftParen?: Token;
274
        readonly rightParen?: Token;
275
        readonly as?: Token;
276
    };
277

278
    public get leadingTrivia(): Token[] {
279
        return this.tokens.functionType?.leadingTrivia;
27,203✔
280
    }
281

282
    public get endTrivia(): Token[] {
283
        return this.tokens.endFunctionType?.leadingTrivia;
2!
284
    }
285

286
    /**
287
     * The range of the function, starting at the 'f' in function or 's' in sub (or the open paren if the keyword is missing),
288
     * and ending with the last n' in 'end function' or 'b' in 'end sub'
289
     */
290
    public get location(): Location {
291
        return util.createBoundingLocation(
9,689✔
292
            this.tokens.functionType,
293
            this.tokens.leftParen,
294
            ...this.parameters ?? [],
29,067!
295
            this.tokens.rightParen,
296
            this.tokens.as,
297
            this.returnTypeExpression,
298
            this.tokens.endFunctionType
299
        );
300
    }
301

302
    transpile(state: BrsTranspileState, name?: Identifier, includeBody = true) {
1,428✔
303
        let results = [] as TranspileResult;
1,428✔
304
        //'function'|'sub'
305
        results.push(
1,428✔
306
            state.transpileToken(this.tokens.functionType, 'function', false, state.skipLeadingComments)
307
        );
308
        //functionName?
309
        if (name) {
1,428✔
310
            results.push(
1,357✔
311
                ' ',
312
                state.transpileToken(name)
313
            );
314
        }
315
        //leftParen
316
        results.push(
1,428✔
317
            state.transpileToken(this.tokens.leftParen, '(')
318
        );
319
        //parameters
320
        for (let i = 0; i < this.parameters.length; i++) {
1,428✔
321
            let param = this.parameters[i];
2,156✔
322
            //add commas
323
            if (i > 0) {
2,156✔
324
                results.push(', ');
1,062✔
325
            }
326
            //add parameter
327
            results.push(param.transpile(state));
2,156✔
328
        }
329
        //right paren
330
        results.push(
1,428✔
331
            state.transpileToken(this.tokens.rightParen, ')')
332
        );
333
        //as [Type]
334
        if (!state.options.removeParameterTypes && this.returnTypeExpression) {
1,428✔
335
            results.push(
40✔
336
                ' ',
337
                //as
338
                state.transpileToken(this.tokens.as, 'as'),
339
                ' ',
340
                //return type
341
                ...this.returnTypeExpression.transpile(state)
342
            );
343
        }
344
        let hasBody = false;
1,428✔
345
        if (includeBody) {
1,428!
346
            state.lineage.unshift(this);
1,428✔
347
            let body = this.body.transpile(state);
1,428✔
348
            hasBody = body.length > 0;
1,428✔
349
            state.lineage.shift();
1,428✔
350
            results.push(...body);
1,428✔
351
        }
352

353
        const lastLocatable = hasBody ? this.body : this.returnTypeExpression ?? this.tokens.leftParen ?? this.tokens.functionType;
1,428!
354
        results.push(
1,428✔
355
            ...state.transpileEndBlockToken(lastLocatable, this.tokens.endFunctionType, `end ${this.tokens.functionType ?? 'function'}`)
4,284✔
356
        );
357
        return results;
1,428✔
358
    }
359

360
    getTypedef(state: BrsTranspileState) {
361
        let results = [
62✔
362
            new SourceNode(1, 0, null, [
363
                //'function'|'sub'
364
                this.tokens.functionType?.text ?? 'function',
372!
365
                //functionName?
366
                ...(isFunctionStatement(this.parent) || isMethodStatement(this.parent) ? [' ', this.parent.tokens.name?.text ?? ''] : []),
519!
367
                //leftParen
368
                '(',
369
                //parameters
370
                ...(
371
                    this.parameters?.map((param, i) => ([
73!
372
                        //separating comma
373
                        i > 0 ? ', ' : '',
73✔
374
                        ...param.getTypedef(state)
375
                    ])) ?? []
62!
376
                ) as any,
377
                //right paren
378
                ')',
379
                //as <ReturnType>
380
                ...(this.returnTypeExpression ? [
62✔
381
                    ' ',
382
                    this.tokens.as?.text ?? 'as',
18!
383
                    ' ',
384
                    ...this.returnTypeExpression.getTypedef(state)
385
                ] : []),
386
                '\n',
387
                state.indent(),
388
                //'end sub'|'end function'
389
                this.tokens.endFunctionType?.text ?? `end ${this.tokens.functionType ?? 'function'}`
372!
390
            ])
391
        ];
392
        return results;
62✔
393
    }
394

395
    walk(visitor: WalkVisitor, options: WalkOptions) {
396
        if (options.walkMode & InternalWalkMode.walkExpressions) {
16,282!
397
            walkArray(this.parameters, visitor, options, this);
16,282✔
398
            walk(this, 'returnTypeExpression', visitor, options);
16,282✔
399
            //This is the core of full-program walking...it allows us to step into sub functions
400
            if (options.walkMode & InternalWalkMode.recurseChildFunctions) {
16,282✔
401
                walk(this, 'body', visitor, options);
16,278✔
402
            }
403
        }
404
    }
405

406
    public getType(options: GetTypeOptions): TypedFunctionType {
407
        //if there's a defined return type, use that
408
        let returnType: BscType;
409

410
        const docs = brsDocParser.parseNode(this.findAncestor(isFunctionStatement));
4,035✔
411

412
        returnType = util.chooseTypeFromCodeOrDocComment(
4,035✔
413
            this.returnTypeExpression?.getType({ ...options, typeChain: undefined }),
12,105✔
UNCOV
414
            docs.getReturnBscType({ ...options, tableProvider: () => this.getSymbolTable() }),
×
415
            options
416
        );
417

418
        const isSub = this.tokens.functionType?.kind === TokenKind.Sub;
4,035✔
419
        //if we don't have a return type and this is a sub, set the return type to `void`. else use `dynamic`
420
        if (!returnType) {
4,035✔
421
            returnType = isSub ? VoidType.instance : DynamicType.instance;
3,307✔
422
        }
423

424
        const resultType = new TypedFunctionType(returnType);
4,035✔
425
        resultType.isSub = isSub;
4,035✔
426
        for (let param of this.parameters) {
4,035✔
427
            resultType.addParameter(param.tokens.name.text, param.getType({ ...options, typeChain: undefined }), !!param.defaultValue);
2,112✔
428
        }
429
        // Figure out this function's name if we can
430
        let funcName = '';
4,035✔
431
        if (isMethodStatement(this.parent) || isInterfaceMethodStatement(this.parent)) {
4,035✔
432
            funcName = this.parent.getName(ParseMode.BrighterScript);
343✔
433
            if (options.typeChain) {
343✔
434
                // Get the typechain info from the parent class
435
                this.parent.parent?.getType(options);
1!
436
            }
437
        } else if (isFunctionStatement(this.parent)) {
3,692✔
438
            funcName = this.parent.getName(ParseMode.BrighterScript);
3,623✔
439
        }
440
        if (funcName) {
4,035✔
441
            resultType.setName(funcName);
3,966✔
442
        }
443
        options.typeChain?.push(new TypeChainEntry({ name: funcName, type: resultType, data: options.data, astNode: this }));
4,035✔
444
        return resultType;
4,035✔
445
    }
446

447
    public clone() {
448
        return this.finalizeClone(
110✔
449
            new FunctionExpression({
450
                parameters: this.parameters?.map(e => e?.clone()),
7✔
451
                body: this.body?.clone(),
330✔
452
                functionType: util.cloneToken(this.tokens.functionType),
453
                endFunctionType: util.cloneToken(this.tokens.endFunctionType),
454
                leftParen: util.cloneToken(this.tokens.leftParen),
455
                rightParen: util.cloneToken(this.tokens.rightParen),
456
                as: util.cloneToken(this.tokens.as),
457
                returnTypeExpression: this.returnTypeExpression?.clone()
330✔
458
            }),
459
            ['body', 'returnTypeExpression']
460
        );
461
    }
462
}
463

464
export class FunctionParameterExpression extends Expression {
1✔
465
    constructor(options: {
466
        name: Identifier;
467
        equals?: Token;
468
        defaultValue?: Expression;
469
        as?: Token;
470
        typeExpression?: TypeExpression;
471
    }) {
472
        super();
2,965✔
473
        this.tokens = {
2,965✔
474
            name: options.name,
475
            equals: options.equals,
476
            as: options.as
477
        };
478
        this.defaultValue = options.defaultValue;
2,965✔
479
        this.typeExpression = options.typeExpression;
2,965✔
480
    }
481

482
    public readonly kind = AstNodeKind.FunctionParameterExpression;
2,965✔
483

484
    readonly tokens: {
485
        readonly name: Identifier;
486
        readonly equals?: Token;
487
        readonly as?: Token;
488
    };
489

490
    public readonly defaultValue?: Expression;
491
    public readonly typeExpression?: TypeExpression;
492

493
    public getType(options: GetTypeOptions) {
494
        const docs = brsDocParser.parseNode(this.findAncestor(isFunctionStatement));
5,384✔
495
        const paramName = this.tokens.name.text;
5,384✔
496

497
        const paramTypeFromCode = this.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime, typeChain: undefined }) ??
5,384✔
498
            this.defaultValue?.getType({ ...options, flags: SymbolTypeFlag.runtime, typeChain: undefined });
8,262✔
499
        const paramTypeFromDoc = docs.getParamBscType(paramName, { ...options, fullName: paramName, tableProvider: () => this.getSymbolTable() });
5,384✔
500

501
        let paramType = util.chooseTypeFromCodeOrDocComment(paramTypeFromCode, paramTypeFromDoc, options) ?? DynamicType.instance;
5,384✔
502
        options.typeChain?.push(new TypeChainEntry({ name: paramName, type: paramType, data: options.data, astNode: this }));
5,384✔
503
        return paramType;
5,384✔
504
    }
505

506
    public get location(): Location | undefined {
507
        return util.createBoundingLocation(
8,644✔
508
            this.tokens.name,
509
            this.tokens.as,
510
            this.typeExpression,
511
            this.tokens.equals,
512
            this.defaultValue
513
        );
514
    }
515

516
    public transpile(state: BrsTranspileState) {
517
        let result: TranspileResult = [
2,164✔
518
            //name
519
            state.transpileToken(this.tokens.name)
520
        ];
521
        //default value
522
        if (this.defaultValue) {
2,164✔
523
            result.push(' = ');
9✔
524
            result.push(this.defaultValue.transpile(state));
9✔
525
        }
526
        //type declaration
527
        if (this.typeExpression && !state.options.removeParameterTypes) {
2,164✔
528
            result.push(' ');
65✔
529
            result.push(state.transpileToken(this.tokens.as, 'as'));
65✔
530
            result.push(' ');
65✔
531
            result.push(
65✔
532
                ...(this.typeExpression?.transpile(state) ?? [])
390!
533
            );
534
        }
535

536
        return result;
2,164✔
537
    }
538

539
    public getTypedef(state: BrsTranspileState): TranspileResult {
540
        const results = [this.tokens.name.text] as TranspileResult;
73✔
541

542
        if (this.defaultValue) {
73!
UNCOV
543
            results.push(' = ', ...this.defaultValue.transpile(state));
×
544
        }
545

546
        if (this.tokens.as) {
73✔
547
            results.push(' as ');
6✔
548

549
            // TODO: Is this conditional needed? Will typeToken always exist
550
            // so long as `asToken` exists?
551
            if (this.typeExpression) {
6!
552
                results.push(...(this.typeExpression?.getTypedef(state) ?? ['']));
6!
553
            }
554
        }
555

556
        return results;
73✔
557
    }
558

559
    walk(visitor: WalkVisitor, options: WalkOptions) {
560
        // eslint-disable-next-line no-bitwise
561
        if (options.walkMode & InternalWalkMode.walkExpressions) {
12,317!
562
            walk(this, 'defaultValue', visitor, options);
12,317✔
563
            walk(this, 'typeExpression', visitor, options);
12,317✔
564
        }
565
    }
566

567
    get leadingTrivia(): Token[] {
568
        return this.tokens.name.leadingTrivia;
4,548✔
569
    }
570

571
    public clone() {
572
        return this.finalizeClone(
8✔
573
            new FunctionParameterExpression({
574
                name: util.cloneToken(this.tokens.name),
575
                as: util.cloneToken(this.tokens.as),
576
                typeExpression: this.typeExpression?.clone(),
24✔
577
                equals: util.cloneToken(this.tokens.equals),
578
                defaultValue: this.defaultValue?.clone()
24✔
579
            }),
580
            ['typeExpression', 'defaultValue']
581
        );
582
    }
583
}
584

585
export class DottedGetExpression extends Expression {
1✔
586
    constructor(options: {
587
        obj: Expression;
588
        name: Identifier;
589
        /**
590
         * Can either be `.`, or `?.` for optional chaining - defaults in transpile to '.'
591
         */
592
        dot?: Token;
593
    }) {
594
        super();
2,781✔
595
        this.tokens = {
2,781✔
596
            name: options.name,
597
            dot: options.dot
598
        };
599
        this.obj = options.obj;
2,781✔
600

601
        this.location = util.createBoundingLocation(this.obj, this.tokens.dot, this.tokens.name);
2,781✔
602
    }
603

604
    readonly tokens: {
605
        readonly name: Identifier;
606
        readonly dot?: Token;
607
    };
608
    readonly obj: Expression;
609

610
    public readonly kind = AstNodeKind.DottedGetExpression;
2,781✔
611

612
    public readonly location: Location | undefined;
613

614
    transpile(state: BrsTranspileState) {
615
        //if the callee starts with a namespace name, transpile the name
616
        if (state.file.calleeStartsWithNamespace(this)) {
899✔
617
            return [
9✔
618
                ...state.transpileLeadingCommentsForAstNode(this),
619
                state.sourceNode(this, this.getName(ParseMode.BrightScript))
620
            ];
621
        } else {
622
            return [
890✔
623
                ...this.obj.transpile(state),
624
                state.transpileToken(this.tokens.dot, '.'),
625
                state.transpileToken(this.tokens.name)
626
            ];
627
        }
628
    }
629

630
    walk(visitor: WalkVisitor, options: WalkOptions) {
631
        if (options.walkMode & InternalWalkMode.walkExpressions) {
10,521!
632
            walk(this, 'obj', visitor, options);
10,521✔
633
        }
634
    }
635

636
    getType(options: GetTypeOptions) {
637
        const objType = this.obj?.getType(options);
6,236!
638
        let result = objType?.getMemberType(this.tokens.name?.text, options);
6,236!
639

640
        if (util.isClassUsedAsFunction(result, this, options)) {
6,236✔
641
            // treat this class constructor as a function
642
            result = FunctionType.instance;
11✔
643
        }
644
        options.typeChain?.push(new TypeChainEntry({
6,236✔
645
            name: this.tokens.name?.text,
7,992!
646
            type: result,
647
            data: options.data,
648
            location: this.tokens.name?.location ?? this.location,
15,984!
649
            astNode: this
650
        }));
651
        if (result ||
6,236✔
652
            options.flags & SymbolTypeFlag.typetime ||
653
            (isPrimitiveType(objType) || isCallableType(objType))) {
654
            // All types should be known at typeTime, or the obj is well known
655
            return result;
6,205✔
656
        }
657
        // It is possible at runtime that a value has been added dynamically to an object, or something
658
        // TODO: maybe have a strict flag on this?
659
        return DynamicType.instance;
31✔
660
    }
661

662
    getName(parseMode: ParseMode) {
663
        return util.getAllDottedGetPartsAsString(this, parseMode);
35✔
664
    }
665

666
    get leadingTrivia(): Token[] {
667
        return this.obj.leadingTrivia;
14,215✔
668
    }
669

670
    public clone() {
671
        return this.finalizeClone(
6✔
672
            new DottedGetExpression({
673
                obj: this.obj?.clone(),
18✔
674
                dot: util.cloneToken(this.tokens.dot),
675
                name: util.cloneToken(this.tokens.name)
676
            }),
677
            ['obj']
678
        );
679
    }
680
}
681

682
export class XmlAttributeGetExpression extends Expression {
1✔
683
    constructor(options: {
684
        obj: Expression;
685
        /**
686
         * Can either be `@`, or `?@` for optional chaining - defaults to '@'
687
         */
688
        at?: Token;
689
        name: Identifier;
690
    }) {
691
        super();
14✔
692
        this.obj = options.obj;
14✔
693
        this.tokens = { at: options.at, name: options.name };
14✔
694
        this.location = util.createBoundingLocation(this.obj, this.tokens.at, this.tokens.name);
14✔
695
    }
696

697
    public readonly kind = AstNodeKind.XmlAttributeGetExpression;
14✔
698

699
    public readonly tokens: {
700
        name: Identifier;
701
        at?: Token;
702
    };
703

704
    public readonly obj: Expression;
705

706
    public readonly location: Location | undefined;
707

708
    transpile(state: BrsTranspileState) {
709
        return [
3✔
710
            ...this.obj.transpile(state),
711
            state.transpileToken(this.tokens.at, '@'),
712
            state.transpileToken(this.tokens.name)
713
        ];
714
    }
715

716
    walk(visitor: WalkVisitor, options: WalkOptions) {
717
        if (options.walkMode & InternalWalkMode.walkExpressions) {
30!
718
            walk(this, 'obj', visitor, options);
30✔
719
        }
720
    }
721

722
    get leadingTrivia(): Token[] {
723
        return this.obj.leadingTrivia;
15✔
724
    }
725

726
    public clone() {
727
        return this.finalizeClone(
2✔
728
            new XmlAttributeGetExpression({
729
                obj: this.obj?.clone(),
6✔
730
                at: util.cloneToken(this.tokens.at),
731
                name: util.cloneToken(this.tokens.name)
732
            }),
733
            ['obj']
734
        );
735
    }
736
}
737

738
export class IndexedGetExpression extends Expression {
1✔
739
    constructor(options: {
740
        obj: Expression;
741
        indexes: Expression[];
742
        /**
743
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.` - defaults to '[' in transpile
744
         */
745
        openingSquare?: Token;
746
        closingSquare?: Token;
747
        questionDot?: Token;//  ? or ?.
748
    }) {
749
        super();
159✔
750
        this.tokens = {
159✔
751
            openingSquare: options.openingSquare,
752
            closingSquare: options.closingSquare,
753
            questionDot: options.questionDot
754
        };
755
        this.obj = options.obj;
159✔
756
        this.indexes = options.indexes;
159✔
757
        this.location = util.createBoundingLocation(
159✔
758
            this.obj,
759
            this.tokens.openingSquare,
760
            this.tokens.questionDot,
761
            this.tokens.openingSquare,
762
            ...this.indexes ?? [],
477✔
763
            this.tokens.closingSquare
764
        );
765
    }
766

767
    public readonly kind = AstNodeKind.IndexedGetExpression;
159✔
768

769
    public readonly obj: Expression;
770
    public readonly indexes: Expression[];
771

772
    readonly tokens: {
773
        /**
774
         * Can either be `[` or `?[`. If `?.[` is used, this will be `[` and `optionalChainingToken` will be `?.` - defaults to '[' in transpile
775
         */
776
        readonly openingSquare?: Token;
777
        readonly closingSquare?: Token;
778
        readonly questionDot?: Token; //  ? or ?.
779
    };
780

781
    public readonly location: Location | undefined;
782

783
    transpile(state: BrsTranspileState) {
784
        const result = [];
64✔
785
        result.push(
64✔
786
            ...this.obj.transpile(state),
787
            this.tokens.questionDot ? state.transpileToken(this.tokens.questionDot) : '',
64✔
788
            state.transpileToken(this.tokens.openingSquare, '[')
789
        );
790
        for (let i = 0; i < this.indexes.length; i++) {
64✔
791
            //add comma between indexes
792
            if (i > 0) {
72✔
793
                result.push(', ');
8✔
794
            }
795
            let index = this.indexes[i];
72✔
796
            result.push(
72✔
797
                ...(index?.transpile(state) ?? [])
432!
798
            );
799
        }
800
        result.push(
64✔
801
            state.transpileToken(this.tokens.closingSquare, ']')
802
        );
803
        return result;
64✔
804
    }
805

806
    walk(visitor: WalkVisitor, options: WalkOptions) {
807
        if (options.walkMode & InternalWalkMode.walkExpressions) {
500!
808
            walk(this, 'obj', visitor, options);
500✔
809
            walkArray(this.indexes, visitor, options, this);
500✔
810
        }
811
    }
812

813
    getType(options: GetTypeOptions): BscType {
814
        const objType = this.obj.getType(options);
201✔
815
        if (isArrayType(objType)) {
201✔
816
            // This is used on an array. What is the default type of that array?
817
            return objType.defaultType;
10✔
818
        }
819
        return super.getType(options);
191✔
820
    }
821

822
    get leadingTrivia(): Token[] {
823
        return this.obj.leadingTrivia;
1,021✔
824
    }
825

826
    public clone() {
827
        return this.finalizeClone(
5✔
828
            new IndexedGetExpression({
829
                obj: this.obj?.clone(),
15✔
830
                questionDot: util.cloneToken(this.tokens.questionDot),
831
                openingSquare: util.cloneToken(this.tokens.openingSquare),
832
                indexes: this.indexes?.map(x => x?.clone()),
6✔
833
                closingSquare: util.cloneToken(this.tokens.closingSquare)
834
            }),
835
            ['obj', 'indexes']
836
        );
837
    }
838
}
839

840
export class GroupingExpression extends Expression {
1✔
841
    constructor(options: {
842
        leftParen?: Token;
843
        rightParen?: Token;
844
        expression: Expression;
845
    }) {
846
        super();
52✔
847
        this.tokens = {
52✔
848
            rightParen: options.rightParen,
849
            leftParen: options.leftParen
850
        };
851
        this.expression = options.expression;
52✔
852
        this.location = util.createBoundingLocation(this.tokens.leftParen, this.expression, this.tokens.rightParen);
52✔
853
    }
854

855
    public readonly tokens: {
856
        readonly leftParen?: Token;
857
        readonly rightParen?: Token;
858
    };
859
    public readonly expression: Expression;
860

861
    public readonly kind = AstNodeKind.GroupingExpression;
52✔
862

863
    public readonly location: Location | undefined;
864

865
    transpile(state: BrsTranspileState) {
866
        if (isTypecastExpression(this.expression)) {
13✔
867
            return this.expression.transpile(state);
7✔
868
        }
869
        return [
6✔
870
            state.transpileToken(this.tokens.leftParen, '('),
871
            ...this.expression.transpile(state),
872
            state.transpileToken(this.tokens.rightParen, ')')
873
        ];
874
    }
875

876
    walk(visitor: WalkVisitor, options: WalkOptions) {
877
        if (options.walkMode & InternalWalkMode.walkExpressions) {
168!
878
            walk(this, 'expression', visitor, options);
168✔
879
        }
880
    }
881

882
    getType(options: GetTypeOptions) {
883
        return this.expression.getType(options);
69✔
884
    }
885

886
    get leadingTrivia(): Token[] {
887
        return this.tokens.leftParen?.leadingTrivia;
278!
888
    }
889

890
    public clone() {
891
        return this.finalizeClone(
2✔
892
            new GroupingExpression({
893
                leftParen: util.cloneToken(this.tokens.leftParen),
894
                expression: this.expression?.clone(),
6✔
895
                rightParen: util.cloneToken(this.tokens.rightParen)
896
            }),
897
            ['expression']
898
        );
899
    }
900
}
901

902
export class LiteralExpression extends Expression {
1✔
903
    constructor(options: {
904
        value: Token;
905
    }) {
906
        super();
7,542✔
907
        this.tokens = {
7,542✔
908
            value: options.value
909
        };
910
    }
911

912
    public readonly tokens: {
913
        readonly value: Token;
914
    };
915

916
    public readonly kind = AstNodeKind.LiteralExpression;
7,542✔
917

918
    public get location() {
919
        return this.tokens.value.location;
21,356✔
920
    }
921

922
    public getType(options?: GetTypeOptions) {
923
        return util.tokenToBscType(this.tokens.value);
3,349✔
924
    }
925

926
    transpile(state: BrsTranspileState) {
927
        let text: string;
928
        if (this.tokens.value.kind === TokenKind.TemplateStringQuasi) {
4,558✔
929
            //wrap quasis with quotes (and escape inner quotemarks)
930
            text = `"${this.tokens.value.text.replace(/"/g, '""')}"`;
28✔
931

932
        } else if (this.tokens.value.kind === TokenKind.StringLiteral) {
4,530✔
933
            text = this.tokens.value.text;
3,032✔
934
            //add trailing quotemark if it's missing. We will have already generated a diagnostic for this.
935
            if (text.endsWith('"') === false) {
3,032✔
936
                text += '"';
1✔
937
            }
938
        } else {
939
            text = this.tokens.value.text;
1,498✔
940
        }
941

942
        return [
4,558✔
943
            state.transpileToken({ ...this.tokens.value, text: text })
944
        ];
945
    }
946

947
    walk(visitor: WalkVisitor, options: WalkOptions) {
948
        //nothing to walk
949
    }
950

951
    get leadingTrivia(): Token[] {
952
        return this.tokens.value.leadingTrivia;
11,196✔
953
    }
954

955
    public clone() {
956
        return this.finalizeClone(
99✔
957
            new LiteralExpression({
958
                value: util.cloneToken(this.tokens.value)
959
            })
960
        );
961
    }
962
}
963

964
/**
965
 * This is a special expression only used within template strings. It exists so we can prevent producing lots of empty strings
966
 * during template string transpile by identifying these expressions explicitly and skipping the bslib_toString around them
967
 */
968
export class EscapedCharCodeLiteralExpression extends Expression {
1✔
969
    constructor(options: {
970
        value: Token & { charCode: number };
971
    }) {
972
        super();
35✔
973
        this.tokens = { value: options.value };
35✔
974
        this.location = util.cloneLocation(this.tokens.value.location);
35✔
975
    }
976

977
    public readonly kind = AstNodeKind.EscapedCharCodeLiteralExpression;
35✔
978

979
    public readonly tokens: {
980
        readonly value: Token & { charCode: number };
981
    };
982

983
    public readonly location: Location;
984

985
    transpile(state: BrsTranspileState) {
986
        return [
13✔
987
            state.sourceNode(this, `chr(${this.tokens.value.charCode})`)
988
        ];
989
    }
990

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

995
    public clone() {
996
        return this.finalizeClone(
3✔
997
            new EscapedCharCodeLiteralExpression({
998
                value: util.cloneToken(this.tokens.value)
999
            })
1000
        );
1001
    }
1002
}
1003

1004
export class ArrayLiteralExpression extends Expression {
1✔
1005
    constructor(options: {
1006
        elements: Array<Expression>;
1007
        open?: Token;
1008
        close?: Token;
1009
    }) {
1010
        super();
145✔
1011
        this.tokens = {
145✔
1012
            open: options.open,
1013
            close: options.close
1014
        };
1015
        this.elements = options.elements;
145✔
1016
        this.location = util.createBoundingLocation(this.tokens.open, ...this.elements ?? [], this.tokens.close);
145✔
1017
    }
1018

1019
    public readonly elements: Array<Expression>;
1020

1021
    public readonly tokens: {
1022
        readonly open?: Token;
1023
        readonly close?: Token;
1024
    };
1025

1026
    public readonly kind = AstNodeKind.ArrayLiteralExpression;
145✔
1027

1028
    public readonly location: Location | undefined;
1029

1030
    transpile(state: BrsTranspileState) {
1031
        let result: TranspileResult = [];
52✔
1032
        result.push(
52✔
1033
            state.transpileToken(this.tokens.open, '[')
1034
        );
1035
        let hasChildren = this.elements.length > 0;
52✔
1036
        state.blockDepth++;
52✔
1037

1038
        for (let i = 0; i < this.elements.length; i++) {
52✔
1039
            let previousElement = this.elements[i - 1];
57✔
1040
            let element = this.elements[i];
57✔
1041

1042
            if (util.isLeadingCommentOnSameLine(previousElement ?? this.tokens.open, element)) {
57✔
1043
                result.push(' ');
3✔
1044
            } else {
1045
                result.push(
54✔
1046
                    '\n',
1047
                    state.indent()
1048
                );
1049
            }
1050
            result.push(
57✔
1051
                ...element.transpile(state)
1052
            );
1053
        }
1054
        state.blockDepth--;
52✔
1055
        //add a newline between open and close if there are elements
1056
        const lastLocatable = this.elements[this.elements.length - 1] ?? this.tokens.open;
52✔
1057
        result.push(...state.transpileEndBlockToken(lastLocatable, this.tokens.close, ']', hasChildren));
52✔
1058

1059
        return result;
52✔
1060
    }
1061

1062
    walk(visitor: WalkVisitor, options: WalkOptions) {
1063
        if (options.walkMode & InternalWalkMode.walkExpressions) {
665!
1064
            walkArray(this.elements, visitor, options, this);
665✔
1065
        }
1066
    }
1067

1068
    getType(options: GetTypeOptions): BscType {
1069
        const innerTypes = this.elements.map(expr => expr.getType(options));
174✔
1070
        return new ArrayType(...innerTypes);
134✔
1071
    }
1072
    get leadingTrivia(): Token[] {
1073
        return this.tokens.open?.leadingTrivia;
453!
1074
    }
1075

1076
    get endTrivia(): Token[] {
1077
        return this.tokens.close?.leadingTrivia;
2!
1078
    }
1079

1080
    public clone() {
1081
        return this.finalizeClone(
4✔
1082
            new ArrayLiteralExpression({
1083
                elements: this.elements?.map(e => e?.clone()),
6✔
1084
                open: util.cloneToken(this.tokens.open),
1085
                close: util.cloneToken(this.tokens.close)
1086
            }),
1087
            ['elements']
1088
        );
1089
    }
1090
}
1091

1092
export class AAMemberExpression extends Expression {
1✔
1093
    constructor(options: {
1094
        key: Token;
1095
        colon?: Token;
1096
        /** The expression evaluated to determine the member's initial value. */
1097
        value: Expression;
1098
        comma?: Token;
1099
    }) {
1100
        super();
286✔
1101
        this.tokens = {
286✔
1102
            key: options.key,
1103
            colon: options.colon,
1104
            comma: options.comma
1105
        };
1106
        this.value = options.value;
286✔
1107
        this.location = util.createBoundingLocation(this.tokens.key, this.tokens.colon, this.value);
286✔
1108
    }
1109

1110
    public readonly kind = AstNodeKind.AAMemberExpression;
286✔
1111

1112
    public readonly location: Location | undefined;
1113

1114
    public readonly tokens: {
1115
        readonly key: Token;
1116
        readonly colon?: Token;
1117
        readonly comma?: Token;
1118
    };
1119

1120
    /** The expression evaluated to determine the member's initial value. */
1121
    public readonly value: Expression;
1122

1123
    transpile(state: BrsTranspileState) {
1124
        //TODO move the logic from AALiteralExpression loop into this function
UNCOV
1125
        return [];
×
1126
    }
1127

1128
    walk(visitor: WalkVisitor, options: WalkOptions) {
1129
        walk(this, 'value', visitor, options);
981✔
1130
    }
1131

1132
    getType(options: GetTypeOptions): BscType {
1133
        return this.value.getType(options);
194✔
1134
    }
1135

1136
    get leadingTrivia(): Token[] {
1137
        return this.tokens.key.leadingTrivia;
730✔
1138
    }
1139

1140
    public clone() {
1141
        return this.finalizeClone(
4✔
1142
            new AAMemberExpression({
1143
                key: util.cloneToken(this.tokens.key),
1144
                colon: util.cloneToken(this.tokens.colon),
1145
                value: this.value?.clone()
12✔
1146
            }),
1147
            ['value']
1148
        );
1149
    }
1150
}
1151

1152
export class AALiteralExpression extends Expression {
1✔
1153
    constructor(options: {
1154
        elements: Array<AAMemberExpression>;
1155
        open?: Token;
1156
        close?: Token;
1157
    }) {
1158
        super();
281✔
1159
        this.tokens = {
281✔
1160
            open: options.open,
1161
            close: options.close
1162
        };
1163
        this.elements = options.elements;
281✔
1164
        this.location = util.createBoundingLocation(this.tokens.open, ...this.elements ?? [], this.tokens.close);
281✔
1165
    }
1166

1167
    public readonly elements: Array<AAMemberExpression>;
1168
    public readonly tokens: {
1169
        readonly open?: Token;
1170
        readonly close?: Token;
1171
    };
1172

1173
    public readonly kind = AstNodeKind.AALiteralExpression;
281✔
1174

1175
    public readonly location: Location | undefined;
1176

1177
    transpile(state: BrsTranspileState) {
1178
        let result: TranspileResult = [];
59✔
1179
        //open curly
1180
        result.push(
59✔
1181
            state.transpileToken(this.tokens.open, '{')
1182
        );
1183
        let hasChildren = this.elements.length > 0;
59✔
1184
        //add newline if the object has children and the first child isn't a comment starting on the same line as opening curly
1185
        if (hasChildren && !util.isLeadingCommentOnSameLine(this.tokens.open, this.elements[0])) {
59✔
1186
            result.push('\n');
24✔
1187
        }
1188
        state.blockDepth++;
59✔
1189
        for (let i = 0; i < this.elements.length; i++) {
59✔
1190
            let element = this.elements[i];
36✔
1191
            let previousElement = this.elements[i - 1];
36✔
1192
            let nextElement = this.elements[i + 1];
36✔
1193

1194
            //don't indent if comment is same-line
1195
            if (util.isLeadingCommentOnSameLine(this.tokens.open, element) ||
36✔
1196
                util.isLeadingCommentOnSameLine(previousElement, element)) {
1197
                result.push(' ');
7✔
1198
            } else {
1199
                //indent line
1200
                result.push(state.indent());
29✔
1201
            }
1202

1203
            //key
1204
            result.push(
36✔
1205
                state.transpileToken(element.tokens.key)
1206
            );
1207
            //colon
1208
            result.push(
36✔
1209
                state.transpileToken(element.tokens.colon, ':'),
1210
                ' '
1211
            );
1212
            //value
1213
            result.push(...element.value.transpile(state));
36✔
1214

1215
            //if next element is a same-line comment, skip the newline
1216
            if (nextElement && !util.isLeadingCommentOnSameLine(element, nextElement)) {
36✔
1217
                //add a newline between statements
1218
                result.push('\n');
5✔
1219
            }
1220
        }
1221
        state.blockDepth--;
59✔
1222

1223
        const lastElement = this.elements[this.elements.length - 1] ?? this.tokens.open;
59✔
1224
        result.push(...state.transpileEndBlockToken(lastElement, this.tokens.close, '}', hasChildren));
59✔
1225

1226
        return result;
59✔
1227
    }
1228

1229
    walk(visitor: WalkVisitor, options: WalkOptions) {
1230
        if (options.walkMode & InternalWalkMode.walkExpressions) {
1,039!
1231
            walkArray(this.elements, visitor, options, this);
1,039✔
1232
        }
1233
    }
1234

1235
    getType(options: GetTypeOptions): BscType {
1236
        const resultType = new AssociativeArrayType();
199✔
1237
        resultType.addBuiltInInterfaces();
199✔
1238
        for (const element of this.elements) {
199✔
1239
            if (isAAMemberExpression(element)) {
194!
1240
                resultType.addMember(element.tokens.key.text, { definingNode: element }, element.getType(options), SymbolTypeFlag.runtime);
194✔
1241
            }
1242
        }
1243
        return resultType;
199✔
1244
    }
1245

1246
    public get leadingTrivia(): Token[] {
1247
        return this.tokens.open?.leadingTrivia;
738!
1248
    }
1249

1250
    public get endTrivia(): Token[] {
1251
        return this.tokens.close?.leadingTrivia;
1!
1252
    }
1253

1254
    public clone() {
1255
        return this.finalizeClone(
6✔
1256
            new AALiteralExpression({
1257
                elements: this.elements?.map(e => e?.clone()),
5✔
1258
                open: util.cloneToken(this.tokens.open),
1259
                close: util.cloneToken(this.tokens.close)
1260
            }),
1261
            ['elements']
1262
        );
1263
    }
1264
}
1265

1266
export class UnaryExpression extends Expression {
1✔
1267
    constructor(options: {
1268
        operator: Token;
1269
        right: Expression;
1270
    }) {
1271
        super();
66✔
1272
        this.tokens = {
66✔
1273
            operator: options.operator
1274
        };
1275
        this.right = options.right;
66✔
1276
        this.location = util.createBoundingLocation(this.tokens.operator, this.right);
66✔
1277
    }
1278

1279
    public readonly kind = AstNodeKind.UnaryExpression;
66✔
1280

1281
    public readonly location: Location | undefined;
1282

1283
    public readonly tokens: {
1284
        readonly operator: Token;
1285
    };
1286
    public readonly right: Expression;
1287

1288
    transpile(state: BrsTranspileState) {
1289
        let separatingWhitespace: string | undefined;
1290
        if (isVariableExpression(this.right)) {
12✔
1291
            separatingWhitespace = this.right.tokens.name.leadingWhitespace;
6✔
1292
        } else if (isLiteralExpression(this.right)) {
6✔
1293
            separatingWhitespace = this.right.tokens.value.leadingWhitespace;
2✔
1294
        } else {
1295
            separatingWhitespace = ' ';
4✔
1296
        }
1297

1298
        return [
12✔
1299
            state.transpileToken(this.tokens.operator),
1300
            separatingWhitespace,
1301
            ...this.right.transpile(state)
1302
        ];
1303
    }
1304

1305
    walk(visitor: WalkVisitor, options: WalkOptions) {
1306
        if (options.walkMode & InternalWalkMode.walkExpressions) {
236!
1307
            walk(this, 'right', visitor, options);
236✔
1308
        }
1309
    }
1310

1311
    getType(options: GetTypeOptions): BscType {
1312
        return util.unaryOperatorResultType(this.tokens.operator, this.right.getType(options));
48✔
1313
    }
1314

1315
    public get leadingTrivia(): Token[] {
1316
        return this.tokens.operator.leadingTrivia;
185✔
1317
    }
1318

1319
    public clone() {
1320
        return this.finalizeClone(
2✔
1321
            new UnaryExpression({
1322
                operator: util.cloneToken(this.tokens.operator),
1323
                right: this.right?.clone()
6✔
1324
            }),
1325
            ['right']
1326
        );
1327
    }
1328
}
1329

1330
export class VariableExpression extends Expression {
1✔
1331
    constructor(options: {
1332
        name: Identifier;
1333
    }) {
1334
        super();
10,768✔
1335
        this.tokens = {
10,768✔
1336
            name: options.name
1337
        };
1338
        this.location = util.cloneLocation(this.tokens.name?.location);
10,768!
1339
    }
1340

1341
    public readonly tokens: {
1342
        readonly name: Identifier;
1343
    };
1344

1345
    public readonly kind = AstNodeKind.VariableExpression;
10,768✔
1346

1347
    public readonly location: Location;
1348

1349
    public getName(parseMode?: ParseMode) {
1350
        return this.tokens.name.text;
21,888✔
1351
    }
1352

1353
    transpile(state: BrsTranspileState) {
1354
        let result: TranspileResult = [];
6,196✔
1355
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
6,196✔
1356
        //if the callee is the name of a known namespace function
1357
        if (namespace && util.isCalleeMemberOfNamespace(this.tokens.name.text, this, namespace)) {
6,196✔
1358
            result.push(
17✔
1359
                //transpile leading comments since the token isn't being transpiled directly
1360
                ...state.transpileLeadingCommentsForAstNode(this),
1361
                state.sourceNode(this, [
1362
                    namespace.getName(ParseMode.BrightScript),
1363
                    '_',
1364
                    this.getName(ParseMode.BrightScript)
1365
                ])
1366
            );
1367
            //transpile  normally
1368
        } else {
1369
            result.push(
6,179✔
1370
                state.transpileToken(this.tokens.name)
1371
            );
1372
        }
1373
        return result;
6,196✔
1374
    }
1375

1376
    walk(visitor: WalkVisitor, options: WalkOptions) {
1377
        //nothing to walk
1378
    }
1379

1380

1381
    getType(options: GetTypeOptions) {
1382
        let resultType: BscType = util.tokenToBscType(this.tokens.name);
21,741✔
1383
        const nameKey = this.getName();
21,741✔
1384
        if (!resultType) {
21,741✔
1385
            const symbolTable = this.getSymbolTable();
17,281✔
1386
            resultType = symbolTable?.getSymbolType(nameKey, { ...options, fullName: nameKey, tableProvider: () => this.getSymbolTable() });
34,400!
1387

1388
            if (util.isClassUsedAsFunction(resultType, this, options)) {
17,281✔
1389
                resultType = FunctionType.instance;
20✔
1390
            }
1391

1392
        }
1393
        options?.typeChain?.push(new TypeChainEntry({ name: nameKey, type: resultType, data: options.data, astNode: this }));
21,741!
1394
        return resultType;
21,741✔
1395
    }
1396

1397
    get leadingTrivia(): Token[] {
1398
        return this.tokens.name.leadingTrivia;
35,890✔
1399
    }
1400

1401
    public clone() {
1402
        return this.finalizeClone(
54✔
1403
            new VariableExpression({
1404
                name: util.cloneToken(this.tokens.name)
1405
            })
1406
        );
1407
    }
1408
}
1409

1410
export class SourceLiteralExpression extends Expression {
1✔
1411
    constructor(options: {
1412
        value: Token;
1413
    }) {
1414
        super();
37✔
1415
        this.tokens = {
37✔
1416
            value: options.value
1417
        };
1418
        this.location = util.cloneLocation(this.tokens.value?.location);
37!
1419
    }
1420

1421
    public readonly location: Location;
1422

1423
    public readonly kind = AstNodeKind.SourceLiteralExpression;
37✔
1424

1425
    public readonly tokens: {
1426
        readonly value: Token;
1427
    };
1428

1429
    /**
1430
     * Find the index of the function in its parent
1431
     */
1432
    private findFunctionIndex(parentFunction: FunctionExpression, func: FunctionExpression) {
1433
        let index = -1;
4✔
1434
        parentFunction.findChild((node) => {
4✔
1435
            if (isFunctionExpression(node)) {
12✔
1436
                index++;
4✔
1437
                if (node === func) {
4!
1438
                    return true;
4✔
1439
                }
1440
            }
1441
        }, {
1442
            walkMode: WalkMode.visitAllRecursive
1443
        });
1444
        return index;
4✔
1445
    }
1446

1447
    private getFunctionName(state: BrsTranspileState, parseMode: ParseMode) {
1448
        let func = this.findAncestor<FunctionExpression>(isFunctionExpression);
8✔
1449
        let nameParts = [] as TranspileResult;
8✔
1450
        let parentFunction: FunctionExpression;
1451
        while ((parentFunction = func.findAncestor<FunctionExpression>(isFunctionExpression))) {
8✔
1452
            let index = this.findFunctionIndex(parentFunction, func);
4✔
1453
            nameParts.unshift(`anon${index}`);
4✔
1454
            func = parentFunction;
4✔
1455
        }
1456
        //get the index of this function in its parent
1457
        if (isFunctionStatement(func.parent)) {
8!
1458
            nameParts.unshift(
8✔
1459
                func.parent.getName(parseMode)
1460
            );
1461
        }
1462
        return nameParts.join('$');
8✔
1463
    }
1464

1465
    /**
1466
     * Get the line number from our token or from the closest ancestor that has a range
1467
     */
1468
    private getClosestLineNumber() {
1469
        let node: AstNode = this;
7✔
1470
        while (node) {
7✔
1471
            if (node.location?.range) {
17✔
1472
                return node.location.range.start.line + 1;
5✔
1473
            }
1474
            node = node.parent;
12✔
1475
        }
1476
        return -1;
2✔
1477
    }
1478

1479
    transpile(state: BrsTranspileState) {
1480
        let text: string;
1481
        switch (this.tokens.value.kind) {
31✔
1482
            case TokenKind.SourceFilePathLiteral:
40✔
1483
                const pathUrl = fileUrl(state.srcPath);
3✔
1484
                text = `"${pathUrl.substring(0, 4)}" + "${pathUrl.substring(4)}"`;
3✔
1485
                break;
3✔
1486
            case TokenKind.SourceLineNumLiteral:
1487
                //TODO find first parent that has range, or default to -1
1488
                text = `${this.getClosestLineNumber()}`;
4✔
1489
                break;
4✔
1490
            case TokenKind.FunctionNameLiteral:
1491
                text = `"${this.getFunctionName(state, ParseMode.BrightScript)}"`;
4✔
1492
                break;
4✔
1493
            case TokenKind.SourceFunctionNameLiteral:
1494
                text = `"${this.getFunctionName(state, ParseMode.BrighterScript)}"`;
4✔
1495
                break;
4✔
1496
            case TokenKind.SourceLocationLiteral:
1497
                const locationUrl = fileUrl(state.srcPath);
3✔
1498
                //TODO find first parent that has range, or default to -1
1499
                text = `"${locationUrl.substring(0, 4)}" + "${locationUrl.substring(4)}:${this.getClosestLineNumber()}"`;
3✔
1500
                break;
3✔
1501
            case TokenKind.PkgPathLiteral:
1502
                text = `"${util.sanitizePkgPath(state.file.pkgPath)}"`;
2✔
1503
                break;
2✔
1504
            case TokenKind.PkgLocationLiteral:
1505
                text = `"${util.sanitizePkgPath(state.file.pkgPath)}:" + str(LINE_NUM)`;
2✔
1506
                break;
2✔
1507
            case TokenKind.LineNumLiteral:
1508
            default:
1509
                //use the original text (because it looks like a variable)
1510
                text = this.tokens.value.text;
9✔
1511
                break;
9✔
1512

1513
        }
1514
        return [
31✔
1515
            state.sourceNode(this, text)
1516
        ];
1517
    }
1518

1519
    walk(visitor: WalkVisitor, options: WalkOptions) {
1520
        //nothing to walk
1521
    }
1522

1523
    get leadingTrivia(): Token[] {
1524
        return this.tokens.value.leadingTrivia;
152✔
1525
    }
1526

1527
    public clone() {
1528
        return this.finalizeClone(
1✔
1529
            new SourceLiteralExpression({
1530
                value: util.cloneToken(this.tokens.value)
1531
            })
1532
        );
1533
    }
1534
}
1535

1536
/**
1537
 * This expression transpiles and acts exactly like a CallExpression,
1538
 * except we need to uniquely identify these statements so we can
1539
 * do more type checking.
1540
 */
1541
export class NewExpression extends Expression {
1✔
1542
    constructor(options: {
1543
        new?: Token;
1544
        call: CallExpression;
1545
    }) {
1546
        super();
133✔
1547
        this.tokens = {
133✔
1548
            new: options.new
1549
        };
1550
        this.call = options.call;
133✔
1551
        this.location = util.createBoundingLocation(this.tokens.new, this.call);
133✔
1552
    }
1553

1554
    public readonly kind = AstNodeKind.NewExpression;
133✔
1555

1556
    public readonly location: Location | undefined;
1557

1558
    public readonly tokens: {
1559
        readonly new?: Token;
1560
    };
1561
    public readonly call: CallExpression;
1562

1563
    /**
1564
     * The name of the class to initialize (with optional namespace prefixed)
1565
     */
1566
    public get className() {
1567
        //the parser guarantees the callee of a new statement's call object will be
1568
        //either a VariableExpression or a DottedGet
1569
        return this.call.callee as (VariableExpression | DottedGetExpression);
92✔
1570
    }
1571

1572
    public transpile(state: BrsTranspileState) {
1573
        const namespace = this.findAncestor<NamespaceStatement>(isNamespaceStatement);
15✔
1574
        const cls = state.file.getClassFileLink(
15✔
1575
            this.className.getName(ParseMode.BrighterScript),
1576
            namespace?.getName(ParseMode.BrighterScript)
45✔
1577
        )?.item;
15✔
1578
        //new statements within a namespace block can omit the leading namespace if the class resides in that same namespace.
1579
        //So we need to figure out if this is a namespace-omitted class, or if this class exists without a namespace.
1580
        return this.call.transpile(state, cls?.getName(ParseMode.BrightScript));
15✔
1581
    }
1582

1583
    walk(visitor: WalkVisitor, options: WalkOptions) {
1584
        if (options.walkMode & InternalWalkMode.walkExpressions) {
821!
1585
            walk(this, 'call', visitor, options);
821✔
1586
        }
1587
    }
1588

1589
    getType(options: GetTypeOptions) {
1590
        const result = this.call.getType(options);
322✔
1591
        if (options.typeChain) {
322✔
1592
            // modify last typechain entry to show it is a new ...()
1593
            const lastEntry = options.typeChain[options.typeChain.length - 1];
3✔
1594
            if (lastEntry) {
3!
1595
                lastEntry.astNode = this;
3✔
1596
            }
1597
        }
1598
        return result;
322✔
1599
    }
1600

1601
    get leadingTrivia(): Token[] {
1602
        return this.tokens.new.leadingTrivia;
576✔
1603
    }
1604

1605
    public clone() {
1606
        return this.finalizeClone(
2✔
1607
            new NewExpression({
1608
                new: util.cloneToken(this.tokens.new),
1609
                call: this.call?.clone()
6✔
1610
            }),
1611
            ['call']
1612
        );
1613
    }
1614
}
1615

1616
export class CallfuncExpression extends Expression {
1✔
1617
    constructor(options: {
1618
        callee: Expression;
1619
        operator?: Token;
1620
        methodName: Identifier;
1621
        openingParen?: Token;
1622
        args?: Expression[];
1623
        closingParen?: Token;
1624
    }) {
1625
        super();
44✔
1626
        this.tokens = {
44✔
1627
            operator: options.operator,
1628
            methodName: options.methodName,
1629
            openingParen: options.openingParen,
1630
            closingParen: options.closingParen
1631
        };
1632
        this.callee = options.callee;
44✔
1633
        this.args = options.args ?? [];
44✔
1634

1635
        this.location = util.createBoundingLocation(
44✔
1636
            this.callee,
1637
            this.tokens.operator,
1638
            this.tokens.methodName,
1639
            this.tokens.openingParen,
1640
            ...this.args ?? [],
132!
1641
            this.tokens.closingParen
1642
        );
1643
    }
1644

1645
    public readonly callee: Expression;
1646
    public readonly args: Expression[];
1647

1648
    public readonly tokens: {
1649
        readonly operator: Token;
1650
        readonly methodName: Identifier;
1651
        readonly openingParen?: Token;
1652
        readonly closingParen?: Token;
1653
    };
1654

1655
    public readonly kind = AstNodeKind.CallfuncExpression;
44✔
1656

1657
    public readonly location: Location | undefined;
1658

1659
    public transpile(state: BrsTranspileState) {
1660
        let result = [] as TranspileResult;
9✔
1661
        result.push(
9✔
1662
            ...this.callee.transpile(state),
1663
            state.sourceNode(this.tokens.operator, '.callfunc'),
1664
            state.transpileToken(this.tokens.openingParen, '('),
1665
            //the name of the function
1666
            state.sourceNode(this.tokens.methodName, ['"', this.tokens.methodName.text, '"'])
1667
        );
1668
        if (this.args?.length > 0) {
9!
1669
            result.push(', ');
4✔
1670
            //transpile args
1671
            for (let i = 0; i < this.args.length; i++) {
4✔
1672
                //add comma between args
1673
                if (i > 0) {
7✔
1674
                    result.push(', ');
3✔
1675
                }
1676
                let arg = this.args[i];
7✔
1677
                result.push(...arg.transpile(state));
7✔
1678
            }
1679
        } else if (state.options.legacyCallfuncHandling) {
5✔
1680
            result.push(', ', 'invalid');
2✔
1681
        }
1682

1683
        result.push(
9✔
1684
            state.transpileToken(this.tokens.closingParen, ')')
1685
        );
1686
        return result;
9✔
1687
    }
1688

1689
    walk(visitor: WalkVisitor, options: WalkOptions) {
1690
        if (options.walkMode & InternalWalkMode.walkExpressions) {
202!
1691
            walk(this, 'callee', visitor, options);
202✔
1692
            walkArray(this.args, visitor, options, this);
202✔
1693
        }
1694
    }
1695

1696
    getType(options: GetTypeOptions) {
1697
        const result = util.getCallFuncType(this, this.tokens.methodName, options) ?? DynamicType.instance;
48✔
1698
        return result;
44✔
1699
    }
1700

1701
    get leadingTrivia(): Token[] {
1702
        return this.callee.leadingTrivia;
298✔
1703
    }
1704

1705
    public clone() {
1706
        return this.finalizeClone(
3✔
1707
            new CallfuncExpression({
1708
                callee: this.callee?.clone(),
9✔
1709
                operator: util.cloneToken(this.tokens.operator),
1710
                methodName: util.cloneToken(this.tokens.methodName),
1711
                openingParen: util.cloneToken(this.tokens.openingParen),
1712
                args: this.args?.map(e => e?.clone()),
2✔
1713
                closingParen: util.cloneToken(this.tokens.closingParen)
1714
            }),
1715
            ['callee', 'args']
1716
        );
1717
    }
1718
}
1719

1720
/**
1721
 * Since template strings can contain newlines, we need to concatenate multiple strings together with chr() calls.
1722
 * This is a single expression that represents the string contatenation of all parts of a single quasi.
1723
 */
1724
export class TemplateStringQuasiExpression extends Expression {
1✔
1725
    constructor(options: {
1726
        expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>;
1727
    }) {
1728
        super();
108✔
1729
        this.expressions = options.expressions;
108✔
1730
        this.location = util.createBoundingLocation(
108✔
1731
            ...this.expressions ?? []
324✔
1732
        );
1733
    }
1734

1735
    public readonly expressions: Array<LiteralExpression | EscapedCharCodeLiteralExpression>;
1736
    public readonly kind = AstNodeKind.TemplateStringQuasiExpression;
108✔
1737

1738
    readonly location: Location | undefined;
1739

1740
    transpile(state: BrsTranspileState, skipEmptyStrings = true) {
35✔
1741
        let result = [] as TranspileResult;
43✔
1742
        let plus = '';
43✔
1743
        for (let expression of this.expressions) {
43✔
1744
            //skip empty strings
1745
            //TODO what does an empty string literal expression look like?
1746
            if (expression.tokens.value.text === '' && skipEmptyStrings === true) {
68✔
1747
                continue;
27✔
1748
            }
1749
            result.push(
41✔
1750
                plus,
1751
                ...expression.transpile(state)
1752
            );
1753
            plus = ' + ';
41✔
1754
        }
1755
        return result;
43✔
1756
    }
1757

1758
    walk(visitor: WalkVisitor, options: WalkOptions) {
1759
        if (options.walkMode & InternalWalkMode.walkExpressions) {
333!
1760
            walkArray(this.expressions, visitor, options, this);
333✔
1761
        }
1762
    }
1763

1764
    public clone() {
1765
        return this.finalizeClone(
15✔
1766
            new TemplateStringQuasiExpression({
1767
                expressions: this.expressions?.map(e => e?.clone())
20✔
1768
            }),
1769
            ['expressions']
1770
        );
1771
    }
1772
}
1773

1774
export class TemplateStringExpression extends Expression {
1✔
1775
    constructor(options: {
1776
        openingBacktick?: Token;
1777
        quasis: TemplateStringQuasiExpression[];
1778
        expressions: Expression[];
1779
        closingBacktick?: Token;
1780
    }) {
1781
        super();
49✔
1782
        this.tokens = {
49✔
1783
            openingBacktick: options.openingBacktick,
1784
            closingBacktick: options.closingBacktick
1785
        };
1786
        this.quasis = options.quasis;
49✔
1787
        this.expressions = options.expressions;
49✔
1788
        this.location = util.createBoundingLocation(
49✔
1789
            this.tokens.openingBacktick,
1790
            this.quasis?.[0],
147✔
1791
            this.quasis?.[this.quasis?.length - 1],
291!
1792
            this.tokens.closingBacktick
1793
        );
1794
    }
1795

1796
    public readonly kind = AstNodeKind.TemplateStringExpression;
49✔
1797

1798
    public readonly tokens: {
1799
        readonly openingBacktick?: Token;
1800
        readonly closingBacktick?: Token;
1801
    };
1802
    public readonly quasis: TemplateStringQuasiExpression[];
1803
    public readonly expressions: Expression[];
1804

1805
    public readonly location: Location | undefined;
1806

1807
    public getType(options: GetTypeOptions) {
1808
        return StringType.instance;
34✔
1809
    }
1810

1811
    transpile(state: BrsTranspileState) {
1812
        if (this.quasis.length === 1 && this.expressions.length === 0) {
20✔
1813
            return this.quasis[0].transpile(state);
10✔
1814
        }
1815
        let result = ['('];
10✔
1816
        let plus = '';
10✔
1817
        //helper function to figure out when to include the plus
1818
        function add(...items) {
1819
            if (items.length > 0) {
40✔
1820
                result.push(
29✔
1821
                    plus,
1822
                    ...items
1823
                );
1824
            }
1825
            //set the plus after the first occurance of a nonzero length set of items
1826
            if (plus === '' && items.length > 0) {
40✔
1827
                plus = ' + ';
10✔
1828
            }
1829
        }
1830

1831
        for (let i = 0; i < this.quasis.length; i++) {
10✔
1832
            let quasi = this.quasis[i];
25✔
1833
            let expression = this.expressions[i];
25✔
1834

1835
            add(
25✔
1836
                ...quasi.transpile(state)
1837
            );
1838
            if (expression) {
25✔
1839
                //skip the toString wrapper around certain expressions
1840
                if (
15✔
1841
                    isEscapedCharCodeLiteralExpression(expression) ||
34✔
1842
                    (isLiteralExpression(expression) && isStringType(expression.getType()))
1843
                ) {
1844
                    add(
3✔
1845
                        ...expression.transpile(state)
1846
                    );
1847

1848
                    //wrap all other expressions with a bslib_toString call to prevent runtime type mismatch errors
1849
                } else {
1850
                    add(
12✔
1851
                        state.bslibPrefix + '_toString(',
1852
                        ...expression.transpile(state),
1853
                        ')'
1854
                    );
1855
                }
1856
            }
1857
        }
1858
        //the expression should be wrapped in parens so it can be used line a single expression at runtime
1859
        result.push(')');
10✔
1860

1861
        return result;
10✔
1862
    }
1863

1864
    walk(visitor: WalkVisitor, options: WalkOptions) {
1865
        if (options.walkMode & InternalWalkMode.walkExpressions) {
161!
1866
            //walk the quasis and expressions in left-to-right order
1867
            for (let i = 0; i < this.quasis?.length; i++) {
161!
1868
                walk(this.quasis, i, visitor, options, this);
274✔
1869

1870
                //this skips the final loop iteration since we'll always have one more quasi than expression
1871
                if (this.expressions[i]) {
274✔
1872
                    walk(this.expressions, i, visitor, options, this);
113✔
1873
                }
1874
            }
1875
        }
1876
    }
1877

1878
    public clone() {
1879
        return this.finalizeClone(
7✔
1880
            new TemplateStringExpression({
1881
                openingBacktick: util.cloneToken(this.tokens.openingBacktick),
1882
                quasis: this.quasis?.map(e => e?.clone()),
12✔
1883
                expressions: this.expressions?.map(e => e?.clone()),
6✔
1884
                closingBacktick: util.cloneToken(this.tokens.closingBacktick)
1885
            }),
1886
            ['quasis', 'expressions']
1887
        );
1888
    }
1889
}
1890

1891
export class TaggedTemplateStringExpression extends Expression {
1✔
1892
    constructor(options: {
1893
        tagName: Identifier;
1894
        openingBacktick?: Token;
1895
        quasis: TemplateStringQuasiExpression[];
1896
        expressions: Expression[];
1897
        closingBacktick?: Token;
1898
    }) {
1899
        super();
12✔
1900
        this.tokens = {
12✔
1901
            tagName: options.tagName,
1902
            openingBacktick: options.openingBacktick,
1903
            closingBacktick: options.closingBacktick
1904
        };
1905
        this.quasis = options.quasis;
12✔
1906
        this.expressions = options.expressions;
12✔
1907

1908
        this.location = util.createBoundingLocation(
12✔
1909
            this.tokens.tagName,
1910
            this.tokens.openingBacktick,
1911
            this.quasis?.[0],
36✔
1912
            this.quasis?.[this.quasis?.length - 1],
69!
1913
            this.tokens.closingBacktick
1914
        );
1915
    }
1916

1917
    public readonly kind = AstNodeKind.TaggedTemplateStringExpression;
12✔
1918

1919
    public readonly tokens: {
1920
        readonly tagName: Identifier;
1921
        readonly openingBacktick?: Token;
1922
        readonly closingBacktick?: Token;
1923
    };
1924

1925
    public readonly quasis: TemplateStringQuasiExpression[];
1926
    public readonly expressions: Expression[];
1927

1928
    public readonly location: Location | undefined;
1929

1930
    transpile(state: BrsTranspileState) {
1931
        let result = [] as TranspileResult;
3✔
1932
        result.push(
3✔
1933
            state.transpileToken(this.tokens.tagName),
1934
            '(['
1935
        );
1936

1937
        //add quasis as the first array
1938
        for (let i = 0; i < this.quasis.length; i++) {
3✔
1939
            let quasi = this.quasis[i];
8✔
1940
            //separate items with a comma
1941
            if (i > 0) {
8✔
1942
                result.push(
5✔
1943
                    ', '
1944
                );
1945
            }
1946
            result.push(
8✔
1947
                ...quasi.transpile(state, false)
1948
            );
1949
        }
1950
        result.push(
3✔
1951
            '], ['
1952
        );
1953

1954
        //add expressions as the second array
1955
        for (let i = 0; i < this.expressions.length; i++) {
3✔
1956
            let expression = this.expressions[i];
5✔
1957
            if (i > 0) {
5✔
1958
                result.push(
2✔
1959
                    ', '
1960
                );
1961
            }
1962
            result.push(
5✔
1963
                ...expression.transpile(state)
1964
            );
1965
        }
1966
        result.push(
3✔
1967
            state.sourceNode(this.tokens.closingBacktick, '])')
1968
        );
1969
        return result;
3✔
1970
    }
1971

1972
    walk(visitor: WalkVisitor, options: WalkOptions) {
1973
        if (options.walkMode & InternalWalkMode.walkExpressions) {
26!
1974
            //walk the quasis and expressions in left-to-right order
1975
            for (let i = 0; i < this.quasis?.length; i++) {
26!
1976
                walk(this.quasis, i, visitor, options, this);
63✔
1977

1978
                //this skips the final loop iteration since we'll always have one more quasi than expression
1979
                if (this.expressions[i]) {
63✔
1980
                    walk(this.expressions, i, visitor, options, this);
37✔
1981
                }
1982
            }
1983
        }
1984
    }
1985

1986
    public clone() {
1987
        return this.finalizeClone(
3✔
1988
            new TaggedTemplateStringExpression({
1989
                tagName: util.cloneToken(this.tokens.tagName),
1990
                openingBacktick: util.cloneToken(this.tokens.openingBacktick),
1991
                quasis: this.quasis?.map(e => e?.clone()),
5✔
1992
                expressions: this.expressions?.map(e => e?.clone()),
3✔
1993
                closingBacktick: util.cloneToken(this.tokens.closingBacktick)
1994
            }),
1995
            ['quasis', 'expressions']
1996
        );
1997
    }
1998
}
1999

2000
export class AnnotationExpression extends Expression {
1✔
2001
    constructor(options: {
2002
        at?: Token;
2003
        name: Token;
2004
        call?: CallExpression;
2005
    }) {
2006
        super();
83✔
2007
        this.tokens = {
83✔
2008
            at: options.at,
2009
            name: options.name
2010
        };
2011
        this.call = options.call;
83✔
2012
        this.name = this.tokens.name.text;
83✔
2013
    }
2014

2015
    public readonly kind = AstNodeKind.AnnotationExpression;
83✔
2016

2017
    public readonly tokens: {
2018
        readonly at: Token;
2019
        readonly name: Token;
2020
    };
2021

2022
    public get location(): Location | undefined {
2023
        return util.createBoundingLocation(
75✔
2024
            this.tokens.at,
2025
            this.tokens.name,
2026
            this.call
2027
        );
2028
    }
2029

2030
    public readonly name: string;
2031

2032
    public call: CallExpression;
2033

2034
    /**
2035
     * Convert annotation arguments to JavaScript types
2036
     * @param strict If false, keep Expression objects not corresponding to JS types
2037
     */
2038
    getArguments(strict = true): ExpressionValue[] {
10✔
2039
        if (!this.call) {
11✔
2040
            return [];
1✔
2041
        }
2042
        return this.call.args.map(e => expressionToValue(e, strict));
20✔
2043
    }
2044

2045
    public get leadingTrivia(): Token[] {
2046
        return this.tokens.at?.leadingTrivia;
50!
2047
    }
2048

2049
    transpile(state: BrsTranspileState) {
2050
        //transpile only our leading comments
2051
        return state.transpileComments(this.leadingTrivia);
16✔
2052
    }
2053

2054
    walk(visitor: WalkVisitor, options: WalkOptions) {
2055
        //nothing to walk
2056
    }
2057
    getTypedef(state: BrsTranspileState) {
2058
        return [
9✔
2059
            '@',
2060
            this.name,
2061
            ...(this.call?.transpile(state) ?? [])
54✔
2062
        ];
2063
    }
2064

2065
    public clone() {
2066
        const clone = this.finalizeClone(
7✔
2067
            new AnnotationExpression({
2068
                at: util.cloneToken(this.tokens.at),
2069
                name: util.cloneToken(this.tokens.name)
2070
            })
2071
        );
2072
        return clone;
7✔
2073
    }
2074
}
2075

2076
export class TernaryExpression extends Expression {
1✔
2077
    constructor(options: {
2078
        test: Expression;
2079
        questionMark?: Token;
2080
        consequent?: Expression;
2081
        colon?: Token;
2082
        alternate?: Expression;
2083
    }) {
2084
        super();
82✔
2085
        this.tokens = {
82✔
2086
            questionMark: options.questionMark,
2087
            colon: options.colon
2088
        };
2089
        this.test = options.test;
82✔
2090
        this.consequent = options.consequent;
82✔
2091
        this.alternate = options.alternate;
82✔
2092
        this.location = util.createBoundingLocation(
82✔
2093
            this.test,
2094
            this.tokens.questionMark,
2095
            this.consequent,
2096
            this.tokens.colon,
2097
            this.alternate
2098
        );
2099
    }
2100

2101
    public readonly kind = AstNodeKind.TernaryExpression;
82✔
2102

2103
    public readonly location: Location | undefined;
2104

2105
    public readonly tokens: {
2106
        readonly questionMark?: Token;
2107
        readonly colon?: Token;
2108
    };
2109

2110
    public readonly test: Expression;
2111
    public readonly consequent?: Expression;
2112
    public readonly alternate?: Expression;
2113

2114
    transpile(state: BrsTranspileState) {
2115
        let result = [] as TranspileResult;
31✔
2116
        const file = state.file;
31✔
2117
        let consequentInfo = util.getExpressionInfo(this.consequent!, file);
31✔
2118
        let alternateInfo = util.getExpressionInfo(this.alternate!, file);
31✔
2119

2120
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
2121
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
31✔
2122
        let mutatingExpressions = [
31✔
2123
            ...consequentInfo.expressions,
2124
            ...alternateInfo.expressions
2125
        ].filter(e => e instanceof CallExpression || e instanceof CallfuncExpression || e instanceof DottedGetExpression);
142✔
2126

2127
        if (mutatingExpressions.length > 0) {
31✔
2128
            result.push(
8✔
2129
                state.sourceNode(
2130
                    this.tokens.questionMark,
2131
                    //write all the scope variables as parameters.
2132
                    //TODO handle when there are more than 31 parameters
2133
                    `(function(${['__bsCondition', ...allUniqueVarNames].join(', ')})`
2134
                ),
2135
                state.newline,
2136
                //double indent so our `end function` line is still indented one at the end
2137
                state.indent(2),
2138
                state.sourceNode(this.test, `if __bsCondition then`),
2139
                state.newline,
2140
                state.indent(1),
2141
                state.sourceNode(this.consequent ?? this.tokens.questionMark, 'return '),
24!
2142
                ...this.consequent?.transpile(state) ?? [state.sourceNode(this.tokens.questionMark, 'invalid')],
48!
2143
                state.newline,
2144
                state.indent(-1),
2145
                state.sourceNode(this.consequent ?? this.tokens.questionMark, 'else'),
24!
2146
                state.newline,
2147
                state.indent(1),
2148
                state.sourceNode(this.consequent ?? this.tokens.questionMark, 'return '),
24!
2149
                ...this.alternate?.transpile(state) ?? [state.sourceNode(this.consequent ?? this.tokens.questionMark, 'invalid')],
48!
2150
                state.newline,
2151
                state.indent(-1),
2152
                state.sourceNode(this.tokens.questionMark, 'end if'),
2153
                state.newline,
2154
                state.indent(-1),
2155
                state.sourceNode(this.tokens.questionMark, 'end function)('),
2156
                ...this.test.transpile(state),
2157
                state.sourceNode(this.tokens.questionMark, `${['', ...allUniqueVarNames].join(', ')})`)
2158
            );
2159
            state.blockDepth--;
8✔
2160
        } else {
2161
            result.push(
23✔
2162
                state.sourceNode(this.test, state.bslibPrefix + `_ternary(`),
2163
                ...this.test.transpile(state),
2164
                state.sourceNode(this.test, `, `),
2165
                ...this.consequent?.transpile(state) ?? ['invalid'],
138✔
2166
                `, `,
2167
                ...this.alternate?.transpile(state) ?? ['invalid'],
138✔
2168
                `)`
2169
            );
2170
        }
2171
        return result;
31✔
2172
    }
2173

2174
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2175
        if (options.walkMode & InternalWalkMode.walkExpressions) {
234!
2176
            walk(this, 'test', visitor, options);
234✔
2177
            walk(this, 'consequent', visitor, options);
234✔
2178
            walk(this, 'alternate', visitor, options);
234✔
2179
        }
2180
    }
2181

2182
    get leadingTrivia(): Token[] {
2183
        return this.test.leadingTrivia;
149✔
2184
    }
2185

2186
    public clone() {
2187
        return this.finalizeClone(
2✔
2188
            new TernaryExpression({
2189
                test: this.test?.clone(),
6✔
2190
                questionMark: util.cloneToken(this.tokens.questionMark),
2191
                consequent: this.consequent?.clone(),
6✔
2192
                colon: util.cloneToken(this.tokens.colon),
2193
                alternate: this.alternate?.clone()
6✔
2194
            }),
2195
            ['test', 'consequent', 'alternate']
2196
        );
2197
    }
2198
}
2199

2200
export class NullCoalescingExpression extends Expression {
1✔
2201
    constructor(options: {
2202
        consequent: Expression;
2203
        questionQuestion?: Token;
2204
        alternate: Expression;
2205
    }) {
2206
        super();
36✔
2207
        this.tokens = {
36✔
2208
            questionQuestion: options.questionQuestion
2209
        };
2210
        this.consequent = options.consequent;
36✔
2211
        this.alternate = options.alternate;
36✔
2212
        this.location = util.createBoundingLocation(
36✔
2213
            this.consequent,
2214
            this.tokens.questionQuestion,
2215
            this.alternate
2216
        );
2217
    }
2218

2219
    public readonly kind = AstNodeKind.NullCoalescingExpression;
36✔
2220

2221
    public readonly location: Location | undefined;
2222

2223
    public readonly tokens: {
2224
        readonly questionQuestion?: Token;
2225
    };
2226

2227
    public readonly consequent: Expression;
2228
    public readonly alternate: Expression;
2229

2230
    transpile(state: BrsTranspileState) {
2231
        let result = [] as TranspileResult;
10✔
2232
        let consequentInfo = util.getExpressionInfo(this.consequent, state.file);
10✔
2233
        let alternateInfo = util.getExpressionInfo(this.alternate, state.file);
10✔
2234

2235
        //get all unique variable names used in the consequent and alternate, and sort them alphabetically so the output is consistent
2236
        let allUniqueVarNames = [...new Set([...consequentInfo.uniqueVarNames, ...alternateInfo.uniqueVarNames])].sort();
10✔
2237
        let hasMutatingExpression = [
10✔
2238
            ...consequentInfo.expressions,
2239
            ...alternateInfo.expressions
2240
        ].find(e => isCallExpression(e) || isCallfuncExpression(e) || isDottedGetExpression(e));
28✔
2241

2242
        if (hasMutatingExpression) {
10✔
2243
            result.push(
6✔
2244
                `(function(`,
2245
                //write all the scope variables as parameters.
2246
                //TODO handle when there are more than 31 parameters
2247
                allUniqueVarNames.join(', '),
2248
                ')',
2249
                state.newline,
2250
                //double indent so our `end function` line is still indented one at the end
2251
                state.indent(2),
2252
                //evaluate the consequent exactly once, and then use it in the following condition
2253
                `__bsConsequent = `,
2254
                ...this.consequent.transpile(state),
2255
                state.newline,
2256
                state.indent(),
2257
                `if __bsConsequent <> invalid then`,
2258
                state.newline,
2259
                state.indent(1),
2260
                'return __bsConsequent',
2261
                state.newline,
2262
                state.indent(-1),
2263
                'else',
2264
                state.newline,
2265
                state.indent(1),
2266
                'return ',
2267
                ...this.alternate.transpile(state),
2268
                state.newline,
2269
                state.indent(-1),
2270
                'end if',
2271
                state.newline,
2272
                state.indent(-1),
2273
                'end function)(',
2274
                allUniqueVarNames.join(', '),
2275
                ')'
2276
            );
2277
            state.blockDepth--;
6✔
2278
        } else {
2279
            result.push(
4✔
2280
                state.bslibPrefix + `_coalesce(`,
2281
                ...this.consequent.transpile(state),
2282
                ', ',
2283
                ...this.alternate.transpile(state),
2284
                ')'
2285
            );
2286
        }
2287
        return result;
10✔
2288
    }
2289

2290
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2291
        if (options.walkMode & InternalWalkMode.walkExpressions) {
84!
2292
            walk(this, 'consequent', visitor, options);
84✔
2293
            walk(this, 'alternate', visitor, options);
84✔
2294
        }
2295
    }
2296

2297
    get leadingTrivia(): Token[] {
2298
        return this.consequent.leadingTrivia;
50✔
2299
    }
2300

2301
    public clone() {
2302
        return this.finalizeClone(
2✔
2303
            new NullCoalescingExpression({
2304
                consequent: this.consequent?.clone(),
6✔
2305
                questionQuestion: util.cloneToken(this.tokens.questionQuestion),
2306
                alternate: this.alternate?.clone()
6✔
2307
            }),
2308
            ['consequent', 'alternate']
2309
        );
2310
    }
2311
}
2312

2313
export class RegexLiteralExpression extends Expression {
1✔
2314
    constructor(options: {
2315
        regexLiteral: Token;
2316
    }) {
2317
        super();
46✔
2318
        this.tokens = {
46✔
2319
            regexLiteral: options.regexLiteral
2320
        };
2321
    }
2322

2323
    public readonly kind = AstNodeKind.RegexLiteralExpression;
46✔
2324
    public readonly tokens: {
2325
        readonly regexLiteral: Token;
2326
    };
2327

2328
    public get location(): Location {
2329
        return this.tokens?.regexLiteral?.location;
150!
2330
    }
2331

2332
    public transpile(state: BrsTranspileState): TranspileResult {
2333
        let text = this.tokens.regexLiteral?.text ?? '';
42!
2334
        let flags = '';
42✔
2335
        //get any flags from the end
2336
        const flagMatch = /\/([a-z]+)$/i.exec(text);
42✔
2337
        if (flagMatch) {
42✔
2338
            text = text.substring(0, flagMatch.index + 1);
2✔
2339
            flags = flagMatch[1];
2✔
2340
        }
2341
        let pattern = text
42✔
2342
            //remove leading and trailing slashes
2343
            .substring(1, text.length - 1)
2344
            //escape quotemarks
2345
            .split('"').join('" + chr(34) + "');
2346

2347
        return [
42✔
2348
            state.sourceNode(this.tokens.regexLiteral, [
2349
                'CreateObject("roRegex", ',
2350
                `"${pattern}", `,
2351
                `"${flags}"`,
2352
                ')'
2353
            ])
2354
        ];
2355
    }
2356

2357
    walk(visitor: WalkVisitor, options: WalkOptions) {
2358
        //nothing to walk
2359
    }
2360

2361
    public clone() {
2362
        return this.finalizeClone(
1✔
2363
            new RegexLiteralExpression({
2364
                regexLiteral: util.cloneToken(this.tokens.regexLiteral)
2365
            })
2366
        );
2367
    }
2368

2369
    get leadingTrivia(): Token[] {
2370
        return this.tokens.regexLiteral?.leadingTrivia;
1,023!
2371
    }
2372
}
2373

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

2377
function expressionToValue(expr: Expression, strict: boolean): ExpressionValue {
2378
    if (!expr) {
30!
UNCOV
2379
        return null;
×
2380
    }
2381
    if (isUnaryExpression(expr) && isLiteralNumber(expr.right)) {
30✔
2382
        return numberExpressionToValue(expr.right, expr.tokens.operator.text);
1✔
2383
    }
2384
    if (isLiteralString(expr)) {
29✔
2385
        //remove leading and trailing quotes
2386
        return expr.tokens.value.text.replace(/^"/, '').replace(/"$/, '');
5✔
2387
    }
2388
    if (isLiteralNumber(expr)) {
24✔
2389
        return numberExpressionToValue(expr);
11✔
2390
    }
2391

2392
    if (isLiteralBoolean(expr)) {
13✔
2393
        return expr.tokens.value.text.toLowerCase() === 'true';
3✔
2394
    }
2395
    if (isArrayLiteralExpression(expr)) {
10✔
2396
        return expr.elements
3✔
2397
            .map(e => expressionToValue(e, strict));
7✔
2398
    }
2399
    if (isAALiteralExpression(expr)) {
7✔
2400
        return expr.elements.reduce((acc, e) => {
3✔
2401
            acc[e.tokens.key.text] = expressionToValue(e.value, strict);
3✔
2402
            return acc;
3✔
2403
        }, {});
2404
    }
2405
    //for annotations, we only support serializing pure string values
2406
    if (isTemplateStringExpression(expr)) {
4✔
2407
        if (expr.quasis?.length === 1 && expr.expressions.length === 0) {
2!
2408
            return expr.quasis[0].expressions.map(x => x.tokens.value.text).join('');
10✔
2409
        }
2410
    }
2411
    return strict ? null : expr;
2✔
2412
}
2413

2414
function numberExpressionToValue(expr: LiteralExpression, operator = '') {
11✔
2415
    if (isIntegerType(expr.getType()) || isLongIntegerType(expr.getType())) {
12!
2416
        return parseInt(operator + expr.tokens.value.text);
12✔
2417
    } else {
UNCOV
2418
        return parseFloat(operator + expr.tokens.value.text);
×
2419
    }
2420
}
2421

2422
export class TypeExpression extends Expression implements TypedefProvider {
1✔
2423
    constructor(options: {
2424
        /**
2425
         * The standard AST expression that represents the type for this TypeExpression.
2426
         */
2427
        expression: Expression;
2428
    }) {
2429
        super();
1,531✔
2430
        this.expression = options.expression;
1,531✔
2431
        this.location = util.cloneLocation(this.expression?.location);
1,531!
2432
    }
2433

2434
    public readonly kind = AstNodeKind.TypeExpression;
1,531✔
2435

2436
    /**
2437
     * The standard AST expression that represents the type for this TypeExpression.
2438
     */
2439
    public readonly expression: Expression;
2440

2441
    public readonly location: Location;
2442

2443
    public transpile(state: BrsTranspileState): TranspileResult {
2444
        return [this.getType({ flags: SymbolTypeFlag.typetime }).toTypeString()];
106✔
2445
    }
2446
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2447
        if (options.walkMode & InternalWalkMode.walkExpressions) {
7,046✔
2448
            walk(this, 'expression', visitor, options);
6,921✔
2449
        }
2450
    }
2451

2452
    public getType(options: GetTypeOptions): BscType {
2453
        return this.expression.getType({ ...options, flags: SymbolTypeFlag.typetime });
5,555✔
2454
    }
2455

2456
    getTypedef(state: TranspileState): TranspileResult {
2457
        // TypeDefs should pass through any valid type names
2458
        return this.expression.transpile(state as BrsTranspileState);
33✔
2459
    }
2460

2461
    getName(parseMode = ParseMode.BrighterScript): string {
195✔
2462
        //TODO: this may not support Complex Types, eg. generics or Unions
2463
        return util.getAllDottedGetPartsAsString(this.expression, parseMode);
195✔
2464
    }
2465

2466
    getNameParts(): string[] {
2467
        //TODO: really, this code is only used to get Namespaces. It could be more clear.
2468
        return util.getAllDottedGetParts(this.expression).map(x => x.text);
20✔
2469
    }
2470

2471
    public clone() {
2472
        return this.finalizeClone(
15✔
2473
            new TypeExpression({
2474
                expression: this.expression?.clone()
45!
2475
            }),
2476
            ['expression']
2477
        );
2478
    }
2479
}
2480

2481
export class TypecastExpression extends Expression {
1✔
2482
    constructor(options: {
2483
        obj: Expression;
2484
        as?: Token;
2485
        typeExpression?: TypeExpression;
2486
    }) {
2487
        super();
73✔
2488
        this.tokens = {
73✔
2489
            as: options.as
2490
        };
2491
        this.obj = options.obj;
73✔
2492
        this.typeExpression = options.typeExpression;
73✔
2493
        this.location = util.createBoundingLocation(
73✔
2494
            this.obj,
2495
            this.tokens.as,
2496
            this.typeExpression
2497
        );
2498
    }
2499

2500
    public readonly kind = AstNodeKind.TypecastExpression;
73✔
2501

2502
    public readonly obj: Expression;
2503

2504
    public readonly tokens: {
2505
        readonly as?: Token;
2506
    };
2507

2508
    public typeExpression?: TypeExpression;
2509

2510
    public readonly location: Location;
2511

2512
    public transpile(state: BrsTranspileState): TranspileResult {
2513
        return this.obj.transpile(state);
13✔
2514
    }
2515
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2516
        if (options.walkMode & InternalWalkMode.walkExpressions) {
353!
2517
            walk(this, 'obj', visitor, options);
353✔
2518
            walk(this, 'typeExpression', visitor, options);
353✔
2519
        }
2520
    }
2521

2522
    public getType(options: GetTypeOptions): BscType {
2523
        const result = this.typeExpression.getType(options);
90✔
2524
        if (options.typeChain) {
90✔
2525
            // modify last typechain entry to show it is a typecast
2526
            const lastEntry = options.typeChain[options.typeChain.length - 1];
18✔
2527
            if (lastEntry) {
18!
2528
                lastEntry.astNode = this;
18✔
2529
            }
2530
        }
2531
        return result;
90✔
2532
    }
2533

2534
    public clone() {
2535
        return this.finalizeClone(
3✔
2536
            new TypecastExpression({
2537
                obj: this.obj?.clone(),
9✔
2538
                as: util.cloneToken(this.tokens.as),
2539
                typeExpression: this.typeExpression?.clone()
9!
2540
            }),
2541
            ['obj', 'typeExpression']
2542
        );
2543
    }
2544
}
2545

2546
export class TypedArrayExpression extends Expression {
1✔
2547
    constructor(options: {
2548
        innerType: Expression;
2549
        leftBracket?: Token;
2550
        rightBracket?: Token;
2551
    }) {
2552
        super();
29✔
2553
        this.tokens = {
29✔
2554
            leftBracket: options.leftBracket,
2555
            rightBracket: options.rightBracket
2556
        };
2557
        this.innerType = options.innerType;
29✔
2558
        this.location = util.createBoundingLocation(
29✔
2559
            this.innerType,
2560
            this.tokens.leftBracket,
2561
            this.tokens.rightBracket
2562
        );
2563
    }
2564

2565
    public readonly tokens: {
2566
        readonly leftBracket?: Token;
2567
        readonly rightBracket?: Token;
2568
    };
2569

2570
    public readonly innerType: Expression;
2571

2572
    public readonly kind = AstNodeKind.TypedArrayExpression;
29✔
2573

2574
    public readonly location: Location;
2575

2576
    public transpile(state: BrsTranspileState): TranspileResult {
UNCOV
2577
        return [this.getType({ flags: SymbolTypeFlag.typetime }).toTypeString()];
×
2578
    }
2579

2580
    public walk(visitor: WalkVisitor, options: WalkOptions) {
2581
        if (options.walkMode & InternalWalkMode.walkExpressions) {
127!
2582
            walk(this, 'innerType', visitor, options);
127✔
2583
        }
2584
    }
2585

2586
    public getType(options: GetTypeOptions): BscType {
2587
        return new ArrayType(this.innerType.getType(options));
122✔
2588
    }
2589

2590
    public clone() {
UNCOV
2591
        return this.finalizeClone(
×
2592
            new TypedArrayExpression({
2593
                innerType: this.innerType?.clone(),
×
2594
                leftBracket: util.cloneToken(this.tokens.leftBracket),
2595
                rightBracket: util.cloneToken(this.tokens.rightBracket)
2596
            }),
2597
            ['innerType']
2598
        );
2599
    }
2600
}
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

© 2025 Coveralls, Inc