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

rokucommunity / brs / #213

27 Jan 2025 05:30PM UTC coverage: 86.996% (-2.2%) from 89.205%
#213

push

web-flow
Implemented several improvements to SceneGraph (#87)

* Implemented several improvements to SceneGraph

* Fixed most test cases

* Reduced unnecessary code

* Fixed typo

* Added Warning when trying to create a non-existent Node

* Fixed parser

* Fixed unit tests

* Implemented support for `infoFields`

* Prettier fix

* Simplified execute callback code and matched behavior with Roku

* Adding comment to clarify the exception

2240 of 2807 branches covered (79.8%)

Branch coverage included in aggregate %.

139 of 304 new or added lines in 18 files covered. (45.72%)

2 existing lines in 1 file now uncovered.

6129 of 6813 relevant lines covered (89.96%)

27562.41 hits per line

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

81.7
/src/interpreter/index.ts
1
import { EventEmitter } from "events";
135✔
2
import * as PP from "../preprocessor";
3

4
import {
135✔
5
    BrsType,
6
    ValueKind,
7
    BrsInvalid,
8
    isBrsNumber,
9
    isBrsString,
10
    BrsBoolean,
11
    BrsString,
12
    isBrsBoolean,
13
    Int32,
14
    isBrsCallable,
15
    Uninitialized,
16
    RoArray,
17
    isIterable,
18
    Callable,
19
    BrsNumber,
20
    Float,
21
    tryCoerce,
22
    isComparable,
23
    isStringComp,
24
    isBoxedNumber,
25
    RoInvalid,
26
    PrimitiveKinds,
27
    Signature,
28
    RoByteArray,
29
    RoList,
30
    RoXMLElement,
31
    RoSGNode,
32
    toAssociativeArray,
33
} from "../brsTypes";
34

35
import { Lexeme, Location } from "../lexer";
135✔
36
import { isToken } from "../lexer/Token";
135✔
37
import { Expr, Stmt, ComponentScopeResolver } from "../parser";
135✔
38
import {
135✔
39
    BrsError,
40
    ErrorDetail,
41
    RuntimeError,
42
    RuntimeErrorDetail,
43
    findErrorDetail,
44
    getLoggerUsing,
45
} from "../Error";
46

47
import * as StdLib from "../stdlib";
135✔
48
import { _brs_ } from "../extensions";
135✔
49

50
import { Scope, Environment, NotFound } from "./Environment";
135✔
51
import { TypeMismatch } from "./TypeMismatch";
135✔
52
import { OutputProxy } from "./OutputProxy";
135✔
53
import { toCallable } from "./BrsFunction";
135✔
54
import { RoAssociativeArray } from "../brsTypes/components/RoAssociativeArray";
135✔
55
import MemoryFileSystem from "memory-fs";
135✔
56
import { BrsComponent } from "../brsTypes/components/BrsComponent";
135✔
57
import { isBoxable, isUnboxable } from "../brsTypes/Boxing";
135✔
58

59
import { ComponentDefinition } from "../scenegraph";
60
import pSettle from "p-settle";
135✔
61
import { CoverageCollector } from "../coverage";
62
import { ManifestValue } from "../preprocessor/Manifest";
63
import { generateArgumentMismatchError } from "./ArgumentMismatch";
135✔
64
import Long from "long";
135✔
65

66
import chalk from "chalk";
135✔
67
import stripAnsi from "strip-ansi";
135✔
68
import { getLexerParserFn } from "../LexerParser";
135✔
69

70
/** The set of options used to configure an interpreter's execution. */
71
export interface ExecutionOptions {
72
    /** The base path for the project. Default: process.cwd() */
73
    root: string;
74
    /** The stdout stream that brs should use. Default: process.stdout. */
75
    stdout: NodeJS.WriteStream;
76
    /** The stderr stream that brs should use. Default: process.stderr. */
77
    stderr: NodeJS.WriteStream;
78
    /** Whether or not to collect coverage statistics. Default: false. */
79
    generateCoverage: boolean;
80
    /** Additional directories to search for component definitions. Default: [] */
81
    componentDirs: string[];
82
    /** Whether or not a component library is being processed. */
83
    isComponentLibrary: boolean;
84
}
85

86
/** The default set of execution options.  Includes the `stdout`/`stderr` pair from the process that invoked `brs`. */
87
export const defaultExecutionOptions: ExecutionOptions = {
135✔
88
    root: process.cwd(),
89
    stdout: process.stdout,
90
    stderr: process.stderr,
91
    generateCoverage: false,
92
    componentDirs: [],
93
    isComponentLibrary: false,
94
};
95
Object.freeze(defaultExecutionOptions);
135✔
96

97
/** The definition of a trace point to be added to the stack trace */
98
export interface TracePoint {
99
    functionName: string;
100
    functionLocation: Location;
101
    callLocation: Location;
102
    signature?: Signature;
103
}
104

105
export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType> {
135✔
106
    private _environment = new Environment();
856✔
107
    private _stack = new Array<TracePoint>();
856✔
108
    private _tryMode = false;
856✔
109
    private _coverageCollector: CoverageCollector | null = null;
856✔
110
    private _manifest: PP.Manifest | undefined;
111

112
    readonly options: ExecutionOptions;
113
    readonly stdout: OutputProxy;
114
    readonly stderr: OutputProxy;
115
    readonly temporaryVolume: MemoryFileSystem = new MemoryFileSystem();
856✔
116

117
    location: Location;
118

119
    /** Allows consumers to observe errors as they're detected. */
120
    readonly events = new EventEmitter();
856✔
121

122
    /** The set of errors detected from executing an AST. */
123
    errors: (BrsError | RuntimeError)[] = [];
856✔
124

125
    get environment() {
126
        return this._environment;
99,701✔
127
    }
128

129
    get stack() {
130
        return this._stack;
15✔
131
    }
132

133
    get manifest() {
134
        return this._manifest != null ? this._manifest : new Map<string, ManifestValue>();
12!
135
    }
136

137
    set manifest(manifest: PP.Manifest) {
138
        this._manifest = manifest;
128✔
139
    }
140

141
    addToStack(tracePoint: TracePoint) {
142
        this._stack.push(tracePoint);
22✔
143
    }
144

145
    setCoverageCollector(collector: CoverageCollector) {
146
        this._coverageCollector = collector;
1✔
147
    }
148

149
    reportCoverageHit(statement: Expr.Expression | Stmt.Statement) {
150
        if (this.options.generateCoverage && this._coverageCollector) {
151,072✔
151
            this._coverageCollector.logHit(statement);
46✔
152
        }
153
    }
154

155
    /**
156
     * Convenience function to subscribe to the `err` events emitted by `interpreter.events`.
157
     * @param errorHandler the function to call for every runtime error emitted after subscribing
158
     * @returns an object with a `dispose` function, used to unsubscribe from errors
159
     */
160
    public onError(errorHandler: (err: BrsError | RuntimeError) => void) {
161
        this.events.on("err", errorHandler);
113✔
162
        return {
113✔
163
            dispose: () => {
164
                this.events.removeListener("err", errorHandler);
×
165
            },
166
        };
167
    }
168

169
    /**
170
     * Convenience function to subscribe to a single `err` event emitted by `interpreter.events`.
171
     * @param errorHandler the function to call for the first runtime error emitted after subscribing
172
     */
173
    public onErrorOnce(errorHandler: (err: BrsError | RuntimeError) => void) {
174
        this.events.once("err", errorHandler);
×
175
    }
176

177
    /**
178
     * Builds out all the sub-environments for the given components. Components are saved into the calling interpreter
179
     * instance. This function will mutate the state of the calling interpreter.
180
     * @param componentMap Map of all components to be assigned to this interpreter
181
     * @param parseFn Function used to parse components into interpretable statements
182
     * @param options
183
     */
184
    public static async withSubEnvsFromComponents(
185
        componentMap: Map<string, ComponentDefinition>,
186
        manifest: Map<string, ManifestValue>,
187
        options: ExecutionOptions = defaultExecutionOptions
×
188
    ) {
189
        const interpreter = new Interpreter(options);
113✔
190
        interpreter.onError(getLoggerUsing(options.stderr));
113✔
191
        const lexerParserFn = getLexerParserFn(manifest, options);
113✔
192

193
        interpreter.environment.nodeDefMap = componentMap;
113✔
194

195
        const componentScopeResolver = new ComponentScopeResolver(componentMap, lexerParserFn);
113✔
196
        await pSettle(
113✔
197
            Array.from(componentMap).map(async (componentKV) => {
198
                let [_, component] = componentKV;
1,757✔
199
                component.environment = interpreter.environment.createSubEnvironment(
1,757✔
200
                    /* includeModuleScope */ false
201
                );
202
                let statements = await componentScopeResolver.resolve(component);
1,757✔
203
                interpreter.inSubEnv((subInterpreter) => {
1,757✔
204
                    let componentMPointer = new RoAssociativeArray([]);
1,757✔
205
                    subInterpreter.environment.setM(componentMPointer);
1,757✔
206
                    subInterpreter.environment.setRootM(componentMPointer);
1,757✔
207
                    subInterpreter.exec(statements);
1,757✔
208
                    return BrsInvalid.Instance;
1,757✔
209
                }, component.environment);
210
            })
211
        );
212

213
        return interpreter;
113✔
214
    }
215

216
    /**
217
     * Merges this environment's node definition mapping with the ones included in an array of other
218
     * interpreters, acting logically equivalent to
219
     * `Object.assign(this.environment, other1.environment, other2.environment, …)`.
220
     * @param interpreters the array of interpreters who's environment's node definition maps will
221
     *                     be merged into this one
222
     */
223
    public mergeNodeDefinitionsWith(interpreters: Interpreter[]): void {
224
        interpreters.map((other) =>
112✔
225
            other.environment.nodeDefMap.forEach((value, key) =>
1✔
226
                this.environment.nodeDefMap.set(key, value)
1✔
227
            )
228
        );
229
    }
230

231
    /**
232
     * Creates a new Interpreter, including any global properties and functions.
233
     * @param options configuration for the execution, including the streams to use for `stdout` and
234
     *                `stderr` and the base directory for path resolution
235
     */
236
    constructor(options: ExecutionOptions = defaultExecutionOptions) {
597✔
237
        this.stdout = new OutputProxy(options.stdout);
856✔
238
        this.stderr = new OutputProxy(options.stderr);
856✔
239
        this.options = options;
856✔
240
        this.location = {
856✔
241
            file: "(none)",
242
            start: { line: -1, column: -1 },
243
            end: { line: -1, column: -1 },
244
        };
245

246
        Object.keys(StdLib)
856✔
247
            .map((name) => (StdLib as any)[name])
52,216✔
248
            .filter((func) => func instanceof Callable)
52,216✔
249
            .filter((func: Callable) => {
250
                if (!func.name) {
48,792!
251
                    throw new Error("Unnamed standard library function detected!");
×
252
                }
253

254
                return !!func.name;
48,792✔
255
            })
256
            .forEach((func: Callable) =>
257
                this._environment.define(Scope.Global, func.name || "", func)
48,792!
258
            );
259

260
        this._environment.define(Scope.Global, "_brs_", _brs_);
856✔
261
    }
262

263
    /**
264
     * Temporarily sets an interpreter's environment to the provided one, then
265
     * passes the sub-interpreter to the provided JavaScript function. Always
266
     * reverts the current interpreter's environment to its original value.
267
     * @param func the JavaScript function to execute with the sub interpreter.
268
     * @param environment (Optional) the environment to run the interpreter in.
269
     */
270
    inSubEnv(func: (interpreter: Interpreter) => BrsType, environment?: Environment): BrsType {
271
        let originalEnvironment = this._environment;
23,622✔
272
        let newEnv = environment ?? this._environment.createSubEnvironment();
23,622✔
273
        // Set the focused node of the sub env, because our current env has the most up-to-date reference.
274
        newEnv.setFocusedNode(this._environment.getFocusedNode());
23,622✔
275

276
        try {
23,622✔
277
            this._environment = newEnv;
23,622✔
278
            const returnValue = func(this);
23,622✔
279
            this._environment = originalEnvironment;
23,328✔
280
            this._environment.setFocusedNode(newEnv.getFocusedNode());
23,328✔
281
            return returnValue;
23,328✔
282
        } catch (err) {
283
            this._environment = originalEnvironment;
294✔
284
            this._environment.setFocusedNode(newEnv.getFocusedNode());
294✔
285
            throw err;
294✔
286
        }
287
    }
288

289
    exec(statements: ReadonlyArray<Stmt.Statement>, ...args: BrsType[]) {
290
        let results = statements.map((statement) => this.execute(statement));
5,061✔
291
        try {
2,033✔
292
            let mainVariable = new Expr.Variable({
2,033✔
293
                kind: Lexeme.Identifier,
294
                text: "main",
295
                isReserved: false,
296
                location: {
297
                    start: {
298
                        line: -1,
299
                        column: -1,
300
                    },
301
                    end: {
302
                        line: -1,
303
                        column: -1,
304
                    },
305
                    file: "(internal)",
306
                },
307
            });
308

309
            let maybeMain = this.evaluate(mainVariable);
2,033✔
310

311
            if (maybeMain.kind === ValueKind.Callable) {
2,033✔
312
                results = [
105✔
313
                    this.evaluate(
314
                        new Expr.Call(
315
                            mainVariable,
316
                            mainVariable.name,
317
                            args.map((arg) => new Expr.Literal(arg, mainVariable.location))
8✔
318
                        )
319
                    ),
320
                ];
321
            }
322
        } catch (err) {
323
            if (err instanceof Stmt.ReturnValue) {
12!
324
                results = [err.value || BrsInvalid.Instance];
×
325
            } else if (!(err instanceof BrsError)) {
12!
326
                // Swallow BrsErrors, because they should have been exposed to the user downstream.
327
                throw err;
×
328
            }
329
        }
330

331
        return results;
2,033✔
332
    }
333

334
    getCallableFunction(functionName: string): Callable | undefined {
335
        let callbackVariable = new Expr.Variable({
63✔
336
            kind: Lexeme.Identifier,
337
            text: functionName,
338
            isReserved: false,
339
            location: {
340
                start: {
341
                    line: -1,
342
                    column: -1,
343
                },
344
                end: {
345
                    line: -1,
346
                    column: -1,
347
                },
348
                file: "(internal)",
349
            },
350
        });
351

352
        let maybeCallback = this.evaluate(callbackVariable);
63✔
353
        if (maybeCallback.kind === ValueKind.Callable) {
63✔
354
            return maybeCallback;
61✔
355
        }
356

357
        // If we can't find the function, return undefined and let the consumer handle it.
358
        return;
2✔
359
    }
360

361
    /**
362
     * Returns the init method (if any) in the current environment as a Callable
363
     */
364
    getInitMethod(): BrsType {
365
        let initVariable = new Expr.Variable({
81✔
366
            kind: Lexeme.Identifier,
367
            text: "init",
368
            isReserved: false,
369
            location: {
370
                start: {
371
                    line: -1,
372
                    column: -1,
373
                },
374
                end: {
375
                    line: -1,
376
                    column: -1,
377
                },
378
                file: "(internal)",
379
            },
380
        });
381

382
        return this.evaluate(initVariable);
81✔
383
    }
384

385
    visitLibrary(statement: Stmt.Library): BrsInvalid {
386
        this.stderr.write("WARNING: 'Library' statement implemented as no-op");
×
387
        return BrsInvalid.Instance;
×
388
    }
389

390
    visitNamedFunction(statement: Stmt.Function): BrsType {
391
        if (statement.name.isReserved) {
4,492✔
392
            this.addError(
1✔
393
                new BrsError(
394
                    `Cannot create a named function with reserved name '${statement.name.text}'`,
395
                    statement.name.location
396
                )
397
            );
398
        }
399

400
        if (this.environment.has(statement.name, [Scope.Module])) {
4,491!
401
            // TODO: Figure out how to determine where the original version was declared
402
            // Maybe `Environment.define` records the location along with the value?
403
            this.addError(
×
404
                new BrsError(
405
                    `Attempting to declare function '${statement.name.text}', but ` +
406
                        `a property of that name already exists in this scope.`,
407
                    statement.name.location
408
                )
409
            );
410
        }
411

412
        this.environment.define(
4,491✔
413
            Scope.Module,
414
            statement.name.text!,
415
            toCallable(statement.func, statement.name.text)
416
        );
417
        return BrsInvalid.Instance;
4,491✔
418
    }
419

420
    visitReturn(statement: Stmt.Return): never {
421
        if (!statement.value) {
129✔
422
            throw new Stmt.ReturnValue(statement.tokens.return.location);
2✔
423
        }
424

425
        let toReturn = this.evaluate(statement.value);
127✔
426
        throw new Stmt.ReturnValue(statement.tokens.return.location, toReturn);
127✔
427
    }
428

429
    visitExpression(statement: Stmt.Expression): BrsType {
430
        return this.evaluate(statement.expression);
4,619✔
431
    }
432

433
    visitPrint(statement: Stmt.Print): BrsType {
434
        // the `tab` function is only in-scope while executing print statements
435
        this.environment.define(Scope.Function, "Tab", StdLib.Tab);
1,239✔
436

437
        statement.expressions.forEach((printable, index) => {
1,239✔
438
            if (isToken(printable)) {
1,780✔
439
                switch (printable.kind) {
35!
440
                    case Lexeme.Comma:
441
                        this.stdout.write(" ".repeat(16 - (this.stdout.position() % 16)));
5✔
442
                        break;
5✔
443
                    case Lexeme.Semicolon:
444
                        break;
30✔
445
                    default:
446
                        this.addError(
×
447
                            new BrsError(
448
                                `Found unexpected print separator '${printable.text}'`,
449
                                printable.location
450
                            )
451
                        );
452
                }
453
            } else {
454
                const toPrint = this.evaluate(printable);
1,745✔
455
                const isNumber = isBrsNumber(toPrint) || isBoxedNumber(toPrint);
1,736✔
456
                const str = isNumber && this.isPositive(toPrint.getValue()) ? " " : "";
1,736✔
457
                this.stdout.write(colorize(str + toPrint.toString()));
1,736✔
458
            }
459
        });
460

461
        let lastExpression = statement.expressions[statement.expressions.length - 1];
1,230✔
462
        if (!isToken(lastExpression) || lastExpression.kind !== Lexeme.Semicolon) {
1,230✔
463
            this.stdout.write("\n");
1,228✔
464
        }
465

466
        // `tab` is only in-scope when executing print statements, so remove it before we leave
467
        this.environment.remove("Tab");
1,230✔
468

469
        return BrsInvalid.Instance;
1,230✔
470
    }
471

472
    visitAssignment(statement: Stmt.Assignment): BrsType {
473
        if (statement.name.isReserved) {
8,951✔
474
            this.addError(
1✔
475
                new BrsError(
476
                    `Cannot assign a value to reserved name '${statement.name.text}'`,
477
                    statement.name.location
478
                )
479
            );
480
            return BrsInvalid.Instance;
×
481
        }
482

483
        let value = this.evaluate(statement.value);
8,950✔
484

485
        let name = statement.name.text;
8,948✔
486

487
        const typeDesignators: Record<string, ValueKind> = {
8,948✔
488
            $: ValueKind.String,
489
            "%": ValueKind.Int32,
490
            "!": ValueKind.Float,
491
            "#": ValueKind.Double,
492
            "&": ValueKind.Int64,
493
        };
494
        let requiredType = typeDesignators[name.charAt(name.length - 1)];
8,948✔
495

496
        if (requiredType) {
8,948✔
497
            let coercedValue = tryCoerce(value, requiredType);
49✔
498
            if (coercedValue != null) {
49✔
499
                value = coercedValue;
40✔
500
            } else {
501
                this.addError(
9✔
502
                    new TypeMismatch({
503
                        message: `Type Mismatch. Attempting to assign incorrect value to statically-typed variable '${name}'`,
504
                        left: {
505
                            type: requiredType,
506
                            location: statement.name.location,
507
                        },
508
                        right: {
509
                            type: value,
510
                            location: statement.value.location,
511
                        },
512
                    })
513
                );
514
            }
515
        }
516

517
        this.environment.define(Scope.Function, statement.name.text, value);
8,939✔
518
        return BrsInvalid.Instance;
8,939✔
519
    }
520

521
    visitDim(statement: Stmt.Dim): BrsType {
522
        if (statement.name.isReserved) {
3!
523
            this.addError(
×
524
                new BrsError(
525
                    `Cannot assign a value to reserved name '${statement.name.text}'`,
526
                    statement.name.location
527
                )
528
            );
529
            return BrsInvalid.Instance;
×
530
        }
531

532
        let dimensionValues: number[] = [];
3✔
533
        statement.dimensions.forEach((expr) => {
3✔
534
            let val = this.evaluate(expr);
5✔
535
            if (val.kind !== ValueKind.Int32) {
5!
536
                this.addError(
×
537
                    new RuntimeError(RuntimeErrorDetail.NonNumericArrayIndex, expr.location)
538
                );
539
            }
540
            // dim takes max-index, so +1 to get the actual array size
541
            dimensionValues.push(val.getValue() + 1);
5✔
542
            return;
5✔
543
        });
544

545
        let createArrayTree = (dimIndex: number = 0): RoArray => {
3✔
546
            let children: RoArray[] = [];
68✔
547
            let size = dimensionValues[dimIndex];
68✔
548
            for (let i = 0; i < size; i++) {
68✔
549
                if (dimIndex < dimensionValues.length) {
65✔
550
                    let subchildren = createArrayTree(dimIndex + 1);
65✔
551
                    if (subchildren !== undefined) children.push(subchildren);
65✔
552
                }
553
            }
554
            let child = new RoArray(children);
68✔
555

556
            return child;
68✔
557
        };
558

559
        let array = createArrayTree();
3✔
560

561
        this.environment.define(Scope.Function, statement.name.text, array);
3✔
562

563
        return BrsInvalid.Instance;
3✔
564
    }
565

566
    visitBinary(expression: Expr.Binary) {
567
        let lexeme = expression.token.kind;
18,125✔
568
        let left = this.evaluate(expression.left);
18,125✔
569
        let right: BrsType = BrsInvalid.Instance;
18,125✔
570

571
        if (lexeme !== Lexeme.And && lexeme !== Lexeme.Or) {
18,125✔
572
            // don't evaluate right-hand-side of boolean expressions, to preserve short-circuiting
573
            // behavior found in other languages. e.g. `foo() && bar()` won't execute `bar()` if
574
            // `foo()` returns `false`.
575
            right = this.evaluate(expression.right);
18,092✔
576
        }
577

578
        // Unbox Numeric or Invalid components to intrinsic types
579
        if (isBoxedNumber(left) || left instanceof RoInvalid) {
18,123✔
580
            left = left.unbox();
8✔
581
        }
582
        if (isBoxedNumber(right) || right instanceof RoInvalid) {
18,123✔
583
            right = right.unbox();
5✔
584
        }
585

586
        /**
587
         * Determines whether or not the provided pair of values are allowed to be compared to each other.
588
         * @param left the left-hand side of a comparison operator
589
         * @param operator the operator to use when comparing `left` and `right`
590
         * @param right the right-hand side of a comparison operator
591
         * @returns `true` if `left` and `right` are allowed to be compared to each other with `operator`,
592
         *          otherwise `false`.
593
         */
594
        function canCheckEquality(left: BrsType, operator: Lexeme, right: BrsType): boolean {
595
            if (left.kind === ValueKind.Invalid || right.kind === ValueKind.Invalid) {
8,284✔
596
                // anything can be checked for *equality* with `invalid`, but greater than / less than comparisons
597
                // are type mismatches
598
                return operator === Lexeme.Equal || operator === Lexeme.LessGreater;
67✔
599
            }
600

601
            return (
8,217✔
602
                (left.kind < ValueKind.Dynamic || isUnboxable(left) || isComparable(left)) &&
16,439!
603
                (right.kind < ValueKind.Dynamic || isUnboxable(right) || isComparable(right))
604
            );
605
        }
606

607
        switch (lexeme) {
18,123!
608
            case Lexeme.LeftShift:
609
            case Lexeme.LeftShiftEqual:
610
                if (
11✔
611
                    isBrsNumber(left) &&
43✔
612
                    isBrsNumber(right) &&
613
                    this.isPositive(right.getValue()) &&
614
                    this.lessThan(right.getValue(), 32)
615
                ) {
616
                    return left.leftShift(right);
7✔
617
                } else if (isBrsNumber(left) && isBrsNumber(right)) {
4!
618
                    return this.addError(
4✔
619
                        new RuntimeError(RuntimeErrorDetail.BadBitShift, expression.right.location)
620
                    );
621
                } else {
622
                    return this.addError(
×
623
                        new TypeMismatch({
624
                            message: `Operator "<<" can't be applied to`,
625
                            left: {
626
                                type: left,
627
                                location: expression.left.location,
628
                            },
629
                            right: {
630
                                type: right,
631
                                location: expression.right.location,
632
                            },
633
                        })
634
                    );
635
                }
636
            case Lexeme.RightShift:
637
            case Lexeme.RightShiftEqual:
638
                if (
9✔
639
                    isBrsNumber(left) &&
35✔
640
                    isBrsNumber(right) &&
641
                    this.isPositive(right.getValue()) &&
642
                    this.lessThan(right.getValue(), 32)
643
                ) {
644
                    return left.rightShift(right);
7✔
645
                } else if (isBrsNumber(left) && isBrsNumber(right)) {
2!
646
                    return this.addError(
2✔
647
                        new RuntimeError(RuntimeErrorDetail.BadBitShift, expression.right.location)
648
                    );
649
                } else {
650
                    return this.addError(
×
651
                        new TypeMismatch({
652
                            message: `Operator ">>" can't be applied to`,
653
                            left: {
654
                                type: left,
655
                                location: expression.left.location,
656
                            },
657
                            right: {
658
                                type: right,
659
                                location: expression.right.location,
660
                            },
661
                        })
662
                    );
663
                }
664
            case Lexeme.Minus:
665
            case Lexeme.MinusEqual:
666
                if (isBrsNumber(left) && isBrsNumber(right)) {
138!
667
                    return left.subtract(right);
138✔
668
                } else {
669
                    return this.addError(
×
670
                        new TypeMismatch({
671
                            message: `Operator "-" can't be applied to`,
672
                            left: {
673
                                type: left,
674
                                location: expression.left.location,
675
                            },
676
                            right: {
677
                                type: right,
678
                                location: expression.right.location,
679
                            },
680
                        })
681
                    );
682
                }
683
            case Lexeme.Star:
684
            case Lexeme.StarEqual:
685
                if (isBrsNumber(left) && isBrsNumber(right)) {
19✔
686
                    return left.multiply(right);
18✔
687
                } else {
688
                    return this.addError(
1✔
689
                        new TypeMismatch({
690
                            message: `Operator "*" can't be applied to`,
691
                            left: {
692
                                type: left,
693
                                location: expression.left.location,
694
                            },
695
                            right: {
696
                                type: right,
697
                                location: expression.right.location,
698
                            },
699
                        })
700
                    );
701
                }
702
            case Lexeme.Caret:
703
                if (isBrsNumber(left) && isBrsNumber(right)) {
7!
704
                    return left.pow(right);
7✔
705
                } else {
706
                    return this.addError(
×
707
                        new TypeMismatch({
708
                            message: `Operator "^" can't be applied to`,
709
                            left: {
710
                                type: left,
711
                                location: expression.left.location,
712
                            },
713
                            right: {
714
                                type: right,
715
                                location: expression.right.location,
716
                            },
717
                        })
718
                    );
719
                }
720
            case Lexeme.Slash:
721
            case Lexeme.SlashEqual:
722
                if (isBrsNumber(left) && isBrsNumber(right)) {
11✔
723
                    return left.divide(right);
11✔
724
                }
725
                return this.addError(
×
726
                    new TypeMismatch({
727
                        message: `Operator "/" can't be applied to`,
728
                        left: {
729
                            type: left,
730
                            location: expression.left.location,
731
                        },
732
                        right: {
733
                            type: right,
734
                            location: expression.right.location,
735
                        },
736
                    })
737
                );
738
            case Lexeme.Mod:
739
                if (isBrsNumber(left) && isBrsNumber(right)) {
9!
740
                    return left.modulo(right);
9✔
741
                } else {
742
                    return this.addError(
×
743
                        new TypeMismatch({
744
                            message: `Operator "mod" can't be applied to`,
745
                            left: {
746
                                type: left,
747
                                location: expression.left.location,
748
                            },
749
                            right: {
750
                                type: right,
751
                                location: expression.right.location,
752
                            },
753
                        })
754
                    );
755
                }
756
            case Lexeme.Backslash:
757
            case Lexeme.BackslashEqual:
758
                if (isBrsNumber(left) && isBrsNumber(right)) {
3!
759
                    return left.intDivide(right);
3✔
760
                } else {
761
                    return this.addError(
×
762
                        new TypeMismatch({
763
                            message: `Operator "\\" can't be applied to`,
764
                            left: {
765
                                type: left,
766
                                location: expression.left.location,
767
                            },
768
                            right: {
769
                                type: right,
770
                                location: expression.right.location,
771
                            },
772
                        })
773
                    );
774
                }
775
            case Lexeme.Plus:
776
            case Lexeme.PlusEqual:
777
                if (isBrsNumber(left) && isBrsNumber(right)) {
9,163✔
778
                    return left.add(right);
8,407✔
779
                } else if (isStringComp(left) && isStringComp(right)) {
756✔
780
                    return left.concat(right);
755✔
781
                } else {
782
                    return this.addError(
1✔
783
                        new TypeMismatch({
784
                            message: `Operator "+" can't be applied to`,
785
                            left: {
786
                                type: left,
787
                                location: expression.left.location,
788
                            },
789
                            right: {
790
                                type: right,
791
                                location: expression.right.location,
792
                            },
793
                        })
794
                    );
795
                }
796
            case Lexeme.Greater:
797
                if (
189✔
798
                    (isBrsNumber(left) && isBrsNumber(right)) ||
388✔
799
                    (isStringComp(left) && isStringComp(right))
800
                ) {
801
                    return left.greaterThan(right);
172✔
802
                }
803

804
                return this.addError(
17✔
805
                    new TypeMismatch({
806
                        message: `Operator ">" can't be applied to`,
807
                        left: {
808
                            type: left,
809
                            location: expression.left.location,
810
                        },
811
                        right: {
812
                            type: right,
813
                            location: expression.right.location,
814
                        },
815
                    })
816
                );
817

818
            case Lexeme.GreaterEqual:
819
                if (
35✔
820
                    (isBrsNumber(left) && isBrsNumber(right)) ||
80✔
821
                    (isStringComp(left) && isStringComp(right))
822
                ) {
823
                    return left.greaterThan(right).or(left.equalTo(right));
18✔
824
                }
825

826
                return this.addError(
17✔
827
                    new TypeMismatch({
828
                        message: `Operator ">=" can't be applied to`,
829
                        left: {
830
                            type: left,
831
                            location: expression.left.location,
832
                        },
833
                        right: {
834
                            type: right,
835
                            location: expression.right.location,
836
                        },
837
                    })
838
                );
839

840
            case Lexeme.Less:
841
                if (
178✔
842
                    (isBrsNumber(left) && isBrsNumber(right)) ||
369✔
843
                    (isStringComp(left) && isStringComp(right))
844
                ) {
845
                    return left.lessThan(right);
158✔
846
                }
847

848
                return this.addError(
20✔
849
                    new TypeMismatch({
850
                        message: `Operator "<" can't be applied to`,
851
                        left: {
852
                            type: left,
853
                            location: expression.left.location,
854
                        },
855
                        right: {
856
                            type: right,
857
                            location: expression.right.location,
858
                        },
859
                    })
860
                );
861
            case Lexeme.LessEqual:
862
                if (
34✔
863
                    (isBrsNumber(left) && isBrsNumber(right)) ||
78✔
864
                    (isStringComp(left) && isStringComp(right))
865
                ) {
866
                    return left.lessThan(right).or(left.equalTo(right));
17✔
867
                }
868

869
                return this.addError(
17✔
870
                    new TypeMismatch({
871
                        message: `Operator "<=" can't be applied to`,
872
                        left: {
873
                            type: left,
874
                            location: expression.left.location,
875
                        },
876
                        right: {
877
                            type: right,
878
                            location: expression.right.location,
879
                        },
880
                    })
881
                );
882
            case Lexeme.Equal:
883
                if (canCheckEquality(left, lexeme, right)) {
121✔
884
                    return left.equalTo(right);
120✔
885
                }
886

887
                return this.addError(
1✔
888
                    new TypeMismatch({
889
                        message: `Operator "=" can't be applied to`,
890
                        left: {
891
                            type: left,
892
                            location: expression.left.location,
893
                        },
894
                        right: {
895
                            type: right,
896
                            location: expression.right.location,
897
                        },
898
                    })
899
                );
900
            case Lexeme.LessGreater:
901
                if (canCheckEquality(left, lexeme, right)) {
8,163✔
902
                    return left.equalTo(right).not();
8,162✔
903
                }
904

905
                return this.addError(
1✔
906
                    new TypeMismatch({
907
                        message: `Operator "<>" can't be applied to`,
908
                        left: {
909
                            type: left,
910
                            location: expression.left.location,
911
                        },
912
                        right: {
913
                            type: right,
914
                            location: expression.right.location,
915
                        },
916
                    })
917
                );
918
            case Lexeme.And:
919
                if (isBrsBoolean(left) && !left.toBoolean()) {
26✔
920
                    // short-circuit ANDs - don't evaluate RHS if LHS is false
921
                    return BrsBoolean.False;
12✔
922
                } else if (isBrsBoolean(left)) {
14✔
923
                    right = this.evaluate(expression.right);
13✔
924
                    if (isBrsBoolean(right) || isBrsNumber(right)) {
13✔
925
                        return (left as BrsBoolean).and(right);
12✔
926
                    }
927

928
                    return this.addError(
1✔
929
                        new TypeMismatch({
930
                            message: `Operator "and" can't be applied to`,
931
                            left: {
932
                                type: left,
933
                                location: expression.left.location,
934
                            },
935
                            right: {
936
                                type: right,
937
                                location: expression.right.location,
938
                            },
939
                        })
940
                    );
941
                } else if (isBrsNumber(left)) {
1!
942
                    right = this.evaluate(expression.right);
1✔
943

944
                    if (isBrsNumber(right) || isBrsBoolean(right)) {
1!
945
                        return left.and(right);
1✔
946
                    }
947

948
                    return this.addError(
×
949
                        new TypeMismatch({
950
                            message: `Operator "and" can't be applied to`,
951
                            left: {
952
                                type: left,
953
                                location: expression.left.location,
954
                            },
955
                            right: {
956
                                type: right,
957
                                location: expression.right.location,
958
                            },
959
                        })
960
                    );
961
                } else {
962
                    return this.addError(
×
963
                        new TypeMismatch({
964
                            message: `Operator "and" can't be applied to`,
965
                            left: {
966
                                type: left,
967
                                location: expression.left.location,
968
                            },
969
                            right: {
970
                                type: right,
971
                                location: expression.right.location,
972
                            },
973
                        })
974
                    );
975
                }
976
            case Lexeme.Or:
977
                if (isBrsBoolean(left) && left.toBoolean()) {
7✔
978
                    // short-circuit ORs - don't evaluate RHS if LHS is true
979
                    return BrsBoolean.True;
2✔
980
                } else if (isBrsBoolean(left)) {
5✔
981
                    right = this.evaluate(expression.right);
4✔
982
                    if (isBrsBoolean(right) || isBrsNumber(right)) {
4✔
983
                        return (left as BrsBoolean).or(right);
3✔
984
                    } else {
985
                        return this.addError(
1✔
986
                            new TypeMismatch({
987
                                message: `Operator "or" can't be applied to`,
988
                                left: {
989
                                    type: left,
990
                                    location: expression.left.location,
991
                                },
992
                                right: {
993
                                    type: right,
994
                                    location: expression.right.location,
995
                                },
996
                            })
997
                        );
998
                    }
999
                } else if (isBrsNumber(left)) {
1!
1000
                    right = this.evaluate(expression.right);
1✔
1001
                    if (isBrsNumber(right) || isBrsBoolean(right)) {
1!
1002
                        return left.or(right);
1✔
1003
                    }
1004

1005
                    return this.addError(
×
1006
                        new TypeMismatch({
1007
                            message: `Operator "or" can't be applied to`,
1008
                            left: {
1009
                                type: left,
1010
                                location: expression.left.location,
1011
                            },
1012
                            right: {
1013
                                type: right,
1014
                                location: expression.right.location,
1015
                            },
1016
                        })
1017
                    );
1018
                } else {
1019
                    return this.addError(
×
1020
                        new TypeMismatch({
1021
                            message: `Operator "or" can't be applied to`,
1022
                            left: {
1023
                                type: left,
1024
                                location: expression.left.location,
1025
                            },
1026
                            right: {
1027
                                type: right,
1028
                                location: expression.right.location,
1029
                            },
1030
                        })
1031
                    );
1032
                }
1033
            default:
1034
                this.addError(
×
1035
                    new BrsError(
1036
                        `Received unexpected token kind '${expression.token.kind}'`,
1037
                        expression.token.location
1038
                    )
1039
                );
1040
        }
1041
    }
1042

1043
    visitTryCatch(statement: Stmt.TryCatch): BrsInvalid {
1044
        let tryMode = this._tryMode;
9✔
1045
        try {
9✔
1046
            this._tryMode = true;
9✔
1047
            this.visitBlock(statement.tryBlock);
9✔
1048
            this._tryMode = tryMode;
1✔
1049
        } catch (err: any) {
1050
            this._tryMode = tryMode;
8✔
1051
            if (!(err instanceof BrsError)) {
8!
1052
                throw err;
×
1053
            }
1054
            const btArray = this.formatBacktrace(err.location, err.backTrace);
8✔
1055
            let errDetail = RuntimeErrorDetail.Internal;
8✔
1056
            let errMessage = err.message;
8✔
1057
            if (err instanceof RuntimeError) {
8✔
1058
                errDetail = err.errorDetail;
8✔
1059
            }
1060
            const errorAA = toAssociativeArray({
8✔
1061
                backtrace: btArray,
1062
                message: errMessage,
1063
                number: errDetail.errno,
1064
                rethrown: false,
1065
            });
1066
            if (err instanceof RuntimeError && err.extraFields?.size) {
8✔
1067
                for (const [key, value] of err.extraFields) {
2✔
1068
                    errorAA.set(new BrsString(key), value);
4✔
1069
                    if (key === "rethrown" && toBool(value)) {
4✔
1070
                        errorAA.set(new BrsString("rethrow_backtrace"), btArray);
1✔
1071
                    }
1072
                }
1073
            }
1074
            this.environment.define(Scope.Function, statement.errorBinding.name.text, errorAA);
8✔
1075
            this.visitBlock(statement.catchBlock);
8✔
1076
        }
1077
        return BrsInvalid.Instance;
8✔
1078
        // Helper Function
1079
        function toBool(value: BrsType): boolean {
1080
            return isBrsBoolean(value) && value.toBoolean();
1✔
1081
        }
1082
    }
1083

1084
    visitThrow(statement: Stmt.Throw): never {
1085
        let errDetail = RuntimeErrorDetail.UserDefined;
2✔
1086
        errDetail.message = "";
2✔
1087
        const extraFields: Map<string, BrsType> = new Map<string, BrsType>();
2✔
1088
        let toThrow = this.evaluate(statement.value);
2✔
1089
        if (isStringComp(toThrow)) {
2!
1090
            errDetail.message = toThrow.getValue();
×
1091
        } else if (toThrow instanceof RoAssociativeArray) {
2!
1092
            for (const [key, element] of toThrow.elements) {
2✔
1093
                if (key.toLowerCase() === "number") {
8✔
1094
                    errDetail = validateErrorNumber(element, errDetail);
2✔
1095
                } else if (key.toLowerCase() === "message") {
6✔
1096
                    errDetail = validateErrorMessage(element, errDetail);
2✔
1097
                } else if (key.toLowerCase() === "backtrace") {
4✔
1098
                    if (element instanceof RoArray) {
1!
1099
                        extraFields.set("backtrace", element);
1✔
1100
                        extraFields.set("rethrown", BrsBoolean.True);
1✔
1101
                    } else {
1102
                        errDetail = RuntimeErrorDetail.MalformedThrow;
×
1103
                        errDetail.message = `Thrown "backtrace" is not an object.`;
×
1104
                    }
1105
                } else if (key.toLowerCase() !== "rethrown") {
3✔
1106
                    extraFields.set(key, element);
2✔
1107
                }
1108
                if (errDetail.errno === RuntimeErrorDetail.MalformedThrow.errno) {
8!
1109
                    extraFields.clear();
×
1110
                    break;
×
1111
                }
1112
            }
1113
        } else {
1114
            errDetail = RuntimeErrorDetail.MalformedThrow;
×
1115
            errDetail.message = `Thrown value neither string nor roAssociativeArray.`;
×
1116
        }
1117
        throw new RuntimeError(errDetail, statement.location, this._stack.slice(), extraFields);
2✔
1118
        // Validation Functions
1119
        function validateErrorNumber(element: BrsType, errDetail: ErrorDetail): ErrorDetail {
1120
            if (element instanceof Int32) {
2!
1121
                errDetail.errno = element.getValue();
2✔
1122
                if (errDetail.message === "") {
2!
1123
                    const foundErr = findErrorDetail(element.getValue());
×
1124
                    errDetail.message = foundErr ? foundErr.message : "UNKNOWN ERROR";
×
1125
                }
1126
            } else if (!(element instanceof BrsInvalid)) {
×
1127
                return {
×
1128
                    errno: RuntimeErrorDetail.MalformedThrow.errno,
1129
                    message: `Thrown "number" is not an integer.`,
1130
                };
1131
            }
1132
            return errDetail;
2✔
1133
        }
1134
        function validateErrorMessage(element: BrsType, errDetail: ErrorDetail): ErrorDetail {
1135
            if (element instanceof BrsString) {
2!
1136
                errDetail.message = element.toString();
2✔
1137
            } else if (!(element instanceof BrsInvalid)) {
×
1138
                return {
×
1139
                    errno: RuntimeErrorDetail.MalformedThrow.errno,
1140
                    message: `Thrown "message" is not a string.`,
1141
                };
1142
            }
1143
            return errDetail;
2✔
1144
        }
1145
    }
1146

1147
    visitBlock(block: Stmt.Block): BrsType {
1148
        block.statements.forEach((statement) => this.execute(statement));
14,902✔
1149
        return BrsInvalid.Instance;
8,878✔
1150
    }
1151

1152
    visitContinueFor(statement: Stmt.ContinueFor): never {
1153
        throw new Stmt.ContinueForReason(statement.location);
10✔
1154
    }
1155

1156
    visitExitFor(statement: Stmt.ExitFor): never {
1157
        throw new Stmt.ExitForReason(statement.location);
3✔
1158
    }
1159

1160
    visitContinueWhile(statement: Stmt.ContinueWhile): never {
1161
        throw new Stmt.ContinueWhileReason(statement.location);
6✔
1162
    }
1163

1164
    visitExitWhile(statement: Stmt.ExitWhile): never {
1165
        throw new Stmt.ExitWhileReason(statement.location);
2✔
1166
    }
1167

1168
    visitCall(expression: Expr.Call) {
1169
        let functionName = "[anonymous function]";
9,913✔
1170
        // TODO: auto-box
1171
        if (
9,913✔
1172
            expression.callee instanceof Expr.Variable ||
19,186✔
1173
            expression.callee instanceof Expr.DottedGet
1174
        ) {
1175
            functionName = expression.callee.name.text;
9,910✔
1176
        }
1177

1178
        // evaluate the function to call (it could be the result of another function call)
1179
        const callee = this.evaluate(expression.callee);
9,913✔
1180
        // evaluate all of the arguments as well (they could also be function calls)
1181
        let args = expression.args.map(this.evaluate, this);
9,911✔
1182

1183
        if (!isBrsCallable(callee)) {
9,911✔
1184
            if (callee instanceof BrsInvalid && expression.optional) {
9✔
1185
                return callee;
4✔
1186
            }
1187
            this.addError(
5✔
1188
                new RuntimeError(RuntimeErrorDetail.NotAFunction, expression.closingParen.location)
1189
            );
1190
        }
1191

1192
        functionName = callee.getName();
9,902✔
1193

1194
        let satisfiedSignature = callee.getFirstSatisfiedSignature(args);
9,902✔
1195

1196
        if (satisfiedSignature) {
9,902✔
1197
            try {
9,895✔
1198
                let signature = satisfiedSignature.signature;
9,895✔
1199
                args = args.map((arg, index) => {
9,895✔
1200
                    // any arguments of type "object" must be automatically boxed
1201
                    if (signature.args[index]?.type.kind === ValueKind.Object && isBoxable(arg)) {
5,172✔
1202
                        return arg.box();
6✔
1203
                    }
1204

1205
                    return arg;
5,166✔
1206
                });
1207
                let mPointer = this._environment.getRootM();
9,895✔
1208
                if (expression.callee instanceof Expr.DottedGet) {
9,895✔
1209
                    mPointer = callee.getContext() ?? mPointer;
9,262✔
1210
                }
1211
                return this.inSubEnv((subInterpreter) => {
9,895✔
1212
                    subInterpreter.environment.setM(mPointer);
9,895✔
1213
                    this._stack.push({
9,895✔
1214
                        functionName: functionName,
1215
                        functionLocation: callee.getLocation() ?? this.location,
29,685✔
1216
                        callLocation: expression.callee.location,
1217
                        signature: signature,
1218
                    });
1219
                    try {
9,895✔
1220
                        const returnValue = callee.call(this, ...args);
9,895✔
1221
                        this._stack.pop();
9,769✔
1222
                        return returnValue;
9,769✔
1223
                    } catch (err) {
1224
                        this._stack.pop();
126✔
1225
                        throw err;
126✔
1226
                    }
1227
                });
1228
            } catch (reason) {
1229
                if (!(reason instanceof Stmt.BlockEnd)) {
126✔
1230
                    // re-throw interpreter errors
1231
                    throw reason;
17✔
1232
                }
1233

1234
                let returnedValue = (reason as Stmt.ReturnValue).value;
109✔
1235
                let returnLocation = (reason as Stmt.ReturnValue).location;
109✔
1236
                const signatureKind = satisfiedSignature.signature.returns;
109✔
1237

1238
                if (returnedValue && signatureKind === ValueKind.Void) {
109✔
1239
                    this.addError(
1✔
1240
                        new RuntimeError(RuntimeErrorDetail.ReturnWithValue, returnLocation)
1241
                    );
1242
                }
1243

1244
                if (!returnedValue && signatureKind !== ValueKind.Void) {
108✔
1245
                    this.addError(
1✔
1246
                        new RuntimeError(RuntimeErrorDetail.ReturnWithoutValue, returnLocation)
1247
                    );
1248
                }
1249

1250
                if (returnedValue) {
107✔
1251
                    let coercedValue = tryCoerce(returnedValue, signatureKind);
107✔
1252
                    if (coercedValue != null) {
107✔
1253
                        return coercedValue;
104✔
1254
                    }
1255
                }
1256

1257
                if (
3✔
1258
                    returnedValue &&
9✔
1259
                    signatureKind !== ValueKind.Dynamic &&
1260
                    signatureKind !== returnedValue.kind
1261
                ) {
1262
                    this.addError(
3✔
1263
                        new TypeMismatch({
1264
                            message: `Unable to cast`,
1265
                            left: {
1266
                                type: signatureKind,
1267
                                location: returnLocation,
1268
                            },
1269
                            right: {
1270
                                type: returnedValue,
1271
                                location: returnLocation,
1272
                            },
1273
                            cast: true,
1274
                        })
1275
                    );
1276
                }
1277

1278
                return returnedValue || BrsInvalid.Instance;
×
1279
            }
1280
        } else {
1281
            this.addError(
7✔
1282
                generateArgumentMismatchError(callee, args, expression.closingParen.location)
1283
            );
1284
        }
1285
    }
1286

1287
    visitDottedGet(expression: Expr.DottedGet) {
1288
        let source = this.evaluate(expression.obj);
10,022✔
1289

1290
        if (isIterable(source)) {
10,021✔
1291
            try {
9,415✔
1292
                const target = source.get(new BrsString(expression.name.text));
9,415✔
1293
                if (isBrsCallable(target) && source instanceof RoAssociativeArray) {
9,415✔
1294
                    target.setContext(source);
143✔
1295
                }
1296
                return target;
9,415✔
1297
            } catch (err: any) {
1298
                this.addError(new BrsError(err.message, expression.name.location));
×
1299
            }
1300
        }
1301

1302
        let boxedSource = isBoxable(source) ? source.box() : source;
606✔
1303
        let errorDetail = RuntimeErrorDetail.DotOnNonObject;
606✔
1304
        if (boxedSource instanceof BrsComponent) {
606✔
1305
            const invalidSource = BrsInvalid.Instance.equalTo(source).toBoolean();
604✔
1306
            // This check is supposed to be placed after method check,
1307
            // but it's here to mimic the behavior of Roku, if they fix, we move it.
1308
            if (invalidSource && expression.optional) {
604✔
1309
                return source;
53✔
1310
            }
1311
            const method = boxedSource.getMethod(expression.name.text);
551✔
1312
            if (method) {
551✔
1313
                return method;
549✔
1314
            } else if (!invalidSource) {
2✔
1315
                errorDetail = RuntimeErrorDetail.MemberFunctionNotFound;
1✔
1316
            }
1317
        }
1318
        this.addError(new RuntimeError(errorDetail, expression.name.location));
4✔
1319
    }
1320

1321
    visitIndexedGet(expression: Expr.IndexedGet): BrsType {
1322
        let source = this.evaluate(expression.obj);
8,205✔
1323
        if (!isIterable(source)) {
8,205✔
1324
            if (source instanceof BrsInvalid && expression.optional) {
5✔
1325
                return source;
5✔
1326
            }
1327
            this.addError(new RuntimeError(RuntimeErrorDetail.UndimmedArray, expression.location));
×
1328
        }
1329

1330
        if (
8,200✔
1331
            source instanceof RoAssociativeArray ||
24,586✔
1332
            source instanceof RoXMLElement ||
1333
            source instanceof RoSGNode
1334
        ) {
1335
            if (expression.indexes.length !== 1) {
8!
1336
                this.addError(
×
1337
                    new RuntimeError(
1338
                        RuntimeErrorDetail.WrongNumberOfParams,
1339
                        expression.closingSquare.location
1340
                    )
1341
                );
1342
            }
1343
            let index = this.evaluate(expression.indexes[0]);
8✔
1344
            if (!isBrsString(index)) {
8!
1345
                this.addError(
×
1346
                    new TypeMismatch({
1347
                        message: `"String" should be used as key, but received`,
1348
                        left: {
1349
                            type: index,
1350
                            location: expression.indexes[0].location,
1351
                        },
1352
                    })
1353
                );
1354
            }
1355
            try {
8✔
1356
                return source.get(index, true);
8✔
1357
            } catch (err: any) {
1358
                this.addError(new BrsError(err.message, expression.closingSquare.location));
×
1359
            }
1360
        }
1361
        if (source instanceof RoByteArray) {
8,192✔
1362
            if (expression.indexes.length !== 1) {
8,097!
1363
                this.addError(
×
1364
                    new RuntimeError(
1365
                        RuntimeErrorDetail.BadNumberOfIndexes,
1366
                        expression.closingSquare.location
1367
                    )
1368
                );
1369
            }
1370
        }
1371
        let current: BrsType = source;
8,192✔
1372
        for (let index of expression.indexes) {
8,192✔
1373
            let dimIndex = this.evaluate(index);
8,200✔
1374
            if (!isBrsNumber(dimIndex)) {
8,200✔
1375
                this.addError(
1✔
1376
                    new RuntimeError(RuntimeErrorDetail.NonNumericArrayIndex, index.location)
1377
                );
1378
            }
1379
            if (
8,199!
1380
                current instanceof RoArray ||
16,298✔
1381
                current instanceof RoByteArray ||
1382
                current instanceof RoList
1383
            ) {
1384
                try {
8,199✔
1385
                    current = current.get(dimIndex);
8,199✔
1386
                } catch (err: any) {
1387
                    this.addError(new BrsError(err.message, index.location));
×
1388
                }
1389
            } else {
1390
                this.addError(
×
1391
                    new RuntimeError(RuntimeErrorDetail.BadNumberOfIndexes, expression.location)
1392
                );
1393
            }
1394
        }
1395
        return current;
8,191✔
1396
    }
1397

1398
    visitGrouping(expr: Expr.Grouping) {
1399
        return this.evaluate(expr.expression);
28✔
1400
    }
1401

1402
    visitFor(statement: Stmt.For): BrsType {
1403
        // BrightScript for/to loops evaluate the counter initial value, final value, and increment
1404
        // values *only once*, at the top of the for/to loop.
1405
        this.execute(statement.counterDeclaration);
24✔
1406
        const startValue = this.evaluate(statement.counterDeclaration.value) as Int32 | Float;
24✔
1407
        const finalValue = this.evaluate(statement.finalValue) as Int32 | Float;
24✔
1408
        let increment = this.evaluate(statement.increment) as Int32 | Float;
24✔
1409
        if (increment instanceof Float) {
24✔
1410
            increment = new Int32(Math.trunc(increment.getValue()));
2✔
1411
        }
1412
        if (
24✔
1413
            (startValue.getValue() > finalValue.getValue() && increment.getValue() > 0) ||
70✔
1414
            (startValue.getValue() < finalValue.getValue() && increment.getValue() < 0)
1415
        ) {
1416
            // Shortcut, do not process anything
1417
            return BrsInvalid.Instance;
2✔
1418
        }
1419
        const counterName = statement.counterDeclaration.name;
22✔
1420
        const step = new Stmt.Assignment(
22✔
1421
            { equals: statement.tokens.for },
1422
            counterName,
1423
            new Expr.Binary(
1424
                new Expr.Variable(counterName),
1425
                {
1426
                    kind: Lexeme.Plus,
1427
                    text: "+",
1428
                    isReserved: false,
1429
                    location: {
1430
                        start: {
1431
                            line: -1,
1432
                            column: -1,
1433
                        },
1434
                        end: {
1435
                            line: -1,
1436
                            column: -1,
1437
                        },
1438
                        file: "(internal)",
1439
                    },
1440
                },
1441
                new Expr.Literal(increment, statement.increment.location)
1442
            )
1443
        );
1444

1445
        if (increment.getValue() > 0) {
22✔
1446
            while (
20✔
1447
                (this.evaluate(new Expr.Variable(counterName)) as Int32 | Float)
1448
                    .greaterThan(finalValue)
1449
                    .not()
1450
                    .toBoolean()
1451
            ) {
1452
                // execute the block
1453
                try {
8,286✔
1454
                    this.execute(statement.body);
8,286✔
1455
                } catch (reason) {
1456
                    if (reason instanceof Stmt.ExitForReason) {
9✔
1457
                        break;
2✔
1458
                    } else if (reason instanceof Stmt.ContinueForReason) {
7✔
1459
                        // continue to the next iteration
1460
                    } else {
1461
                        // re-throw returns, runtime errors, etc.
1462
                        throw reason;
1✔
1463
                    }
1464
                }
1465

1466
                // then increment the counter
1467
                this.execute(step);
8,283✔
1468
            }
1469
        } else {
1470
            while (
2✔
1471
                (this.evaluate(new Expr.Variable(counterName)) as Int32 | Float)
1472
                    .lessThan(finalValue)
1473
                    .not()
1474
                    .toBoolean()
1475
            ) {
1476
                // execute the block
1477
                try {
89✔
1478
                    this.execute(statement.body);
89✔
1479
                } catch (reason) {
1480
                    if (reason instanceof Stmt.ExitForReason) {
×
1481
                        break;
×
1482
                    } else if (reason instanceof Stmt.ContinueForReason) {
×
1483
                        // continue to the next iteration
1484
                    } else {
1485
                        // re-throw returns, runtime errors, etc.
1486
                        throw reason;
×
1487
                    }
1488
                }
1489

1490
                // then increment the counter
1491
                this.execute(step);
89✔
1492
            }
1493
        }
1494

1495
        return BrsInvalid.Instance;
21✔
1496
    }
1497

1498
    visitForEach(statement: Stmt.ForEach): BrsType {
1499
        let target = this.evaluate(statement.target);
21✔
1500
        if (!isIterable(target)) {
21!
1501
            // Roku device does not crash if the value is not iterable, just send a console message
1502
            const message = `BRIGHTSCRIPT: ERROR: Runtime: FOR EACH value is ${ValueKind.toString(
×
1503
                target.kind
1504
            )}`;
1505
            const location = `${statement.item.location.file}(${statement.item.location.start.line})`;
×
1506
            this.stderr.write(`${message}: ${location}\n`);
×
1507
            return BrsInvalid.Instance;
×
1508
        }
1509

1510
        target.getElements().every((element) => {
21✔
1511
            this.environment.define(Scope.Function, statement.item.text!, element);
70✔
1512

1513
            // execute the block
1514
            try {
70✔
1515
                this.execute(statement.body);
70✔
1516
            } catch (reason) {
1517
                if (reason instanceof Stmt.ExitForReason) {
5✔
1518
                    // break out of the loop
1519
                    return false;
1✔
1520
                } else if (reason instanceof Stmt.ContinueForReason) {
4!
1521
                    // continue to the next iteration
1522
                } else {
1523
                    // re-throw returns, runtime errors, etc.
1524
                    throw reason;
×
1525
                }
1526
            }
1527

1528
            // keep looping
1529
            return true;
69✔
1530
        });
1531

1532
        return BrsInvalid.Instance;
21✔
1533
    }
1534

1535
    visitWhile(statement: Stmt.While): BrsType {
1536
        while (this.evaluate(statement.condition).equalTo(BrsBoolean.True).toBoolean()) {
17✔
1537
            try {
63✔
1538
                this.execute(statement.body);
63✔
1539
            } catch (reason) {
1540
                if (reason instanceof Stmt.ExitWhileReason) {
9✔
1541
                    break;
2✔
1542
                } else if (reason instanceof Stmt.ContinueWhileReason) {
7✔
1543
                    // continue to the next iteration
1544
                } else {
1545
                    // re-throw returns, runtime errors, etc.
1546
                    throw reason;
1✔
1547
                }
1548
            }
1549
        }
1550

1551
        return BrsInvalid.Instance;
16✔
1552
    }
1553

1554
    visitIf(statement: Stmt.If): BrsType {
1555
        if (this.evaluate(statement.condition).equalTo(BrsBoolean.True).toBoolean()) {
8,405✔
1556
            this.execute(statement.thenBranch);
55✔
1557
            return BrsInvalid.Instance;
47✔
1558
        } else {
1559
            for (const elseIf of statement.elseIfs || []) {
8,350✔
1560
                if (this.evaluate(elseIf.condition).equalTo(BrsBoolean.True).toBoolean()) {
8✔
1561
                    this.execute(elseIf.thenBranch);
5✔
1562
                    return BrsInvalid.Instance;
5✔
1563
                }
1564
            }
1565

1566
            if (statement.elseBranch) {
8,345✔
1567
                this.execute(statement.elseBranch);
8✔
1568
            }
1569

1570
            return BrsInvalid.Instance;
8,344✔
1571
        }
1572
    }
1573

1574
    visitAnonymousFunction(func: Expr.Function): BrsType {
1575
        return toCallable(func);
45✔
1576
    }
1577

1578
    visitLiteral(expression: Expr.Literal): BrsType {
1579
        return expression.value;
12,305✔
1580
    }
1581

1582
    visitArrayLiteral(expression: Expr.ArrayLiteral): RoArray {
1583
        return new RoArray(expression.elements.map((expr) => this.evaluate(expr)));
164✔
1584
    }
1585

1586
    visitAALiteral(expression: Expr.AALiteral): BrsType {
1587
        return new RoAssociativeArray(
92✔
1588
            expression.elements.map((member) => ({
135✔
1589
                name: member.name,
1590
                value: this.evaluate(member.value),
1591
            }))
1592
        );
1593
    }
1594

1595
    visitDottedSet(statement: Stmt.DottedSet) {
1596
        let source = this.evaluate(statement.obj);
147✔
1597
        let value = this.evaluate(statement.value);
147✔
1598

1599
        if (!isIterable(source)) {
147!
1600
            this.addError(new RuntimeError(RuntimeErrorDetail.BadLHS, statement.name.location));
×
1601
        }
1602

1603
        try {
147✔
1604
            source.set(new BrsString(statement.name.text), value);
147✔
1605
        } catch (err: any) {
1606
            this.addError(new BrsError(err.message, statement.name.location));
×
1607
        }
1608

1609
        return BrsInvalid.Instance;
147✔
1610
    }
1611

1612
    visitIndexedSet(statement: Stmt.IndexedSet) {
1613
        let value = this.evaluate(statement.value);
22✔
1614
        let source = this.evaluate(statement.obj);
22✔
1615

1616
        if (!isIterable(source)) {
22!
1617
            this.addError(new RuntimeError(RuntimeErrorDetail.BadLHS, statement.obj.location));
×
1618
        }
1619

1620
        if (
22✔
1621
            source instanceof RoAssociativeArray ||
44✔
1622
            source instanceof RoXMLElement ||
1623
            source instanceof RoSGNode
1624
        ) {
1625
            if (statement.indexes.length !== 1) {
11!
1626
                this.addError(
×
1627
                    new RuntimeError(
1628
                        RuntimeErrorDetail.WrongNumberOfParams,
1629
                        statement.closingSquare.location
1630
                    )
1631
                );
1632
            }
1633
            let index = this.evaluate(statement.indexes[0]);
11✔
1634
            if (!isBrsString(index)) {
11!
1635
                this.addError(
×
1636
                    new TypeMismatch({
1637
                        message: `"String" should be used as key, but received`,
1638
                        left: {
1639
                            type: index,
1640
                            location: statement.indexes[0].location,
1641
                        },
1642
                    })
1643
                );
1644
            }
1645
            try {
11✔
1646
                source.set(index, value, true);
11✔
1647
            } catch (err: any) {
1648
                this.addError(new BrsError(err.message, statement.closingSquare.location));
×
1649
            }
1650
            return BrsInvalid.Instance;
11✔
1651
        }
1652
        if (source instanceof RoByteArray) {
11!
1653
            if (statement.indexes.length !== 1) {
×
1654
                this.addError(
×
1655
                    new RuntimeError(
1656
                        RuntimeErrorDetail.BadNumberOfIndexes,
1657
                        statement.closingSquare.location
1658
                    )
1659
                );
1660
            }
1661
        }
1662

1663
        let current: BrsType = source;
11✔
1664
        for (let i = 0; i < statement.indexes.length; i++) {
11✔
1665
            let index = this.evaluate(statement.indexes[i]);
12✔
1666
            if (!isBrsNumber(index)) {
12!
1667
                this.addError(
×
1668
                    new RuntimeError(
1669
                        RuntimeErrorDetail.NonNumericArrayIndex,
1670
                        statement.indexes[i].location
1671
                    )
1672
                );
1673
            }
1674

1675
            if (i < statement.indexes.length - 1) {
12✔
1676
                if (
1!
1677
                    current instanceof RoArray ||
1!
1678
                    current instanceof RoByteArray ||
1679
                    current instanceof RoList
1680
                ) {
1681
                    try {
1✔
1682
                        current = current.get(index);
1✔
1683
                    } catch (err: any) {
1684
                        this.addError(new BrsError(err.message, statement.closingSquare.location));
×
1685
                    }
1686
                } else {
1687
                    this.addError(
×
1688
                        new RuntimeError(RuntimeErrorDetail.BadNumberOfIndexes, statement.location)
1689
                    );
1690
                }
1691
            } else if (
11!
1692
                current instanceof RoArray ||
11!
1693
                current instanceof RoByteArray ||
1694
                current instanceof RoList
1695
            ) {
1696
                try {
11✔
1697
                    current.set(index, value);
11✔
1698
                } catch (err: any) {
1699
                    this.addError(new BrsError(err.message, statement.closingSquare.location));
×
1700
                }
1701
            } else {
1702
                this.addError(
×
1703
                    new RuntimeError(RuntimeErrorDetail.BadNumberOfIndexes, statement.location)
1704
                );
1705
            }
1706
        }
1707

1708
        return BrsInvalid.Instance;
11✔
1709
    }
1710

1711
    visitIncrement(statement: Stmt.Increment) {
1712
        let target = this.evaluate(statement.value);
259✔
1713
        if (isBoxedNumber(target)) {
259✔
1714
            target = target.unbox();
1✔
1715
        }
1716

1717
        if (!isBrsNumber(target)) {
259!
1718
            let operation = statement.token.kind === Lexeme.PlusPlus ? "increment" : "decrement";
×
1719
            this.addError(
×
1720
                new TypeMismatch({
1721
                    message: `Attempting to ${operation} value of non-numeric type`,
1722
                    left: {
1723
                        type: target,
1724
                        location: statement.location,
1725
                    },
1726
                })
1727
            );
1728
        }
1729

1730
        let result: BrsNumber;
1731
        if (statement.token.kind === Lexeme.PlusPlus) {
259✔
1732
            result = target.add(new Int32(1));
256✔
1733
        } else {
1734
            result = target.subtract(new Int32(1));
3✔
1735
        }
1736

1737
        if (statement.value instanceof Expr.Variable) {
259✔
1738
            // store the result of the operation
1739
            this.environment.define(Scope.Function, statement.value.name.text, result);
255✔
1740
        } else if (statement.value instanceof Expr.DottedGet) {
4✔
1741
            // immediately execute a dotted "set" statement
1742
            this.execute(
2✔
1743
                new Stmt.DottedSet(
1744
                    statement.value.obj,
1745
                    statement.value.name,
1746
                    new Expr.Literal(result, statement.location)
1747
                )
1748
            );
1749
        } else if (statement.value instanceof Expr.IndexedGet) {
2✔
1750
            // immediately execute an indexed "set" statement
1751
            this.execute(
2✔
1752
                new Stmt.IndexedSet(
1753
                    statement.value.obj,
1754
                    statement.value.indexes,
1755
                    new Expr.Literal(result, statement.location),
1756
                    statement.value.closingSquare
1757
                )
1758
            );
1759
        }
1760

1761
        // always return `invalid`, because ++/-- are purely side-effects in BrightScript
1762
        return BrsInvalid.Instance;
259✔
1763
    }
1764

1765
    visitUnary(expression: Expr.Unary) {
1766
        let right = this.evaluate(expression.right);
75✔
1767
        if (isBoxedNumber(right)) {
75✔
1768
            right = right.unbox();
1✔
1769
        }
1770

1771
        switch (expression.operator.kind) {
75✔
1772
            case Lexeme.Minus:
1773
                if (isBrsNumber(right)) {
46✔
1774
                    return right.multiply(new Int32(-1));
45✔
1775
                } else {
1776
                    return this.addError(
1✔
1777
                        new BrsError(
1778
                            `Attempting to negate non-numeric value.
1779
                            value type: ${ValueKind.toString(right.kind)}`,
1780
                            expression.operator.location
1781
                        )
1782
                    );
1783
                }
1784
            case Lexeme.Plus:
1785
                if (isBrsNumber(right)) {
8!
1786
                    return right;
8✔
1787
                } else {
1788
                    return this.addError(
×
1789
                        new BrsError(
1790
                            `Attempting to apply unary positive operator to non-numeric value.
1791
                            value type: ${ValueKind.toString(right.kind)}`,
1792
                            expression.operator.location
1793
                        )
1794
                    );
1795
                }
1796
            case Lexeme.Not:
1797
                if (isBrsBoolean(right) || isBrsNumber(right)) {
21!
1798
                    return right.not();
21✔
1799
                } else {
1800
                    return this.addError(
×
1801
                        new BrsError(
1802
                            `Type Mismatch. Operator "not" can't be applied to "${ValueKind.toString(
1803
                                right.kind
1804
                            )}".`,
1805
                            expression.operator.location
1806
                        )
1807
                    );
1808
                }
1809
        }
1810

1811
        return BrsInvalid.Instance;
×
1812
    }
1813

1814
    visitVariable(expression: Expr.Variable) {
1815
        try {
54,795✔
1816
            return this.environment.get(expression.name);
54,795✔
1817
        } catch (err) {
1818
            if (err instanceof NotFound) {
1,983✔
1819
                return Uninitialized.Instance;
1,983✔
1820
            }
1821

1822
            throw err;
×
1823
        }
1824
    }
1825

1826
    evaluate(this: Interpreter, expression: Expr.Expression): BrsType {
1827
        this.location = expression.location;
113,662✔
1828
        this.reportCoverageHit(expression);
113,662✔
1829

1830
        return expression.accept<BrsType>(this);
113,662✔
1831
    }
1832

1833
    execute(this: Interpreter, statement: Stmt.Statement): BrsType {
1834
        this.location = statement.location;
36,939✔
1835
        this.reportCoverageHit(statement);
36,939✔
1836

1837
        return statement.accept<BrsType>(this);
36,939✔
1838
    }
1839

1840
    /**
1841
     * Returns the Backtrace formatted as an array for Try/Catch
1842
     * @param loc the location of the error
1843
     * @param bt the backtrace array, default is current stack trace
1844
     * @returns an array with the backtrace formatted
1845
     */
1846
    formatBacktrace(loc: Location, bt?: TracePoint[]): RoArray {
1847
        const backTrace = bt ?? this._stack;
8!
1848
        const btArray: BrsType[] = [];
8✔
1849
        for (let index = backTrace.length - 1; index >= 0; index--) {
8✔
1850
            const func = backTrace[index];
13✔
1851
            if (!func.signature) {
13!
1852
                continue;
×
1853
            }
1854
            const kind = ValueKind.toString(func.signature.returns);
13✔
1855
            let args = "";
13✔
1856
            func.signature.args.forEach((arg) => {
13✔
1857
                args += args !== "" ? "," : "";
5!
1858
                args += `${arg.name.text} As ${ValueKind.toString(arg.type.kind)}`;
5✔
1859
            });
1860
            const funcSig = `${func.functionName}(${args}) As ${kind}`;
13✔
1861
            const line = loc.start.line;
13✔
1862
            const info = { filename: loc?.file ?? "()", function: funcSig, line_number: line };
13!
1863
            btArray.unshift(toAssociativeArray(info));
13✔
1864
            loc = func.callLocation;
13✔
1865
        }
1866
        return new RoArray(btArray);
8✔
1867
    }
1868

1869
    /**
1870
     * Method to return the selected scope of the interpreter for the REPL and Micro Debugger
1871
     * @returns a string representation of the variables in the selected scope
1872
     */
1873
    formatVariables(scope: Scope = Scope.Function): string {
×
NEW
1874
        let vars = "";
×
NEW
1875
        if (scope === Scope.Function) {
×
NEW
1876
            vars += `${"global".padEnd(16)} Interface:ifGlobal\r\n`;
×
NEW
1877
            vars += `${"m".padEnd(16)} roAssociativeArray count:${
×
1878
                this.environment.getM().getElements().length
1879
            }\r\n`;
1880
        }
NEW
1881
        let fnc = this.environment.getList(scope);
×
1882
        fnc.forEach((value, key) => {
×
1883
            const varName = key.padEnd(17);
×
1884
            if (PrimitiveKinds.has(value.kind)) {
×
1885
                let text = value.toString();
×
1886
                let lf = text.length <= 94 ? "\r\n" : "...\r\n";
×
1887
                if (value.kind === ValueKind.String) {
×
1888
                    text = `"${text.substring(0, 94)}"`;
×
1889
                }
NEW
1890
                vars += `${varName}${ValueKind.toString(value.kind)} val:${text}${lf}`;
×
1891
            } else if (isIterable(value)) {
×
1892
                const count = value.getElements().length;
×
NEW
1893
                vars += `${varName}${value.getComponentName()} count:${count}\r\n`;
×
1894
            } else if (value instanceof BrsComponent && isUnboxable(value)) {
×
1895
                const unboxed = value.unbox();
×
NEW
1896
                vars += `${varName}${value.getComponentName()} val:${unboxed.toString()}\r\n`;
×
1897
            } else if (value.kind === ValueKind.Object) {
×
NEW
1898
                vars += `${varName}${value.getComponentName()}\r\n`;
×
1899
            } else if (value.kind === ValueKind.Callable) {
×
NEW
1900
                vars += `${varName}${ValueKind.toString(value.kind)} val:${value.getName()}\r\n`;
×
1901
            } else {
NEW
1902
                vars += `${varName}${value.toString().substring(0, 94)}\r\n`;
×
1903
            }
1904
        });
NEW
1905
        return vars;
×
1906
    }
1907

1908
    /** Method to return a string with the current source code location
1909
     * @returns a string representation of the location
1910
     */
1911
    formatLocation(location: Location = this.location) {
8✔
1912
        let formattedLocation: string;
1913
        if (location.start.line) {
8!
1914
            formattedLocation = `pkg:/${location.file}(${location.start.line})`;
8✔
1915
        } else {
1916
            formattedLocation = `pkg:/${location.file}(??)`;
×
1917
        }
1918
        return formattedLocation;
8✔
1919
    }
1920

1921
    /**
1922
     * Emits an error via this processor's `events` property, then throws it.
1923
     * @param err the ParseError to emit then throw
1924
     */
1925
    public addError(err: BrsError): never {
1926
        if (!err.backTrace) {
121✔
1927
            err.backTrace = this._stack.slice();
121✔
1928
        }
1929
        if (!this._tryMode) {
121✔
1930
            // do not save/emit the error if we are in a try block
1931
            this.errors.push(err);
115✔
1932
            this.events.emit("err", err);
115✔
1933
        }
1934
        throw err;
121✔
1935
    }
1936

1937
    /**
1938
     * Method to evaluate if a number is positive
1939
     * @param value number to evaluate
1940
     * @returns boolean indicating if the number is positive
1941
     */
1942
    private isPositive(value: number | Long): boolean {
1943
        if (value instanceof Long) {
371✔
1944
            return value.isPositive();
11✔
1945
        }
1946
        return value >= 0;
360✔
1947
    }
1948

1949
    /**
1950
     * Method to evaluate if a number is lees than the other
1951
     * @param value Number to evaluate
1952
     * @param compare Number to compare
1953
     * @returns Boolean indicating if the number is less than the other
1954
     */
1955
    private lessThan(value: number | Long, compare: number): boolean {
1956
        if (value instanceof Long) {
18!
1957
            return value.lessThan(compare);
×
1958
        }
1959
        return value < compare;
18✔
1960
    }
1961
}
1962

1963
/**
1964
 * Colorizes the console messages.
1965
 *
1966
 */
1967
export function colorize(log: string) {
135✔
1968
    return log
1,736✔
1969
        .replace(/\b(down|error|errors|failure|fail|fatal|false)(:|\b)/gi, chalk.red("$1$2"))
1970
        .replace(/\b(warning|warn|test|null|undefined|invalid)(:|\b)/gi, chalk.yellow("$1$2"))
1971
        .replace(/\b(help|hint|info|information|true|log)(:|\b)/gi, chalk.cyan("$1$2"))
1972
        .replace(/\b(running|success|successfully|valid)(:|\b)/gi, chalk.green("$1$2"))
1973
        .replace(/\b(debug|roku|brs|brightscript)(:|\b)/gi, chalk.magenta("$1$2"))
1974
        .replace(/(\b\d+\.?\d*?\b)/g, chalk.ansi256(122)(`$1`)) // Numeric
1975
        .replace(/\S+@\S+\.\S+/g, (match: string) => {
1976
            return chalk.blueBright(stripAnsi(match)); // E-Mail
1✔
1977
        })
1978
        .replace(/\b([a-z]+):\/{1,2}[^\/].*/gi, (match: string) => {
1979
            return chalk.blue.underline(stripAnsi(match)); // URL
5✔
1980
        })
1981
        .replace(/<(.*?)>/g, (match: string) => {
1982
            return chalk.greenBright(stripAnsi(match)); // Delimiters < >
25✔
1983
        })
1984
        .replace(/"(.*?)"/g, (match: string) => {
1985
            return chalk.ansi256(222)(stripAnsi(match)); // Quotes
30✔
1986
        });
1987
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc