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

rokucommunity / brs / #144

22 Nov 2024 01:10AM UTC coverage: 89.216% (+0.06%) from 89.154%
#144

push

web-flow
Merge bafb5e85d into a699c56c7

2151 of 2605 branches covered (82.57%)

Branch coverage included in aggregate %.

24 of 24 new or added lines in 4 files covered. (100.0%)

5 existing lines in 3 files now uncovered.

6023 of 6557 relevant lines covered (91.86%)

28606.08 hits per line

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

62.27
/src/index.ts
1
import * as fs from "fs";
135✔
2
import * as readline from "readline";
135✔
3
import chalk from "chalk";
135✔
4

5
import { promisify } from "util";
135✔
6
const mkdtemp = promisify(fs.mkdtemp);
135✔
7
import decompress from "decompress";
135✔
8
import sanitizeFilename from "sanitize-filename";
135✔
9

10
import { Lexer } from "./lexer";
135✔
11
import * as PP from "./preprocessor";
135✔
12
import {
135✔
13
    getComponentDefinitionMap,
14
    ComponentDefinition,
15
    ComponentScript,
16
} from "./componentprocessor";
17
import { Parser } from "./parser";
135✔
18
import { Interpreter, ExecutionOptions, defaultExecutionOptions, colorize } from "./interpreter";
135✔
19
import { resetTestData } from "./extensions";
135✔
20
import * as BrsError from "./Error";
135✔
21
import * as LexerParser from "./LexerParser";
135✔
22
import { CoverageCollector } from "./coverage";
135✔
23
import { loadTranslationFiles } from "./stdlib";
135✔
24

25
import * as _lexer from "./lexer";
135✔
26
export { _lexer as lexer };
135✔
27
import * as BrsTypes from "./brsTypes";
135✔
28
export { BrsTypes as types };
135✔
29
export { PP as preprocessor };
135✔
30
import * as _parser from "./parser";
135✔
31
export { _parser as parser };
135✔
32
import { URL } from "url";
135✔
33
import * as path from "path";
135✔
34
import pSettle from "p-settle";
135✔
35
import os from "os";
135✔
36

37
let coverageCollector: CoverageCollector | null = null;
135✔
38

39
/**
40
 * Executes a BrightScript file by path and writes its output to the streams
41
 * provided in `options`.
42
 *
43
 * @param filename the absolute path to the `.brs` file to be executed
44
 * @param options configuration for the execution, including the streams to use for `stdout` and
45
 *                `stderr` and the base directory for path resolution
46
 *
47
 * @returns a `Promise` that will be resolve if `filename` is successfully
48
 *          executed, or be rejected if an error occurs.
49
 */
50
export async function execute(filenames: string[], options: Partial<ExecutionOptions>) {
135✔
51
    let { lexerParserFn, interpreter } = await loadFiles(options);
106✔
52
    let mainStatements = await lexerParserFn(filenames);
106✔
53
    return interpreter.exec(mainStatements);
105✔
54
}
55

