UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

246 lines 12.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.queryStarCommand = exports.queryCommand = void 0; exports.validateQueries = validateQueries; const ansi_1 = require("../../../util/text/ansi"); const query_1 = require("../../../queries/query"); const virtual_queries_1 = require("../../../queries/virtual-query/virtual-queries"); const query_print_1 = require("../../../queries/query-print"); const json_1 = require("../../../util/json"); const args_1 = require("../../../util/text/args"); const record_1 = require("../../../util/record"); const retriever_1 = require("../../../r-bridge/retriever"); const path_input_1 = require("../path-input"); /** * Whether the analyzer already holds exactly `input` as its sole request, so a prior analysis (e.g. a * dataflow computed via `:df#`) can be reused instead of being discarded by a reset. */ function analyzerHasTarget(analyzer, input) { const requested = (0, retriever_1.requestFromInput)(input); const current = analyzer.inspectContext().files.loadingOrder.getUnorderedRequests(); return current.length === 1 && current[0].request === requested.request && current[0].content === requested.content; } function printHelp(output) { output.stderr(`Format: ${(0, ansi_1.italic)(':query <query> <code>', output.formatter)}`); output.stdout('Queries starting with \'@<type>\' are interpreted as a query of the given type.'); output.stdout(`Start with '?<type>' instead to see documentation on a query, e.g. ${(0, ansi_1.bold)(':query ?guess-dep-versions', output.formatter)} (a bare ${(0, ansi_1.bold)(':query ?', output.formatter)} lists them all).`); output.stdout(`With this, ${(0, ansi_1.bold)(':query @config', output.formatter)} prints the result of the config query.`); output.stdout(`If you want to run the linter on a project use:\n ${(0, ansi_1.bold)(':query @linter file://<path>', output.formatter)} (or ${(0, ansi_1.bold)('watch://<path>', output.formatter)} to re-run on changes).`); output.stdout((0, ansi_1.ansiInfo)('Otherwise, you can also directly pass the query json. Then, the query is an array of query objects to represent multiple queries.')); output.stdout((0, ansi_1.ansiInfo)('The example') + (0, ansi_1.italic)(String.raw `:query "[{\"type\": \"call-context\", \"callName\": \"mean\" }]" mean(1:10)`, output.formatter, { color: 7 /* Colors.White */, effect: ansi_1.ColorEffect.Foreground }) + (0, ansi_1.ansiInfo)('would return the call context of the mean function.')); output.stdout('Please have a look at the wiki for more info: https://github.com/flowr-analysis/flowr/wiki/Query-API'); } /** the single boundary cast for Joi's untyped {@link Joi.Description}, shared by the doc and template renderers */ function describeSchema(schema) { return schema.describe(); } /** Print documentation for one query type from its Joi schema (description + each parameter), or list all when no name is given. */ function printQueryDoc(output, name) { if (name.length === 0) { output.stdout(`Queries: ${Object.keys(query_1.SupportedQueries).sort().map(q => (0, ansi_1.bold)('@' + q, output.formatter)).join(', ')}`); output.stdout(`Use ${(0, ansi_1.bold)(':query ?<type>', output.formatter)} for details on one, e.g. ${(0, ansi_1.bold)(':query ?guess-dep-versions', output.formatter)}.`); return; } const def = Object.entries(query_1.SupportedQueries).find(([key]) => key === name)?.[1]; if (def === undefined) { output.stderr(`Unknown query ${(0, ansi_1.italic)(name, output.formatter)}; use ${(0, ansi_1.bold)(':query ?', output.formatter)} to list every query.`); return; } const desc = describeSchema(def.schema); output.stdout(`${(0, ansi_1.bold)('@' + name, output.formatter)}${desc.flags?.description ? ` ${(0, ansi_1.faint)('— ' + desc.flags.description, output.formatter)}` : ''}`); const params = Object.entries(desc.keys ?? {}).filter(([key]) => key !== 'type'); if (params.length === 0) { output.stdout((0, ansi_1.faint)(' (no parameters)', output.formatter)); } for (const [key, spec] of params) { const presence = spec.flags?.presence === 'required' ? 'required' : 'optional'; const allowed = Array.isArray(spec.allow) && spec.allow.length > 0 ? ` {${spec.allow.join('|')}}` : ''; output.stdout(` ${(0, ansi_1.bold)(key, output.formatter)} ${(0, ansi_1.faint)(`(${spec.type}${allowed}, ${presence})`, output.formatter)}${spec.flags?.description ? ': ' + spec.flags.description : ''}`); } const syntax = 'syntax' in def && typeof def.syntax === 'string' ? def.syntax : `@${name} <code | file://path>`; output.stdout(`Run: ${(0, ansi_1.bold)(':query ' + syntax, output.formatter)}`); output.stdout(`JSON: ${(0, ansi_1.italic)(queryTemplate(name, def.schema), output.formatter)}`); const wiki = `https://github.com/flowr-analysis/flowr/wiki/${encodeURIComponent((0, query_1.queryWikiPage)(def.title).replaceAll(' ', '-'))}`; output.stdout(`Docs: ${output.formatter.hyperlink(`${name} query`, wiki)}`); } /** A copy-pasteable JSON template for a query type: its `type` plus each required field as a placeholder. */ function queryTemplate(type, schema) { const keys = describeSchema(schema).keys ?? {}; const fields = [`\\"type\\": \\"${type}\\"`]; for (const [key, value] of Object.entries(keys)) { if (key !== 'type' && value.flags?.presence === 'required') { fields.push(`\\"${key}\\": <${key}>`); } } return `:query "[{ ${fields.join(', ')} }]" <code | file://path>`; } /** Validates each query against its own type's schema and, on failure, prints a template for that type. */ function validateQueries(output, queries) { for (const q of queries) { const type = q?.type; if (typeof type !== 'string' || (!Object.hasOwn(query_1.SupportedQueries, type) && !Object.hasOwn(virtual_queries_1.SupportedVirtualQueries, type))) { output.stderr(`Unknown query type ${(0, ansi_1.italic)(JSON.stringify(type), output.formatter)}, use ${(0, ansi_1.bold)(':query help', output.formatter)} for the list of queries.`); return false; } const def = Object.hasOwn(query_1.SupportedQueries, type) ? query_1.SupportedQueries[type] : undefined; const { error } = (def?.schema ?? (0, query_1.VirtualQuerySchema)()).validate(q); if (error) { output.stderr(`Invalid ${(0, ansi_1.bold)('@' + type, output.formatter)} query:`); for (const detail of error.details) { output.stderr(` - ${detail.message}`); } if (def) { output.stderr(` Template: ${(0, ansi_1.italic)(queryTemplate(type, def.schema), output.formatter)}`); } return false; } } return true; } async function processQueryArgs(output, analyzer, remainingArgs) { const query = remainingArgs.shift(); if (!query) { output.stderr('No query provided, use \':query help\' to get more information.'); return; } if (query === 'help') { printHelp(output); return; } // `?<type>` (or `? <type>`) documents a query instead of running it; a bare `?` lists them all if (query.startsWith('?')) { printQueryDoc(output, query.slice(1) || (remainingArgs.shift() ?? '')); return; } let parsedQuery; let input; if (query.startsWith('@')) { const queryName = query.slice(1); const queryObj = query_1.SupportedQueries[queryName]; if (queryObj?.fromLine) { const parseResult = queryObj.fromLine(output, remainingArgs, analyzer.flowrConfig); const q = parseResult.query; parsedQuery = q ? (Array.isArray(q) ? q : [q]) : []; input = parseResult.rCode; } else { parsedQuery = [{ type: query.slice(1) }]; input = remainingArgs.join(' ').trim(); } if (!validateQueries(output, parsedQuery)) { return; } } else if (query.startsWith('[')) { parsedQuery = JSON.parse(query); if (!validateQueries(output, parsedQuery)) { return; } input = remainingArgs.join(' ').trim(); } else { parsedQuery = [{ type: 'call-context', callName: query }]; } if (input) { input = unquoteWhole(input); input = (0, path_input_1.handlePathLikeInput)(output, input, analyzer.flowrConfig); // reuse a prior analysis (e.g. a dataflow from :df#) when the target is unchanged if (!analyzerHasTarget(analyzer, input)) { analyzer.reset(); analyzer.addRequest(input); } } return { query: await (0, query_1.executeQueries)({ analyzer, }, parsedQuery), parsedQuery, analyzer }; } const ConfigLineRegex = /^(@config)(?:\s+([\s\S]*))?$/; /** * Function for splitting the input line. * All input is treated as arguments, no R code is separated so that the individual queries can handle it. * `@config` is passed its rest-of-line untouched, since its own `fromLine` reads a single raw token and the * generic tokenizer would otherwise strip quotes out of a `+path=["a"]` value. * @param line - The input line */ function parseArgs(line) { const configMatch = ConfigLineRegex.exec(line); if (configMatch) { const [, name, rest] = configMatch; return { rCode: undefined, remaining: rest === undefined ? [name] : [name, rest] }; } const [query, rest] = splitQueryFromRest(line); return { rCode: undefined, /* the rest keeps its quotes: they belong to the R code, where dropping them turns a string into a symbol */ remaining: [query, ...(0, args_1.splitAtEscapeSensitive)(rest, false)] }; } /** * Undoes a wrapping of the whole code in quotes, as `:query \@dependencies "library(x)"` writes it. A quote the * code itself contains ends the wrapping early, so it is left alone. */ function unquoteWhole(code) { const quote = code[0]; if ((quote !== '"' && quote !== '\'') || code.length < 2 || code[code.length - 1] !== quote) { return code; } for (let i = 1; i < code.length - 1; i++) { if (code[i] === quote && code[i - 1] !== '\\') { return code; } } return code.slice(1, -1); } /** * Separates the query from everything after it. The query is either a bare token (`@static-slice`) or the * quoted JSON form (`"[{\"type\": ...}]"`), whose escapes are undone; the rest is handed on verbatim. */ function splitQueryFromRest(line) { const trimmed = line.trimStart(); const quote = trimmed[0]; if (quote !== '"' && quote !== '\'') { const space = trimmed.indexOf(' '); return space < 0 ? [trimmed, ''] : [trimmed.slice(0, space), trimmed.slice(space + 1)]; } let end = 1; while (end < trimmed.length && !(trimmed[end] === quote && trimmed[end - 1] !== '\\')) { end++; } return [trimmed.slice(1, end).replace(/\\(.)/g, '$1'), trimmed.slice(end + 1).trimStart()]; } exports.queryCommand = { description: 'Query the given R code (use \'help\' for more information)', isCodeCommand: true, usageExample: ':query "<query>" <code>', aliases: [], script: false, argsParser: parseArgs, fn: async ({ output, analyzer, remainingArgs }) => { const totalStart = Date.now(); const results = await processQueryArgs(output, analyzer, remainingArgs); const totalEnd = Date.now(); if (results) { output.stdout(await (0, query_print_1.asciiSummaryOfQueryResult)(ansi_1.ansiFormatter, totalEnd - totalStart, results.query, results.analyzer, results.parsedQuery)); } } }; exports.queryStarCommand = { description: 'Similar to query, but returns the output in json format.', isCodeCommand: true, usageExample: ':query* <query> <code>', aliases: [], script: false, argsParser: parseArgs, fn: async ({ output, analyzer, remainingArgs }) => { const results = await processQueryArgs(output, analyzer, remainingArgs); if (results) { const json = record_1.Record.map(results.query, ([query, queryResults]) => [query, query_1.SupportedQueries[query]?.jsonFormatter?.(queryResults) ?? queryResults]); output.stdout(JSON.stringify(json, json_1.jsonReplacer)); } } }; //# sourceMappingURL=repl-query.js.map