@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
395 lines • 24.8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GuessDepVersionsQueryDefinition = exports.DefaultCandidateCap = void 0;
const joi_1 = __importDefault(require("joi"));
const ansi_1 = require("../../../util/text/ansi");
const time_1 = require("../../../util/text/time");
const r_version_1 = require("../../../util/r-version");
const arrays_1 = require("../../../util/collections/arrays");
const identifier_1 = require("../../../dataflow/environments/identifier");
const signature_query_executor_1 = require("../signature-query/signature-query-executor");
const config_1 = require("../../../config");
const guess_dep_versions_query_executor_1 = require("./guess-dep-versions-query-executor");
/** the default cap on how many surviving candidate versions are listed per dependency */
exports.DefaultCandidateCap = 16;
/** parse a repl line: `[(clean[:<=YYYY])] [pkg ...] [--date YYYY.MM.DD] [--max N] [--iterations N] [--disabled <letters>] [--explode [--oldest] [--limit N] [--prefer pkg=ver ...]]` */
function guessDepVersionsLineParser(_output, line, _config) {
const packages = [];
const codeParts = [];
let date;
let maxCandidates;
let maxIterations;
let clean = false;
let explode = false;
let order;
let limit;
const prefer = {};
const disabled = new Set();
// an optional parenthesised clause `(clean)`, `(<=2025)`, or `(clean:<=2025)`: `clean` drops declared constraints, a (`<=`-prefixed) date caps the window
const tokens = line.slice();
const open = tokens.findIndex(t => t.startsWith('('));
const close = open < 0 ? -1 : tokens.findIndex((t, i) => i >= open && t.endsWith(')'));
if (open >= 0 && close >= open) {
const clause = tokens.slice(open, close + 1).join(' ').replace(/^\(|\)$/g, '');
for (const part of clause.split(/[:,]/).map(s => s.trim()).filter(s => s.length > 0)) {
if (part === 'clean') {
clean = true;
}
else {
date = part.replace(/^<=/, '').trim(); // `<=2025` or a bare `2025`/`2025.06` cap
}
}
tokens.splice(open, close - open + 1);
}
for (let i = 0; i < tokens.length; i++) {
const tok = tokens[i];
if (tok.length === 0) {
continue;
}
if (tok === '--date') {
date = tokens[++i];
}
else if (tok === '--max') {
maxCandidates = Number(tokens[++i]);
}
else if (tok === '--iterations' || tok === '--iter') {
maxIterations = Number(tokens[++i]);
}
else if (tok === '--explode') {
explode = true;
}
else if (tok === '--oldest') {
order = 'oldest';
}
else if (tok === '--newest') {
order = 'newest';
}
else if (tok === '--limit') {
limit = Number(tokens[++i]);
}
else if (tok === '--prefer') {
const [pkg, ver] = (tokens[++i] ?? '').split('=');
if (pkg && ver) {
prefer[pkg] = ver;
}
}
else if (tok === '--only') {
packages.push(...(tokens[++i] ?? '').split(',').map(s => s.trim()).filter(s => s.length > 0));
}
else if (tok === '--disabled') {
for (const ch of tokens[++i] ?? '') {
const source = letterToSource[ch];
if (source) {
disabled.add(source);
}
}
}
else if (!tok.startsWith('--')) {
// every bare token is the code to analyse (a `file://`/`watch://` target, a bare path the repl
// auto-prepends `file://` to, or inline R code); package filters are the explicit `--only` flag, so nothing is guessed
codeParts.push(tok);
}
}
const explodeOpts = explode ? {
explode: {
...(order ? { order } : {}),
...(limit !== undefined && !Number.isNaN(limit) ? { limit } : {}),
...(Object.keys(prefer).length > 0 ? { prefer } : {})
}
} : {};
return {
rCode: codeParts.length > 0 ? codeParts.join(' ') : undefined,
query: [{
type: 'guess-dep-versions',
...(packages.length > 0 ? { packages } : {}),
...(date ? { date } : {}),
...(maxCandidates !== undefined && !Number.isNaN(maxCandidates) ? { maxCandidates } : {}),
...(maxIterations !== undefined && !Number.isNaN(maxIterations) ? { maxIterations } : {}),
...(clean ? { clean } : {}),
...(disabled.size > 0 ? { disabled: [...disabled] } : {}),
...explodeOpts
}]
};
}
const evidenceColor = {
declared: 6 /* Colors.Cyan */,
transitive: 4 /* Colors.Blue */,
signature: 5 /* Colors.Magenta */,
date: 3 /* Colors.Yellow */,
'base-r': 2 /* Colors.Green */,
available: 7 /* Colors.White */,
indirect: 1 /* Colors.Red */
};
/** priority when several sources state the same bound: the lowest-ranked stays active, the rest gray out */
const sourceRank = {
declared: 0,
transitive: 1,
indirect: 2,
'base-r': 3,
date: 4,
signature: 5,
available: 6
};
/** a one-letter marker per evidence source (shown instead of a bullet, so the source is legible without color) */
const evidenceLetter = {
declared: 'd',
transitive: 't',
signature: 's',
date: 'D',
'base-r': 'b',
available: '#',
indirect: 'i'
};
/** inverse of {@link evidenceLetter}, decoding a `--disabled` flag's letters back to sources */
const letterToSource = Object.fromEntries(Object.entries(evidenceLetter).map(([source, letter]) => [letter, source]));
/** the tightest bound among a function's signature constraints for the given operator: highest `>=` or lowest `<=` */
function tightestBound(evs, op) {
let best, bestVer;
for (const e of evs) {
if (!e.bound?.startsWith(op)) {
continue;
}
const ver = e.bound.slice(op.length).trim();
if (bestVer === undefined || (op === '>=' ? r_version_1.RVersion.compare(ver, bestVer) > 0 : r_version_1.RVersion.compare(ver, bestVer) < 0)) {
bestVer = ver;
best = e.bound;
}
}
return best;
}
/** a version-valued bound like `>=1.2.0` or `>= 3.1.0` (space-tolerant) split into operator and version (dates such as `<=2021-05-31` are not version bounds) */
function versionBound(bound) {
const m = /^(>=|<=)\s*(\d[\w.-]*)$/.exec(bound ?? '');
if (!m || !r_version_1.RVersion.parse(m[2])) {
return undefined;
}
return { op: m[1] === '>=' ? '>=' : '<=', ver: m[2] };
}
/**
* Order the bounds the way they are read: enforced before partial (which narrow nothing), lower bounds before upper
* ones, and the tightest of each first, so the bound that actually decides the range heads its group.
*/
function compareBounds(a, b) {
if (Boolean(a.partial) !== Boolean(b.partial)) {
return a.partial ? 1 : -1;
}
const va = versionBound(a.bound), vb = versionBound(b.bound);
if (va === undefined || vb === undefined) {
return va === vb ? 0 : va === undefined ? 1 : -1; // a non-version bound (a date, `*`) sorts last
}
if (va.op !== vb.op) {
return va.op === '>=' ? -1 : 1;
}
return va.op === '>=' ? r_version_1.RVersion.compare(vb.ver, va.ver) : r_version_1.RVersion.compare(va.ver, vb.ver);
}
/** whether a dependency's version is actually narrowed (not every database version survives); a fully redundant one is unconstrained */
function isConstrained(dep) {
return dep.constrained !== false;
}
/** the assumed R version with where it came from, as a detected one bounds the guess without describing the code */
function rVersionText(out, formatter) {
return (0, ansi_1.italic)(out.rVersion, formatter) + (out.rVersionOrigin ? ' ' + (0, ansi_1.faint)(`(${out.rVersionOrigin})`, formatter) : '');
}
/** a version-combination count with thousands separators (exponential only past a trillion, where digits stop being useful) */
function formatCombinations(n) {
return n >= 1e12 ? n.toExponential(1) : String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
/** a list, truncated with a `(+n)` tail once it gets long, so it stays on one readable line */
function truncatedList(items, shown = 4) {
return items.length > shown ? `${items.slice(0, shown).join(', ')} (+${items.length - shown})` : items.join(', ');
}
/** a short, readable phrase for one non-signature bound, its origin(s) folded in (so several packages sharing a bound collapse to one line) */
function nonSignaturePhrase(ev, origins) {
const b = ev.bound ? ev.bound.replace(/([<>]=?)\s+/g, '$1') + ' ' : '';
// a partial bound holds for only some versions of its origin, so it never narrows: say so instead of implying it did
const some = ev.partial ? 'some ' : '', not = ev.partial ? ' (not applied)' : '';
switch (ev.source) {
case 'declared': return `${b}declared`;
case 'transitive': return `${b}required by ${some}${origins.join(', ')}${ev.partial ? ' versions' : ''}${not}`;
case 'date': return `releases up to ${origins[0]}`;
case 'base-r': return `${b}bounded by ${origins[0]}`;
case 'available': return `${b}available in database`;
case 'indirect': return `${b}via ${origins.join(', ')}${not}`;
default: return b.trim();
}
}
/** a version bound is dominated when a strictly tighter same-direction bound exists (a redundant `>=`/`<=`) */
function isDominated(bound, tightestGe, tightestLe) {
const b = versionBound(bound);
if (!b) {
return false;
}
return b.op === '>=' ? tightestGe !== undefined && r_version_1.RVersion.compare(b.ver, tightestGe) < 0
: tightestLe !== undefined && r_version_1.RVersion.compare(b.ver, tightestLe) > 0;
}
exports.GuessDepVersionsQueryDefinition = {
title: 'Guess Dependency Versions Query',
executor: guess_dep_versions_query_executor_1.executeGuessDepVersionsQuery,
asciiSummarizer: (formatter, _analyzer, queryResults, result, _query) => {
const out = queryResults;
result.push(`Query: ${(0, ansi_1.bold)('guess-dep-versions', formatter)} (${(0, time_1.printAsMs)(out['.meta'].timing, 0)})`);
if (out.dateCutoff) {
result.push(` ╰ up to ${(0, ansi_1.italic)(out.dateCutoff, formatter)}${out.rVersion ? `, R ${rVersionText(out, formatter)}` : ''}`);
}
else if (out.rVersion) {
result.push(` ╰ R ${rVersionText(out, formatter)}`);
}
if (out.dependencies.some(d => d.evidence.length > 0)) {
const legend = Object.keys(evidenceLetter).map(s => `${(0, ansi_1.color)(evidenceLetter[s], evidenceColor[s], formatter)} ${s}`).join(' ');
result.push(` ${(0, ansi_1.italic)('evidence', formatter)}: ${legend}`);
}
if (out.message) {
result.push(` ╰ ${(0, ansi_1.color)(out.message, 1 /* Colors.Red */, formatter)}`);
}
const resolvable = out.dependencies.filter(d => d.totalVersions !== undefined && d.totalVersions > 0);
// the sample illustrates the configured selection policy: the oldest satisfying version, else the newest
const selection = out.versionSelection ?? config_1.VersionSelection.Newest;
const pick = (d) => selection === config_1.VersionSelection.Oldest ? d.minVersion : d.maxVersion;
const sample = resolvable.filter(d => pick(d)).map(d => `${d.package}@${pick(d)}`);
if (sample.length > 0) {
// only the newest-selection sample can claim "works with all newest versions"; the reference "newest" is the
// newest release the date window allows (== each pick when a date cutoff is set), else the database's latest
const latestChecks = selection !== config_1.VersionSelection.Newest ? [] : out.dependencies
.map(d => ({ name: d.package, max: d.maxVersion, newest: out.dateCutoff !== undefined ? d.maxVersion : versionBound(tightestBound(d.evidence.filter(e => e.source === 'available'), '<='))?.ver }))
.filter((c) => c.max !== undefined && c.newest !== undefined);
const staleLatest = latestChecks.filter(c => c.max !== c.newest);
const latestSuffix = latestChecks.length === 0 ? ''
: staleLatest.length === 0
? ' ' + (0, ansi_1.color)('(works with all newest versions)', 2 /* Colors.Green */, formatter)
: ' ' + (0, ansi_1.faint)(`(newest excluded: ${staleLatest.map(c => `${c.name} ${c.newest}, use ${c.max}`).join('; ')})`, formatter);
result.push(` ${(0, ansi_1.color)('▶', 2 /* Colors.Green */, formatter)} ${(0, ansi_1.bold)('sample', formatter)} ${(0, ansi_1.faint)(`(${selection})`, formatter)}: ${sample.join(', ')}${latestSuffix}`);
const runnable = out.runnableCombinations, possible = out.possibleCombinations;
if (runnable !== undefined && possible !== undefined && possible > 0) {
const share = (of) => {
const pct = runnable / of * 100;
return pct < 10 ? pct.toFixed(1) : String(Math.round(pct));
};
// against the whole database, and against what the project already declares (so: what the guess added)
const declared = out.declaredCombinations;
const ofDeclared = declared !== undefined && declared > 0 ? `, ${share(declared)}% of declared` : '';
result.push(` ${(0, ansi_1.italic)('runnable combinations', formatter)}: ${(0, ansi_1.bold)(formatCombinations(runnable), formatter)} ${(0, ansi_1.faint)(`(${share(possible)}%${ofDeclared})`, formatter)}`);
}
}
// the packages locked to one shared version
for (const group of out.linkedGroups ?? []) {
const rep = out.dependencies.find(d => group.includes(d.package) && d.maxVersion !== undefined);
result.push(` ${(0, ansi_1.italic)('linked', formatter)}: ${group.join(' + ')}${rep?.maxVersion ? (0, ansi_1.faint)(' @ ' + rep.maxVersion, formatter) : ''}`);
}
for (const dep of out.dependencies) {
const coupled = dep.coupledWith ?? [];
const groupTag = (dep.base ? ' ' + (0, ansi_1.italic)('[base]', formatter)
: dep.linkedWith ? ' ' + (0, ansi_1.italic)(`[linked: ${dep.linkedWith.join(', ')}]`, formatter) : '')
+ (coupled.length > 0 ? ' ' + (0, ansi_1.italic)(`[coupled: ${truncatedList(coupled, 3)}]`, formatter) : '')
+ (dep.orphan ? ' ' + (0, ansi_1.color)(`[orphan: attach library(${dep.package}) for ${truncatedList(dep.orphanFunctions ?? [], 3)}]`, 3 /* Colors.Yellow */, formatter) : '');
const anyVersion = coupled.length > 0 ? '(any version on its own)' : '(any version)';
const note = dep.known === false ? ' ' + (0, ansi_1.color)('(not in database)', 1 /* Colors.Red */, formatter)
: dep.base ? '' : dep.used === false ? ' ' + (0, ansi_1.faint)('(not called)', formatter) : !isConstrained(dep) && dep.totalVersions ? ' ' + (0, ansi_1.faint)(anyVersion, formatter) : '';
const tag = groupTag + note;
const range = dep.unsatisfiable ? (0, ansi_1.color)('unsatisfiable', 1 /* Colors.Red */, formatter) : (0, ansi_1.bold)(dep.range, formatter);
const count = dep.totalVersions !== undefined ? `${dep.candidateCount}/${dep.totalVersions} versions` : `${dep.candidateCount} candidate${dep.candidateCount === 1 ? '' : 's'}`;
const avail = dep.evidence.filter(e => e.source === 'available');
const dbGe = versionBound(tightestBound(avail, '>='))?.ver, dbLe = versionBound(tightestBound(avail, '<='))?.ver;
const dbRange = dbGe !== undefined && dbLe !== undefined ? `, db ${dbGe} - ${dbLe}` : '';
result.push(` ${(0, ansi_1.bold)('━ ' + dep.package, formatter)}${tag} ${range} ${(0, ansi_1.faint)('(' + count + dbRange + ')', formatter)}`);
for (const o of dep.orphanEvidence ?? []) {
// why the package is inferred at all: the call is undefined without it, plus why it beat another exporter
const where = o.location ? ` at ${o.location}` : '';
const why = o.reason === 'sole exporter' ? ` ${(0, ansi_1.faint)(`(only ${dep.package})`, formatter)}`
: o.reason === 'most downloaded' ? ` ${(0, ansi_1.faint)(`(most downloaded of ${o.exporters})`, formatter)}` : '';
result.push(` ${(0, ansi_1.color)('!', 3 /* Colors.Yellow */, formatter)} ${o.function}()${where} resolves to no definition${why}`);
}
if (dep.orphanAlternatives?.length) {
// attributing an orphan is a guess, so name the exporters it beat and what each of them would fit
const alts = dep.orphanAlternatives.map(a => `${a.package} ${a.range}`);
result.push(` ${(0, ansi_1.italic)('or instead', formatter)}: ${truncatedList(alts, 4)}`);
}
// only enforced evidence sets the tightest bound; a partial one narrows nothing and must not hide a real bound
const enforced = dep.evidence.filter(e => !e.partial);
const tightestGe = versionBound(tightestBound(enforced, '>='))?.ver;
const tightestLe = versionBound(tightestBound(enforced.filter(e => versionBound(e.bound)), '<='))?.ver;
const active = [], dominated = [];
const nonSig = [...(0, arrays_1.arraysGroupBy)(dep.evidence.filter(e => e.source !== 'signature' && e.source !== 'available'), e => `${e.source}|${e.bound ?? ''}|${e.partial ? 'p' : ''}`)]
.map(([, evs]) => ({ ev: evs[0], origins: [...new Set(evs.map(e => e.origin))] }))
.sort((x, y) => compareBounds(x.ev, y.ev));
const bestRankByBound = new Map();
for (const { ev } of nonSig) {
const vb = versionBound(ev.bound);
if (vb === undefined || isDominated(ev.bound, tightestGe, tightestLe)) {
continue;
}
const key = vb.op + vb.ver, rank = sourceRank[ev.source];
const prev = bestRankByBound.get(key);
if (prev === undefined || rank < prev) {
bestRankByBound.set(key, rank);
}
}
for (const { ev, origins } of nonSig) {
const phrase = nonSignaturePhrase(ev, origins);
const vb = versionBound(ev.bound);
const best = vb === undefined ? undefined : bestRankByBound.get(vb.op + vb.ver);
const redundant = best !== undefined && sourceRank[ev.source] > best;
if (isDominated(ev.bound, tightestGe, tightestLe) || redundant) {
dominated.push(` ${(0, ansi_1.faint)(evidenceLetter[ev.source] + ' ' + phrase, formatter)}`);
}
else {
active.push(` ${(0, ansi_1.color)(evidenceLetter[ev.source], evidenceColor[ev.source], formatter)} ${phrase}`);
}
}
// signature evidence: bare names, grouped by bound, tightest first; non-tightest bounds are redundant (grayed)
const sigMarker = (0, ansi_1.color)(evidenceLetter.signature, evidenceColor.signature, formatter);
const sigRecords = [...(0, arrays_1.arraysGroupBy)(dep.evidence.filter(e => e.source === 'signature'), e => e.function ?? e.origin)].map(([fn, evs]) => {
const ge = tightestBound(evs, '>='), le = tightestBound(evs, '<=');
const geVer = versionBound(ge)?.ver, leVer = versionBound(le)?.ver;
const params = [...new Set(evs.map(e => e.parameter).filter((pm) => pm !== undefined))];
const at = evs.find(e => e.location)?.location;
const reasons = [evs.some(e => !e.parameter) ? 'new' : undefined, params.length > 0 ? `params: [${truncatedList(params)}]` : undefined,
at ? `at ${at}` : undefined].filter(Boolean).join(', ');
const name = identifier_1.Identifier.getName(identifier_1.Identifier.parse(fn));
const url = (0, signature_query_executor_1.rdrrDocUrl)(dep.package, name, { base: dep.base, cran: !dep.base });
const label = (url ? formatter.hyperlink(name, url, true) : name) + (reasons ? ` (${reasons})` : '');
const redundant = !(geVer !== undefined && geVer === tightestGe) && !(leVer !== undefined && leVer === tightestLe);
return { bounds: [ge, le].filter(Boolean).join(' '), label, redundant, geVer, leVer };
}).sort((a, b) => r_version_1.RVersion.compare(b.geVer, a.geVer) || r_version_1.RVersion.compare(b.leVer, a.leVer));
for (const [bounds, recs] of (0, arrays_1.arraysGroupBy)(sigRecords, r => r.bounds)) {
const body = `${bounds ? bounds + ' ' : ''}${recs.map(r => r.label).join(', ')}`;
if (recs[0].redundant) {
dominated.push(` ${(0, ansi_1.faint)(evidenceLetter.signature + ' ' + body, formatter)}`);
}
else {
active.push(` ${sigMarker} ${body}`);
}
}
result.push(...active, ...dominated);
}
if (out.assignments) {
result.push(` ${(0, ansi_1.bold)('assignments', formatter)}: ${out.assignments.length}`);
const first = out.assignments[0];
if (first) {
result.push(` ${(0, ansi_1.italic)('preferred', formatter)}: ${Object.entries(first.versions).map(([p, v]) => `${p}@${v}`).join(', ')}`);
}
}
return true;
},
fromLine: guessDepVersionsLineParser,
syntax: '@guess-dep-versions [<pkg> ...] [(clean | <=YYYY.MM.DD)] [--date YYYY.MM.DD] [--max <n>] [--iterations <n>] [--disabled <letters>] [--explode [--oldest] [--limit <n>] [--prefer <pkg>=<ver>]] <code | file://path>',
schema: joi_1.default.object({
type: joi_1.default.string().valid('guess-dep-versions').required().description('The type of the query.'),
packages: joi_1.default.array().items(joi_1.default.string()).optional().description('Restrict the guess to these packages; omit to guess for every declared and used dependency.'),
date: joi_1.default.string().optional().description('Only consider versions released on or before this day, written YYYY.MM.DD (also YYYY or YYYY.MM).'),
maxCandidates: joi_1.default.number().integer().min(0).optional().description('Cap the number of candidate versions listed per dependency.'),
maxIterations: joi_1.default.number().integer().min(0).optional().description('Bound both fixpoint loops (mutual transitive refinement and arc consistency).'),
clean: joi_1.default.boolean().optional().description('Ignore the project declared constraints (DESCRIPTION/lockfile/transitive); guess purely from code usage and the date/R bounds.'),
disabled: joi_1.default.array().items(joi_1.default.string().valid('declared', 'transitive', 'signature', 'date', 'base-r', 'available', 'indirect')).optional()
.description('Exclude these evidence sources from consideration entirely (repl: --disabled followed by their one-letter codes, e.g. --disabled ds for declared+signature).'),
explode: joi_1.default.object({
order: joi_1.default.string().valid('newest', 'oldest').optional().description('Iterate each dependency newest-first (default) or oldest-first.'),
prefer: joi_1.default.object().pattern(joi_1.default.string(), joi_1.default.string()).optional().description('A version to prefer per dependency when it survives the constraints.'),
limit: joi_1.default.number().integer().min(0).optional().description('Cap the number of version combinations considered. Combinations whose versions cannot be loaded together are skipped, so fewer assignments may come out.')
}).optional().description('Also explode the guessed space into concrete per-dependency version assignments.')
}).description('Guesses the possible version range of every dependency from declared constraints and signature-database usage.'),
flattenInvolvedNodes: () => []
};
//# sourceMappingURL=guess-dep-versions-query-format.js.map