@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
213 lines • 9.11 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RRange = exports.RVersion = void 0;
exports.rReleaseDate = rReleaseDate;
const semver_1 = require("semver");
function makeVersion(version, original) {
const semver = new semver_1.SemVer(version);
semver.str = original;
return semver;
}
function tryNormalizeNumberPart(part) {
try {
const res = Number(part);
if (!Number.isNaN(res)) {
return String(res);
}
}
catch {
return part;
}
return part;
}
function normalizeVersion(version) {
version = version.trim();
let comparator = '';
// extract comparator if present
const comparatorMatch = version.match(/^([<>=~^]+)/);
if (comparatorMatch) {
comparator = comparatorMatch[1].replace('==', '=');
version = version.slice(comparatorMatch[1].length).trim();
}
// R separates version components by `.` or `-` alike (`7.3-65` is `7.3.65`, and above `7.3.0`), so a numeric
// part is a component wherever it stands; only a non-numeric tail is a SemVer pre-release
const parts = version.split(/[.-]/).filter(p => p.length > 0);
let numeric = 0;
while (numeric < parts.length && /^\d+$/.test(parts[numeric])) {
numeric++;
}
const mainVersionParts = parts.slice(0, numeric);
const preReleaseParts = parts.slice(numeric);
if (mainVersionParts.length > 3) {
const newPreReleasePart = mainVersionParts.splice(3).join('.');
preReleaseParts.unshift(newPreReleasePart);
}
else {
while (mainVersionParts.length < 3) {
mainVersionParts.push('0');
}
}
let prerelease = '';
if (preReleaseParts.length > 0) {
prerelease = '-' + preReleaseParts.join('-').split('.').map(part => tryNormalizeNumberPart(part)).join('.');
}
return comparator + mainVersionParts.map(tryNormalizeNumberPart).join('.') + prerelease;
}
const AnyVerWithMaybeRangeRegex = /(\s*[<>=~^]*\s*\d+(\.\d*)*(-[0-9A-Za-z-.]+)?)/g;
function normalizeVersions(versions) {
const parts = [];
// extract all version-like parts and normalize them individually
let match;
let index = 0;
while ((match = AnyVerWithMaybeRangeRegex.exec(versions)) !== null) {
const prefix = versions.slice(index, match.index);
if (prefix.length > 0) {
parts.push(prefix);
}
const versionPart = match[1];
parts.push(normalizeVersion(versionPart));
index = match.index + versionPart.length;
}
if (index < versions.length) {
parts.push(versions.slice(index));
}
return parts.join('');
}
function makeRange(range, original) {
const semverRange = new semver_1.Range(range);
semverRange.str = original;
return semverRange;
}
/** Helpers for R package versions (`1.2-3` style), which are freer than SemVer. */
exports.RVersion = {
name: 'RVersion',
/**
* Parse an R version string into a {@link SemVer}, normalizing R's freer scheme (e.g. `0.4-9`). Unlike
* `new SemVer(version)` this coerces where needed; the original string is available via `.str`. Returns
* undefined (never throws) when the string cannot be coerced to a version at all.
* @see {@link RVersion.parseOrZero}
*/
parse(version) {
try {
return makeVersion(version, version);
}
catch { /* try to normalize */ }
try {
return makeVersion(normalizeVersions(version), version);
}
catch { /* fall back to coercion */ }
// like {@link RRange.parse}, this never throws: a truly un-coercible version (e.g. an empty `Version:`
// field or pure text) yields undefined so the caller can detect it rather than crashing
const coerced = (0, semver_1.coerce)(version, { loose: true, includePrerelease: true });
return coerced === null ? undefined : makeVersion(coerced.version, version);
},
/**
* Compare two R version strings following R's `numeric_version` scheme: split on `.` and `-`, compare
* numerically, shorter versions padded with zeros (`0.4-9 < 0.4.10 < 1.0`). Negative/zero/positive for
* `a` less than/equal to/greater than `b`.
*/
compare(a, b) {
// an absent version sorts below any concrete one (so ascending order puts `undefined` first, descending last)
if (a === undefined || b === undefined) {
return a === b ? 0 : a === undefined ? -1 : 1;
}
const pa = a.split(/[.-]/), pb = b.split(/[.-]/);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const x = Number(pa[i] ?? 0) || 0, y = Number(pb[i] ?? 0) || 0;
if (x !== y) {
return x - y;
}
}
return 0;
},
/**
* Like {@link parse} but never undefined: a truly un-coercible version sorts as `0.0.0` while keeping its
* original string as `.str`, so it stays usable as a sort key.
* @see {@link RVersion.parse}
*/
parseOrZero(version) {
const parsed = exports.RVersion.parse(version);
if (parsed !== undefined) {
return parsed;
}
const fallback = exports.RVersion.parse('0.0.0'); // `0.0.0` always parses
fallback.str = version;
return fallback;
},
/** The highest of some version strings by R's `numeric_version` order (`0.9.0 < 0.10.0`); undefined if none. */
highest(versions) {
let best;
for (const v of versions) {
if (best === undefined || exports.RVersion.compare(v, best) > 0) {
best = v;
}
}
return best;
}
};
/** release dates of R minor versions (`major.minor`); base R is stored undated in the sigdb, so this date-bounds it */
const RReleaseDates = {
'4.5': '2025-04-11', '4.4': '2024-04-24', '4.3': '2023-04-21', '4.2': '2022-04-22',
'4.1': '2021-05-18', '4.0': '2020-04-24', '3.6': '2019-04-26', '3.5': '2018-04-23',
'3.4': '2017-04-21', '3.3': '2016-05-03', '3.2': '2015-04-16', '3.1': '2014-04-10',
'3.0': '2013-04-03', '2.15': '2012-03-30', '2.14': '2011-10-31', '2.13': '2011-04-13'
};
/** the release date of an R version, matched by its `major.minor`, or `undefined` for versions older than the table */
function rReleaseDate(version) {
const m = /^(\d+)\.(\d+)/.exec(version);
const iso = m ? RReleaseDates[`${m[1]}.${m[2]}`] : undefined;
return iso ? new Date(iso) : undefined;
}
/** Helpers for R package version ranges (DESCRIPTION constraints like `>= 0.4-9`). */
exports.RRange = {
name: 'RRange',
/**
* Parse an R version range string into a {@link Range}, normalizing R's freer scheme. The original range
* string is available via `.str`.
*
* This never throws: an unparseable constraint (e.g. `>= abc`, or a git/URL "version" from a lockfile) yields
* `undefined` rather than aborting the caller, so a malformed constraint is *detectable* and simply contributes
* no version bound. Parsing is attempted first verbatim, then after normalizing R's scheme to SemVer.
*/
parse(range) {
try {
return makeRange(range, range);
}
catch { /* try to normalize R range to SemVer */ }
try {
return makeRange(normalizeVersions(range), range);
}
catch { /* unparseable even after normalization -> report as "no constraint" */ }
return undefined;
},
/**
* Whether an R version string satisfies a range constraint, normalizing R's freer scheme on both sides.
* `range` may be a parsed {@link Range} or a raw constraint string. Returns `false` if either is unparseable
* (never throws), so a malformed version/constraint simply does not match.
*/
satisfies(version, range) {
const v = exports.RVersion.parse(version);
if (v === undefined) {
return false;
}
const r = typeof range === 'string' ? exports.RRange.parse(range) : range;
return r !== undefined && (0, semver_1.satisfies)(v, r, { loose: true, includePrerelease: true });
},
/**
* The range every one of `ranges` allows (semver reads a space-separated list as a conjunction). `undefined` if
* none were given or the combination is unparseable; it may still be a range no version satisfies, which
* {@link RVersion.parse|minVersion} reports.
*/
intersect(ranges) {
return ranges.length > 0 ? exports.RRange.parse(ranges.map(r => r.raw).join(' ')) : undefined;
},
/** how a set of alternative requirements reads, in semver's `a || b` form (for reporting, not for parsing back) */
formatAlternatives(ranges) {
return ranges.map(r => r.raw).join(' || ');
},
/** whether `version` satisfies at least one of `ranges` (an empty list constrains nothing, so it does) */
satisfiesAny(version, ranges) {
return ranges.length === 0 || ranges.some(r => exports.RRange.satisfies(version, r));
}
};
//# sourceMappingURL=r-version.js.map