UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

304 lines 16.9 kB
"use strict"; 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