56
async function loadFiles(options: Partial<ExecutionOptions>) {
57
    const executionOptions = { ...defaultExecutionOptions, ...options };
111✔
58

59
    let manifest = await PP.getManifest(executionOptions.root);
111✔
60
    let maybeLibraryName = options.isComponentLibrary
111✔
61
        ? manifest.get("sg_component_libs_provided")
62
        : undefined;
63
    if (typeof maybeLibraryName === "boolean") {
111!
64
        throw new Error(
×
65
            "Encountered invalid boolean value for manifest key 'sg_component_libs_provided'"
66
        );
67
    } else if (options.isComponentLibrary && maybeLibraryName == null) {
111!
68
        throw new Error(
×
69
            "Could not find required manifest key 'sg_component_libs_provided' in component library"
70
        );
71
    }
72
    let componentDefinitions = await getComponentDefinitionMap(
111✔
73
        executionOptions.root,
74
        executionOptions.componentDirs,
75
        maybeLibraryName
76
    );
77

78
    let componentLibraries = Array.from(componentDefinitions.values())
111✔
79
        .map((component: ComponentDefinition) => ({
1,755✔
80
            component,
81
            libraries: component.children.filter(
82
                (child) => child.name.toLowerCase() === "componentlibrary"
931✔
83
            ),
84
        }))
85
        .filter(({ libraries }) => libraries && libraries.length > 0);
1,755✔
86

87
    let knownComponentLibraries = new Map<string, string>();
111✔
88
    let componentLibrariesToLoad: ReturnType<typeof loadFiles>[] = [];
111✔
89
    for (let { component, libraries } of componentLibraries) {
111✔
90
        for (let cl of libraries) {
1✔
91
            let uri = cl.fields.uri;
2✔
92
            if (!uri) {
2!
93
                continue;
×
94
            }
95
            if (uri.startsWith("http://") || uri.startsWith("https://")) {
2✔
96
                executionOptions.stderr.write(
1✔
97
                    `WARNING: Only pkg:/-local component libraries are supported; ignoring '${uri}'\n`
98
                );
99
                continue;
1✔
100
            }
101
            let posixRoot = executionOptions.root.replace(/[\/\\]+/g, path.posix.sep);
1✔
102
            let posixPath = component.xmlPath.replace(/[\/\\]+/g, path.posix.sep);
1✔
103
            if (process.platform === "win32") {
1!
UNCOV
104
                posixRoot = posixRoot.replace(/^[a-zA-Z]:/, "");
×
UNCOV
105
                posixPath = posixPath.replace(/^[a-zA-Z]:/, "");
×
106
            }
107

108
            let packageUri = new URL(
1✔
109
                uri,
110
                `pkg:/${path.posix.relative(posixRoot, posixPath)}`
111
            ).toString();
112
            if (knownComponentLibraries.has(packageUri)) {
1!
113
                continue;
×
114
            }
115

116
            let sanitizedUri = sanitizeFilename(packageUri);
1✔
117
            let tempdir = await mkdtemp(path.join(os.tmpdir(), `brs-${sanitizedUri}`), "utf8");
1✔
118
            knownComponentLibraries.set(packageUri, tempdir);
1✔
119

120
            let zipFileOnDisk = path.join(executionOptions.root, new URL(uri).pathname);
1✔
121

122
            componentLibrariesToLoad.push(
1✔
123
                decompress(zipFileOnDisk, tempdir).then(() =>
124
                    loadFiles({
1✔
125
                        ...options,
126
                        root: tempdir,
127
                        componentDirs: [],
128
                        isComponentLibrary: true,
129
                    })
130
                )
131
            );
132
        }
133
    }
134

135
    componentDefinitions.forEach((component: ComponentDefinition) => {
111✔
136
        if (component.scripts.length < 1) return;
1,755✔
137
        try {
1,291✔
138
            component.scripts = component.scripts.map((script: ComponentScript) => {
1,291✔
139
                script.uri = path.join(executionOptions.root, new URL(script.uri).pathname);
1,699✔
140
                return script;
1,699✔
141
            });
142
        } catch (error) {
143
            throw new Error(
×
144
                `Encountered an error when parsing component ${component.name}: ${error}`
145
            );
146
        }
147
    });
148

149
    let lexerParserFn = LexerParser.getLexerParserFn(manifest, options);
111✔
150
    const interpreter = await Interpreter.withSubEnvsFromComponents(
111✔
151
        componentDefinitions,
152
        lexerParserFn,
153
        executionOptions
154
    );
155
    if (!interpreter) {
111!
156
        throw new Error("Unable to build interpreter.");
×
157
    }
158

159
    // Store manifest as a property on the Interpreter for further reusing
160
    interpreter.manifest = manifest;
111✔
161

162
    let componentLibraryInterpreters = (await pSettle(componentLibrariesToLoad))
111✔
163
        .filter((result) => result.isFulfilled)
1✔
164
        .map((result) => result.value!.interpreter);
1✔
165
    interpreter.mergeNodeDefinitionsWith(componentLibraryInterpreters);
111✔
166

167
    await loadTranslationFiles(interpreter, executionOptions.root);
111✔
168

169
    if (executionOptions.generateCoverage) {
111✔
170
        coverageCollector = new CoverageCollector(executionOptions.root, lexerParserFn);
1✔
171
        await coverageCollector.crawlBrsFiles();
1✔
172
        interpreter.setCoverageCollector(coverageCollector);
1✔
173
    }
174
    return { lexerParserFn, interpreter };
111✔
175
}
176

