瀏覽代碼

Merge pull request #2596 from Seldaek/spaceyAND

SemVer 2.0 and npm semver support
Jordi Boggiano 10 年之前
父節點
當前提交
72a35263ec

+ 16 - 8
doc/01-basic-usage.md

@@ -72,17 +72,19 @@ means any version in the `1.0` development branch. It would match `1.0.0`,
 
 Version constraints can be specified in a few different ways.
 
-Name           | Example                                                            | Description
--------------- | ------------------------------------------------------------------ | -----------
-Exact version  | `1.0.2`                                                            | You can specify the exact version of a package.
-Range          | `>=1.0` `>=1.0,<2.0` <code>&gt;=1.0,&lt;1.1 &#124; &gt;=1.2</code> | By using comparison operators you can specify ranges of valid versions. Valid operators are `>`, `>=`, `<`, `<=`, `!=`. <br />You can define multiple ranges. Ranges separated by a comma (`,`) will be treated as a **logical AND**. A pipe (<code>&#124;</code>) will be treated as a **logical OR**. AND has higher precedence than OR.
-Wildcard       | `1.0.*`                                                            | You can specify a pattern with a `*` wildcard. `1.0.*` is the equivalent of `>=1.0,<1.1`.
-Tilde Operator | `~1.2`                                                             | Very useful for projects that follow semantic versioning. `~1.2` is equivalent to `>=1.2,<2.0`. For more details, read the next section below.
+Name           | Example                                                                  | Description
+-------------- | ------------------------------------------------------------------------ | -----------
+Exact version  | `1.0.2`                                                                  | You can specify the exact version of a package.
+Range          | `>=1.0` `>=1.0 <2.0` <code>&gt;=1.0 &lt;1.1 &#124;&#124; &gt;=1.2</code> | By using comparison operators you can specify ranges of valid versions. Valid operators are `>`, `>=`, `<`, `<=`, `!=`. <br />You can define multiple ranges. Ranges separated by a space (` `) or comma (`,`) will be treated as a **logical AND**. A double pipe (<code>&#124;&#124;</code>) will be treated as a **logical OR**. AND has higher precedence than OR.
+Hyphen Range   | `1.0 - 2.0`                                                              | Inclusive set of versions. Partial versions on the right include are completed with a wildcard. For example `1.0 - 2.0` is equivalent to `>=1.0.0 <2.1` as the `2.0` becomes `2.0.*`. On the other hand `1.0.0 - 2.1.0` is equivalent to `>=1.0.0 <=2.1.0`.
+Wildcard       | `1.0.*`                                                                  | You can specify a pattern with a `*` wildcard. `1.0.*` is the equivalent of `>=1.0 <1.1`.
+Tilde Operator | `~1.2`                                                                   | Very useful for projects that follow semantic versioning. `~1.2` is equivalent to `>=1.2 <2.0`. For more details, read the next section below.
+Caret Operator | `^1.2.3`                                                                 | Very useful for projects that follow semantic versioning. `^1.2.3` is equivalent to `>=1.2.3 <2.0`. For more details, read the next section below.
 
-### Next Significant Release (Tilde Operator)
+### Next Significant Release (Tilde and Caret Operators)
 
 The `~` operator is best explained by example: `~1.2` is equivalent to
-`>=1.2,<2.0`, while `~1.2.3` is equivalent to `>=1.2.3,<1.3`. As you can see
+`>=1.2 <2.0.0`, while `~1.2.3` is equivalent to `>=1.2.3 <1.3.0`. As you can see
 it is mostly useful for projects respecting [semantic
 versioning](http://semver.org/). A common usage would be to mark the minimum
 minor version you depend on, like `~1.2` (which allows anything up to, but not
@@ -90,6 +92,12 @@ including, 2.0). Since in theory there should be no backwards compatibility
 breaks until 2.0, that works well. Another way of looking at it is that using
 `~` specifies a minimum version, but allows the last digit specified to go up.
 
+The `^` operator behaves very similarly but it sticks closer to semantic
+versioning, and will always allow non-breaking updates. For example `^1.2.3`
+is equivalent to `>=1.2.3 <2.0.0` as none of the releases until 2.0 should
+break backwards compatibility. For pre-1.0 versions it also acts with safety
+in mind and treats `^0.3` as `>=0.3.0 <0.4.0`
+
 > **Note:** Though `2.0-beta.1` is strictly before `2.0`, a version constraint
 > like `~1.2` would not install it. As said above `~1.2` only means the `.2`
 > can change but the `1.` part is fixed.

+ 2 - 2
doc/04-schema.md

@@ -345,10 +345,10 @@ dependencies from being installed.
 Lists packages that conflict with this version of this package. They
 will not be allowed to be installed together with your package.
 
-Note that when specifying ranges like `<1.0, >= 1.1` in a `conflict` link,
+Note that when specifying ranges like `<1.0 >=1.1` in a `conflict` link,
 this will state a conflict with all versions that are less than 1.0 *and* equal
 or newer than 1.1 at the same time, which is probably not what you want. You
-probably want to go for `<1.0 | >= 1.1` in this case.
+probably want to go for `<1.0 | >=1.1` in this case.
 
 #### replace
 

+ 72 - 8
src/Composer/Package/Version/VersionParser.php

@@ -103,6 +103,11 @@ class VersionParser
             $version = $match[1];
         }
 
