Refactor ExpressionParser to improve error handling

This commit is contained in:
Zafar Khaja 2014-08-14 20:29:13 +03:00
parent 399df4d267
commit f3b43eee9c
8 changed files with 232 additions and 62 deletions

View File

@ -25,6 +25,8 @@ package com.github.zafarkhaja.semver;
import com.github.zafarkhaja.semver.expr.Expression; import com.github.zafarkhaja.semver.expr.Expression;
import com.github.zafarkhaja.semver.expr.ExpressionParser; 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; import java.util.Comparator;
/** /**
@ -310,6 +312,9 @@ public class Version implements Comparable<Version> {
* @param expr the SemVer Expression * @param expr the SemVer Expression
* @return {@code true} if this version satisfies the specified * @return {@code true} if this version satisfies the specified
* SemVer Expression or {@code false} otherwise * 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 * @since 0.7.0
*/ */
public boolean satisfies(String expr) { public boolean satisfies(String expr) {

View File

@ -78,12 +78,14 @@ public class ExpressionParser implements Parser<Expression> {
* @param input a string representing the SemVer Expression * @param input a string representing the SemVer Expression
* @return the AST for the SemVer Expressions * @return the AST for the SemVer Expressions
* @throws LexerException when encounters an illegal character * @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 @Override
public Expression parse(String input) { public Expression parse(String input) {
tokens = lexer.tokenize(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; Expression expr;
if (tokens.positiveLookahead(NOT)) { if (tokens.positiveLookahead(NOT)) {
tokens.consume(); tokens.consume();
tokens.consume(LEFT_PAREN); consumeNextToken(LEFT_PAREN);
expr = new Not(parseSemVerExpression()); expr = new Not(parseSemVerExpression());
tokens.consume(RIGHT_PAREN); consumeNextToken(RIGHT_PAREN);
} else if (tokens.positiveLookahead(LEFT_PAREN)) { } else if (tokens.positiveLookahead(LEFT_PAREN)) {
tokens.consume(LEFT_PAREN); consumeNextToken(LEFT_PAREN);
expr = parseSemVerExpression(); expr = parseSemVerExpression();
tokens.consume(RIGHT_PAREN); consumeNextToken(RIGHT_PAREN);
} else { } else {
expr = parseExpression(); expr = parseExpression();
} }
@ -224,21 +226,21 @@ public class ExpressionParser implements Parser<Expression> {
* @return the expression AST * @return the expression AST
*/ */
private Expression parseTildeExpression() { private Expression parseTildeExpression() {
tokens.consume(TILDE); consumeNextToken(TILDE);
int major = intOf(tokens.consume(NUMERIC).lexeme); int major = intOf(consumeNextToken(NUMERIC).lexeme);
if (!tokens.positiveLookahead(DOT)) { if (!tokens.positiveLookahead(DOT)) {
return new GreaterOrEqual(versionOf(major, 0, 0)); return new GreaterOrEqual(versionOf(major, 0, 0));
} }
tokens.consume(DOT); consumeNextToken(DOT);
int minor = intOf(tokens.consume(NUMERIC).lexeme); int minor = intOf(consumeNextToken(NUMERIC).lexeme);
if (!tokens.positiveLookahead(DOT)) { if (!tokens.positiveLookahead(DOT)) {
return new And( return new And(
new GreaterOrEqual(versionOf(major, minor, 0)), new GreaterOrEqual(versionOf(major, minor, 0)),
new Less(versionOf(major + 1, 0, 0)) new Less(versionOf(major + 1, 0, 0))
); );
} }
tokens.consume(DOT); consumeNextToken(DOT);
int patch = intOf(tokens.consume(NUMERIC).lexeme); int patch = intOf(consumeNextToken(NUMERIC).lexeme);
return new And( return new And(
new GreaterOrEqual(versionOf(major, minor, patch)), new GreaterOrEqual(versionOf(major, minor, patch)),
new Less(versionOf(major, minor + 1, 0)) new Less(versionOf(major, minor + 1, 0))
@ -270,8 +272,8 @@ public class ExpressionParser implements Parser<Expression> {
* @return the expression AST * @return the expression AST
*/ */
private Expression parseVersionExpression() { private Expression parseVersionExpression() {
int major = intOf(tokens.consume(NUMERIC).lexeme); int major = intOf(consumeNextToken(NUMERIC).lexeme);
tokens.consume(DOT); consumeNextToken(DOT);
if (tokens.positiveLookahead(STAR)) { if (tokens.positiveLookahead(STAR)) {
tokens.consume(); tokens.consume();
return new And( return new And(
@ -279,9 +281,9 @@ public class ExpressionParser implements Parser<Expression> {
new Less(versionOf(major + 1, 0, 0)) new Less(versionOf(major + 1, 0, 0))
); );
} }
int minor = intOf(tokens.consume(NUMERIC).lexeme); int minor = intOf(consumeNextToken(NUMERIC).lexeme);
tokens.consume(DOT); consumeNextToken(DOT);
tokens.consume(STAR); consumeNextToken(STAR);
return new And( return new And(
new GreaterOrEqual(versionOf(major, minor, 0)), new GreaterOrEqual(versionOf(major, minor, 0)),
new Less(versionOf(major, minor + 1, 0)) new Less(versionOf(major, minor + 1, 0))
@ -313,7 +315,7 @@ public class ExpressionParser implements Parser<Expression> {
*/ */
private Expression parseRangeExpression() { private Expression parseRangeExpression() {
Expression ge = new GreaterOrEqual(parseVersion()); Expression ge = new GreaterOrEqual(parseVersion());
tokens.consume(HYPHEN); consumeNextToken(HYPHEN);
Expression le = new LessOrEqual(parseVersion()); Expression le = new LessOrEqual(parseVersion());
return new And(ge, le); return new And(ge, le);
} }
@ -332,16 +334,16 @@ public class ExpressionParser implements Parser<Expression> {
* @return the parsed version * @return the parsed version
*/ */
private Version parseVersion() { private Version parseVersion() {
int major = intOf(tokens.consume(NUMERIC).lexeme); int major = intOf(consumeNextToken(NUMERIC).lexeme);
int minor = 0; int minor = 0;
if (tokens.positiveLookahead(DOT)) { if (tokens.positiveLookahead(DOT)) {
tokens.consume(); tokens.consume();
minor = intOf(tokens.consume(NUMERIC).lexeme); minor = intOf(consumeNextToken(NUMERIC).lexeme);
} }
int patch = 0; int patch = 0;
if (tokens.positiveLookahead(DOT)) { if (tokens.positiveLookahead(DOT)) {
tokens.consume(); tokens.consume();
patch = intOf(tokens.consume(NUMERIC).lexeme); patch = intOf(consumeNextToken(NUMERIC).lexeme);
} }
return versionOf(major, minor, patch); return versionOf(major, minor, patch);
} }
@ -391,4 +393,19 @@ public class ExpressionParser implements Parser<Expression> {
private int intOf(String value) { private int intOf(String value) {
return Integer.parseInt(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);
}
}
} }

View File

@ -64,15 +64,7 @@ class Lexer {
LEFT_PAREN("\\("), LEFT_PAREN("\\("),
RIGHT_PAREN("\\)"), RIGHT_PAREN("\\)"),
WHITESPACE("\\s+"), WHITESPACE("\\s+"),
EOL("?!") { EOL("?!");
/**
* {@inheritDoc}
*/
@Override
public boolean isMatchedBy(Token token) {
return token == null;
}
};
/** /**
* A pattern matching this type. * A pattern matching this type.
@ -123,14 +115,22 @@ class Lexer {
final String lexeme; 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 type the type of this token
* @param lexeme the lexeme 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.type = type;
this.lexeme = (lexeme == null) ? "" : lexeme; this.lexeme = (lexeme == null) ? "" : lexeme;
this.position = position;
} }
/** /**
@ -145,7 +145,10 @@ class Lexer {
return false; return false;
} }
Token token = (Token) other; 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; int hash = 5;
hash = 71 * hash + type.hashCode(); hash = 71 * hash + type.hashCode();
hash = 71 * hash + lexeme.hashCode(); hash = 71 * hash + lexeme.hashCode();
hash = 71 * hash + position;
return hash; return hash;
} }
@ -166,7 +170,11 @@ class Lexer {
*/ */
@Override @Override
public String toString() { 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) { Stream<Token> tokenize(String input) {
List<Token> tokens = new ArrayList<Token>(); List<Token> tokens = new ArrayList<Token>();
int tokenPos = 0;
while (!input.isEmpty()) { while (!input.isEmpty()) {
boolean matched = false; boolean matched = false;
for (Token.Type tokenType : Token.Type.values()) { for (Token.Type tokenType : Token.Type.values()) {
@ -194,8 +203,13 @@ class Lexer {
matched = true; matched = true;
input = matcher.replaceFirst(""); input = matcher.replaceFirst("");
if (tokenType != Token.Type.WHITESPACE) { if (tokenType != Token.Type.WHITESPACE) {
tokens.add(new Token(tokenType, matcher.group())); tokens.add(new Token(
tokenType,
matcher.group(),
tokenPos
));
} }
tokenPos += matcher.end();
break; break;
} }
} }
@ -203,6 +217,7 @@ class Lexer {
throw new LexerException(input); throw new LexerException(input);
} }
} }
tokens.add(new Token(Token.Type.EOL, null, tokenPos));
return new Stream<Token>(tokens.toArray(new Token[tokens.size()])); return new Stream<Token>(tokens.toArray(new Token[tokens.size()]));
} }
} }

View File

@ -24,7 +24,8 @@
package com.github.zafarkhaja.semver.expr; package com.github.zafarkhaja.semver.expr;
import com.github.zafarkhaja.semver.ParseException; 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; import java.util.Arrays;
/** /**
@ -45,6 +46,17 @@ public class UnexpectedTokenException extends ParseException {
*/ */
private final Token.Type[] expected; 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 * Constructs a {@code UnexpectedTokenException} instance
* with the unexpected token and the expected types. * with the unexpected token and the expected types.
@ -57,6 +69,24 @@ public class UnexpectedTokenException extends ParseException {
this.expected = expected; 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 * Returns the string representation of this exception
* containing the information about the unexpected * containing the information about the unexpected

View File

@ -24,7 +24,6 @@
package com.github.zafarkhaja.semver.expr; package com.github.zafarkhaja.semver.expr;
import com.github.zafarkhaja.semver.Version; import com.github.zafarkhaja.semver.Version;
import com.github.zafarkhaja.semver.util.UnexpectedElementException;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -197,7 +196,7 @@ public class ExpressionParserTest {
ExpressionParser parser = new ExpressionParser(new Lexer()); ExpressionParser parser = new ExpressionParser(new Lexer());
try { try {
parser.parse("((>=1.0.1 & < 2)"); parser.parse("((>=1.0.1 & < 2)");
} catch (UnexpectedElementException e) { } catch (UnexpectedTokenException e) {
return; return;
} }
fail("Should raise error if closing parenthesis is missing"); fail("Should raise error if closing parenthesis is missing");

View File

@ -38,12 +38,13 @@ public class LexerTest {
@Test @Test
public void shouldTokenizeVersionString() { public void shouldTokenizeVersionString() {
Token[] expected = { Token[] expected = {
new Token(GREATER, ">"), new Token(GREATER, ">", 0),
new Token(NUMERIC, "1"), new Token(NUMERIC, "1", 1),
new Token(DOT, "."), new Token(DOT, ".", 2),
new Token(NUMERIC, "0"), new Token(NUMERIC, "0", 3),
new Token(DOT, "."), new Token(DOT, ".", 4),
new Token(NUMERIC, "0"), new Token(NUMERIC, "0", 5),
new Token(EOL, null, 6),
}; };
Lexer lexer = new Lexer(); Lexer lexer = new Lexer();
Stream<Token> stream = lexer.tokenize(">1.0.0"); Stream<Token> stream = lexer.tokenize(">1.0.0");
@ -53,14 +54,30 @@ public class LexerTest {
@Test @Test
public void shouldSkipWhitespaces() { public void shouldSkipWhitespaces() {
Token[] expected = { Token[] expected = {
new Token(GREATER, ">"), new Token(GREATER, ">", 0),
new Token(NUMERIC, "1"), new Token(NUMERIC, "1", 2),
new Token(EOL, null, 3),
}; };
Lexer lexer = new Lexer(); Lexer lexer = new Lexer();
Stream<Token> stream = lexer.tokenize("> 1"); Stream<Token> stream = lexer.tokenize("> 1");
assertArrayEquals(expected, stream.toArray()); 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<Token> stream = lexer.tokenize("1.2.3");
assertArrayEquals(expected, stream.toArray());
}
@Test @Test
public void shouldRaiseErrorOnIllegalCharacter() { public void shouldRaiseErrorOnIllegalCharacter() {
Lexer lexer = new Lexer(); Lexer lexer = new Lexer();

View File

@ -41,23 +41,23 @@ public class LexerTokenTest {
@Test @Test
public void shouldBeReflexive() { public void shouldBeReflexive() {
Token token = new Token(NUMERIC, "1"); Token token = new Token(NUMERIC, "1", 0);
assertTrue(token.equals(token)); assertTrue(token.equals(token));
} }
@Test @Test
public void shouldBeSymmetric() { public void shouldBeSymmetric() {
Token t1 = new Token(EQUAL, "="); Token t1 = new Token(EQUAL, "=", 0);
Token t2 = new Token(EQUAL, "="); Token t2 = new Token(EQUAL, "=", 0);
assertTrue(t1.equals(t2)); assertTrue(t1.equals(t2));
assertTrue(t2.equals(t1)); assertTrue(t2.equals(t1));
} }
@Test @Test
public void shouldBeTransitive() { public void shouldBeTransitive() {
Token t1 = new Token(GREATER, ">"); Token t1 = new Token(GREATER, ">", 0);
Token t2 = new Token(GREATER, ">"); Token t2 = new Token(GREATER, ">", 0);
Token t3 = new Token(GREATER, ">"); Token t3 = new Token(GREATER, ">", 0);
assertTrue(t1.equals(t2)); assertTrue(t1.equals(t2));
assertTrue(t2.equals(t3)); assertTrue(t2.equals(t3));
assertTrue(t1.equals(t3)); assertTrue(t1.equals(t3));
@ -65,8 +65,8 @@ public class LexerTokenTest {
@Test @Test
public void shouldBeConsistent() { public void shouldBeConsistent() {
Token t1 = new Token(HYPHEN, "-"); Token t1 = new Token(HYPHEN, "-", 0);
Token t2 = new Token(HYPHEN, "-"); Token t2 = new Token(HYPHEN, "-", 0);
assertTrue(t1.equals(t2)); assertTrue(t1.equals(t2));
assertTrue(t1.equals(t2)); assertTrue(t1.equals(t2));
assertTrue(t1.equals(t2)); assertTrue(t1.equals(t2));
@ -74,28 +74,35 @@ public class LexerTokenTest {
@Test @Test
public void shouldReturnFalseIfOtherVersionIsOfDifferentType() { public void shouldReturnFalseIfOtherVersionIsOfDifferentType() {
Token t1 = new Token(DOT, "."); Token t1 = new Token(DOT, ".", 0);
assertFalse(t1.equals(new String("."))); assertFalse(t1.equals(new String(".")));
} }
@Test @Test
public void shouldReturnFalseIfOtherVersionIsNull() { public void shouldReturnFalseIfOtherVersionIsNull() {
Token t1 = new Token(AND, "&"); Token t1 = new Token(AND, "&", 0);
Token t2 = null; Token t2 = null;
assertFalse(t1.equals(t2)); assertFalse(t1.equals(t2));
} }
@Test @Test
public void shouldReturnFalseIfTypesAreDifferent() { public void shouldReturnFalseIfTypesAreDifferent() {
Token t1 = new Token(EQUAL, "="); Token t1 = new Token(EQUAL, "=", 0);
Token t2 = new Token(NOT_EQUAL, "!="); Token t2 = new Token(NOT_EQUAL, "!=", 0);
assertFalse(t1.equals(t2)); assertFalse(t1.equals(t2));
} }
@Test @Test
public void shouldReturnFalseIfLexemesAreDifferent() { public void shouldReturnFalseIfLexemesAreDifferent() {
Token t1 = new Token(NUMERIC, "1"); Token t1 = new Token(NUMERIC, "1", 0);
Token t2 = new Token(NUMERIC, "2"); 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)); assertFalse(t1.equals(t2));
} }
} }
@ -104,8 +111,8 @@ public class LexerTokenTest {
@Test @Test
public void shouldReturnSameHashCodeIfTokensAreEqual() { public void shouldReturnSameHashCodeIfTokensAreEqual() {
Token t1 = new Token(NUMERIC, "1"); Token t1 = new Token(NUMERIC, "1", 0);
Token t2 = new Token(NUMERIC, "1"); Token t2 = new Token(NUMERIC, "1", 0);
assertTrue(t1.equals(t2)); assertTrue(t1.equals(t2));
assertEquals(t1.hashCode(), t2.hashCode()); assertEquals(t1.hashCode(), t2.hashCode());
} }

View File

@ -0,0 +1,80 @@
/*
* The MIT License
*
* Copyright 2014 Zafar Khaja <zafarkhaja@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@gmail.com>
*/
@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 } },
});
}
}