177
/**
178
 * Returns a summary of the code coverage.
179
 */
180
export function getCoverageResults() {
135✔
181
    if (!coverageCollector) return;
1!
182
    return coverageCollector.getCoverage();
1✔
183
}
184

185
/**
186
 * A synchronous version of the lexer-parser flow.
187
 *
188
 * @param filename the paths to BrightScript files to lex and parse synchronously
189
 * @param options configuration for the execution, including the streams to use for `stdout` and
190
 *                `stderr` and the base directory for path resolution
191
 *
192
 * @returns the AST produced from lexing and parsing the provided files
193
 */
194
export function lexParseSync(filenames: string[], options: Partial<ExecutionOptions>) {
135✔
195
    const executionOptions = { ...defaultExecutionOptions, ...options };
23✔
196

197
    let manifest = PP.getManifestSync(executionOptions.root);
23✔
198

199
    return filenames
23✔
200
        .map((filename) => {
201
            let lexer = new Lexer();
22✔
202
            let preprocessor = new PP.Preprocessor();
22✔
203
            let parser = new Parser();
22✔
204
            [lexer, preprocessor, parser].forEach((emitter) =>
22✔
205
                emitter.onError(BrsError.getLoggerUsing(executionOptions.stderr))
66✔
206
            );
207

208
            let contents = fs.readFileSync(filename, "utf8");
22✔
209
            let scanResults = lexer.scan(contents, filename);
22✔
210
            let preprocessorResults = preprocessor.preprocess(scanResults.tokens, manifest);
22✔
211
            let parseResults = parser.parse(preprocessorResults.processedTokens);
22✔
212

213
            if (parseResults.errors.length > 0) {
22✔
214
                throw "Error occurred parsing";
4✔
215
            }
216

217
            return parseResults.statements;
18✔
218
        })
219
        .reduce((allStatements, statements) => [...allStatements, ...statements], []);
18✔
220
}
221

222
export type ExecuteWithScope = (filenames: string[], args: BrsTypes.BrsType[]) => BrsTypes.BrsType;
223
/**
224
 * Runs a set of files to create an execution scope, and generates an execution function that runs in that scope.
225
 *
226
 * @param filenamesForScope List of filenames to put into the execution scope
227
 * @param options Execution options
228
 *
229
 * @returns A function to execute files using the created scope.
230
 */
231
export async function createExecuteWithScope(
135✔
232
    filenamesForScope: string[],
233
    options: Partial<ExecutionOptions>
234
): Promise<ExecuteWithScope> {
235
    let { lexerParserFn, interpreter } = await loadFiles(options);
4✔
236
    let mainStatements = await lexerParserFn(filenamesForScope);
4✔
237
    interpreter.exec(mainStatements);
4✔
238
    // Clear any errors that accumulated, so that we can isolate errors from future calls to the execute function.
239
    interpreter.errors = [];
4✔
240

241
    return (filenames: string[], args: BrsTypes.BrsType[]) => {
4✔
242
        // Reset any mocks so that subsequent executions don't interfere with each other.
243
        interpreter.environment.resetMocks();
7✔
244
        resetTestData();
7✔
245

246
        let ast = lexParseSync(filenames, interpreter.options);
7✔
247
        let execErrors: BrsError.BrsError[] = [];
7✔
248
        let returnValue = interpreter.inSubEnv((subInterpreter) => {
7✔
249
            let value = subInterpreter.exec(ast, ...args)[0] || BrsTypes.BrsInvalid.Instance;
7!
250
            execErrors = subInterpreter.errors;
7✔
251
            subInterpreter.errors = [];
7✔
252
            return value;
7✔
253
        });
254

255
        // Re-throw any errors the interpreter encounters. We can't throw them directly from the `inSubEnv` call,
256
        // because they get caught by upstream handlers.
257
        if (execErrors.length) {
7✔
258
            throw execErrors;
2✔
259
        }
260

261
        return returnValue;
5✔
262
    };
263
}
264

265
/**
266
 * Launches an interactive read-execute-print loop, which reads input from
267
 * `stdin` and executes it.
268
 *
269
 * **NOTE:** Currently limited to single-line inputs :(
270
 */
