diff --git a/java/.mvn/jvm.config b/java/.mvn/jvm.config new file mode 100644 index 00000000..32599cef --- /dev/null +++ b/java/.mvn/jvm.config @@ -0,0 +1,10 @@ +--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED diff --git a/java/pom.xml b/java/pom.xml index 1a50722f..06ce0f8f 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -1,15 +1,16 @@ - + 4.0.0 io.cucumber cucumber-parent - 4.5.0 + 5.0.0-SNAPSHOT tag-expressions - 8.0.1-SNAPSHOT + 9.0.0-SNAPSHOT jar Cucumber Tag Expressions Parses boolean infix expressions @@ -27,7 +28,6 @@ HEAD - @@ -37,14 +37,27 @@ pom import + + + org.assertj + assertj-bom + 3.27.6 + pom + import + - org.hamcrest - hamcrest - 3.0 + org.jspecify + jspecify + 1.0.0 + + + + org.assertj + assertj-core test @@ -60,4 +73,21 @@ + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + validate + validate + + check + + + + + + diff --git a/java/src/main/java/io/cucumber/tagexpressions/TagExpressionException.java b/java/src/main/java/io/cucumber/tagexpressions/TagExpressionException.java index e3d3266f..d9ebb334 100644 --- a/java/src/main/java/io/cucumber/tagexpressions/TagExpressionException.java +++ b/java/src/main/java/io/cucumber/tagexpressions/TagExpressionException.java @@ -1,7 +1,7 @@ package io.cucumber.tagexpressions; public final class TagExpressionException extends RuntimeException { - TagExpressionException(String message, Object... args) { - super(String.format(message, args)); + TagExpressionException(String message) { + super(message); } } diff --git a/java/src/main/java/io/cucumber/tagexpressions/TagExpressionParser.java b/java/src/main/java/io/cucumber/tagexpressions/TagExpressionParser.java index e71d74f3..762733ac 100644 --- a/java/src/main/java/io/cucumber/tagexpressions/TagExpressionParser.java +++ b/java/src/main/java/io/cucumber/tagexpressions/TagExpressionParser.java @@ -3,25 +3,33 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static java.util.Objects.requireNonNull; + public final class TagExpressionParser { - private static final Map ASSOC = new HashMap() {{ - put("or", Assoc.LEFT); - put("and", Assoc.LEFT); - put("not", Assoc.RIGHT); - }}; - private static final Map PREC = new HashMap() {{ - put("(", -2); - put(")", -1); - put("or", 0); - put("and", 1); - put("not", 2); - }}; + private static boolean assoc(String token, Assoc assoc) { + return switch (token) { + case "or", "and" -> assoc == Assoc.LEFT; + case "not" -> assoc == Assoc.RIGHT; + default -> throw new IllegalArgumentException(token); + }; + } + + private static int prec(String token) { + return switch (token) { + case "(" -> -2; + case ")" -> -1; + case "or" -> 0; + case "and" -> 1; + case "not" -> 2; + default -> throw new IllegalArgumentException(token); + }; + } + private static final char ESCAPING_CHAR = '\\'; private final String infix; @@ -30,7 +38,7 @@ public static Expression parse(String infix) { } private TagExpressionParser(String infix) { - this.infix = infix; + this.infix = requireNonNull(infix); } private Expression parse() { @@ -47,10 +55,7 @@ private Expression parse() { expectedTokenType = TokenType.OPERAND; } else if (isBinary(token)) { check(expectedTokenType, TokenType.OPERATOR); - while (operators.size() > 0 && isOperator(operators.peek()) && ( - (ASSOC.get(token) == Assoc.LEFT && PREC.get(token) <= PREC.get(operators.peek())) - || - (ASSOC.get(token) == Assoc.RIGHT && PREC.get(token) < PREC.get(operators.peek()))) + while (!operators.isEmpty() && isTokenForOperator(token, operators.peek()) ) { pushExpr(pop(operators), expressions); } @@ -62,11 +67,11 @@ private Expression parse() { expectedTokenType = TokenType.OPERAND; } else if (")".equals(token)) { check(expectedTokenType, TokenType.OPERATOR); - while (operators.size() > 0 && !"(".equals(operators.peek())) { + while (!operators.isEmpty() && !"(".equals(operators.peek())) { pushExpr(pop(operators), expressions); } - if (operators.size() == 0) { - throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Unmatched ).", this.infix); + if (operators.isEmpty()) { + throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Unmatched ).".formatted(this.infix)); } if ("(".equals(operators.peek())) { pop(operators); @@ -79,9 +84,9 @@ private Expression parse() { } } - while (operators.size() > 0) { + while (!operators.isEmpty()) { if ("(".equals(operators.peek())) { - throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Unmatched (.", infix); + throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Unmatched (.".formatted(infix)); } pushExpr(pop(operators), expressions); } @@ -89,6 +94,15 @@ private Expression parse() { return expressions.pop(); } + @SuppressWarnings("UnnecessaryParentheses") + private boolean isTokenForOperator(String token, String operator) { + if (!isOperator(operator)) { + return false; + } + return (assoc(token, Assoc.LEFT) && prec(token) <= prec(operator)) || + (assoc(token, Assoc.RIGHT) && prec(token) < prec(operator)); + } + private static List tokenize(String expr) { List tokens = new ArrayList<>(); boolean isEscaped = false; @@ -100,12 +114,12 @@ private static List tokenize(String expr) { token.append(c); isEscaped = false; } else { - throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Illegal escape before \"%s\".", expr, c); + throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Illegal escape before \"%s\".".formatted(expr, c)); } } else if (c == ESCAPING_CHAR) { isEscaped = true; } else if (c == '(' || c == ')' || Character.isWhitespace(c)) { - if (token.length() > 0) { + if (!token.isEmpty()) { tokens.add(token.toString()); token = new StringBuilder(); } @@ -116,7 +130,7 @@ private static List tokenize(String expr) { token.append(c); } } - if (token.length() > 0) { + if (!token.isEmpty()) { tokens.add(token.toString()); } return tokens; @@ -124,33 +138,32 @@ private static List tokenize(String expr) { private void check(TokenType expectedTokenType, TokenType tokenType) { if (expectedTokenType != tokenType) { - throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Expected %s.", infix, expectedTokenType.toString().toLowerCase()); + throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Expected %s.".formatted(infix, expectedTokenType.toString().toLowerCase(Locale.US))); } } private T pop(Deque stack) { if (stack.isEmpty()) - throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of an empty stack", infix); + throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of an empty stack".formatted(infix)); return stack.pop(); } private void pushExpr(String token, Deque stack) { - switch (token) { - case "and": + Expression expression = switch (token) { + case "and" -> { Expression rightAndExpr = pop(stack); - stack.push(new And(pop(stack), rightAndExpr)); - break; - case "or": + Expression leftAndExpr = pop(stack); + yield new And(leftAndExpr, rightAndExpr); + } + case "or" -> { Expression rightOrExpr = pop(stack); - stack.push(new Or(pop(stack), rightOrExpr)); - break; - case "not": - stack.push(new Not(pop(stack))); - break; - default: - stack.push(new Literal(token)); - break; - } + Expression leftOrExpr = pop(stack); + yield new Or(leftOrExpr, rightOrExpr); + } + case "not" -> new Not(pop(stack)); + default -> new Literal(token); + }; + stack.push(expression); } private boolean isUnary(String token) { @@ -162,7 +175,7 @@ private boolean isBinary(String token) { } private boolean isOperator(String token) { - return ASSOC.get(token) != null; + return isBinary(token) || isUnary(token); } private enum TokenType { @@ -213,7 +226,7 @@ public boolean evaluate(List variables) { @Override public String toString() { - return "( " + left.toString() + " or " + right.toString() + " )"; + return "( " + left + " or " + right + " )"; } } @@ -233,7 +246,7 @@ public boolean evaluate(List variables) { @Override public String toString() { - return "( " + left.toString() + " and " + right.toString() + " )"; + return "( " + left + " and " + right + " )"; } } @@ -251,15 +264,15 @@ public boolean evaluate(List variables) { @Override public String toString() { - if (And.class.isInstance(expr) || Or.class.isInstance(expr)) { + if (expr instanceof And || expr instanceof Or) { // -- HINT: Binary Operators already have already ' ( ... ) '. - return "not " + expr.toString(); + return "not " + expr; } - return "not ( " + expr.toString() + " )"; + return "not ( " + expr + " )"; } } - private static class True implements Expression { + private static final class True implements Expression { @Override public boolean evaluate(List variables) { return true; diff --git a/java/src/main/java/io/cucumber/tagexpressions/package-info.java b/java/src/main/java/io/cucumber/tagexpressions/package-info.java new file mode 100644 index 00000000..811dedc3 --- /dev/null +++ b/java/src/main/java/io/cucumber/tagexpressions/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package io.cucumber.tagexpressions; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/java/src/test/java/io/cucumber/tagexpressions/ErrorsTest.java b/java/src/test/java/io/cucumber/tagexpressions/ErrorsTest.java index f641d431..0d8595e3 100644 --- a/java/src/test/java/io/cucumber/tagexpressions/ErrorsTest.java +++ b/java/src/test/java/io/cucumber/tagexpressions/ErrorsTest.java @@ -10,20 +10,22 @@ import java.util.Map; import static java.nio.file.Files.newInputStream; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class ErrorsTest { - private static List> acceptance_tests_pass() throws IOException { + static List> acceptance_tests_pass() throws IOException { return new Yaml().loadAs(newInputStream(Paths.get("..", "testdata", "errors.yml")), List.class); } @ParameterizedTest @MethodSource void acceptance_tests_pass(Map expectation) { + String expression = requireNonNull(expectation.get("expression")); TagExpressionException e = assertThrows(TagExpressionException.class, - () -> TagExpressionParser.parse(expectation.get("expression"))); + () -> TagExpressionParser.parse(expression)); assertEquals(expectation.get("error"), e.getMessage()); } diff --git a/java/src/test/java/io/cucumber/tagexpressions/EvaluationsTest.java b/java/src/test/java/io/cucumber/tagexpressions/EvaluationsTest.java index 4c69b740..38e19ae6 100644 --- a/java/src/test/java/io/cucumber/tagexpressions/EvaluationsTest.java +++ b/java/src/test/java/io/cucumber/tagexpressions/EvaluationsTest.java @@ -11,29 +11,20 @@ import java.util.stream.Collectors; import static java.nio.file.Files.newInputStream; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; class EvaluationsTest { - static class Expectation { - final String expression; - final List variables; - final boolean result; - public Expectation(String expression, List variables, boolean result) { - this.expression = expression; - this.variables = variables; - this.result = result; - } - } - - private static List acceptance_tests_pass() throws IOException { - List> evaluations = new Yaml().loadAs(newInputStream(Paths.get("..", "testdata", "evaluations.yml")), List.class); + @SuppressWarnings("unchecked") + static List acceptance_tests_pass() throws IOException { + List> evaluations = new Yaml().loadAs(newInputStream(Paths.get("..", "testdata", "evaluations.yml")), List.class); return evaluations.stream().flatMap(map -> { - String expression = (String) map.get("expression"); - List> tests = (List>) map.get("tests"); + String expression = (String) requireNonNull(map.get("expression")); + List> tests = (List>) requireNonNull(map.get("tests")); return tests.stream().map(test -> { - List variables = (List) test.get("variables"); - boolean result = (boolean) test.get("result"); + List variables = (List) requireNonNull(test.get("variables")); + boolean result = (boolean) requireNonNull(test.get("result")); return new Expectation(expression, variables, result); }); }).collect(Collectors.toList()); @@ -46,4 +37,16 @@ void acceptance_tests_pass(Expectation expectation) { expr.evaluate(expectation.variables); assertEquals(expectation.result, expr.evaluate(expectation.variables)); } + + static class Expectation { + final String expression; + final List variables; + final boolean result; + + Expectation(String expression, List variables, boolean result) { + this.expression = expression; + this.variables = variables; + this.result = result; + } + } } diff --git a/java/src/test/java/io/cucumber/tagexpressions/ParsingTest.java b/java/src/test/java/io/cucumber/tagexpressions/ParsingTest.java index ffa08964..90fb5d37 100644 --- a/java/src/test/java/io/cucumber/tagexpressions/ParsingTest.java +++ b/java/src/test/java/io/cucumber/tagexpressions/ParsingTest.java @@ -10,19 +10,21 @@ import java.util.Map; import static java.nio.file.Files.newInputStream; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; class ParsingTest { - private static List> acceptance_tests_pass() throws IOException { + static List> acceptance_tests_pass() throws IOException { return new Yaml().loadAs(newInputStream(Paths.get("..", "testdata", "parsing.yml")), List.class); } @ParameterizedTest @MethodSource void acceptance_tests_pass(Map expectation) { - Expression expr = TagExpressionParser.parse(expectation.get("expression")); - String formatted = expectation.get("formatted"); + String expression = requireNonNull(expectation.get("expression")); + Expression expr = TagExpressionParser.parse(expression); + String formatted = requireNonNull(expectation.get("formatted")); assertEquals(formatted, expr.toString()); Expression expr2 = TagExpressionParser.parse(formatted);