+        // ignore build metadata
+        if (preg_match('{^([^,\s+]+)\+[^\s]+$}', $version, $match)) {
+            $version = $match[1];
+        }
+
         // match master-like branches
         if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) {
             return '9999999-dev';
@@ -178,10 +183,10 @@ class VersionParser
             return $this->normalize($name);
         }
 
-        if (preg_match('#^v?(\d+)(\.(?:\d+|[x*]))?(\.(?:\d+|[x*]))?(\.(?:\d+|[x*]))?$#i', $name, $matches)) {
+        if (preg_match('#^v?(\d+)(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?$#i', $name, $matches)) {
             $version = '';
             for ($i = 1; $i < 5; $i++) {
-                $version .= isset($matches[$i]) ? str_replace('*', 'x', $matches[$i]) : '.x';
+                $version .= isset($matches[$i]) ? str_replace(array('*', 'X'), 'x', $matches[$i]) : '.x';
             }
 
             return str_replace('x', '9999999', $version).'-dev';
@@ -230,11 +235,10 @@ class VersionParser
             $constraints = $match[1];
         }
 
-        $orConstraints = preg_split('{\s*\|\s*}', trim($constraints));
+        $orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraints));
         $orGroups = array();
         foreach ($orConstraints as $constraints) {
-            $andConstraints = preg_split('{\s*,\s*}', $constraints);
-
+            $andConstraints = preg_split('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}', $constraints);
             if (count($andConstraints) > 1) {
                 $constraintObjects = array();
                 foreach ($andConstraints as $constraint) {
@@ -273,16 +277,18 @@ class VersionParser
             }
         }
 
-        if (preg_match('{^[x*](\.[x*])*$}i', $constraint)) {
+        if (preg_match('{^[xX*](\.[xX*])*$}i', $constraint)) {
             return array(new EmptyConstraint);
         }
 
+        $versionRegex = '(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?'.self::$modifierRegex;
+
         // match tilde constraints
         // like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous
         // version, to ensure that unstable instances of the current version are allowed.
         // however, if a stability suffix is added to the constraint, then a >= match on the current version is
         // used instead
-        if (preg_match('{^~>?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?'.self::$modifierRegex.'?$}i', $constraint, $matches)) {
+        if (preg_match('{^~>?'.$versionRegex.'$}i', $constraint, $matches)) {
             if (substr($constraint, 0, 2) === '~>') {
                 throw new \UnexpectedValueException(
                     'Could not parse version constraint '.$constraint.': '.
@@ -329,8 +335,39 @@ class VersionParser
             );
         }
 
+        // match caret constraints
+        if (preg_match('{^\^'.$versionRegex.'($)}i', $constraint, $matches)) {
+            // Work out which position in the version we are operating at
+            if ('0' !== $matches[1] || '' === $matches[2]) {
+                $position = 1;
+            } elseif ('0' !== $matches[2] || '' === $matches[3]) {
+                $position = 2;
+            } else {
+                $position = 3;
+            }
+
+            // Calculate the stability suffix
+            $stabilitySuffix = '';
+            if (empty($matches[5]) && empty($matches[7])) {
+                $stabilitySuffix .= '-dev';
+            }
+
+            $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1));
+            $lowerBound = new VersionConstraint('>=', $lowVersion);
+
+            // For upper bound, we increment the position of one more significance,
+            // but highPosition = 0 would be illegal
+            $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev';
+            $upperBound = new VersionConstraint('<', $highVersion);
+
+            return array(
+                $lowerBound,
+                $upperBound
+            );
+        }
+
         // match wildcard constraints
-        if (preg_match('{^(\d+)(?:\.(\d+))?(?:\.(\d+))?\.[x*]$}', $constraint, $matches)) {
+        if (preg_match('{^(\d+)(?:\.(\d+))?(?:\.(\d+))?\.[xX*]$}', $constraint, $matches)) {
             if (isset($matches[3]) && '' !== $matches[3]) {
                 $position = 3;
             } elseif (isset($matches[2]) && '' !== $matches[2]) {
@@ -352,6 +389,33 @@ class VersionParser
             );
         }
 
+        // match hyphen constraints
+        if (preg_match('{^(?P<from>'.$versionRegex.') +- +(?P<to>'.$versionRegex.')($)}i', $constraint, $matches)) {
+            // Calculate the stability suffix
+            $lowStabilitySuffix = '';
+            if (empty($matches[6]) && empty($matches[8])) {
+                $lowStabilitySuffix = '-dev';
+            }
+
+            $lowVersion = $this->normalize($matches['from']);
+            $lowerBound = new VersionConstraint('>=', $lowVersion . $lowStabilitySuffix);
+
+            $highVersion = $matches[10];
+            if ((!empty($matches[11]) && !empty($matches[12])) || !empty($matches[14]) || !empty($matches[16])) {
+                $highVersion = $this->normalize($matches['to']);
+                $upperBound = new VersionConstraint('<=', $highVersion);
+            } else {
+                $highMatch = array('', $matches[10], $matches[11], $matches[12], $matches[13]);
+                $highVersion = $this->manipulateVersionString($highMatch, empty($matches[11]) ? 1 : 2, 1) . '-dev';
+                $upperBound = new VersionConstraint('<', $highVersion);
+            }
+
+            return array(
+                $lowerBound,
+                $upperBound
+            );
+        }
+
         // match operators constraints
         if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) {
             try {

+ 108 - 6
tests/Composer/Test/Package/Version/VersionParserTest.php

@@ -90,6 +90,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
             'forces w.x.y.z/2'   => array('0',                   '0.0.0.0'),
             'parses long'        => array('10.4.13-beta',        '10.4.13.0-beta'),
             'parses long/2'      => array('10.4.13beta2',        '10.4.13.0-beta2'),
+            'parses long/semver' => array('10.4.13beta.2',       '10.4.13.0-beta2'),
             'expand shorthand'   => array('10.4.13-b',           '10.4.13.0-beta'),
             'expand shorthand2'  => array('10.4.13-b5',          '10.4.13.0-beta5'),
             'strips leading v'   => array('v1.0.0',              '1.0.0.0'),
@@ -109,6 +110,10 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
             'parses arbitrary2'  => array('DEV-FOOBAR',          'dev-FOOBAR'),
             'parses arbitrary3'  => array('dev-feature/foo',     'dev-feature/foo'),
             'ignores aliases'    => array('dev-master as 1.0.0', '9999999-dev'),
+            'semver metadata'    => array('dev-master+foo.bar',  '9999999-dev'),
+            'semver metadata/2'  => array('1.0.0-beta.5+foo',    '1.0.0.0-beta5'),
+            'semver metadata/3'  => array('1.0.0+foo',           '1.0.0.0'),
+            'metadata w/ alias'  => array('1.0.0+foo as 2.0',    '1.0.0.0'),
         );
     }
 
@@ -130,6 +135,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
             'invalid type'      => array('1.0.0-meh'),
             'too many bits'     => array('1.0.0.0.0'),
             'non-dev arbitrary' => array('feature-foo'),
+            'metadata w/ space' => array('1.0.0+foo bar'),
         );
     }
 