271
export function repl() {
135✔
272
    const replInterpreter = new Interpreter();
×
273
    replInterpreter.onError(BrsError.getLoggerUsing(process.stderr));
×
274

275
    const rl = readline.createInterface({
×
276
        input: process.stdin,
277
        output: process.stdout,
278
    });
279
    rl.setPrompt(`${chalk.magenta("brs")}> `);
×
280
    rl.on("line", (line) => {
×
281
        const cmd = line.trim().toLowerCase();
×
282
        if (["quit", "exit", "q"].includes(cmd)) {
×
283
            process.exit();
×
284
        } else if (["cls", "clear"].includes(cmd)) {
×
285
            process.stdout.write("\x1Bc");
×
286
            rl.prompt();
×
287
            return;
×
288
        } else if (["help", "hint"].includes(cmd)) {
×
289
            printHelp();
×
290
            rl.prompt();
×
291
            return;
×
292
        } else if (["vars", "var"].includes(cmd)) {
×
293
            console.log(chalk.cyanBright(`\r\nLocal variables:\r\n`));
×
294
            console.log(chalk.cyanBright(replInterpreter.formatLocalVariables()));
×
295
            rl.prompt();
×
296
            return;
×
297
        }
298
        let results = run(line, defaultExecutionOptions, replInterpreter);
×
299
        if (results) {
×
300
            results.map((result) => {
×
301
                if (result !== BrsTypes.BrsInvalid.Instance) {
×
302
                    console.log(colorize(result.toString()));
×
303
                }
304
            });
305
        }
306
        rl.prompt();
×
307
    });
308

309
    console.log(colorize("type `help` to see the list of valid REPL commands.\r\n"));
×
310
    rl.prompt();
×
311
}
312

313
/**
314
 * Runs an arbitrary string of BrightScript code.
315
 * @param contents the BrightScript code to lex, parse, and interpret.
316
 * @param options the streams to use for `stdout` and `stderr`. Mostly used for
317
 *                testing.
318
 * @param interpreter an interpreter to use when executing `contents`. Required
319
 *                    for `repl` to have persistent state between user inputs.
320
 * @returns an array of statement execution results, indicating why each
321
 *          statement exited and what its return value was, or `undefined` if
322
 *          `interpreter` threw an Error.
323
 */
324
function run(
325
    contents: string,
326
    options: ExecutionOptions = defaultExecutionOptions,
×
327
    interpreter: Interpreter
328
) {
329
    const lexer = new Lexer();
×
330
    const parser = new Parser();
×
331
    const logErrorFn = BrsError.getLoggerUsing(options.stderr);
×
332

333
    lexer.onError(logErrorFn);
×
334
    parser.onError(logErrorFn);
×
335

336
    const scanResults = lexer.scan(contents, "REPL");
×
337
    if (scanResults.errors.length > 0) {
×
338
        return;
×
339
    }
340

341
    const parseResults = parser.parse(scanResults.tokens);
×
342
    if (parseResults.errors.length > 0) {
×
343
        return;
×
344
    }
345

346
    if (parseResults.statements.length === 0) {
×
347
        return;
×
348
    }
349

350
    try {
×
351
        return interpreter.exec(parseResults.statements);
×
352
    } catch (e) {
353
        //options.stderr.write(e.message);
354
        return;
×
355
    }
356
}
357

358
/**
359
 * Display the help message on the console.
360
 */
361
function printHelp() {
362
    let helpMsg = "\r\n";
×
363
    helpMsg += "REPL Command List:\r\n";
×
364
    helpMsg += "   print|?         Print variable value or expression\r\n";
×
365
    helpMsg += "   var|vars        Display variables and their types/values\r\n";
×
366
    helpMsg += "   help|hint       Show this REPL command list\r\n";
×
367
    helpMsg += "   clear|cls       Clear terminal screen\r\n";
×
368
    helpMsg += "   exit|quit|q     Terminate REPL session\r\n\r\n";
×
369
    helpMsg += "   Type any valid BrightScript expression for a live compile and run.\r\n";
×
370
    console.log(chalk.cyanBright(helpMsg));
×
371
}
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