diff --git a/CHANGELOG.md b/CHANGELOG.md index c62228b44..2bef8a2d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +- [Java] Make CucumberExpressionParser.parse public ([#340](https://github.com/cucumber/cucumber-expressions/pull/340)) ### Changed - [Ruby] Minor cosmetic / CI changes for development (Nothing front-facing) - [Python] PEP 639 licence metadata specification ([#361](https://github.com/cucumber/cucumber-expressions/pull/361)) diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java b/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java index 50c0fef67..6ba5567ce 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java @@ -1,22 +1,12 @@ package io.cucumber.cucumberexpressions; -import java.util.List; import java.util.Objects; import java.util.StringJoiner; -import static java.util.Arrays.asList; import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.joining; final class Ast { - private static final char escapeCharacter = '\\'; - private static final char alternationCharacter = '/'; - private static final char beginParameterCharacter = '{'; - private static final char endParameterCharacter = '}'; - private static final char beginOptionalCharacter = '('; - private static final char endOptionalCharacter = ')'; - interface Located { int start(); @@ -24,133 +14,21 @@ interface Located { } - static final class Node implements Located { - - private final Type type; - private final List nodes; - private final String token; - private final int start; - private final int end; - - Node(Type type, int start, int end, String token) { - this(type, start, end, null, token); - } - - Node(Type type, int start, int end, List nodes) { - this(type, start, end, nodes, null); - } - - private Node(Type type, int start, int end, List nodes, String token) { - this.type = requireNonNull(type); - this.nodes = nodes; - this.token = token; - this.start = start; - this.end = end; - } - - enum Type { - TEXT_NODE, - OPTIONAL_NODE, - ALTERNATION_NODE, - ALTERNATIVE_NODE, - PARAMETER_NODE, - EXPRESSION_NODE - } - - public int start() { - return start; - } - - public int end() { - return end; - } - - List nodes() { - return nodes; - } - - Type type() { - return type; - } - - String text() { - if (nodes == null) - return token; - - return nodes().stream() - .map(Node::text) - .collect(joining()); - } - - @Override - public String toString() { - return toString(0).toString(); - } - - private StringBuilder toString(int depth) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < depth; i++) { - sb.append(" "); - } - sb.append("{") - .append("\"type\": \"").append(type) - .append("\", \"start\": ") - .append(start) - .append(", \"end\": ") - .append(end); - - if (token != null) { - sb.append(", \"token\": \"").append(token.replaceAll("\\\\", "\\\\\\\\")).append("\""); - } - - if (nodes != null) { - sb.append(", \"nodes\": "); - if (!nodes.isEmpty()) { - StringBuilder padding = new StringBuilder(); - for (int i = 0; i < depth; i++) { - padding.append(" "); - } - sb.append(nodes.stream() - .map(node -> node.toString(depth + 1)) - .collect(joining(",\n", "[\n", "\n" +padding + "]"))); - - } else { - sb.append("[]"); - } - } - sb.append("}"); - return sb; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - Node node = (Node) o; - return start == node.start && - end == node.end && - type == node.type && - Objects.equals(nodes, node.nodes) && - Objects.equals(token, node.token); - } - - @Override - public int hashCode() { - return Objects.hash(type, nodes, token, start, end); - } - - } - static final class Token implements Located { + private static final char escapeCharacter = '\\'; + private static final char alternationCharacter = '/'; + private static final char beginParameterCharacter = '{'; + private static final char endParameterCharacter = '}'; + private static final char beginOptionalCharacter = '('; + private static final char endOptionalCharacter = ')'; + final String text; - final Token.Type type; + final Type type; final int start; final int end; - Token(String text, Token.Type type, int start, int end) { + Token(String text, Type type, int start, int end) { this.text = requireNonNull(text); this.type = requireNonNull(type); this.start = start; @@ -224,10 +102,10 @@ public int hashCode() { @Override public String toString() { - return new StringJoiner(", ", "" + "{", "}") + return new StringJoiner(", ", "{", "}") .add("\"type\": \"" + type + "\"") - .add("\"start\": " + start + "") - .add("\"end\": " + end + "") + .add("\"start\": " + start) + .add("\"end\": " + end) .add("\"text\": \"" + text + "\"") .toString(); } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java index c0ea66155..b683e23ca 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java @@ -1,24 +1,21 @@ package io.cucumber.cucumberexpressions; -import io.cucumber.cucumberexpressions.Ast.Node; +import io.cucumber.cucumberexpressions.Node.Type; import org.apiguardian.api.API; -import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.function.Function; import java.util.regex.Pattern; -import static io.cucumber.cucumberexpressions.Ast.Node.Type.OPTIONAL_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.Type.PARAMETER_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.Type.TEXT_NODE; +import static io.cucumber.cucumberexpressions.Node.Type.OPTIONAL_NODE; +import static io.cucumber.cucumberexpressions.Node.Type.PARAMETER_NODE; +import static io.cucumber.cucumberexpressions.Node.Type.TEXT_NODE; import static io.cucumber.cucumberexpressions.CucumberExpressionException.createAlternativeMayNotBeEmpty; import static io.cucumber.cucumberexpressions.CucumberExpressionException.createAlternativeMayNotExclusivelyContainOptionals; -import static io.cucumber.cucumberexpressions.CucumberExpressionException.createInvalidParameterTypeName; import static io.cucumber.cucumberexpressions.CucumberExpressionException.createOptionalIsNotAllowedInOptional; import static io.cucumber.cucumberexpressions.CucumberExpressionException.createOptionalMayNotBeEmpty; import static io.cucumber.cucumberexpressions.CucumberExpressionException.createParameterIsNotAllowedInOptional; -import static io.cucumber.cucumberexpressions.ParameterType.isValidParameterTypeName; import static io.cucumber.cucumberexpressions.RegexpUtils.escapeRegex; import static io.cucumber.cucumberexpressions.UndefinedParameterTypeException.createUndefinedParameterType; import static java.util.stream.Collectors.joining; @@ -130,8 +127,8 @@ private void assertNoOptionals(Node node, assertNoNodeOfType(OPTIONAL_NODE, node, createNodeContainedAnOptionalException); } - private void assertNoNodeOfType(Node.Type nodeType, Node node, - Function createException) { + private void assertNoNodeOfType(Type nodeType, Node node, + Function createException) { node.nodes() .stream() .filter(astNode -> nodeType.equals(astNode.type())) @@ -144,7 +141,7 @@ private void assertNoNodeOfType(Node.Type nodeType, Node node, @Override - public List> match(String text, Type... typeHints) { + public List> match(String text, java.lang.reflect.Type... typeHints) { final Group group = treeRegexp.match(text); if (group == null) { return null; @@ -153,7 +150,7 @@ public List> match(String text, Type... typeHints) { List> parameterTypes = new ArrayList<>(this.parameterTypes); for (int i = 0; i < parameterTypes.size(); i++) { ParameterType parameterType = parameterTypes.get(i); - Type type = i < typeHints.length ? typeHints[i] : String.class; + java.lang.reflect.Type type = i < typeHints.length ? typeHints[i] : String.class; if (parameterType.isAnonymous()) { ParameterByTypeTransformer defaultTransformer = parameterTypeRegistry.getDefaultParameterTransformer(); parameterTypes.set(i, parameterType.deAnonymize(type, arg -> defaultTransformer.transform(arg, type))); diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java index 0dd23f1cd..0d0496d47 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java @@ -1,7 +1,6 @@ package io.cucumber.cucumberexpressions; import io.cucumber.cucumberexpressions.Ast.Located; -import io.cucumber.cucumberexpressions.Ast.Node; import io.cucumber.cucumberexpressions.Ast.Token; import io.cucumber.cucumberexpressions.Ast.Token.Type; import org.apiguardian.api.API; @@ -18,7 +17,7 @@ public class CucumberExpressionException extends RuntimeException { } static CucumberExpressionException createMissingEndToken(String expression, Type beginToken, Type endToken, - Token current) { + Token current) { return new CucumberExpressionException(message( current.start(), expression, diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java index b77be40e9..9701c6cb6 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java @@ -1,19 +1,19 @@ package io.cucumber.cucumberexpressions; -import io.cucumber.cucumberexpressions.Ast.Node; import io.cucumber.cucumberexpressions.Ast.Token; import io.cucumber.cucumberexpressions.Ast.Token.Type; +import org.apiguardian.api.API; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import static io.cucumber.cucumberexpressions.Ast.Node.Type.ALTERNATION_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.Type.ALTERNATIVE_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.Type.EXPRESSION_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.Type.OPTIONAL_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.Type.PARAMETER_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.Type.TEXT_NODE; +import static io.cucumber.cucumberexpressions.Node.Type.ALTERNATION_NODE; +import static io.cucumber.cucumberexpressions.Node.Type.ALTERNATIVE_NODE; +import static io.cucumber.cucumberexpressions.Node.Type.EXPRESSION_NODE; +import static io.cucumber.cucumberexpressions.Node.Type.OPTIONAL_NODE; +import static io.cucumber.cucumberexpressions.Node.Type.PARAMETER_NODE; +import static io.cucumber.cucumberexpressions.Node.Type.TEXT_NODE; import static io.cucumber.cucumberexpressions.Ast.Token.Type.ALTERNATION; import static io.cucumber.cucumberexpressions.Ast.Token.Type.BEGIN_OPTIONAL; import static io.cucumber.cucumberexpressions.Ast.Token.Type.BEGIN_PARAMETER; @@ -27,8 +27,13 @@ import static io.cucumber.cucumberexpressions.CucumberExpressionException.createMissingEndToken; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; -final class CucumberExpressionParser { +/** + * A parser for Cucumber expressions + */ +@API(since = "18.1", status = EXPERIMENTAL) +public final class CucumberExpressionParser { /* * text := whitespace | ')' | '}' | . @@ -160,7 +165,13 @@ final class CucumberExpressionParser { ) ); - Node parse(String expression) { + /** + * Parses as Cucumber expression into an AST of {@link Node nodes}. + * @param expression the expression to parse + * @return an AST of nodes + * @throws CucumberExpressionException if the expression could not be parsed + */ + public Node parse(String expression) { CucumberExpressionTokenizer tokenizer = new CucumberExpressionTokenizer(); List tokens = tokenizer.tokenize(expression); Result result = cucumberExpressionParser.parse(expression, tokens, 0); diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java index 9a2a9df25..b4c12e538 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java @@ -70,7 +70,7 @@ private Type tokenTypeOf(Integer token, boolean treatAsText) { } private boolean shouldContinueTokenType(Type previousTokenType, - Type currentTokenType) { + Type currentTokenType) { return currentTokenType == previousTokenType && (currentTokenType == Type.WHITE_SPACE || currentTokenType == Type.TEXT); } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/GroupBuilder.java b/java/src/main/java/io/cucumber/cucumberexpressions/GroupBuilder.java index f5222f6f6..e0f6336b5 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/GroupBuilder.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/GroupBuilder.java @@ -9,7 +9,7 @@ final class GroupBuilder { private final List groupBuilders = new ArrayList<>(); private boolean capturing = true; private String source; - private int startIndex; + private final int startIndex; private int endIndex; GroupBuilder(int startIndex) { diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Node.java b/java/src/main/java/io/cucumber/cucumberexpressions/Node.java new file mode 100644 index 000000000..a761ff4de --- /dev/null +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Node.java @@ -0,0 +1,140 @@ +package io.cucumber.cucumberexpressions; + +import org.apiguardian.api.API; + +import java.util.List; +import java.util.Objects; + +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +@API(since = "18.1", status = EXPERIMENTAL) +public final class Node implements Ast.Located { + + private final Type type; + private final List nodes; + private final String token; + private final int start; + private final int end; + + Node(Type type, int start, int end, String token) { + this(type, start, end, null, requireNonNull(token)); + } + + Node(Type type, int start, int end, List nodes) { + this(type, start, end, requireNonNull(nodes), null); + } + + private Node(Type type, int start, int end, List nodes, String token) { + this.type = requireNonNull(type); + this.nodes = nodes; + this.token = token; + this.start = start; + this.end = end; + } + + public enum Type { + TEXT_NODE, + OPTIONAL_NODE, + ALTERNATION_NODE, + ALTERNATIVE_NODE, + PARAMETER_NODE, + EXPRESSION_NODE + } + + public int start() { + return start; + } + + public int end() { + return end; + } + + /** + * @return child nodes, {@code null} if a leaf-node + */ + public List nodes() { + return nodes; + } + + public Type type() { + return type; + } + + /** + * @return the text contained with in this node, {@code null} if not a leaf-node + */ + public String token() { + return token; + } + + String text() { + if (nodes == null) + return token; + + return nodes().stream() + .map(Node::text) + .collect(joining()); + } + + @Override + public String toString() { + return toString(0).toString(); + } + + private StringBuilder toString(int depth) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < depth; i++) { + sb.append(" "); + } + sb.append("{") + .append("\"type\": \"").append(type) + .append("\", \"start\": ") + .append(start) + .append(", \"end\": ") + .append(end); + + if (token != null) { + sb.append(", \"token\": \"").append(token.replaceAll("\\\\", "\\\\\\\\")).append("\""); + } + + if (nodes != null) { + sb.append(", \"nodes\": "); + if (!nodes.isEmpty()) { + StringBuilder padding = new StringBuilder(); + for (int i = 0; i < depth; i++) { + padding.append(" "); + } + sb.append(nodes.stream() + .map(node -> node.toString(depth + 1)) + .collect(joining(",\n", "[\n", "\n" + padding + "]"))); + + } else { + sb.append("[]"); + } + } + sb.append("}"); + return sb; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Node node = (Node) o; + return start == node.start && + end == node.end && + type == node.type && + Objects.equals(nodes, node.nodes) && + Objects.equals(token, node.token); + } + + @Override + public int hashCode() { + return Objects.hash(type, nodes, token, start, end); + } + +} diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java b/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java index 53c53f3a4..10d5e3440 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java @@ -48,7 +48,7 @@ public List> match(String text, Type... typeHints) { // When there is a conflict between the type hint from the regular expression and the method // prefer the parameter type associated with the regular expression. This ensures we will - // use the internal/user registered parameter transformer rather then the default. + // use the internal/user registered parameter transformer rather than the default. // // Unless the parameter type indicates it is the stronger type hint. if (parameterType != null && hasTypeHint && !parameterType.useRegexpMatchAsStrongTypeHint()) { diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/TreeRegexp.java b/java/src/main/java/io/cucumber/cucumberexpressions/TreeRegexp.java index 37e2f9830..4b647b5b1 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/TreeRegexp.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/TreeRegexp.java @@ -91,7 +91,7 @@ Group match(CharSequence s) { return groupBuilder.build(matcher, IntStream.rangeClosed(0, matcher.groupCount()).iterator()); } - public GroupBuilder getGroupBuilder() { + GroupBuilder getGroupBuilder() { return groupBuilder; } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/UndefinedParameterTypeException.java b/java/src/main/java/io/cucumber/cucumberexpressions/UndefinedParameterTypeException.java index b2b9efede..f90feec8f 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/UndefinedParameterTypeException.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/UndefinedParameterTypeException.java @@ -1,6 +1,5 @@ package io.cucumber.cucumberexpressions; -import io.cucumber.cucumberexpressions.Ast.Node; import org.apiguardian.api.API; @API(status = API.Status.STABLE) diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java index 3221a59dd..bd95a710b 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java @@ -1,6 +1,6 @@ package io.cucumber.cucumberexpressions; -import io.cucumber.cucumberexpressions.Ast.Node; +import io.cucumber.cucumberexpressions.Node.Type; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.converter.ArgumentConversionException; @@ -71,7 +71,7 @@ public Expectation convert(Object source, ParameterContext context) throws Argum } static class YamlableNode { - public Ast.Node.Type type; + public Type type; public List nodes; public String token; public int start; diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java index d9554df54..d9b8e218b 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java @@ -15,6 +15,7 @@ import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; +import java.nio.file.DirectoryStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -38,7 +39,10 @@ class CucumberExpressionTest { private static List acceptance_tests_pass() throws IOException { List paths = new ArrayList<>(); - newDirectoryStream(Paths.get("..", "testdata", "cucumber-expression", "matching")).forEach(paths::add); + Path path = Paths.get("..", "testdata", "cucumber-expression", "matching"); + try (DirectoryStream directories = newDirectoryStream(path)){ + directories.forEach(paths::add); + } paths.sort(Comparator.naturalOrder()); return paths; } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java index 11c0cef97..c1427f663 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import io.cucumber.cucumberexpressions.Ast.Token; +import io.cucumber.cucumberexpressions.Ast.Token.Type; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.converter.ArgumentConversionException; @@ -76,7 +77,7 @@ public Expectation convert(Object source, ParameterContext context) throws Argum static class YamlableToken { public String text; - public Token.Type type; + public Type type; public int start; public int end;