@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
246 lines • 12.6 kB
JavaScript
;
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