@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
304 lines • 16.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.executeGuessDepVersionsQuery = executeGuessDepVersionsQuery;
const guess_dep_versions_query_format_1 = require("./guess-dep-versions-query-format");
const r_version_1 = require("../../../util/r-version");
const objects_1 = require("../../../util/objects");
const visualize_functions_1 = require("../dependencies-query/function-info/visualize-functions");
const dependency_version_space_1 = require("../../../project/dependency-version-space");
/** flowR's curated builtin-library function to package map (e.g. `ggplot` to `ggplot2`), disambiguating an orphan call several packages re-export */
const BuiltinLibraryByFunction = new Map(visualize_functions_1.VisualizeFunctions.filter(f => f.package !== undefined).map(f => [f.name, f.package]));
/** the sources `query` disables: `clean` is sugar for disabling `declared`+`transitive`, `disabled` adds any others by name */
function disabledSources(query) {
if (!query.clean && !query.disabled?.length) {
return dependency_version_space_1.NoDisabledSources;
}
return new Set([...(query.clean ? ['declared', 'transitive'] : []), ...(query.disabled ?? [])]);
}
function locatorOf(idMap) {
return id => {
const node = id === undefined ? undefined : idMap.get(id);
const at = node?.location ?? node?.info.fullRange;
return at === undefined ? undefined : `${node?.info.file ? node.info.file + ':' : ''}${at[0]}:${at[1]}`;
};
}
/** collects and deduplicates provenance-carrying constraints, resolving their call site to a location */
class EvidenceCollector {
locate;
seen = new Set();
list = [];
constructor(locate) {
this.locate = locate;
}
add = (c) => {
const key = `${c.source}|${c.origin}|${c.bound ?? ''}|${c.detail}`;
if (this.seen.has(key)) {
return;
}
this.seen.add(key);
this.list.push((0, objects_1.compactRecord)({ ...c, location: this.locate(c.at) }));
};
}
/** merge several guess-dep-versions queries into one: union the packages, keep the tightest (earliest date, smallest cap) */
function mergeQueries(queries) {
if (queries.length === 1) {
return queries[0];
}
const packages = new Set();
let anyAll = false;
let earliestDate, earliestTime = Infinity, anyDate;
let maxCandidates;
let maxIterations;
let explode;
const disabled = new Set();
let anyClean = false;
for (const q of queries) {
if (q.packages) {
for (const p of q.packages) {
packages.add(p);
}
}
else {
anyAll = true;
}
if (q.date) {
anyDate ??= q.date;
const parsed = (0, dependency_version_space_1.dateCutoff)(q.date);
if (parsed && parsed.getTime() < earliestTime) {
earliestTime = parsed.getTime();
earliestDate = q.date;
}
}
if (q.maxCandidates !== undefined) {
maxCandidates = maxCandidates === undefined ? q.maxCandidates : Math.min(maxCandidates, q.maxCandidates);
}
if (q.maxIterations !== undefined) {
maxIterations = maxIterations === undefined ? q.maxIterations : Math.max(maxIterations, q.maxIterations);
}
explode ??= q.explode;
anyClean ||= q.clean === true;
for (const s of q.disabled ?? []) {
disabled.add(s);
}
}
// keep the tightest date, or any malformed date to report it
const date = earliestDate ?? anyDate;
return {
type: 'guess-dep-versions',
...(anyAll || packages.size === 0 ? {} : { packages: [...packages] }),
...(date ? { date } : {}),
...(maxCandidates !== undefined ? { maxCandidates } : {}),
...(maxIterations !== undefined ? { maxIterations } : {}),
...(explode ? { explode } : {}),
...(anyClean ? { clean: true } : {}),
...(disabled.size > 0 ? { disabled: [...disabled] } : {})
};
}
/** format version range string from survivors and declared constraints, honest about gaps */
function rangeString(survivors, nonContiguous, unsatisfiable, declaredRange, declaredConstraints, cap) {
if (survivors.length === 0) {
if (unsatisfiable) {
return declaredConstraints.join(' ') || '<contradiction>';
}
return declaredRange ? declaredRange.raw : '*';
}
if (survivors.length === 1) {
return survivors[0];
}
const min = survivors[0], max = survivors[survivors.length - 1];
if (!nonContiguous) {
return `>=${min} <=${max}`;
}
// a version between min and max was rejected, so `>=min <=max` would falsely imply it is acceptable: be explicit
return survivors.length <= cap ? survivors.join(', ') : `${min}...${max} (${survivors.length} discrete)`;
}
/** build the reported guess for one package from its already-computed surviving versions and provenance */
function guessPackage(name, cap, surviving, evidence, { used, linkedWith, coupledWith, orphanFunctions, orphanEvidence, orphanAlternatives }) {
const { declaredRange, declaredConstraints, unsatisfiable } = surviving;
const survivors = surviving.survivors.map(e => e.ver);
const preSignature = surviving.preSignature.map(e => e.ver);
// non-contiguous: the signature pass rejected a version that lies between the surviving min and max
const nonContiguous = survivors.length > 1 && preSignature.some(v => r_version_1.RVersion.compare(v, survivors[0]) > 0 && r_version_1.RVersion.compare(v, survivors[survivors.length - 1]) < 0 && !survivors.includes(v));
const candidates = survivors.slice(0, cap);
return (0, objects_1.compactRecord)({
package: name,
base: surviving.base,
declaredConstraints,
range: rangeString(survivors, nonContiguous, unsatisfiable, declaredRange, declaredConstraints, cap),
minVersion: survivors.length > 0 ? survivors[0] : undefined,
maxVersion: survivors.length > 0 ? survivors[survivors.length - 1] : undefined,
candidateCount: survivors.length,
totalVersions: surviving.total,
constrained: surviving.total !== undefined && survivors.length === surviving.total ? false : undefined,
candidates: candidates.length > 0 ? candidates : undefined,
truncated: survivors.length > cap ? true : undefined,
evidence: evidence.list,
unsatisfiable: unsatisfiable ? true : undefined,
linkedWith: linkedWith && linkedWith.length > 0 ? linkedWith : undefined,
coupledWith: coupledWith && coupledWith.length > 0 ? coupledWith : undefined,
known: surviving.known ? undefined : false,
orphan: orphanFunctions && orphanFunctions.length > 0 ? true : undefined,
orphanFunctions: orphanFunctions && orphanFunctions.length > 0 ? orphanFunctions : undefined,
orphanEvidence: orphanEvidence && orphanEvidence.length > 0 ? orphanEvidence : undefined,
orphanAlternatives: orphanAlternatives && orphanAlternatives.length > 0 ? orphanAlternatives : undefined,
used
});
}
/**
* Executes a guess-dep-versions query.
*/
async function executeGuessDepVersionsQuery({ analyzer }, queries) {
const start = Date.now();
const query = mergeQueries(queries);
const ctx = analyzer.inspectContext();
const deps = ctx.deps;
const sources = deps.signatureSources();
if (sources.length === 0) {
return {
'.meta': { timing: Date.now() - start },
dependencies: [],
message: 'No signature database is loaded; version guessing needs the signature database (see the Signature Database wiki).'
};
}
let message;
let cutoff;
if (query.date) {
cutoff = (0, dependency_version_space_1.dateCutoff)(query.date);
if (!cutoff) {
message = `could not parse date '${query.date}', expected YYYY.MM.DD; ignoring the date bound`;
}
}
// bound base packages by R only when the version is genuinely known; in `auto` mode with nothing detected, base tries every R release
const rVersion = ctx.rVersionKnown ? (ctx.meta.getRVersion() ?? ctx.resolvedRVersion) : undefined;
const rVersionOrigin = rVersion !== undefined ? ctx.rVersionOrigin : undefined;
// the analyzed package guesses versions for its dependencies, not for itself
const self = ctx.meta.getNamespace();
const graph = (await analyzer.dataflow()).graph;
const locate = locatorOf((await analyzer.normalize()).idMap);
const usage = (0, dependency_version_space_1.collectUsage)(graph, deps);
// fold orphan calls (`ggplot()` with ggplot2 neither declared nor loaded) into usage; a package the project does
// not already know is flagged for downstream attachment (see collectOrphanUsage)
const known = new Set([...deps.getDependencies().map(d => d.name), ...deps.declaredPackageNames()]);
const orphans = (0, dependency_version_space_1.collectOrphanUsage)(graph, deps, usage, pkg => known.has(pkg), { self, builtinLibraryOf: name => BuiltinLibraryByFunction.get(name) });
const sorted = (query.packages && query.packages.length > 0 ? [...query.packages] : (0, dependency_version_space_1.defaultTargets)(deps, usage)).filter(name => name !== self).sort();
const disabled = disabledSources(query);
const space = new dependency_version_space_1.VersionSpace({ deps, usage, cutoff, rVersion, disabled });
const maxIterations = query.maxIterations ?? dependency_version_space_1.DefaultFixpointIterations;
const transitive = space.refineTransitive(sorted, maxIterations);
const cap = query.maxCandidates ?? guess_dep_versions_query_format_1.DefaultCandidateCap;
const explodeOrder = query.explode?.order ?? 'newest';
const guessedAll = sorted.map(name => {
const evidence = new EvidenceCollector(locate);
const surviving = space.survivors(name, transitive.get(name) ?? [], evidence.add);
return { name, evidence, surviving };
});
const initial = new Map(guessedAll.map(g => [g.name, g.surviving.survivors]));
const { survivors: pruned, blockers } = query.clean || disabled.has('indirect')
? { survivors: initial, blockers: new Map() }
: (0, dependency_version_space_1.enforceArcConsistency)(space, initial, maxIterations);
guessedAll.forEach(g => {
g.surviving = { ...g.surviving, survivors: pruned.get(g.name) ?? g.surviving.survivors };
const max = g.surviving.survivors.at(-1)?.ver;
for (const [partner, constraint] of blockers.get(g.name) ?? []) {
if (max !== undefined) {
g.evidence.add({ source: 'indirect', origin: `${partner} ${constraint}`, detail: `${g.name} capped by ${partner}`, bound: `<=${max}` });
}
}
});
// linked packages share one version: base/R group and configured groups; intersect survivor sets to keep them consistent
const groups = [
guessedAll.filter(g => g.surviving.base).map(g => g.name),
...(ctx.config.solver.versionManagement?.linkedVersionGroups ?? [])
];
const linkedGroups = [];
const linkedWith = new Map();
for (const group of groups) {
const members = guessedAll.filter(g => group.includes(g.name));
if (members.length > 1) {
const shared = (0, dependency_version_space_1.intersectSurvivors)(members.map(m => m.surviving.survivors));
for (const m of members) {
m.surviving = { ...m.surviving, survivors: shared };
}
linkedGroups.push(members.map(m => m.name));
for (const m of members) {
linkedWith.set(m.name, members.filter(o => o !== m).map(o => o.name));
}
}
}
// a package is a counted factor only when a *real* constraint bears on it (declared/transitive/signature/indirect);
// date/available narrowing alone must not promote an otherwise any-version package into the product, or a tighter
// date cutoff could paradoxically grow the count by turning more packages into factors. A partial constraint does
// count: it does not narrow the package on its own, but it does couple it to the package that declares it
const reallyConstrained = (g) => g.evidence.list.some(e => e.source === 'declared' || e.source === 'transitive' || e.source === 'signature' || e.source === 'indirect');
// one factor per linked group (its members share a version) plus every other constrained package
const grouped = new Set(linkedGroups.flat());
const representatives = [
...linkedGroups.map(group => guessedAll.find(g => group.includes(g.name))).filter(g => g !== undefined),
...guessedAll.filter(g => !grouped.has(g.name) && (g.surviving.total ?? 0) > 0 && reallyConstrained(g))
];
const factors = representatives.map(g => ({ name: g.name, survivors: g.surviving.survivors.map(e => e.ver) }));
const { total: runnableCombinations, couplings } = (0, dependency_version_space_1.countRunnableCombinations)(space, factors);
const possibleCombinations = representatives.reduce((p, g) => p * (g.surviving.total ?? 1), 1);
// the baseline the guess narrows down from: what the project already declares, before usage and interdependencies
const anyDeclared = !disabled.has('declared') && representatives.some(g => g.surviving.declaredConstraints.length > 0);
const declaredCombinations = anyDeclared ? representatives.reduce((p, g) => p * g.surviving.declared, 1) : undefined;
// a coupled package's version is not free: report the partners, flagging one that only some versions impose
const coupledWith = new Map();
for (const c of couplings) {
for (const [pkg, partner] of [[c.a, c.b], [c.b, c.a]]) {
coupledWith.set(pkg, [...coupledWith.get(pkg) ?? [], c.always ? partner : `${partner} (partial)`]);
}
}
// the packages an orphan could have meant instead, resolved in a space of their own so that reporting them
// does not turn them into dependencies of the project; built only when an orphan actually had a choice
const altSpace = orphans.alternativeUsage.size > 0
? new dependency_version_space_1.VersionSpace({ deps, usage: orphans.alternativeUsage, cutoff, rVersion, disabled }) : undefined;
const orphanAlternatives = (pkg) => (orphans.alternatives.get(pkg) ?? []).map(alt => {
const s = altSpace.survivors(alt, []);
const versions = s.survivors.map(e => e.ver);
return (0, objects_1.compactRecord)({
package: alt,
range: rangeString(versions, false, s.unsatisfiable, s.declaredRange, s.declaredConstraints, cap),
minVersion: versions[0],
maxVersion: versions[versions.length - 1],
candidateCount: versions.length,
totalVersions: s.total
});
});
const dependencies = [];
const ordered = [];
for (const g of guessedAll) {
const orphanCalls = [...orphans.attributed.get(g.name) ?? []].sort(([a], [b]) => a.localeCompare(b));
dependencies.push(guessPackage(g.name, cap, g.surviving, g.evidence, {
used: usage.has(g.name),
linkedWith: linkedWith.get(g.name),
coupledWith: coupledWith.get(g.name),
orphanFunctions: orphanCalls.map(([fn]) => fn),
orphanEvidence: orphanCalls.map(([fn, call]) => (0, objects_1.compactRecord)({ function: fn, location: locate(call.at), reason: call.reason, exporters: call.exporters })),
orphanAlternatives: orphanAlternatives(g.name)
}));
const oc = query.explode ? (0, dependency_version_space_1.orderedCandidatesOf)(space.resolve(g.name).src, g.name, g.surviving, query.explode.prefer?.[g.name], explodeOrder) : undefined;
if (oc) {
ordered.push(oc);
}
}
const assignments = query.explode
? [...(0, dependency_version_space_1.assignmentsOf)(ordered, query.explode.limit ?? dependency_version_space_1.DefaultExplodeLimit, (0, dependency_version_space_1.declaredDependenciesOf)(space))]
.map(a => (0, objects_1.compactRecord)({ versions: Object.fromEntries(a.versions), unverified: a.unverified }))
: undefined;
return (0, objects_1.compactRecord)({
'.meta': { timing: Date.now() - start },
dependencies,
dateCutoff: cutoff ? (0, dependency_version_space_1.isoDay)(cutoff) : undefined,
rVersion,
rVersionOrigin,
versionSelection: ctx.config.solver.sigdb.versionSelection,
runnableCombinations,
possibleCombinations,
declaredCombinations,
linkedGroups: linkedGroups.length > 0 ? linkedGroups : undefined,
assignments,
message
});
}
//# sourceMappingURL=guess-dep-versions-query-executor.js.map