Files
renovate/lib/modules/versioning/gradle/index.ts
RahulGautamSingh 96ad4beb6a fix(versioning): use BigInt to avoid integer precision loss in gradle, maven and loose (#43901)
* fix(versioning): use BigInt to avoid integer precision loss in gradle, maven and loose

* fix(versioning): preserve exact update types

* Update lib/modules/versioning/maven/index.ts

* fix(versioning): satisfy func-style and prefer-at lint rules

* treat missing loose version section as zero in isSame

* fix coverage

* revert unrelated custom-manager rangeStrategy change

---------

Co-authored-by: Sergei Zharinov <zharinov@users.noreply.github.com>
2026-09-22 17:41:11 +00:00

320 lines
7.5 KiB
TypeScript

import { isBigint } from '@sindresorhus/is';
import type { RangeStrategy } from '../../../types/versioning.ts';
import { regEx } from '../../../util/regex.ts';
import mavenVersion from '../maven/index.ts';
import type { NewValueConfig, VersioningApi } from '../types.ts';
import {
TokenType,
compare,
isValid,
isVersion,
parse,
parseMavenBasedRange,
parsePrefixRange,
parseSingleVersionRange,
} from './compare.ts';
export const id = 'gradle';
export const displayName = 'Gradle';
export const urls = [
'[Gradle version ordering](https://docs.gradle.org/current/userguide/single_versions.html#version_ordering)',
];
export const supportsRanges = true;
export const supportedRangeStrategies: RangeStrategy[] = ['bump'];
const sectionIndexes = {
major: 0,
minor: 1,
patch: 2,
} as const;
function equals(a: string, b: string): boolean {
return compare(a, b) === 0;
}
function getMajor(version: string): number | null {
const tokens = parse(version?.replace(regEx(/^v/i), ''));
if (tokens) {
const majorToken = tokens?.[0];
if (majorToken?.type === TokenType.Number) {
return parseInt(majorToken.val.toString(), 10);
}
}
return null;
}
function getMinor(version: string): number | null {
const tokens = parse(version?.replace(regEx(/^v/i), ''));
if (tokens) {
const majorToken = tokens[0];
const minorToken = tokens[1];
if (
majorToken?.type === TokenType.Number &&
minorToken?.type === TokenType.Number
) {
return parseInt(minorToken.val.toString(), 10);
}
return 0;
}
return null;
}
function getPatch(version: string): number | null {
const tokens = parse(version?.replace(regEx(/^v/i), ''));
if (tokens) {
const majorToken = tokens[0];
const minorToken = tokens[1];
const patchToken = tokens[2];
if (
majorToken?.type === TokenType.Number &&
minorToken?.type === TokenType.Number &&
patchToken?.type === TokenType.Number
) {
return parseInt(patchToken.val.toString(), 10);
}
return 0;
}
return null;
}
function isGreaterThan(a: string, b: string): boolean {
return compare(a, b) === 1;
}
function getExactSection(
version: string,
type: keyof typeof sectionIndexes,
): bigint | null {
const tokens = parse(version.replace(regEx(/^v/i), ''));
if (!tokens) {
return null;
}
const sectionIndex = sectionIndexes[type];
if (sectionIndex === 0) {
const token = tokens[sectionIndex];
return token?.type === TokenType.Number && isBigint(token.val)
? token.val
: null;
}
for (let index = 0; index <= sectionIndex; index += 1) {
if (tokens[index]?.type !== TokenType.Number) {
return 0n;
}
}
return BigInt(tokens[sectionIndex].val);
}
function isSame(
type: keyof typeof sectionIndexes,
a: string,
b: string,
): boolean {
if (!(isVersion(a) && isVersion(b))) {
return false;
}
return getExactSection(a, type) === getExactSection(b, type);
}
const unstable = new Set([
'dev',
'a',
'alpha',
'b',
'beta',
'm',
'mt',
'milestone',
'rc',
'cr',
'preview',
'snapshot',
]);
function isStable(version: string): boolean {
const tokens = parse(version);
if (tokens) {
for (const token of tokens) {
if (token.type === TokenType.String) {
const val = token.val.toString().toLowerCase();
if (unstable.has(val)) {
return false;
}
}
}
return true;
}
return false;
}
function matches(a: string, b: string): boolean {
const versionTokens = parse(a);
if (!a || !versionTokens || !b) {
return false;
}
if (isVersion(b)) {
return equals(a, b);
}
const singleVersionRange = parseSingleVersionRange(b);
if (singleVersionRange) {
const { val } = singleVersionRange;
return equals(a, val);
}
const prefixRange = parsePrefixRange(b);
if (prefixRange) {
const tokens = prefixRange.tokens;
if (tokens.length === 0) {
return true;
}
const x = versionTokens
.slice(0, tokens.length)
.map(({ val }) => val)
.join('.');
const y = tokens.map(({ val }) => val).join('.');
return equals(x, y);
}
const mavenBasedRange = parseMavenBasedRange(b);
if (!mavenBasedRange) {
return false;
}
const { leftBound, leftVal, rightBound, rightVal } = mavenBasedRange;
let leftResult = true;
let rightResult = true;
if (leftVal) {
leftResult =
leftBound === 'exclusive'
? compare(leftVal, a) === -1
: compare(leftVal, a) !== 1;
}
if (rightVal) {
rightResult =
rightBound === 'exclusive'
? compare(a, rightVal) === -1
: compare(a, rightVal) !== 1;
}
return leftResult && rightResult;
}
function getSatisfyingVersion(
versions: string[],
range: string,
): string | null {
return versions.reduce((result: string | null, version) => {
if (matches(version, range)) {
if (!result) {
return version;
}
if (isGreaterThan(version, result)) {
return version;
}
}
return result;
}, null);
}
function minSatisfyingVersion(
versions: string[],
range: string,
): string | null {
return versions.reduce((result: string | null, version) => {
if (matches(version, range)) {
if (!result) {
return version;
}
if (compare(version, result) === -1) {
return version;
}
}
return result;
}, null);
}
function getNewValue({
currentValue,
rangeStrategy,
newVersion,
}: NewValueConfig): string | null {
if (isVersion(currentValue)) {
return newVersion;
}
// Check if our version is of the form "1.2.+"
const prefixRange = parsePrefixRange(currentValue);
const parsedNewVersion = parse(newVersion);
if (prefixRange && parsedNewVersion) {
if (prefixRange.tokens.length > 0) {
if (prefixRange.tokens.length <= parsedNewVersion.length) {
const newPrefixed = prefixRange.tokens
.map((_, i) => parsedNewVersion[i].val)
.join('.');
return `${newPrefixed}.+`;
}
// our new version is shorter than our prefix range so drop our prefix range
return newVersion;
}
// our version is already "+" which includes ever version
return null;
}
const mavenRange = parseMavenBasedRange(currentValue);
if (mavenRange?.preferredVal) {
const { leftVal, rightVal, preferredVal } = mavenRange;
const baseRange = currentValue.slice(
0,
currentValue.lastIndexOf(`!!${preferredVal}`),
);
const newBaseRange = mavenVersion.getNewValue({
currentValue: baseRange,
rangeStrategy,
newVersion,
});
// v8 ignore if: the implementation has a non-null return type
if (newBaseRange === null) {
return null;
}
const preferredIsBoundary =
preferredVal === leftVal || preferredVal === rightVal;
const newParsed = parseMavenBasedRange(newBaseRange);
const preferredStillPresent =
newParsed?.leftVal === preferredVal ||
newParsed?.rightVal === preferredVal;
const newPreferredVal =
preferredIsBoundary && !preferredStillPresent ? newVersion : preferredVal;
return `${newBaseRange}!!${newPreferredVal}`;
}
return mavenVersion.getNewValue({ currentValue, rangeStrategy, newVersion });
}
export const api: VersioningApi = {
equals,
getMajor,
getMinor,
getPatch,
isCompatible: isVersion,
isGreaterThan,
isSingleVersion: isVersion,
isSame,
isStable,
isValid,
isVersion,
matches,
getSatisfyingVersion,
minSatisfyingVersion,
getNewValue,
sortVersions: compare,
};
export default api;