@@ -208,7 +214,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
             'match any'         => array('*',           new EmptyConstraint()),
             'match any/2'       => array('*.*',         new EmptyConstraint()),
             'match any/3'       => array('*.x.*',       new EmptyConstraint()),
-            'match any/4'       => array('x.x.x.*',     new EmptyConstraint()),
+            'match any/4'       => array('x.X.x.*',     new EmptyConstraint()),
             'not equal'         => array('<>1.0.0',     new VersionConstraint('<>', '1.0.0.0')),
             'not equal/2'       => array('!=1.0.0',     new VersionConstraint('!=', '1.0.0.0')),
             'greater than'      => array('>1.0.0',      new VersionConstraint('>', '1.0.0.0')),
@@ -221,6 +227,8 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
             'completes version' => array('=1.0',        new VersionConstraint('=', '1.0.0.0')),
             'shorthand beta'    => array('1.2.3b5',     new VersionConstraint('=', '1.2.3.0-beta5')),
             'accepts spaces'    => array('>= 1.2.3',    new VersionConstraint('>=', '1.2.3.0')),
+            'accepts spaces/2'  => array('< 1.2.3',     new VersionConstraint('<', '1.2.3.0-dev')),
+            'accepts spaces/3'  => array('> 1.2.3',     new VersionConstraint('>', '1.2.3.0')),
             'accepts master'    => array('>=dev-master',    new VersionConstraint('>=', '9999999-dev')),
             'accepts master/2'  => array('dev-master',      new VersionConstraint('=', '9999999-dev')),
             'accepts arbitrary' => array('dev-feature-a',   new VersionConstraint('=', 'dev-feature-a')),
