diff --git a/src/main/java/com/github/zafarkhaja/semver/expr/ExpressionParser.java b/src/main/java/com/github/zafarkhaja/semver/expr/ExpressionParser.java index 4073fa9..96ed0bb 100644 --- a/src/main/java/com/github/zafarkhaja/semver/expr/ExpressionParser.java +++ b/src/main/java/com/github/zafarkhaja/semver/expr/ExpressionParser.java @@ -31,8 +31,8 @@ import com.github.zafarkhaja.semver.util.Stream.ElementType; import com.github.zafarkhaja.semver.util.UnexpectedElementException; import java.util.EnumSet; import java.util.Iterator; -import static com.github.zafarkhaja.semver.expr.Lexer.Token.Type.*; import static com.github.zafarkhaja.semver.expr.CompositeExpression.Helper.*; +import static com.github.zafarkhaja.semver.expr.Lexer.Token.Type.*; /** * A parser for the SemVer Expressions. @@ -152,6 +152,7 @@ public class ExpressionParser implements Parser { * ::= * | * | + * | * | * } * @@ -161,6 +162,8 @@ public class ExpressionParser implements Parser { private CompositeExpression parseExpression() { if (tokens.positiveLookahead(TILDE)) { return parseTildeExpression(); + } if (tokens.positiveLookahead(CARET)) { + return parseCaretExpression(); } else if (isVersionExpression()) { return parseVersionExpression(); } else if (isRangeExpression()) { @@ -230,18 +233,61 @@ public class ExpressionParser implements Parser { consumeNextToken(TILDE); int major = intOf(consumeNextToken(NUMERIC).lexeme); if (!tokens.positiveLookahead(DOT)) { - return gte(versionFor(major)); + return gte(versionFor(major)).and(lt(versionFor(major + 1))); } consumeNextToken(DOT); int minor = intOf(consumeNextToken(NUMERIC).lexeme); if (!tokens.positiveLookahead(DOT)) { - return gte(versionFor(major, minor)).and(lt(versionFor(major + 1))); + return gte(versionFor(major, minor)).and(lt(versionFor(major, minor + 1))); } consumeNextToken(DOT); int patch = intOf(consumeNextToken(NUMERIC).lexeme); return gte(versionFor(major, minor, patch)).and(lt(versionFor(major, minor + 1))); } + /** + * Parses the {@literal } non-terminal. + * + *
+     * {@literal
+     *  ::= "^" 
+     * }
+     * 
+ * + * @return the expression AST + */ + private CompositeExpression parseCaretExpression() { + consumeNextToken(CARET); + int major = intOf(consumeNextToken(NUMERIC).lexeme); + if (!tokens.positiveLookahead(DOT)) { + return gte(versionFor(major)).and(lt(versionFor(major + 1))); + } + consumeNextToken(DOT); + int minor = intOf(consumeNextToken(NUMERIC).lexeme); + if (!tokens.positiveLookahead(DOT)) { + if (major > 0) { + return gte(versionFor(major, minor)).and(lt(versionFor(major + 1, minor))); + } else { + return gte(versionFor(major, minor)).and(lt(versionFor(major, minor + 1))); + } + } + consumeNextToken(DOT); + int patch = intOf(consumeNextToken(NUMERIC).lexeme); + CompositeExpression ltExp; + if (major > 0) { + ltExp = lt(versionFor(major + 1)); + } else if (minor > 0) { + ltExp = lt(versionFor(major, minor + 1)); + } else { + if (patch > 0) { + ltExp = lt(versionFor(major, minor, patch + 1)); + } else { + ltExp = lt(versionFor(major, minor, patch)); + } + } + return gte(versionFor(major, minor, patch)).and(ltExp); + } + /** * Determines if the following version terminals are part * of the {@literal } non-terminal. @@ -251,7 +297,7 @@ public class ExpressionParser implements Parser { * {@code false} otherwise */ private boolean isVersionExpression() { - return isVersionFollowedBy(STAR); + return isVersionFollowedBy(WILDCARD) || isVersionFollowedBy(EOL); } /** @@ -259,24 +305,48 @@ public class ExpressionParser implements Parser { * *
      * {@literal
-     *  ::=  "." "*"
-     *                  |  "."  "." "*"
+     *  ::= 
+     *                  |  "." 
+     *                  |  "."  "." 
+     *
+     *  ::= "*" | "x" | "X"
      * }
      * 
* * @return the expression AST */ private CompositeExpression parseVersionExpression() { + if (tokens.positiveLookahead(WILDCARD)) { + tokens.consume(); + return gte(versionFor(0, 0, 0)); + } int major = intOf(consumeNextToken(NUMERIC).lexeme); - consumeNextToken(DOT); - if (tokens.positiveLookahead(STAR)) { + if (tokens.positiveLookahead(DOT)) { + consumeNextToken(DOT); + } + if (tokens.positiveLookahead(WILDCARD) || tokens.positiveLookahead(EOL)) { + if (tokens.positiveLookahead(WILDCARD)) { + tokens.consume(); + } + return gte(versionFor(major)).and(lt(versionFor(major + 1))); + } + if (tokens.positiveLookahead(WILDCARD)) { tokens.consume(); return gte(versionFor(major)).and(lt(versionFor(major + 1))); } int minor = intOf(consumeNextToken(NUMERIC).lexeme); - consumeNextToken(DOT); - consumeNextToken(STAR); - return gte(versionFor(major, minor)).and(lt(versionFor(major, minor + 1))); + if (tokens.positiveLookahead(DOT)) { + consumeNextToken(DOT); + } + if (tokens.positiveLookahead(WILDCARD) || tokens.positiveLookahead(EOL)) { + if (tokens.positiveLookahead(WILDCARD)) { + tokens.consume(); + } + return gte(versionFor(major, minor)).and(lt(versionFor(major, minor + 1))); + } + + int patch = intOf(consumeNextToken(NUMERIC).lexeme); + return gte(versionFor(major, minor, patch)); } /** diff --git a/src/main/java/com/github/zafarkhaja/semver/expr/Lexer.java b/src/main/java/com/github/zafarkhaja/semver/expr/Lexer.java index 4aa3ade..a19237c 100644 --- a/src/main/java/com/github/zafarkhaja/semver/expr/Lexer.java +++ b/src/main/java/com/github/zafarkhaja/semver/expr/Lexer.java @@ -57,7 +57,8 @@ class Lexer { LESS("<(?!=)"), LESS_EQUAL("<="), TILDE("~"), - STAR("\\*"), + WILDCARD("[\\*xX]"), + CARET("\\^"), AND("&"), OR("\\|"), NOT("!(?!=)"), diff --git a/src/test/java/com/github/zafarkhaja/semver/VersionTest.java b/src/test/java/com/github/zafarkhaja/semver/VersionTest.java index 1b5fb9f..e7c747a 100644 --- a/src/test/java/com/github/zafarkhaja/semver/VersionTest.java +++ b/src/test/java/com/github/zafarkhaja/semver/VersionTest.java @@ -26,7 +26,8 @@ package com.github.zafarkhaja.semver; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; -import static com.github.zafarkhaja.semver.expr.CompositeExpression.Helper.*; +import static com.github.zafarkhaja.semver.expr.CompositeExpression.Helper.gte; +import static com.github.zafarkhaja.semver.expr.CompositeExpression.Helper.lt; import static org.junit.Assert.*; /** @@ -307,13 +308,6 @@ public class VersionTest { assertTrue(0 > v1.compareWithBuildsTo(v2)); } - @Test - public void shouldCheckIfVersionSatisfiesExpressionString() { - Version v = Version.valueOf("2.0.0-beta"); - assertTrue(v.satisfies("~1.0")); - assertFalse(v.satisfies(">=2.0 & <3.0")); - } - @Test public void shouldCheckIfVersionSatisfiesExpression() { Version v = Version.valueOf("2.0.0-beta"); diff --git a/src/test/java/com/github/zafarkhaja/semver/expr/ExpressionParserTest.java b/src/test/java/com/github/zafarkhaja/semver/expr/ExpressionParserTest.java index 510be1a..c08768b 100644 --- a/src/test/java/com/github/zafarkhaja/semver/expr/ExpressionParserTest.java +++ b/src/test/java/com/github/zafarkhaja/semver/expr/ExpressionParserTest.java @@ -89,7 +89,7 @@ public class ExpressionParserTest { ExpressionParser parser = new ExpressionParser(new Lexer()); Expression expr1 = parser.parse("~1"); assertTrue(expr1.interpret(Version.valueOf("1.2.3"))); - assertTrue(expr1.interpret(Version.valueOf("3.2.1"))); + assertFalse(expr1.interpret(Version.valueOf("3.2.1"))); Expression expr2 = parser.parse("~1.2"); assertTrue(expr2.interpret(Version.valueOf("1.2.3"))); assertFalse(expr2.interpret(Version.valueOf("2.0.0"))); @@ -201,4 +201,81 @@ public class ExpressionParserTest { } fail("Should raise error if closing parenthesis is missing"); } + + @Test + public void shouldCheckIfPrereleaseVersionSatisfiesExpression() { + Version v = Version.valueOf("2.1.0-beta"); + assertTrue(v.satisfies("*")); // >=0.0.0 + assertTrue(v.satisfies("x")); // >=0.0.0 + assertTrue(v.satisfies("X")); // >=0.0.0 + assertTrue(v.satisfies("2.*")); // >=2.0.0 <3.0.0 + assertTrue(v.satisfies("2.x")); // >=2.0.0 <3.0.0 + assertTrue(v.satisfies("2.X")); // >=2.0.0 <3.0.0 + assertTrue(v.satisfies("2.0.*")); // >=2.0.0 <2.1.0 + assertTrue(v.satisfies("2.0.x")); // >=2.0.0 <2.1.0 + assertTrue(v.satisfies("2.0.X")); // >=2.0.0 <2.1.0 + assertTrue(v.satisfies("2")); // >=2.0.0 <3.0.0 + assertTrue(v.satisfies("2.0")); // >=2.0.0 <2.1.0 + assertTrue(v.satisfies("2.0.0")); // >=2.0.0 + assertTrue(v.satisfies("~2")); // >=2.0.0 <3.0.0 + assertTrue(v.satisfies("~2.0")); // >=2.0.0 <2.1.0 + assertTrue(v.satisfies("~2.0.0")); // >=2.0.0 <2.1.0 + assertTrue(v.satisfies(">=2.0 & <3.0")); // >=2.0.0 & <3.0.0 + assertTrue(v.satisfies("2.0.0 - 2.1.0")); // >=2.0.0 & <=2.1.0 + assertTrue(v.satisfies("2.0 - 2.1")); // >=2.0.0 & <=2.1.0 + assertTrue(v.satisfies("^2")); // >=2.0.0 <3.0.0 + assertTrue(v.satisfies("^2.0.3")); // >=2.0.3 <3.0.0 + assertFalse(v.satisfies("2.1.*")); // >=2.1.0 <2.2.0 + assertFalse(v.satisfies("2.1.x")); // >=2.1.0 <2.2.0 + assertFalse(v.satisfies("2.1.X")); // >=2.1.0 <2.2.0 + assertFalse(v.satisfies("~1")); // >=1.0.0 <2.0.0 + assertFalse(v.satisfies("~1.2.3")); // >=1.2.3 <1.3.0 + assertFalse(v.satisfies("~1.2")); // >=1.2.0 <1.3.0 + assertFalse(v.satisfies("~2.2.3")); // >=2.2.3 <2.3.0 + assertFalse(v.satisfies("^0.2.3")); // >=0.2.3 <0.3.0 + assertFalse(v.satisfies("^0.0.3")); // >=0.0.3 <0.0.4 + assertFalse(v.satisfies("^0")); // >=0.0.0 <0.1.0 + assertFalse(v.satisfies("^0.0")); // >=0.0.0 <0.1.0 + assertFalse(v.satisfies("^0.0.0")); // >=0.0.0 <0.0.0 + assertFalse(v.satisfies(">=1.0 & <2.0")); // >=1.0.0 & <2.0.0 + assertFalse(v.satisfies("1 - 2")); // >=1.0.0 & <2.0.0 + } + + @Test + public void shouldCheckIfStableVersionSatisfiesExpression() { + Version v = Version.valueOf("2.1.0"); + assertTrue(v.satisfies("*")); // >=0.0.0 + assertTrue(v.satisfies("x")); // >=0.0.0 + assertTrue(v.satisfies("X")); // >=0.0.0 + assertTrue(v.satisfies("2.*")); // >=2.0.0 <3.0.0 + assertTrue(v.satisfies("2.x")); // >=2.0.0 <3.0.0 + assertTrue(v.satisfies("2.X")); // >=2.0.0 <3.0.0 + assertTrue(v.satisfies("2.1.*")); // >=2.1.0 <2.2.0 + assertTrue(v.satisfies("2.1.x")); // >=2.1.0 <2.2.0 + assertTrue(v.satisfies("2.1.X")); // >=2.1.0 <2.2.0 + assertTrue(v.satisfies("2")); // >=2.0.0 <3.0.0 + assertTrue(v.satisfies("2.0.0")); // >=2.0.0 + assertTrue(v.satisfies("~2")); // >=2.0.0 <3.0.0 + assertTrue(v.satisfies(">=2.0 & <3.0")); // >=2.0.0 & <3.0.0 + assertTrue(v.satisfies("2.0.0 - 2.1.0")); // >=2.0.0 & <=2.1.0 + assertTrue(v.satisfies("2.0 - 2.1")); // >=2.0.0 & <=2.1.0 + assertTrue(v.satisfies("^2")); // >=2.0.0 <3.0.0 + assertTrue(v.satisfies("^2.0")); // >=2.0.0 <3.0.0 + assertTrue(v.satisfies("^2.0.3")); // >=2.0.3 <3.0.0 + assertFalse(v.satisfies("2.0")); // >=2.0.0 <2.1.0 + assertFalse(v.satisfies("2.0.*")); // >=2.0.0 <2.1.0 + assertFalse(v.satisfies("~2.0.0")); // >=2.0.0 <2.1.0 + assertFalse(v.satisfies("~2.0")); // >=2.0.0 <2.1.0 + assertFalse(v.satisfies("~1")); // >=1.0.0 <2.0.0 + assertFalse(v.satisfies("~1.2.3")); // >=1.2.3 <1.3.0 + assertFalse(v.satisfies("~1.2")); // >=1.2.0 <1.3.0 + assertFalse(v.satisfies("~2.2.3")); // >=2.2.3 <2.3.0 + assertFalse(v.satisfies("^0.2.3")); // >=0.2.3 <0.3.0 + assertFalse(v.satisfies("^0.0.3")); // >=0.0.3 <0.0.4 + assertFalse(v.satisfies("^0")); // >=0.0.0 <0.1.0 + assertFalse(v.satisfies("^0.0")); // >=0.0.0 <0.1.0 + assertFalse(v.satisfies("^0.0.0")); // >=0.0.0 <0.0.0 + assertFalse(v.satisfies(">=1.0 & <2.0")); // >=1.0.0 & <2.0.0 + assertFalse(v.satisfies("1 - 2")); // >=1.0.0 & <2.0.0 + } }