UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

350 lines 19.2 kB
"use strict"; 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