@@ -253,7 +261,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
             array('20.*',    new VersionConstraint('>=', '20.0.0.0-dev'), new VersionConstraint('<', '21.0.0.0-dev')),
             array('2.0.*',   new VersionConstraint('>=', '2.0.0.0-dev'), new VersionConstraint('<', '2.1.0.0-dev')),
             array('2.2.x',   new VersionConstraint('>=', '2.2.0.0-dev'), new VersionConstraint('<', '2.3.0.0-dev')),
-            array('2.10.x',  new VersionConstraint('>=', '2.10.0.0-dev'), new VersionConstraint('<', '2.11.0.0-dev')),
+            array('2.10.X',  new VersionConstraint('>=', '2.10.0.0-dev'), new VersionConstraint('<', '2.11.0.0-dev')),
             array('2.1.3.*', new VersionConstraint('>=', '2.1.3.0-dev'), new VersionConstraint('<', '2.1.4.0-dev')),
             array('0.*',     null, new VersionConstraint('<', '1.0.0.0-dev')),
         );
@@ -291,13 +299,92 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
         );
     }
 
-    public function testParseConstraintsMulti()
+    /**
+     * @dataProvider caretConstraints
+     */
+    public function testParseCaretWildcard($input, $min, $max)
+    {
+        $parser = new VersionParser;
+        if ($min) {
+            $expected = new MultiConstraint(array($min, $max));
+        } else {
+            $expected = $max;
+        }
+
+        $this->assertSame((string) $expected, (string) $parser->parseConstraints($input));
+    }
+
+    public function caretConstraints()
+    {
+        return array(
+            array('^1',            new VersionConstraint('>=', '1.0.0.0-dev'), new VersionConstraint('<', '2.0.0.0-dev')),
+            array('^0',            new VersionConstraint('>=', '0.0.0.0-dev'), new VersionConstraint('<', '1.0.0.0-dev')),
+            array('^0.0',          new VersionConstraint('>=', '0.0.0.0-dev'), new VersionConstraint('<', '0.1.0.0-dev')),
+            array('^1.2',          new VersionConstraint('>=', '1.2.0.0-dev'), new VersionConstraint('<', '2.0.0.0-dev')),
+            array('^1.2.3-beta.2', new VersionConstraint('>=', '1.2.3.0-beta2'), new VersionConstraint('<', '2.0.0.0-dev')),
+            array('^1.2.3.4',      new VersionConstraint('>=', '1.2.3.4-dev'), new VersionConstraint('<', '2.0.0.0-dev')),
+            array('^1.2.3',        new VersionConstraint('>=', '1.2.3.0-dev'), new VersionConstraint('<', '2.0.0.0-dev')),
+            array('^0.2.3',        new VersionConstraint('>=', '0.2.3.0-dev'), new VersionConstraint('<', '0.3.0.0-dev')),
+            array('^0.2',          new VersionConstraint('>=', '0.2.0.0-dev'), new VersionConstraint('<', '0.3.0.0-dev')),
+            array('^0.0.3',        new VersionConstraint('>=', '0.0.3.0-dev'), new VersionConstraint('<', '0.0.4.0-dev')),
+            array('^0.0.3-alpha',  new VersionConstraint('>=', '0.0.3.0-alpha'), new VersionConstraint('<', '0.0.4.0-dev')),
+            array('^0.0.3-dev',    new VersionConstraint('>=', '0.0.3.0-dev'), new VersionConstraint('<', '0.0.4.0-dev')),
+        );
+    }
+
+    /**
+     * @dataProvider hyphenConstraints
+     */
+    public function testParseHyphen($input, $min, $max)
+    {
+        $parser = new VersionParser;
+        if ($min) {
+            $expected = new MultiConstraint(array($min, $max));
+        } else {
+            $expected = $max;
+        }
+
+        $this->assertSame((string) $expected, (string) $parser->parseConstraints($input));
+    }
+
+    public function hyphenConstraints()
+    {
+        return array(
+            array('1 - 2',                  new VersionConstraint('>=', '1.0.0.0-dev'),   new VersionConstraint('<',  '3.0.0.0-dev')),
+            array('1.2.3 - 2.3.4.5',        new VersionConstraint('>=', '1.2.3.0-dev'),   new VersionConstraint('<=', '2.3.4.5')),
+            array('1.2-beta - 2.3',         new VersionConstraint('>=', '1.2.0.0-beta'),  new VersionConstraint('<',  '2.4.0.0-dev')),
+            array('1.2-beta - 2.3-dev',     new VersionConstraint('>=', '1.2.0.0-beta'),  new VersionConstraint('<=', '2.3.0.0-dev')),
+            array('1.2-RC - 2.3.1',         new VersionConstraint('>=', '1.2.0.0-RC'),    new VersionConstraint('<=', '2.3.1.0')),
+            array('1.2.3-alpha - 2.3-RC',   new VersionConstraint('>=', '1.2.3.0-alpha'), new VersionConstraint('<=', '2.3.0.0-RC')),
+        );
+    }
+
+    /**
+     * @dataProvider multiConstraintProvider
+     */
+    public function testParseConstraintsMulti($constraint)
     {
         $parser = new VersionParser;
         $first = new VersionConstraint('>', '2.0.0.0');
         $second = new VersionConstraint('<=', '3.0.0.0');
         $multi = new MultiConstraint(array($first, $second));
-        $this->assertSame((string) $multi, (string) $parser->parseConstraints('>2.0,<=3.0'));
+        $this->assertSame((string) $multi, (string) $parser->parseConstraints($constraint));
+    }
+
+    public function multiConstraintProvider()
+    {
+        return array(
+            array('>2.0,<=3.0'),
+            array('>2.0 <=3.0'),
+            array('>2.0  <=3.0'),
+            array('>2.0, <=3.0'),
+            array('>2.0 ,<=3.0'),
+            array('>2.0 , <=3.0'),
+            array('>2.0   , <=3.0'),
+            array('> 2.0   <=  3.0'),
+            array('> 2.0  ,  <=  3.0'),
+            array('  > 2.0  ,  <=  3.0 '),
+        );
     }
 
     public function testParseConstraintsMultiWithStabilitySuffix()
