UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

395 lines 24.8 kB
"use strict"; 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