diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index 526e9e0ac..786c58ae5 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -103,7 +103,7 @@ abstract class BasePackage implements PackageInterface public function matches($name, LinkConstraintInterface $constraint) { if ($this->name === $name) { - return $constraint->matches(new VersionConstraint('==', $this->getVersion(), $this->getReleaseType())); + return $constraint->matches(new VersionConstraint('==', $this->getVersion())); } foreach ($this->getProvides() as $link) { @@ -141,7 +141,7 @@ abstract class BasePackage implements PackageInterface */ public function getUniqueName() { - return $this->getName().'-'.$this->getVersion().'-'.$this->getReleaseType(); + return $this->getName().'-'.$this->getVersion(); } /** diff --git a/src/Composer/Package/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php index d13482264..123d0fa86 100644 --- a/src/Composer/Package/Dumper/ArrayDumper.php +++ b/src/Composer/Package/Dumper/ArrayDumper.php @@ -31,7 +31,6 @@ class ArrayDumper 'distType', 'distUrl', 'distSha1Checksum', - 'releaseType', 'version', 'license', 'requires', diff --git a/src/Composer/Package/LinkConstraint/VersionConstraint.php b/src/Composer/Package/LinkConstraint/VersionConstraint.php index c8ae4cb5b..2590aa3df 100644 --- a/src/Composer/Package/LinkConstraint/VersionConstraint.php +++ b/src/Composer/Package/LinkConstraint/VersionConstraint.php @@ -36,8 +36,6 @@ class VersionConstraint extends SpecificConstraint $operator = '=='; } - // TODO add third parameter releaseType and match that too - // TODO add fourth parameter devSnapshot and match that too $this->operator = $operator; $this->version = $version; } diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 83a6e2051..69d67b5e3 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -16,6 +16,7 @@ use Composer\Package; /** * @author Konstantin Kudryashiv + * @author Jordi Boggiano */ class ArrayLoader { @@ -28,13 +29,22 @@ class ArrayLoader 'suggest' => 'suggests', ); + protected $versionParser; + + public function __construct($parser = null) + { + $this->versionParser = $parser; + if (!$parser) { + $this->versionParser = new Package\Version\VersionParser; + } + } + public function load($config) { $this->validateConfig($config); - $versionParser = new Package\Version\VersionParser(); - $version = $versionParser->parse($config['version']); - $package = new Package\MemoryPackage($config['name'], $version['version'], $version['type']); + $version = $this->versionParser->normalize($config['version']); + $package = new Package\MemoryPackage($config['name'], $version); $package->setType(isset($config['type']) ? $config['type'] : 'library'); @@ -87,15 +97,10 @@ class ArrayLoader private function loadLinksFromConfig($srcPackageName, $description, array $linksSpecs) { $links = array(); - foreach ($linksSpecs as $packageName => $version) { + foreach ($linksSpecs as $packageName => $constraint) { $name = strtolower($packageName); - preg_match('#^([>=<~]*)([\d.]+.*)$#', $version, $match); - if (!$match[1]) { - $match[1] = '='; - } - - $constraint = new Package\LinkConstraint\VersionConstraint($match[1], $match[2]); + $constraint = $this->versionParser->parseConstraints($constraint); $links[] = new Package\Link($srcPackageName, $packageName, $constraint, $description); } diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index e6e1fc19e..ed7c08bf4 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -131,13 +131,6 @@ interface PackageInterface */ function getDistSha1Checksum(); - /** - * Returns the release type of this package, e.g. stable or beta - * - * @return string The release type - */ - function getReleaseType(); - /** * Returns the version of this package * diff --git a/src/Composer/Package/PackageLock.php b/src/Composer/Package/PackageLock.php index 5e79cccce..e30ee3605 100644 --- a/src/Composer/Package/PackageLock.php +++ b/src/Composer/Package/PackageLock.php @@ -43,8 +43,8 @@ class PackageLock $versionParser = new VersionParser(); $packages = array(); foreach ($lockList as $info) { - $version = $versionParser->parse($info['version']); - $packages[] = new MemoryPackage($info['package'], $version['version'], $version['type']); + $version = $versionParser->normalize($info['version']); + $packages[] = new MemoryPackage($info['package'], $version); } return $packages; diff --git a/src/Composer/Package/Version/VersionParser.php b/src/Composer/Package/Version/VersionParser.php index 39c0d1215..a49a16d9f 100644 --- a/src/Composer/Package/Version/VersionParser.php +++ b/src/Composer/Package/Version/VersionParser.php @@ -12,32 +12,117 @@ namespace Composer\Package\Version; +use Composer\Package\LinkConstraint\MultiConstraint; +use Composer\Package\LinkConstraint\VersionConstraint; + /** * Version parser * - * @author Konstantin Kudryashov - * @author Nils Adermann + * @author Jordi Boggiano */ class VersionParser { + private $modifierRegex = '[.-]?(?:(beta|RC|alpha|patch|pl|p)(?:[.-]?(\d+))?)?([.-]?dev)?'; + /** - * Parses a version string and returns an array with the version, its type (alpha, beta, RC, stable) and a dev flag (for development branches tracking) + * Normalizes a version string to be able to perform comparisons on it * * @param string $version * @return array */ - public function parse($version) + public function normalize($version) { - if (!preg_match('#^v?(\d+)(\.\d+)?(\.\d+)?-?((?:beta|RC|alpha)\d*)?-?(dev)?$#i', $version, $matches)) { - throw new \UnexpectedValueException('Invalid version string '.$version); + $version = trim($version); + + // match classical versioning + if (preg_match('{^v?(\d{1,3})(\.\d+)?(\.\d+)?(\.\d+)?'.$this->modifierRegex.'$}i', $version, $matches)) { + $version = $matches[1] + .(!empty($matches[2]) ? $matches[2] : '.0') + .(!empty($matches[3]) ? $matches[3] : '.0') + .(!empty($matches[4]) ? $matches[4] : '.0'); + $index = 5; + } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)'.$this->modifierRegex.'$}i', $version, $matches)) { // match date-based versioning + $version = preg_replace('{\D}', '-', $matches[1]); + $index = 2; } - return array( - 'version' => $matches[1] - .(!empty($matches[2]) ? $matches[2] : '.0') - .(!empty($matches[3]) ? $matches[3] : '.0'), - 'type' => strtolower(!empty($matches[4]) ? $matches[4] : 'stable'), - 'dev' => !empty($matches[5]), - ); + // add version modifiers if a version was matched + if (isset($index)) { + if (!empty($matches[$index])) { + $mod = array('p', 'pl', 'rc'); + $modNormalized = array('patch', 'patch', 'RC'); + $version .= '-'.str_replace($mod, $modNormalized, strtolower($matches[$index])) + . (!empty($matches[$index+1]) ? $matches[$index+1] : ''); + } + + if (!empty($matches[$index+2])) { + $version .= '-dev'; + } + + return $version; + } + + throw new \UnexpectedValueException('Invalid version string '.$version); + } + + /** + * Parses as constraint string into LinkConstraint objects + * + * @param string $constraints + * @return \Composer\Package\LinkConstraint\LinkConstraintInterface + */ + public function parseConstraints($constraints) + { + $constraints = preg_split('{\s*,\s*}', trim($constraints)); + + if (count($constraints) > 1) { + $constraintObjects = array(); + foreach ($constraints as $key => $constraint) { + $constraintObjects = array_merge($constraintObjects, $this->parseConstraint($constraint)); + } + } else { + $constraintObjects = $this->parseConstraint($constraints[0]); + } + + if (1 === count($constraintObjects)) { + return $constraintObjects[0]; + } + + return new MultiConstraint($constraintObjects); + } + + private function parseConstraint($constraint) + { + if ('*' === $constraint || '*.*' === $constraint || '*.*.*' === $constraint) { + return array(); + } + + // match wildcard constraints + if (preg_match('{^(\d+)(?:\.(\d+))?(?:\.(\d+))?\.\*$}', $constraint, $matches)) { + if (isset($matches[3])) { + $lowVersion = $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.0'; + $highVersion = $matches[1] . '.' . $matches[2] . '.' . ($matches[3]+1) . '.0'; + } elseif (isset($matches[2])) { + $lowVersion = $matches[1] . '.' . $matches[2] . '.0.0'; + $highVersion = $matches[1] . '.' . ($matches[2]+1) . '.0.0'; + } else { + $lowVersion = $matches[1] . '.0.0.0'; + $highVersion = ($matches[1]+1) . '.0.0.0'; + } + + return array( + new VersionConstraint('>=', $lowVersion), + new VersionConstraint('<', $highVersion), + ); + } + + // match operators constraints + if (preg_match('{^(>=?|<=?|==?)?\s*(\d+.*)}', $constraint, $matches)) { + $version = $this->normalize($matches[2]); + + return array(new VersionConstraint($matches[1] ?: '=', $version)); + } + + throw new \UnexpectedValueException('Could not parse version constraint '.$constraint); } } diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index a93e32bf0..fa87d7206 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -36,12 +36,12 @@ class PlatformRepository extends ArrayRepository implements WritableRepositoryIn $versionParser = new VersionParser(); try { - $version = $versionParser->parse(PHP_VERSION); + $version = $versionParser->normalize(PHP_VERSION); } catch (\UnexpectedValueException $e) { - $version = $versionParser->parse(preg_replace('#^(.+?)(-.+)?$#', '$1', PHP_VERSION)); + $version = $versionParser->normalize(preg_replace('#^(.+?)(-.+)?$#', '$1', PHP_VERSION)); } - $php = new MemoryPackage('php', $version['version'], $version['type']); + $php = new MemoryPackage('php', $version); parent::addPackage($php); foreach (get_loaded_extensions() as $ext) { @@ -51,12 +51,12 @@ class PlatformRepository extends ArrayRepository implements WritableRepositoryIn $reflExt = new \ReflectionExtension($ext); try { - $version = $versionParser->parse($reflExt->getVersion()); + $version = $versionParser->normalize($reflExt->getVersion()); } catch (\UnexpectedValueException $e) { - $version = array('version' => '0', 'type' => 'stable'); + $version = $versionParser->normalize('0'); } - $ext = new MemoryPackage('ext/'.strtolower($ext), $version['version'], $version['type']); + $ext = new MemoryPackage('ext/'.strtolower($ext), $version); parent::addPackage($ext); } } diff --git a/tests/Composer/Test/Package/MemoryPackageTest.php b/tests/Composer/Test/Package/MemoryPackageTest.php index 7176b8e4a..d61e97987 100644 --- a/tests/Composer/Test/Package/MemoryPackageTest.php +++ b/tests/Composer/Test/Package/MemoryPackageTest.php @@ -18,11 +18,10 @@ class MemoryPackageTest extends \PHPUnit_Framework_TestCase { public function testMemoryPackage() { - $package = new MemoryPackage('foo', '1', 'beta'); + $package = new MemoryPackage('foo', '1-beta'); $this->assertEquals('foo', $package->getName()); - $this->assertEquals('1', $package->getVersion()); - $this->assertEquals('beta', $package->getReleaseType()); + $this->assertEquals('1-beta', $package->getVersion()); $this->assertEquals('foo-1-beta', (string) $package); } diff --git a/tests/Composer/Test/Package/Version/VersionParserTest.php b/tests/Composer/Test/Package/Version/VersionParserTest.php new file mode 100644 index 000000000..64c8616bd --- /dev/null +++ b/tests/Composer/Test/Package/Version/VersionParserTest.php @@ -0,0 +1,146 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Package\Version; + +use Composer\Package\Version\VersionParser; +use Composer\Package\LinkConstraint\MultiConstraint; +use Composer\Package\LinkConstraint\VersionConstraint; + +class VersionParserTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider successfulNormalizedVersions + */ + public function testNormalizeSucceeds($input, $expected) + { + $parser = new VersionParser; + $this->assertEquals($expected, $parser->normalize($input)); + } + + public function successfulNormalizedVersions() + { + return array( + 'none' => array('1.0.0', '1.0.0.0'), + 'none' => array('1.2.3.4', '1.2.3.4'), + 'parses state' => array('1.0.0RC1dev', '1.0.0.0-RC1-dev'), + 'CI parsing' => array('1.0.0-rC15-dev', '1.0.0.0-RC15-dev'), + 'delimiters' => array('1.0.0.RC.15-dev', '1.0.0.0-RC15-dev'), + 'forces w.x.y.z' => array('1.0-dev', '1.0.0.0-dev'), + 'forces w.x.y.z' => array('0', '0.0.0.0'), + 'parses long' => array('10.4.13-beta', '10.4.13.0-beta'), + 'strips leading v' => array('v1.0.0', '1.0.0'), + 'strips leading v' => array('v20100102', '20100102'), + 'parses dates y-m' => array('2010.01', '2010-01'), + 'parses dates w/ .' => array('2010.01.02', '2010-01-02'), + 'parses dates w/ -' => array('2010-01-02', '2010-01-02'), + 'parses numbers' => array('2010-01-02.5', '2010-01-02-5'), + 'parses datetime' => array('20100102-203040', '20100102-203040'), + 'parses dt+number' => array('20100102203040-10', '20100102203040-10'), + 'parses dt+patch' => array('20100102-203040-p1', '20100102-203040-patch1'), + ); + } + + /** + * @dataProvider failingNormalizedVersions + * @expectedException UnexpectedValueException + */ + public function testNormalizeFails($input) + { + $parser = new VersionParser; + $parser->normalize($input); + } + + public function failingNormalizedVersions() + { + return array( + 'empty ' => array(''), + 'invalid chars' => array('a'), + 'invalid type' => array('1.0.0-meh'), + 'too many bits' => array('1.0.0.0.0'), + ); + } + + /** + * @dataProvider simpleConstraints + */ + public function testParseConstraintsSimple($input, $expected) + { + $parser = new VersionParser; + $this->assertEquals((string) $expected, (string) $parser->parseConstraints($input)); + } + + public function simpleConstraints() + { + return array( + 'greater than' => array('>1.0.0', new VersionConstraint('>', '1.0.0.0')), + 'lesser than' => array('<1.2.3.4', new VersionConstraint('<', '1.2.3.4')), + 'less/eq than' => array('<=1.2.3', new VersionConstraint('<=', '1.2.3.0')), + 'great/eq than' => array('>=1.2.3', new VersionConstraint('>=', '1.2.3.0')), + 'equals' => array('=1.2.3', new VersionConstraint('=', '1.2.3.0')), + 'double equals' => array('==1.2.3', new VersionConstraint('=', '1.2.3.0')), + 'no op means eq' => array('1.2.3', new VersionConstraint('=', '1.2.3.0')), + 'completes version' => array('=1.0', new VersionConstraint('=', '1.0.0.0')), + 'accepts spaces' => array('>= 1.2.3', new VersionConstraint('>=', '1.2.3.0')), + ); + } + + /** + * @dataProvider wildcardConstraints + */ + public function testParseConstraintsWildcard($input, $min, $max) + { + $parser = new VersionParser; + $expected = new MultiConstraint(array($min, $max)); + + $this->assertEquals((string) $expected, (string) $parser->parseConstraints($input)); + } + + public function wildcardConstraints() + { + return array( + array('2.*', new VersionConstraint('>=', '2.0.0.0'), new VersionConstraint('<', '3.0.0.0')), + array('20.*', new VersionConstraint('>=', '20.0.0.0'), new VersionConstraint('<', '21.0.0.0')), + array('2.0.*', new VersionConstraint('>=', '2.0.0.0'), new VersionConstraint('<', '2.1.0.0')), + array('2.2.*', new VersionConstraint('>=', '2.2.0.0'), new VersionConstraint('<', '2.3.0.0')), + array('2.10.*', new VersionConstraint('>=', '2.10.0.0'), new VersionConstraint('<', '2.11.0.0')), + array('2.1.3.*', new VersionConstraint('>=', '2.1.3.0'), new VersionConstraint('<', '2.1.4.0')), + ); + } + + public function testParseConstraintsMulti() + { + $parser = new VersionParser; + $first = new VersionConstraint('>', '2.0.0.0'); + $second = new VersionConstraint('<=', '3.0.0.0'); + $multi = new MultiConstraint(array($first, $second)); + $this->assertEquals((string) $multi, (string) $parser->parseConstraints('>2.0,<=3.0')); + } + + /** + * @dataProvider failingConstraints + * @expectedException UnexpectedValueException + */ + public function testParseConstraintsFails($input) + { + $parser = new VersionParser; + $parser->parseConstraints($input); + } + + public function failingConstraints() + { + return array( + 'empty ' => array(''), + 'invalid version' => array('1.0.0-meh'), + ); + } +} diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php index ee777b52f..048c9c8d7 100644 --- a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php @@ -49,7 +49,7 @@ class FilesystemRepositoryTest extends \PHPUnit_Framework_TestCase $data = json_decode(file_get_contents($this->repositoryFile), true); $this->assertEquals(array( - array('name' => 'package1', 'type' => 'vendor', 'version' => '1.0.0', 'releaseType' => 'beta', 'names' => array('package1')) + array('name' => 'package1', 'type' => 'vendor', 'version' => '1.0.0.0-beta', 'names' => array('package1')) ), $data); } }