mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 17:51:49 +00:00
3412de84ce
* refactor(lint): enable oxlint prefer-named-capture-group at error Numbered capture groups (m[1], $1) break silently when a regex is edited and group order shifts. Named groups (?<name>...) make intent explicit and accesses (m.groups.name) resilient to reordering. This enables the built-in eslint rule prefer-named-capture-group, surfacing 321 pre-existing unnamed-group violations across ~121 files to be burned down incrementally in follow-up commits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(versioning): name capture groups for prefer-named-capture-group Burns down all prefer-named-capture-group violations in lib/modules/versioning/. Unused capture groups (only reachable via .test() or whole-match) become non-capturing (?:...); groups whose value is read become named (?<name>...), with accesses rewritten to match.groups!.<name> (or destructured from .groups!) instead of positional match[n]/$n so a future edit that reorders groups cannot silently break the reader. Two cases needed extra care to preserve behavior: - semver/common.ts and poetry/patterns.ts split() on a regex whose capture group is relied upon by String.prototype.split to keep the separator in the output array; the group is kept capturing and named, not converted to non-capturing. - hashicorp/convertor.ts's semverRegex is only ever embedded via .source into a named `(?<version>...)` group elsewhere and never matched directly, so its internal groups are safe to make non-capturing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(datasource): name capture groups for prefer-named-capture-group Burns down all prefer-named-capture-group violations under lib/modules/datasource/. Same treatment as the versioning module: unused groups become non-capturing, used groups get names, and positional match[n] reads move to match.groups!.<name>. docker/ecr.ts and pypi/common.ts keep their positional destructuring (`const [, region] = ...`, `!!m &&`) working unchanged since a named capture group is still addressable by its numeric index in JavaScript — only pypi/common.ts's access was rewritten to m.groups!.owner for clarity since it read a single group directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(platform): name capture groups for prefer-named-capture-group Burns down all prefer-named-capture-group violations under lib/modules/platform/. Uses the repo's established idiom of `match.groups?.<name>` after an optional-chained exec()/match() (see bitbucket/index.ts hostnameWithoutApiPrefix, gerrit/utils.ts extractSourceBranch) and `$<name>` in String.replace replacement strings. Unused groups (markdown/HTML massaging regexes matched only to be stripped) become non-capturing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(manager): name capture groups in ansible-galaxy for prefer-named-capture-group Burns down prefer-named-capture-group violations in lib/modules/manager/ansible-galaxy/. newBlockRegEx and blockLineRegEx dropped their unused outer wrapping group (only the inner key/value groups were ever read) and named the key/value groups; nameMatchRegex's remaining unused groups (protocol/user-info/port alternatives) become non-capturing since only source/hostname/depName/version are read via .groups. Accesses in collections.ts and roles.ts move from lineMatch[2]/lineMatch[3] to lineMatch.groups!.key/value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(manager): name capture groups for prefer-named-capture-group (batch 1) Burns down prefer-named-capture-group violations across the first alphabetical slice of lib/modules/manager/ (ansible through mix). Used groups get names with accesses rewritten to match.groups!.<name> (or $<name> in replacement strings); groups whose value is never read become non-capturing (?:...). Two String.split() call sites (git-submodules/extract.ts, meteor/extract.ts) keep the established "named group still readable positionally from the split() result array" idiom instead of restructuring the destructuring. bazel/rules/go.ts and buildpacks/extract.ts dockerRef verified byte-for-byte equivalent before/after via a quick node script since several nested groups collapsed to non-capturing at once. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(manager): name capture groups for prefer-named-capture-group (batch 2) Burns down prefer-named-capture-group violations across the remaining alphabetical slice of lib/modules/manager/ (npm through terragrunt). Same treatment as batch 1: used groups get names with accesses moved to match.groups!.<name> or $<name> replacement strings; unused groups (wrapping alternations, quantifier repetition groups only consumed via .test(), or values never read from a match) become non-capturing. terraform/extractors/others/modules.ts and terragrunt/modules.ts share near-identical URL-parsing regexes (github/bitbucket/git/azure-devops ref matchers) — the same set of always-unused `depth=`/`&depth=` query-string groups is converted to non-capturing in both files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(config): name capture groups for prefer-named-capture-group Burns down prefer-named-capture-group violations in lib/config/. Used groups get names with replacement strings switched from $1/$2 to $<name>; the unused separator group in schedule-migration.ts's afterBeforeRegex becomes non-capturing. presets/parse.ts keeps assigning through the already-declared `repo` variable rather than array-destructuring the exec() result, matching this file's existing style for the other named-group regexes just above it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(util): name capture groups for prefer-named-capture-group Burns down prefer-named-capture-group violations across lib/util/ and lib/logger/. Same treatment as previous batches: named groups for values that are read (via $<name> replacement strings, .groups!.<name>, or destructured from .groups), non-capturing groups where nothing reads the captured value (e.g. semantic.ts's angular-commit .test()-only regex). www-authenticate.ts's tokenizer entries each get their own named group but keep accessing the match generically via match[1], since a positionally-indexed named group is still readable by index. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(workers): name capture groups for prefer-named-capture-group Burns down prefer-named-capture-group violations in lib/workers/. Used groups get names with $<name> replacement strings; unused wrapper/alternation groups (e.g. semantic-commit-message.ts's optional scope/issue wrappers, release-notes.ts's date-heading and horizontal-rule matchers that are only probed with .search()/replaced wholesale) become non-capturing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * refactor(test,tools): name capture groups for prefer-named-capture-group Finishes the prefer-named-capture-group burndown: test/, tools/, and vitest.config.mts. Groups whose value is read get names ($<name> replacement strings or .groups.<name> access); groups only used via .test()/.search() or that feed a .match(/…/g) call (whose return value is the array of whole matches, not captured groups) become non-capturing. This is the last remaining slice of pre-existing violations, so the rule (already enabled at error repo-wide) now passes with zero findings across the whole codebase. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RxHFNsSworLcv23xXk9Hk * fix prettier failure * refactor(lint): use a named capture group in `v8-ignore-no-count` New rule from main trips the newly enabled `prefer-named-capture-group` oxlint rule. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
577 lines
16 KiB
TypeScript
577 lines
16 KiB
TypeScript
import { gte, lt, lte, satisfies } from '@renovatebot/pep440';
|
|
import { parse as parseRange } from '@renovatebot/pep440/lib/specifier.js';
|
|
import { parse as parseVersion } from '@renovatebot/pep440/lib/version.js';
|
|
import { isTruthy } from '@sindresorhus/is';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import { coerceArray } from '../../../util/array.ts';
|
|
import { regEx } from '../../../util/regex.ts';
|
|
import type { NewValueConfig } from '../types.ts';
|
|
|
|
const UserPolicyPrecisionMap = {
|
|
Major: 0,
|
|
Minor: 1,
|
|
Micro: 2,
|
|
Bug: 3,
|
|
None: Infinity,
|
|
} as const;
|
|
const PrecisionUserPolicyMap = {
|
|
0: 'Major',
|
|
1: 'Minor',
|
|
2: 'Micro',
|
|
3: 'Bug',
|
|
} as const;
|
|
|
|
type UserPolicy =
|
|
(typeof UserPolicyPrecisionMap)[keyof typeof UserPolicyPrecisionMap];
|
|
|
|
/**
|
|
* Calculate current update range precision.
|
|
* @param ranges A {@link Range} consists of current range
|
|
* @returns A {@link UserPolicy}
|
|
*/
|
|
function getRangePrecision(ranges: Range[]): UserPolicy {
|
|
const bound = coerceArray(
|
|
parseVersion((ranges[1] || ranges[0]).version)?.release,
|
|
);
|
|
let rangePrecision = -1;
|
|
// range is defined by a single bound.
|
|
// ie. <1.2.2.3,
|
|
// >=7
|
|
if (ranges.length === 1) {
|
|
rangePrecision = bound.length - 1;
|
|
}
|
|
// Range is defined by both upper and lower bounds.
|
|
if (ranges.length === 2) {
|
|
const lowerBound = coerceArray(parseVersion(ranges[0].version)?.release);
|
|
rangePrecision = bound.findIndex((el, index) => el > lowerBound[index]);
|
|
}
|
|
// Tune down Major precision if followed by a zero
|
|
if (
|
|
rangePrecision === UserPolicyPrecisionMap.Major &&
|
|
rangePrecision + 1 < bound.length &&
|
|
bound[rangePrecision + 1] === 0
|
|
) {
|
|
rangePrecision++;
|
|
}
|
|
// Could not calculate user precision
|
|
// Default to the smallest possible
|
|
// istanbul ignore next
|
|
if (rangePrecision === -1) {
|
|
rangePrecision = bound.length - 1;
|
|
}
|
|
const key =
|
|
PrecisionUserPolicyMap[
|
|
rangePrecision as keyof typeof PrecisionUserPolicyMap
|
|
];
|
|
return UserPolicyPrecisionMap[key as keyof typeof UserPolicyPrecisionMap];
|
|
}
|
|
|
|
/**
|
|
* @param policy Required range precision
|
|
* @param newVersion The newly accepted version
|
|
* @param baseVersion Optional Current upper bound
|
|
* @returns A string represents a future version upper bound.
|
|
*/
|
|
function getFutureVersion(
|
|
policy: UserPolicy,
|
|
newVersion: string,
|
|
baseVersion?: string,
|
|
): number[] {
|
|
const toRelease = coerceArray(parseVersion(newVersion)?.release);
|
|
const baseRelease = coerceArray(
|
|
parseVersion(baseVersion ?? newVersion)?.release,
|
|
);
|
|
return baseRelease.map((_, index) => {
|
|
const toPart = toRelease[index] ?? 0;
|
|
if (index < policy) {
|
|
return toPart;
|
|
}
|
|
if (index === policy) {
|
|
return toPart + (baseVersion === undefined ? 0 : 1);
|
|
}
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
interface Range {
|
|
operator: string;
|
|
prefix?: string;
|
|
version: string;
|
|
}
|
|
|
|
export function getPinnedValue(newVersion: string): string {
|
|
return `==${newVersion}`;
|
|
}
|
|
|
|
export function getNewValue({
|
|
currentValue,
|
|
rangeStrategy,
|
|
currentVersion,
|
|
newVersion,
|
|
isReplacement,
|
|
}: NewValueConfig): string | null {
|
|
let ranges: Range[];
|
|
let updatedRange: (string | null)[];
|
|
|
|
// no symbol: accept only that specific version specified
|
|
if (currentValue === currentVersion || isReplacement) {
|
|
return newVersion;
|
|
}
|
|
|
|
// Handle bare versions (e.g., "v0.7.15") that don't strictly equal
|
|
// currentVersion due to normalization, treating them as pinned while
|
|
// preserving the v-prefix.
|
|
if (parseVersion(currentValue)) {
|
|
const vPrefix = regEx(/^(?<prefix>[vV])/).exec(currentValue);
|
|
if (vPrefix) {
|
|
return `${vPrefix.groups!.prefix}${newVersion}`;
|
|
}
|
|
return newVersion;
|
|
}
|
|
|
|
try {
|
|
ranges = parseCurrentRange(currentValue);
|
|
if (!ranges.length) {
|
|
// an empty string is an allowed value for PEP440 range
|
|
// it means get any version
|
|
logger.warn({ currentValue }, 'Empty currentValue');
|
|
return currentValue;
|
|
}
|
|
} catch (err) {
|
|
logger.warn({ currentValue, err }, 'Unexpected range error');
|
|
return null;
|
|
}
|
|
|
|
// return early if newVersion is excluded from range
|
|
if (
|
|
ranges.some(
|
|
(range) => range.operator === '!=' && range.version === newVersion,
|
|
)
|
|
) {
|
|
logger.debug(
|
|
`Cannot calculate new value as the newVersion:\`${newVersion}\` is excluded from range: \`${currentValue}\``,
|
|
);
|
|
return null;
|
|
}
|
|
|
|
switch (rangeStrategy) {
|
|
case 'auto':
|
|
case 'replace':
|
|
updatedRange = handleReplaceStrategy(
|
|
{
|
|
currentValue,
|
|
rangeStrategy,
|
|
currentVersion,
|
|
newVersion,
|
|
},
|
|
ranges,
|
|
);
|
|
break;
|
|
case 'widen':
|
|
updatedRange = handleWidenStrategy(
|
|
{
|
|
currentValue,
|
|
rangeStrategy,
|
|
currentVersion,
|
|
newVersion,
|
|
},
|
|
ranges,
|
|
);
|
|
break;
|
|
case 'bump':
|
|
updatedRange = handleBumpStrategy(
|
|
{
|
|
currentValue,
|
|
rangeStrategy,
|
|
currentVersion,
|
|
newVersion,
|
|
},
|
|
ranges,
|
|
);
|
|
break;
|
|
default:
|
|
// Unsupported rangeStrategy
|
|
// Valid rangeStrategy values are: bump, extend, pin, replace.
|
|
// https://docs.renovatebot.com/modules/versioning/#pep440-versioning
|
|
logger.debug(
|
|
`Unsupported rangeStrategy: ${rangeStrategy}. Using "replace" instead.`,
|
|
);
|
|
return getNewValue({
|
|
currentValue,
|
|
rangeStrategy: 'replace',
|
|
currentVersion,
|
|
newVersion,
|
|
});
|
|
}
|
|
|
|
let result = updatedRange.filter(isTruthy).join(', ');
|
|
|
|
if (result.includes(', ') && !currentValue.includes(', ')) {
|
|
result = result.replace(regEx(/, /g), ',');
|
|
}
|
|
const checkedResult = checkRangeAndRemoveUnnecessaryRangeLimit(
|
|
result,
|
|
newVersion,
|
|
);
|
|
|
|
// istanbul ignore if
|
|
if (!satisfies(newVersion, checkedResult)) {
|
|
// we failed at creating the range
|
|
logger.warn(
|
|
{ result, newVersion, currentValue },
|
|
'pep440: failed to calculate newValue',
|
|
);
|
|
return null;
|
|
}
|
|
return checkedResult;
|
|
}
|
|
|
|
export function isLessThanRange(input: string, range: string): boolean {
|
|
try {
|
|
let invertResult = true;
|
|
|
|
const results = range
|
|
.split(',')
|
|
.map((x) =>
|
|
x
|
|
.replace(regEx(/\s*/g), '')
|
|
.split(regEx(/(?<op>~=|==|!=|<=|>=|<|>|===)/))
|
|
.slice(1),
|
|
)
|
|
.map(([op, version]) => {
|
|
if (['!=', '<=', '<'].includes(op)) {
|
|
return true;
|
|
}
|
|
invertResult = false;
|
|
if (['~=', '==', '>=', '==='].includes(op)) {
|
|
return lt(input, version);
|
|
}
|
|
if (op === '>') {
|
|
return lte(input, version);
|
|
}
|
|
// istanbul ignore next
|
|
return false;
|
|
});
|
|
|
|
const result = results.every((res) => res === true);
|
|
|
|
return invertResult ? !result : result;
|
|
} catch /* istanbul ignore next */ {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function parseCurrentRange(currentValue: string): Range[] {
|
|
const ranges: Range[] = parseRange(currentValue);
|
|
if (!ranges) {
|
|
throw new TypeError('Invalid pep440 currentValue');
|
|
}
|
|
if (ranges.some((range) => range.operator === '===')) {
|
|
// the operator "===" is used for legacy non PEP440 versions
|
|
throw new TypeError('PEP440 arbitrary equality (===) not supported');
|
|
}
|
|
return ranges;
|
|
}
|
|
|
|
function handleLowerBound(range: Range, newVersion: string): string | null {
|
|
// used to mark minimum supported version
|
|
// lower the bound if the new version is lower than current range
|
|
if (['>', '>='].includes(range.operator)) {
|
|
if (lte(newVersion, range.version)) {
|
|
// this looks like a rollback
|
|
return `>=${newVersion}`;
|
|
}
|
|
// otherwise, treat it same as exclude
|
|
return range.operator + range.version;
|
|
}
|
|
// istanbul ignore next
|
|
return null;
|
|
}
|
|
|
|
function handleUpperBound(range: Range, newVersion: string): string | null {
|
|
// this is used to exclude future versions
|
|
if (range.operator === '<') {
|
|
// if newVersion is that future version
|
|
if (gte(newVersion, range.version)) {
|
|
// now here things get tricky
|
|
// we calculate the new future version
|
|
const precision = getRangePrecision([range]);
|
|
const futureVersion = getFutureVersion(
|
|
precision,
|
|
newVersion,
|
|
range.version,
|
|
);
|
|
return range.operator + futureVersion.join('.');
|
|
}
|
|
// newVersion is in range, for other than "replace" strategies
|
|
return range.operator + range.version;
|
|
}
|
|
// istanbul ignore next
|
|
return null;
|
|
}
|
|
|
|
function updateRangeValue(
|
|
{ currentValue, newVersion }: NewValueConfig,
|
|
range: Range,
|
|
): string | null {
|
|
// used to exclude versions,
|
|
// we assume that's for a good reason
|
|
if (range.operator === '!=') {
|
|
return range.operator + range.version;
|
|
}
|
|
|
|
// keep the .* suffix
|
|
if (range.prefix) {
|
|
const futureVersion = getFutureVersion(
|
|
UserPolicyPrecisionMap.None,
|
|
newVersion,
|
|
range.version,
|
|
).join('.');
|
|
return `${range.operator}${futureVersion}.*`;
|
|
}
|
|
if (range.operator === '~=') {
|
|
const baseVersion = coerceArray(parseVersion(range.version)?.release);
|
|
const futureVersion = coerceArray(parseVersion(newVersion)?.release);
|
|
const baseLen = baseVersion.length;
|
|
const newVerLen = futureVersion.length;
|
|
// trim redundant trailing version specifiers
|
|
if (baseLen < newVerLen) {
|
|
return (
|
|
range.operator + futureVersion.slice(0, baseVersion.length).join('.')
|
|
);
|
|
}
|
|
// pad with specifiers
|
|
if (baseLen > newVerLen) {
|
|
for (let i = baseLen - newVerLen - 1; i >= 0; i--) {
|
|
futureVersion.push(0);
|
|
}
|
|
return range.operator + futureVersion.join('.');
|
|
}
|
|
return range.operator + newVersion;
|
|
}
|
|
if (['==', '<='].includes(range.operator)) {
|
|
if (lte(newVersion, range.version)) {
|
|
return range.operator + range.version;
|
|
}
|
|
return range.operator + newVersion;
|
|
}
|
|
|
|
let output = handleUpperBound(range, newVersion);
|
|
if (output) {
|
|
// manged to update upperbound
|
|
// no need to try anything else
|
|
return output;
|
|
}
|
|
output = handleLowerBound(range, newVersion);
|
|
if (output) {
|
|
return output;
|
|
}
|
|
|
|
// unless PEP440 changes, this won't happen
|
|
// istanbul ignore next
|
|
logger.error(
|
|
{ newVersion, currentValue, range },
|
|
'pep440: failed to process range',
|
|
);
|
|
// istanbul ignore next
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Checks for zero specifiers.
|
|
* returns true if one of the bounds' length is < 3.
|
|
* @param ranges A {@link Range} array.
|
|
* @returns A boolean value
|
|
* Used mainly for cosmetics for the rez versioning syntax.
|
|
*/
|
|
function hasZeroSpecifier(ranges: Range[]): boolean {
|
|
return ranges.some((range) => {
|
|
const release = parseVersion(range.version)?.release;
|
|
return release && release.length < 3;
|
|
});
|
|
}
|
|
|
|
function trimTrailingZeros(numbers: number[]): number[] {
|
|
let i = numbers.length - 1;
|
|
while (numbers[i] === 0) {
|
|
i--;
|
|
}
|
|
return numbers.slice(0, i + 1);
|
|
}
|
|
|
|
function divideCompatibleReleaseRange(currentRange: Range): Range[] {
|
|
const currentVersionUpperBound = currentRange.version
|
|
.split('.')
|
|
.map((num) => parseInt(num, 10));
|
|
if (currentVersionUpperBound.length > 1) {
|
|
currentVersionUpperBound.splice(-1);
|
|
}
|
|
currentVersionUpperBound[currentVersionUpperBound.length - 1] += 1;
|
|
return [
|
|
{ operator: '>=', version: currentRange.version },
|
|
{
|
|
operator: '<',
|
|
version: currentVersionUpperBound.join('.'),
|
|
},
|
|
];
|
|
}
|
|
|
|
function handleWidenStrategy(
|
|
{ currentValue, rangeStrategy, currentVersion, newVersion }: NewValueConfig,
|
|
ranges: Range[],
|
|
): (string | null)[] {
|
|
// newVersion is within range
|
|
if (satisfies(newVersion, currentValue)) {
|
|
return [currentValue];
|
|
}
|
|
let rangePrecision = getRangePrecision(ranges);
|
|
const trimZeros = hasZeroSpecifier(ranges);
|
|
let newRanges: Range[] = [];
|
|
if (ranges.length === 1 && ranges[0].operator === '~=') {
|
|
// If range operator is '~=', divide the range into its logical equivalent.
|
|
// Example: ~=1.0 --> >=1.0,<2
|
|
newRanges = divideCompatibleReleaseRange(ranges[0]);
|
|
} else {
|
|
newRanges = ranges;
|
|
}
|
|
return newRanges.map((range) => {
|
|
// newVersion is over the upper bound
|
|
if (range.operator === '<' && gte(newVersion, range.version)) {
|
|
const upperBound = coerceArray(parseVersion(range.version)?.release);
|
|
const len = upperBound.length;
|
|
// Match the precision of the smallest specifier if other than 0
|
|
if (upperBound[len - 1] !== 0) {
|
|
const key =
|
|
PrecisionUserPolicyMap[
|
|
(len - 1) as keyof typeof PrecisionUserPolicyMap
|
|
];
|
|
rangePrecision =
|
|
UserPolicyPrecisionMap[key as keyof typeof UserPolicyPrecisionMap];
|
|
}
|
|
let futureVersion = getFutureVersion(
|
|
rangePrecision,
|
|
newVersion,
|
|
range.version,
|
|
);
|
|
if (trimZeros) {
|
|
futureVersion = trimTrailingZeros(futureVersion);
|
|
}
|
|
return range.operator + futureVersion.join('.');
|
|
}
|
|
// default
|
|
return updateRangeValue(
|
|
{
|
|
currentValue,
|
|
rangeStrategy,
|
|
currentVersion,
|
|
newVersion,
|
|
},
|
|
range,
|
|
);
|
|
});
|
|
}
|
|
|
|
function handleReplaceStrategy(
|
|
{ currentValue, rangeStrategy, currentVersion, newVersion }: NewValueConfig,
|
|
ranges: Range[],
|
|
): (string | null)[] {
|
|
// newVersion is within range
|
|
if (satisfies(newVersion, currentValue)) {
|
|
return [currentValue];
|
|
}
|
|
const trimZeros = hasZeroSpecifier(ranges);
|
|
return ranges.map((range) => {
|
|
// newVersion is over the upper bound
|
|
if (range.operator === '<' && gte(newVersion, range.version)) {
|
|
const rangePrecision = getRangePrecision(ranges);
|
|
let futureVersion = getFutureVersion(
|
|
rangePrecision,
|
|
newVersion,
|
|
range.version,
|
|
);
|
|
if (trimZeros) {
|
|
futureVersion = trimTrailingZeros(futureVersion);
|
|
}
|
|
return range.operator + futureVersion.join('.');
|
|
}
|
|
// handle lower bound
|
|
if (['>', '>='].includes(range.operator)) {
|
|
if (lte(newVersion, range.version)) {
|
|
// this looks like a rollback
|
|
return `>=${newVersion}`;
|
|
}
|
|
// update the lower bound to reflect the accepted new version
|
|
const lowerBound = coerceArray(parseVersion(range.version)?.release);
|
|
const rangePrecision = lowerBound.length - 1;
|
|
let newBase = getFutureVersion(rangePrecision, newVersion);
|
|
if (trimZeros) {
|
|
newBase = trimTrailingZeros(newBase);
|
|
}
|
|
// trim last element of the newBase when new accepted version is out of range.
|
|
// example: let new bound be >8.2.5 & newVersion be 8.2.5
|
|
// return value will be: >8.2
|
|
if (
|
|
range.operator === '>' &&
|
|
newVersion === newBase.join('.') &&
|
|
newBase.length > 1
|
|
) {
|
|
newBase.pop();
|
|
}
|
|
return range.operator + newBase.join('.');
|
|
}
|
|
// default
|
|
return updateRangeValue(
|
|
{
|
|
currentValue,
|
|
rangeStrategy,
|
|
currentVersion,
|
|
newVersion,
|
|
},
|
|
range,
|
|
);
|
|
});
|
|
}
|
|
|
|
function handleBumpStrategy(
|
|
{ currentValue, rangeStrategy, currentVersion, newVersion }: NewValueConfig,
|
|
ranges: Range[],
|
|
): (string | null)[] {
|
|
return ranges.map((range) => {
|
|
// bump lower bound to current new version
|
|
if (range.operator === '>=') {
|
|
return range.operator + newVersion;
|
|
}
|
|
return updateRangeValue(
|
|
{
|
|
currentValue,
|
|
rangeStrategy,
|
|
currentVersion,
|
|
newVersion,
|
|
},
|
|
range,
|
|
);
|
|
});
|
|
}
|
|
|
|
export function checkRangeAndRemoveUnnecessaryRangeLimit(
|
|
rangeInput: string,
|
|
newVersion: string,
|
|
): string {
|
|
let newRange: string = rangeInput;
|
|
if (rangeInput.includes(',')) {
|
|
const newRes = rangeInput.split(',');
|
|
if (
|
|
newRes[0].includes('.*') &&
|
|
newRes[0].includes('==') &&
|
|
newRes[1].includes('>=') &&
|
|
satisfies(newVersion, newRes[0])
|
|
) {
|
|
newRange = newRes[0];
|
|
}
|
|
} else {
|
|
return rangeInput;
|
|
}
|
|
|
|
return newRange;
|
|
}
|