From f2258258c792f25ac48a3eda2466a1be1161b452 Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Fri, 27 Sep 2024 11:07:14 -0600 Subject: [PATCH 01/16] feat(versioning): add PVP versioning scheme This initial version only supports ranges with form >=X.Y && { + describe('.isGreaterThan(version, other)', () => { + it('should return true', () => { + expect(pvp.isGreaterThan('1.23.1', '1.9.6')).toBeTrue(); + expect(pvp.isGreaterThan('4.0.0', '3.0.0')).toBeTrue(); + expect(pvp.isGreaterThan('3.0.1', '3.0.0')).toBeTrue(); + expect(pvp.isGreaterThan('4.10', '4.1')).toBeTrue(); + expect(pvp.isGreaterThan('1.0.0', '1.0')).toBeTrue(); + }); + + it('should return false', () => { + expect(pvp.isGreaterThan('2.0.2', '3.1.0')).toBeFalse(); // less + expect(pvp.isGreaterThan('3.0.0', '3.0.0')).toBeFalse(); // equal + expect(pvp.isGreaterThan('4.1', '4.10')).toBeFalse(); + expect(pvp.isGreaterThan('1.0', '1.0.0')).toBeFalse(); + }); + }); + + describe('.parse(range)', () => { + it('should parse >=1.0 && <1.1', () => { + const parsed = parse('>=1.0 && <1.1'); + expect(parsed).not.toBeNull(); + expect(parsed!.lower).toEqual('1.0'); + expect(parsed!.upper).toEqual('1.1'); + }); + }); + + describe('.getMajor(version)', () => { + it('should extract second component as decimal digit', () => { + expect(pvp.getMajor('1.0.0')).toEqual(1.0); + expect(pvp.getMajor('1.0.1')).toEqual(1.0); + expect(pvp.getMajor('1.1.1')).toEqual(1.1); + }); + }); + + describe('.getMinor(version)', () => { + it('should extract minor as third component in version', () => { + expect(pvp.getMinor('1.0')).toBeNull(); + expect(pvp.getMinor('1.0.0')).toEqual(0); + expect(pvp.getMinor('1.0.1')).toEqual(1); + expect(pvp.getMinor('1.1.2')).toEqual(2); + }); + }); + + describe('.getPatch(version)', () => { + it('should return null where there is no patch version', () => { + expect(pvp.getPatch('1.0.0')).toBeNull(); + }); + it('should extract all remaining components as decimal digits', () => { + expect(pvp.getPatch('1.0.0.5.1')).toEqual(5.1); + expect(pvp.getPatch('1.0.1.6')).toEqual(6); + expect(pvp.getPatch('1.1.2.7')).toEqual(7); + }); + }); + + describe('.matches(version, range)', () => { + it('should return true when version has same major', () => { + expect(pvp.matches('1.0.1', '>=1.0 && <1.1')).toBeTrue(); + expect(pvp.matches('4.1', '>=4.0 && <4.10')).toBeTrue(); + expect(pvp.matches('4.1', '>=4.1 && <4.10')).toBeTrue(); + expect(pvp.matches('4.1.0', '>=4.1 && <4.10')).toBeTrue(); + expect(pvp.matches('4.10', '>=4.1 && <4.10.0')).toBeTrue(); + expect(pvp.matches('4.10', '>=4.0 && <4.10.1')).toBeTrue(); + }); + + it('should return false when version has different major', () => { + expect(pvp.matches('1.0.0', '>=2.0 && <2.1')).toBeFalse(); + expect(pvp.matches('4', '>=4.0 && <4.10')).toBeFalse(); + expect(pvp.matches('4.10', '>=4.1 && <4.10')).toBeFalse(); + }); + }); + + describe('.getSatisfyingVersion(versions, range)', () => { + it('should return max satisfying version in range', () => { + expect( + pvp.getSatisfyingVersion( + ['1.0.0', '1.0.4', '1.3.0', '2.0.0'], + '>=1.0 && <1.1', + ), + ).toBe('1.0.4'); + }); + }); + + describe('.minSatisfyingVersion(versions, range)', () => { + it('should return min satisfying version in range', () => { + expect( + pvp.minSatisfyingVersion( + ['0.9', '1.0.0', '1.0.4', '1.3.0', '2.0.0'], + '>=1.0 && <1.1', + ), + ).toBe('1.0.0'); + }); + }); + + describe('.isLessThanRange(version, range)', () => { + it('should return true', () => { + expect(pvp.isLessThanRange?.('2.0.2', '>=3.0 && <3.1')).toBeTrue(); + expect(pvp.isLessThanRange?.('3', '>=3.0 && <3.1')).toBeTrue(); + }); + + it('should return false', () => { + expect(pvp.isLessThanRange?.('3', '>=3 && <3.1')).toBeFalse(); + expect(pvp.isLessThanRange?.('3.0', '>=3.0 && <3.1')).toBeFalse(); + expect(pvp.isLessThanRange?.('3.0.0', '>=3.0 && <3.1')).toBeFalse(); + expect(pvp.isLessThanRange?.('4.0.0', '>=3.0 && <3.1')).toBeFalse(); + expect(pvp.isLessThanRange?.('3.1.0', '>=3.0 && <3.1')).toBeFalse(); + }); + }); + + describe('.extractAllComponents(version)', () => { + it('should return an empty array when there are no numbers', () => { + expect(extractAllComponents('')).toEqual([]); + }); + it('should parse 3.0', () => { + expect(extractAllComponents('3.0')).toEqual([3, 0]); + }); + }); + + describe('.isValid(version)', () => { + it('should accept four components', () => { + expect(pvp.isValid('1.0.0.0')).toBeTrue(); + }); + it('should reject zero components', () => { + expect(pvp.isValid('')).toBeFalse(); + }); + }); + + describe('.getNewValue(newValueConfig)', () => { + it('should bump the upper end of the range if necessary', () => { + expect( + pvp.getNewValue({ + currentValue: '>=1.0 && <1.1', + newVersion: '1.1', + rangeStrategy: 'auto', + }), + ).toEqual('>=1.0 && <1.2'); + }); + it("shouldn't modify the range if not necessary", () => { + expect( + pvp.getNewValue({ + currentValue: '>=1.2 && <1.3', + newVersion: '1.2.3', + rangeStrategy: 'auto', + }), + ).toBeNull(); + }); + }); + + describe('.getComponents(...)', () => { + it('"0" is valid major version', () => { + expect(getComponents('0')?.major).toEqual([0]); + }); + }); + + describe('.isSame(...)', () => { + it('should compare major components correctly', () => { + expect(pvp.isSame?.('major', '4.10', '4.1')).toBeFalse(); + expect(pvp.isSame?.('major', '4.1.0', '5.1.0')).toBeFalse(); + expect(pvp.isSame?.('major', '4.1', '5.1')).toBeFalse(); + expect(pvp.isSame?.('major', '0', '1')).toBeFalse(); + expect(pvp.isSame?.('major', '4.1', '4.1.0')).toBeTrue(); + expect(pvp.isSame?.('major', '4.1.1', '4.1.2')).toBeTrue(); + expect(pvp.isSame?.('major', '0', '0')).toBeTrue(); + }); + it('should compare minor components correctly', () => { + expect(pvp.isSame?.('minor', '4.1.0', '5.1.0')).toBeTrue(); + expect(pvp.isSame?.('minor', '4.1', '4.1')).toBeTrue(); + expect(pvp.isSame?.('minor', '4.1', '5.1')).toBeTrue(); + expect(pvp.isSame?.('minor', '4.1.0', '4.1.1')).toBeFalse(); + }); + }); + + describe('.isValid(version)', () => { + it('should accept 1.0 as valid', () => { + expect(pvp.isValid('1.0')).toBeTrue(); + }); + it('should accept >=1.0 && <1.1 as valid (range)', () => { + expect(pvp.isValid('>=1.0 && <1.1')).toBeTrue(); + }); + }); + + describe('.isVersion(maybeRange)', () => { + it('should accept 1.0 as valid version', () => { + expect(pvp.isVersion('1.0')).toBeTrue(); + }); + it('should reject >=1.0 && <1.1 as it is a range, not a version', () => { + expect(pvp.isVersion('>=1.0 && <1.1')).toBeFalse(); + }); + }); + + describe('.equals(a, b)', () => { + it('should regard 1.01 and 1.1 as equal', () => { + expect(pvp.equals('1.01', '1.1')).toBeTrue(); + }); + it('should regard 1.01 and 1.0 are not equal', () => { + expect(pvp.equals('1.01', '1.0')).toBeFalse(); + }); + }); + + describe('.isSingleVersion(range)', () => { + it('should consider ==1.0 a single version', () => { + expect(pvp.isSingleVersion('==1.0')).toBeTrue(); + }); + it('should return false for ranges', () => { + expect(pvp.isSingleVersion('>=1.0 && <1.1')).toBeFalse(); + }); + }); + + describe('.subset(subRange, superRange)', () => { + it('1.1-1.2 is inside 1.0-2.0', () => { + expect(pvp.subset?.('>=1.0 && <1.1', '>=1.0 && <2.0')).toBeTrue(); + }); + it('1.0-2.0 is inside 1.0-2.0', () => { + expect(pvp.subset?.('>=1.0 && <2.0', '>=1.0 && <2.0')).toBeTrue(); + }); + it('1.0-2.1 outside 1.0-2.0', () => { + expect(pvp.subset?.('>=1.0 && <2.1', '>=1.0 && <2.0')).toBeFalse(); + }); + it('0.9-2.0 outside 1.0-2.0', () => { + expect(pvp.subset?.('>=0.9 && <2.1', '>=1.0 && <2.0')).toBeFalse(); + }); + }); +}); diff --git a/lib/modules/versioning/pvp/index.ts b/lib/modules/versioning/pvp/index.ts new file mode 100644 index 00000000000000..73a7a526db939f --- /dev/null +++ b/lib/modules/versioning/pvp/index.ts @@ -0,0 +1,274 @@ +import type { NewValueConfig } from '../types'; +import type { RangeStrategy } from '../../../types/versioning'; +import type { VersioningApi } from '../types'; +import { logger } from '../../../logger'; +import { regEx } from '../../../util/regex'; + +export const id = 'pvp'; +export const displayName = 'Package Versioning Policy (Haskell)'; +export const urls = []; +export const supportsRanges = true; +export const supportedRangeStrategies: RangeStrategy[] = ['auto']; + +type Parsed = { lower: string; upper: string }; +type Components = { major: number[]; minor: number[]; patch: number[] }; + +export function parse(input: string): Parsed | null { + input = input.replaceAll(' ', ''); + const r = regEx(/>=(?[\d\.]+)&&<(?[\d\.]+)/); + const m = r.exec(input); + if (!m?.groups) { + return null; + } + return { + lower: m.groups['lower'], + upper: m.groups['upper'], + }; +} + +export function extractAllComponents(version: string): number[] { + let versionMajor = version.split('.'); + const versionIntMajor: number[] = versionMajor.map((x) => parseInt(x, 10)); + let ret = []; + for (let l of versionIntMajor) { + if (l < 0 || !isFinite(l)) { + continue; + } + ret.push(l); + } + return ret; +} + +function compareIntArray( + versionIntMajor: number[], + otherIntMajor: number[], +): 'lt' | 'eq' | 'gt' { + for ( + let i = 0; + i < Math.min(versionIntMajor.length, otherIntMajor.length); + i++ + ) { + if (versionIntMajor[i] > otherIntMajor[i]) { + return 'gt'; + } + if (versionIntMajor[i] < otherIntMajor[i]) { + return 'lt'; + } + } + if (versionIntMajor.length === otherIntMajor.length) { + return 'eq'; + } + if (versionIntMajor.length > otherIntMajor.length) { + return 'gt'; + } + return 'lt'; +} + +function isGreaterThan(version: string, other: string): boolean { + const versionIntMajor = extractAllComponents(version); + const otherIntMajor = extractAllComponents(other); + return compareIntArray(versionIntMajor, otherIntMajor) === 'gt'; +} + +function getMajor(version: string): number { + // This basically can't be implemented correctly, since + // 1.1 and 1.10 become equal when converted to float. + // Consumers should use isSame instead. + const l1 = version.split('.'); + return Number(l1.slice(0, 2).join('.')); +} + +function getMinor(version: string): number | null { + const l1 = version.split('.'); + if (l1.length < 3) { + return null; + } + return Number(l1[2]); +} + +function getPatch(version: string): number | null { + const l1 = version.split('.'); + let components = l1.slice(3); + if (components.length === 0) { + return null; + } + return Number(components[0] + '.' + components.slice(1).join('')); +} + +function matches(version: string, range: string): boolean { + let parsed = parse(range); + if (parsed === null) { + return false; + } + const ver = extractAllComponents(version); + const lower = extractAllComponents(parsed.lower); + const upper = extractAllComponents(parsed.upper); + return ( + 'gt' === compareIntArray(upper, ver) && + ['eq', 'lt'].includes(compareIntArray(lower, ver)) + ); +} + +function getSatisfyingVersion( + versions: string[], + range: string, +): string | null { + let copy = versions.slice(0); + copy.sort((a, b) => (isGreaterThan(a, b) ? -1 : 1)); + const result = copy.find((v) => matches(v, range)); + return result ?? null; +} + +function minSatisfyingVersion( + versions: string[], + range: string, +): string | null { + let copy = versions.slice(0); + copy.sort((a, b) => (isGreaterThan(a, b) ? 1 : -1)); + const result = copy.find((v) => matches(v, range)); + return result ?? null; +} + +function isLessThanRange(version: string, range: string): boolean { + let parsed = parse(range); + if (parsed === null) { + return false; + } + const compos = extractAllComponents(version); + const lower = extractAllComponents(parsed.lower); + return 'lt' === compareIntArray(compos, lower); +} + +export function getComponents(splitOne: string): Components | null { + const c = extractAllComponents(splitOne); + if (c.length === 0) { + return null; + } + return { + major: c.slice(0, 2), + minor: c.slice(2, 3), + patch: c.slice(3), + }; +} + +function plusOne(majorOne: number[]): string { + return `${majorOne[0]}.${majorOne[1] + 1}`; +} + +function getNewValue({ + currentValue, + newVersion, + rangeStrategy, +}: NewValueConfig): string | null { + if (rangeStrategy !== 'auto') { + logger.info( + { rangeStrategy, currentValue, newVersion }, + `PVP can't handle this range strategy.`, + ); + return null; + } + const parsed = parse(currentValue); + if (parsed === null) { + logger.info( + { currentValue, newVersion }, + 'could not parse PVP version range', + ); + return null; + } + if (isLessThanRange(newVersion, currentValue)) { + // ignore new releases in old release series + return null; + } + if (matches(newVersion, currentValue)) { + // the upper bound is already high enough + return null; + } + const compos = getComponents(newVersion); + if (compos === null) { + logger.warn( + { currentValue, newVersion, compos }, + 'did not find two major parts', + ); + return null; + } + const majorPlusOne = plusOne(compos.major); + if (!matches(newVersion, `>=${parsed.lower} && <${majorPlusOne}`)) { + logger.warn( + { newVersion }, + "Even though the major bound was bumped, the newVersion still isn't accepted.", + ); + return null; + } + return `>=${parsed.lower} && <${majorPlusOne}`; +} + +function isSame(type: 'major' | 'minor' | 'patch', a: string, b: string) { + const aComponents = getComponents(a); + const bComponents = getComponents(b); + if (aComponents === null || bComponents === null) { + return false; + } + if (type === 'major') { + return 'eq' === compareIntArray(aComponents.major, bComponents.major); + } else if (type === 'minor') { + return 'eq' === compareIntArray(aComponents.minor, bComponents.minor); + } else { + return 'eq' === compareIntArray(aComponents.patch, bComponents.patch); + } +} + +function subset(subRange: string, superRange: string): boolean | undefined { + const sub = parse(subRange); + const sup = parse(superRange); + if (sub === null || sup === null) { + return undefined; + } + const subLower = extractAllComponents(sub.lower); + const subUpper = extractAllComponents(sub.upper); + const supLower = extractAllComponents(sup.lower); + const supUpper = extractAllComponents(sup.upper); + if ('lt' === compareIntArray(subLower, supLower)) { + return false; + } + if ('gt' === compareIntArray(subUpper, supUpper)) { + return false; + } + return true; +} + +function isVersion(maybeRange: string | undefined | null): boolean { + return typeof maybeRange === 'string' && parse(maybeRange) === null; +} + +function isValid(ver: string) { + return extractAllComponents(ver).length >= 1; +} + +function isSingleVersion(range: string): boolean { + range = range.trim(); + const r = regEx(/^[\d\.]+$/); + return range.startsWith('==') && r.test(range.slice(2)); +} + +export const api: VersioningApi = { + isValid, + isVersion, + isStable: () => true, + isCompatible: () => true, + getMajor, + getMinor, + getPatch, + isSingleVersion, + sortVersions: (a, b) => (isGreaterThan(a, b) ? 1 : -1), + equals: (a, b) => + 'eq' === compareIntArray(extractAllComponents(a), extractAllComponents(b)), + matches, + getSatisfyingVersion, + minSatisfyingVersion, + isLessThanRange, + isGreaterThan, + getNewValue, + isSame, + subset, +}; +export default api; diff --git a/lib/modules/versioning/pvp/readme.md b/lib/modules/versioning/pvp/readme.md new file mode 100644 index 00000000000000..a6df93438f5d68 --- /dev/null +++ b/lib/modules/versioning/pvp/readme.md @@ -0,0 +1,7 @@ +This is like same-major, except that the first _two_ components are parts of the major version. That is, in `A.B.C.D`: + +- `A.B`: major version +- `C`: minor +- `D`: 'patch' according to the PVP spec, but since Renovate doesn't support this category, it is also part of the minor. + +Any additional components are also part of the minor version. From 4f2dd9be7956b7de4351568783c469986fc19fef Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Mon, 4 Nov 2024 08:41:29 -0600 Subject: [PATCH 02/16] chose(versioning): PVP: correct readme, address eslint --- lib/modules/versioning/pvp/index.spec.ts | 59 ++++++++++++++---------- lib/modules/versioning/pvp/index.ts | 37 ++++++++------- lib/modules/versioning/pvp/readme.md | 8 ++-- 3 files changed, 57 insertions(+), 47 deletions(-) diff --git a/lib/modules/versioning/pvp/index.spec.ts b/lib/modules/versioning/pvp/index.spec.ts index 4d7a574458ead3..27e3a7f624a6fd 100644 --- a/lib/modules/versioning/pvp/index.spec.ts +++ b/lib/modules/versioning/pvp/index.spec.ts @@ -1,5 +1,4 @@ -import pvp from '.'; -import { getComponents, extractAllComponents, parse } from '.'; +import pvp, { extractAllComponents, getComponents, parse } from '.'; describe('modules/versioning/pvp/index', () => { describe('.isGreaterThan(version, other)', () => { @@ -23,25 +22,25 @@ describe('modules/versioning/pvp/index', () => { it('should parse >=1.0 && <1.1', () => { const parsed = parse('>=1.0 && <1.1'); expect(parsed).not.toBeNull(); - expect(parsed!.lower).toEqual('1.0'); - expect(parsed!.upper).toEqual('1.1'); + expect(parsed!.lower).toBe('1.0'); + expect(parsed!.upper).toBe('1.1'); }); }); describe('.getMajor(version)', () => { it('should extract second component as decimal digit', () => { - expect(pvp.getMajor('1.0.0')).toEqual(1.0); - expect(pvp.getMajor('1.0.1')).toEqual(1.0); - expect(pvp.getMajor('1.1.1')).toEqual(1.1); + expect(pvp.getMajor('1.0.0')).toBe(1.0); + expect(pvp.getMajor('1.0.1')).toBe(1.0); + expect(pvp.getMajor('1.1.1')).toBe(1.1); }); }); describe('.getMinor(version)', () => { it('should extract minor as third component in version', () => { expect(pvp.getMinor('1.0')).toBeNull(); - expect(pvp.getMinor('1.0.0')).toEqual(0); - expect(pvp.getMinor('1.0.1')).toEqual(1); - expect(pvp.getMinor('1.1.2')).toEqual(2); + expect(pvp.getMinor('1.0.0')).toBe(0); + expect(pvp.getMinor('1.0.1')).toBe(1); + expect(pvp.getMinor('1.1.2')).toBe(2); }); }); @@ -49,10 +48,11 @@ describe('modules/versioning/pvp/index', () => { it('should return null where there is no patch version', () => { expect(pvp.getPatch('1.0.0')).toBeNull(); }); + it('should extract all remaining components as decimal digits', () => { - expect(pvp.getPatch('1.0.0.5.1')).toEqual(5.1); - expect(pvp.getPatch('1.0.1.6')).toEqual(6); - expect(pvp.getPatch('1.1.2.7')).toEqual(7); + expect(pvp.getPatch('1.0.0.5.1')).toBe(5.1); + expect(pvp.getPatch('1.0.1.6')).toBe(6); + expect(pvp.getPatch('1.1.2.7')).toBe(7); }); }); @@ -114,17 +114,27 @@ describe('modules/versioning/pvp/index', () => { it('should return an empty array when there are no numbers', () => { expect(extractAllComponents('')).toEqual([]); }); + it('should parse 3.0', () => { expect(extractAllComponents('3.0')).toEqual([3, 0]); }); }); describe('.isValid(version)', () => { + it('should reject zero components', () => { + expect(pvp.isValid('')).toBeFalse(); + }); + it('should accept four components', () => { expect(pvp.isValid('1.0.0.0')).toBeTrue(); }); - it('should reject zero components', () => { - expect(pvp.isValid('')).toBeFalse(); + + it('should accept 1.0 as valid', () => { + expect(pvp.isValid('1.0')).toBeTrue(); + }); + + it('should accept >=1.0 && <1.1 as valid (range)', () => { + expect(pvp.isValid('>=1.0 && <1.1')).toBeTrue(); }); }); @@ -136,8 +146,9 @@ describe('modules/versioning/pvp/index', () => { newVersion: '1.1', rangeStrategy: 'auto', }), - ).toEqual('>=1.0 && <1.2'); + ).toBe('>=1.0 && <1.2'); }); + it("shouldn't modify the range if not necessary", () => { expect( pvp.getNewValue({ @@ -165,6 +176,7 @@ describe('modules/versioning/pvp/index', () => { expect(pvp.isSame?.('major', '4.1.1', '4.1.2')).toBeTrue(); expect(pvp.isSame?.('major', '0', '0')).toBeTrue(); }); + it('should compare minor components correctly', () => { expect(pvp.isSame?.('minor', '4.1.0', '5.1.0')).toBeTrue(); expect(pvp.isSame?.('minor', '4.1', '4.1')).toBeTrue(); @@ -173,19 +185,11 @@ describe('modules/versioning/pvp/index', () => { }); }); - describe('.isValid(version)', () => { - it('should accept 1.0 as valid', () => { - expect(pvp.isValid('1.0')).toBeTrue(); - }); - it('should accept >=1.0 && <1.1 as valid (range)', () => { - expect(pvp.isValid('>=1.0 && <1.1')).toBeTrue(); - }); - }); - describe('.isVersion(maybeRange)', () => { it('should accept 1.0 as valid version', () => { expect(pvp.isVersion('1.0')).toBeTrue(); }); + it('should reject >=1.0 && <1.1 as it is a range, not a version', () => { expect(pvp.isVersion('>=1.0 && <1.1')).toBeFalse(); }); @@ -195,6 +199,7 @@ describe('modules/versioning/pvp/index', () => { it('should regard 1.01 and 1.1 as equal', () => { expect(pvp.equals('1.01', '1.1')).toBeTrue(); }); + it('should regard 1.01 and 1.0 are not equal', () => { expect(pvp.equals('1.01', '1.0')).toBeFalse(); }); @@ -204,6 +209,7 @@ describe('modules/versioning/pvp/index', () => { it('should consider ==1.0 a single version', () => { expect(pvp.isSingleVersion('==1.0')).toBeTrue(); }); + it('should return false for ranges', () => { expect(pvp.isSingleVersion('>=1.0 && <1.1')).toBeFalse(); }); @@ -213,12 +219,15 @@ describe('modules/versioning/pvp/index', () => { it('1.1-1.2 is inside 1.0-2.0', () => { expect(pvp.subset?.('>=1.0 && <1.1', '>=1.0 && <2.0')).toBeTrue(); }); + it('1.0-2.0 is inside 1.0-2.0', () => { expect(pvp.subset?.('>=1.0 && <2.0', '>=1.0 && <2.0')).toBeTrue(); }); + it('1.0-2.1 outside 1.0-2.0', () => { expect(pvp.subset?.('>=1.0 && <2.1', '>=1.0 && <2.0')).toBeFalse(); }); + it('0.9-2.0 outside 1.0-2.0', () => { expect(pvp.subset?.('>=0.9 && <2.1', '>=1.0 && <2.0')).toBeFalse(); }); diff --git a/lib/modules/versioning/pvp/index.ts b/lib/modules/versioning/pvp/index.ts index 73a7a526db939f..40ea75278f513d 100644 --- a/lib/modules/versioning/pvp/index.ts +++ b/lib/modules/versioning/pvp/index.ts @@ -1,8 +1,7 @@ -import type { NewValueConfig } from '../types'; -import type { RangeStrategy } from '../../../types/versioning'; -import type { VersioningApi } from '../types'; import { logger } from '../../../logger'; +import type { RangeStrategy } from '../../../types/versioning'; import { regEx } from '../../../util/regex'; +import type { NewValueConfig , VersioningApi } from '../types'; export const id = 'pvp'; export const displayName = 'Package Versioning Policy (Haskell)'; @@ -14,9 +13,9 @@ type Parsed = { lower: string; upper: string }; type Components = { major: number[]; minor: number[]; patch: number[] }; export function parse(input: string): Parsed | null { - input = input.replaceAll(' ', ''); - const r = regEx(/>=(?[\d\.]+)&&<(?[\d\.]+)/); - const m = r.exec(input); + const noSpaces = input.replaceAll(' ', ''); + const r = regEx(/>=(?[\d.]+)&&<(?[\d.]+)/); + const m = r.exec(noSpaces); if (!m?.groups) { return null; } @@ -27,10 +26,10 @@ export function parse(input: string): Parsed | null { } export function extractAllComponents(version: string): number[] { - let versionMajor = version.split('.'); + const versionMajor = version.split('.'); const versionIntMajor: number[] = versionMajor.map((x) => parseInt(x, 10)); - let ret = []; - for (let l of versionIntMajor) { + const ret = []; + for (const l of versionIntMajor) { if (l < 0 || !isFinite(l)) { continue; } @@ -88,7 +87,7 @@ function getMinor(version: string): number | null { function getPatch(version: string): number | null { const l1 = version.split('.'); - let components = l1.slice(3); + const components = l1.slice(3); if (components.length === 0) { return null; } @@ -96,7 +95,7 @@ function getPatch(version: string): number | null { } function matches(version: string, range: string): boolean { - let parsed = parse(range); + const parsed = parse(range); if (parsed === null) { return false; } @@ -113,7 +112,7 @@ function getSatisfyingVersion( versions: string[], range: string, ): string | null { - let copy = versions.slice(0); + const copy = versions.slice(0); copy.sort((a, b) => (isGreaterThan(a, b) ? -1 : 1)); const result = copy.find((v) => matches(v, range)); return result ?? null; @@ -123,14 +122,14 @@ function minSatisfyingVersion( versions: string[], range: string, ): string | null { - let copy = versions.slice(0); + const copy = versions.slice(0); copy.sort((a, b) => (isGreaterThan(a, b) ? 1 : -1)); const result = copy.find((v) => matches(v, range)); return result ?? null; } function isLessThanRange(version: string, range: string): boolean { - let parsed = parse(range); + const parsed = parse(range); if (parsed === null) { return false; } @@ -202,7 +201,7 @@ function getNewValue({ return `>=${parsed.lower} && <${majorPlusOne}`; } -function isSame(type: 'major' | 'minor' | 'patch', a: string, b: string) { +function isSame(type: 'major' | 'minor' | 'patch', a: string, b: string): boolean { const aComponents = getComponents(a); const bComponents = getComponents(b); if (aComponents === null || bComponents === null) { @@ -240,14 +239,14 @@ function isVersion(maybeRange: string | undefined | null): boolean { return typeof maybeRange === 'string' && parse(maybeRange) === null; } -function isValid(ver: string) { +function isValid(ver: string): boolean { return extractAllComponents(ver).length >= 1; } function isSingleVersion(range: string): boolean { - range = range.trim(); - const r = regEx(/^[\d\.]+$/); - return range.startsWith('==') && r.test(range.slice(2)); + const noSpaces = range.trim(); + const r = regEx(/^[\d.]+$/); + return noSpaces.startsWith('==') && r.test(noSpaces.slice(2)); } export const api: VersioningApi = { diff --git a/lib/modules/versioning/pvp/readme.md b/lib/modules/versioning/pvp/readme.md index a6df93438f5d68..ea9a8722b87eaf 100644 --- a/lib/modules/versioning/pvp/readme.md +++ b/lib/modules/versioning/pvp/readme.md @@ -1,7 +1,9 @@ -This is like same-major, except that the first _two_ components are parts of the major version. That is, in `A.B.C.D`: +[Package Versioning Policy](https://pvp.haskell.org/) is used with Haskell. +It's like semver, except that the first _two_ components are parts of the major +version. That is, in `A.B.C`: - `A.B`: major version - `C`: minor -- `D`: 'patch' according to the PVP spec, but since Renovate doesn't support this category, it is also part of the minor. -Any additional components are also part of the minor version. +The range syntax comes from Cabal, specifically the [build-depends +section](https://cabal.readthedocs.io/en/3.10/cabal-package.html). From 602fc89becdb35dd9d967239979916b7569d5774 Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Mon, 4 Nov 2024 08:42:34 -0600 Subject: [PATCH 03/16] chore(versioning): PVP: address prettier --- lib/modules/versioning/pvp/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/modules/versioning/pvp/index.ts b/lib/modules/versioning/pvp/index.ts index 40ea75278f513d..e0cfa7c6eb56bf 100644 --- a/lib/modules/versioning/pvp/index.ts +++ b/lib/modules/versioning/pvp/index.ts @@ -1,7 +1,7 @@ import { logger } from '../../../logger'; import type { RangeStrategy } from '../../../types/versioning'; import { regEx } from '../../../util/regex'; -import type { NewValueConfig , VersioningApi } from '../types'; +import type { NewValueConfig, VersioningApi } from '../types'; export const id = 'pvp'; export const displayName = 'Package Versioning Policy (Haskell)'; @@ -201,7 +201,11 @@ function getNewValue({ return `>=${parsed.lower} && <${majorPlusOne}`; } -function isSame(type: 'major' | 'minor' | 'patch', a: string, b: string): boolean { +function isSame( + type: 'major' | 'minor' | 'patch', + a: string, + b: string, +): boolean { const aComponents = getComponents(a); const bComponents = getComponents(b); if (aComponents === null || bComponents === null) { From c34c6021465600bf7a3ee9762b22bcaf00bd815d Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Mon, 4 Nov 2024 09:06:10 -0600 Subject: [PATCH 04/16] chore(versioning): fix ordering of versioning schemes --- lib/modules/versioning/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/versioning/api.ts b/lib/modules/versioning/api.ts index 2eea67b4da3f9e..211c3f8740fe29 100644 --- a/lib/modules/versioning/api.ts +++ b/lib/modules/versioning/api.ts @@ -72,8 +72,8 @@ api.set(nuget.id, nuget.api); api.set(pep440.id, pep440.api); api.set(perl.id, perl.api); api.set(poetry.id, poetry.api); -api.set(python.id, python.api); api.set(pvp.id, pvp.api); +api.set(python.id, python.api); api.set(redhat.id, redhat.api); api.set(regex.id, regex.api); api.set(rez.id, rez.api); From ece9d655e9b82818447e7c0d62928386fb72a71a Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Mon, 4 Nov 2024 21:45:51 -0600 Subject: [PATCH 05/16] chore(versioning): PVP: improve test coverage --- lib/modules/versioning/pvp/index.spec.ts | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/lib/modules/versioning/pvp/index.spec.ts b/lib/modules/versioning/pvp/index.spec.ts index 27e3a7f624a6fd..349dffbe9f8e80 100644 --- a/lib/modules/versioning/pvp/index.spec.ts +++ b/lib/modules/versioning/pvp/index.spec.ts @@ -71,6 +71,10 @@ describe('modules/versioning/pvp/index', () => { expect(pvp.matches('4', '>=4.0 && <4.10')).toBeFalse(); expect(pvp.matches('4.10', '>=4.1 && <4.10')).toBeFalse(); }); + + it("should return false when range can't be parsed", () => { + expect(pvp.matches('4', 'gibberish')).toBeFalse(); + }); }); describe('.getSatisfyingVersion(versions, range)', () => { @@ -108,6 +112,10 @@ describe('modules/versioning/pvp/index', () => { expect(pvp.isLessThanRange?.('4.0.0', '>=3.0 && <3.1')).toBeFalse(); expect(pvp.isLessThanRange?.('3.1.0', '>=3.0 && <3.1')).toBeFalse(); }); + + it("should return false when range can't be parsed", () => { + expect(pvp.isLessThanRange?.('3', 'gibberish')).toBeFalse(); + }); }); describe('.extractAllComponents(version)', () => { @@ -158,12 +166,46 @@ describe('modules/versioning/pvp/index', () => { }), ).toBeNull(); }); + + it('should return null when given unimplemented range strategies like update-lockfile', () => { + expect( + pvp.getNewValue({ + currentValue: '>=1.0 && <1.1', + newVersion: '1.2.3', + rangeStrategy: 'update-lockfile', + }), + ).toBeNull(); + }); + + it("should return null when given range that can't be parsed", () => { + expect( + pvp.getNewValue({ + currentValue: 'gibberish', + newVersion: '1.2.3', + rangeStrategy: 'auto', + }), + ).toBeNull(); + }); + + it('should return null when newVersion is below range', () => { + expect( + pvp.getNewValue({ + currentValue: '>=1.0 && <1.1', + newVersion: '0.9', + rangeStrategy: 'auto', + }), + ).toBeNull(); + }); }); describe('.getComponents(...)', () => { it('"0" is valid major version', () => { expect(getComponents('0')?.major).toEqual([0]); }); + + it('returns null when no components could be extracted', () => { + expect(getComponents('')).toBeNull(); + }); }); describe('.isSame(...)', () => { @@ -183,6 +225,10 @@ describe('modules/versioning/pvp/index', () => { expect(pvp.isSame?.('minor', '4.1', '5.1')).toBeTrue(); expect(pvp.isSame?.('minor', '4.1.0', '4.1.1')).toBeFalse(); }); + + it('should return false when the given a invalid version', () => { + expect(pvp.isSame?.('minor', '', '0')).toBeFalse(); + }); }); describe('.isVersion(maybeRange)', () => { @@ -231,5 +277,9 @@ describe('modules/versioning/pvp/index', () => { it('0.9-2.0 outside 1.0-2.0', () => { expect(pvp.subset?.('>=0.9 && <2.1', '>=1.0 && <2.0')).toBeFalse(); }); + + it("returns undefined when passed a range that can't be parsed", () => { + expect(pvp.subset?.('gibberish', '')).toBeUndefined(); + }); }); }); From 80d565d53ab84dadd8e6ae7e4d5fa4937b18ff24 Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Mon, 4 Nov 2024 22:36:57 -0600 Subject: [PATCH 06/16] chore(versioning): PVP: reach 100% test coverage --- lib/modules/versioning/pvp/index.spec.ts | 66 ++++++++++++++++++++++++ lib/modules/versioning/pvp/index.ts | 58 ++++++++++++++------- 2 files changed, 105 insertions(+), 19 deletions(-) diff --git a/lib/modules/versioning/pvp/index.spec.ts b/lib/modules/versioning/pvp/index.spec.ts index 349dffbe9f8e80..69bd23fe93db01 100644 --- a/lib/modules/versioning/pvp/index.spec.ts +++ b/lib/modules/versioning/pvp/index.spec.ts @@ -75,6 +75,10 @@ describe('modules/versioning/pvp/index', () => { it("should return false when range can't be parsed", () => { expect(pvp.matches('4', 'gibberish')).toBeFalse(); }); + + it('should return false when given an invalid version', () => { + expect(pvp.matches('', '>=1.0 && <1.1')).toBeFalse(); + }); }); describe('.getSatisfyingVersion(versions, range)', () => { @@ -86,6 +90,24 @@ describe('modules/versioning/pvp/index', () => { ), ).toBe('1.0.4'); }); + + it('should handle unsorted inputs', () => { + expect( + pvp.getSatisfyingVersion( + ['2.0.0', '1.0.0', '1.0.4', '1.3.0'], + '>=1.0 && <1.1', + ), + ).toBe('1.0.4'); + }); + + it('should return null when no versions match', () => { + expect( + pvp.getSatisfyingVersion( + ['1.0.0', '1.0.4', '1.3.0', '2.0.0'], + '>=3.0 && <4.0', + ), + ).toBeNull(); + }); }); describe('.minSatisfyingVersion(versions, range)', () => { @@ -116,6 +138,10 @@ describe('modules/versioning/pvp/index', () => { it("should return false when range can't be parsed", () => { expect(pvp.isLessThanRange?.('3', 'gibberish')).toBeFalse(); }); + + it('should return true when version is invalid', () => { + expect(pvp.isLessThanRange?.('', '>=3.0 && <3.1')).toBeTrue(); + }); }); describe('.extractAllComponents(version)', () => { @@ -196,6 +222,16 @@ describe('modules/versioning/pvp/index', () => { }), ).toBeNull(); }); + + it('should return null when newVersion is empty', () => { + expect( + pvp.getNewValue({ + currentValue: '>=1.0 && <1.1', + newVersion: '', + rangeStrategy: 'auto', + }), + ).toBeNull(); + }); }); describe('.getComponents(...)', () => { @@ -226,6 +262,12 @@ describe('modules/versioning/pvp/index', () => { expect(pvp.isSame?.('minor', '4.1.0', '4.1.1')).toBeFalse(); }); + it('should compare patch components correctly', () => { + expect(pvp.isSame?.('patch', '1.0.0.0', '1.0.0.0')).toBeTrue(); + expect(pvp.isSame?.('patch', '1.0.0.0', '2.0.0.0')).toBeTrue(); + expect(pvp.isSame?.('patch', '1.0.0.0', '1.0.0.1')).toBeFalse(); + }); + it('should return false when the given a invalid version', () => { expect(pvp.isSame?.('minor', '', '0')).toBeFalse(); }); @@ -282,4 +324,28 @@ describe('modules/versioning/pvp/index', () => { expect(pvp.subset?.('gibberish', '')).toBeUndefined(); }); }); + + describe('.sortVersions()', () => { + it('should sort 1.0 and 1.1', () => { + expect(pvp.sortVersions('1.0', '1.1')).toBe(-1); + expect(pvp.sortVersions('1.1', '1.0')).toBe(1); + expect(pvp.sortVersions('1.0', '1.0')).toBe(0); + }); + }); + + describe('.isStable()', () => { + it('should consider 0.0.0 stable', () => { + // in PVP, stability is not conveyed in the version number + // so we consider all versions stable + expect(pvp.isStable('0.0.0')).toBeTrue(); + }); + }); + + describe('.isCompatible()', () => { + it('should consider 0.0.0 compatible', () => { + // in PVP, there is no extra information besides the numbers + // so we consider all versions compatible + expect(pvp.isCompatible('0.0.0')).toBeTrue(); + }); + }); }); diff --git a/lib/modules/versioning/pvp/index.ts b/lib/modules/versioning/pvp/index.ts index e0cfa7c6eb56bf..9160ac864ef5ec 100644 --- a/lib/modules/versioning/pvp/index.ts +++ b/lib/modules/versioning/pvp/index.ts @@ -108,24 +108,29 @@ function matches(version: string, range: string): boolean { ); } -function getSatisfyingVersion( +function satisfyingVersion( versions: string[], range: string, + onGreaterThan: (isGreaterThan: boolean) => boolean, ): string | null { const copy = versions.slice(0); - copy.sort((a, b) => (isGreaterThan(a, b) ? -1 : 1)); + copy.sort((a, b) => (onGreaterThan(isGreaterThan(a, b)) ? -1 : 1)); const result = copy.find((v) => matches(v, range)); return result ?? null; } +function getSatisfyingVersion( + versions: string[], + range: string, +): string | null { + return satisfyingVersion(versions, range, (x) => x); +} + function minSatisfyingVersion( versions: string[], range: string, ): string | null { - const copy = versions.slice(0); - copy.sort((a, b) => (isGreaterThan(a, b) ? 1 : -1)); - const result = copy.find((v) => matches(v, range)); - return result ?? null; + return satisfyingVersion(versions, range, (x) => !x); } function isLessThanRange(version: string, range: string): boolean { @@ -182,15 +187,10 @@ function getNewValue({ // the upper bound is already high enough return null; } + // isLessThanRange returns true when newVersion is invalid. so we can assert it non-null const compos = getComponents(newVersion); - if (compos === null) { - logger.warn( - { currentValue, newVersion, compos }, - 'did not find two major parts', - ); - return null; - } - const majorPlusOne = plusOne(compos.major); + const majorPlusOne = plusOne(compos!.major); + // istanbul ignore next: since all versions that can be parsed, can also be bumped, this can never happen if (!matches(newVersion, `>=${parsed.lower} && <${majorPlusOne}`)) { logger.warn( { newVersion }, @@ -253,18 +253,38 @@ function isSingleVersion(range: string): boolean { return noSpaces.startsWith('==') && r.test(noSpaces.slice(2)); } +function equals(a: string, b: string): boolean { + return ( + 'eq' === compareIntArray(extractAllComponents(a), extractAllComponents(b)) + ); +} + +function sortVersions(a: string, b: string): number { + if (equals(a, b)) { + return 0; + } + return isGreaterThan(a, b) ? 1 : -1; +} + +function isStable(version: string): boolean { + return true; +} + +function isCompatible(version: string): boolean { + return true; +} + export const api: VersioningApi = { isValid, isVersion, - isStable: () => true, - isCompatible: () => true, + isStable, + isCompatible, getMajor, getMinor, getPatch, isSingleVersion, - sortVersions: (a, b) => (isGreaterThan(a, b) ? 1 : -1), - equals: (a, b) => - 'eq' === compareIntArray(extractAllComponents(a), extractAllComponents(b)), + sortVersions, + equals, matches, getSatisfyingVersion, minSatisfyingVersion, From 6259efcbb386fc33e17bbb47604c4458ee1eb665 Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Sat, 9 Nov 2024 11:09:01 -0600 Subject: [PATCH 07/16] chore(versioning): convert most tests to table format --- lib/modules/versioning/pvp/index.spec.ts | 417 ++++++++++------------- 1 file changed, 178 insertions(+), 239 deletions(-) diff --git a/lib/modules/versioning/pvp/index.spec.ts b/lib/modules/versioning/pvp/index.spec.ts index 69bd23fe93db01..f4335dad912a91 100644 --- a/lib/modules/versioning/pvp/index.spec.ts +++ b/lib/modules/versioning/pvp/index.spec.ts @@ -2,19 +2,25 @@ import pvp, { extractAllComponents, getComponents, parse } from '.'; describe('modules/versioning/pvp/index', () => { describe('.isGreaterThan(version, other)', () => { - it('should return true', () => { - expect(pvp.isGreaterThan('1.23.1', '1.9.6')).toBeTrue(); - expect(pvp.isGreaterThan('4.0.0', '3.0.0')).toBeTrue(); - expect(pvp.isGreaterThan('3.0.1', '3.0.0')).toBeTrue(); - expect(pvp.isGreaterThan('4.10', '4.1')).toBeTrue(); - expect(pvp.isGreaterThan('1.0.0', '1.0')).toBeTrue(); - }); - - it('should return false', () => { - expect(pvp.isGreaterThan('2.0.2', '3.1.0')).toBeFalse(); // less - expect(pvp.isGreaterThan('3.0.0', '3.0.0')).toBeFalse(); // equal - expect(pvp.isGreaterThan('4.1', '4.10')).toBeFalse(); - expect(pvp.isGreaterThan('1.0', '1.0.0')).toBeFalse(); + it.each` + first | second + ${'1.23.1'} | ${'1.9.6'} + ${'4.0.0'} | ${'3.0.0'} + ${'3.0.1'} | ${'3.0.0'} + ${'4.10'} | ${'4.1'} + ${'1.0.0'} | ${'1.0'} + `('pvp.isGreaterThan($first, $second)', ({ first, second }) => { + expect(pvp.isGreaterThan(first, second)).toBeTrue(); + }); + + it.each` + first | second + ${'2.0.2'} | ${'3.1.0'} + ${'3.0.0'} | ${'3.0.0'} + ${'4.1'} | ${'4.10'} + ${'1.0'} | ${'1.0.0'} + `('pvp.isGreaterThan($first, $second)', ({ first, second }) => { + expect(pvp.isGreaterThan(first, second)).toBeFalse(); }); }); @@ -28,86 +34,74 @@ describe('modules/versioning/pvp/index', () => { }); describe('.getMajor(version)', () => { - it('should extract second component as decimal digit', () => { - expect(pvp.getMajor('1.0.0')).toBe(1.0); - expect(pvp.getMajor('1.0.1')).toBe(1.0); - expect(pvp.getMajor('1.1.1')).toBe(1.1); + it.each` + version | expected + ${'1.0.0'} | ${1.0} + ${'1.0.1'} | ${1.0} + ${'1.1.1'} | ${1.1} + `('pvp.getMajor("$version") === $expected', ({ version, expected }) => { + expect(pvp.getMajor(version)).toBe(expected); }); }); describe('.getMinor(version)', () => { - it('should extract minor as third component in version', () => { - expect(pvp.getMinor('1.0')).toBeNull(); - expect(pvp.getMinor('1.0.0')).toBe(0); - expect(pvp.getMinor('1.0.1')).toBe(1); - expect(pvp.getMinor('1.1.2')).toBe(2); + it.each` + version | expected + ${'1.0'} | ${null} + ${'1.0.0'} | ${0} + ${'1.0.1'} | ${1} + ${'1.1.2'} | ${2} + `('pvp.getMinor("$version") === $expected', ({ version, expected }) => { + expect(pvp.getMinor(version)).toBe(expected); }); }); describe('.getPatch(version)', () => { - it('should return null where there is no patch version', () => { - expect(pvp.getPatch('1.0.0')).toBeNull(); - }); - - it('should extract all remaining components as decimal digits', () => { - expect(pvp.getPatch('1.0.0.5.1')).toBe(5.1); - expect(pvp.getPatch('1.0.1.6')).toBe(6); - expect(pvp.getPatch('1.1.2.7')).toBe(7); + it.each` + version | expected + ${'1.0.0'} | ${null} + ${'1.0.0.5.1'} | ${5.1} + ${'1.0.1.6'} | ${6} + ${'1.1.2.7'} | ${7} + `('pvp.getPatch("$version") === $expected', ({ version, expected }) => { + expect(pvp.getPatch(version)).toBe(expected); }); }); describe('.matches(version, range)', () => { - it('should return true when version has same major', () => { - expect(pvp.matches('1.0.1', '>=1.0 && <1.1')).toBeTrue(); - expect(pvp.matches('4.1', '>=4.0 && <4.10')).toBeTrue(); - expect(pvp.matches('4.1', '>=4.1 && <4.10')).toBeTrue(); - expect(pvp.matches('4.1.0', '>=4.1 && <4.10')).toBeTrue(); - expect(pvp.matches('4.10', '>=4.1 && <4.10.0')).toBeTrue(); - expect(pvp.matches('4.10', '>=4.0 && <4.10.1')).toBeTrue(); - }); - - it('should return false when version has different major', () => { - expect(pvp.matches('1.0.0', '>=2.0 && <2.1')).toBeFalse(); - expect(pvp.matches('4', '>=4.0 && <4.10')).toBeFalse(); - expect(pvp.matches('4.10', '>=4.1 && <4.10')).toBeFalse(); - }); - - it("should return false when range can't be parsed", () => { - expect(pvp.matches('4', 'gibberish')).toBeFalse(); - }); - - it('should return false when given an invalid version', () => { - expect(pvp.matches('', '>=1.0 && <1.1')).toBeFalse(); - }); + it.each` + version | range | expected + ${'1.0.1'} | ${'>=1.0 && <1.1'} | ${true} + ${'4.1'} | ${'>=4.0 && <4.10'} | ${true} + ${'4.1'} | ${'>=4.1 && <4.10'} | ${true} + ${'4.1.0'} | ${'>=4.1 && <4.10'} | ${true} + ${'4.10'} | ${'>=4.1 && <4.10.0'} | ${true} + ${'4.10'} | ${'>=4.0 && <4.10.1'} | ${true} + ${'1.0.0'} | ${'>=2.0 && <2.1'} | ${false} + ${'4'} | ${'>=4.0 && <4.10'} | ${false} + ${'4.10'} | ${'>=4.1 && <4.10'} | ${false} + ${'4'} | ${'gibberish'} | ${false} + ${''} | ${'>=1.0 && <1.1'} | ${false} + `( + 'pvp.matches("$version", "$range") === $expected', + ({ version, range, expected }) => { + expect(pvp.matches(version, range)).toBe(expected); + }, + ); }); describe('.getSatisfyingVersion(versions, range)', () => { - it('should return max satisfying version in range', () => { - expect( - pvp.getSatisfyingVersion( - ['1.0.0', '1.0.4', '1.3.0', '2.0.0'], - '>=1.0 && <1.1', - ), - ).toBe('1.0.4'); - }); - - it('should handle unsorted inputs', () => { - expect( - pvp.getSatisfyingVersion( - ['2.0.0', '1.0.0', '1.0.4', '1.3.0'], - '>=1.0 && <1.1', - ), - ).toBe('1.0.4'); - }); - - it('should return null when no versions match', () => { - expect( - pvp.getSatisfyingVersion( - ['1.0.0', '1.0.4', '1.3.0', '2.0.0'], - '>=3.0 && <4.0', - ), - ).toBeNull(); - }); + it.each` + versions | range | expected + ${['1.0.0', '1.0.4', '1.3.0', '2.0.0']} | ${'>=1.0 && <1.1'} | ${'1.0.4'} + ${['2.0.0', '1.0.0', '1.0.4', '1.3.0']} | ${'>=1.0 && <1.1'} | ${'1.0.4'} + ${['1.0.0', '1.0.4', '1.3.0', '2.0.0']} | ${'>=3.0 && <4.0'} | ${null} + `( + 'pvp.getSatisfyingVersion($versions, "$range") === $expected', + ({ versions, range, expected }) => { + expect(pvp.getSatisfyingVersion(versions, range)).toBe(expected); + }, + ); }); describe('.minSatisfyingVersion(versions, range)', () => { @@ -122,26 +116,23 @@ describe('modules/versioning/pvp/index', () => { }); describe('.isLessThanRange(version, range)', () => { - it('should return true', () => { - expect(pvp.isLessThanRange?.('2.0.2', '>=3.0 && <3.1')).toBeTrue(); - expect(pvp.isLessThanRange?.('3', '>=3.0 && <3.1')).toBeTrue(); - }); - - it('should return false', () => { - expect(pvp.isLessThanRange?.('3', '>=3 && <3.1')).toBeFalse(); - expect(pvp.isLessThanRange?.('3.0', '>=3.0 && <3.1')).toBeFalse(); - expect(pvp.isLessThanRange?.('3.0.0', '>=3.0 && <3.1')).toBeFalse(); - expect(pvp.isLessThanRange?.('4.0.0', '>=3.0 && <3.1')).toBeFalse(); - expect(pvp.isLessThanRange?.('3.1.0', '>=3.0 && <3.1')).toBeFalse(); - }); - - it("should return false when range can't be parsed", () => { - expect(pvp.isLessThanRange?.('3', 'gibberish')).toBeFalse(); - }); - - it('should return true when version is invalid', () => { - expect(pvp.isLessThanRange?.('', '>=3.0 && <3.1')).toBeTrue(); - }); + it.each` + version | range | expected + ${'2.0.2'} | ${'>=3.0 && <3.1'} | ${true} + ${'3'} | ${'>=3.0 && <3.1'} | ${true} + ${'3'} | ${'>=3 && <3.1'} | ${false} + ${'3.0'} | ${'>=3.0 && <3.1'} | ${false} + ${'3.0.0'} | ${'>=3.0 && <3.1'} | ${false} + ${'4.0.0'} | ${'>=3.0 && <3.1'} | ${false} + ${'3.1.0'} | ${'>=3.0 && <3.1'} | ${false} + ${'3'} | ${'gibberish'} | ${false} + ${''} | ${'>=3.0 && <3.1'} | ${true} + `( + 'pvp.isLessThanRange?.("$version", "$range") === $expected', + ({ version, range, expected }) => { + expect(pvp.isLessThanRange?.(version, range)).toBe(expected); + }, + ); }); describe('.extractAllComponents(version)', () => { @@ -155,83 +146,34 @@ describe('modules/versioning/pvp/index', () => { }); describe('.isValid(version)', () => { - it('should reject zero components', () => { - expect(pvp.isValid('')).toBeFalse(); - }); - - it('should accept four components', () => { - expect(pvp.isValid('1.0.0.0')).toBeTrue(); - }); - - it('should accept 1.0 as valid', () => { - expect(pvp.isValid('1.0')).toBeTrue(); - }); - - it('should accept >=1.0 && <1.1 as valid (range)', () => { - expect(pvp.isValid('>=1.0 && <1.1')).toBeTrue(); + it.each` + version | expected + ${''} | ${false} + ${'1.0.0.0'} | ${true} + ${'1.0'} | ${true} + ${'>=1.0 && <1.1'} | ${true} + `('pvp.isValid("$version") === $expected', ({ version, expected }) => { + expect(pvp.isValid(version)).toBe(expected); }); }); describe('.getNewValue(newValueConfig)', () => { - it('should bump the upper end of the range if necessary', () => { - expect( - pvp.getNewValue({ - currentValue: '>=1.0 && <1.1', - newVersion: '1.1', - rangeStrategy: 'auto', - }), - ).toBe('>=1.0 && <1.2'); - }); - - it("shouldn't modify the range if not necessary", () => { - expect( - pvp.getNewValue({ - currentValue: '>=1.2 && <1.3', - newVersion: '1.2.3', - rangeStrategy: 'auto', - }), - ).toBeNull(); - }); - - it('should return null when given unimplemented range strategies like update-lockfile', () => { - expect( - pvp.getNewValue({ - currentValue: '>=1.0 && <1.1', - newVersion: '1.2.3', - rangeStrategy: 'update-lockfile', - }), - ).toBeNull(); - }); - - it("should return null when given range that can't be parsed", () => { - expect( - pvp.getNewValue({ - currentValue: 'gibberish', - newVersion: '1.2.3', - rangeStrategy: 'auto', - }), - ).toBeNull(); - }); - - it('should return null when newVersion is below range', () => { - expect( - pvp.getNewValue({ - currentValue: '>=1.0 && <1.1', - newVersion: '0.9', - rangeStrategy: 'auto', - }), - ).toBeNull(); - }); - - it('should return null when newVersion is empty', () => { - expect( - pvp.getNewValue({ - currentValue: '>=1.0 && <1.1', - newVersion: '', - rangeStrategy: 'auto', - }), - ).toBeNull(); - }); + it.each` + currentValue | newVersion | rangeStrategy | expected + ${'>=1.0 && <1.1'} | ${'1.1'} | ${'auto'} | ${'>=1.0 && <1.2'} + ${'>=1.2 && <1.3'} | ${'1.2.3'} | ${'auto'} | ${null} + ${'>=1.0 && <1.1'} | ${'1.2.3'} | ${'update-lockfile'} | ${null} + ${'gibberish'} | ${'1.2.3'} | ${'auto'} | ${null} + ${'>=1.0 && <1.1'} | ${'0.9'} | ${'auto'} | ${null} + ${'>=1.0 && <1.1'} | ${''} | ${'auto'} | ${null} + `( + 'pvp.getNewValue({currentValue: "$currentValue", newVersion: "$newVersion", rangeStrategy: "$rangeStrategy"}) === $expected', + ({ currentValue, newVersion, rangeStrategy, expected }) => { + expect( + pvp.getNewValue({ currentValue, newVersion, rangeStrategy }), + ).toBe(expected); + }, + ); }); describe('.getComponents(...)', () => { @@ -245,91 +187,88 @@ describe('modules/versioning/pvp/index', () => { }); describe('.isSame(...)', () => { - it('should compare major components correctly', () => { - expect(pvp.isSame?.('major', '4.10', '4.1')).toBeFalse(); - expect(pvp.isSame?.('major', '4.1.0', '5.1.0')).toBeFalse(); - expect(pvp.isSame?.('major', '4.1', '5.1')).toBeFalse(); - expect(pvp.isSame?.('major', '0', '1')).toBeFalse(); - expect(pvp.isSame?.('major', '4.1', '4.1.0')).toBeTrue(); - expect(pvp.isSame?.('major', '4.1.1', '4.1.2')).toBeTrue(); - expect(pvp.isSame?.('major', '0', '0')).toBeTrue(); - }); - - it('should compare minor components correctly', () => { - expect(pvp.isSame?.('minor', '4.1.0', '5.1.0')).toBeTrue(); - expect(pvp.isSame?.('minor', '4.1', '4.1')).toBeTrue(); - expect(pvp.isSame?.('minor', '4.1', '5.1')).toBeTrue(); - expect(pvp.isSame?.('minor', '4.1.0', '4.1.1')).toBeFalse(); - }); - - it('should compare patch components correctly', () => { - expect(pvp.isSame?.('patch', '1.0.0.0', '1.0.0.0')).toBeTrue(); - expect(pvp.isSame?.('patch', '1.0.0.0', '2.0.0.0')).toBeTrue(); - expect(pvp.isSame?.('patch', '1.0.0.0', '1.0.0.1')).toBeFalse(); - }); - - it('should return false when the given a invalid version', () => { - expect(pvp.isSame?.('minor', '', '0')).toBeFalse(); - }); + it.each` + type | a | b | expected + ${'major'} | ${'4.10'} | ${'4.1'} | ${false} + ${'major'} | ${'4.1.0'} | ${'5.1.0'} | ${false} + ${'major'} | ${'4.1'} | ${'5.1'} | ${false} + ${'major'} | ${'0'} | ${'1'} | ${false} + ${'major'} | ${'4.1'} | ${'4.1.0'} | ${true} + ${'major'} | ${'4.1.1'} | ${'4.1.2'} | ${true} + ${'major'} | ${'0'} | ${'0'} | ${true} + ${'minor'} | ${'4.1.0'} | ${'5.1.0'} | ${true} + ${'minor'} | ${'4.1'} | ${'4.1'} | ${true} + ${'minor'} | ${'4.1'} | ${'5.1'} | ${true} + ${'minor'} | ${'4.1.0'} | ${'4.1.1'} | ${false} + ${'minor'} | ${''} | ${'0'} | ${false} + ${'patch'} | ${'1.0.0.0'} | ${'1.0.0.0'} | ${true} + ${'patch'} | ${'1.0.0.0'} | ${'2.0.0.0'} | ${true} + ${'patch'} | ${'1.0.0.0'} | ${'1.0.0.1'} | ${false} + `( + 'pvp.isSame("$type", "$a", "$b") === $expected', + ({ type, a, b, expected }) => { + expect(pvp.isSame?.(type, a, b)).toBe(expected); + }, + ); }); describe('.isVersion(maybeRange)', () => { - it('should accept 1.0 as valid version', () => { - expect(pvp.isVersion('1.0')).toBeTrue(); - }); - - it('should reject >=1.0 && <1.1 as it is a range, not a version', () => { - expect(pvp.isVersion('>=1.0 && <1.1')).toBeFalse(); + it.each` + version | expected + ${'1.0'} | ${true} + ${'>=1.0 && <1.1'} | ${false} + `('pvp.isVersion("$version") === $expected', ({ version, expected }) => { + expect(pvp.isVersion(version)).toBe(expected); }); }); describe('.equals(a, b)', () => { - it('should regard 1.01 and 1.1 as equal', () => { - expect(pvp.equals('1.01', '1.1')).toBeTrue(); - }); - - it('should regard 1.01 and 1.0 are not equal', () => { - expect(pvp.equals('1.01', '1.0')).toBeFalse(); + it.each` + a | b | expected + ${'1.01'} | ${'1.1'} | ${true} + ${'1.01'} | ${'1.0'} | ${false} + `('pvp.equals("$a", "$b") === $expected', ({ a, b, expected }) => { + expect(pvp.equals(a, b)).toBe(expected); }); }); describe('.isSingleVersion(range)', () => { - it('should consider ==1.0 a single version', () => { - expect(pvp.isSingleVersion('==1.0')).toBeTrue(); - }); - - it('should return false for ranges', () => { - expect(pvp.isSingleVersion('>=1.0 && <1.1')).toBeFalse(); - }); + it.each` + version | expected + ${'==1.0'} | ${true} + ${'>=1.0 && <1.1'} | ${false} + `( + 'pvp.isSingleVersion("$version") === $expected', + ({ version, expected }) => { + expect(pvp.isSingleVersion(version)).toBe(expected); + }, + ); }); describe('.subset(subRange, superRange)', () => { - it('1.1-1.2 is inside 1.0-2.0', () => { - expect(pvp.subset?.('>=1.0 && <1.1', '>=1.0 && <2.0')).toBeTrue(); - }); - - it('1.0-2.0 is inside 1.0-2.0', () => { - expect(pvp.subset?.('>=1.0 && <2.0', '>=1.0 && <2.0')).toBeTrue(); - }); - - it('1.0-2.1 outside 1.0-2.0', () => { - expect(pvp.subset?.('>=1.0 && <2.1', '>=1.0 && <2.0')).toBeFalse(); - }); - - it('0.9-2.0 outside 1.0-2.0', () => { - expect(pvp.subset?.('>=0.9 && <2.1', '>=1.0 && <2.0')).toBeFalse(); - }); - - it("returns undefined when passed a range that can't be parsed", () => { - expect(pvp.subset?.('gibberish', '')).toBeUndefined(); - }); + it.each` + subRange | superRange | expected + ${'>=1.0 && <1.1'} | ${'>=1.0 && <2.0'} | ${true} + ${'>=1.0 && <2.0'} | ${'>=1.0 && <2.0'} | ${true} + ${'>=1.0 && <2.1'} | ${'>=1.0 && <2.0'} | ${false} + ${'>=0.9 && <2.1'} | ${'>=1.0 && <2.0'} | ${false} + ${'gibberish'} | ${''} | ${undefined} + `( + 'pvp.subbet("$subRange", "$superRange") === $expected', + ({ subRange, superRange, expected }) => { + expect(pvp.subset?.(subRange, superRange)).toBe(expected); + }, + ); }); describe('.sortVersions()', () => { - it('should sort 1.0 and 1.1', () => { - expect(pvp.sortVersions('1.0', '1.1')).toBe(-1); - expect(pvp.sortVersions('1.1', '1.0')).toBe(1); - expect(pvp.sortVersions('1.0', '1.0')).toBe(0); + it.each` + a | b | expected + ${'1.0'} | ${'1.1'} | ${-1} + ${'1.1'} | ${'1.0'} | ${1} + ${'1.0'} | ${'1.0'} | ${0} + `('pvp.sortVersions("$a", "$b") === $expected', ({ a, b, expected }) => { + expect(pvp.sortVersions(a, b)).toBe(expected); }); }); From 1beeda4c402896ebc5e49d8d4b584d301955cfaa Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Sat, 9 Nov 2024 13:27:31 -0600 Subject: [PATCH 08/16] chore(versioning): PVP: Address PR review comments --- lib/modules/versioning/pvp/index.spec.ts | 93 +++++++++++----------- lib/modules/versioning/pvp/index.ts | 99 +++++++++++++++--------- lib/modules/versioning/pvp/readme.md | 6 ++ lib/modules/versioning/pvp/types.ts | 9 +++ 4 files changed, 126 insertions(+), 81 deletions(-) create mode 100644 lib/modules/versioning/pvp/types.ts diff --git a/lib/modules/versioning/pvp/index.spec.ts b/lib/modules/versioning/pvp/index.spec.ts index f4335dad912a91..92a9b3894ef2d1 100644 --- a/lib/modules/versioning/pvp/index.spec.ts +++ b/lib/modules/versioning/pvp/index.spec.ts @@ -1,32 +1,28 @@ -import pvp, { extractAllComponents, getComponents, parse } from '.'; +import pvp, { extractAllComponents, getComponents, parseRange } from '.'; describe('modules/versioning/pvp/index', () => { describe('.isGreaterThan(version, other)', () => { it.each` - first | second - ${'1.23.1'} | ${'1.9.6'} - ${'4.0.0'} | ${'3.0.0'} - ${'3.0.1'} | ${'3.0.0'} - ${'4.10'} | ${'4.1'} - ${'1.0.0'} | ${'1.0'} - `('pvp.isGreaterThan($first, $second)', ({ first, second }) => { - expect(pvp.isGreaterThan(first, second)).toBeTrue(); - }); - - it.each` - first | second - ${'2.0.2'} | ${'3.1.0'} - ${'3.0.0'} | ${'3.0.0'} - ${'4.1'} | ${'4.10'} - ${'1.0'} | ${'1.0.0'} - `('pvp.isGreaterThan($first, $second)', ({ first, second }) => { - expect(pvp.isGreaterThan(first, second)).toBeFalse(); + first | second | expected + ${'1.23.1'} | ${'1.9.6'} | ${true} + ${'4.0.0'} | ${'3.0.0'} | ${true} + ${'3.0.1'} | ${'3.0.0'} | ${true} + ${'4.10'} | ${'4.1'} | ${true} + ${'1.0.0'} | ${'1.0'} | ${true} + ${'2.0.2'} | ${'3.1.0'} | ${false} + ${'3.0.0'} | ${'3.0.0'} | ${false} + ${'4.1'} | ${'4.10'} | ${false} + ${'1.0'} | ${'1.0.0'} | ${false} + ${''} | ${'1.0'} | ${false} + ${'1.0'} | ${''} | ${false} + `('pvp.isGreaterThan($first, $second)', ({ first, second, expected }) => { + expect(pvp.isGreaterThan(first, second)).toBe(expected); }); }); - describe('.parse(range)', () => { + describe('.parseRange(range)', () => { it('should parse >=1.0 && <1.1', () => { - const parsed = parse('>=1.0 && <1.1'); + const parsed = parseRange('>=1.0 && <1.1'); expect(parsed).not.toBeNull(); expect(parsed!.lower).toBe('1.0'); expect(parsed!.upper).toBe('1.1'); @@ -39,6 +35,7 @@ describe('modules/versioning/pvp/index', () => { ${'1.0.0'} | ${1.0} ${'1.0.1'} | ${1.0} ${'1.1.1'} | ${1.1} + ${''} | ${null} `('pvp.getMajor("$version") === $expected', ({ version, expected }) => { expect(pvp.getMajor(version)).toBe(expected); }); @@ -58,11 +55,13 @@ describe('modules/versioning/pvp/index', () => { describe('.getPatch(version)', () => { it.each` - version | expected - ${'1.0.0'} | ${null} - ${'1.0.0.5.1'} | ${5.1} - ${'1.0.1.6'} | ${6} - ${'1.1.2.7'} | ${7} + version | expected + ${'1.0.0'} | ${null} + ${'1.0.0.5.1'} | ${5.1} + ${'1.0.1.6'} | ${6} + ${'1.1.2.7'} | ${7} + ${'0.0.0.0.1'} | ${0.1} + ${'0.0.0.0.10'} | ${0.1} `('pvp.getPatch("$version") === $expected', ({ version, expected }) => { expect(pvp.getPatch(version)).toBe(expected); }); @@ -126,7 +125,7 @@ describe('modules/versioning/pvp/index', () => { ${'4.0.0'} | ${'>=3.0 && <3.1'} | ${false} ${'3.1.0'} | ${'>=3.0 && <3.1'} | ${false} ${'3'} | ${'gibberish'} | ${false} - ${''} | ${'>=3.0 && <3.1'} | ${true} + ${''} | ${'>=3.0 && <3.1'} | ${false} `( 'pvp.isLessThanRange?.("$version", "$range") === $expected', ({ version, range, expected }) => { @@ -136,8 +135,8 @@ describe('modules/versioning/pvp/index', () => { }); describe('.extractAllComponents(version)', () => { - it('should return an empty array when there are no numbers', () => { - expect(extractAllComponents('')).toEqual([]); + it('should return null when there are no numbers', () => { + expect(extractAllComponents('')).toBeNull(); }); it('should parse 3.0', () => { @@ -188,22 +187,23 @@ describe('modules/versioning/pvp/index', () => { describe('.isSame(...)', () => { it.each` - type | a | b | expected - ${'major'} | ${'4.10'} | ${'4.1'} | ${false} - ${'major'} | ${'4.1.0'} | ${'5.1.0'} | ${false} - ${'major'} | ${'4.1'} | ${'5.1'} | ${false} - ${'major'} | ${'0'} | ${'1'} | ${false} - ${'major'} | ${'4.1'} | ${'4.1.0'} | ${true} - ${'major'} | ${'4.1.1'} | ${'4.1.2'} | ${true} - ${'major'} | ${'0'} | ${'0'} | ${true} - ${'minor'} | ${'4.1.0'} | ${'5.1.0'} | ${true} - ${'minor'} | ${'4.1'} | ${'4.1'} | ${true} - ${'minor'} | ${'4.1'} | ${'5.1'} | ${true} - ${'minor'} | ${'4.1.0'} | ${'4.1.1'} | ${false} - ${'minor'} | ${''} | ${'0'} | ${false} - ${'patch'} | ${'1.0.0.0'} | ${'1.0.0.0'} | ${true} - ${'patch'} | ${'1.0.0.0'} | ${'2.0.0.0'} | ${true} - ${'patch'} | ${'1.0.0.0'} | ${'1.0.0.1'} | ${false} + type | a | b | expected + ${'major'} | ${'4.10'} | ${'4.1'} | ${false} + ${'major'} | ${'4.1.0'} | ${'5.1.0'} | ${false} + ${'major'} | ${'4.1'} | ${'5.1'} | ${false} + ${'major'} | ${'0'} | ${'1'} | ${false} + ${'major'} | ${'4.1'} | ${'4.1.0'} | ${true} + ${'major'} | ${'4.1.1'} | ${'4.1.2'} | ${true} + ${'major'} | ${'0'} | ${'0'} | ${true} + ${'minor'} | ${'4.1.0'} | ${'5.1.0'} | ${true} + ${'minor'} | ${'4.1'} | ${'4.1'} | ${true} + ${'minor'} | ${'4.1'} | ${'5.1'} | ${true} + ${'minor'} | ${'4.1.0'} | ${'4.1.1'} | ${false} + ${'minor'} | ${''} | ${'0'} | ${false} + ${'patch'} | ${'1.0.0.0'} | ${'1.0.0.0'} | ${true} + ${'patch'} | ${'1.0.0.0'} | ${'2.0.0.0'} | ${true} + ${'patch'} | ${'1.0.0.0'} | ${'1.0.0.1'} | ${false} + ${'patch'} | ${'0.0.0.0.1'} | ${'0.0.0.0.10'} | ${false} `( 'pvp.isSame("$type", "$a", "$b") === $expected', ({ type, a, b, expected }) => { @@ -227,6 +227,8 @@ describe('modules/versioning/pvp/index', () => { a | b | expected ${'1.01'} | ${'1.1'} | ${true} ${'1.01'} | ${'1.0'} | ${false} + ${''} | ${'1.0'} | ${false} + ${'1.0'} | ${''} | ${false} `('pvp.equals("$a", "$b") === $expected', ({ a, b, expected }) => { expect(pvp.equals(a, b)).toBe(expected); }); @@ -253,6 +255,7 @@ describe('modules/versioning/pvp/index', () => { ${'>=1.0 && <2.1'} | ${'>=1.0 && <2.0'} | ${false} ${'>=0.9 && <2.1'} | ${'>=1.0 && <2.0'} | ${false} ${'gibberish'} | ${''} | ${undefined} + ${'>=. && <.'} | ${'>=. && <.'} | ${undefined} `( 'pvp.subbet("$subRange", "$superRange") === $expected', ({ subRange, superRange, expected }) => { diff --git a/lib/modules/versioning/pvp/index.ts b/lib/modules/versioning/pvp/index.ts index 9160ac864ef5ec..ebc0a82ce57281 100644 --- a/lib/modules/versioning/pvp/index.ts +++ b/lib/modules/versioning/pvp/index.ts @@ -2,6 +2,7 @@ import { logger } from '../../../logger'; import type { RangeStrategy } from '../../../types/versioning'; import { regEx } from '../../../util/regex'; import type { NewValueConfig, VersioningApi } from '../types'; +import type { Components, Range } from './types'; export const id = 'pvp'; export const displayName = 'Package Versioning Policy (Haskell)'; @@ -9,13 +10,12 @@ export const urls = []; export const supportsRanges = true; export const supportedRangeStrategies: RangeStrategy[] = ['auto']; -type Parsed = { lower: string; upper: string }; -type Components = { major: number[]; minor: number[]; patch: number[] }; +// This range format was chosen because it is common in the ecosystem +const gteAndLtRange = />=(?[\d.]+)&&<(?[\d.]+)/; -export function parse(input: string): Parsed | null { +export function parseRange(input: string): Range | null { const noSpaces = input.replaceAll(' ', ''); - const r = regEx(/>=(?[\d.]+)&&<(?[\d.]+)/); - const m = r.exec(noSpaces); + const m = regEx(gteAndLtRange).exec(noSpaces); if (!m?.groups) { return null; } @@ -25,13 +25,13 @@ export function parse(input: string): Parsed | null { }; } -export function extractAllComponents(version: string): number[] { +export function extractAllComponents(version: string): number[] | null { const versionMajor = version.split('.'); - const versionIntMajor: number[] = versionMajor.map((x) => parseInt(x, 10)); - const ret = []; + const versionIntMajor = versionMajor.map((x) => parseInt(x, 10)); + const ret: number[] = []; for (const l of versionIntMajor) { if (l < 0 || !isFinite(l)) { - continue; + return null; } ret.push(l); } @@ -66,42 +66,50 @@ function compareIntArray( function isGreaterThan(version: string, other: string): boolean { const versionIntMajor = extractAllComponents(version); const otherIntMajor = extractAllComponents(other); + if (versionIntMajor === null || otherIntMajor === null) { + return false; + } return compareIntArray(versionIntMajor, otherIntMajor) === 'gt'; } -function getMajor(version: string): number { +function getMajor(version: string): number | null { // This basically can't be implemented correctly, since // 1.1 and 1.10 become equal when converted to float. // Consumers should use isSame instead. - const l1 = version.split('.'); - return Number(l1.slice(0, 2).join('.')); + const components = getComponents(version); + if (components === null) { + return null; + } + return Number(components.major.join('.')); } function getMinor(version: string): number | null { - const l1 = version.split('.'); - if (l1.length < 3) { + const components = getComponents(version); + if (components === null || components.minor.length === 0) { return null; } - return Number(l1[2]); + return Number(components.minor.join('.')); } function getPatch(version: string): number | null { - const l1 = version.split('.'); - const components = l1.slice(3); - if (components.length === 0) { + const components = getComponents(version); + if (components === null || components.patch.length === 0) { return null; } - return Number(components[0] + '.' + components.slice(1).join('')); + return Number(components.patch[0] + '.' + components.patch.slice(1).join('')); } function matches(version: string, range: string): boolean { - const parsed = parse(range); + const parsed = parseRange(range); if (parsed === null) { return false; } const ver = extractAllComponents(version); const lower = extractAllComponents(parsed.lower); const upper = extractAllComponents(parsed.upper); + if (ver === null || lower === null || upper === null) { + return false; + } return ( 'gt' === compareIntArray(upper, ver) && ['eq', 'lt'].includes(compareIntArray(lower, ver)) @@ -111,10 +119,13 @@ function matches(version: string, range: string): boolean { function satisfyingVersion( versions: string[], range: string, - onGreaterThan: (isGreaterThan: boolean) => boolean, + reverse: boolean, ): string | null { const copy = versions.slice(0); - copy.sort((a, b) => (onGreaterThan(isGreaterThan(a, b)) ? -1 : 1)); + copy.sort((a, b) => { + const multiplier = reverse ? 1 : -1; + return sortVersions(a, b) * multiplier; + }); const result = copy.find((v) => matches(v, range)); return result ?? null; } @@ -123,29 +134,32 @@ function getSatisfyingVersion( versions: string[], range: string, ): string | null { - return satisfyingVersion(versions, range, (x) => x); + return satisfyingVersion(versions, range, false); } function minSatisfyingVersion( versions: string[], range: string, ): string | null { - return satisfyingVersion(versions, range, (x) => !x); + return satisfyingVersion(versions, range, true); } function isLessThanRange(version: string, range: string): boolean { - const parsed = parse(range); + const parsed = parseRange(range); if (parsed === null) { return false; } const compos = extractAllComponents(version); const lower = extractAllComponents(parsed.lower); + if (compos === null || lower === null) { + return false; + } return 'lt' === compareIntArray(compos, lower); } export function getComponents(splitOne: string): Components | null { const c = extractAllComponents(splitOne); - if (c.length === 0) { + if (c === null) { return null; } return { @@ -171,7 +185,7 @@ function getNewValue({ ); return null; } - const parsed = parse(currentValue); + const parsed = parseRange(currentValue); if (parsed === null) { logger.info( { currentValue, newVersion }, @@ -187,9 +201,11 @@ function getNewValue({ // the upper bound is already high enough return null; } - // isLessThanRange returns true when newVersion is invalid. so we can assert it non-null const compos = getComponents(newVersion); - const majorPlusOne = plusOne(compos!.major); + if (compos === null) { + return null; + } + const majorPlusOne = plusOne(compos.major); // istanbul ignore next: since all versions that can be parsed, can also be bumped, this can never happen if (!matches(newVersion, `>=${parsed.lower} && <${majorPlusOne}`)) { logger.warn( @@ -221,8 +237,8 @@ function isSame( } function subset(subRange: string, superRange: string): boolean | undefined { - const sub = parse(subRange); - const sup = parse(superRange); + const sub = parseRange(subRange); + const sup = parseRange(superRange); if (sub === null || sup === null) { return undefined; } @@ -230,6 +246,14 @@ function subset(subRange: string, superRange: string): boolean | undefined { const subUpper = extractAllComponents(sub.upper); const supLower = extractAllComponents(sup.lower); const supUpper = extractAllComponents(sup.upper); + if ( + subLower === null || + subUpper === null || + supLower === null || + supUpper === null + ) { + return undefined; + } if ('lt' === compareIntArray(subLower, supLower)) { return false; } @@ -240,11 +264,11 @@ function subset(subRange: string, superRange: string): boolean | undefined { } function isVersion(maybeRange: string | undefined | null): boolean { - return typeof maybeRange === 'string' && parse(maybeRange) === null; + return typeof maybeRange === 'string' && parseRange(maybeRange) === null; } function isValid(ver: string): boolean { - return extractAllComponents(ver).length >= 1; + return extractAllComponents(ver) !== null || parseRange(ver) !== null; } function isSingleVersion(range: string): boolean { @@ -254,9 +278,12 @@ function isSingleVersion(range: string): boolean { } function equals(a: string, b: string): boolean { - return ( - 'eq' === compareIntArray(extractAllComponents(a), extractAllComponents(b)) - ); + const aComponents = extractAllComponents(a); + const bComponents = extractAllComponents(b); + if (aComponents === null || bComponents === null) { + return false; + } + return 'eq' === compareIntArray(aComponents, bComponents); } function sortVersions(a: string, b: string): number { diff --git a/lib/modules/versioning/pvp/readme.md b/lib/modules/versioning/pvp/readme.md index ea9a8722b87eaf..241fe43d4707a6 100644 --- a/lib/modules/versioning/pvp/readme.md +++ b/lib/modules/versioning/pvp/readme.md @@ -5,5 +5,11 @@ version. That is, in `A.B.C`: - `A.B`: major version - `C`: minor +The remaining components are all considered parts of the patch version, and +they will be concatenated to form a `number`, i.e. IEEE 754 double. This means +that both `0.0.0.0.1` and `0.0.0.0.10` have patch version `0.1`. + The range syntax comes from Cabal, specifically the [build-depends section](https://cabal.readthedocs.io/en/3.10/cabal-package.html). + +This module is considered experimental since it has limited range support. diff --git a/lib/modules/versioning/pvp/types.ts b/lib/modules/versioning/pvp/types.ts new file mode 100644 index 00000000000000..f56602a86ad6f5 --- /dev/null +++ b/lib/modules/versioning/pvp/types.ts @@ -0,0 +1,9 @@ +export type Range = { + lower: string; + upper: string; +}; +export type Components = { + major: number[]; + minor: number[]; + patch: number[]; +}; From ffea9b2a8db7d701c5074de5dceb8b65813aeb56 Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Sun, 17 Nov 2024 11:33:01 -0600 Subject: [PATCH 09/16] chore(versioning): PVP: support ranges of form =W.X --- lib/modules/versioning/pvp/index.ts | 8 ++++++-- lib/modules/versioning/pvp/readme.md | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/modules/versioning/pvp/index.ts b/lib/modules/versioning/pvp/index.ts index ebc0a82ce57281..31998de8668695 100644 --- a/lib/modules/versioning/pvp/index.ts +++ b/lib/modules/versioning/pvp/index.ts @@ -12,12 +12,16 @@ export const supportedRangeStrategies: RangeStrategy[] = ['auto']; // This range format was chosen because it is common in the ecosystem const gteAndLtRange = />=(?[\d.]+)&&<(?[\d.]+)/; +const ltAndGteRange = /<(?[\d.]+)&&<(?[\d.]+)/; export function parseRange(input: string): Range | null { const noSpaces = input.replaceAll(' ', ''); - const m = regEx(gteAndLtRange).exec(noSpaces); + let m = regEx(gteAndLtRange).exec(noSpaces); if (!m?.groups) { - return null; + m = regEx(ltAndGteRange).exec(noSpaces); + if (!m?.groups) { + return null; + } } return { lower: m.groups['lower'], diff --git a/lib/modules/versioning/pvp/readme.md b/lib/modules/versioning/pvp/readme.md index 241fe43d4707a6..519546a2d59178 100644 --- a/lib/modules/versioning/pvp/readme.md +++ b/lib/modules/versioning/pvp/readme.md @@ -12,4 +12,6 @@ that both `0.0.0.0.1` and `0.0.0.0.10` have patch version `0.1`. The range syntax comes from Cabal, specifically the [build-depends section](https://cabal.readthedocs.io/en/3.10/cabal-package.html). -This module is considered experimental since it has limited range support. +This module is considered experimental since it only supports ranges of forms: +* `>=W.X && =W.X` From 65e107a5f914c2abdf9614cc4929dc171149e31d Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Sun, 17 Nov 2024 11:34:20 -0600 Subject: [PATCH 10/16] chore(versioning): PVP: test range <4.10 && >=4.1 Co-authored-by: Sebastian Poxhofer --- lib/modules/versioning/pvp/index.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/modules/versioning/pvp/index.spec.ts b/lib/modules/versioning/pvp/index.spec.ts index 92a9b3894ef2d1..01541928116389 100644 --- a/lib/modules/versioning/pvp/index.spec.ts +++ b/lib/modules/versioning/pvp/index.spec.ts @@ -74,6 +74,7 @@ describe('modules/versioning/pvp/index', () => { ${'4.1'} | ${'>=4.0 && <4.10'} | ${true} ${'4.1'} | ${'>=4.1 && <4.10'} | ${true} ${'4.1.0'} | ${'>=4.1 && <4.10'} | ${true} + ${'4.1.0'} | ${'<4.10 && >=4.1'} | ${true} ${'4.10'} | ${'>=4.1 && <4.10.0'} | ${true} ${'4.10'} | ${'>=4.0 && <4.10.1'} | ${true} ${'1.0.0'} | ${'>=2.0 && <2.1'} | ${false} From c0d1d666ed1403e750d8c87ced568d876d81acda Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Sun, 17 Nov 2024 11:45:00 -0600 Subject: [PATCH 11/16] chore(versioning): PVP: Fix markdown lint, fix reversed range regex --- lib/modules/versioning/pvp/index.ts | 2 +- lib/modules/versioning/pvp/readme.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/modules/versioning/pvp/index.ts b/lib/modules/versioning/pvp/index.ts index 31998de8668695..d7038fde130548 100644 --- a/lib/modules/versioning/pvp/index.ts +++ b/lib/modules/versioning/pvp/index.ts @@ -12,7 +12,7 @@ export const supportedRangeStrategies: RangeStrategy[] = ['auto']; // This range format was chosen because it is common in the ecosystem const gteAndLtRange = />=(?[\d.]+)&&<(?[\d.]+)/; -const ltAndGteRange = /<(?[\d.]+)&&<(?[\d.]+)/; +const ltAndGteRange = /<(?[\d.]+)&&>=(?[\d.]+)/; export function parseRange(input: string): Range | null { const noSpaces = input.replaceAll(' ', ''); diff --git a/lib/modules/versioning/pvp/readme.md b/lib/modules/versioning/pvp/readme.md index 519546a2d59178..e0c2b605b4ee7a 100644 --- a/lib/modules/versioning/pvp/readme.md +++ b/lib/modules/versioning/pvp/readme.md @@ -13,5 +13,6 @@ The range syntax comes from Cabal, specifically the [build-depends section](https://cabal.readthedocs.io/en/3.10/cabal-package.html). This module is considered experimental since it only supports ranges of forms: -* `>=W.X && =W.X` + +- `>=W.X && =W.X` From 0ad6e7fa743c10dac8ae3c316e566370267731dd Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Wed, 20 Nov 2024 12:02:31 +0100 Subject: [PATCH 12/16] chore(versioning): PVP: create files {range,util}{.spec,}.ts --- lib/modules/versioning/pvp/index.spec.ts | 31 +-------- lib/modules/versioning/pvp/index.ts | 84 +++--------------------- lib/modules/versioning/pvp/range.spec.ts | 12 ++++ lib/modules/versioning/pvp/range.ts | 21 ++++++ lib/modules/versioning/pvp/types.ts | 8 +-- lib/modules/versioning/pvp/util.spec.ts | 23 +++++++ lib/modules/versioning/pvp/util.ts | 55 ++++++++++++++++ 7 files changed, 125 insertions(+), 109 deletions(-) create mode 100644 lib/modules/versioning/pvp/range.spec.ts create mode 100644 lib/modules/versioning/pvp/range.ts create mode 100644 lib/modules/versioning/pvp/util.spec.ts create mode 100644 lib/modules/versioning/pvp/util.ts diff --git a/lib/modules/versioning/pvp/index.spec.ts b/lib/modules/versioning/pvp/index.spec.ts index 01541928116389..900baa3ae7ef8e 100644 --- a/lib/modules/versioning/pvp/index.spec.ts +++ b/lib/modules/versioning/pvp/index.spec.ts @@ -1,4 +1,4 @@ -import pvp, { extractAllComponents, getComponents, parseRange } from '.'; +import pvp from '.'; describe('modules/versioning/pvp/index', () => { describe('.isGreaterThan(version, other)', () => { @@ -20,15 +20,6 @@ describe('modules/versioning/pvp/index', () => { }); }); - describe('.parseRange(range)', () => { - it('should parse >=1.0 && <1.1', () => { - const parsed = parseRange('>=1.0 && <1.1'); - expect(parsed).not.toBeNull(); - expect(parsed!.lower).toBe('1.0'); - expect(parsed!.upper).toBe('1.1'); - }); - }); - describe('.getMajor(version)', () => { it.each` version | expected @@ -135,16 +126,6 @@ describe('modules/versioning/pvp/index', () => { ); }); - describe('.extractAllComponents(version)', () => { - it('should return null when there are no numbers', () => { - expect(extractAllComponents('')).toBeNull(); - }); - - it('should parse 3.0', () => { - expect(extractAllComponents('3.0')).toEqual([3, 0]); - }); - }); - describe('.isValid(version)', () => { it.each` version | expected @@ -176,16 +157,6 @@ describe('modules/versioning/pvp/index', () => { ); }); - describe('.getComponents(...)', () => { - it('"0" is valid major version', () => { - expect(getComponents('0')?.major).toEqual([0]); - }); - - it('returns null when no components could be extracted', () => { - expect(getComponents('')).toBeNull(); - }); - }); - describe('.isSame(...)', () => { it.each` type | a | b | expected diff --git a/lib/modules/versioning/pvp/index.ts b/lib/modules/versioning/pvp/index.ts index d7038fde130548..8b3b48471a834a 100644 --- a/lib/modules/versioning/pvp/index.ts +++ b/lib/modules/versioning/pvp/index.ts @@ -2,7 +2,13 @@ import { logger } from '../../../logger'; import type { RangeStrategy } from '../../../types/versioning'; import { regEx } from '../../../util/regex'; import type { NewValueConfig, VersioningApi } from '../types'; -import type { Components, Range } from './types'; +import { parseRange } from './range'; +import { + compareIntArray, + extractAllComponents, + getComponents, + plusOne, +} from './util'; export const id = 'pvp'; export const displayName = 'Package Versioning Policy (Haskell)'; @@ -10,62 +16,7 @@ export const urls = []; export const supportsRanges = true; export const supportedRangeStrategies: RangeStrategy[] = ['auto']; -// This range format was chosen because it is common in the ecosystem -const gteAndLtRange = />=(?[\d.]+)&&<(?[\d.]+)/; -const ltAndGteRange = /<(?[\d.]+)&&>=(?[\d.]+)/; - -export function parseRange(input: string): Range | null { - const noSpaces = input.replaceAll(' ', ''); - let m = regEx(gteAndLtRange).exec(noSpaces); - if (!m?.groups) { - m = regEx(ltAndGteRange).exec(noSpaces); - if (!m?.groups) { - return null; - } - } - return { - lower: m.groups['lower'], - upper: m.groups['upper'], - }; -} - -export function extractAllComponents(version: string): number[] | null { - const versionMajor = version.split('.'); - const versionIntMajor = versionMajor.map((x) => parseInt(x, 10)); - const ret: number[] = []; - for (const l of versionIntMajor) { - if (l < 0 || !isFinite(l)) { - return null; - } - ret.push(l); - } - return ret; -} - -function compareIntArray( - versionIntMajor: number[], - otherIntMajor: number[], -): 'lt' | 'eq' | 'gt' { - for ( - let i = 0; - i < Math.min(versionIntMajor.length, otherIntMajor.length); - i++ - ) { - if (versionIntMajor[i] > otherIntMajor[i]) { - return 'gt'; - } - if (versionIntMajor[i] < otherIntMajor[i]) { - return 'lt'; - } - } - if (versionIntMajor.length === otherIntMajor.length) { - return 'eq'; - } - if (versionIntMajor.length > otherIntMajor.length) { - return 'gt'; - } - return 'lt'; -} +const digitsAndDots = regEx(/^[\d.]+$/); function isGreaterThan(version: string, other: string): boolean { const versionIntMajor = extractAllComponents(version); @@ -161,22 +112,6 @@ function isLessThanRange(version: string, range: string): boolean { return 'lt' === compareIntArray(compos, lower); } -export function getComponents(splitOne: string): Components | null { - const c = extractAllComponents(splitOne); - if (c === null) { - return null; - } - return { - major: c.slice(0, 2), - minor: c.slice(2, 3), - patch: c.slice(3), - }; -} - -function plusOne(majorOne: number[]): string { - return `${majorOne[0]}.${majorOne[1] + 1}`; -} - function getNewValue({ currentValue, newVersion, @@ -277,8 +212,7 @@ function isValid(ver: string): boolean { function isSingleVersion(range: string): boolean { const noSpaces = range.trim(); - const r = regEx(/^[\d.]+$/); - return noSpaces.startsWith('==') && r.test(noSpaces.slice(2)); + return noSpaces.startsWith('==') && digitsAndDots.test(noSpaces.slice(2)); } function equals(a: string, b: string): boolean { diff --git a/lib/modules/versioning/pvp/range.spec.ts b/lib/modules/versioning/pvp/range.spec.ts new file mode 100644 index 00000000000000..52b9128d8843cf --- /dev/null +++ b/lib/modules/versioning/pvp/range.spec.ts @@ -0,0 +1,12 @@ +import { parseRange } from './range'; + +describe('modules/versioning/pvp/range', () => { + describe('.parseRange(range)', () => { + it('should parse >=1.0 && <1.1', () => { + const parsed = parseRange('>=1.0 && <1.1'); + expect(parsed).not.toBeNull(); + expect(parsed!.lower).toBe('1.0'); + expect(parsed!.upper).toBe('1.1'); + }); + }); +}); diff --git a/lib/modules/versioning/pvp/range.ts b/lib/modules/versioning/pvp/range.ts new file mode 100644 index 00000000000000..3fcc9c96b60ae2 --- /dev/null +++ b/lib/modules/versioning/pvp/range.ts @@ -0,0 +1,21 @@ +import { regEx } from '../../../util/regex'; +import type { Range } from './types'; + +// This range format was chosen because it is common in the ecosystem +const gteAndLtRange = />=(?[\d.]+)&&<(?[\d.]+)/; +const ltAndGteRange = /<(?[\d.]+)&&>=(?[\d.]+)/; + +export function parseRange(input: string): Range | null { + const noSpaces = input.replaceAll(' ', ''); + let m = regEx(gteAndLtRange).exec(noSpaces); + if (!m?.groups) { + m = regEx(ltAndGteRange).exec(noSpaces); + if (!m?.groups) { + return null; + } + } + return { + lower: m.groups['lower'], + upper: m.groups['upper'], + }; +} diff --git a/lib/modules/versioning/pvp/types.ts b/lib/modules/versioning/pvp/types.ts index f56602a86ad6f5..288fc6c6d9d546 100644 --- a/lib/modules/versioning/pvp/types.ts +++ b/lib/modules/versioning/pvp/types.ts @@ -1,9 +1,9 @@ -export type Range = { +export interface Range { lower: string; upper: string; -}; -export type Components = { +} +export interface Components { major: number[]; minor: number[]; patch: number[]; -}; +} diff --git a/lib/modules/versioning/pvp/util.spec.ts b/lib/modules/versioning/pvp/util.spec.ts new file mode 100644 index 00000000000000..3caaabd51ea5f6 --- /dev/null +++ b/lib/modules/versioning/pvp/util.spec.ts @@ -0,0 +1,23 @@ +import { extractAllComponents, getComponents } from './util'; + +describe('modules/versioning/pvp/util', () => { + describe('.extractAllComponents(version)', () => { + it('should return null when there are no numbers', () => { + expect(extractAllComponents('')).toBeNull(); + }); + + it('should parse 3.0', () => { + expect(extractAllComponents('3.0')).toEqual([3, 0]); + }); + }); + + describe('.getComponents(...)', () => { + it('"0" is valid major version', () => { + expect(getComponents('0')?.major).toEqual([0]); + }); + + it('returns null when no components could be extracted', () => { + expect(getComponents('')).toBeNull(); + }); + }); +}); diff --git a/lib/modules/versioning/pvp/util.ts b/lib/modules/versioning/pvp/util.ts new file mode 100644 index 00000000000000..7640df6097bb0a --- /dev/null +++ b/lib/modules/versioning/pvp/util.ts @@ -0,0 +1,55 @@ +import type { Components } from './types'; + +export function extractAllComponents(version: string): number[] | null { + const versionMajor = version.split('.'); + const versionIntMajor = versionMajor.map((x) => parseInt(x, 10)); + const ret: number[] = []; + for (const l of versionIntMajor) { + if (l < 0 || !isFinite(l)) { + return null; + } + ret.push(l); + } + return ret; +} + +export function getComponents(splitOne: string): Components | null { + const c = extractAllComponents(splitOne); + if (c === null) { + return null; + } + return { + major: c.slice(0, 2), + minor: c.slice(2, 3), + patch: c.slice(3), + }; +} + +export function plusOne(majorOne: number[]): string { + return `${majorOne[0]}.${majorOne[1] + 1}`; +} + +export function compareIntArray( + versionIntMajor: number[], + otherIntMajor: number[], +): 'lt' | 'eq' | 'gt' { + for ( + let i = 0; + i < Math.min(versionIntMajor.length, otherIntMajor.length); + i++ + ) { + if (versionIntMajor[i] > otherIntMajor[i]) { + return 'gt'; + } + if (versionIntMajor[i] < otherIntMajor[i]) { + return 'lt'; + } + } + if (versionIntMajor.length === otherIntMajor.length) { + return 'eq'; + } + if (versionIntMajor.length > otherIntMajor.length) { + return 'gt'; + } + return 'lt'; +} From 5fa3e31bd009b40e8555b67c47818a7388fdccf1 Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Fri, 22 Nov 2024 11:11:14 +0100 Subject: [PATCH 13/16] chore(versioning): PVP: rename components to parts --- lib/modules/versioning/pvp/index.ts | 68 ++++++++++++------------- lib/modules/versioning/pvp/readme.md | 4 +- lib/modules/versioning/pvp/types.ts | 2 +- lib/modules/versioning/pvp/util.spec.ts | 16 +++--- lib/modules/versioning/pvp/util.ts | 13 +++-- 5 files changed, 51 insertions(+), 52 deletions(-) diff --git a/lib/modules/versioning/pvp/index.ts b/lib/modules/versioning/pvp/index.ts index 8b3b48471a834a..89da608925c697 100644 --- a/lib/modules/versioning/pvp/index.ts +++ b/lib/modules/versioning/pvp/index.ts @@ -5,8 +5,8 @@ import type { NewValueConfig, VersioningApi } from '../types'; import { parseRange } from './range'; import { compareIntArray, - extractAllComponents, - getComponents, + extractAllParts, + getParts, plusOne, } from './util'; @@ -19,8 +19,8 @@ export const supportedRangeStrategies: RangeStrategy[] = ['auto']; const digitsAndDots = regEx(/^[\d.]+$/); function isGreaterThan(version: string, other: string): boolean { - const versionIntMajor = extractAllComponents(version); - const otherIntMajor = extractAllComponents(other); + const versionIntMajor = extractAllParts(version); + const otherIntMajor = extractAllParts(other); if (versionIntMajor === null || otherIntMajor === null) { return false; } @@ -31,27 +31,27 @@ function getMajor(version: string): number | null { // This basically can't be implemented correctly, since // 1.1 and 1.10 become equal when converted to float. // Consumers should use isSame instead. - const components = getComponents(version); - if (components === null) { + const parts = getParts(version); + if (parts === null) { return null; } - return Number(components.major.join('.')); + return Number(parts.major.join('.')); } function getMinor(version: string): number | null { - const components = getComponents(version); - if (components === null || components.minor.length === 0) { + const parts = getParts(version); + if (parts === null || parts.minor.length === 0) { return null; } - return Number(components.minor.join('.')); + return Number(parts.minor.join('.')); } function getPatch(version: string): number | null { - const components = getComponents(version); - if (components === null || components.patch.length === 0) { + const parts = getParts(version); + if (parts === null || parts.patch.length === 0) { return null; } - return Number(components.patch[0] + '.' + components.patch.slice(1).join('')); + return Number(parts.patch[0] + '.' + parts.patch.slice(1).join('')); } function matches(version: string, range: string): boolean { @@ -59,9 +59,9 @@ function matches(version: string, range: string): boolean { if (parsed === null) { return false; } - const ver = extractAllComponents(version); - const lower = extractAllComponents(parsed.lower); - const upper = extractAllComponents(parsed.upper); + const ver = extractAllParts(version); + const lower = extractAllParts(parsed.lower); + const upper = extractAllParts(parsed.upper); if (ver === null || lower === null || upper === null) { return false; } @@ -104,8 +104,8 @@ function isLessThanRange(version: string, range: string): boolean { if (parsed === null) { return false; } - const compos = extractAllComponents(version); - const lower = extractAllComponents(parsed.lower); + const compos = extractAllParts(version); + const lower = extractAllParts(parsed.lower); if (compos === null || lower === null) { return false; } @@ -140,7 +140,7 @@ function getNewValue({ // the upper bound is already high enough return null; } - const compos = getComponents(newVersion); + const compos = getParts(newVersion); if (compos === null) { return null; } @@ -161,17 +161,17 @@ function isSame( a: string, b: string, ): boolean { - const aComponents = getComponents(a); - const bComponents = getComponents(b); - if (aComponents === null || bComponents === null) { + const aParts = getParts(a); + const bParts = getParts(b); + if (aParts === null || bParts === null) { return false; } if (type === 'major') { - return 'eq' === compareIntArray(aComponents.major, bComponents.major); + return 'eq' === compareIntArray(aParts.major, bParts.major); } else if (type === 'minor') { - return 'eq' === compareIntArray(aComponents.minor, bComponents.minor); + return 'eq' === compareIntArray(aParts.minor, bParts.minor); } else { - return 'eq' === compareIntArray(aComponents.patch, bComponents.patch); + return 'eq' === compareIntArray(aParts.patch, bParts.patch); } } @@ -181,10 +181,10 @@ function subset(subRange: string, superRange: string): boolean | undefined { if (sub === null || sup === null) { return undefined; } - const subLower = extractAllComponents(sub.lower); - const subUpper = extractAllComponents(sub.upper); - const supLower = extractAllComponents(sup.lower); - const supUpper = extractAllComponents(sup.upper); + const subLower = extractAllParts(sub.lower); + const subUpper = extractAllParts(sub.upper); + const supLower = extractAllParts(sup.lower); + const supUpper = extractAllParts(sup.upper); if ( subLower === null || subUpper === null || @@ -207,7 +207,7 @@ function isVersion(maybeRange: string | undefined | null): boolean { } function isValid(ver: string): boolean { - return extractAllComponents(ver) !== null || parseRange(ver) !== null; + return extractAllParts(ver) !== null || parseRange(ver) !== null; } function isSingleVersion(range: string): boolean { @@ -216,12 +216,12 @@ function isSingleVersion(range: string): boolean { } function equals(a: string, b: string): boolean { - const aComponents = extractAllComponents(a); - const bComponents = extractAllComponents(b); - if (aComponents === null || bComponents === null) { + const aParts = extractAllParts(a); + const bParts = extractAllParts(b); + if (aParts === null || bParts === null) { return false; } - return 'eq' === compareIntArray(aComponents, bComponents); + return 'eq' === compareIntArray(aParts, bParts); } function sortVersions(a: string, b: string): number { diff --git a/lib/modules/versioning/pvp/readme.md b/lib/modules/versioning/pvp/readme.md index e0c2b605b4ee7a..c542f9c7d64414 100644 --- a/lib/modules/versioning/pvp/readme.md +++ b/lib/modules/versioning/pvp/readme.md @@ -1,11 +1,11 @@ [Package Versioning Policy](https://pvp.haskell.org/) is used with Haskell. -It's like semver, except that the first _two_ components are parts of the major +It's like semver, except that the first _two_ parts are of the major version. That is, in `A.B.C`: - `A.B`: major version - `C`: minor -The remaining components are all considered parts of the patch version, and +The remaining parts are all considered of the patch version, and they will be concatenated to form a `number`, i.e. IEEE 754 double. This means that both `0.0.0.0.1` and `0.0.0.0.10` have patch version `0.1`. diff --git a/lib/modules/versioning/pvp/types.ts b/lib/modules/versioning/pvp/types.ts index 288fc6c6d9d546..d2ab5e550debb4 100644 --- a/lib/modules/versioning/pvp/types.ts +++ b/lib/modules/versioning/pvp/types.ts @@ -2,7 +2,7 @@ export interface Range { lower: string; upper: string; } -export interface Components { +export interface Parts { major: number[]; minor: number[]; patch: number[]; diff --git a/lib/modules/versioning/pvp/util.spec.ts b/lib/modules/versioning/pvp/util.spec.ts index 3caaabd51ea5f6..c8f46e91e599bc 100644 --- a/lib/modules/versioning/pvp/util.spec.ts +++ b/lib/modules/versioning/pvp/util.spec.ts @@ -1,23 +1,23 @@ -import { extractAllComponents, getComponents } from './util'; +import { extractAllParts, getParts } from './util'; describe('modules/versioning/pvp/util', () => { - describe('.extractAllComponents(version)', () => { + describe('.extractAllParts(version)', () => { it('should return null when there are no numbers', () => { - expect(extractAllComponents('')).toBeNull(); + expect(extractAllParts('')).toBeNull(); }); it('should parse 3.0', () => { - expect(extractAllComponents('3.0')).toEqual([3, 0]); + expect(extractAllParts('3.0')).toEqual([3, 0]); }); }); - describe('.getComponents(...)', () => { + describe('.getParts(...)', () => { it('"0" is valid major version', () => { - expect(getComponents('0')?.major).toEqual([0]); + expect(getParts('0')?.major).toEqual([0]); }); - it('returns null when no components could be extracted', () => { - expect(getComponents('')).toBeNull(); + it('returns null when no parts could be extracted', () => { + expect(getParts('')).toBeNull(); }); }); }); diff --git a/lib/modules/versioning/pvp/util.ts b/lib/modules/versioning/pvp/util.ts index 7640df6097bb0a..2ea0115388a578 100644 --- a/lib/modules/versioning/pvp/util.ts +++ b/lib/modules/versioning/pvp/util.ts @@ -1,10 +1,9 @@ -import type { Components } from './types'; +import type { Parts } from './types'; -export function extractAllComponents(version: string): number[] | null { - const versionMajor = version.split('.'); - const versionIntMajor = versionMajor.map((x) => parseInt(x, 10)); +export function extractAllParts(version: string): number[] | null { + const parts = version.split('.').map((x) => parseInt(x, 10)); const ret: number[] = []; - for (const l of versionIntMajor) { + for (const l of parts) { if (l < 0 || !isFinite(l)) { return null; } @@ -13,8 +12,8 @@ export function extractAllComponents(version: string): number[] | null { return ret; } -export function getComponents(splitOne: string): Components | null { - const c = extractAllComponents(splitOne); +export function getParts(splitOne: string): Parts | null { + const c = extractAllParts(splitOne); if (c === null) { return null; } From fe928952b2c2473f4d57afc1e767dd7bc26aa31f Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Fri, 22 Nov 2024 11:33:41 +0100 Subject: [PATCH 14/16] chore(versioning): PVP: use regEx helper at top level, rename binder --- lib/modules/versioning/pvp/range.ts | 8 ++++---- lib/modules/versioning/pvp/util.ts | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/modules/versioning/pvp/range.ts b/lib/modules/versioning/pvp/range.ts index 3fcc9c96b60ae2..7bf77b2c43758f 100644 --- a/lib/modules/versioning/pvp/range.ts +++ b/lib/modules/versioning/pvp/range.ts @@ -2,14 +2,14 @@ import { regEx } from '../../../util/regex'; import type { Range } from './types'; // This range format was chosen because it is common in the ecosystem -const gteAndLtRange = />=(?[\d.]+)&&<(?[\d.]+)/; -const ltAndGteRange = /<(?[\d.]+)&&>=(?[\d.]+)/; +const gteAndLtRange = regEx(/>=(?[\d.]+)&&<(?[\d.]+)/); +const ltAndGteRange = regEx(/<(?[\d.]+)&&>=(?[\d.]+)/); export function parseRange(input: string): Range | null { const noSpaces = input.replaceAll(' ', ''); - let m = regEx(gteAndLtRange).exec(noSpaces); + let m = gteAndLtRange.exec(noSpaces); if (!m?.groups) { - m = regEx(ltAndGteRange).exec(noSpaces); + m = ltAndGteRange.exec(noSpaces); if (!m?.groups) { return null; } diff --git a/lib/modules/versioning/pvp/util.ts b/lib/modules/versioning/pvp/util.ts index 2ea0115388a578..d4e4cfe8ac6ad3 100644 --- a/lib/modules/versioning/pvp/util.ts +++ b/lib/modules/versioning/pvp/util.ts @@ -29,25 +29,25 @@ export function plusOne(majorOne: number[]): string { } export function compareIntArray( - versionIntMajor: number[], - otherIntMajor: number[], + versionPartsInt: number[], + otherPartsInt: number[], ): 'lt' | 'eq' | 'gt' { for ( let i = 0; - i < Math.min(versionIntMajor.length, otherIntMajor.length); + i < Math.min(versionPartsInt.length, otherPartsInt.length); i++ ) { - if (versionIntMajor[i] > otherIntMajor[i]) { + if (versionPartsInt[i] > otherPartsInt[i]) { return 'gt'; } - if (versionIntMajor[i] < otherIntMajor[i]) { + if (versionPartsInt[i] < otherPartsInt[i]) { return 'lt'; } } - if (versionIntMajor.length === otherIntMajor.length) { + if (versionPartsInt.length === otherPartsInt.length) { return 'eq'; } - if (versionIntMajor.length > otherIntMajor.length) { + if (versionPartsInt.length > otherPartsInt.length) { return 'gt'; } return 'lt'; From 6d1d708dfc0ad1f8088fd6a358c46b3a6aba5399 Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Fri, 22 Nov 2024 11:35:14 +0100 Subject: [PATCH 15/16] chore(versioning): PVP: run prettier --- lib/modules/versioning/pvp/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/modules/versioning/pvp/index.ts b/lib/modules/versioning/pvp/index.ts index 89da608925c697..8f0c8bae294078 100644 --- a/lib/modules/versioning/pvp/index.ts +++ b/lib/modules/versioning/pvp/index.ts @@ -3,12 +3,7 @@ import type { RangeStrategy } from '../../../types/versioning'; import { regEx } from '../../../util/regex'; import type { NewValueConfig, VersioningApi } from '../types'; import { parseRange } from './range'; -import { - compareIntArray, - extractAllParts, - getParts, - plusOne, -} from './util'; +import { compareIntArray, extractAllParts, getParts, plusOne } from './util'; export const id = 'pvp'; export const displayName = 'Package Versioning Policy (Haskell)'; From 4c1b565e4aa60b4d48c7d9e498fb829dfc060e48 Mon Sep 17 00:00:00 2001 From: Michael Kriese Date: Fri, 6 Dec 2024 10:11:07 +0100 Subject: [PATCH 16/16] Update lib/modules/versioning/pvp/index.ts --- lib/modules/versioning/pvp/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/versioning/pvp/index.ts b/lib/modules/versioning/pvp/index.ts index 8f0c8bae294078..59e8b3026ab91c 100644 --- a/lib/modules/versioning/pvp/index.ts +++ b/lib/modules/versioning/pvp/index.ts @@ -7,7 +7,7 @@ import { compareIntArray, extractAllParts, getParts, plusOne } from './util'; export const id = 'pvp'; export const displayName = 'Package Versioning Policy (Haskell)'; -export const urls = []; +export const urls = ['https://pvp.haskell.org']; export const supportsRanges = true; export const supportedRangeStrategies: RangeStrategy[] = ['auto'];