@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
335 lines (334 loc) • 23.3 kB
TypeScript
/**
* The dependency version space: for each dependency, the set of signature-database versions that survive the
* project's constraints (declared ranges, transitive requirements, the base-R/assumed-R bound, a date cutoff, and
* how the code actually calls the package), and the explosion of that space into concrete per-dependency version
* assignments. This is a source-agnostic resolver over {@link PackageSignatureSource} and the dependencies context;
* the `guess-dep-versions` query presents it, but it is usable on its own (e.g. for compatibility-matrix tooling).
*/
import { type Range } from 'semver';
import { type VersionString } from '../util/r-version';
import { type PackageSignatureSource } from './sigdb/reader';
import type { DecodedFunction, ResolvedDependency } from './sigdb/decode';
import { FunctionArgument, type DataflowGraph } from '../dataflow/graph/graph';
import type { ReadOnlyFlowrAnalyzerDependenciesContext } from './context/flowr-analyzer-dependencies-context';
import type { ReadonlyFlowrAnalysisProvider } from './flowr-analyzer';
import type { NodeId } from '../r-bridge/lang-4.x/ast/model/processing/node-id';
/** the pseudo-package standing for the analyzed project itself, never one of its own dependencies */
export declare const ProjectPackage = "current";
/** where a single bound on a dependency's version came from */
export type ConstraintSource = 'declared' | 'transitive' | 'signature' | 'date' | 'base-r' | 'available' | 'indirect';
/**
* One provenance-carrying constraint on a dependency's version: *where* it comes from ({@link source}/{@link origin})
* and *what* it requires ({@link bound}). The set of these is exactly why a range is what it is, so it can answer
* "it must be `>= 4.2.0` because ...".
*/
export interface DerivedConstraint {
readonly source: ConstraintSource;
/** the concrete origin of the constraint, e.g. `project metadata`, `dplyr 1.1.0`, `dplyr::filter` */
readonly origin: string;
/** a human-readable explanation, e.g. `dplyr::filter has parameter '.by' only from 1.1.0` */
readonly detail: string;
/** the qualified function that carried the evidence (for {@link ConstraintSource|signature} constraints) */
readonly function?: string;
/** the argument/parameter that carried the evidence (for signature constraints) */
readonly parameter?: string;
/** the call site that carried the evidence, so a bound can point at the line forcing it */
readonly at?: NodeId;
/** the version bound this constraint establishes, if any (e.g. `>=1.1.0`, `<=2021-05-31`) */
readonly bound?: string;
/**
* Set when the bound only holds for *some* of the origin's own candidate versions (e.g. only the newer releases
* of `dplyr` require `R >= 4.1`). Such a constraint is reported but never filters, as picking another version of
* the origin avoids it.
*/
readonly partial?: boolean;
}
/** notified of each constraint as it is applied, so a caller can collect provenance (e.g. into the query's evidence) */
export type ConstraintObserver = (constraint: DerivedConstraint) => void;
/** resolves (and memoizes) one function's decoded signature at a given version of the current package */
export type FnResolver = (fn: string, version: VersionString) => DecodedFunction | undefined;
/** how a single function of a package is used in the code */
interface FunctionUsage {
/** per named argument used anywhere, the first call supplying it (drives the lower-bound evidence) */
readonly named: Map<string, NodeId>;
/** one representative argument list per distinct call *shape* (drives signature compatibility) */
readonly calls: Map<string, readonly FunctionArgument[]>;
/** the first call of the function, absent when only a class use implies it */
at?: NodeId;
}
/** per-package function usage, keyed by the function's (unqualified) name */
export type PackageUsage = Map<string, FunctionUsage>;
/** one dated release in a package's version timeline */
export interface TimelineEntry {
readonly ver: VersionString;
readonly date?: Date;
}
/**
* The version qualifier a package imposes on one of its own dependencies (a transitive constraint). Because the
* declaring package's own version is usually a guess too, the requirement is read from *every* version of it that is
* still in play: {@link ranges} holds one alternative per distinct requirement found, and only a
* {@link universal} constraint (one that every one of those versions declares) may filter.
*/
export interface TransitiveConstraint {
/** the alternative requirements; a version satisfying any one of them is acceptable */
readonly ranges: readonly Range[];
/** the declaring package + version, e.g. `dplyr 1.1.0`, or just `dplyr` when read from several of its versions */
readonly from: string;
/** whether every considered version of the declaring package requires *something*, so the constraint can filter */
readonly universal: boolean;
}
/** the versions of a package before and after the signature-usage filter, plus the declared inputs used to build them */
export interface SurvivingEntries {
/** versions after all constraints including signature-usage compatibility */
readonly survivors: TimelineEntry[];
/** versions after the declared/transitive/base/date constraints but *before* the signature filter */
readonly preSignature: TimelineEntry[];
/** the memoized function resolver used for the signature pass (shared so evidence reuses the decodes) */
readonly getFn: FnResolver;
/** the combined, satisfiable declared range (`inferredRange`), or `undefined` if none/contradictory */
readonly declaredRange: Range | undefined;
/** the raw declared version constraints */
readonly declaredConstraints: readonly string[];
/** whether the package is an R-core / base package */
readonly base: boolean;
/** whether the declared + transitive constraints contradict each other (no version can satisfy them all) */
readonly unsatisfiable: boolean;
/** the total number of versions the database carries for the package (the full history the candidates are drawn from) */
readonly total: number;
/** whether the database carries the package at all: `false` means "no record", not "no constraint" */
readonly known: boolean;
/** how many of those versions the *declared* constraints alone allow, the baseline the guess narrows down from */
readonly declared: number;
}
/** one package's surviving versions, ordered by preference for the constraint-space explosion */
export interface OrderedCandidates {
readonly pkg: string;
readonly versions: readonly VersionString[];
}
/** options for {@link explodeDependencyVersions} */
export interface VersionExplodeOptions {
/** iterate each package's versions newest-first (default) or oldest-first */
readonly order?: 'newest' | 'oldest';
/** a version to prefer per package, used first when it survives the constraints (package name to version) */
readonly prefer?: Readonly<Record<string, VersionString>>;
/** restrict to these packages (default: every declared and used dependency) */
readonly packages?: readonly string[];
/** only consider releases on or before this day, `YYYY.MM.DD` (also `YYYY` or `YYYY.MM`) */
readonly date?: string;
/** cap the number of assignments produced (default {@link DefaultExplodeLimit}) */
readonly limit?: number;
}
/** a concrete, sigdb-available version choice for every resolvable dependency */
export interface VersionAssignment {
readonly versions: ReadonlyMap<string, VersionString>;
/**
* The requirements of the chosen versions that nothing here could settle, because they bear on a package the
* assignment does not choose (`dplyr 1.1.0 requires rlang >= 1.0.0`). Such an assignment holds for what it
* states and may still fail at `library()` time, so it is proposed but not verified.
*/
readonly unverified?: readonly string[];
}
export declare const DefaultExplodeLimit = 256;
/** the date cutoff (end of the named day/month/year) for a `YYYY.MM.DD` spec, or `undefined` if malformed */
export declare function dateCutoff(spec: string): Date | undefined;
/** an ISO `YYYY-MM-DD` day */
export declare function isoDay(date: Date): string;
/** scan the dataflow graph for every call that resolves (via {@link Dataflow.qualify}) to a package export */
export declare function collectUsage(graph: DataflowGraph, deps?: ReadOnlyFlowrAnalyzerDependenciesContext): Map<string, PackageUsage>;
/** why one orphan call was attributed to its package, given that several may export the name */
export type OrphanReason = 'builtin' | 'sole exporter' | 'most downloaded';
/** one orphan call: the undefined name, where it is called, and why it was pinned on the package */
export interface OrphanCall {
readonly at: NodeId;
readonly reason: OrphanReason;
/** how many packages export the name, the field the {@link OrphanReason} picked from */
readonly exporters: number;
}
/**
* What the orphan calls of one analyzed program implicated, as {@link collectOrphanUsage} reports it.
*/
export interface OrphanUsage {
/** per package the project does not already know: the orphan calls that pointed at it, by function name */
readonly attributed: Map<string, Map<string, OrphanCall>>;
/** per such package: the other exporters of those names that lost the pick, most downloaded first */
readonly alternatives: Map<string, string[]>;
/** the same calls recorded against every alternative, so a caller can ask which of its versions would fit */
readonly alternativeUsage: Map<string, PackageUsage>;
}
/** options for {@link collectOrphanUsage} */
export interface OrphanUsageOptions {
/** the analyzed project's own namespace, never inferred as one of its own orphan dependencies */
readonly self?: string;
/** flowR's curated map of a builtin-modeled library function to its package (e.g. `ggplot` to `ggplot2`), the authoritative disambiguator when several packages export the name */
readonly builtinLibraryOf?: (name: string) => string | undefined;
}
/**
* Fold the analyzed code's *orphan* calls into `usage` and report which functions implicated each **unknown**
* package. An orphan is a bare call (`ggplot()`) whose name is not bound to a local/parameter/closure/import
* definition and is not a default-attached base export, but is exported by exactly one signature-database package.
* Such a call never qualifies to `pkg::fn`, so {@link collectUsage} is blind to it (even when flowR models the
* function as a builtin, as it does for `ggplot`), yet it pins the package's version just as a qualified call
* would; folding it into `usage` makes the package a guess target. The returned map lists, per package the project
* does not already declare or load (`isKnown` is `false`), the orphan function names that pointed at it (e.g.
* `ggplot2` from `ggplot()`) -- a note for a downstream handler to attach the library, since the symbol would be
* undefined were the package not loaded. Disambiguation is `options.builtinLibraryOf` (flowR's curated map, e.g.
* `ggplot` to `ggplot2`, authoritative even when several packages re-export the name), then a package the project
* already declares or loads, and finally the most downloaded of at most {@link MaxOrphanProviders} exporters; a
* name beyond that many packages export says nothing about which one is meant and is skipped, as are quoted (NSE)
* uses and forward-referenced closures. The exporters that lost the pick are kept as
* {@link OrphanUsage.alternatives}, since the guess is exactly that -- a guess.
*/
export declare function collectOrphanUsage(graph: DataflowGraph, deps: ReadOnlyFlowrAnalyzerDependenciesContext, usage: Map<string, PackageUsage>, isKnown: (pkg: string) => boolean, options?: OrphanUsageOptions): OrphanUsage;
/** intersection of multiple survivor sets: versions that survive in every set */
export declare function intersectSurvivors(survivorSets: readonly (readonly TimelineEntry[])[]): TimelineEntry[];
/**
* The transitive constraints declared packages place on their own dependencies (one level deep). `versionsOf` gives
* the versions of each declaring package that are still in play. The requirements are read from all of them, so a
* constraint only ever filters when *every* one of them declares it (see {@link TransitiveConstraint}). Without it,
* the single {@link Package.resolvedVersion|resolved version} is used, which is by definition universal.
*/
export declare function collectTransitiveConstraints(deps: ReadOnlyFlowrAnalyzerDependenciesContext, sources: readonly PackageSignatureSource[], versionsOf?: (pkg: string) => readonly VersionString[] | undefined): Map<string, TransitiveConstraint[]>;
/** the default bound for the fixpoint loops, overridable per query with {@link GuessDepVersionsQuery.maxIterations} */
export declare const DefaultFixpointIterations = 8;
/** repeatedly run `step` until it reports no further change (returns `false`) or `maxIterations` is reached */
export declare function iterateToFixpoint(maxIterations: number, step: () => boolean): void;
/** the per-analysis inputs every constraint pass shares, see {@link VersionSpace} */
export interface VersionSpaceOptions {
readonly deps: ReadOnlyFlowrAnalyzerDependenciesContext;
/** how the analyzed code calls each package, from {@link collectUsage} */
readonly usage: ReadonlyMap<string, PackageUsage>;
/** only consider releases up to this instant */
readonly cutoff?: Date;
/** the assumed R version bounding base packages, when genuinely known */
readonly rVersion?: string;
/** constraint sources to skip entirely: neither filtered on nor reported */
readonly disabled?: ReadonlySet<ConstraintSource>;
}
/** a target's sigdb package key, merged source and memoized signature resolver */
interface PackageResolution {
readonly key: string;
readonly src: PackageSignatureSource | undefined;
readonly getFn: FnResolver;
/** the package's release timeline, ascending */
readonly timeline: readonly TimelineEntry[];
/** the usage the signature pass judges by (base primitives dropped for a base package), `undefined` if unused */
readonly usage: PackageUsage | undefined;
/** whether a version's signatures accept how the code calls it, see {@link makeSignatureFilter} */
readonly signatureOk: (version: VersionString) => boolean;
}
/**
* The version space of one analysis: the surviving versions of each dependency and the transitive constraints
* refined to a fixpoint. Holding the shared inputs here keeps the passes to a handful of arguments and lets each
* package's sigdb key, source and decoded signatures be resolved once. The passes revisit the same versions
* repeatedly, so that memoization is what makes the fixpoint affordable.
*/
export declare class VersionSpace {
readonly deps: ReadOnlyFlowrAnalyzerDependenciesContext;
readonly sources: readonly PackageSignatureSource[];
readonly usage: ReadonlyMap<string, PackageUsage>;
readonly cutoff: Date | undefined;
readonly rVersion: string | undefined;
readonly disabled: ReadonlySet<ConstraintSource>;
private readonly resolved;
constructor({ deps, usage, cutoff, rVersion, disabled }: VersionSpaceOptions);
/** the sigdb package, source and signature resolver of a target, resolved once */
resolve(name: string): PackageResolution;
/** the versions of `name` surviving every constraint, see {@link survivingEntries} */
survivors(name: string, transitive: readonly TransitiveConstraint[], observe?: ConstraintObserver): SurvivingEntries;
/**
* The transitive constraints, refined to a fixpoint: each pass re-reads every declaring package's requirements
* from the versions of it that survived the previous pass, so two packages can tighten each other. Feeding back
* the whole surviving set (not one representative version) is what keeps a requirement that only *some* of those
* versions declare from filtering (see {@link TransitiveConstraint}).
*
* The first pass runs with *no* transitive constraint at all, so every later pass can only shrink the surviving
* sets and thus only tighten the constraints: the refinement is monotone and cannot oscillate, and stopping early
* at `maxIterations` leaves it on the permissive side rather than at an arbitrary point. A declaring package that
* is not among the `targets` is not being guessed, so its {@link Package.resolvedVersion} stands in.
*/
refineTransitive(targets: readonly string[], maxIterations?: number): Map<string, TransitiveConstraint[]>;
}
/** the outcome of {@link enforceArcConsistency}: the pruned version sets, and which partner blocked each package */
export interface ArcConsistency {
readonly survivors: Map<string, TimelineEntry[]>;
/** per package, the partner that rejected a version and the requirement it could not meet */
readonly blockers: Map<string, Map<string, string>>;
}
/** drop, to a fixpoint, the versions of each package that no co-guessed dependency can satisfy */
export declare function enforceArcConsistency(space: VersionSpace, initial: ReadonlyMap<string, TimelineEntry[]>, maxIterations?: number): ArcConsistency;
/** a package (or a linked group sharing one version) and its surviving versions, one factor of the combination count */
export interface CountFactor {
readonly name: string;
readonly survivors: readonly string[];
}
/** two packages whose version choices are not independent, and whether that holds for all of their versions */
export interface VersionCoupling {
readonly a: string;
readonly b: string;
/** `false` when only *some* versions of the two require each other, so the coupling does not always apply */
readonly always: boolean;
/** whether the coupling was counted; a coupling closing a cycle in the graph is dropped from the count */
readonly counted: boolean;
}
/** the result of {@link countRunnableCombinations} */
export interface CountedCombinations {
/** the number of runnable version tuples; an upper bound when {@link couplings} contains uncounted entries */
readonly total: number;
/** every coupling found between the counted factors */
readonly couplings: readonly VersionCoupling[];
}
/**
* The runnable-combination count: how many version tuples satisfy the requirements the factors place on each other.
* A requirement need not hold for every version of the declaring package (`A 0.2.5` may pin `B` to `0.2.1` while
* `A 0.3.0` pins it to `0.3.2`), so the two are counted as *coupled* rather than as independent factors: a version
* of `A` only ever multiplies in the versions of `B` it actually admits.
*
* Counting all couplings exactly is #CSP-hard, so they are counted over a spanning forest of the coupling graph,
* preferring the couplings that always apply. That is exact whenever the graph has no cycle, which covers the common
* shared-hub shape. A coupling that would close a cycle is dropped and reported as uncounted, leaving the result an
* upper bound. Only the forest's couplings need their compatibility matrix, so the work stays linear in the factors.
*/
export declare function countRunnableCombinations(space: VersionSpace, factors: readonly CountFactor[]): CountedCombinations;
/** an empty set, shared so callers that do not disable anything need not allocate one */
export declare const NoDisabledSources: ReadonlySet<ConstraintSource>;
/**
* Apply every constraint (declared, transitive, base-R, date, then signature usage) to a package's timeline. When an
* `observe` callback is given, emits the provenance of each constraint (including the signature lower bounds).
* Prefer {@link VersionSpace.survivors}, which is the same call with the shared inputs already bound.
*/
export declare function survivingEntries(space: VersionSpace, name: string, transitive: readonly TransitiveConstraint[], observe?: ConstraintObserver): SurvivingEntries;
/** the sigdb package a target's version history is drawn from: `R` reuses `base` (their releases coincide), everything else is itself */
export declare function timelinePackageKey(name: string, sources: readonly PackageSignatureSource[]): string;
/** the ordered candidate list for one package, or `undefined` when nothing survives (shared by the query and the iterator) */
export declare function orderedCandidatesOf(src: PackageSignatureSource | undefined, name: string, surviving: SurvivingEntries, prefer: string | undefined, order: 'newest' | 'oldest'): OrderedCandidates | undefined;
/** the default explosion targets: every declared and used dependency (excluding `current`, the analyzed package's own namespace) */
export declare function defaultTargets(deps: ReadOnlyFlowrAnalyzerDependenciesContext, usage: ReadonlyMap<string, PackageUsage>): string[];
/** the requirements one concrete version declares, as {@link coInstallability} reads them */
export type DependencyResolver = (pkg: string, version: VersionString) => readonly ResolvedDependency[] | undefined;
/**
* Whether the chosen versions can be loaded together, checking every requirement the chosen versions declare
* against the version chosen for the required package (R itself against the chosen base-R version). A requirement
* on a package the assignment does not choose cannot be settled here and is reported as
* {@link VersionAssignment.unverified} instead of silently passing, as `library()` still enforces it.
*/
export declare function coInstallability(versions: ReadonlyMap<string, VersionString>, declares: DependencyResolver): {
ok: boolean;
unverified?: readonly string[];
};
/**
* Lazily yield concrete version assignments (one version per package) in odometer order over the per-package lists.
* With `declares`, a combination whose versions cannot be loaded together is skipped rather than proposed; `limit`
* bounds the combinations *considered* either way, so fewer than `limit` assignments may come out.
*/
export declare function assignmentsOf(perPackage: readonly OrderedCandidates[], limit: number, declares?: DependencyResolver): Generator<VersionAssignment>;
/** the {@link DependencyResolver} backed by the signature database a version space resolves each package in */
export declare function declaredDependenciesOf(space: VersionSpace): DependencyResolver;
/**
* Explode the guessed constraint space into concrete, signature-database-available version assignments: a lazy
* iterator over one chosen version per resolvable dependency. Each package's versions are ordered by preference
* (an explicitly {@link VersionExplodeOptions.prefer|preferred} version, then non-archived releases, then newest or
* oldest first), so the first assignments are the most preferred. The iterator is bounded by
* {@link VersionExplodeOptions.limit} so an enormous product cannot run away.
*/
export declare function explodeDependencyVersions(analyzer: ReadonlyFlowrAnalysisProvider, options?: VersionExplodeOptions): AsyncGenerator<VersionAssignment>;
export {};