From 94cdf2cd3ea41de1bd0a9e41de602b5b25ed5e7f Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@arkins.net> Date: Fri, 8 Jun 2018 08:44:16 +0200 Subject: [PATCH] feat: composer range support (#2099) Adds range support for composer. Mostly leverages existing npm semver range support, but massages where necessary to support Composer differences. Closes #2097 --- lib/manager/composer/extract.js | 9 +- lib/versioning/index.js | 2 + lib/versioning/semver-composer/index.js | 66 ++++++++++++ .../__snapshots__/extract.spec.js.snap | 81 +++++++------- test/versioning/semver-composer.spec.js | 102 ++++++++++++++++++ 5 files changed, 215 insertions(+), 45 deletions(-) create mode 100644 lib/versioning/semver-composer/index.js create mode 100644 test/versioning/semver-composer.spec.js diff --git a/lib/manager/composer/extract.js b/lib/manager/composer/extract.js index aa54acefa2..7b0fc76580 100644 --- a/lib/manager/composer/extract.js +++ b/lib/manager/composer/extract.js @@ -1,4 +1,4 @@ -const semver = require('../../versioning')('semver'); +const semverComposer = require('../../versioning')('semverComposer'); module.exports = { extractDependencies, @@ -24,15 +24,18 @@ function extractDependencies(content, packageFile) { depType, depName, currentValue, - verionScheme: 'semver', + versionScheme: 'semverComposer', purl: 'pkg:packagist/' + depName, }; if (!depName.includes('/')) { dep.skipReason = 'unsupported'; } - if (!semver.isVersion(currentValue)) { + if (!semverComposer.isValid(currentValue)) { dep.skipReason = 'unsupported-constraint'; } + if (currentValue === '*') { + dep.skipReason = 'any-version'; + } deps.push(dep); } } catch (err) /* istanbul ignore next */ { diff --git a/lib/versioning/index.js b/lib/versioning/index.js index e179235b12..7cad3bb8c0 100644 --- a/lib/versioning/index.js +++ b/lib/versioning/index.js @@ -1,8 +1,10 @@ const semver = require('./semver'); +const semverComposer = require('./semver-composer'); const pep440 = require('./pep440'); const schemes = { semver, + semverComposer, pep440, }; diff --git a/lib/versioning/semver-composer/index.js b/lib/versioning/semver-composer/index.js new file mode 100644 index 0000000000..0148e9f0b2 --- /dev/null +++ b/lib/versioning/semver-composer/index.js @@ -0,0 +1,66 @@ +const semver = require('../semver'); + +function composer2npm(input) { + if (semver.isVersion(input)) { + return input; + } + let output = input; + // ~4 to ^4 and ~4.1 to ^4.1 + output = output.replace(/(?:^|\s)~([1-9][0-9]*(?:\.[0-9]*)?)(?: |$)/g, '^$1'); + // ~0.4 to >=0.4 <1 + output = output.replace(/(?:^|\s)~(0\.[1-9][0-9]*)(?: |$)/g, '>=$1 <1'); + return output; +} + +const isLessThanRange = (version, range) => + semver.isLessThanRange(version, composer2npm(range)); + +const isValid = input => semver.isValid(composer2npm(input)); + +const matches = (version, range) => + semver.matches(version, composer2npm(range)); + +const maxSatisfyingVersion = (versions, range) => + semver.maxSatisfyingVersion(versions, composer2npm(range)); + +const minSatisfyingVersion = (versions, range) => + semver.minSatisfyingVersion(versions, composer2npm(range)); + +function getNewValue(config, fromVersion, toVersion) { + const { currentValue } = config; + if ( + semver.isValid(currentValue) && + composer2npm(currentValue) === currentValue + ) { + return semver.getNewValue(config, fromVersion, toVersion); + } + const toMajor = semver.getMajor(toVersion); + const toMinor = semver.getMinor(toVersion); + // handle ~0.4 case first + if (currentValue.match(/^~(0\.[1-9][0-9]*)$/)) { + if (toMajor === 0) { + return `~0.${toMinor}`; + } + return `~${toMajor}.0`; + } + // handle ~4 case + if (currentValue.match(/^~([0-9]*)$/)) { + return `~${toMajor}`; + } + // handle ~4.1 case + if (currentValue.match(/^~([0-9]*(?:\.[0-9]*)?)$/)) { + return `~${toMajor}.${toMinor}`; + } + logger.warn('Unsupported composer selector'); + return toVersion; +} + +module.exports = { + ...semver, + isLessThanRange, + isValid, + matches, + maxSatisfyingVersion, + minSatisfyingVersion, + getNewValue, +}; diff --git a/test/manager/composer/__snapshots__/extract.spec.js.snap b/test/manager/composer/__snapshots__/extract.spec.js.snap index afa89b446d..0ca7e75d76 100644 --- a/test/manager/composer/__snapshots__/extract.spec.js.snap +++ b/test/manager/composer/__snapshots__/extract.spec.js.snap @@ -8,8 +8,8 @@ Object { "depName": "php", "depType": "require", "purl": "pkg:packagist/php", - "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "skipReason": "unsupported", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -17,7 +17,7 @@ Object { "depType": "require", "purl": "pkg:packagist/symfony/assetic-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -25,7 +25,7 @@ Object { "depType": "require", "purl": "pkg:packagist/symfony/monolog-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -33,22 +33,21 @@ Object { "depType": "require", "purl": "pkg:packagist/symfony/swiftmailer-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "2.1.*", "depName": "symfony/symfony", "depType": "require", "purl": "pkg:packagist/symfony/symfony", - "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "2.2.2", "depName": "doctrine/common", "depType": "require", "purl": "pkg:packagist/doctrine/common", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -56,7 +55,7 @@ Object { "depType": "require", "purl": "pkg:packagist/doctrine/doctrine-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -64,15 +63,14 @@ Object { "depType": "require", "purl": "pkg:packagist/doctrine/doctrine-fixtures-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "2.2.x-dev", "depName": "doctrine/orm", "depType": "require", "purl": "pkg:packagist/doctrine/orm", - "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -80,7 +78,7 @@ Object { "depType": "require", "purl": "pkg:packagist/exercise/elastica-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -88,45 +86,45 @@ Object { "depType": "require", "purl": "pkg:packagist/friendsofsymfony/rest-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "*", "depName": "friendsofsymfony/user-bundle", "depType": "require", "purl": "pkg:packagist/friendsofsymfony/user-bundle", - "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "skipReason": "any-version", + "versionScheme": "semverComposer", }, Object { "currentValue": "*", "depName": "fzaninotto/faker", "depType": "require", "purl": "pkg:packagist/fzaninotto/faker", - "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "skipReason": "any-version", + "versionScheme": "semverComposer", }, Object { "currentValue": "1.0.1", "depName": "jms/di-extra-bundle", "depType": "require", "purl": "pkg:packagist/jms/di-extra-bundle", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "*", "depName": "jms/payment-core-bundle", "depType": "require", "purl": "pkg:packagist/jms/payment-core-bundle", - "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "skipReason": "any-version", + "versionScheme": "semverComposer", }, Object { "currentValue": "1.1.0", "depName": "jms/security-extra-bundle", "depType": "require", "purl": "pkg:packagist/jms/security-extra-bundle", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -134,7 +132,7 @@ Object { "depType": "require", "purl": "pkg:packagist/knplabs/knp-menu-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -142,7 +140,7 @@ Object { "depType": "require", "purl": "pkg:packagist/knplabs/knp-paginator-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -150,7 +148,7 @@ Object { "depType": "require", "purl": "pkg:packagist/liip/imagine-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -158,7 +156,7 @@ Object { "depType": "require", "purl": "pkg:packagist/merk/dough-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -166,7 +164,7 @@ Object { "depType": "require", "purl": "pkg:packagist/sensio/distribution-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -174,7 +172,7 @@ Object { "depType": "require", "purl": "pkg:packagist/sensio/framework-extra-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -182,7 +180,7 @@ Object { "depType": "require", "purl": "pkg:packagist/sensio/generator-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -190,7 +188,7 @@ Object { "depType": "require", "purl": "pkg:packagist/simplethings/entity-audit-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -198,7 +196,7 @@ Object { "depType": "require", "purl": "pkg:packagist/stof/doctrine-extensions-bundle", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "dev-master", @@ -206,47 +204,46 @@ Object { "depType": "require", "purl": "pkg:packagist/twig/extensions", "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "2.3.*", "depName": "behat/behat", "depType": "require-dev", "purl": "pkg:packagist/behat/behat", - "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "versionScheme": "semverComposer", }, Object { "currentValue": "*", "depName": "behat/behat-bundle", "depType": "require-dev", "purl": "pkg:packagist/behat/behat-bundle", - "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "skipReason": "any-version", + "versionScheme": "semverComposer", }, Object { "currentValue": "*", "depName": "behat/mink-bundle", "depType": "require-dev", "purl": "pkg:packagist/behat/mink-bundle", - "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "skipReason": "any-version", + "versionScheme": "semverComposer", }, Object { "currentValue": "*", "depName": "behat/sahi-client", "depType": "require-dev", "purl": "pkg:packagist/behat/sahi-client", - "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "skipReason": "any-version", + "versionScheme": "semverComposer", }, Object { "currentValue": "*", "depName": "behat/common-contexts", "depType": "require-dev", "purl": "pkg:packagist/behat/common-contexts", - "skipReason": "unsupported-constraint", - "verionScheme": "semver", + "skipReason": "any-version", + "versionScheme": "semverComposer", }, ], } diff --git a/test/versioning/semver-composer.spec.js b/test/versioning/semver-composer.spec.js new file mode 100644 index 0000000000..7cd77f7195 --- /dev/null +++ b/test/versioning/semver-composer.spec.js @@ -0,0 +1,102 @@ +const semver = require('../../lib/versioning')('semverComposer'); + +describe('semver.isValid(input)', () => { + it('should support simple semver', () => { + expect(!!semver.isValid('1.2.3')).toBe(true); + }); + it('should support semver with dash', () => { + expect(!!semver.isValid('1.2.3-foo')).toBe(true); + }); + it('should reject semver without dash', () => { + expect(!!semver.isValid('1.2.3foo')).toBe(false); + }); + it('should support ranges', () => { + expect(!!semver.isValid('~1.2.3')).toBe(true); + expect(!!semver.isValid('^1.2.3')).toBe(true); + expect(!!semver.isValid('>1.2.3')).toBe(true); + }); +}); +describe('semver.isLessThanRange()', () => { + it('handles massaged tilde', () => { + expect(semver.isLessThanRange('0.3.1', '~0.4')).toBe(true); + expect(semver.isLessThanRange('0.5.1', '~0.4')).toBe(false); + }); +}); +describe('semver.maxSatisfyingVersion()', () => { + it('handles massaged tilde', () => { + expect( + semver.maxSatisfyingVersion( + ['0.4.0', '0.5.0', '4.0.0', '4.2.0', '5.0.0'], + '~4' + ) + ).toBe('4.2.0'); + expect( + semver.maxSatisfyingVersion( + ['0.4.0', '0.5.0', '4.0.0', '4.2.0', '5.0.0'], + '~0.4' + ) + ).toBe('0.5.0'); + }); +}); +describe('semver.minSatisfyingVersion()', () => { + it('handles massaged tilde', () => { + expect( + semver.minSatisfyingVersion( + ['0.4.0', '0.5.0', '4.0.0', '4.2.0', '5.0.0'], + '~4' + ) + ).toBe('4.0.0'); + expect( + semver.minSatisfyingVersion( + ['0.4.0', '0.5.0', '4.0.0', '4.2.0', '5.0.0'], + '~0.4' + ) + ).toBe('0.4.0'); + }); +}); +describe('semver.matches()', () => { + it('handles massaged tilde', () => { + expect(semver.matches('0.3.1', '~0.4')).toBe(false); + expect(semver.matches('0.5.1', '~0.4')).toBe(true); + }); +}); +describe('semver.getNewValue()', () => { + it('bumps short caret to same', () => { + expect( + semver.getNewValue( + { currentValue: '^1.0', rangeStrategy: 'bump' }, + '1.0.0', + '1.0.7' + ) + ).toEqual('^1.0'); + }); + it('handles tilde zero', () => { + expect( + semver.getNewValue({ currentValue: '~0.2' }, '0.2.0', '0.3.0') + ).toEqual('~0.3'); + expect( + semver.getNewValue({ currentValue: '~0.2' }, '0.2.0', '1.1.0') + ).toEqual('~1.0'); + }); + it('handles tilde major', () => { + expect( + semver.getNewValue({ currentValue: '~4' }, '4.0.0', '4.2.0') + ).toEqual('~4'); + expect( + semver.getNewValue({ currentValue: '~4' }, '4.0.0', '5.1.0') + ).toEqual('~5'); + }); + it('handles tilde minor', () => { + expect( + semver.getNewValue({ currentValue: '~4.0' }, '4.0.0', '4.2.0') + ).toEqual('~4.2'); + expect( + semver.getNewValue({ currentValue: '~4.0' }, '4.0.0', '5.1.0') + ).toEqual('~5.1'); + }); + it('returns toVersion if unsupported', () => { + expect( + semver.getNewValue({ currentValue: '+4.0.0' }, '4.0.0', '4.2.0') + ).toEqual('4.2.0'); + }); +}); -- GitLab