@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
350 lines • 19.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigQueryDefinition = void 0;
exports.validateConfigUpdate = validateConfigUpdate;
const config_query_executor_1 = require("./config-query-executor");
const ansi_1 = require("../../../util/text/ansi");
const time_1 = require("../../../util/text/time");
const joi_1 = __importDefault(require("joi"));
const config_1 = require("../../../config");
const json_1 = require("../../../util/json");
const core_1 = require("../../../cli/repl/core");
const schema_1 = require("../../../util/schema");
const strings_1 = require("../../../util/text/strings");
function configReplCompleter(partialLine, startingNewArg, _config) {
// the config query is a single token
if (partialLine.length > 1 || (partialLine.length === 1 && startingNewArg)) {
return { completions: [] };
}
const token = partialLine[0] ?? '';
const update = token.startsWith('+');
const raw = update ? token.slice(1) : token;
const sigil = update ? '+' : '';
// on the empty token also offer `+` (writes a value) before the inspectable keys (which read); label it like the keys
const lead = token.length === 0 ? ['+'] : [];
const leadLabels = token.length === 0 ? [['+', (0, core_1.describeCompletion)('+', 'change config values')]] : [];
// once `+key=` is typed, complete the value (its type), not more keys
if (update && raw.includes('=')) {
const eq = raw.indexOf('=');
const keyPath = raw.slice(0, eq).split('.').filter(p => p.length > 0);
const valuePart = raw.slice(eq + 1);
const prefix = `+${raw.slice(0, eq)}=`;
const { concrete, placeholder } = splitValueHints(configSchemaInfo(keyPath));
const values = (0, strings_1.matchByPrefixOrSubsequence)(concrete, valuePart);
return {
completions: values.map(v => prefix + v),
labels: new Map(values.map(v => [prefix + v, v])),
hints: placeholder !== undefined && valuePart.length === 0 ? [prefix + placeholder] : undefined
};
}
// a glob reads several keys, so expand it to the paths it matches (the trailing segment completes as a prefix)
if (!update && raw.includes('*')) {
const matches = configGlobMatcher(raw.endsWith('*') ? raw : `${raw}*`);
const found = sortConfigPaths(configPaths().filter(matches)).map(p => p.join('.'));
return { completions: found, labels: new Map(found.map(p => [p, (0, core_1.describeCompletion)(p, configSchemaInfo(p.split('.')).type ?? '')])), preFiltered: true };
}
const path = raw.split('.').filter(p => p.length > 0);
const fullPath = path.slice();
const lastPath = token.endsWith('.') ? '' : path.pop() ?? '';
/* the schema knows every option, a value only those that are set; `reset` (top level, update only) is not a schema key but a fixed action */
const options = update && path.length === 0 ? [...configSchemaKeys(path), 'reset'] : configSchemaKeys(path);
const atLeaf = lastPath.length > 0 && options.includes(lastPath) && configSchemaInfo([...path, lastPath]).type !== 'object';
if (!atLeaf) {
const offered = (0, strings_1.matchByPrefixOrSubsequence)(options, lastPath);
const have = offered.map(k => `${sigil}${[...path, k].join('.')}`);
if (have.length > 0) {
// label starts with the full completion (incl. sigil) so readline keeps the sigil on common-prefix insertion
return { completions: [...lead, ...have], labels: new Map([...leadLabels, ...have.map((c, i) => [c, offered[i] === 'reset' && path.length === 0 ? (0, core_1.describeCompletion)(c, 'discard runtime config updates') : (0, core_1.describeCompletion)(c, configSchemaInfo([...path, offered[i]]).type ?? '')])]) };
}
else if (lastPath.length > 0 && configSchemaKeys(fullPath).length > 0) {
return { completions: [`${sigil}${fullPath.join('.')}.`] };
}
}
const leafInfo = configSchemaInfo(fullPath);
if (leafInfo.type === undefined) {
return { completions: [] }; // not a real config key: offer nothing (in particular, never append `=`)
}
const leaf = `${sigil}${fullPath.join('.')}`;
if (update) {
const { concrete, placeholder } = splitValueHints(leafInfo);
if (concrete.length > 0) { // a boolean/enum offers its values to Tab
return { completions: concrete.map(v => `${leaf}=${v}`) };
}
if (placeholder !== undefined) { // a free type: complete up to `=` and only *show* the `<type>`, never insert it
return { completions: [`${leaf}=`], hints: [`${leaf}=${placeholder}`] };
}
}
return { completions: [`${leaf}${update ? '=' : ''}`] };
}
/** every `.`-separated path the config schema allows, intermediate objects included, for {@link configGlobMatcher} */
function configPaths() {
configSchemaDescription ??= config_1.FlowrConfig.Schema.describe();
return (0, schema_1.descriptionPaths)(configSchemaDescription);
}
/** shallower (fewer path segments) first, alphabetical among equally deep paths */
function sortConfigPaths(paths) {
return [...paths].sort((a, b) => a.length - b.length || a.join('.').localeCompare(b.join('.')));
}
/** matches a config path against a glob, `*` covering one segment and `**` any number (`**.enabled`, `solver.*`) */
function configGlobMatcher(pattern) {
const segments = pattern.split('.');
let regex = '^';
for (let i = 0; i < segments.length; i++) {
const last = i === segments.length - 1;
if (segments[i] === '**') {
regex += last ? '[^.]+(?:\\.[^.]+)*' : '(?:[^.]+\\.)*';
}
else {
regex += segments[i].split('*').map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('[^.]*') + (last ? '' : '\\.');
}
}
const matches = new RegExp(regex + '$');
return path => matches.test(path.join('.'));
}
/** an error message if `path` is not a key of the config schema (naming the first unknown segment and its real siblings), or `undefined` if it is a valid key */
function unknownConfigKey(path, formatter) {
configSchemaDescription ??= config_1.FlowrConfig.Schema.describe();
const unknown = (0, schema_1.firstUnknownSchemaSegment)(configSchemaDescription, path);
if (unknown === undefined) {
return undefined; // every segment is a known (or pattern-/free-form-accepted) key
}
if (unknown.segment.includes('=')) {
const full = path.join('.');
return `To set a value, prefix the key with ${(0, ansi_1.bold)('+', formatter)}: ${(0, ansi_1.bold)('+' + full, formatter)}. Without it ${(0, ansi_1.bold)(full, formatter)} is read as a key to inspect.`;
}
const where = unknown.at.length === 0 ? 'the top level' : (0, ansi_1.bold)(unknown.at.join('.'), formatter);
return `Unknown config key ${(0, ansi_1.bold)(path.join('.'), formatter)}: no ${(0, ansi_1.bold)(unknown.segment, formatter)} at ${where}. Available: ${[...unknown.available].join(', ') || '(none)'}`;
}
/** an error message if `value` does not fit the schema type at `path`, or `undefined` if it fits */
function badConfigValue(path, value, formatter) {
const info = configSchemaInfo(path);
const expected = (t) => `${(0, ansi_1.bold)(path.join('.'), formatter)} expects a ${t}, got ${(0, ansi_1.bold)(JSON.stringify(value), formatter)}`;
if (info.valids && info.valids.length > 0 && !info.valids.includes(value)) {
return expected(`one of ${info.valids.map(v => JSON.stringify(v)).join(', ')}`);
}
if (info.type === 'boolean' && typeof value !== 'boolean') {
return expected('boolean (true/false)');
}
if (info.type === 'number' && typeof value !== 'number') {
return expected('number');
}
if (info.type === 'string' && typeof value !== 'string') {
return expected('string');
}
if (info.type === 'array' && !Array.isArray(value)) {
return expected('list (e.g. ["a","b"])');
}
if (info.type === 'object' && (typeof value !== 'object' || value === null || Array.isArray(value))) {
return `${(0, ansi_1.bold)(path.join('.'), formatter)} is a config section, not a value to set; set one of its fields instead`;
}
return undefined;
}
/** yields each leaf `[path, value]` of a (possibly nested) config update object */
function* configUpdateLeaves(update, prefix = []) {
for (const [key, value] of Object.entries(update)) {
const path = [...prefix, key];
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
yield* configUpdateLeaves(value, path);
}
else {
yield [path, value];
}
}
}
/**
* The first schema violation (an unknown key or a wrong-typed value) in a config update, or `undefined` if it is
* valid. Shared by the repl line parser and the {@link executeConfigQuery|executor}, so a programmatic or JSON-API
* update is validated exactly like a `:config +key=value` line and never merges junk or a mistyped value.
*/
function validateConfigUpdate(update, formatter = ansi_1.voidFormatter) {
for (const [path, value] of configUpdateLeaves(update)) {
const err = unknownConfigKey(path, formatter) ?? badConfigValue(path, value, formatter);
if (err !== undefined) {
return err;
}
}
return undefined;
}
function configQueryLineParser(output, line, _config) {
const first = line[0] ?? '';
if (first === '+reset') {
return { query: [{ type: 'config', reset: true }] };
}
if (first.startsWith('+')) {
const [pathPart, ...valueParts] = first.slice(1).split('=');
// build the update object
const path = pathPart.split('.').filter(p => p.length > 0);
if (path.length === 0 || valueParts.length !== 1) {
return configError(output, `Invalid config update syntax, must be of the form ${(0, ansi_1.bold)('+path.to.field=value', output.formatter)}`);
}
const raw = valueParts[0];
let value;
try {
value = JSON.parse(raw); // numbers, booleans, arrays, ...
}
catch {
try {
value = JSON.parse(raw.replace(/'([^']*)'/g, (_, s) => JSON.stringify(s))); // allow 'single' quotes too
}
catch {
value = raw; // fall back to a plain string
}
}
const update = path.reduceRight((acc, key) => ({ [key]: acc }), value);
const err = validateConfigUpdate(update, output.formatter);
return err !== undefined ? configError(output, err) : { query: [{ type: 'config', update }] };
}
// a plain path inspects that part; an empty suffix dumps the whole config
const path = first.split('.').filter(p => p.length > 0);
if (path.length === 0) {
return { query: [{ type: 'config' }] };
}
if (first.includes('*')) {
// a glob reads several keys at once (inspection only, setting stays a single explicit key)
const matches = configGlobMatcher(first);
const found = sortConfigPaths(configPaths().filter(matches));
return found.length === 0
? configError(output, `No config key matches ${(0, ansi_1.bold)(first, output.formatter)}`)
: { query: found.map(p => ({ type: 'config', inspect: p })) };
}
const unknown = unknownConfigKey(path, output.formatter);
return unknown !== undefined ? configError(output, unknown) : { query: [{ type: 'config', inspect: path }] };
}
/** report a parse/validation error for the line and produce no query, so nothing runs and the config is not dumped */
function configError(output, message) {
output.stdout(message);
return { query: undefined };
}
/** the dotted leaf keys of a (possibly nested) config update object */
function collectKeysFromUpdate(update) {
return [...configUpdateLeaves(update)].map(([path]) => path.join('.'));
}
function getValueAtPath(obj, path) {
let current = obj;
for (const key of path) {
if (current && typeof current === 'object' && current[key] !== undefined) {
current = current[key];
}
else {
return undefined;
}
}
return current;
}
let configSchemaDescription;
/** the keys the schema offers below a path, including the unset optional ones */
function configSchemaKeys(path) {
configSchemaDescription ??= config_1.FlowrConfig.Schema.describe();
return (0, schema_1.descriptionPathKeys)(configSchemaDescription, path);
}
/** the Joi-schema type + description of a config path (from {@link FlowrConfig.Schema}), used to document a key inspection */
function configSchemaInfo(path) {
configSchemaDescription ??= config_1.FlowrConfig.Schema.describe();
return (0, schema_1.descriptionPathInfo)(configSchemaDescription, path);
}
/** value completions for a config leaf: booleans, an enum's members (offered bare, like `true`/`false`), or a compact `<type>` hint */
function configValueHints(info) {
if (info.type === 'boolean') {
return ['true', 'false'];
}
if (info.valids && info.valids.length > 0) {
// a string enum completes as its bare member (like a boolean); non-strings keep their JSON form
return info.valids.map(v => typeof v === 'string' ? v : JSON.stringify(v));
}
return info.type ? [`<${info.type}>`] : [];
}
/** the leaf's `concrete` completable values (booleans/enum members) vs a `<type>` `placeholder` that is only shown, never inserted */
function splitValueHints(info) {
const hints = configValueHints(info);
return { concrete: hints.filter(v => !v.startsWith('<')), placeholder: hints.find(v => v.startsWith('<')) };
}
/** children of an inspected object listed before the rest is summarized */
const MaxInspectedChildren = 15;
/** the longest a child's value may get before only its size is shown */
const MaxInspectedValueWidth = 48;
/** a nested object is only summarized, inspecting it directly unfolds it */
function compactValue(value) {
const json = JSON.stringify(value, json_1.jsonReplacer);
if (value === null || typeof value !== 'object' || json.length <= MaxInspectedValueWidth) {
return json;
}
const keys = Array.isArray(value) ? value.length : Object.keys(value).length;
return Array.isArray(value) ? `[${keys} entries]` : `{${keys} keys}`;
}
/** an object reads better as its children than as one json blob */
function inspectedChildren(value, path, formatter) {
const entries = Object.entries(value);
const shown = entries.slice(0, MaxInspectedChildren);
const longest = Math.max(...shown.map(([key]) => key.length));
const lines = shown.map(([key, child]) => {
const type = configSchemaInfo([...path, key]).type;
return ` - ${(key + ':').padEnd(longest + 1)} ${compactValue(child)}`
+ (type ? ` ${(0, ansi_1.italic)(`(${type})`, formatter)}` : '');
});
if (entries.length > shown.length) {
lines.push(` ${(0, ansi_1.italic)(`... and ${entries.length - shown.length} more, inspect them with ${path.join('.')}.<key>`, formatter)}`);
}
return lines;
}
exports.ConfigQueryDefinition = {
title: 'Config Query',
syntax: '@config [<path> | <glob>] | @config +<path>=<value> | @config +reset',
executor: config_query_executor_1.executeConfigQuery,
asciiSummarizer: (formatter, _analyzer, queryResults, result, queries) => {
const out = queryResults;
result.push(`Query: ${(0, ansi_1.bold)('config', formatter)} (${(0, time_1.printAsMs)(out['.meta'].timing, 0)})`);
const configQueries = queries.filter(q => q.type === 'config');
const inspects = configQueries.filter(q => q.inspect).map(q => q.inspect);
if (configQueries.some(q => q.reset)) {
result.push(' ╰ Configuration reset to the analyzer\'s base config');
}
else if (configQueries.some(q => q.update)) {
const updatedKeys = configQueries.flatMap(q => q.update ? collectKeysFromUpdate(q.update) : []);
result.push(' ╰ Updated configuration:');
for (const key of updatedKeys) {
const path = key.split('.');
const newValue = getValueAtPath(out.config, path);
result.push(` - ${key}=${JSON.stringify(newValue, json_1.jsonReplacer)}`);
}
}
else if (inspects.length > 0) {
result.push(' ╰ Config:');
for (const path of inspects) {
const info = configSchemaInfo(path);
const value = getValueAtPath(out.config, [...path]);
const type = info.type ? ` ${(0, ansi_1.italic)(`(${info.type})`, formatter)}` : '';
const children = value !== null && typeof value === 'object' && !Array.isArray(value)
? inspectedChildren(value, path, formatter) : undefined;
result.push(` - ${path.join('.')}${type}${children ? '' : `: ${JSON.stringify(value, json_1.jsonReplacer)}`}`);
if (info.description) {
result.push(` ${(0, ansi_1.italic)(info.description, formatter)}`);
}
result.push(...children ?? []);
}
}
else {
result.push(` ╰ Config:\n${JSON.stringify(out.config, json_1.jsonReplacer, 4)}`);
}
if (out.specialization) {
const keys = collectKeysFromUpdate(out.specialization.overwrite);
result.push(` ╰ Specialized for project kind ${(0, ansi_1.bold)(out.specialization.kind, formatter)} (overrides ${keys.join(', ') || '(nothing)'})`);
}
else {
result.push(` ╰ ${(0, ansi_1.italic)('No project-kind specialization in effect', formatter)}`);
}
return true;
},
completer: configReplCompleter,
fromLine: configQueryLineParser,
schema: joi_1.default.object({
type: joi_1.default.string().valid('config').required().description('The type of the query.'),
update: joi_1.default.object().optional().description('An optional partial configuration to update the current configuration with before returning it. Only the provided fields will be updated, all other fields will remain unchanged.'),
inspect: joi_1.default.array().items(joi_1.default.string()).optional().description('An optional `.`-separated path (as a string array) to read a single configuration value instead of returning the whole configuration.'),
reset: joi_1.default.boolean().optional().description('If true, discard every prior update and revert to the config the analyzer was created with, before returning it.')
}).description('The config query retrieves the current configuration of the flowR instance and optionally also updates it.'),
flattenInvolvedNodes: () => []
};
//# sourceMappingURL=config-query-format.js.map