@@ -314,7 +401,10 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
         $this->assertSame((string) $multi, (string) $parser->parseConstraints('>=1.1.0-alpha4,<1.2-beta2'));
     }
 
-    public function testParseConstraintsMultiDisjunctiveHasPrioOverConjuctive()
+    /**
+     * @dataProvider multiConstraintProvider2
+     */
+    public function testParseConstraintsMultiDisjunctiveHasPrioOverConjuctive($constraint)
     {
         $parser = new VersionParser;
         $first = new VersionConstraint('>', '2.0.0.0');
@@ -322,7 +412,16 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
         $third = new VersionConstraint('>', '2.0.6.0');
         $multi1 = new MultiConstraint(array($first, $second));
         $multi2 = new MultiConstraint(array($multi1, $third), false);
-        $this->assertSame((string) $multi2, (string) $parser->parseConstraints('>2.0,<2.0.5 | >2.0.6'));
+        $this->assertSame((string) $multi2, (string) $parser->parseConstraints($constraint));
+    }
+
+    public function multiConstraintProvider2()
+    {
+        return array(
+            array('>2.0,<2.0.5 | >2.0.6'),
+            array('>2.0,<2.0.5 || >2.0.6'),
+            array('> 2.0 , <2.0.5 | >  2.0.6'),
+        );
     }
 
     public function testParseConstraintsMultiWithStabilities()
@@ -349,6 +448,9 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
         return array(
             'empty '            => array(''),
             'invalid version'   => array('1.0.0-meh'),
+            'operator abuse'    => array('>2.0,,<=3.0'),
+            'operator abuse/2'  => array('>2.0 ,, <=3.0'),
+            'operator abuse/3'  => array('>2.0 ||| <=3.0'),
         );
     }