diff --git a/src/main/java/com/github/zafarkhaja/semver/Version.java b/src/main/java/com/github/zafarkhaja/semver/Version.java index 3e33603..e49d87d 100644 --- a/src/main/java/com/github/zafarkhaja/semver/Version.java +++ b/src/main/java/com/github/zafarkhaja/semver/Version.java @@ -25,6 +25,8 @@ package com.github.zafarkhaja.semver; import com.github.zafarkhaja.semver.expr.Expression; import com.github.zafarkhaja.semver.expr.ExpressionParser; +import com.github.zafarkhaja.semver.expr.LexerException; +import com.github.zafarkhaja.semver.expr.UnexpectedTokenException; import java.util.Comparator; /** @@ -310,6 +312,9 @@ public class Version implements Comparable { * @param expr the SemVer Expression * @return {@code true} if this version satisfies the specified * SemVer Expression or {@code false} otherwise + * @throws ParseException in case of a general parse error + * @throws LexerException when encounters an illegal character + * @throws UnexpectedTokenException when comes across an unexpected token * @since 0.7.0 */ public boolean satisfies(String expr) { 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 040fa52..486f87c 100644 --- a/src/main/java/com/github/zafarkhaja/semver/expr/ExpressionParser.java +++ b/src/main/java/com/github/zafarkhaja/semver/expr/ExpressionParser.java @@ -78,12 +78,14 @@ public class ExpressionParser implements Parser { * @param input a string representing the SemVer Expression * @return the AST for the SemVer Expressions * @throws LexerException when encounters an illegal character - * @throws UnexpectedElementException when consumes a token of an unexpected type + * @throws UnexpectedTokenException when consumes a token of an unexpected type */ @Override public Expression parse(String input) { tokens = lexer.tokenize(input); - return parseSemVerExpression(); + Expression expr = parseSemVerExpression(); + consumeNextToken(EOL); + return expr; } /** @@ -104,13 +106,13 @@ public class ExpressionParser implements Parser { Expression expr; if (tokens.positiveLookahead(NOT)) { tokens.consume(); - tokens.consume(LEFT_PAREN); + consumeNextToken(LEFT_PAREN); expr = new Not(parseSemVerExpression()); - tokens.consume(RIGHT_PAREN); + consumeNextToken(RIGHT_PAREN); } else if (tokens.positiveLookahead(LEFT_PAREN)) { - tokens.consume(LEFT_PAREN); + consumeNextToken(LEFT_PAREN); expr = parseSemVerExpression(); - tokens.consume(RIGHT_PAREN); + consumeNextToken(RIGHT_PAREN); } else { expr = parseExpression(); } @@ -224,21 +226,21 @@ public class ExpressionParser implements Parser { * @return the expression AST */ private Expression parseTildeExpression() { - tokens.consume(TILDE); - int major = intOf(tokens.consume(NUMERIC).lexeme); + consumeNextToken(TILDE); + int major = intOf(consumeNextToken(NUMERIC).lexeme); if (!tokens.positiveLookahead(DOT)) { return new GreaterOrEqual(versionOf(major, 0, 0)); } - tokens.consume(DOT); - int minor = intOf(tokens.consume(NUMERIC).lexeme); + consumeNextToken(DOT); + int minor = intOf(consumeNextToken(NUMERIC).lexeme); if (!tokens.positiveLookahead(DOT)) { return new And( new GreaterOrEqual(versionOf(major, minor, 0)), new Less(versionOf(major + 1, 0, 0)) ); } - tokens.consume(DOT); - int patch = intOf(tokens.consume(NUMERIC).lexeme); + consumeNextToken(DOT); + int patch = intOf(consumeNextToken(NUMERIC).lexeme); return new And( new GreaterOrEqual(versionOf(major, minor, patch)), new Less(versionOf(major, minor + 1, 0)) @@ -270,8 +272,8 @@ public class ExpressionParser implements Parser { * @return the expression AST */ private Expression parseVersionExpression() { - int major = intOf(tokens.consume(NUMERIC).lexeme); - tokens.consume(DOT); + int major = intOf(consumeNextToken(NUMERIC).lexeme); + consumeNextToken(DOT); if (tokens.positiveLookahead(STAR)) { tokens.consume(); return new And( @@ -279,9 +281,9 @@ public class ExpressionParser implements Parser { new Less(versionOf(major + 1, 0, 0)) ); } - int minor = intOf(tokens.consume(NUMERIC).lexeme); - tokens.consume(DOT); - tokens.consume(STAR); + int minor = intOf(consumeNextToken(NUMERIC).lexeme); + consumeNextToken(DOT); + consumeNextToken(STAR); return new And( new GreaterOrEqual(versionOf(major, minor, 0)), new Less(versionOf(major, minor + 1, 0)) @@ -313,7 +315,7 @@ public class ExpressionParser implements Parser { */ private Expression parseRangeExpression() { Expression ge = new GreaterOrEqual(parseVersion()); - tokens.consume(HYPHEN); + consumeNextToken(HYPHEN); Expression le = new LessOrEqual(parseVersion()); return new And(ge, le); } @@ -332,16 +334,16 @@ public class ExpressionParser implements Parser { * @return the parsed version */ private Version parseVersion() { - int major = intOf(tokens.consume(NUMERIC).lexeme); + int major = intOf(consumeNextToken(NUMERIC).lexeme); int minor = 0; if (tokens.positiveLookahead(DOT)) { tokens.consume(); - minor = intOf(tokens.consume(NUMERIC).lexeme); + minor = intOf(consumeNextToken(NUMERIC).lexeme); } int patch = 0; if (tokens.positiveLookahead(DOT)) { tokens.consume(); - patch = intOf(tokens.consume(NUMERIC).lexeme); + patch = intOf(consumeNextToken(NUMERIC).lexeme); } return versionOf(major, minor, patch); } @@ -391,4 +393,19 @@ public class ExpressionParser implements Parser { private int intOf(String value) { return Integer.parseInt(value); } + + /** + * Tries to consume the next token in the stream. + * + * @param expected the expected types of the next token + * @return the next token in the stream + * @throws UnexpectedTokenException when encounters an unexpected token type + */ + private Token consumeNextToken(Token.Type... expected) { + try { + return tokens.consume(expected); + } catch (UnexpectedElementException e) { + throw new UnexpectedTokenException(e); + } + } } 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 b63efc1..384a681 100644 --- a/src/main/java/com/github/zafarkhaja/semver/expr/Lexer.java +++ b/src/main/java/com/github/zafarkhaja/semver/expr/Lexer.java @@ -64,15 +64,7 @@ class Lexer { LEFT_PAREN("\\("), RIGHT_PAREN("\\)"), WHITESPACE("\\s+"), - EOL("?!") { - /** - * {@inheritDoc} - */ - @Override - public boolean isMatchedBy(Token token) { - return token == null; - } - }; + EOL("?!"); /** * A pattern matching this type. @@ -123,14 +115,22 @@ class Lexer { final String lexeme; /** - * Constructs a {@code Token} instance with the type and lexeme. + * The position of this token. + */ + final int position; + + /** + * Constructs a {@code Token} instance + * with the type, lexeme and position. * * @param type the type of this token * @param lexeme the lexeme of this token + * @param position the position of this token */ - Token(Type type, String lexeme) { + Token(Type type, String lexeme, int position) { this.type = type; this.lexeme = (lexeme == null) ? "" : lexeme; + this.position = position; } /** @@ -145,7 +145,10 @@ class Lexer { return false; } Token token = (Token) other; - return type.equals(token.type) && lexeme.equals(token.lexeme); + return + type.equals(token.type) && + lexeme.equals(token.lexeme) && + position == token.position; } /** @@ -156,6 +159,7 @@ class Lexer { int hash = 5; hash = 71 * hash + type.hashCode(); hash = 71 * hash + lexeme.hashCode(); + hash = 71 * hash + position; return hash; } @@ -166,7 +170,11 @@ class Lexer { */ @Override public String toString() { - return type.name() + "(" + lexeme + ")"; + return String.format( + "%s(%s) at position %d", + type.name(), + lexeme, position + ); } } @@ -186,6 +194,7 @@ class Lexer { */ Stream tokenize(String input) { List tokens = new ArrayList(); + int tokenPos = 0; while (!input.isEmpty()) { boolean matched = false; for (Token.Type tokenType : Token.Type.values()) { @@ -194,8 +203,13 @@ class Lexer { matched = true; input = matcher.replaceFirst(""); if (tokenType != Token.Type.WHITESPACE) { - tokens.add(new Token(tokenType, matcher.group())); + tokens.add(new Token( + tokenType, + matcher.group(), + tokenPos + )); } + tokenPos += matcher.end(); break; } } @@ -203,6 +217,7 @@ class Lexer { throw new LexerException(input); } } + tokens.add(new Token(Token.Type.EOL, null, tokenPos)); return new Stream(tokens.toArray(new Token[tokens.size()])); } } diff --git a/src/main/java/com/github/zafarkhaja/semver/expr/UnexpectedTokenException.java b/src/main/java/com/github/zafarkhaja/semver/expr/UnexpectedTokenException.java index 44973fd..9a9ce5a 100644 --- a/src/main/java/com/github/zafarkhaja/semver/expr/UnexpectedTokenException.java +++ b/src/main/java/com/github/zafarkhaja/semver/expr/UnexpectedTokenException.java @@ -24,7 +24,8 @@ package com.github.zafarkhaja.semver.expr; import com.github.zafarkhaja.semver.ParseException; -import com.github.zafarkhaja.semver.expr.Lexer.*; +import com.github.zafarkhaja.semver.expr.Lexer.Token; +import com.github.zafarkhaja.semver.util.UnexpectedElementException; import java.util.Arrays; /** @@ -45,6 +46,17 @@ public class UnexpectedTokenException extends ParseException { */ private final Token.Type[] expected; + /** + * Constructs a {@code UnexpectedTokenException} instance with + * the wrapped {@code UnexpectedElementException} exception. + * + * @param cause the wrapped exception + */ + UnexpectedTokenException(UnexpectedElementException cause) { + unexpected = (Token) cause.getUnexpectedElement(); + expected = (Token.Type[]) cause.getExpectedElementTypes(); + } + /** * Constructs a {@code UnexpectedTokenException} instance * with the unexpected token and the expected types. @@ -57,6 +69,24 @@ public class UnexpectedTokenException extends ParseException { this.expected = expected; } + /** + * Gets the unexpected token. + * + * @return the unexpected token + */ + Token getUnexpectedToken() { + return unexpected; + } + + /** + * Gets the expected token types. + * + * @return an array of expected token types + */ + Token.Type[] getExpectedTokenTypes() { + return expected; + } + /** * Returns the string representation of this exception * containing the information about the unexpected 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 7c0e429..226686a 100644 --- a/src/test/java/com/github/zafarkhaja/semver/expr/ExpressionParserTest.java +++ b/src/test/java/com/github/zafarkhaja/semver/expr/ExpressionParserTest.java @@ -24,7 +24,6 @@ package com.github.zafarkhaja.semver.expr; import com.github.zafarkhaja.semver.Version; -import com.github.zafarkhaja.semver.util.UnexpectedElementException; import org.junit.Test; import static org.junit.Assert.*; @@ -197,7 +196,7 @@ public class ExpressionParserTest { ExpressionParser parser = new ExpressionParser(new Lexer()); try { parser.parse("((>=1.0.1 & < 2)"); - } catch (UnexpectedElementException e) { + } catch (UnexpectedTokenException e) { return; } fail("Should raise error if closing parenthesis is missing"); diff --git a/src/test/java/com/github/zafarkhaja/semver/expr/LexerTest.java b/src/test/java/com/github/zafarkhaja/semver/expr/LexerTest.java index 1da42c7..6fd19a4 100644 --- a/src/test/java/com/github/zafarkhaja/semver/expr/LexerTest.java +++ b/src/test/java/com/github/zafarkhaja/semver/expr/LexerTest.java @@ -38,12 +38,13 @@ public class LexerTest { @Test public void shouldTokenizeVersionString() { Token[] expected = { - new Token(GREATER, ">"), - new Token(NUMERIC, "1"), - new Token(DOT, "."), - new Token(NUMERIC, "0"), - new Token(DOT, "."), - new Token(NUMERIC, "0"), + new Token(GREATER, ">", 0), + new Token(NUMERIC, "1", 1), + new Token(DOT, ".", 2), + new Token(NUMERIC, "0", 3), + new Token(DOT, ".", 4), + new Token(NUMERIC, "0", 5), + new Token(EOL, null, 6), }; Lexer lexer = new Lexer(); Stream stream = lexer.tokenize(">1.0.0"); @@ -53,14 +54,30 @@ public class LexerTest { @Test public void shouldSkipWhitespaces() { Token[] expected = { - new Token(GREATER, ">"), - new Token(NUMERIC, "1"), + new Token(GREATER, ">", 0), + new Token(NUMERIC, "1", 2), + new Token(EOL, null, 3), }; Lexer lexer = new Lexer(); Stream stream = lexer.tokenize("> 1"); assertArrayEquals(expected, stream.toArray()); } + @Test + public void shouldEndWithEol() { + Token[] expected = { + new Token(NUMERIC, "1", 0), + new Token(DOT, ".", 1), + new Token(NUMERIC, "2", 2), + new Token(DOT, ".", 3), + new Token(NUMERIC, "3", 4), + new Token(EOL, null, 5), + }; + Lexer lexer = new Lexer(); + Stream stream = lexer.tokenize("1.2.3"); + assertArrayEquals(expected, stream.toArray()); + } + @Test public void shouldRaiseErrorOnIllegalCharacter() { Lexer lexer = new Lexer(); diff --git a/src/test/java/com/github/zafarkhaja/semver/expr/LexerTokenTest.java b/src/test/java/com/github/zafarkhaja/semver/expr/LexerTokenTest.java index 36c4729..649cc3e 100644 --- a/src/test/java/com/github/zafarkhaja/semver/expr/LexerTokenTest.java +++ b/src/test/java/com/github/zafarkhaja/semver/expr/LexerTokenTest.java @@ -41,23 +41,23 @@ public class LexerTokenTest { @Test public void shouldBeReflexive() { - Token token = new Token(NUMERIC, "1"); + Token token = new Token(NUMERIC, "1", 0); assertTrue(token.equals(token)); } @Test public void shouldBeSymmetric() { - Token t1 = new Token(EQUAL, "="); - Token t2 = new Token(EQUAL, "="); + Token t1 = new Token(EQUAL, "=", 0); + Token t2 = new Token(EQUAL, "=", 0); assertTrue(t1.equals(t2)); assertTrue(t2.equals(t1)); } @Test public void shouldBeTransitive() { - Token t1 = new Token(GREATER, ">"); - Token t2 = new Token(GREATER, ">"); - Token t3 = new Token(GREATER, ">"); + Token t1 = new Token(GREATER, ">", 0); + Token t2 = new Token(GREATER, ">", 0); + Token t3 = new Token(GREATER, ">", 0); assertTrue(t1.equals(t2)); assertTrue(t2.equals(t3)); assertTrue(t1.equals(t3)); @@ -65,8 +65,8 @@ public class LexerTokenTest { @Test public void shouldBeConsistent() { - Token t1 = new Token(HYPHEN, "-"); - Token t2 = new Token(HYPHEN, "-"); + Token t1 = new Token(HYPHEN, "-", 0); + Token t2 = new Token(HYPHEN, "-", 0); assertTrue(t1.equals(t2)); assertTrue(t1.equals(t2)); assertTrue(t1.equals(t2)); @@ -74,28 +74,35 @@ public class LexerTokenTest { @Test public void shouldReturnFalseIfOtherVersionIsOfDifferentType() { - Token t1 = new Token(DOT, "."); + Token t1 = new Token(DOT, ".", 0); assertFalse(t1.equals(new String("."))); } @Test public void shouldReturnFalseIfOtherVersionIsNull() { - Token t1 = new Token(AND, "&"); + Token t1 = new Token(AND, "&", 0); Token t2 = null; assertFalse(t1.equals(t2)); } @Test public void shouldReturnFalseIfTypesAreDifferent() { - Token t1 = new Token(EQUAL, "="); - Token t2 = new Token(NOT_EQUAL, "!="); + Token t1 = new Token(EQUAL, "=", 0); + Token t2 = new Token(NOT_EQUAL, "!=", 0); assertFalse(t1.equals(t2)); } @Test public void shouldReturnFalseIfLexemesAreDifferent() { - Token t1 = new Token(NUMERIC, "1"); - Token t2 = new Token(NUMERIC, "2"); + Token t1 = new Token(NUMERIC, "1", 0); + Token t2 = new Token(NUMERIC, "2", 0); + assertFalse(t1.equals(t2)); + } + + @Test + public void shouldReturnFalseIfPositionsAreDifferent() { + Token t1 = new Token(NUMERIC, "1", 1); + Token t2 = new Token(NUMERIC, "1", 2); assertFalse(t1.equals(t2)); } } @@ -104,8 +111,8 @@ public class LexerTokenTest { @Test public void shouldReturnSameHashCodeIfTokensAreEqual() { - Token t1 = new Token(NUMERIC, "1"); - Token t2 = new Token(NUMERIC, "1"); + Token t1 = new Token(NUMERIC, "1", 0); + Token t2 = new Token(NUMERIC, "1", 0); assertTrue(t1.equals(t2)); assertEquals(t1.hashCode(), t2.hashCode()); } diff --git a/src/test/java/com/github/zafarkhaja/semver/expr/ParserErrorHandlingTest.java b/src/test/java/com/github/zafarkhaja/semver/expr/ParserErrorHandlingTest.java new file mode 100644 index 0000000..dc1abf4 --- /dev/null +++ b/src/test/java/com/github/zafarkhaja/semver/expr/ParserErrorHandlingTest.java @@ -0,0 +1,80 @@ +/* + * The MIT License + * + * Copyright 2014 Zafar Khaja . + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.github.zafarkhaja.semver.expr; + +import com.github.zafarkhaja.semver.expr.Lexer.Token; +import java.util.Arrays; +import java.util.Collection; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import static com.github.zafarkhaja.semver.expr.Lexer.Token.Type.*; +import static org.junit.Assert.*; + +/** + * + * @author Zafar Khaja + */ +@RunWith(Parameterized.class) +public class ParserErrorHandlingTest { + + private final String invalidExpr; + private final Token unexpected; + private final Token.Type[] expected; + + public ParserErrorHandlingTest( + String invalidExpr, + Token unexpected, + Token.Type[] expected + ) { + this.invalidExpr = invalidExpr; + this.unexpected = unexpected; + this.expected = expected; + } + + @Test + public void shouldCorrectlyHandleParseErrors() { + try { + ExpressionParser.newInstance().parse(invalidExpr); + } catch (UnexpectedTokenException e) { + assertEquals(unexpected, e.getUnexpectedToken()); + assertArrayEquals(expected, e.getExpectedTokenTypes()); + return; + } + fail("Uncaught exception"); + } + + @Parameters(name = "{0}") + public static Collection parameters() { + return Arrays.asList(new Object[][] { + { "1)", new Token(RIGHT_PAREN, ")", 1), new Token.Type[] { EOL } }, + { "(>1.0.1", new Token(EOL, null, 7), new Token.Type[] { RIGHT_PAREN } }, + { "((>=1 & <2)", new Token(EOL, null, 11), new Token.Type[] { RIGHT_PAREN } }, + { ">=1.0.0 &", new Token(EOL, null, 9), new Token.Type[] { NUMERIC } }, + { "(>2.0 |)", new Token(RIGHT_PAREN, ")", 7), new Token.Type[] { NUMERIC } }, + { "& 1.2", new Token(AND, "&", 0), new Token.Type[] { NUMERIC } }, + }); + } +}