diff --git a/lib/config/definitions.js b/lib/config/definitions.js index 4966f0a976214caff87e964f298ab2d0eaa13f6b..e98004294d16efccb098942793f4900f62c089df 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -404,6 +404,7 @@ const options = [ 'docker', 'hashicorp', 'hex', + 'ivy', 'loose', 'maven', 'node', diff --git a/lib/versioning/ivy/index.js b/lib/versioning/ivy/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ffcc1e883c3310d8104977fb00b2a88c415d639a --- /dev/null +++ b/lib/versioning/ivy/index.js @@ -0,0 +1,73 @@ +const { + equals, + getMajor, + getMinor, + getPatch, + isGreaterThan, + isSingleVersion, + isStable, + matches: mavenMatches, + maxSatisfyingVersion, + minSatisfyingVersion, + getNewValue, + sortVersions, +} = require('../maven/index'); + +const { TYPE_QUALIFIER, tokenize, isSubversion } = require('../maven/compare'); + +const { + REV_TYPE_LATEST, + REV_TYPE_SUBREV, + parseDynamicRevision, +} = require('./parse'); + +function isVersion(str) { + if (!str) { + return false; + } + return isSingleVersion(str) || !!parseDynamicRevision(str); +} + +function matches(a, b) { + if (!a) return false; + if (!b) return false; + const dynamicRevision = parseDynamicRevision(b); + if (!dynamicRevision) return equals(a, b); + const { type, value } = dynamicRevision; + + if (type === REV_TYPE_LATEST) { + if (!value) return true; + const tokens = tokenize(a); + if (tokens.length) { + const token = tokens[tokens.length - 1]; + if (token.type === TYPE_QUALIFIER) { + return token.val.toLowerCase() === value; + } + } + return false; + } + + if (type === REV_TYPE_SUBREV) { + return isSubversion(value, a); + } + + return mavenMatches(a, value); +} + +module.exports = { + equals, + getMajor, + getMinor, + getPatch, + isCompatible: isVersion, + isGreaterThan, + isSingleVersion, + isStable, + isValid: isVersion, + isVersion, + matches, + maxSatisfyingVersion, + minSatisfyingVersion, + getNewValue, + sortVersions, +}; diff --git a/lib/versioning/ivy/parse.js b/lib/versioning/ivy/parse.js new file mode 100644 index 0000000000000000000000000000000000000000..d08d80d18dc19e80fec67e759874bedf9c3cfec9 --- /dev/null +++ b/lib/versioning/ivy/parse.js @@ -0,0 +1,46 @@ +const { isSingleVersion, parseRange, rangeToStr } = require('../maven/compare'); + +const REV_TYPE_LATEST = 'REV_TYPE_LATEST'; +const REV_TYPE_SUBREV = 'REV_TYPE_SUBREVISION'; +const REV_TYPE_RANGE = 'REV_TYPE_RANGE'; + +function parseDynamicRevision(str) { + if (!str) return null; + + const LATEST_REGEX = /^latest\.|^latest$/i; + if (LATEST_REGEX.test(str)) { + const value = str.replace(LATEST_REGEX, '').toLowerCase() || null; + return { + type: REV_TYPE_LATEST, + value: value !== 'integration' ? value : null, + }; + } + + const SUBREV_REGEX = /\.\+$/; + if (SUBREV_REGEX.test(str)) { + const value = str.replace(SUBREV_REGEX, ''); + if (isSingleVersion(value)) { + return { + type: REV_TYPE_SUBREV, + value, + }; + } + } + + const range = parseRange(str); + if (range && range.length === 1) { + return { + type: REV_TYPE_RANGE, + value: rangeToStr(range), + }; + } + + return null; +} + +module.exports = { + REV_TYPE_LATEST, + REV_TYPE_SUBREV, + REV_TYPE_RANGE, + parseDynamicRevision, +}; diff --git a/lib/versioning/maven/compare.js b/lib/versioning/maven/compare.js index 5123672f7da719a77079b3571800b6b6f2dedfd9..e5efe85802570d22a2a8aeb06cae52e4ec4483b0 100644 --- a/lib/versioning/maven/compare.js +++ b/lib/versioning/maven/compare.js @@ -240,8 +240,10 @@ function parseRange(rangeStr) { return { leftType: null, leftValue: null, + leftBracket: null, rightType: null, rightValue: null, + rightBracket: null, }; } @@ -257,18 +259,22 @@ function parseRange(rangeStr) { result.push({ leftType: INCLUDING_POINT, leftValue: ver, + leftBracket: '[', rightType: INCLUDING_POINT, rightValue: ver, + rightBracket: ']', }); interval = emptyInterval(); } else if (subStr[0] === '[') { const ver = subStr.slice(1); interval.leftType = INCLUDING_POINT; interval.leftValue = ver; - } else if (subStr[0] === '(') { + interval.leftBracket = '['; + } else if (subStr[0] === '(' || subStr[0] === ']') { const ver = subStr.slice(1); interval.leftType = EXCLUDING_POINT; interval.leftValue = ver; + interval.leftBracket = subStr[0]; } else { result = null; } @@ -276,12 +282,14 @@ function parseRange(rangeStr) { const ver = subStr.slice(0, -1); interval.rightType = INCLUDING_POINT; interval.rightValue = ver; + interval.rightBracket = ']'; result.push(interval); interval = emptyInterval(); - } else if (/\)$/.test(subStr)) { + } else if (/\)$/.test(subStr) || /\[$/.test(subStr)) { const ver = subStr.slice(0, -1); interval.rightType = EXCLUDING_POINT; interval.rightValue = ver; + interval.rightBracket = /\)$/.test(subStr) ? ')' : '['; result.push(interval); interval = emptyInterval(); } else { @@ -324,26 +332,26 @@ function parseRange(rangeStr) { function rangeToStr(fullRange) { if (fullRange === null) return null; - const leftBracket = val => (val.leftType === INCLUDING_POINT ? '[' : '('); - const rightBracket = val => (val.rightType === INCLUDING_POINT ? ']' : ')'); const valToStr = val => (val === null ? '' : val); if (fullRange.length === 1) { - const val = fullRange[0]; - if (val.leftValue === val.rightValue) { - return `${leftBracket(val)}${valToStr(val.leftValue)}${rightBracket( - val - )}`; + const { leftBracket, rightBracket, leftValue, rightValue } = fullRange[0]; + if ( + leftValue === rightValue && + leftBracket === '[' && + rightBracket === ']' + ) { + return `[${valToStr(leftValue)}]`; } } const intervals = fullRange.map(val => [ - leftBracket(val), + val.leftBracket, valToStr(val.leftValue), ',', valToStr(val.rightValue), - rightBracket(val), + val.rightBracket, ].join('') ); return intervals.join(','); @@ -389,12 +397,31 @@ function autoExtendMavenRange(currentRepresentation, newValue) { return rangeToStr(range); } +function isSubversion(majorVersion, minorVersion) { + const majorTokens = tokenize(majorVersion); + const minorTokens = tokenize(minorVersion); + + let result = true; + const len = majorTokens.length; + for (let idx = 0; idx < len; idx += 1) { + const major = majorTokens[idx]; + const minor = minorTokens[idx] || nullFor(majorTokens[idx]); + const cmpResult = tokenCmp(major, minor); + if (cmpResult !== 0) { + result = false; + break; + } + } + return result; +} + module.exports = { PREFIX_DOT, PREFIX_HYPHEN, TYPE_NUMBER, TYPE_QUALIFIER, tokenize, + isSubversion, compare, isSingleVersion, isVersion, diff --git a/renovate-schema.json b/renovate-schema.json index c2bb05f900260468940fa2aaad7409c847e3491d..8179476d634016a53f7561a5488766e138fd5ae5 100644 --- a/renovate-schema.json +++ b/renovate-schema.json @@ -260,6 +260,7 @@ "docker", "hashicorp", "hex", + "ivy", "loose", "maven", "node", diff --git a/test/versioning/ivy.spec.js b/test/versioning/ivy.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..b9e86e05ce42d3389749f0019f0741b6dd710e18 --- /dev/null +++ b/test/versioning/ivy.spec.js @@ -0,0 +1,128 @@ +const { + REV_TYPE_LATEST, + REV_TYPE_SUBREV, + REV_TYPE_RANGE, + parseDynamicRevision, +} = require('../../lib/versioning/ivy/parse'); + +const { isVersion, matches } = require('../../lib/versioning/ivy/index'); + +describe('versioning/ivy/match', () => { + it('parses dynamic revisions', () => { + expect(parseDynamicRevision(null)).toBeNull(); + expect(parseDynamicRevision('')).toBeNull(); + + expect(parseDynamicRevision('latest')).toEqual({ + type: REV_TYPE_LATEST, + value: null, + }); + expect(parseDynamicRevision('latest.release')).toEqual({ + type: REV_TYPE_LATEST, + value: 'release', + }); + expect(parseDynamicRevision('latest.milestone')).toEqual({ + type: REV_TYPE_LATEST, + value: 'milestone', + }); + expect(parseDynamicRevision('latest.integration')).toEqual({ + type: REV_TYPE_LATEST, + value: null, + }); + + expect(parseDynamicRevision('.+')).toBeNull(); + expect(parseDynamicRevision('1.0.+')).toEqual({ + type: REV_TYPE_SUBREV, + value: '1.0', + }); + expect(parseDynamicRevision('1.2.3.+')).toEqual({ + type: REV_TYPE_SUBREV, + value: '1.2.3', + }); + + [ + '[1.0,2.0]', + '[1.0,2.0[', + ']1.0,2.0]', + ']1.0,2.0[', + '[1.0,)', + ']1.0,)', + '(,2.0]', + '(,2.0[', + ].forEach(value => { + expect(parseDynamicRevision(value)).toEqual({ + type: REV_TYPE_RANGE, + value, + }); + }); + + expect(parseDynamicRevision('[0,1),(1,)')).toBeNull(); + }); +}); + +describe('versioning/ivy/index', () => { + it('validates version string', () => { + expect(isVersion('')).toBe(false); + expect(isVersion('1.0.0')).toBe(true); + expect(isVersion('0')).toBe(true); + expect(isVersion('0.1-2-sp')).toBe(true); + expect(isVersion('1-final')).toBe(true); + expect(isVersion('v1.0.0')).toBe(true); + expect(isVersion('x1.0.0')).toBe(true); + expect(isVersion('2.1.1.RELEASE')).toBe(true); + expect(isVersion('Greenwich.SR1')).toBe(true); + expect(isVersion('.1')).toBe(false); + expect(isVersion('1.')).toBe(false); + expect(isVersion('-1')).toBe(false); + expect(isVersion('1-')).toBe(false); + + expect(isVersion('latest')).toBe(true); + expect(isVersion('latest.release')).toBe(true); + expect(isVersion('latest.milestone')).toBe(true); + expect(isVersion('latest.integration')).toBe(true); + expect(isVersion('1.0.+')).toBe(true); + expect(isVersion('1.0+')).toBe(false); + expect(isVersion(']0,1[')).toBe(true); + expect(isVersion('[0,1]')).toBe(true); + expect(isVersion('[0,1),(1,2]')).toBe(false); + }); + it('matches against dynamic revisions', () => { + expect(matches('', 'latest')).toBe(false); + expect(matches('0', '')).toBe(false); + expect(matches('0', 'latest')).toBe(true); + expect(matches('0', 'latest.integration')).toBe(true); + + expect(matches('0', 'latest.release')).toBe(false); + expect(matches('release', 'latest.release')).toBe(true); + expect(matches('0.release', 'latest.release')).toBe(true); + expect(matches('0-release', 'latest.release')).toBe(true); + expect(matches('0release', 'latest.release')).toBe(true); + expect(matches('0.RELEASE', 'latest.release')).toBe(true); + + expect(matches('0', 'latest.milestone')).toBe(false); + expect(matches('milestone', 'latest.milestone')).toBe(true); + expect(matches('0.milestone', 'latest.milestone')).toBe(true); + expect(matches('0-milestone', 'latest.milestone')).toBe(true); + expect(matches('0milestone', 'latest.milestone')).toBe(true); + expect(matches('0.MILESTONE', 'latest.milestone')).toBe(true); + + expect(matches('0', '1.0.+')).toBe(false); + expect(matches('1.1.0', '1.2.+')).toBe(false); + expect(matches('1.2.0', '1.2.+')).toBe(true); + expect(matches('1.2.milestone', '1.2.+')).toBe(true); + expect(matches('1.3', '1.2.+')).toBe(false); + + expect(matches('1', '1')).toBe(true); + expect(matches('1', '0')).toBe(false); + expect(matches('1', '[0,1]')).toBe(true); + expect(matches('0', '(0,1)')).toBe(false); + expect(matches('0', '(0,1[')).toBe(false); + expect(matches('0', ']0,1)')).toBe(false); + expect(matches('1', '(0,1)')).toBe(false); + expect(matches('1', '(0,2)')).toBe(true); + expect(matches('1', '[0,2]')).toBe(true); + expect(matches('1', '(,1]')).toBe(true); + expect(matches('1', '(,1)')).toBe(false); + expect(matches('1', '[1,)')).toBe(true); + expect(matches('1', '(1,)')).toBe(false); + }); +}); diff --git a/test/versioning/maven.spec.js b/test/versioning/maven.spec.js index 8a822ef85be39448cfbaf29aed00b445764c8595..4e7ee9438b6818f2e870a62c37470ac8b6062df9 100644 --- a/test/versioning/maven.spec.js +++ b/test/versioning/maven.spec.js @@ -122,68 +122,86 @@ describe('versioning/maven/compare', () => { { leftType: 'INCLUDING_POINT', leftValue: '1.0', + leftBracket: '[', rightType: 'INCLUDING_POINT', rightValue: '1.0', + rightBracket: ']', }, ], '(,1.0]': [ { leftType: 'EXCLUDING_POINT', leftValue: null, + leftBracket: '(', rightType: 'INCLUDING_POINT', rightValue: '1.0', + rightBracket: ']', }, ], '[1.2,1.3]': [ { leftType: 'INCLUDING_POINT', leftValue: '1.2', + leftBracket: '[', rightType: 'INCLUDING_POINT', rightValue: '1.3', + rightBracket: ']', }, ], '[1.0,2.0)': [ { leftType: 'INCLUDING_POINT', leftValue: '1.0', + leftBracket: '[', rightType: 'EXCLUDING_POINT', rightValue: '2.0', + rightBracket: ')', }, ], '[1.5,)': [ { leftType: 'INCLUDING_POINT', leftValue: '1.5', + leftBracket: '[', rightType: 'EXCLUDING_POINT', rightValue: null, + rightBracket: ')', }, ], '(,1.0],[1.2,)': [ { leftType: 'EXCLUDING_POINT', leftValue: null, + leftBracket: '(', rightType: 'INCLUDING_POINT', rightValue: '1.0', + rightBracket: ']', }, { leftType: 'INCLUDING_POINT', leftValue: '1.2', + leftBracket: '[', rightType: 'EXCLUDING_POINT', rightValue: null, + rightBracket: ')', }, ], '(,1.1),(1.1,)': [ { leftType: 'EXCLUDING_POINT', leftValue: null, + leftBracket: '(', rightType: 'EXCLUDING_POINT', rightValue: '1.1', + rightBracket: ')', }, { leftType: 'EXCLUDING_POINT', leftValue: '1.1', + leftBracket: '(', rightType: 'EXCLUDING_POINT', rightValue: null, + rightBracket: ')', }, ], }; @@ -205,9 +223,13 @@ describe('versioning/maven/compare', () => { ['[1.0.0,1.2.3]', '1.2.4', '[1.0.0,1.2.4]'], ['[1.0.0,1.2.23]', '1.1.0', '[1.0.0,1.2.23]'], ['(,1.0]', '2.0', '(,2.0]'], + ['],1.0]', '2.0', '],2.0]'], ['(,1.0)', '2.0', '(,2.0)'], + ['],1.0[', '2.0', '],2.0['], ['[1.0,1.2],[1.3,1.5)', '1.2.4', '[1.0,1.2.4],[1.3,1.5)'], + ['[1.0,1.2],[1.3,1.5[', '1.2.4', '[1.0,1.2.4],[1.3,1.5['], ['[1.2.3,)', '1.2.4', '[1.2.4,)'], + ['[1.2.3,[', '1.2.4', '[1.2.4,['], ['[1.2.3,]', '1.2.4', '[1.2.3,]'], // invalid range ]; sample.forEach(([oldRepr, newValue, newRepr]) => {