Implement partial compatibility with node-semver

This commit enhances expressions for greater compatibility
with node-semver (https://github.com/npm/node-semver):

- Added support for the Caret operator (^)
- Added support for the X-Ranges with additional wildcard "x"
- Fixed the Tilde ranges to be fully compatible

Known issues:

- The X-Ranges in the form of short versions are not interpreted
  correctly if not followed by EOL: while version "1.2.3" does
  satisfy expression "1", it doesn't satisfy expression "1 | 2".
This commit is contained in:
Shay Yaakov 2014-12-28 19:08:48 +02:00 committed by Zafar Khaja
parent ea68756f5e
commit 4b6c9edd66
4 changed files with 163 additions and 21 deletions

View File

@ -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<Expression> {
* <expr> ::= <comparison-expr>
* | <version-expr>
* | <tilde-expr>
* | <caret-expr>
* | <range-expr>
* }
* </pre>
@ -161,6 +162,8 @@ public class ExpressionParser implements Parser<Expression> {
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<Expression> {
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 <caret-expr>} non-terminal.
*
* <pre>
* {@literal
* <caret-expr> ::= "^" <version>
* }
* </pre>
*
* @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 <version-expr>} non-terminal.
@ -251,7 +297,7 @@ public class ExpressionParser implements Parser<Expression> {
* {@code false} otherwise
*/
private boolean isVersionExpression() {
return isVersionFollowedBy(STAR);
return isVersionFollowedBy(WILDCARD) || isVersionFollowedBy(EOL);
}
/**
@ -259,26 +305,50 @@ public class ExpressionParser implements Parser<Expression> {
*
* <pre>
* {@literal
* <version-expr> ::= <major> "." "*"
* | <major> "." <minor> "." "*"
* <version-expr> ::= <wildcard>
* | <major> "." <wildcard>
* | <major> "." <minor> "." <wildcard>
*
* <wildcard> ::= "*" | "x" | "X"
* }
* </pre>
*
* @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);
if (tokens.positiveLookahead(DOT)) {
consumeNextToken(DOT);
if (tokens.positiveLookahead(STAR)) {
}
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);
if (tokens.positiveLookahead(DOT)) {
consumeNextToken(DOT);
consumeNextToken(STAR);
}
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));
}
/**
* Determines if the following version terminals are
* part of the {@literal <range-expr>} non-terminal.

View File

@ -57,7 +57,8 @@ class Lexer {
LESS("<(?!=)"),
LESS_EQUAL("<="),
TILDE("~"),
STAR("\\*"),
WILDCARD("[\\*xX]"),
CARET("\\^"),
AND("&"),
OR("\\|"),
NOT("!(?!=)"),

View File

@ -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");

View File

@ -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
}
}