From 533f70961442d1dee7bc1cdd9200dee2de2e4927 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 15 May 2025 16:10:37 +0200 Subject: [PATCH 1/4] [Java] Make CucumberExpressionParser.parse public Fixes: #51 By doing this we're exposing the public API surface. If #41 ever gets implemented, it may require a change to the AST. We can live with that. --- CHANGELOG.md | 2 + .../io/cucumber/cucumberexpressions/Ast.java | 57 +++++++++++-------- .../CucumberExpressionParser.java | 13 ++++- .../cucumberexpressions/GroupBuilder.java | 2 +- .../RegularExpression.java | 2 +- .../cucumberexpressions/TreeRegexp.java | 2 +- .../CucumberExpressionTest.java | 6 +- 7 files changed, 54 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8963d6f5..0d152f692 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)) ## [18.0.1] - 2024-10-28 ### Fixed diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java b/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java index 50c0fef67..ab2568244 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java @@ -4,18 +4,17 @@ 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 { +public 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 = ')'; + public static final char escapeCharacter = '\\'; + public static final char alternationCharacter = '/'; + public static final char beginParameterCharacter = '{'; + public static final char endParameterCharacter = '}'; + public static final char beginOptionalCharacter = '('; + public static final char endOptionalCharacter = ')'; interface Located { int start(); @@ -24,7 +23,7 @@ interface Located { } - static final class Node implements Located { + public static final class Node implements Located { private final Type type; private final List nodes; @@ -33,11 +32,11 @@ static final class Node implements Located { private final int end; Node(Type type, int start, int end, String token) { - this(type, start, end, null, token); + this(type, start, end, null, requireNonNull(token)); } Node(Type type, int start, int end, List nodes) { - this(type, start, end, nodes, null); + this(type, start, end, requireNonNull(nodes), null); } private Node(Type type, int start, int end, List nodes, String token) { @@ -48,7 +47,7 @@ private Node(Type type, int start, int end, List nodes, String token) { this.end = end; } - enum Type { + public enum Type { TEXT_NODE, OPTIONAL_NODE, ALTERNATION_NODE, @@ -65,14 +64,24 @@ public int end() { return end; } - List nodes() { + /** + * @return child nodes, {@code null} if a leaf-node + */ + public List nodes() { return nodes; } - Type type() { + 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; @@ -106,13 +115,13 @@ private StringBuilder toString(int depth) { 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 + "]"))); + 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("[]"); @@ -224,10 +233,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/CucumberExpressionParser.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java index b77be40e9..1df7ab037 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java @@ -28,7 +28,10 @@ import static java.util.Arrays.asList; import static java.util.Collections.singletonList; -final class CucumberExpressionParser { +/** + * A parser for Cucumber expressions + */ +public final class CucumberExpressionParser { /* * text := whitespace | ')' | '}' | . @@ -160,7 +163,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/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/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/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; } From c1f4ced90603645c35bf07a82c7a3c3d1dc23c36 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Mon, 10 Nov 2025 23:22:00 +0100 Subject: [PATCH 2/4] Mark API as public --- .../io/cucumber/cucumberexpressions/Ast.java | 42 ++++++++++-------- .../CucumberExpression.java | 13 +++--- .../CucumberExpressionException.java | 6 +-- .../CucumberExpressionParser.java | 44 +++++++++---------- .../CucumberExpressionTokenizer.java | 28 ++++++------ .../CucumberExpressionParserTest.java | 3 +- .../CucumberExpressionTokenizerTest.java | 3 +- 7 files changed, 72 insertions(+), 67 deletions(-) diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java b/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java index ab2568244..b3916b1d5 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java @@ -1,12 +1,16 @@ package io.cucumber.cucumberexpressions; +import org.apiguardian.api.API; + import java.util.List; import java.util.Objects; import java.util.StringJoiner; 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 Ast { public static final char escapeCharacter = '\\'; @@ -25,21 +29,21 @@ interface Located { public static final class Node implements Located { - private final Type type; + private final NodeType 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) { + Node(NodeType type, int start, int end, String token) { this(type, start, end, null, requireNonNull(token)); } - Node(Type type, int start, int end, List nodes) { + Node(NodeType 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) { + private Node(NodeType type, int start, int end, List nodes, String token) { this.type = requireNonNull(type); this.nodes = nodes; this.token = token; @@ -47,7 +51,7 @@ private Node(Type type, int start, int end, List nodes, String token) { this.end = end; } - public enum Type { + public enum NodeType { TEXT_NODE, OPTIONAL_NODE, ALTERNATION_NODE, @@ -71,7 +75,7 @@ public List nodes() { return nodes; } - public Type type() { + public NodeType type() { return type; } @@ -155,11 +159,11 @@ public int hashCode() { static final class Token implements Located { final String text; - final Token.Type type; + final TokenType type; final int start; final int end; - Token(String text, Token.Type type, int start, int end) { + Token(String text, TokenType type, int start, int end) { this.text = requireNonNull(text); this.type = requireNonNull(type); this.start = start; @@ -182,23 +186,23 @@ static boolean canEscape(Integer token) { return false; } - static Type typeOf(Integer token) { + static TokenType typeOf(Integer token) { if (Character.isWhitespace(token)) { - return Type.WHITE_SPACE; + return TokenType.WHITE_SPACE; } switch (token) { case (int) alternationCharacter: - return Type.ALTERNATION; + return TokenType.ALTERNATION; case (int) beginParameterCharacter: - return Type.BEGIN_PARAMETER; + return TokenType.BEGIN_PARAMETER; case (int) endParameterCharacter: - return Type.END_PARAMETER; + return TokenType.END_PARAMETER; case (int) beginOptionalCharacter: - return Type.BEGIN_OPTIONAL; + return TokenType.BEGIN_OPTIONAL; case (int) endOptionalCharacter: - return Type.END_OPTIONAL; + return TokenType.END_OPTIONAL; } - return Type.TEXT; + return TokenType.TEXT; } static boolean isEscapeCharacter(int token) { @@ -241,7 +245,7 @@ public String toString() { .toString(); } - enum Type { + enum TokenType { START_OF_LINE, END_OF_LINE, WHITE_SPACE, @@ -255,11 +259,11 @@ enum Type { private final String symbol; private final String purpose; - Type() { + TokenType() { this(null, null); } - Type(String symbol, String purpose) { + TokenType(String symbol, String purpose) { this.symbol = symbol; this.purpose = purpose; } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java index c0ea66155..57976c6bd 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import io.cucumber.cucumberexpressions.Ast.Node; +import io.cucumber.cucumberexpressions.Ast.Node.NodeType; import org.apiguardian.api.API; import java.lang.reflect.Type; @@ -9,16 +10,14 @@ 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.Ast.Node.NodeType.OPTIONAL_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.PARAMETER_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.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 +129,8 @@ private void assertNoOptionals(Node node, assertNoNodeOfType(OPTIONAL_NODE, node, createNodeContainedAnOptionalException); } - private void assertNoNodeOfType(Node.Type nodeType, Node node, - Function createException) { + private void assertNoNodeOfType(NodeType nodeType, Node node, + Function createException) { node.nodes() .stream() .filter(astNode -> nodeType.equals(astNode.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..5c55568c5 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java @@ -3,7 +3,7 @@ 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 io.cucumber.cucumberexpressions.Ast.Token.TokenType; import org.apiguardian.api.API; @API(status = API.Status.STABLE) @@ -17,8 +17,8 @@ public class CucumberExpressionException extends RuntimeException { super(message, cause); } - static CucumberExpressionException createMissingEndToken(String expression, Type beginToken, Type endToken, - Token current) { + static CucumberExpressionException createMissingEndToken(String expression, TokenType beginToken, TokenType endToken, + 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 1df7ab037..468113933 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java @@ -2,26 +2,26 @@ import io.cucumber.cucumberexpressions.Ast.Node; import io.cucumber.cucumberexpressions.Ast.Token; -import io.cucumber.cucumberexpressions.Ast.Token.Type; +import io.cucumber.cucumberexpressions.Ast.Token.TokenType; 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.Ast.Token.Type.ALTERNATION; -import static io.cucumber.cucumberexpressions.Ast.Token.Type.BEGIN_OPTIONAL; -import static io.cucumber.cucumberexpressions.Ast.Token.Type.BEGIN_PARAMETER; -import static io.cucumber.cucumberexpressions.Ast.Token.Type.END_OF_LINE; -import static io.cucumber.cucumberexpressions.Ast.Token.Type.END_OPTIONAL; -import static io.cucumber.cucumberexpressions.Ast.Token.Type.END_PARAMETER; -import static io.cucumber.cucumberexpressions.Ast.Token.Type.START_OF_LINE; -import static io.cucumber.cucumberexpressions.Ast.Token.Type.WHITE_SPACE; +import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.ALTERNATION_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.ALTERNATIVE_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.EXPRESSION_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.OPTIONAL_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.PARAMETER_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.TEXT_NODE; +import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.ALTERNATION; +import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.BEGIN_OPTIONAL; +import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.BEGIN_PARAMETER; +import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.END_OF_LINE; +import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.END_OPTIONAL; +import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.END_PARAMETER; +import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.START_OF_LINE; +import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.WHITE_SPACE; import static io.cucumber.cucumberexpressions.CucumberExpressionException.createAlternationNotAllowedInOptional; import static io.cucumber.cucumberexpressions.CucumberExpressionException.createInvalidParameterTypeName; import static io.cucumber.cucumberexpressions.CucumberExpressionException.createMissingEndToken; @@ -197,9 +197,9 @@ private Result(int consumed, List ast) { } private static Parser parseBetween( - Node.Type type, - Type beginToken, - Type endToken, + Node.NodeType type, + TokenType beginToken, + TokenType endToken, List parsers) { return (expression, tokens, current) -> { if (!lookingAt(tokens, current, beginToken)) { @@ -225,7 +225,7 @@ private static Result parseTokensUntil( List parsers, List tokens, int startAt, - Type... endTokens) { + TokenType... endTokens) { int current = startAt; int size = tokens.size(); List ast = new ArrayList<>(); @@ -259,8 +259,8 @@ private static Result parseToken(String expression, List parsers, throw new IllegalStateException("No eligible parsers for " + tokens); } - private static boolean lookingAtAny(List tokens, int at, Type... tokenTypes) { - for (Type tokeType : tokenTypes) { + private static boolean lookingAtAny(List tokens, int at, TokenType... tokenTypes) { + for (TokenType tokeType : tokenTypes) { if (lookingAt(tokens, at, tokeType)) { return true; } @@ -268,7 +268,7 @@ private static boolean lookingAtAny(List tokens, int at, Type... tokenTyp return false; } - private static boolean lookingAt(List tokens, int at, Type token) { + private static boolean lookingAt(List tokens, int at, TokenType token) { if (at < 0) { // If configured correctly this will never happen // Keep for completeness diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java index 9a2a9df25..191f8e3de 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java @@ -1,7 +1,7 @@ package io.cucumber.cucumberexpressions; import io.cucumber.cucumberexpressions.Ast.Token; -import io.cucumber.cucumberexpressions.Ast.Token.Type; +import io.cucumber.cucumberexpressions.Ast.Token.TokenType; import java.util.ArrayList; import java.util.Iterator; @@ -30,8 +30,8 @@ private static class TokenIterator implements Iterator { private final OfInt codePoints; private StringBuilder buffer = new StringBuilder(); - private Type previousTokenType = null; - private Type currentTokenType = Type.START_OF_LINE; + private TokenType previousTokenType = null; + private TokenType currentTokenType = TokenType.START_OF_LINE; private boolean treatAsText; private int bufferStartIndex; private int escaped; @@ -41,9 +41,9 @@ private static class TokenIterator implements Iterator { this.codePoints = expression.codePoints().iterator(); } - private Token convertBufferToToken(Type tokenType) { + private Token convertBufferToToken(TokenType tokenType) { int escapeTokens = 0; - if (tokenType == Type.TEXT) { + if (tokenType == TokenType.TEXT) { escapeTokens = escaped; escaped = 0; } @@ -59,25 +59,25 @@ private void advanceTokenTypes() { currentTokenType = null; } - private Type tokenTypeOf(Integer token, boolean treatAsText) { + private TokenType tokenTypeOf(Integer token, boolean treatAsText) { if (!treatAsText) { return Token.typeOf(token); } if (Token.canEscape(token)) { - return Type.TEXT; + return TokenType.TEXT; } throw createCantEscape(expression, bufferStartIndex + buffer.codePointCount(0, buffer.length()) + escaped); } - private boolean shouldContinueTokenType(Type previousTokenType, - Type currentTokenType) { + private boolean shouldContinueTokenType(TokenType previousTokenType, + TokenType currentTokenType) { return currentTokenType == previousTokenType - && (currentTokenType == Type.WHITE_SPACE || currentTokenType == Type.TEXT); + && (currentTokenType == TokenType.WHITE_SPACE || currentTokenType == TokenType.TEXT); } @Override public boolean hasNext() { - return previousTokenType != Type.END_OF_LINE; + return previousTokenType != TokenType.END_OF_LINE; } @Override @@ -85,7 +85,7 @@ public Token next() { if (!hasNext()) { throw new NoSuchElementException(); } - if (currentTokenType == Type.START_OF_LINE) { + if (currentTokenType == TokenType.START_OF_LINE) { Token token = convertBufferToToken(currentTokenType); advanceTokenTypes(); return token; @@ -101,7 +101,7 @@ public Token next() { currentTokenType = tokenTypeOf(codePoint, treatAsText); treatAsText = false; - if (previousTokenType == Type.START_OF_LINE || + if (previousTokenType == TokenType.START_OF_LINE || shouldContinueTokenType(previousTokenType, currentTokenType)) { advanceTokenTypes(); buffer.appendCodePoint(codePoint); @@ -119,7 +119,7 @@ public Token next() { return token; } - currentTokenType = Type.END_OF_LINE; + currentTokenType = TokenType.END_OF_LINE; if (treatAsText) { throw createTheEndOfLineCanNotBeEscaped(expression); } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java index 3221a59dd..9635429d1 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import io.cucumber.cucumberexpressions.Ast.Node; +import io.cucumber.cucumberexpressions.Ast.Node.NodeType; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.converter.ArgumentConversionException; @@ -71,7 +72,7 @@ public Expectation convert(Object source, ParameterContext context) throws Argum } static class YamlableNode { - public Ast.Node.Type type; + public NodeType type; public List nodes; public String token; public int start; diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java index 11c0cef97..760a4cebe 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.TokenType; 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 TokenType type; public int start; public int end; From 58f974a8c72d2e6213bb67cc9b110ae961258ccb Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Mon, 10 Nov 2025 23:24:46 +0100 Subject: [PATCH 3/4] Mark API as public --- .../cucumber/cucumberexpressions/CucumberExpressionParser.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java index 468113933..024cc8ba1 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java @@ -3,6 +3,7 @@ import io.cucumber.cucumberexpressions.Ast.Node; import io.cucumber.cucumberexpressions.Ast.Token; import io.cucumber.cucumberexpressions.Ast.Token.TokenType; +import org.apiguardian.api.API; import java.util.ArrayList; import java.util.Arrays; @@ -27,10 +28,12 @@ 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; /** * A parser for Cucumber expressions */ +@API(since = "18.1", status = EXPERIMENTAL) public final class CucumberExpressionParser { /* From ca3c8bda17d42521058356ccddef848467683b10 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Mon, 10 Nov 2025 23:31:48 +0100 Subject: [PATCH 4/4] Extract API to package --- .../io/cucumber/cucumberexpressions/Ast.java | 177 +++--------------- .../CucumberExpression.java | 16 +- .../CucumberExpressionException.java | 5 +- .../CucumberExpressionParser.java | 45 +++-- .../CucumberExpressionTokenizer.java | 28 +-- .../io/cucumber/cucumberexpressions/Node.java | 140 ++++++++++++++ .../UndefinedParameterTypeException.java | 1 - .../CucumberExpressionParserTest.java | 5 +- .../CucumberExpressionTokenizerTest.java | 4 +- 9 files changed, 210 insertions(+), 211 deletions(-) create mode 100644 java/src/main/java/io/cucumber/cucumberexpressions/Node.java diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java b/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java index b3916b1d5..6ba5567ce 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java @@ -1,24 +1,11 @@ package io.cucumber.cucumberexpressions; -import org.apiguardian.api.API; - -import java.util.List; import java.util.Objects; import java.util.StringJoiner; 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 Ast { - public static final char escapeCharacter = '\\'; - public static final char alternationCharacter = '/'; - public static final char beginParameterCharacter = '{'; - public static final char endParameterCharacter = '}'; - public static final char beginOptionalCharacter = '('; - public static final char endOptionalCharacter = ')'; +final class Ast { interface Located { int start(); @@ -27,143 +14,21 @@ interface Located { } - public static final class Node implements Located { - - private final NodeType type; - private final List nodes; - private final String token; - private final int start; - private final int end; - - Node(NodeType type, int start, int end, String token) { - this(type, start, end, null, requireNonNull(token)); - } - - Node(NodeType type, int start, int end, List nodes) { - this(type, start, end, requireNonNull(nodes), null); - } - - private Node(NodeType 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 NodeType { - 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 NodeType 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); - } - - } - 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 TokenType type; + final Type type; final int start; final int end; - Token(String text, TokenType 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; @@ -186,23 +51,23 @@ static boolean canEscape(Integer token) { return false; } - static TokenType typeOf(Integer token) { + static Type typeOf(Integer token) { if (Character.isWhitespace(token)) { - return TokenType.WHITE_SPACE; + return Type.WHITE_SPACE; } switch (token) { case (int) alternationCharacter: - return TokenType.ALTERNATION; + return Type.ALTERNATION; case (int) beginParameterCharacter: - return TokenType.BEGIN_PARAMETER; + return Type.BEGIN_PARAMETER; case (int) endParameterCharacter: - return TokenType.END_PARAMETER; + return Type.END_PARAMETER; case (int) beginOptionalCharacter: - return TokenType.BEGIN_OPTIONAL; + return Type.BEGIN_OPTIONAL; case (int) endOptionalCharacter: - return TokenType.END_OPTIONAL; + return Type.END_OPTIONAL; } - return TokenType.TEXT; + return Type.TEXT; } static boolean isEscapeCharacter(int token) { @@ -245,7 +110,7 @@ public String toString() { .toString(); } - enum TokenType { + enum Type { START_OF_LINE, END_OF_LINE, WHITE_SPACE, @@ -259,11 +124,11 @@ enum TokenType { private final String symbol; private final String purpose; - TokenType() { + Type() { this(null, null); } - TokenType(String symbol, String purpose) { + Type(String symbol, String purpose) { this.symbol = symbol; this.purpose = purpose; } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java index 57976c6bd..b683e23ca 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java @@ -1,18 +1,16 @@ package io.cucumber.cucumberexpressions; -import io.cucumber.cucumberexpressions.Ast.Node; -import io.cucumber.cucumberexpressions.Ast.Node.NodeType; +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.NodeType.OPTIONAL_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.PARAMETER_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.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.createOptionalIsNotAllowedInOptional; @@ -129,7 +127,7 @@ private void assertNoOptionals(Node node, assertNoNodeOfType(OPTIONAL_NODE, node, createNodeContainedAnOptionalException); } - private void assertNoNodeOfType(NodeType nodeType, Node node, + private void assertNoNodeOfType(Type nodeType, Node node, Function createException) { node.nodes() .stream() @@ -143,7 +141,7 @@ private void assertNoNodeOfType(NodeType 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; @@ -152,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 5c55568c5..0d0496d47 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java @@ -1,9 +1,8 @@ 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.TokenType; +import io.cucumber.cucumberexpressions.Ast.Token.Type; import org.apiguardian.api.API; @API(status = API.Status.STABLE) @@ -17,7 +16,7 @@ public class CucumberExpressionException extends RuntimeException { super(message, cause); } - static CucumberExpressionException createMissingEndToken(String expression, TokenType beginToken, TokenType endToken, + static CucumberExpressionException createMissingEndToken(String expression, Type beginToken, Type endToken, Token current) { return new CucumberExpressionException(message( current.start(), diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java index 024cc8ba1..9701c6cb6 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java @@ -1,28 +1,27 @@ package io.cucumber.cucumberexpressions; -import io.cucumber.cucumberexpressions.Ast.Node; import io.cucumber.cucumberexpressions.Ast.Token; -import io.cucumber.cucumberexpressions.Ast.Token.TokenType; +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.NodeType.ALTERNATION_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.ALTERNATIVE_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.EXPRESSION_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.OPTIONAL_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.PARAMETER_NODE; -import static io.cucumber.cucumberexpressions.Ast.Node.NodeType.TEXT_NODE; -import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.ALTERNATION; -import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.BEGIN_OPTIONAL; -import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.BEGIN_PARAMETER; -import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.END_OF_LINE; -import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.END_OPTIONAL; -import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.END_PARAMETER; -import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.START_OF_LINE; -import static io.cucumber.cucumberexpressions.Ast.Token.TokenType.WHITE_SPACE; +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; +import static io.cucumber.cucumberexpressions.Ast.Token.Type.END_OF_LINE; +import static io.cucumber.cucumberexpressions.Ast.Token.Type.END_OPTIONAL; +import static io.cucumber.cucumberexpressions.Ast.Token.Type.END_PARAMETER; +import static io.cucumber.cucumberexpressions.Ast.Token.Type.START_OF_LINE; +import static io.cucumber.cucumberexpressions.Ast.Token.Type.WHITE_SPACE; import static io.cucumber.cucumberexpressions.CucumberExpressionException.createAlternationNotAllowedInOptional; import static io.cucumber.cucumberexpressions.CucumberExpressionException.createInvalidParameterTypeName; import static io.cucumber.cucumberexpressions.CucumberExpressionException.createMissingEndToken; @@ -200,9 +199,9 @@ private Result(int consumed, List ast) { } private static Parser parseBetween( - Node.NodeType type, - TokenType beginToken, - TokenType endToken, + Node.Type type, + Type beginToken, + Type endToken, List parsers) { return (expression, tokens, current) -> { if (!lookingAt(tokens, current, beginToken)) { @@ -228,7 +227,7 @@ private static Result parseTokensUntil( List parsers, List tokens, int startAt, - TokenType... endTokens) { + Type... endTokens) { int current = startAt; int size = tokens.size(); List ast = new ArrayList<>(); @@ -262,8 +261,8 @@ private static Result parseToken(String expression, List parsers, throw new IllegalStateException("No eligible parsers for " + tokens); } - private static boolean lookingAtAny(List tokens, int at, TokenType... tokenTypes) { - for (TokenType tokeType : tokenTypes) { + private static boolean lookingAtAny(List tokens, int at, Type... tokenTypes) { + for (Type tokeType : tokenTypes) { if (lookingAt(tokens, at, tokeType)) { return true; } @@ -271,7 +270,7 @@ private static boolean lookingAtAny(List tokens, int at, TokenType... tok return false; } - private static boolean lookingAt(List tokens, int at, TokenType token) { + private static boolean lookingAt(List tokens, int at, Type token) { if (at < 0) { // If configured correctly this will never happen // Keep for completeness diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java index 191f8e3de..b4c12e538 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java @@ -1,7 +1,7 @@ package io.cucumber.cucumberexpressions; import io.cucumber.cucumberexpressions.Ast.Token; -import io.cucumber.cucumberexpressions.Ast.Token.TokenType; +import io.cucumber.cucumberexpressions.Ast.Token.Type; import java.util.ArrayList; import java.util.Iterator; @@ -30,8 +30,8 @@ private static class TokenIterator implements Iterator { private final OfInt codePoints; private StringBuilder buffer = new StringBuilder(); - private TokenType previousTokenType = null; - private TokenType currentTokenType = TokenType.START_OF_LINE; + private Type previousTokenType = null; + private Type currentTokenType = Type.START_OF_LINE; private boolean treatAsText; private int bufferStartIndex; private int escaped; @@ -41,9 +41,9 @@ private static class TokenIterator implements Iterator { this.codePoints = expression.codePoints().iterator(); } - private Token convertBufferToToken(TokenType tokenType) { + private Token convertBufferToToken(Type tokenType) { int escapeTokens = 0; - if (tokenType == TokenType.TEXT) { + if (tokenType == Type.TEXT) { escapeTokens = escaped; escaped = 0; } @@ -59,25 +59,25 @@ private void advanceTokenTypes() { currentTokenType = null; } - private TokenType tokenTypeOf(Integer token, boolean treatAsText) { + private Type tokenTypeOf(Integer token, boolean treatAsText) { if (!treatAsText) { return Token.typeOf(token); } if (Token.canEscape(token)) { - return TokenType.TEXT; + return Type.TEXT; } throw createCantEscape(expression, bufferStartIndex + buffer.codePointCount(0, buffer.length()) + escaped); } - private boolean shouldContinueTokenType(TokenType previousTokenType, - TokenType currentTokenType) { + private boolean shouldContinueTokenType(Type previousTokenType, + Type currentTokenType) { return currentTokenType == previousTokenType - && (currentTokenType == TokenType.WHITE_SPACE || currentTokenType == TokenType.TEXT); + && (currentTokenType == Type.WHITE_SPACE || currentTokenType == Type.TEXT); } @Override public boolean hasNext() { - return previousTokenType != TokenType.END_OF_LINE; + return previousTokenType != Type.END_OF_LINE; } @Override @@ -85,7 +85,7 @@ public Token next() { if (!hasNext()) { throw new NoSuchElementException(); } - if (currentTokenType == TokenType.START_OF_LINE) { + if (currentTokenType == Type.START_OF_LINE) { Token token = convertBufferToToken(currentTokenType); advanceTokenTypes(); return token; @@ -101,7 +101,7 @@ public Token next() { currentTokenType = tokenTypeOf(codePoint, treatAsText); treatAsText = false; - if (previousTokenType == TokenType.START_OF_LINE || + if (previousTokenType == Type.START_OF_LINE || shouldContinueTokenType(previousTokenType, currentTokenType)) { advanceTokenTypes(); buffer.appendCodePoint(codePoint); @@ -119,7 +119,7 @@ public Token next() { return token; } - currentTokenType = TokenType.END_OF_LINE; + currentTokenType = Type.END_OF_LINE; if (treatAsText) { throw createTheEndOfLineCanNotBeEscaped(expression); } 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/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 9635429d1..bd95a710b 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java @@ -1,7 +1,6 @@ package io.cucumber.cucumberexpressions; -import io.cucumber.cucumberexpressions.Ast.Node; -import io.cucumber.cucumberexpressions.Ast.Node.NodeType; +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; @@ -72,7 +71,7 @@ public Expectation convert(Object source, ParameterContext context) throws Argum } static class YamlableNode { - public NodeType type; + public Type type; public List nodes; public String token; public int start; diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java index 760a4cebe..c1427f663 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java @@ -1,7 +1,7 @@ package io.cucumber.cucumberexpressions; import io.cucumber.cucumberexpressions.Ast.Token; -import io.cucumber.cucumberexpressions.Ast.Token.TokenType; +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; @@ -77,7 +77,7 @@ public Expectation convert(Object source, ParameterContext context) throws Argum static class YamlableToken { public String text; - public TokenType type; + public Type type; public int start; public int end;