UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

451 lines 21.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.describeCompletion = describeCompletion; exports.replCompleter = replCompleter; exports.completionSuggestion = completionSuggestion; exports.makeDefaultReplReadline = makeDefaultReplReadline; exports.handleString = handleString; exports.toFileStatement = toFileStatement; exports.extractWatchPath = extractWatchPath; exports.stopWatching = stopWatching; exports.startWatching = startWatching; exports.replProcessAnswer = replProcessAnswer; exports.repl = repl; exports.loadReplHistory = loadReplHistory; /** * Basically a helper file to allow the main 'flowr' script (located in the source root) to provide its repl * @module */ const prompt_1 = require("./prompt"); const path_input_1 = require("./path-input"); const readline = __importStar(require("readline")); const ghost_hint_1 = require("./ghost-hint"); const repl_execute_1 = require("./commands/repl-execute"); const os_1 = __importDefault(require("os")); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const args_1 = require("../../util/text/args"); const ansi_1 = require("../../util/text/ansi"); const repl_commands_1 = require("./commands/repl-commands"); const scripts_info_1 = require("../common/scripts-info"); const retriever_1 = require("../../r-bridge/retriever"); const repl_main_1 = require("./commands/repl-main"); const log_1 = require("../../util/log"); const query_1 = require("../../queries/query"); const repl_signature_1 = require("./commands/repl-signature"); const strings_1 = require("../../util/text/strings"); const instrument_dataflow_count_1 = require("../../dataflow/instrument/instrument-dataflow-count"); const proc_1 = require("../../util/proc"); let _replCompleterKeywords = undefined; function replCompleterKeywords() { if (_replCompleterKeywords === undefined) { _replCompleterKeywords = Array.from((0, repl_commands_1.getCommandNames)(), s => `:${s}`); } return _replCompleterKeywords; } const defaultHistoryFile = path_1.default.join(os_1.default.tmpdir(), '.flowrhistory'); /** Labels a completion with what it does, see {@link CommandCompletions#labels|labels}. */ function describeCompletion(insert, describe) { return `${insert} ${ansi_1.ansiFormatter.format(describe, { style: 2 /* FontStyles.Faint */ })}`; } function computeCompletions(line, config) { const splitLine = (0, args_1.splitAtEscapeSensitive)(line); // did we just type a space (and are starting a new arg right now)? const startingNewArg = line.endsWith(' '); // if we typed a command fully already, autocomplete the arguments if (splitLine.length > 1 || startingNewArg) { const commandNameColon = replCompleterKeywords().find(k => splitLine[0] === k); if (commandNameColon) { let completions = []; let currentArg = startingNewArg ? '' : splitLine[splitLine.length - 1]; let labels; let hints; let preFiltered = false; const commandName = commandNameColon.slice(1); const cmd = (0, repl_commands_1.getCommand)(commandName); if (cmd?.script === true) { // autocomplete script arguments const options = scripts_info_1.scripts[commandName].options; completions = completions.concat((0, scripts_info_1.getValidOptionsForCompletion)(options, splitLine).map(o => `${o} `)); } else if (commandName.startsWith('query')) { const { completions: queryCompletions, argumentPart: splitArg, labels: queryLabels, hints: queryHints, preFiltered: queryPreFiltered } = replQueryCompleter(splitLine, startingNewArg, config); if (splitArg !== undefined) { currentArg = splitArg; } completions = completions.concat(queryCompletions); labels = queryLabels; hints = queryHints; preFiltered = queryPreFiltered ?? false; } else if (cmd === repl_signature_1.signatureCommand) { const { completions: sigCompletions, argumentPart: splitArg } = (0, repl_signature_1.replSignatureCompleter)(splitLine, startingNewArg); if (splitArg !== undefined) { currentArg = splitArg; } completions = completions.concat(sigCompletions); } else if (cmd?.isCodeCommand) { completions.push(retriever_1.fileProtocol); completions.push(path_input_1.watchProtocol); } // prefix matches when any exist, else fuzzy (subsequence) matches, so e.g. `+sg` still completes to `+specializeConfig` return [preFiltered ? completions : (0, strings_1.matchByPrefixOrSubsequence)(completions, currentArg), currentArg, labels, hints?.filter(h => h.startsWith(currentArg))]; } } // if no command is already typed, just return all commands that match return [replCompleterKeywords().filter(k => k.startsWith(line)).map(k => `${k} `), line]; } /** * Used by the repl to provide automatic completions for a given (partial) input line. Returns every matching * option, so Tab shows the full menu and only completes when a single option remains (standard readline behavior). */ function replCompleter(line, config) { const [completions, fragment, labels] = computeCompletions(line, config); if (labels === undefined || completions.length <= 1) { return [completions, fragment]; } return [completions.map(c => labels.get(c) ?? c), fragment]; } /** * The remaining text of the best (first) completion, i.e. what the inline ghost previews as you type; empty when * there is nothing to suggest. Independent of {@link replCompleter}: the ghost hints the best match even when Tab * would still offer several. */ function completionSuggestion(line, config) { if (line.length === 0) { return ''; } const [completions, fragment, , hints] = computeCompletions(line, config); // prefer an insertable completion; otherwise preview a display-only hint (e.g. a `<string>` type placeholder) const best = completions[0] ?? hints?.[0]; if (best === undefined || !best.startsWith(fragment) || best.length <= fragment.length) { return ''; } else if (best === retriever_1.fileProtocol || best === path_input_1.watchProtocol) { return ''; } return best.slice(fragment.length); } function replQueryCompleter(splitLine, startingNewArg, config) { const nonEmpty = splitLine.slice(1).map(s => s.trim()).filter(s => s.length > 0); /* `?<type>` documents a query instead of running it, and completes to the same names */ const asking = !startingNewArg && nonEmpty.length === 1 && nonEmpty[0].startsWith('?'); const queryShorts = asking ? Object.keys(query_1.SupportedQueries).map(q => `?${q}`) : ['help'].concat(Object.keys(query_1.SupportedQueries).map(q => `@${q}`)); if (asking || nonEmpty.length === 0 || (nonEmpty.length === 1 && queryShorts.some(q => q.startsWith(nonEmpty[0]) && nonEmpty[0] !== q && !startingNewArg))) { return { completions: queryShorts.map(q => `${q} `) }; } else { const q = nonEmpty[0].slice(1); const queryElement = query_1.SupportedQueries[q]; if (queryElement?.completer) { return queryElement.completer(nonEmpty.slice(1), startingNewArg, config); } } return { completions: [] }; } /** * Produces default readline options for the flowR REPL */ function makeDefaultReplReadline(config, historyFile = defaultHistoryFile) { return { input: process.stdin, output: process.stdout, tabSize: 4, terminal: true, history: historyFile ? loadReplHistory(historyFile) : [], removeHistoryDuplicates: true, completer: (c) => replCompleter(c, config) }; } /** * Handles a string input for the REPL, returning the parsed string and any remaining input. */ function handleString(code) { return { rCode: code.length === 0 ? undefined : (0, strings_1.startAndEndsWith)(code, '"') ? JSON.parse(code) : code, remaining: [] }; } /** Convert a statement containing {@link watchProtocol} into one using {@link fileProtocol} so it can be executed. */ function toFileStatement(statement) { return statement.replaceAll(path_input_1.watchProtocol, retriever_1.fileProtocol); } /** Return the watch path from the first `watch://<path>` token in `statement`, or `undefined` if none is present. */ function extractWatchPath(statement) { return /watch:\/\/(\S+)/.exec(statement)?.[1]; } let activeWatcher; /** Stop the active watcher. Pass `notify = false` when silently switching to a new watch target. */ function stopWatching(output, notify = true) { if (activeWatcher !== undefined) { try { activeWatcher.close(); } catch { /* already closed */ } activeWatcher = undefined; if (notify) { output.stdout('Watch mode stopped.'); } } } /** `watchImpl` is injectable so tests can pass a fake `fs.watch`. */ function startWatching(watchPath, output, onFire, watchImpl = fs_1.default.watch) { const faint = (s) => output.formatter.format(s, { style: 2 /* FontStyles.Faint */ }); const isDir = fs_1.default.existsSync(watchPath) && fs_1.default.statSync(watchPath).isDirectory(); let debounce; const watcher = watchImpl(watchPath, { recursive: isDir }, () => { clearTimeout(debounce); debounce = setTimeout(() => { output.stdout(faint(`\nChange detected in '${watchPath}', re-running...`)); onFire(); }, 50); }); output.stdout(faint(`Watching ${isDir ? 'folder' : 'file'} '${watchPath}'. Press Ctrl+C or enter another command to stop.`)); return watcher; } async function executeStatement(output, statement, analyzer, allowRSessionAccess) { const time = Date.now(); const heatMap = new Map(); if (analyzer.inspectContext().config.repl.dfProcessorHeat) { analyzer.context().config.solver.instrument.dataflowExtractors = (0, instrument_dataflow_count_1.instrumentDataflowCount)(heatMap, map => map.clear()); } if (statement.startsWith(':')) { const command = statement.slice(1).split(' ')[0].toLowerCase(); const bold = (s) => output.formatter.format(s, { style: 1 /* FontStyles.Bold */ }); const reportFailure = (e) => { output.stderr(`${bold(`Failed to execute command ${command}`)}: ${e?.message}. Using the ${bold('--verbose')} flag on startup may provide additional information.\n`); if (log_1.log.settings.minLevel < 6 /* LogLevel.Fatal */) { console.error(e); } }; try { const processor = (0, repl_commands_1.getCommand)(command); if (processor) { await (0, query_1.genericWrapReplFailIfNoRequest)(async () => { const remainingLine = statement.slice(command.length + 2).trim(); if (processor.isCodeCommand) { const args = processor.argsParser(remainingLine); if (args.rCode) { const rawPath = args.rCode.startsWith(retriever_1.fileProtocol) ? args.rCode.slice(retriever_1.fileProtocol.length) : undefined; const alreadyKnown = rawPath !== undefined && analyzer.context().files.getFileByPath(rawPath) !== undefined; if (!alreadyKnown) { analyzer.reset(); analyzer.addRequest((0, path_input_1.handlePathLikeInput)(output, args.rCode, analyzer.flowrConfig)); } } await processor.fn({ output, analyzer, remainingArgs: args.remaining }); } else { await processor.fn({ output, analyzer, remainingLine, allowRSessionAccess }); } }, output, analyzer); } else { output.stderr(`the command '${command}' is unknown, try ${bold(':help')} for more information\n`); } } catch (e) { reportFailure(e); } } else { await (0, repl_execute_1.tryExecuteRShellCommand)({ output, analyzer, remainingLine: statement, allowRSessionAccess }); } if (analyzer.inspectContext().config.repl.quickStats) { try { const duration = Date.now() - time; console.log(output.formatter.format(`[REPL Stats] Processed in ${duration}ms`, { style: 3 /* FontStyles.Italic */, effect: ansi_1.ColorEffect.Foreground, color: 7 /* Colors.White */ })); const memoryUsage = process.memoryUsage(); const memoryUsageStr = Object.entries(memoryUsage).map(([key, value]) => `${key}: ${(value / 1024 / 1024).toFixed(2)} MB`).join(', '); console.log(output.formatter.format(`[REPL Stats] Memory Usage: ${memoryUsageStr}`, { style: 3 /* FontStyles.Italic */, effect: ansi_1.ColorEffect.Foreground, color: 7 /* Colors.White */ })); } catch { // do nothing, this is just a nice-to-have } } if (heatMap.size > 0 && analyzer.inspectContext().config.repl.dfProcessorHeat) { const sorted = Array.from(heatMap.entries()).sort((a, b) => b[1] - a[1]); console.log(output.formatter.format('[REPL Stats] Dataflow Processor Heatmap:', { style: 3 /* FontStyles.Italic */, effect: ansi_1.ColorEffect.Foreground, color: 7 /* Colors.White */ })); const longestKey = Math.max(...Array.from(heatMap.keys(), k => k.length)); const longestValue = Math.max(...Array.from(heatMap.values(), v => v.toString().length)); for (const [rType, count] of sorted) { console.log(output.formatter.format(` - ${(rType + ':').padEnd(longestKey + 1, ' ')} ${count.toString().padStart(longestValue, ' ')}`, { style: 3 /* FontStyles.Italic */, effect: ansi_1.ColorEffect.Foreground, color: 7 /* Colors.White */ })); } } } async function replProcessStatement(output, statement, analyzer, allowRSessionAccess) { stopWatching(output, !statement.includes(path_input_1.watchProtocol)); if (statement.includes(path_input_1.watchProtocol)) { const watchPath = extractWatchPath(statement); const effective = toFileStatement(statement); await executeStatement(output, effective, analyzer, allowRSessionAccess); if (watchPath) { try { activeWatcher = startWatching(watchPath, output, () => { analyzer.context().files.getFileByPath(watchPath)?.invalidate(); void executeStatement(output, effective, analyzer, allowRSessionAccess); }); activeWatcher.unref(); activeWatcher.on('error', (err) => { output.stderr(`Watch error for '${watchPath}': ${err.message}`); stopWatching(output, false); }); } catch (e) { output.stderr(`Cannot watch '${watchPath}': ${e.message}`); } } return; } await executeStatement(output, statement, analyzer, allowRSessionAccess); } /** * This function interprets the given `expr` as a REPL command (see {@link repl} for more on the semantics). * @param analyzer - The flowR analyzer to use. * @param output - Defines two methods that every function in the repl uses to output its data. * @param expr - The expression to process. * @param allowRSessionAccess - If true, allows the execution of arbitrary R code. */ async function replProcessAnswer(analyzer, output, expr, allowRSessionAccess) { const statements = (0, args_1.splitAtEscapeSensitive)(expr, false, /^;\s*:/); for (const statement of statements) { await replProcessStatement(output, statement.trim(), analyzer, allowRSessionAccess); } } /** * Provides a never-ending repl (read-evaluate-print loop) processor that can be used to interact with a {@link RShell} as well as all flowR scripts. * * The repl allows for two kinds of inputs: * - Starting with a colon `:`, indicating a command (probe `:help`, and refer to {@link commands}) </li> * - Starting with anything else, indicating default R code to be directly executed. If you kill the underlying shell, that is on you! </li> * @param options - The options for the repl. See {@link FlowrReplOptions} for more information. * * For the execution, this function makes use of {@link replProcessAnswer}. */ async function repl({ analyzer, historyFile = defaultHistoryFile, rl = readline.createInterface(makeDefaultReplReadline(analyzer.flowrConfig, historyFile)), output = repl_main_1.standardReplOutput, allowRSessionAccess = false }) { if (historyFile) { rl.on('history', h => fs_1.default.writeFileSync(historyFile, h.join('\n'), { encoding: 'utf-8' })); } const ghostHint = (0, ghost_hint_1.installGhostHint)(rl, analyzer.flowrConfig, { suggest: line => completionSuggestion(line, analyzer.flowrConfig) }); let sigintCount = 0; rl.on('SIGINT', () => { process.stdout.write('\n'); if (activeWatcher !== undefined) { stopWatching(output); rl.setPrompt((0, prompt_1.prompt)()); sigintCount = 0; } else { sigintCount++; if (sigintCount >= 2) { stopWatching(output, false); (0, proc_1.exitSafe)(0); } else { output.stdout('(Press Ctrl+C again or type :quit to exit)'); setTimeout(() => { sigintCount = 0; }, 2000).unref(); } } rl.prompt(); }); rl.on('close', () => { stopWatching(output, false); (0, proc_1.exitSafe)(0); }); // the incredible repl :D, we kill it with ':quit' // noinspection InfiniteLoopJS while (true) { await new Promise((resolve, reject) => { rl.question(activeWatcher !== undefined ? '' : (0, prompt_1.prompt)(), answer => { ghostHint.clear(); sigintCount = 0; rl.pause(); replProcessAnswer(analyzer, output, answer, allowRSessionAccess).then(() => { rl.resume(); resolve(); }).catch(reject); }); if (activeWatcher === undefined) { ghostHint.show(); } }); } } /** * Loads the REPL history from the given file. */ function loadReplHistory(historyFile) { try { if (!fs_1.default.existsSync(historyFile)) { return undefined; } return fs_1.default.readFileSync(historyFile, { encoding: 'utf-8' }).split('\n'); } catch (e) { log_1.log.error(`Failed to load repl history from ${historyFile}: ${e?.message}`); log_1.log.error(e?.stack); return undefined; } } //# sourceMappingURL=core.js.map