diff --git a/lib/modules/versioning/ruby/index.spec.ts b/lib/modules/versioning/ruby/index.spec.ts index fec50cc579eca8c9a7dd6e9f2668335b3866b9e8..474111a83f7042ab4e999d6638fe7ea3ce21d75b 100644 --- a/lib/modules/versioning/ruby/index.spec.ts +++ b/lib/modules/versioning/ruby/index.spec.ts @@ -265,24 +265,44 @@ describe('modules/versioning/ruby/index', () => { ${"'>= 3.0.5', '< 3.2'"} | ${'replace'} | ${'3.1.5'} | ${'3.2.1'} | ${"'>= 3.0.5', '< 3.3'"} ${"'0.0.10'"} | ${'auto'} | ${'0.0.10'} | ${'0.0.11'} | ${"'0.0.11'"} ${"'0.0.10'"} | ${'replace'} | ${'0.0.10'} | ${'0.0.11'} | ${"'0.0.11'"} + ${'>= 3.2, < 5.0'} | ${'bump'} | ${'4.0.2'} | ${'6.0.1'} | ${'>= 6.0.1, < 6.0.2'} + ${'~> 5.2, >= 5.2.5'} | ${'bump'} | ${'5.3.0'} | ${'6.0.0'} | ${'~> 6.0'} + ${'~> 5.2, >= 5.2.5'} | ${'bump'} | ${'5.3.0'} | ${'6.0.1'} | ${'~> 6.0, >= 6.0.1'} + ${'~> 5.2.0, >= 5.2.5'} | ${'bump'} | ${'5.2.5'} | ${'5.3.1'} | ${'~> 5.3.1'} + ${'4.2.0'} | ${'bump'} | ${'4.2.0'} | ${'4.2.5.1'} | ${'4.2.5.1'} + ${'4.2.5.1'} | ${'bump'} | ${'0.1'} | ${'4.3.0'} | ${'4.3.0'} + ${'~> 1'} | ${'bump'} | ${'1.2.0'} | ${'2.0.3'} | ${'~> 2, >= 2.0.3'} + ${'= 5.2.2'} | ${'bump'} | ${'5.2.2'} | ${'5.2.2.1'} | ${'= 5.2.2.1'} ${'1.0.3'} | ${'bump'} | ${'1.0.3'} | ${'1.2.3'} | ${'1.2.3'} ${'v1.0.3'} | ${'bump'} | ${'1.0.3'} | ${'1.2.3'} | ${'v1.2.3'} ${'= 1.0.3'} | ${'bump'} | ${'1.0.3'} | ${'1.2.3'} | ${'= 1.2.3'} - ${'!= 1.0.3'} | ${'bump'} | ${'1.0.0'} | ${'1.2.3'} | ${'!= 1.0.3'} + ${'!= 1.0.3'} | ${'bump'} | ${'1.0.0'} | ${'1.2.3'} | ${'>= 1.2.3'} + ${'!= 1.0.3'} | ${'bump'} | ${'1.0.0'} | ${'1.0.2'} | ${'!= 1.0.3'} + ${'!= 1.0.3'} | ${'bump'} | ${'1.0.0'} | ${'1.0.3'} | ${'!= 1.0.3'} ${'> 1.0.3'} | ${'bump'} | ${'1.0.4'} | ${'1.2.3'} | ${'> 1.2.2'} - ${'> 1.2.3'} | ${'bump'} | ${'1.0.0'} | ${'1.0.3'} | ${'> 1.2.3'} + ${'> 1.2.3'} | ${'bump'} | ${'1.0.0'} | ${'1.0.3'} | ${'> 1.0.2'} ${'< 1.0.3'} | ${'bump'} | ${'1.0.0'} | ${'1.2.3'} | ${'< 1.2.4'} ${'< 1.2.3'} | ${'bump'} | ${'1.0.0'} | ${'1.0.3'} | ${'< 1.2.3'} ${'< 1.2.2'} | ${'bump'} | ${'1.0.0'} | ${'1.2.3'} | ${'< 1.2.4'} ${'< 1.2.3'} | ${'bump'} | ${'1.0.0'} | ${'1.2.3'} | ${'< 1.2.4'} ${'< 1.2'} | ${'bump'} | ${'1.0.0'} | ${'1.2.3'} | ${'< 1.3'} ${'< 1'} | ${'bump'} | ${'0.9.0'} | ${'1.2.3'} | ${'< 2'} + ${'< 1.2.3'} | ${'bump'} | ${'1.0.0'} | ${'1.2.2'} | ${'< 1.2.3'} ${'>= 1.0.3'} | ${'bump'} | ${'1.0.3'} | ${'1.2.3'} | ${'>= 1.2.3'} + ${'>= 1.0.3'} | ${'bump'} | ${'1.0.3'} | ${'1.0.2'} | ${'>= 1.0.2'} ${'<= 1.0.3'} | ${'bump'} | ${'1.0.3'} | ${'1.2.3'} | ${'<= 1.2.3'} - ${'~> 1.0.3'} | ${'bump'} | ${'1.0.3'} | ${'1.2.3'} | ${'~> 1.2.0'} - ${'~> 1.0.3'} | ${'bump'} | ${'1.0.3'} | ${'1.0.4'} | ${'~> 1.0.0'} - ${'~> 4.7, >= 4.7.4'} | ${'bump'} | ${'4.7.5'} | ${'4.7.9'} | ${'~> 4.7.0, >= 4.7.9'} + ${'<= 1.0.3'} | ${'bump'} | ${'1.0.0'} | ${'1.0.2'} | ${'<= 1.0.3'} + ${'~> 1.0.3'} | ${'bump'} | ${'1.0.3'} | ${'1.2.3'} | ${'~> 1.2.3'} + ${'~> 1.0.3'} | ${'bump'} | ${'1.0.3'} | ${'1.0.4'} | ${'~> 1.0.4'} + ${'~> 4.7, >= 4.7.4'} | ${'bump'} | ${'4.7.5'} | ${'4.7.9'} | ${'~> 4.7, >= 4.7.9'} + ${'~> 4.7, >= 4.7.4'} | ${'bump'} | ${'4.7.5'} | ${'4.8.0'} | ${'~> 4.8'} + ${'>= 2.0.0, <= 2.15'} | ${'bump'} | ${'2.15.0'} | ${'2.20.1'} | ${'>= 2.20.1, <= 2.20.1'} + ${'~> 5.2.0'} | ${'bump'} | ${'5.2.4.1'} | ${'6.0.2.1'} | ${'~> 6.0.2, >= 6.0.2.1'} + ${'~> 4.0, < 5'} | ${'bump'} | ${'4.7.5'} | ${'5.0.0'} | ${'~> 5.0, < 6'} + ${'~> 4.0, < 5'} | ${'bump'} | ${'4.7.5'} | ${'5.0.1'} | ${'~> 5.0, >= 5.0.1, < 6'} + ${'~> 4.0, < 5'} | ${'bump'} | ${'4.7.5'} | ${'5.1.0'} | ${'~> 5.1, < 6'} ${'>= 3.2, < 5.0'} | ${'replace'} | ${'4.0.2'} | ${'6.0.1'} | ${'>= 3.2, < 6.0.2'} + ${'~> 5.2, >= 5.2.5'} | ${'replace'} | ${'5.3.0'} | ${'6.0.0'} | ${'~> 6.0, >= 6.0.0'} ${'~> 5.2, >= 5.2.5'} | ${'replace'} | ${'5.3.0'} | ${'6.0.1'} | ${'~> 6.0, >= 6.0.1'} ${'~> 5.2.0, >= 5.2.5'} | ${'replace'} | ${'5.2.5'} | ${'5.3.1'} | ${'~> 5.3.0, >= 5.3.1'} ${'4.2.0'} | ${'replace'} | ${'4.2.0'} | ${'4.2.5.1'} | ${'4.2.5.1'} @@ -293,23 +313,66 @@ describe('modules/versioning/ruby/index', () => { ${'v1.0.3'} | ${'replace'} | ${'1.0.3'} | ${'1.2.3'} | ${'v1.2.3'} ${'= 1.0.3'} | ${'replace'} | ${'1.0.3'} | ${'1.2.3'} | ${'= 1.2.3'} ${'!= 1.0.3'} | ${'replace'} | ${'1.0.0'} | ${'1.2.3'} | ${'!= 1.0.3'} + ${'!= 1.0.3'} | ${'replace'} | ${'1.0.0'} | ${'1.0.2'} | ${'!= 1.0.3'} + ${'!= 1.0.3'} | ${'replace'} | ${'1.0.0'} | ${'1.0.3'} | ${'!= 1.0.3'} + ${'> 1.0.3'} | ${'replace'} | ${'1.0.4'} | ${'1.2.3'} | ${'> 1.0.3'} + ${'> 1.2.3'} | ${'replace'} | ${'1.0.0'} | ${'1.0.3'} | ${'> 1.0.2'} ${'< 1.0.3'} | ${'replace'} | ${'1.0.0'} | ${'1.2.3'} | ${'< 1.2.4'} + ${'< 1.2.3'} | ${'replace'} | ${'1.0.0'} | ${'1.0.3'} | ${'< 1.2.3'} ${'< 1.2.2'} | ${'replace'} | ${'1.0.0'} | ${'1.2.3'} | ${'< 1.2.4'} ${'< 1.2.3'} | ${'replace'} | ${'1.0.0'} | ${'1.2.3'} | ${'< 1.2.4'} ${'< 1.2'} | ${'replace'} | ${'1.0.0'} | ${'1.2.3'} | ${'< 1.3'} ${'< 1'} | ${'replace'} | ${'0.9.0'} | ${'1.2.3'} | ${'< 2'} ${'< 1.2.3'} | ${'replace'} | ${'1.0.0'} | ${'1.2.2'} | ${'< 1.2.3'} ${'>= 1.0.3'} | ${'replace'} | ${'1.0.3'} | ${'1.2.3'} | ${'>= 1.0.3'} + ${'>= 1.0.3'} | ${'replace'} | ${'1.0.3'} | ${'1.0.2'} | ${'>= 1.0.2'} ${'<= 1.0.3'} | ${'replace'} | ${'1.0.0'} | ${'1.2.3'} | ${'<= 1.2.3'} ${'<= 1.0.3'} | ${'replace'} | ${'1.0.0'} | ${'1.0.2'} | ${'<= 1.0.3'} ${'~> 1.0.3'} | ${'replace'} | ${'1.0.0'} | ${'1.2.3'} | ${'~> 1.2.0'} ${'~> 1.0.3'} | ${'replace'} | ${'1.0.0'} | ${'1.0.4'} | ${'~> 1.0.3'} ${'~> 4.7, >= 4.7.4'} | ${'replace'} | ${'1.0.0'} | ${'4.7.9'} | ${'~> 4.7, >= 4.7.4'} + ${'~> 4.7, >= 4.7.4'} | ${'replace'} | ${'4.7.5'} | ${'4.8.0'} | ${'~> 4.7, >= 4.7.4'} ${'>= 2.0.0, <= 2.15'} | ${'replace'} | ${'2.15.0'} | ${'2.20.1'} | ${'>= 2.0.0, <= 2.20.1'} ${'~> 5.2.0'} | ${'replace'} | ${'5.2.4.1'} | ${'6.0.2.1'} | ${'~> 6.0.0'} ${'~> 4.0, < 5'} | ${'replace'} | ${'4.7.5'} | ${'5.0.0'} | ${'~> 5.0, < 6'} ${'~> 4.0, < 5'} | ${'replace'} | ${'4.7.5'} | ${'5.0.1'} | ${'~> 5.0, < 6'} - ${'~> 4.0, < 5'} | ${'replace'} | ${'4.7.5'} | ${'5.1.0'} | ${'~> 5.1, < 6'} + ${'~> 4.0, < 5'} | ${'replace'} | ${'4.7.5'} | ${'5.1.0'} | ${'~> 5.0, < 6'} + ${'>= 3.2, < 5.0'} | ${'widen'} | ${'4.0.2'} | ${'6.0.1'} | ${'>= 3.2, < 6.0.2'} + ${'~> 5.2, >= 5.2.5'} | ${'widen'} | ${'5.3.0'} | ${'6.0.0'} | ${'>= 5.2.5, < 7'} + ${'~> 5.2, >= 5.2.5'} | ${'widen'} | ${'5.3.0'} | ${'6.0.1'} | ${'>= 5.2.5, < 7'} + ${'~> 5.2.0, >= 5.2.5'} | ${'widen'} | ${'5.2.5'} | ${'5.3.1'} | ${'>= 5.2.5, < 5.4'} + ${'4.2.0'} | ${'widen'} | ${'4.2.0'} | ${'4.2.5.1'} | ${'4.2.5.1'} + ${'4.2.5.1'} | ${'widen'} | ${'0.1'} | ${'4.3.0'} | ${'4.3.0'} + ${'~> 1'} | ${'widen'} | ${'1.2.0'} | ${'2.0.3'} | ${'>= 1, < 3'} + ${'= 5.2.2'} | ${'widen'} | ${'5.2.2'} | ${'5.2.2.1'} | ${'= 5.2.2.1'} + ${'1.0.3'} | ${'widen'} | ${'1.0.3'} | ${'1.2.3'} | ${'1.2.3'} + ${'v1.0.3'} | ${'widen'} | ${'1.0.3'} | ${'1.2.3'} | ${'v1.2.3'} + ${'= 1.0.3'} | ${'widen'} | ${'1.0.3'} | ${'1.2.3'} | ${'= 1.2.3'} + ${'!= 1.0.3'} | ${'widen'} | ${'1.0.0'} | ${'1.2.3'} | ${'!= 1.0.3'} + ${'!= 1.0.3'} | ${'widen'} | ${'1.0.0'} | ${'1.0.2'} | ${'!= 1.0.3'} + ${'!= 1.0.3'} | ${'widen'} | ${'1.0.0'} | ${'1.0.3'} | ${'!= 1.0.3'} + ${'> 1.0.3'} | ${'widen'} | ${'1.0.4'} | ${'1.2.3'} | ${'> 1.0.3'} + ${'> 1.2.3'} | ${'widen'} | ${'1.0.0'} | ${'1.0.3'} | ${'> 1.0.2'} + ${'< 1.0.3'} | ${'widen'} | ${'1.0.0'} | ${'1.2.3'} | ${'< 1.2.4'} + ${'< 1.2.3'} | ${'widen'} | ${'1.0.0'} | ${'1.0.3'} | ${'< 1.2.3'} + ${'< 1.2.2'} | ${'widen'} | ${'1.0.0'} | ${'1.2.3'} | ${'< 1.2.4'} + ${'< 1.2.3'} | ${'widen'} | ${'1.0.0'} | ${'1.2.3'} | ${'< 1.2.4'} + ${'< 1.2'} | ${'widen'} | ${'1.0.0'} | ${'1.2.3'} | ${'< 1.3'} + ${'< 1'} | ${'widen'} | ${'0.9.0'} | ${'1.2.3'} | ${'< 2'} + ${'< 1.2.3'} | ${'widen'} | ${'1.0.0'} | ${'1.2.2'} | ${'< 1.2.3'} + ${'>= 1.0.3'} | ${'widen'} | ${'1.0.3'} | ${'1.2.3'} | ${'>= 1.0.3'} + ${'>= 1.0.3'} | ${'widen'} | ${'1.0.3'} | ${'1.0.2'} | ${'>= 1.0.2'} + ${'<= 1.0.3'} | ${'widen'} | ${'1.0.0'} | ${'1.2.3'} | ${'<= 1.2.3'} + ${'<= 1.0.3'} | ${'widen'} | ${'1.0.0'} | ${'1.0.2'} | ${'<= 1.0.3'} + ${'~> 1.0.3'} | ${'widen'} | ${'1.0.0'} | ${'1.2.3'} | ${'>= 1.0.3, < 1.2.4'} + ${'~> 1.0.3'} | ${'widen'} | ${'1.0.0'} | ${'1.0.4'} | ${'~> 1.0.3'} + ${'~> 4.7, >= 4.7.4'} | ${'widen'} | ${'1.0.0'} | ${'4.7.9'} | ${'~> 4.7, >= 4.7.4'} + ${'~> 4.7, >= 4.7.4'} | ${'widen'} | ${'4.7.5'} | ${'4.8.0'} | ${'~> 4.7, >= 4.7.4'} + ${'>= 2.0.0, <= 2.15'} | ${'widen'} | ${'2.15.0'} | ${'2.20.1'} | ${'>= 2.0.0, <= 2.20.1'} + ${'~> 5.2.0'} | ${'widen'} | ${'5.2.4.1'} | ${'6.0.2.1'} | ${'>= 5.2.0, < 6.0.3'} + ${'~> 4.0, < 5'} | ${'widen'} | ${'4.7.5'} | ${'5.0.0'} | ${'>= 4.0, < 6, < 6'} + ${'~> 4.0, < 5'} | ${'widen'} | ${'4.7.5'} | ${'5.0.1'} | ${'>= 4.0, < 6, < 6'} + ${'~> 4.0, < 5'} | ${'widen'} | ${'4.7.5'} | ${'5.1.0'} | ${'>= 4.0, < 6, < 6'} ${'< 1.0.3'} | ${'auto'} | ${'1.0.3'} | ${'1.2.4'} | ${'< 1.2.5'} ${'< 1.0.3'} | ${'replace'} | ${'1.0.3'} | ${'1.2.4'} | ${'< 1.2.5'} ${'< 1.0.3'} | ${'widen'} | ${'1.0.3'} | ${'1.2.4'} | ${'< 1.2.5'} diff --git a/lib/modules/versioning/ruby/index.ts b/lib/modules/versioning/ruby/index.ts index 9be31daea9d9397ab7cb052a8304679c8fb330a3..7c5aebf13f59a738582d1687bd131e5d08098089 100644 --- a/lib/modules/versioning/ruby/index.ts +++ b/lib/modules/versioning/ruby/index.ts @@ -12,7 +12,7 @@ import { regEx } from '../../../util/regex'; import type { NewValueConfig, VersioningApi } from '../types'; import { isSingleOperator, isValidOperator } from './operator'; import { ltr, parse as parseRange } from './range'; -import { bump, pin, replace } from './strategies'; +import { bump, pin, replace, widen } from './strategies'; import { parse as parseVersion } from './version'; export const id = 'ruby'; @@ -127,13 +127,18 @@ const getNewValue = ({ newValue = bump({ range: vtrim(currentValue), to: vtrim(newVersion) }); break; case 'auto': - case 'widen': case 'replace': newValue = replace({ range: vtrim(currentValue), to: vtrim(newVersion), }); break; + case 'widen': + newValue = widen({ + range: vtrim(currentValue), + to: vtrim(newVersion), + }); + break; // istanbul ignore next default: logger.warn(`Unsupported strategy ${rangeStrategy}`); diff --git a/lib/modules/versioning/ruby/range.ts b/lib/modules/versioning/ruby/range.ts index 8e7533460f90702245bff188037b9546864f5e5d..02f8a0a1b778830b945094db2754ee5eda56990e 100644 --- a/lib/modules/versioning/ruby/range.ts +++ b/lib/modules/versioning/ruby/range.ts @@ -1,3 +1,4 @@ +import { satisfies } from '@renovatebot/ruby-semver'; import { parse as _parse } from '@renovatebot/ruby-semver/dist/ruby/requirement.js'; import { Version, create } from '@renovatebot/ruby-semver/dist/ruby/version.js'; import { logger } from '../../../logger'; @@ -8,6 +9,14 @@ export interface Range { version: string; operator: string; delimiter: string; + /** + * If the range is `~>` and immediately followed by `>=`, + * the latter range is considered the former's companion + * and assigned here instead of being an independent range. + * + * Example: `'~> 6.2', '>= 6.2.1'` + */ + companion?: Range; } const parse = (range: string): Range => { @@ -30,6 +39,60 @@ const parse = (range: string): Range => { }; }; +/** Wrapper for {@link satisfies} for {@link Range} record. */ +export function satisfiesRange(ver: string, range: Range): boolean { + if (range.companion) { + return ( + satisfies(ver, `${range.operator}${range.version}`) && + satisfiesRange(ver, range.companion) + ); + } else { + return satisfies(ver, `${range.operator}${range.version}`); + } +} + +/** + * Parses a comma-delimited list of range parts, + * with special treatment for a pair of `~>` and `>=` parts. + */ +export function parseRanges(range: string): Range[] { + const originalRanges = range.split(',').map(parse); + const ranges: Range[] = []; + for (let i = 0; i < originalRanges.length; ) { + if ( + i + 1 < originalRanges.length && + originalRanges[i].operator === PGTE && + originalRanges[i + 1].operator === GTE + ) { + ranges.push({ + ...originalRanges[i], + companion: originalRanges[i + 1], + }); + i += 2; + } else { + ranges.push(originalRanges[i]); + i++; + } + } + return ranges; +} + +/** + * Stringifies a list of range parts into a comma-separated string, + * with special treatment for a pair of `~>` and `>=` parts. + */ +export function stringifyRanges(ranges: Range[]): string { + return ranges + .map((r) => { + if (r.companion) { + return `${r.operator}${r.delimiter}${r.version}, ${r.companion.operator}${r.companion.delimiter}${r.companion.version}`; + } else { + return `${r.operator}${r.delimiter}${r.version}`; + } + }) + .join(', '); +} + type GemRequirement = [string, Version]; const ltr = (version: string, range: string): boolean => { diff --git a/lib/modules/versioning/ruby/strategies/bump.ts b/lib/modules/versioning/ruby/strategies/bump.ts index 5db8cb2456a555e5f2d136baadab07e2c18d5685..0167dc9edec598ec84f4da9bc29e33aceba26daa 100644 --- a/lib/modules/versioning/ruby/strategies/bump.ts +++ b/lib/modules/versioning/ruby/strategies/bump.ts @@ -1,35 +1,45 @@ -import { gte, lte } from '@renovatebot/ruby-semver'; -import { logger } from '../../../../logger'; -import { EQUAL, GT, GTE, LT, LTE, NOT_EQUAL, PGTE } from '../operator'; -import { parse as parseRange } from '../range'; -import { decrement, floor, increment } from '../version'; +import { gt, gte, lt } from '@renovatebot/ruby-semver'; +import { GTE, LT, LTE, NOT_EQUAL, PGTE } from '../operator'; +import { Range, parseRanges, stringifyRanges } from '../range'; +import { adapt, trimZeroes } from '../version'; +import { replacePart } from './replace'; export default ({ range, to }: { range: string; to: string }): string => { - const ranges = range.split(',').map(parseRange); - const results = ranges.map(({ operator, version: ver, delimiter }) => { + const parts = parseRanges(range).map((part): Range => { + const { operator, version: ver } = part; switch (operator) { - case GT: - return lte(to, ver) - ? `${GT}${delimiter}${ver}` - : `${GT}${delimiter}${decrement(to)}`; + // Update upper bound (`<` and `<=`) ranges only if the new version violates them case LT: - return gte(to, ver) - ? `${LT}${delimiter}${increment(ver, to)}` - : `${LT}${delimiter}${ver}`; - case PGTE: - return `${operator}${delimiter}${floor(to)}`; - case GTE: + return gte(to, ver) ? replacePart(part, to) : part; case LTE: - case EQUAL: - return `${operator}${delimiter}${to}`; + return gt(to, ver) ? replacePart(part, to) : part; + // `~>` ranges. + case PGTE: { + // Try to add / remove extra `>=` constraint. + const trimmed = adapt(to, ver); + if (trimZeroes(trimmed) === trimZeroes(to)) { + // E.g. `'~> 5.2', '>= 5.2.0'`. In this case the latter is redundant. + return { ...part, version: trimmed, companion: undefined }; + } else { + // E.g. `'~> 5.2', '>= 5.2.1'`. + return { + ...part, + version: trimmed, + companion: { operator: GTE, delimiter: ' ', version: to }, + }; + } + } case NOT_EQUAL: - return `${NOT_EQUAL}${delimiter}${ver}`; - // istanbul ignore next + if (lt(ver, to)) { + // The version to exclude is now out of range. + return { ...part, operator: GTE, version: to }; + } + return part; default: - logger.warn(`Unsupported operator '${operator}'`); - return null; + // For `=` and lower bound ranges, always keep it stick to the new version. + return replacePart(part, to); } }); - return results.join(', '); + return stringifyRanges(parts); }; diff --git a/lib/modules/versioning/ruby/strategies/index.ts b/lib/modules/versioning/ruby/strategies/index.ts index e30fd6d6652dfdb321a70daffd312082f21487ff..c08882ed2830d49ba29a6c63e1c1468e805ff7db 100644 --- a/lib/modules/versioning/ruby/strategies/index.ts +++ b/lib/modules/versioning/ruby/strategies/index.ts @@ -1,5 +1,6 @@ import bump from './bump'; import pin from './pin'; import replace from './replace'; +import widen from './widen'; -export { pin, bump, replace }; +export { pin, bump, replace, widen }; diff --git a/lib/modules/versioning/ruby/strategies/replace.ts b/lib/modules/versioning/ruby/strategies/replace.ts index acc6e859c021f72867d77bd8c374a1948eea6872..3b8f06bc2ea0e74e3c0a6fda8b29df4266d9bfa8 100644 --- a/lib/modules/versioning/ruby/strategies/replace.ts +++ b/lib/modules/versioning/ruby/strategies/replace.ts @@ -1,103 +1,52 @@ -import { satisfies } from '@renovatebot/ruby-semver'; import { logger } from '../../../../logger'; -import bump from './bump'; +import { EQUAL, GT, GTE, LT, LTE, NOT_EQUAL, PGTE } from '../operator'; +import { Range, parseRanges, satisfiesRange, stringifyRanges } from '../range'; +import { adapt, decrement, floor, increment } from '../version'; -function countInstancesOf(str: string, char: string): number { - return str.split(char).length - 1; -} - -function isMajorRange(range: string): boolean { - const splitRange = range.split(',').map((part) => part.trim()); - return ( - splitRange.length === 1 && - splitRange[0]?.startsWith('~>') && - countInstancesOf(splitRange[0], '.') === 0 - ); -} - -function isCommonRubyMajorRange(range: string): boolean { - const splitRange = range.split(',').map((part) => part.trim()); - return ( - splitRange.length === 2 && - splitRange[0]?.startsWith('~>') && - countInstancesOf(splitRange[0], '.') === 1 && - splitRange[1]?.startsWith('>=') - ); -} - -function isCommonRubyMinorRange(range: string): boolean { - const splitRange = range.split(',').map((part) => part.trim()); - return ( - splitRange.length === 2 && - splitRange[0]?.startsWith('~>') && - countInstancesOf(splitRange[0], '.') === 2 && - splitRange[1]?.startsWith('>=') - ); -} - -function reduceOnePrecision(version: string): string { - const versionParts = version.split('.'); - // istanbul ignore if - if (versionParts.length === 1) { - return version; +// Common logic for replace, widen, and bump strategies +// It basically makes the range stick to the new version. +export function replacePart(part: Range, to: string): Range { + const { operator, version: ver, companion } = part; + switch (operator) { + case LT: + return { ...part, version: increment(ver, to) }; + case LTE: + return { ...part, version: to }; + case PGTE: + if (companion) { + return { + ...part, + version: floor(adapt(to, ver)), + companion: { ...companion, version: to }, + }; + } else { + return { ...part, version: floor(adapt(to, ver)) }; + } + case GT: + return { ...part, version: decrement(to) }; + case GTE: + case EQUAL: + return { ...part, version: to }; + case NOT_EQUAL: + return part; + // istanbul ignore next + default: + logger.warn(`Unsupported operator '${operator}'`); + return { operator: '', delimiter: ' ', version: '' }; } - versionParts.pop(); - return versionParts.join('.'); } -export function matchPrecision(existing: string, next: string): string { - let res = next; - while (res.split('.').length > existing.split('.').length) { - res = reduceOnePrecision(res); - } - return res; -} - -export default ({ to, range }: { range: string; to: string }): string => { - if (satisfies(to, range)) { - return range; - } - let newRange; - if (isCommonRubyMajorRange(range)) { - const firstPart = reduceOnePrecision(to); - newRange = `~> ${firstPart}, >= ${to}`; - } else if (isCommonRubyMinorRange(range)) { - const firstPart = reduceOnePrecision(to) + '.0'; - newRange = `~> ${firstPart}, >= ${to}`; - } else if (isMajorRange(range)) { - const majorPart = to.split('.')[0]; - newRange = '~>' + (range.includes(' ') ? ' ' : '') + majorPart; - } else { - const lastPart = range - .split(',') - .map((part) => part.trim()) - .slice(-1) - .join(); - const lastPartPrecision = lastPart.split('.').length; - const toPrecision = to.split('.').length; - let massagedTo: string = to; - if (!lastPart.startsWith('<') && toPrecision > lastPartPrecision) { - massagedTo = to.split('.').slice(0, lastPartPrecision).join('.'); - } - const newLastPart = bump({ to: massagedTo, range: lastPart }); - newRange = range.replace(lastPart, newLastPart); - const firstPart = range - .split(',') - .map((part) => part.trim()) - .shift(); - if (firstPart && !satisfies(to, firstPart)) { - let newFirstPart = bump({ to: massagedTo, range: firstPart }); - newFirstPart = matchPrecision(firstPart, newFirstPart); - newRange = newRange.replace(firstPart, newFirstPart); +export default ({ range, to }: { range: string; to: string }): string => { + const parts = parseRanges(range).map((part): Range => { + if (satisfiesRange(to, part)) { + // The new version satisfies the range. Keep it as-is. + // Note that consecutive `~>` and `>=` parts are combined into one Range object, + // therefore both parts are updated if the new version violates one of them. + return part; } - } - // istanbul ignore if - if (!satisfies(to, newRange)) { - logger.warn( - { range, to, newRange }, - 'Ruby versioning getNewValue problem: to version is not satisfied by new range' - ); - return range; - } - return newRange; + + return replacePart(part, to); + }); + + return stringifyRanges(parts); }; diff --git a/lib/modules/versioning/ruby/strategies/widen.ts b/lib/modules/versioning/ruby/strategies/widen.ts new file mode 100644 index 0000000000000000000000000000000000000000..05ad8f0c032c9b643431044c4ab0265d8741ddeb --- /dev/null +++ b/lib/modules/versioning/ruby/strategies/widen.ts @@ -0,0 +1,31 @@ +import { GTE, LT, PGTE } from '../operator'; +import { Range, parseRanges, satisfiesRange, stringifyRanges } from '../range'; +import { increment, pgteUpperBound } from '../version'; +import { replacePart } from './replace'; + +export default ({ range, to }: { range: string; to: string }): string => { + const parts = parseRanges(range).flatMap((part): Range[] => { + if (satisfiesRange(to, part)) { + return [part]; + } + + const { operator, version: ver, companion } = part; + switch (operator) { + // `~>` works as both lower bound and upper bound. + // We need to decompose it to get wider range. + case PGTE: { + // Prefer constraints from `>=` + const baseVersion = companion ? companion.version : ver; + const limit = increment(pgteUpperBound(ver), to); + return [ + { operator: GTE, delimiter: ' ', version: baseVersion }, + { operator: LT, delimiter: ' ', version: limit }, + ]; + } + default: + return [replacePart(part, to)]; + } + }); + + return stringifyRanges(parts); +}; diff --git a/lib/modules/versioning/ruby/version.ts b/lib/modules/versioning/ruby/version.ts index 3e279fe8a9266a88bd2f90d5b1ae025ec2aeb959..cd421a6fea0a67132bc89742fa1dbe8e2f765ba9 100644 --- a/lib/modules/versioning/ruby/version.ts +++ b/lib/modules/versioning/ruby/version.ts @@ -28,11 +28,34 @@ const parse = (version: string): RubyVersion => ({ prerelease: prerelease(version), }); +const floor = (version: string): string => { + const segments = releaseSegments(version); + if (segments.length <= 1) { + // '~> 2' is equivalent to '~> 2.0', thus no need to floor + return segments.join('.'); + } + return [...segments.slice(0, -1), 0].join('.'); +}; + const adapt = (left: string, right: string): string => left.split('.').slice(0, right.split('.').length).join('.'); -const floor = (version: string): string => - [...releaseSegments(version).slice(0, -1), 0].join('.'); +const trimZeroes = (version: string): string => { + const segments = version.split('.'); + while (segments.length > 0 && segments[segments.length - 1] === '0') { + segments.pop(); + } + return segments.join('.'); +}; + +// Returns the upper bound of `~>` operator. +const pgteUpperBound = (version: string): string => { + const segments = releaseSegments(version); + if (segments.length > 1) { + segments.pop(); + } + return incrementLastSegment(segments.join('.')); +}; // istanbul ignore next const incrementLastSegment = (version: string): string => { @@ -119,4 +142,12 @@ const decrement = (version: string): string => { return nextSegments.reverse().join('.'); }; -export { parse, floor, increment, decrement }; +export { + parse, + adapt, + floor, + trimZeroes, + pgteUpperBound, + increment, + decrement, +};