UNPKG

@eagleoutice/flowr-dev

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

335 lines (334 loc) 23.3 kB
/** * 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 {};