[jsemver] 89/95: Refactor ExpressionParser to improve error handling
Alexandre Viau
reazem-guest at moszumanska.debian.org
Mon Feb 16 14:58:34 UTC 2015
This is an automated email from the git hooks/post-receive script.
reazem-guest pushed a commit to branch master
in repository jsemver.
commit f3b43eee9c2d50681d57996a66f666eedac11582
Author: Zafar Khaja <zafarkhaja at gmail.com>
Date: Thu Aug 14 20:29:13 2014 +0300
Refactor ExpressionParser to improve error handling
---
.../java/com/github/zafarkhaja/semver/Version.java | 5 ++
.../zafarkhaja/semver/expr/ExpressionParser.java | 59 ++++++++++------
.../com/github/zafarkhaja/semver/expr/Lexer.java | 43 ++++++++----
.../semver/expr/UnexpectedTokenException.java | 32 ++++++++-
.../semver/expr/ExpressionParserTest.java | 3 +-
.../github/zafarkhaja/semver/expr/LexerTest.java | 33 ++++++---
.../zafarkhaja/semver/expr/LexerTokenTest.java | 39 ++++++-----
.../semver/expr/ParserErrorHandlingTest.java | 80 ++++++++++++++++++++++
8 files changed, 232 insertions(+), 62 deletions(-)
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<Version> {
* @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<Expression> {
* @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> {
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<Expression> {
* @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<Expression> {
* @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<Expression> {
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<Expression> {
*/
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<Expression> {
* @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<Expression> {
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<Token> tokenize(String input) {
List<Token> tokens = new ArrayList<Token>();
+ 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<Token>(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;
/**
@@ -46,6 +47,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.
*
@@ -58,6 +70,24 @@ public class UnexpectedTokenException extends ParseException {
}
/**
+ * 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
* token and, if available, about the expected types.
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<Token> stream = lexer.tokenize(">1.0.0");
@@ -53,8 +54,9 @@ 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<Token> stream = lexer.tokenize("> 1");
@@ -62,6 +64,21 @@ public class LexerTest {
}
@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<Token> stream = lexer.tokenize("1.2.3");
+ assertArrayEquals(expected, stream.toArray());
+ }
+
+ @Test
public void shouldRaiseErrorOnIllegalCharacter() {
Lexer lexer = new Lexer();
try {
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 <zafarkhaja at gmail.com>.
+ *
+ * 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 <zafarkhaja at gmail.com>
+ */
+ at 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<Object[]> 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 } },
+ });
+ }
+}
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-java/jsemver.git
More information about the pkg-java-commits
mailing list