diff --git a/src/Composer/Package/Version/VersionParser.php b/src/Composer/Package/Version/VersionParser.php index 39c0d1215..55d93b176 100644 --- a/src/Composer/Package/Version/VersionParser.php +++ b/src/Composer/Package/Version/VersionParser.php @@ -12,32 +12,92 @@ 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 { /** - * 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); - return array( - 'version' => $matches[1] + // match classical versioning + if (preg_match('{^v?(\d{1,3})(\.\d+)?(\.\d+)?-?((?:beta|RC|alpha)\d*)?(-?dev)?$}i', $version, $matches)) { + return $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]), - ); + .(!empty($matches[3]) ? $matches[3] : '.0') + .(!empty($matches[4]) ? '-'.strtolower($matches[4]) : '') + .(!empty($matches[5]) ? '-dev' : ''); + } + + // match date-based versioning + if (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1})?)((?:beta|RC|alpha)\d*)?(-?dev)?$}i', $version, $matches)) { + return preg_replace('{\D}', '-', $matches[1]) + .(!empty($matches[2]) ? '-'.strtolower($matches[2]) : '') + .(!empty($matches[3]) ? '-dev' : ''); + } + + throw new \UnexpectedValueException('Invalid version string '.$version); + } + + 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+))?\.\*$}', $constraint, $matches)) { + $lowVersion = $matches[1] . '.' . (isset($matches[2]) ? $matches[2] : '0') . '.0'; + $highVersion = (isset($matches[2]) + ? $matches[1] . '.' . ($matches[2]+1) + : ($matches[1]+1) . '.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/tests/Composer/Test/Package/Version/VersionParserTest.php b/tests/Composer/Test/Package/Version/VersionParserTest.php new file mode 100644 index 000000000..b35452a7f --- /dev/null +++ b/tests/Composer/Test/Package/Version/VersionParserTest.php @@ -0,0 +1,131 @@ + + * 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'), + 'parses state' => array('1.0.0RC1dev', '1.0.0-rc1-dev'), + 'CI parsing' => array('1.0.0-rC15-dev', '1.0.0-rc15-dev'), + 'forces x.y.z' => array('1.0-dev', '1.0.0-dev'), + 'parses long' => array('10.4.13-beta', '10.4.13-beta'), + 'strips leading v' => array('v1.0.0', '1.0.0'), + 'strips leading v' => array('v20100102', '20100102'), + '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'), + ); + } + + /** + * @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'), + ); + } + + /** + * @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')), + 'lesser than' => array('<1.2.3', new VersionConstraint('<', '1.2.3')), + 'less/eq than' => array('<=1.2.3', new VersionConstraint('<=', '1.2.3')), + 'great/eq than' => array('>=1.2.3', new VersionConstraint('>=', '1.2.3')), + 'equals' => array('=1.2.3', new VersionConstraint('=', '1.2.3')), + 'double equals' => array('==1.2.3', new VersionConstraint('=', '1.2.3')), + 'no op means eq' => array('1.2.3', new VersionConstraint('=', '1.2.3')), + 'completes version' => array('=1.0', new VersionConstraint('=', '1.0.0')), + 'accepts spaces' => array('>= 1.2.3', new VersionConstraint('>=', '1.2.3')), + ); + } + + /** + * @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'), new VersionConstraint('<', '3.0.0')), + array('20.*', new VersionConstraint('>=', '20.0.0'), new VersionConstraint('<', '21.0.0')), + array('2.0.*', new VersionConstraint('>=', '2.0.0'), new VersionConstraint('<', '2.1.0')), + array('2.2.*', new VersionConstraint('>=', '2.2.0'), new VersionConstraint('<', '2.3.0')), + array('2.10.*', new VersionConstraint('>=', '2.10.0'), new VersionConstraint('<', '2.11.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'), + ); + } +}