From 47ebc109cb86aa0a9fa2396c35d2b5d3bc4252c7 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 13 Nov 2025 23:34:00 +0100 Subject: [PATCH 1/9] Use Java 17 --- CHANGELOG.md | 2 + java/.mvn/jvm.config | 10 + java/pom.xml | 33 +- .../AmbiguousParameterTypeException.java | 24 +- .../cucumberexpressions/Argument.java | 3 +- .../io/cucumber/cucumberexpressions/Ast.java | 196 +++--------- .../BuiltInParameterTransformer.java | 13 +- .../CaptureGroupTransformer.java | 3 +- .../CucumberExpression.java | 74 ++--- .../CucumberExpressionException.java | 11 +- .../CucumberExpressionGenerator.java | 4 +- .../CucumberExpressionParser.java | 112 ++++--- .../CucumberExpressionTokenizer.java | 35 +-- .../cucumberexpressions/Expression.java | 5 +- .../ExpressionFactory.java | 7 +- .../GeneratedExpression.java | 7 +- .../cucumber/cucumberexpressions/Group.java | 18 +- .../cucumberexpressions/GroupBuilder.java | 10 +- .../KeyboardFriendlyDecimalFormatSymbols.java | 6 +- .../io/cucumber/cucumberexpressions/Node.java | 149 +++++++++ .../cucumberexpressions/NumberParser.java | 3 +- .../ParameterByTypeTransformer.java | 4 +- .../cucumberexpressions/ParameterType.java | 100 +++---- .../ParameterTypeMatcher.java | 1 + .../ParameterTypeRegistry.java | 50 ++-- .../cucumberexpressions/PatternCompiler.java | 4 +- .../PatternCompilerProvider.java | 18 +- .../cucumberexpressions/RegexpUtils.java | 11 +- .../RegularExpression.java | 5 +- .../cucumberexpressions/Transformer.java | 6 +- .../cucumberexpressions/TreeRegexp.java | 6 +- .../cucumberexpressions/TypeReference.java | 2 +- .../UndefinedParameterTypeException.java | 1 - .../cucumberexpressions/package-info.java | 4 + .../cucumberexpressions/ArgumentTest.java | 9 +- .../BuiltInParameterTransformerTest.java | 41 +-- ...atorialGeneratedExpressionFactoryTest.java | 11 +- .../CucumberExpressionGeneratorTest.java | 33 +- .../CucumberExpressionParserTest.java | 66 ++-- .../CucumberExpressionTest.java | 92 ++++-- .../CucumberExpressionTokenizerTest.java | 59 ++-- .../CucumberExpressionTransformationTest.java | 28 +- .../cucumberexpressions/CustomMatchers.java | 13 +- .../CustomParameterTypeTest.java | 282 +++++++----------- .../EnumParameterTypeTest.java | 17 +- .../ExpressionFactoryTest.java | 1 - .../GenericParameterTypeTest.java | 22 +- ...boardFriendlyDecimalFormatSymbolsTest.java | 5 +- .../cucumberexpressions/NumberParserTest.java | 11 +- .../ParameterByTypeTransformerTest.java | 41 +-- .../ParameterTypeComparatorTest.java | 40 +-- .../ParameterTypeRegistryTest.java | 214 +++++++------ .../PatternCompilerProviderTest.java | 56 ++-- .../RegularExpressionTest.java | 247 ++++++++------- .../cucumberexpressions/TreeRegexpTest.java | 29 +- 55 files changed, 1187 insertions(+), 1067 deletions(-) create mode 100644 java/.mvn/jvm.config create mode 100644 java/src/main/java/io/cucumber/cucumberexpressions/Node.java create mode 100644 java/src/main/java/io/cucumber/cucumberexpressions/package-info.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a22f06f1c..501877f0d 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/.mvn/jvm.config b/java/.mvn/jvm.config new file mode 100644 index 000000000..32599cefe --- /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 326209ad2..b6522dbc7 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -5,7 +5,7 @@ io.cucumber cucumber-parent - 4.5.0 + 5.0.0-SNAPSHOT cucumber-expressions @@ -43,10 +43,23 @@ pom import + + org.assertj + assertj-bom + 3.27.6 + pom + import + + + org.jspecify + jspecify + 1.0.0 + + org.apiguardian apiguardian-api @@ -79,6 +92,11 @@ junit-jupiter test + + org.assertj + assertj-core + test + @@ -96,6 +114,19 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + + + validate + validate + + check + + + + diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/AmbiguousParameterTypeException.java b/java/src/main/java/io/cucumber/cucumberexpressions/AmbiguousParameterTypeException.java index 11edead2a..c7cb40e85 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/AmbiguousParameterTypeException.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/AmbiguousParameterTypeException.java @@ -15,17 +15,19 @@ public final class AmbiguousParameterTypeException extends CucumberExpressionExc private final List generatedExpressions; AmbiguousParameterTypeException(String parameterTypeRegexp, Pattern expressionRegexp, SortedSet> parameterTypes, List generatedExpressions) { - super(String.format("Your Regular Expression /%s/\n" + - "matches multiple parameter types with regexp /%s/:\n" + - " %s\n" + - "\n" + - "I couldn't decide which one to use. You have two options:\n" + - "\n" + - "1) Use a Cucumber Expression instead of a Regular Expression. Try one of these:\n" + - " %s\n" + - "\n" + - "2) Make one of the parameter types preferential and continue to use a Regular Expression.\n" + - "\n", + super(String.format(""" + Your Regular Expression /%s/ + matches multiple parameter types with regexp /%s/: + %s + + I couldn't decide which one to use. You have two options: + + 1) Use a Cucumber Expression instead of a Regular Expression. Try one of these: + %s + + 2) Make one of the parameter types preferential and continue to use a Regular Expression. + + """, expressionRegexp.pattern(), parameterTypeRegexp, parameterTypeNames(parameterTypes), diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Argument.java b/java/src/main/java/io/cucumber/cucumberexpressions/Argument.java index 3dd15413b..57f61e7d8 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/Argument.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Argument.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import java.lang.reflect.Type; import java.util.ArrayList; @@ -38,7 +39,7 @@ public Group getGroup() { return group; } - public T getValue() { + public @Nullable T getValue() { return parameterType.transform(group.getValues()); } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java b/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java index 50c0fef67..8e1c67c84 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java @@ -1,22 +1,14 @@ package io.cucumber.cucumberexpressions; -import java.util.List; +import org.jspecify.annotations.Nullable; + 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 +16,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; @@ -161,45 +41,41 @@ static boolean canEscape(Integer token) { if (Character.isWhitespace(token)) { return true; } - switch (token) { - case (int) escapeCharacter: - case (int) alternationCharacter: - case (int) beginParameterCharacter: - case (int) endParameterCharacter: - case (int) beginOptionalCharacter: - case (int) endOptionalCharacter: - return true; - } - return false; + return switch (token) { + case (int) escapeCharacter, + (int) alternationCharacter, + (int) beginParameterCharacter, + (int) endParameterCharacter, + (int) beginOptionalCharacter, + (int) endOptionalCharacter -> true; + default -> false; + }; } static Type typeOf(Integer token) { if (Character.isWhitespace(token)) { return Type.WHITE_SPACE; } - switch (token) { - case (int) alternationCharacter: - return Type.ALTERNATION; - case (int) beginParameterCharacter: - return Type.BEGIN_PARAMETER; - case (int) endParameterCharacter: - return Type.END_PARAMETER; - case (int) beginOptionalCharacter: - return Type.BEGIN_OPTIONAL; - case (int) endOptionalCharacter: - return Type.END_OPTIONAL; - } - return Type.TEXT; + return switch (token) { + case (int) alternationCharacter -> Type.ALTERNATION; + case (int) beginParameterCharacter -> Type.BEGIN_PARAMETER; + case (int) endParameterCharacter -> Type.END_PARAMETER; + case (int) beginOptionalCharacter -> Type.BEGIN_OPTIONAL; + case (int) endOptionalCharacter -> Type.END_OPTIONAL; + default -> Type.TEXT; + }; } static boolean isEscapeCharacter(int token) { return token == escapeCharacter; } + @Override public int start() { return start; } + @Override public int end() { return end; } @@ -224,10 +100,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(); } @@ -243,14 +119,14 @@ enum Type { ALTERNATION("" + alternationCharacter, "alternation"), TEXT; - private final String symbol; - private final String purpose; + private final @Nullable String symbol; + private final @Nullable String purpose; Type() { this(null, null); } - Type(String symbol, String purpose) { + Type(@Nullable String symbol, @Nullable String purpose) { this.symbol = symbol; this.purpose = purpose; } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/BuiltInParameterTransformer.java b/java/src/main/java/io/cucumber/cucumberexpressions/BuiltInParameterTransformer.java index 5c0b7a1e9..bfdd7b0ec 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/BuiltInParameterTransformer.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/BuiltInParameterTransformer.java @@ -1,5 +1,7 @@ package io.cucumber.cucumberexpressions; +import org.jspecify.annotations.Nullable; + import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.math.BigDecimal; @@ -18,11 +20,12 @@ final class BuiltInParameterTransformer implements ParameterByTypeTransformer { } @Override - public Object transform(String fromValue, Type toValueType) { + public @Nullable Object transform(@Nullable String fromValue, Type toValueType) { return doTransform(fromValue, toValueType, toValueType); } - private Object doTransform(String fromValue, Type toValueType, Type originalToValueType) { + @Nullable + private Object doTransform(@Nullable String fromValue, Type toValueType, Type originalToValueType) { Type optionalValueType; if ((optionalValueType = getOptionalGenericType(toValueType)) != null) { Object wrappedValue = doTransform(fromValue, optionalValueType, originalToValueType); @@ -99,16 +102,16 @@ private Object doTransform(String fromValue, Type toValueType, Type originalToVa throw createIllegalArgumentException(fromValue, originalToValueType); } + @Nullable private Type getOptionalGenericType(Type type) { if (Optional.class.equals(type)) { return Object.class; } - if (!(type instanceof ParameterizedType)) { + if (!(type instanceof ParameterizedType parameterizedType)) { return null; } - ParameterizedType parameterizedType = (ParameterizedType) type; if (Optional.class.equals(parameterizedType.getRawType())) { return parameterizedType.getActualTypeArguments()[0]; } @@ -116,7 +119,7 @@ private Type getOptionalGenericType(Type type) { return null; } - private IllegalArgumentException createIllegalArgumentException(String fromValue, Type toValueType) { + private IllegalArgumentException createIllegalArgumentException(@Nullable String fromValue, Type toValueType) { return new IllegalArgumentException( "Can't transform '" + fromValue + "' to " + toValueType + "\n" + "BuiltInParameterTransformer only supports a limited number of class types\n" + diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CaptureGroupTransformer.java b/java/src/main/java/io/cucumber/cucumberexpressions/CaptureGroupTransformer.java index 26e85fb0f..99b562895 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CaptureGroupTransformer.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CaptureGroupTransformer.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; /** * Transformer for a @{@link ParameterType} with (multiple) capture groups. @@ -20,5 +21,5 @@ public interface CaptureGroupTransformer { * @return the transformed object * @throws Throwable if transformation failed */ - T transform(String[] args) throws Throwable; + @Nullable T transform(@Nullable String[] args) throws Throwable; } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java index c0ea66155..cd7da0022 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java @@ -1,7 +1,7 @@ package io.cucumber.cucumberexpressions; -import io.cucumber.cucumberexpressions.Ast.Node; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import java.lang.reflect.Type; import java.util.ArrayList; @@ -9,16 +9,13 @@ 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.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; @@ -41,50 +38,42 @@ public final class CucumberExpression implements Expression { } private String rewriteToRegex(Node node) { - switch (node.type()) { - case TEXT_NODE: - return escapeRegex(node.text()); - case OPTIONAL_NODE: - return rewriteOptional(node); - case ALTERNATION_NODE: - return rewriteAlternation(node); - case ALTERNATIVE_NODE: - return rewriteAlternative(node); - case PARAMETER_NODE: - return rewriteParameter(node); - case EXPRESSION_NODE: - return rewriteExpression(node); - default: - // Can't happen as long as the switch case is exhaustive - throw new IllegalArgumentException(node.type().name()); - } + // Can't happen as long as the switch case is exhaustive + return switch (node.type()) { + case TEXT_NODE -> escapeRegex(node.text()); + case OPTIONAL_NODE -> rewriteOptional(node); + case ALTERNATION_NODE -> rewriteAlternation(node); + case ALTERNATIVE_NODE -> rewriteAlternative(node); + case PARAMETER_NODE -> rewriteParameter(node); + case EXPRESSION_NODE -> rewriteExpression(node); + }; } private String rewriteOptional(Node node) { assertNoParameters(node, astNode -> createParameterIsNotAllowedInOptional(astNode, source)); assertNoOptionals(node, astNode -> createOptionalIsNotAllowedInOptional(astNode, source)); assertNotEmpty(node, astNode -> createOptionalMayNotBeEmpty(astNode, source)); - return node.nodes().stream() + return node.requireNodes().stream() .map(this::rewriteToRegex) .collect(joining("", "(?:", ")?")); } private String rewriteAlternation(Node node) { // Make sure the alternative parts aren't empty and don't contain parameter types - for (Node alternative : node.nodes()) { - if (alternative.nodes().isEmpty()) { + for (Node alternative : node.requireNodes()) { + if (alternative.requireNodes().isEmpty()) { throw createAlternativeMayNotBeEmpty(alternative, source); } assertNotEmpty(alternative, astNode -> createAlternativeMayNotExclusivelyContainOptionals(astNode, source)); } - return node.nodes() + return node.requireNodes() .stream() .map(this::rewriteToRegex) .collect(joining("|", "(?:", ")")); } private String rewriteAlternative(Node node) { - return node.nodes().stream() + return node.requireNodes().stream() .map(this::rewriteToRegex) .collect(joining()); } @@ -106,45 +95,44 @@ private String rewriteParameter(Node node) { private String rewriteExpression(Node node) { - return node.nodes().stream() + return node.requireNodes().stream() .map(this::rewriteToRegex) .collect(joining("", "^", "$")); } private void assertNotEmpty(Node node, - Function createNodeWasNotEmptyException) { - node.nodes() + Function createNodeWasNotEmptyException) { + if (node.requireNodes() .stream() - .filter(astNode -> TEXT_NODE.equals(astNode.type())) - .findFirst() - .orElseThrow(() -> createNodeWasNotEmptyException.apply(node)); + .noneMatch(astNode -> Node.Type.TEXT_NODE.equals(astNode.type()))) { + throw createNodeWasNotEmptyException.apply(node); + } } private void assertNoParameters(Node node, - Function createNodeContainedAParameterException) { + Function createNodeContainedAParameterException) { assertNoNodeOfType(PARAMETER_NODE, node, createNodeContainedAParameterException); } private void assertNoOptionals(Node node, - Function createNodeContainedAnOptionalException) { + Function createNodeContainedAnOptionalException) { assertNoNodeOfType(OPTIONAL_NODE, node, createNodeContainedAnOptionalException); } private void assertNoNodeOfType(Node.Type nodeType, Node node, - Function createException) { - node.nodes() + Function createException) { + node.requireNodes() .stream() .filter(astNode -> nodeType.equals(astNode.type())) .map(createException) - .findFirst() - .ifPresent(exception -> { - throw exception; - }); + .findFirst().ifPresent(exception -> { + throw exception; + }); } @Override - public List> match(String text, Type... typeHints) { + public @Nullable List> match(String text, Type... typeHints) { final Group group = treeRegexp.match(text); if (group == null) { return null; diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java index 0dd23f1cd..011e708ad 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java @@ -1,9 +1,7 @@ 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; @API(status = API.Status.STABLE) @@ -17,8 +15,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, Token.Type beginToken, Token.Type endToken, + Token current) { return new CucumberExpressionException(message( current.start(), expression, @@ -66,6 +64,7 @@ static CucumberExpressionException createParameterIsNotAllowedInOptional(Node no "An optional may not contain a parameter type", "If you did not mean to use an parameter type you can use '\\{' to escape the '{'")); } + static CucumberExpressionException createOptionalIsNotAllowedInOptional(Node node, String expression) { return new CucumberExpressionException(message( node.start(), @@ -85,7 +84,7 @@ static CucumberExpressionException createOptionalMayNotBeEmpty(Node node, String } static CucumberExpressionException createAlternativeMayNotExclusivelyContainOptionals(Node node, - String expression) { + String expression) { return new CucumberExpressionException(message( node.start(), expression, @@ -131,7 +130,7 @@ static CucumberExpressionException createInvalidParameterTypeName(Token token, S } static String message(int index, String expression, String pointer, String problem, - String solution) { + String solution) { return thisCucumberExpressionHasAProblemAt(index) + "\n" + expression + "\n" + diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionGenerator.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionGenerator.java index 1cdd6fd3e..b56b2fa98 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionGenerator.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionGenerator.java @@ -79,7 +79,9 @@ public List generateExpressions(String text) { } private String escape(String s) { - return s.replaceAll("%", "%%") // Escape for String.format + return s + // Escape for String.format + .replaceAll("%", "%%") .replaceAll("\\(", "\\\\(") .replaceAll("\\{", "\\\\{") .replaceAll("/", "\\\\/"); diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java index b77be40e9..cbd80eda3 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java @@ -1,19 +1,16 @@ 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.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 +24,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 | ')' | '}' | . @@ -58,31 +60,25 @@ final class CucumberExpressionParser { */ private static final Parser nameParser = (expression, tokens, current) -> { Token token = tokens.get(current); - switch (token.type) { - case WHITE_SPACE: - case TEXT: - return new Result(1, new Node(TEXT_NODE, token.start(), token.end(), token.text)); - case BEGIN_OPTIONAL: - case END_OPTIONAL: - case BEGIN_PARAMETER: - case END_PARAMETER: - case ALTERNATION: - throw createInvalidParameterTypeName(token, expression); - case START_OF_LINE: - case END_OF_LINE: - default: - // If configured correctly this will never happen - return new Result(0); - } + return switch (token.type) { + case WHITE_SPACE, TEXT -> new Result(1, new Node(TEXT_NODE, token.start(), token.end(), token.text)); + case BEGIN_OPTIONAL, + END_OPTIONAL, + BEGIN_PARAMETER, + END_PARAMETER, + ALTERNATION -> throw createInvalidParameterTypeName(token, expression); + // If configured correctly this will never happen + default -> new Result(0); + }; }; /* * parameter := '{' + name* + '}' */ private static final Parser parameterParser = parseBetween( - PARAMETER_NODE, - BEGIN_PARAMETER, - END_PARAMETER, + Node.Type.PARAMETER_NODE, + Token.Type.BEGIN_PARAMETER, + Token.Type.END_PARAMETER, singletonList(nameParser) ); @@ -91,6 +87,7 @@ final class CucumberExpressionParser { * option := optional | parameter | text */ private static final Parser optionalParser; + static { List parsers = new ArrayList<>(); optionalParser = parseBetween( @@ -160,37 +157,24 @@ 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); return result.ast.get(0); } - private interface Parser { - Result parse(String expression, List tokens, int current); - - } - - private static final class Result { - final int consumed; - final List ast; - - private Result(int consumed, Node... ast) { - this(consumed, Arrays.asList(ast)); - } - - private Result(int consumed, List ast) { - this.consumed = consumed; - this.ast = ast; - } - - } - private static Parser parseBetween( Node.Type type, - Type beginToken, - Type endToken, + Token.Type beginToken, + Token.Type endToken, List parsers) { return (expression, tokens, current) -> { if (!lookingAt(tokens, current, beginToken)) { @@ -216,7 +200,7 @@ private static Result parseTokensUntil( List parsers, List tokens, int startAt, - Type... endTokens) { + Token.Type... endTokens) { int current = startAt; int size = tokens.size(); List ast = new ArrayList<>(); @@ -238,8 +222,8 @@ private static Result parseTokensUntil( } private static Result parseToken(String expression, List parsers, - List tokens, - int startAt) { + List tokens, + int startAt) { for (Parser parser : parsers) { Result result = parser.parse(expression, tokens, startAt); if (result.consumed != 0) { @@ -250,8 +234,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, Token.Type... tokenTypes) { + for (Token.Type tokeType : tokenTypes) { if (lookingAt(tokens, at, tokeType)) { return true; } @@ -259,7 +243,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, Token.Type token) { if (at < 0) { // If configured correctly this will never happen // Keep for completeness @@ -302,10 +286,20 @@ private static List createAlternativeNodes(int start, int end, List } else { Node leftSeparator = separators.get(i - 1); Node rightSeparator = separators.get(i); - nodes.add(new Node(ALTERNATIVE_NODE, leftSeparator.end(), rightSeparator.start(), n)); + nodes.add(new Node(ALTERNATIVE_NODE, /* start= */leftSeparator.end(), /* end= */rightSeparator.start(), n)); } } return nodes; } + private interface Parser { + Result parse(String expression, List tokens, int current); + + } + + private record Result(int consumed, List ast) { + private Result(int consumed, Node... ast) { + this(consumed, asList(ast)); + } + } } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java index 9a2a9df25..cfd1e1c07 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 org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.Iterator; @@ -11,6 +11,7 @@ import static io.cucumber.cucumberexpressions.CucumberExpressionException.createCantEscape; import static io.cucumber.cucumberexpressions.CucumberExpressionException.createTheEndOfLineCanNotBeEscaped; +import static java.util.Objects.requireNonNull; final class CucumberExpressionTokenizer { @@ -30,8 +31,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 Token.@Nullable Type previousTokenType = null; + private Token.@Nullable Type currentTokenType = Token.Type.START_OF_LINE; private boolean treatAsText; private int bufferStartIndex; private int escaped; @@ -41,9 +42,9 @@ private static class TokenIterator implements Iterator { this.codePoints = expression.codePoints().iterator(); } - private Token convertBufferToToken(Type tokenType) { + private Token convertBufferToToken(Token.Type tokenType) { int escapeTokens = 0; - if (tokenType == Type.TEXT) { + if (tokenType == Token.Type.TEXT) { escapeTokens = escaped; escaped = 0; } @@ -59,25 +60,25 @@ private void advanceTokenTypes() { currentTokenType = null; } - private Type tokenTypeOf(Integer token, boolean treatAsText) { + private Token.Type tokenTypeOf(Integer token, boolean treatAsText) { if (!treatAsText) { return Token.typeOf(token); } if (Token.canEscape(token)) { - return Type.TEXT; + return Token.Type.TEXT; } throw createCantEscape(expression, bufferStartIndex + buffer.codePointCount(0, buffer.length()) + escaped); } - private boolean shouldContinueTokenType(Type previousTokenType, - Type currentTokenType) { + private boolean shouldContinueTokenType(Token.@Nullable Type previousTokenType, + Token.@Nullable Type currentTokenType) { return currentTokenType == previousTokenType - && (currentTokenType == Type.WHITE_SPACE || currentTokenType == Type.TEXT); + && (currentTokenType == Token.Type.WHITE_SPACE || currentTokenType == Token.Type.TEXT); } @Override public boolean hasNext() { - return previousTokenType != Type.END_OF_LINE; + return previousTokenType != Token.Type.END_OF_LINE; } @Override @@ -85,7 +86,7 @@ public Token next() { if (!hasNext()) { throw new NoSuchElementException(); } - if (currentTokenType == Type.START_OF_LINE) { + if (currentTokenType == Token.Type.START_OF_LINE) { Token token = convertBufferToToken(currentTokenType); advanceTokenTypes(); return token; @@ -101,25 +102,25 @@ public Token next() { currentTokenType = tokenTypeOf(codePoint, treatAsText); treatAsText = false; - if (previousTokenType == Type.START_OF_LINE || + if (previousTokenType == Token.Type.START_OF_LINE || shouldContinueTokenType(previousTokenType, currentTokenType)) { advanceTokenTypes(); buffer.appendCodePoint(codePoint); } else { - Token t = convertBufferToToken(previousTokenType); + Token t = convertBufferToToken(requireNonNull(previousTokenType)); advanceTokenTypes(); buffer.appendCodePoint(codePoint); return t; } } - if (buffer.length() > 0) { - Token token = convertBufferToToken(previousTokenType); + if (!buffer.isEmpty()) { + Token token = convertBufferToToken(requireNonNull(previousTokenType)); advanceTokenTypes(); return token; } - currentTokenType = Type.END_OF_LINE; + currentTokenType = Token.Type.END_OF_LINE; if (treatAsText) { throw createTheEndOfLineCanNotBeEscaped(expression); } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Expression.java b/java/src/main/java/io/cucumber/cucumberexpressions/Expression.java index c3abf6d2e..1c99abbfc 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/Expression.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Expression.java @@ -1,15 +1,16 @@ package io.cucumber.cucumberexpressions; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import java.lang.reflect.Type; import java.util.List; -import java.util.Set; import java.util.regex.Pattern; @API(status = API.Status.STABLE) public interface Expression { - List> match(String text, Type... typeHints); + + @Nullable List> match(String text, Type... typeHints); Pattern getRegexp(); diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/ExpressionFactory.java b/java/src/main/java/io/cucumber/cucumberexpressions/ExpressionFactory.java index e5ebb2e6f..399641250 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/ExpressionFactory.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/ExpressionFactory.java @@ -2,7 +2,6 @@ import org.apiguardian.api.API; -import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -11,9 +10,9 @@ * using heuristics. This is particularly useful for languages that don't have a * literal syntax for regular expressions. In Java, a regular expression has to be represented as a String. * - * A string that starts with `^` and/or ends with `$` (or written in script style, i.e. starting with `/` - * and ending with `/`) is considered a regular expression. - * Everything else is considered a Cucumber expression. + *

A string that starts with `^` and/or ends with `$` (or written in script style, i.e. starting with `/` + * and ending with `/`) is considered a regular expression. + * Everything else is considered a Cucumber expression. */ @API(status = API.Status.STABLE) public final class ExpressionFactory { diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/GeneratedExpression.java b/java/src/main/java/io/cucumber/cucumberexpressions/GeneratedExpression.java index ec5113a6c..5e15ee221 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/GeneratedExpression.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/GeneratedExpression.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import java.text.Collator; import java.util.ArrayList; @@ -11,7 +12,7 @@ import java.util.Map; @API(status = API.Status.STABLE) -public class GeneratedExpression { +public final class GeneratedExpression { private static final Collator ENGLISH_COLLATOR = Collator.getInstance(Locale.ENGLISH); private static final String[] JAVA_KEYWORDS = { "abstract", "assert", "boolean", "break", "byte", "case", @@ -34,8 +35,8 @@ public class GeneratedExpression { this.parameterTypes = parameterTypes; } - private static boolean isJavaKeyword(String keyword) { - return (Arrays.binarySearch(JAVA_KEYWORDS, keyword, ENGLISH_COLLATOR) >= 0); + private static boolean isJavaKeyword(@Nullable String keyword) { + return Arrays.binarySearch(JAVA_KEYWORDS, keyword, ENGLISH_COLLATOR) >= 0; } public String getSource() { diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Group.java b/java/src/main/java/io/cucumber/cucumberexpressions/Group.java index 9b61fc6fe..fcf287fc3 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/Group.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Group.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import java.util.List; import java.util.regex.Pattern; @@ -12,19 +13,20 @@ import java.util.Collection; @API(status = API.Status.STABLE) -public class Group { +public final class Group { private final List children; - private final String value; + private final @Nullable String value; private final int start; private final int end; - Group(String value, int start, int end, List children) { + Group(@Nullable String value, int start, int end, List children) { this.value = value; this.start = start; this.end = end; this.children = children; } - + + @Nullable public String getValue() { return value; } @@ -62,11 +64,9 @@ public static Collection parse(Pattern expression) { private static List toGroups(List children) { List list = new ArrayList<>(); - if (children != null) { - for (GroupBuilder child : children) { - list.add(new Group(child.getSource(), child.getStartIndex(), child.getEndIndex(), - toGroups(child.getChildren()))); - } + for (GroupBuilder child : children) { + list.add(new Group(child.getSource(), child.getStartIndex(), child.getEndIndex(), + toGroups(child.getChildren()))); } return list; } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/GroupBuilder.java b/java/src/main/java/io/cucumber/cucumberexpressions/GroupBuilder.java index f5222f6f6..d24bb77f5 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/GroupBuilder.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/GroupBuilder.java @@ -1,15 +1,19 @@ package io.cucumber.cucumberexpressions; +import org.jspecify.annotations.Nullable; + import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; +import static java.util.Objects.requireNonNull; + final class GroupBuilder { private final List groupBuilders = new ArrayList<>(); private boolean capturing = true; - private String source; - private int startIndex; + private @Nullable String source; + private final int startIndex; private int endIndex; GroupBuilder(int startIndex) { @@ -48,7 +52,7 @@ List getChildren() { } String getSource() { - return source; + return requireNonNull(source); } void setSource(String source) { diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/KeyboardFriendlyDecimalFormatSymbols.java b/java/src/main/java/io/cucumber/cucumberexpressions/KeyboardFriendlyDecimalFormatSymbols.java index 39b5e07b5..0de2bb29c 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/KeyboardFriendlyDecimalFormatSymbols.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/KeyboardFriendlyDecimalFormatSymbols.java @@ -8,8 +8,12 @@ *

* Note quite complete, feel free to make a suggestion. */ -class KeyboardFriendlyDecimalFormatSymbols { +final class KeyboardFriendlyDecimalFormatSymbols { + private KeyboardFriendlyDecimalFormatSymbols(){ + // utility class + } + static DecimalFormatSymbols getInstance(Locale locale) { DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale); 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..5b26eb691 --- /dev/null +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Node.java @@ -0,0 +1,149 @@ +package io.cucumber.cucumberexpressions; + +import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; + +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 @Nullable List nodes; + private final @Nullable 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, @Nullable List nodes, @Nullable 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 + } + + @Override + public int start() { + return start; + } + + @Override + public int end() { + return end; + } + + /** + * Returns child nodes, {@code null} if a leaf-node + */ + @Nullable + public List nodes() { + return nodes; + } + + List requireNodes() { + return requireNonNull(nodes); + } + + public Type type() { + return type; + } + + /** + * Returns the text contained with in this node, {@code null} if not a leaf-node + */ + @Nullable + public String token() { + return token; + } + + String text() { + if (nodes == null) + return requireNonNull(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/NumberParser.java b/java/src/main/java/io/cucumber/cucumberexpressions/NumberParser.java index 5cdc120c7..704df8677 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/NumberParser.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/NumberParser.java @@ -12,8 +12,7 @@ final class NumberParser { NumberParser(Locale locale) { numberFormat = DecimalFormat.getNumberInstance(locale); - if (numberFormat instanceof DecimalFormat) { - DecimalFormat decimalFormat = (DecimalFormat) numberFormat; + if (numberFormat instanceof DecimalFormat decimalFormat) { decimalFormat.setParseBigDecimal(true); DecimalFormatSymbols symbols = KeyboardFriendlyDecimalFormatSymbols.getInstance(locale); decimalFormat.setDecimalFormatSymbols(symbols); diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/ParameterByTypeTransformer.java b/java/src/main/java/io/cucumber/cucumberexpressions/ParameterByTypeTransformer.java index 0d076b751..4aa7c32d3 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/ParameterByTypeTransformer.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/ParameterByTypeTransformer.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import java.lang.reflect.Type; @@ -13,5 +14,6 @@ @FunctionalInterface public interface ParameterByTypeTransformer { - Object transform(String fromValue, Type toValueType) throws Throwable; + @Nullable + Object transform(@Nullable String fromValue, Type toValueType) throws Throwable; } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/ParameterType.java b/java/src/main/java/io/cucumber/cucumberexpressions/ParameterType.java index c3f834807..f1bcfbdf9 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/ParameterType.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/ParameterType.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import java.lang.reflect.Type; import java.util.List; @@ -8,10 +9,13 @@ import java.util.regex.Pattern; import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; @API(status = API.Status.STABLE) public final class ParameterType implements Comparable> { - @SuppressWarnings("RegExpRedundantEscape") // Android can't parse unescaped braces + + // Android can't parse unescaped braces + @SuppressWarnings("RegExpRedundantEscape") private static final Pattern ILLEGAL_PARAMETER_NAME_PATTERN = Pattern.compile("([{}()\\\\/])"); private static final Pattern UNESCAPE_PATTERN = Pattern.compile("(\\\\([\\[$.|?*+\\]]))"); @@ -24,10 +28,11 @@ public final class ParameterType implements Comparable> { private final boolean anonymous; private final boolean useRegexpMatchAsStrongTypeHint; - static void checkParameterTypeName(String name) { + static String requireValidParameterTypeName(String name) { if (!isValidParameterTypeName(name)) { throw CucumberExpressionException.createInvalidParameterTypeName(name); } + return name; } static boolean isValidParameterTypeName(String name) { @@ -37,17 +42,13 @@ static boolean isValidParameterTypeName(String name) { } static ParameterType createAnonymousParameterType(String regexp) { - return new ParameterType<>("", singletonList(regexp), Object.class, new CaptureGroupTransformer() { - - public Object transform(String[] arg) { - throw new UnsupportedOperationException("Anonymous transform must be deanonymized before use"); - } + return new ParameterType<>("", singletonList(regexp), Object.class, arg -> { + throw new UnsupportedOperationException("Anonymous transform must be deanonymized before use"); }, false, true, false, true); } - @SuppressWarnings("unchecked") - static ParameterType fromEnum(final Class enumClass) { - Enum[] enumConstants = enumClass.getEnumConstants(); + static > ParameterType fromEnum(final Class enumClass) { + var enumConstants = enumClass.getEnumConstants(); StringBuilder regexpBuilder = new StringBuilder(); for (int i = 0; i < enumConstants.length; i++) { if (i > 0) @@ -58,25 +59,24 @@ static ParameterType fromEnum(final Class enumClass) { enumClass.getSimpleName(), regexpBuilder.toString(), enumClass, - (String arg) -> (E) Enum.valueOf(enumClass, arg) + (@Nullable String arg) -> arg == null ? null : Enum.valueOf(enumClass, arg) ); } - private ParameterType(String name, List regexps, Type type, CaptureGroupTransformer transformer, - boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint, - boolean anonymous) { - if (regexps == null) - throw new NullPointerException("regexps cannot be null"); - if (type == null) - throw new NullPointerException("type cannot be null"); - if (transformer == null) - throw new NullPointerException("transformer cannot be null"); - if (name != null) - checkParameterTypeName(name); - this.name = name; - this.regexps = regexps; - this.type = type; - this.transformer = transformer; + private ParameterType( + String name, + List regexps, + Type type, + CaptureGroupTransformer transformer, + boolean useForSnippets, + boolean preferForRegexpMatch, + boolean useRegexpMatchAsStrongTypeHint, + boolean anonymous + ) { + this.name = requireValidParameterTypeName(requireNonNull(name)); + this.regexps = requireNonNull(regexps); + this.type = requireNonNull(type); + this.transformer = requireNonNull(transformer); this.useForSnippets = useForSnippets; this.preferForRegexpMatch = preferForRegexpMatch; this.anonymous = anonymous; @@ -84,13 +84,13 @@ private ParameterType(String name, List regexps, Type type, CaptureGroup } public ParameterType(String name, List regexps, Type type, CaptureGroupTransformer transformer, - boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { + boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { this(name, regexps, type, transformer, useForSnippets, preferForRegexpMatch, useRegexpMatchAsStrongTypeHint, false); } public ParameterType(String name, List regexps, Type type, CaptureGroupTransformer transformer, - boolean useForSnippets, boolean preferForRegexpMatch) { + boolean useForSnippets, boolean preferForRegexpMatch) { // Unless explicitly set useRegexpMatchAsStrongTypeHint is true. // // Reasoning: @@ -111,12 +111,12 @@ public ParameterType(String name, List regexps, Type type, CaptureGroupT } public ParameterType(String name, List regexps, Class type, CaptureGroupTransformer transformer, - boolean useForSnippets, boolean preferForRegexpMatch) { + boolean useForSnippets, boolean preferForRegexpMatch) { this(name, regexps, (Type) type, transformer, useForSnippets, preferForRegexpMatch); } public ParameterType(String name, String regexp, Class type, CaptureGroupTransformer transformer, - boolean useForSnippets, boolean preferForRegexpMatch) { + boolean useForSnippets, boolean preferForRegexpMatch) { this(name, singletonList(regexp), type, transformer, useForSnippets, preferForRegexpMatch); } @@ -129,35 +129,35 @@ public ParameterType(String name, String regexp, Class type, CaptureGroupTran } public ParameterType(String name, List regexps, Type type, Transformer transformer, - boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { + boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { this(name, regexps, type, new TransformerAdaptor<>(transformer), useForSnippets, preferForRegexpMatch, useRegexpMatchAsStrongTypeHint); } public ParameterType(String name, List regexps, Type type, Transformer transformer, - boolean useForSnippets, boolean preferForRegexpMatch) { + boolean useForSnippets, boolean preferForRegexpMatch) { this(name, regexps, type, new TransformerAdaptor<>(transformer), useForSnippets, preferForRegexpMatch); } public ParameterType(String name, List regexps, Class type, Transformer transformer, - boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { + boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { this(name, regexps, (Type) type, transformer, useForSnippets, preferForRegexpMatch, useRegexpMatchAsStrongTypeHint); } public ParameterType(String name, List regexps, Class type, Transformer transformer, - boolean useForSnippets, boolean preferForRegexpMatch) { + boolean useForSnippets, boolean preferForRegexpMatch) { this(name, regexps, (Type) type, transformer, useForSnippets, preferForRegexpMatch); } public ParameterType(String name, String regexp, Class type, Transformer transformer, boolean useForSnippets, - boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { + boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { this(name, singletonList(regexp), type, transformer, useForSnippets, preferForRegexpMatch, useRegexpMatchAsStrongTypeHint); } public ParameterType(String name, String regexp, Class type, Transformer transformer, boolean useForSnippets, - boolean preferForRegexpMatch) { + boolean preferForRegexpMatch) { this(name, singletonList(regexp), type, transformer, useForSnippets, preferForRegexpMatch); } @@ -172,7 +172,7 @@ public ParameterType(String name, String regexp, Class type, Transformer t /** * This is used in the type name in typed expressions * - * @return human readable type name + * @return human-readable type name */ public String getName() { return name; @@ -207,7 +207,7 @@ public boolean preferForRegexpMatch() { } /** - * Indicates whether or not this is a parameter type should be used for generating + * Indicates whether this is a parameter type should be used for generating * {@link GeneratedExpression}s from text. Typically, parameter types with greedy regexps * should return false. * @@ -240,17 +240,18 @@ ParameterType deAnonymize(Type type, Transformer transformer) { preferForRegexpMatch, useRegexpMatchAsStrongTypeHint, anonymous); } + @Nullable T transform(List groupValues) { if (transformer instanceof TransformerAdaptor) { if (groupValues.size() > 1) { if (isAnonymous()) { - throw new CucumberExpressionException(String.format("" + + throw new CucumberExpressionException(String.format( "Anonymous ParameterType has multiple capture groups %s. " + - "You can only use a single capture group in an anonymous ParameterType.", regexps)); + "You can only use a single capture group in an anonymous ParameterType.", regexps)); } - throw new CucumberExpressionException(String.format("" + + throw new CucumberExpressionException(String.format( "ParameterType {%s} was registered with a Transformer but has multiple capture groups %s. " + - "Did you mean to use a CaptureGroupTransformer?", name, regexps)); + "Did you mean to use a CaptureGroupTransformer?", name, regexps)); } } @@ -267,14 +268,12 @@ T transform(List groupValues) { } @Override - public int compareTo(ParameterType o) { - if (preferForRegexpMatch() && !o.preferForRegexpMatch()) + public int compareTo(ParameterType that) { + if (preferForRegexpMatch() && !that.preferForRegexpMatch()) return -1; - if (o.preferForRegexpMatch() && !preferForRegexpMatch()) + if (that.preferForRegexpMatch() && !preferForRegexpMatch()) return 1; - String name = getName() != null ? getName() : ""; - String otherName = o.getName() != null ? o.getName() : ""; - return name.compareTo(otherName); + return getName().compareTo(that.getName()); } public int weight() { @@ -289,13 +288,12 @@ private static final class TransformerAdaptor implements CaptureGroupTransfor private final Transformer transformer; private TransformerAdaptor(Transformer transformer) { - if (transformer == null) - throw new NullPointerException("transformer cannot be null"); + requireNonNull(transformer); this.transformer = transformer; } @Override - public T transform(String[] args) throws Throwable { + public @Nullable T transform(@Nullable String[] args) throws Throwable { return transformer.transform(args[0]); } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/ParameterTypeMatcher.java b/java/src/main/java/io/cucumber/cucumberexpressions/ParameterTypeMatcher.java index 2f638e421..972433775 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/ParameterTypeMatcher.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/ParameterTypeMatcher.java @@ -76,6 +76,7 @@ ParameterType getParameterType() { return parameterType; } + @Override public String toString() { return parameterType.getType().toString(); } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/ParameterTypeRegistry.java b/java/src/main/java/io/cucumber/cucumberexpressions/ParameterTypeRegistry.java index 5c6a3261a..8cde5b6c1 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/ParameterTypeRegistry.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/ParameterTypeRegistry.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import java.math.BigDecimal; import java.math.BigInteger; @@ -66,64 +67,67 @@ private ParameterTypeRegistry(ParameterByTypeTransformer defaultParameterTransfo .replace("{expnt}", "" + numberFormat.getExponentSeparator()) ); - defineParameterType(new ParameterType<>("biginteger", INTEGER_REGEXPS, BigInteger.class, new Transformer() { + defineParameterType(new ParameterType<>("biginteger", INTEGER_REGEXPS, BigInteger.class, new Transformer<>() { @Override - public BigInteger transform(String arg) throws Throwable { + public @Nullable BigInteger transform(@Nullable String arg) throws Throwable { return (BigInteger) internalParameterTransformer.transform(arg, BigInteger.class); } }, false, false, false)); - defineParameterType(new ParameterType<>("bigdecimal", localizedFloatRegexp, BigDecimal.class, new Transformer() { + defineParameterType(new ParameterType<>("bigdecimal", localizedFloatRegexp, BigDecimal.class, new Transformer<>() { @Override - public BigDecimal transform(String arg) throws Throwable { + public @Nullable BigDecimal transform(@Nullable String arg) throws Throwable { return (BigDecimal) internalParameterTransformer.transform(arg, BigDecimal.class); } }, false, false, false)); - defineParameterType(new ParameterType<>("byte", INTEGER_REGEXPS, Byte.class, new Transformer() { + defineParameterType(new ParameterType<>("byte", INTEGER_REGEXPS, Byte.class, new Transformer<>() { @Override - public Byte transform(String arg) throws Throwable { + public @Nullable Byte transform(@Nullable String arg) throws Throwable { return (Byte) internalParameterTransformer.transform(arg, Byte.class); } }, false, false, false)); - defineParameterType(new ParameterType<>("short", INTEGER_REGEXPS, Short.class, new Transformer() { + defineParameterType(new ParameterType<>("short", INTEGER_REGEXPS, Short.class, new Transformer<>() { @Override - public Short transform(String arg) throws Throwable { + public @Nullable Short transform(@Nullable String arg) throws Throwable { return (Short) internalParameterTransformer.transform(arg, Short.class); } }, false, false, false)); - defineParameterType(new ParameterType<>("int", INTEGER_REGEXPS, Integer.class, new Transformer() { + defineParameterType(new ParameterType<>("int", INTEGER_REGEXPS, Integer.class, new Transformer<>() { @Override - public Integer transform(String arg) throws Throwable { + public @Nullable Integer transform(@Nullable String arg) throws Throwable { return (Integer) internalParameterTransformer.transform(arg, Integer.class); } }, true, true, false)); - defineParameterType(new ParameterType<>("long", INTEGER_REGEXPS, Long.class, new Transformer() { + defineParameterType(new ParameterType<>("long", INTEGER_REGEXPS, Long.class, new Transformer<>() { @Override - public Long transform(String arg) throws Throwable { + public @Nullable Long transform(@Nullable String arg) throws Throwable { return (Long) internalParameterTransformer.transform(arg, Long.class); } }, false, false)); - defineParameterType(new ParameterType<>("float", localizedFloatRegexp, Float.class, new Transformer() { + defineParameterType(new ParameterType<>("float", localizedFloatRegexp, Float.class, new Transformer<>() { @Override - public Float transform(String arg) throws Throwable { + public @Nullable Float transform(@Nullable String arg) throws Throwable { return (Float) internalParameterTransformer.transform(arg, Float.class); } }, false, false)); - defineParameterType(new ParameterType<>("double", localizedFloatRegexp, Double.class, new Transformer() { + defineParameterType(new ParameterType<>("double", localizedFloatRegexp, Double.class, new Transformer<>() { @Override - public Double transform(String arg) throws Throwable { + public @Nullable Double transform(@Nullable String arg) throws Throwable { return (Double) internalParameterTransformer.transform(arg, Double.class); } }, true, true, false)); - defineParameterType(new ParameterType<>("word", WORD_REGEXPS, String.class, new Transformer() { + defineParameterType(new ParameterType<>("word", WORD_REGEXPS, String.class, new Transformer<>() { @Override - public String transform(String arg) throws Throwable { + public @Nullable String transform(@Nullable String arg) throws Throwable { return (String) internalParameterTransformer.transform(arg, String.class); } }, false, false, false)); defineParameterType(new ParameterType<>("string", STRING_REGEXPS, String.class, new CaptureGroupTransformer() { @Override - public String transform(String... args) throws Throwable { + public @Nullable String transform(@Nullable String[] args) throws Throwable { String arg = args[0] != null ? args[0] : args[1]; + if (arg == null) { + return null; + } return (String) internalParameterTransformer.transform(arg .replaceAll("\\\\\"", "\"") .replaceAll("\\\\'", "'"), @@ -147,7 +151,7 @@ public void defineParameterType(ParameterType parameterType) { for (String parameterTypeRegexp : parameterType.getRegexps()) { if (!parameterTypesByRegexp.containsKey(parameterTypeRegexp)) { - parameterTypesByRegexp.put(parameterTypeRegexp, new TreeSet>()); + parameterTypesByRegexp.put(parameterTypeRegexp, new TreeSet<>()); } SortedSet> parameterTypes = parameterTypesByRegexp.get(parameterTypeRegexp); if (!parameterTypes.isEmpty() && parameterTypes.first().preferForRegexpMatch() && parameterType.preferForRegexpMatch()) { @@ -169,11 +173,13 @@ public void setDefaultParameterTransformer(ParameterByTypeTransformer defaultPar this.defaultParameterTransformer = defaultParameterTransformer; } - ParameterType lookupByTypeName(String typeName) { + @SuppressWarnings("unchecked") + @Nullable ParameterType lookupByTypeName(String typeName) { return (ParameterType) parameterTypeByName.get(typeName); } - ParameterType lookupByRegexp(String parameterTypeRegexp, Pattern expressionRegexp, String text) { + @SuppressWarnings("unchecked") + @Nullable ParameterType lookupByRegexp(String parameterTypeRegexp, Pattern expressionRegexp, String text) { SortedSet> parameterTypes = parameterTypesByRegexp.get(parameterTypeRegexp); if (parameterTypes == null) return null; if (parameterTypes.size() > 1 && !parameterTypes.first().preferForRegexpMatch()) { diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/PatternCompiler.java b/java/src/main/java/io/cucumber/cucumberexpressions/PatternCompiler.java index 787813275..60874bf50 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/PatternCompiler.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/PatternCompiler.java @@ -15,9 +15,11 @@ public interface PatternCompiler { /** + * Returns a new {@link Pattern} instance from provided {@code regexp} + * * @param regexp regular expression * @param flags additional flags (e.g. {@link Pattern#UNICODE_CHARACTER_CLASS}) - * @return new {@link Pattern} instance from provided {@code regexp} + * @return a new pattern */ Pattern compile(String regexp, int flags); } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/PatternCompilerProvider.java b/java/src/main/java/io/cucumber/cucumberexpressions/PatternCompilerProvider.java index aee79158c..d7903a913 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/PatternCompilerProvider.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/PatternCompilerProvider.java @@ -1,5 +1,7 @@ package io.cucumber.cucumberexpressions; +import org.jspecify.annotations.Nullable; + import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -7,7 +9,7 @@ final class PatternCompilerProvider { // visible from tests - static PatternCompiler service; + static @Nullable PatternCompiler service; private PatternCompilerProvider() { } @@ -16,23 +18,23 @@ static synchronized PatternCompiler getCompiler() { if (service == null) { ServiceLoader loader = ServiceLoader.load(PatternCompiler.class); Iterator iterator = loader.iterator(); - findPatternCompiler(iterator); + service = findPatternCompiler(iterator); } return service; } - static void findPatternCompiler(Iterator iterator) { + static PatternCompiler findPatternCompiler(Iterator iterator) { if (iterator.hasNext()) { - service = iterator.next(); + PatternCompiler service = iterator.next(); if (iterator.hasNext()) { - throwMoreThanOneCompilerException(iterator); + throwMoreThanOneCompilerException(service, iterator); } - } else { - service = new DefaultPatternCompiler(); + return service; } + return new DefaultPatternCompiler(); } - private static void throwMoreThanOneCompilerException(Iterator iterator) { + private static void throwMoreThanOneCompilerException(PatternCompiler service, Iterator iterator) { List> allCompilers = new ArrayList<>(); allCompilers.add(service.getClass()); while (iterator.hasNext()) { diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/RegexpUtils.java b/java/src/main/java/io/cucumber/cucumberexpressions/RegexpUtils.java index 0c8c1767e..26ac3da04 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/RegexpUtils.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/RegexpUtils.java @@ -1,6 +1,6 @@ package io.cucumber.cucumberexpressions; -class RegexpUtils { +final class RegexpUtils { /** * List of characters to be escaped. * The last char is '}' with index 125, so we need only 126 characters. @@ -24,6 +24,10 @@ class RegexpUtils { CHAR_TO_ESCAPE['\\'] = true; } + private RegexpUtils(){ + // utility class + } + /** * Escapes the regexp characters (the ones from "^$(){}[].+*?\") * from the given text, so that they are not considered as regexp @@ -32,7 +36,7 @@ class RegexpUtils { * @param text the non-null input text * @return the input text with escaped regexp characters */ - public static String escapeRegex(String text) { + static String escapeRegex(String text) { /* Note on performance: this code has been benchmarked for escaping frequencies of 100%, 50%, 20%, 10%, 1%, 0.1%. @@ -40,7 +44,8 @@ public static String escapeRegex(String text) { this variant is the faster on all escaping frequencies. */ int length = text.length(); - StringBuilder sb = null; // lazy initialization + // lazy initialization + StringBuilder sb = null; int blockStart = 0; int maxChar = CHAR_TO_ESCAPE.length; for (int i = 0; i < length; i++) { diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java b/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java index 53c53f3a4..beff2fd20 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import java.lang.reflect.Type; import java.util.ArrayList; @@ -30,7 +31,7 @@ public final class RegularExpression implements Expression { } @Override - public List> match(String text, Type... typeHints) { + public @Nullable List> match(String text, Type... typeHints) { final Group group = treeRegexp.match(text); if (group == null) { return null; @@ -48,7 +49,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/Transformer.java b/java/src/main/java/io/cucumber/cucumberexpressions/Transformer.java index 687e36407..23c0f68ae 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/Transformer.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Transformer.java @@ -1,5 +1,7 @@ package io.cucumber.cucumberexpressions; +import org.jspecify.annotations.Nullable; + /** * Transformer for a @{@link ParameterType} with zero or one capture groups. * @@ -12,11 +14,11 @@ public interface Transformer { * from the sole capture group or matches the whole expression. Nested * capture groups are ignored. *

- * If the capture group is optional arg may be null. + * If the capture group is optional {@code arg} may be {@code null}. * * @param arg the value of the single capture group * @return the transformed object * @throws Throwable if transformation failed */ - T transform(String arg) throws Throwable; + @Nullable T transform(@Nullable String arg) throws Throwable; } diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/TreeRegexp.java b/java/src/main/java/io/cucumber/cucumberexpressions/TreeRegexp.java index 37e2f9830..3fc085f3f 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/TreeRegexp.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/TreeRegexp.java @@ -1,7 +1,8 @@ package io.cucumber.cucumberexpressions; +import org.jspecify.annotations.Nullable; + import java.util.ArrayDeque; -import java.util.Collections; import java.util.Deque; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -84,6 +85,7 @@ Pattern pattern() { return pattern; } + @Nullable Group match(CharSequence s) { final Matcher matcher = pattern.matcher(s); if (!matcher.matches()) @@ -91,7 +93,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/TypeReference.java b/java/src/main/java/io/cucumber/cucumberexpressions/TypeReference.java index 9486b2a71..83c553cf0 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/TypeReference.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/TypeReference.java @@ -15,7 +15,7 @@ protected TypeReference() { this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; } - public Type getType() { + public final Type getType() { return this.type; } } \ No newline at end of file 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/main/java/io/cucumber/cucumberexpressions/package-info.java b/java/src/main/java/io/cucumber/cucumberexpressions/package-info.java new file mode 100644 index 000000000..d7f8bd1c6 --- /dev/null +++ b/java/src/main/java/io/cucumber/cucumberexpressions/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package io.cucumber.cucumberexpressions; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/ArgumentTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/ArgumentTest.java index 0404d0250..cf91024f0 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/ArgumentTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/ArgumentTest.java @@ -6,16 +6,19 @@ import java.util.Locale; import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; public class ArgumentTest { @Test public void exposes_parameter_type() { TreeRegexp treeRegexp = new TreeRegexp("three (.*) mice"); + Group group = requireNonNull(treeRegexp.match("three blind mice")); + ParameterTypeRegistry parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); - List> arguments = Argument.build( - treeRegexp.match("three blind mice"), - singletonList(parameterTypeRegistry.lookupByTypeName("string"))); + List> parameterTypes = singletonList(parameterTypeRegistry.lookupByTypeName("string")); + + List> arguments = Argument.build(group, parameterTypes); Argument argument = arguments.get(0); assertEquals("string", argument.getParameterType().getName()); } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/BuiltInParameterTransformerTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/BuiltInParameterTransformerTest.java index fa36c5781..e49a212ed 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/BuiltInParameterTransformerTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/BuiltInParameterTransformerTest.java @@ -26,10 +26,10 @@ public void simple_object_mapper_only_supports_class_types() { Type abstractListOfE = ArrayList.class.getGenericSuperclass(); final Executable testMethod = () -> objectMapper.transform("something", abstractListOfE); - String expected = "" + - "Can't transform 'something' to java.util.AbstractList\n" + - "BuiltInParameterTransformer only supports a limited number of class types\n" + - "Consider using a different object mapper or register a parameter type for java.util.AbstractList"; + String expected = """ + Can't transform 'something' to java.util.AbstractList + BuiltInParameterTransformer only supports a limited number of class types + Consider using a different object mapper or register a parameter type for java.util.AbstractList"""; final IllegalArgumentException thrownException = assertThrows(IllegalArgumentException.class, testMethod); assertThat("Unexpected message", thrownException.getMessage(), is(equalTo(expected))); @@ -42,9 +42,10 @@ public void simple_object_mapper_only_supports_some_class_types() { final IllegalArgumentException thrownException = assertThrows(IllegalArgumentException.class, testMethod); assertThat("Unexpected message", thrownException.getMessage(), is(equalTo( - "Can't transform 'something' to class java.util.Date\n" + - "BuiltInParameterTransformer only supports a limited number of class types\n" + - "Consider using a different object mapper or register a parameter type for class java.util.Date" + """ + Can't transform 'something' to class java.util.Date + BuiltInParameterTransformer only supports a limited number of class types + Consider using a different object mapper or register a parameter type for class java.util.Date""" ))); } @@ -56,9 +57,10 @@ public void simple_object_mapper_only_supports_some_optional_types() { final IllegalArgumentException thrownException = assertThrows(IllegalArgumentException.class, testMethod); assertThat("Unexpected message", thrownException.getMessage(), is(equalTo( - "Can't transform 'something' to java.util.Optional\n" + - "BuiltInParameterTransformer only supports a limited number of class types\n" + - "Consider using a different object mapper or register a parameter type for java.util.Optional" + """ + Can't transform 'something' to java.util.Optional + BuiltInParameterTransformer only supports a limited number of class types + Consider using a different object mapper or register a parameter type for java.util.Optional""" ))); } @@ -70,9 +72,10 @@ public void simple_object_mapper_only_supports_some_generic_types() { final IllegalArgumentException thrownException = assertThrows(IllegalArgumentException.class, testMethod); assertThat("Unexpected message", thrownException.getMessage(), is(equalTo( - "Can't transform 'something' to java.util.function.Supplier\n" + - "BuiltInParameterTransformer only supports a limited number of class types\n" + - "Consider using a different object mapper or register a parameter type for java.util.function.Supplier" + """ + Can't transform 'something' to java.util.function.Supplier + BuiltInParameterTransformer only supports a limited number of class types + Consider using a different object mapper or register a parameter type for java.util.function.Supplier""" ))); } @@ -107,8 +110,10 @@ public void should_throw_exception_for_empty_string_with_type_char() { final IllegalArgumentException thrownException = assertThrows(IllegalArgumentException.class, testMethod); assertThat("Unexpected message", thrownException.getMessage(), is(equalTo( - "Can't transform '' to class java.lang.Character\nBuiltInParameterTransformer only supports a limited number of class types\n" + - "Consider using a different object mapper or register a parameter type for class java.lang.Character" + """ + Can't transform '' to class java.lang.Character + BuiltInParameterTransformer only supports a limited number of class types + Consider using a different object mapper or register a parameter type for class java.lang.Character""" ))); } @@ -118,8 +123,10 @@ public void should_throw_exception_for_nonsingelchar_string_with_type_char() { final IllegalArgumentException thrownException = assertThrows(IllegalArgumentException.class, testMethod); assertThat("Unexpected message", thrownException.getMessage(), is(equalTo( - "Can't transform 'ab' to class java.lang.Character\nBuiltInParameterTransformer only supports a limited number of class types\n" + - "Consider using a different object mapper or register a parameter type for class java.lang.Character" + """ + Can't transform 'ab' to class java.lang.Character + BuiltInParameterTransformer only supports a limited number of class types + Consider using a different object mapper or register a parameter type for class java.lang.Character""" ))); } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CombinatorialGeneratedExpressionFactoryTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CombinatorialGeneratedExpressionFactoryTest.java index d37292980..d9f538524 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CombinatorialGeneratedExpressionFactoryTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CombinatorialGeneratedExpressionFactoryTest.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.shadow.de.siegmar.fastcsv.util.Nullable; import java.util.ArrayList; import java.util.List; @@ -46,31 +47,31 @@ public void generates_multiple_expressions() { } public static class Color { - Color(String s) { + Color(@Nullable String s) { assertNotNull(s); } } public static class CssColor { - CssColor(String s) { + CssColor(@Nullable String s) { assertNotNull(s); } } public static class Date { - Date(String s) { + Date(@Nullable String s) { assertNotNull(s); } } public static class DateTime { - DateTime(String s) { + DateTime(@Nullable String s) { assertNotNull(s); } } public static class Timestamp { - Timestamp(String s) { + Timestamp(@Nullable String s) { assertNotNull(s); } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionGeneratorTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionGeneratorTest.java index 405024801..eaf540db0 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionGeneratorTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionGeneratorTest.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.shadow.de.siegmar.fastcsv.util.Nullable; import java.text.DateFormat; import java.text.ParseException; @@ -112,12 +113,7 @@ public void numbers_only_second_argument_when_type_is_not_reserved_keyword() { "currency", "[A-Z]{3}", Currency.class, - new Transformer() { - @Override - public Currency transform(String arg) { - return Currency.getInstance(arg); - } - } + (Transformer) Currency::getInstance )); assertExpression( "I have a {currency} account and a {currency} account", asList("currency", "currency2"), @@ -130,12 +126,7 @@ public void does_not_suggest_parameter_type_when_surrounded_by_alphanum() { "direction", "(up|down)", String.class, - new Transformer() { - @Override - public String transform(String arg) { - return arg; - } - }, + (Transformer) arg -> arg, true, false )); @@ -150,12 +141,7 @@ public void does_suggest_parameter_type_when_surrounded_by_space() { "direction", "(up|down)", String.class, - new Transformer() { - @Override - public String transform(String arg) { - return arg; - } - }, + (Transformer) arg -> arg, true, false )); @@ -176,12 +162,7 @@ public void prefers_leftmost_match_when_there_is_overlap() { "left", "b c", String.class, - new Transformer() { - @Override - public String transform(String arg) { - return arg; - } - } + (Transformer) arg -> arg )); assertExpression( "a {left} d e f g", singletonList("left"), @@ -221,9 +202,9 @@ public void generates_all_combinations_of_expressions_when_several_parameter_typ "date", "x", Date.class, - new Transformer() { + new Transformer<>() { @Override - public Date transform(String arg) { + public Date transform(@Nullable String arg) { try { return df.parse(arg); } catch (ParseException e) { diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java index 3221a59dd..c32fd3c93 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 org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.converter.ArgumentConversionException; @@ -14,13 +15,17 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static java.nio.file.Files.newDirectoryStream; import static java.nio.file.Files.newInputStream; +import static java.util.Objects.requireNonNull; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -28,9 +33,11 @@ class CucumberExpressionParserTest { private final CucumberExpressionParser parser = new CucumberExpressionParser(); - private static List acceptance_tests_pass() throws IOException { + static List acceptance_tests_pass() throws IOException { List paths = new ArrayList<>(); - newDirectoryStream(Paths.get("..", "testdata", "cucumber-expression", "parser")).forEach(paths::add); + try (var directories = newDirectoryStream(Paths.get("..", "testdata", "cucumber-expression", "parser"))) { + directories.forEach(paths::add); + } paths.sort(Comparator.naturalOrder()); return paths; } @@ -40,7 +47,7 @@ private static List acceptance_tests_pass() throws IOException { void acceptance_tests_pass(@ConvertWith(Converter.class) Expectation expectation) { if (expectation.exception == null) { Node node = parser.parse(expectation.expression); - assertThat(node, is(expectation.expected_ast.toNode())); + assertThat(node, equalTo(expectation.expectedAst)); } else { CucumberExpressionException exception = assertThrows( CucumberExpressionException.class, @@ -49,40 +56,57 @@ void acceptance_tests_pass(@ConvertWith(Converter.class) Expectation expectation } } - static class Expectation { - public String expression; - public YamlableNode expected_ast; - public String exception; - } - + @NullMarked static class Converter implements ArgumentConverter { Yaml yaml = new Yaml(); @Override - public Expectation convert(Object source, ParameterContext context) throws ArgumentConversionException { + public Expectation convert(@Nullable Object source, ParameterContext context) throws ArgumentConversionException { + if (source == null) { + throw new ArgumentConversionException("Could not load null"); + } try { Path path = (Path) source; InputStream inputStream = newInputStream(path); - return yaml.loadAs(inputStream, Expectation.class); + Map expectation = yaml.loadAs(inputStream, Map.class); + return new Expectation( + (String) requireNonNull(expectation.get("expression")), + convertExpectedAst(expectation), + (String) expectation.get("exception") + ); } catch (IOException e) { throw new ArgumentConversionException("Could not load " + source, e); } } - } - static class YamlableNode { - public Ast.Node.Type type; - public List nodes; - public String token; - public int start; - public int end; + @SuppressWarnings("unchecked") + private @Nullable Node convertExpectedAst(Map expectation) { + var expectedAst = (Map) expectation.get("expected_ast"); + return expectedAst == null ? null : convertNode(expectedAst); + } - public Node toNode() { + private Node convertNode(Map expectation){ + var type = Node.Type.valueOf((String) requireNonNull(expectation.get("type"))); + var nodes = getNodes(expectation).stream().map(this::convertNode).collect(Collectors.toList()); + var token = (String) expectation.get("token"); + var start = (int) requireNonNull(expectation.get("start")); + var end = (int) requireNonNull(expectation.get("end")); if (token != null) { return new Node(type, start, end, token); } else { - return new Node(type, start, end, nodes.stream().map(YamlableNode::toNode).collect(Collectors.toList())); + return new Node(type, start, end, nodes); } } + + @SuppressWarnings("unchecked") + private List> getNodes(Map expectation) { + var nodes = expectation.get("nodes"); + return nodes != null ? (List>) nodes : Collections.emptyList(); + } + } + + record Expectation(String expression, @Nullable Node expectedAst, @Nullable String exception) { + } + } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java index d9554df54..634c5bb40 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java @@ -1,5 +1,7 @@ package io.cucumber.cucumberexpressions; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.function.Executable; @@ -15,30 +17,40 @@ 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; +import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.stream.Collectors; import static java.nio.file.Files.newDirectoryStream; import static java.nio.file.Files.newInputStream; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.core.Is.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +@NullMarked class CucumberExpressionTest { private final ParameterTypeRegistry parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); - private static List acceptance_tests_pass() throws IOException { + 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; } @@ -48,44 +60,27 @@ private static List acceptance_tests_pass() throws IOException { void acceptance_tests_pass(@ConvertWith(Converter.class) Expectation expectation) { if (expectation.exception == null) { CucumberExpression expression = new CucumberExpression(expectation.expression, parameterTypeRegistry); - List> match = expression.match(expectation.text); + List> match = expression.match(requireNonNull(expectation.text)); List values = match == null ? null : match.stream() .map(Argument::getValue) .collect(Collectors.toList()); - assertThat(values, CustomMatchers.equalOrCloseTo(expectation.expected_args)); + if (expectation.expectedArgs == null) { + assertThat(values, nullValue()); + } else { + assertThat(values, CustomMatchers.equalOrCloseTo(expectation.expectedArgs)); + } } else { Executable executable = () -> { CucumberExpression expression = new CucumberExpression(expectation.expression, parameterTypeRegistry); - expression.match(expectation.text); + if (expectation.text != null) { + expression.match(expectation.text); + } }; CucumberExpressionException exception = assertThrows(CucumberExpressionException.class, executable); assertThat(exception.getMessage(), equalTo(expectation.exception)); } } - - static class Expectation { - public String expression; - public String text; - public List expected_args; - public String exception; - } - - static class Converter implements ArgumentConverter { - Yaml yaml = new Yaml(); - - @Override - public Expectation convert(Object source, ParameterContext context) throws ArgumentConversionException { - try { - Path path = (Path) source; - InputStream inputStream = newInputStream(path); - return yaml.loadAs(inputStream, Expectation.class); - } catch (IOException e) { - throw new ArgumentConversionException("Could not load " + source, e); - } - } - } - // Misc tests @Test @@ -105,6 +100,7 @@ void documents_match_arguments() { String expr = "I have {int} cuke(s)"; Expression expression = new CucumberExpression(expr, parameterTypeRegistry); List> args = expression.match("I have 7 cukes"); + assertNotNull(args); assertEquals(7, args.get(0).getValue()); } @@ -145,7 +141,7 @@ void matches_double_with_comma_for_locale_using_comma() { @Test void matches_float_with_zero() { List values = match("{float}", "0", Locale.ENGLISH); - assertEquals(0.0f, values.get(0)); + assertEquals(singletonList(0.0f), values); } @Test @@ -158,8 +154,8 @@ void unmatched_optional_groups_have_null_values() { }.getType(), new CaptureGroupTransformer>() { @Override - public List transform(String... args) { - return asList(args); + public List transform(@Nullable String[] args) { + return Arrays.asList(args); } }, false, @@ -169,15 +165,18 @@ public List transform(String... args) { assertThat(match("{textAndOrNumber}", "123", parameterTypeRegistry), is(singletonList(asList(null, "123")))); } + @Nullable private List match(String expr, String text, Type... typeHints) { return match(expr, text, parameterTypeRegistry, typeHints); } + @Nullable private List match(String expr, String text, Locale locale, Type... typeHints) { ParameterTypeRegistry parameterTypeRegistry = new ParameterTypeRegistry(locale); return match(expr, text, parameterTypeRegistry, typeHints); } + @Nullable private List match(String expr, String text, ParameterTypeRegistry parameterTypeRegistry, Type... typeHints) { CucumberExpression expression = new CucumberExpression(expr, parameterTypeRegistry); List> args = expression.match(text, typeHints); @@ -192,4 +191,35 @@ private List match(String expr, String text, ParameterTypeRegistry parameterT return list; } } + + + public record Expectation(String expression, @Nullable String text, @Nullable List expectedArgs, + @Nullable String exception) { + } + + @NullMarked + static class Converter implements ArgumentConverter { + Yaml yaml = new Yaml(); + + @Override + public Expectation convert(@Nullable Object source, ParameterContext context) throws ArgumentConversionException { + if (source == null) { + throw new ArgumentConversionException("Could not load null"); + } + + try { + Path path = (Path) source; + InputStream inputStream = newInputStream(path); + Map document = yaml.loadAs(inputStream, Map.class); + return new Expectation( + (String) requireNonNull(document.get("expression")), + (String) document.get("text"), + (List) document.get("expected_args"), + (String) document.get("exception") + ); + } catch (IOException e) { + throw new ArgumentConversionException("Could not load " + source, e); + } + } + } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java index 11c0cef97..082645daf 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java @@ -1,6 +1,8 @@ package io.cucumber.cucumberexpressions; import io.cucumber.cucumberexpressions.Ast.Token; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.converter.ArgumentConversionException; @@ -11,15 +13,18 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.file.DirectoryStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static java.nio.file.Files.newDirectoryStream; import static java.nio.file.Files.newInputStream; +import static java.util.Objects.requireNonNull; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -28,9 +33,11 @@ class CucumberExpressionTokenizerTest { private final CucumberExpressionTokenizer tokenizer = new CucumberExpressionTokenizer(); - private static List acceptance_tests_pass() throws IOException { + static List acceptance_tests_pass() throws IOException { List paths = new ArrayList<>(); - newDirectoryStream(Paths.get("..", "testdata", "cucumber-expression", "tokenizer")).forEach(paths::add); + try (DirectoryStream directories = newDirectoryStream(Paths.get("..", "testdata", "cucumber-expression", "tokenizer"))) { + directories.forEach(paths::add); + } paths.sort(Comparator.naturalOrder()); return paths; } @@ -40,11 +47,7 @@ private static List acceptance_tests_pass() throws IOException { void acceptance_tests_pass(@ConvertWith(Converter.class) Expectation expectation) { if (expectation.exception == null) { List tokens = tokenizer.tokenize(expectation.expression); - List expectedTokens = expectation.expected_tokens - .stream() - .map(YamlableToken::toToken) - .collect(Collectors.toList()); - assertThat(tokens, is(expectedTokens)); + assertThat(tokens, is(expectation.expectedTokens)); } else { CucumberExpressionException exception = assertThrows( CucumberExpressionException.class, @@ -53,35 +56,45 @@ void acceptance_tests_pass(@ConvertWith(Converter.class) Expectation expectation } } - static class Expectation { - public String expression; - public List expected_tokens; - public String exception; - } - + @NullMarked static class Converter implements ArgumentConverter { Yaml yaml = new Yaml(); @Override - public Expectation convert(Object source, ParameterContext context) throws ArgumentConversionException { + public Expectation convert(@Nullable Object source, ParameterContext context) throws ArgumentConversionException { + if (source == null) { + throw new ArgumentConversionException("Could not load null"); + } try { Path path = (Path) source; InputStream inputStream = newInputStream(path); - return yaml.loadAs(inputStream, Expectation.class); + Map expectation = yaml.loadAs(inputStream, Map.class); + return new Expectation( + (String) requireNonNull(expectation.get("expression")), + convertExpectedTokens(expectation), + (String) expectation.get("exception") + ); } catch (IOException e) { throw new ArgumentConversionException("Could not load " + source, e); } } - } - static class YamlableToken { - public String text; - public Token.Type type; - public int start; - public int end; + @SuppressWarnings("unchecked") + private @Nullable List convertExpectedTokens(Map expectation) { + var expectedAst = (List>) expectation.get("expected_tokens"); + return expectedAst == null ? null : expectedAst.stream().map(this::convertToken).collect(Collectors.toList()); + } - public Token toToken() { - return new Token(text, type, start, end); + private Token convertToken(Map expectation) { + var token = (String) requireNonNull(expectation.get("text")); + var type = Token.Type.valueOf((String) requireNonNull(expectation.get("type"))); + var start = (int) requireNonNull(expectation.get("start")); + var end = (int) requireNonNull(expectation.get("end")); + return new Token(token, type, start, end); } + + } + + record Expectation(String expression, @Nullable List expectedTokens, @Nullable String exception) { } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTransformationTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTransformationTest.java index a4c86c75c..a260281a8 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTransformationTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTransformationTest.java @@ -1,5 +1,7 @@ package io.cucumber.cucumberexpressions; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.converter.ArgumentConversionException; @@ -10,23 +12,28 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.file.DirectoryStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Map; import static java.nio.file.Files.newDirectoryStream; import static java.nio.file.Files.newInputStream; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; class CucumberExpressionTransformationTest { private final ParameterTypeRegistry parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); - private static List acceptance_tests_pass() throws IOException { + static List acceptance_tests_pass() throws IOException { List paths = new ArrayList<>(); - newDirectoryStream(Paths.get("..", "testdata", "cucumber-expression", "transformation")).forEach(paths::add); + try(DirectoryStream directories = newDirectoryStream(Paths.get("..", "testdata", "cucumber-expression", "transformation"))) { + directories.forEach(paths::add); + } paths.sort(Comparator.naturalOrder()); return paths; } @@ -35,23 +42,28 @@ private static List acceptance_tests_pass() throws IOException { @MethodSource void acceptance_tests_pass(@ConvertWith(Converter.class) Expectation expectation) { CucumberExpression expression = new CucumberExpression(expectation.expression, parameterTypeRegistry); - assertEquals(expectation.expected_regex, expression.getRegexp().pattern()); + assertEquals(expectation.expectedRegex, expression.getRegexp().pattern()); } - static class Expectation { - public String expression; - public String expected_regex; + record Expectation(String expression, String expectedRegex) { } + @NullMarked static class Converter implements ArgumentConverter { Yaml yaml = new Yaml(); @Override - public Expectation convert(Object source, ParameterContext context) throws ArgumentConversionException { + public Expectation convert(@Nullable Object source, ParameterContext context) throws ArgumentConversionException { + if (source == null) { + throw new ArgumentConversionException("Could not load null"); + } try { Path path = (Path) source; InputStream inputStream = newInputStream(path); - return yaml.loadAs(inputStream, Expectation.class); + Map expectation = yaml.loadAs(inputStream, Map.class); + return new Expectation( + (String) requireNonNull(expectation.get("expression")), + (String) requireNonNull(expectation.get("expected_regex"))); } catch (IOException e) { throw new ArgumentConversionException("Could not load " + source, e); } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CustomMatchers.java b/java/src/test/java/io/cucumber/cucumberexpressions/CustomMatchers.java index ce1d296aa..c1f6b31cf 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CustomMatchers.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CustomMatchers.java @@ -14,7 +14,13 @@ import static org.hamcrest.Matchers.equalTo; -public class CustomMatchers { +final class CustomMatchers { + + private CustomMatchers(){ + // utility class + } + + @SuppressWarnings({"unchecked", "rawtypes"}) public static Matcher> equalOrCloseTo(List list) { if (list == null || list.isEmpty()) return equalTo(list); List> matchers = list.stream().map(EqualOrCloseTo::new).collect(Collectors.toList()); @@ -24,10 +30,11 @@ public static Matcher> equalOrCloseTo(List list) { private static class EqualOrCloseTo extends BaseMatcher { private final Object expectedValue; - public EqualOrCloseTo(Object expectedValue) { + EqualOrCloseTo(Object expectedValue) { this.expectedValue = expectedValue; } + @SuppressWarnings({"rawtypes", "unchecked"}) @Override public boolean matches(Object actual) { if(actual instanceof BigDecimal) { @@ -35,7 +42,7 @@ public boolean matches(Object actual) { } else if(actual instanceof BigInteger) { return new IsEqual(this.expectedValue).matches(actual.toString()); } else if(actual instanceof Double || actual instanceof Float) { - return new IsCloseTo(((Double)this.expectedValue), 0.0001).matches(((Number)actual).doubleValue()); + return new IsCloseTo((Double)this.expectedValue, 0.0001).matches(((Number)actual).doubleValue()); } else if(actual instanceof Byte) { return new IsEqual(((Integer)this.expectedValue).byteValue()).matches(actual); } else if(actual instanceof Short) { diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java index 81eb19cea..0962986e1 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java @@ -1,5 +1,6 @@ package io.cucumber.cucumberexpressions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; @@ -10,298 +11,241 @@ import static java.lang.Integer.parseInt; import static java.util.Arrays.asList; +import static java.util.Objects.requireNonNull; import static java.util.regex.Pattern.compile; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.fail; -public class CustomParameterTypeTest { +class CustomParameterTypeTest { private ParameterTypeRegistry parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); - public static class Coordinate { - private final int x; - private final int y; - private final int z; - - Coordinate(int x, int y, int z) { - this.x = x; - this.y = y; - this.z = z; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Coordinate that = (Coordinate) o; - return x == that.x && y == that.y && z == that.z; - } - - @Override - public int hashCode() { - int result = x; - result = 31 * result + y; - result = 31 * result + z; - return result; - } - } - @BeforeEach - public void create_parameter() { + @SuppressWarnings("TrailingComment") + void create_parameter() { parameterTypeRegistry.defineParameterType(new ParameterType<>( - "color", // name - "red|blue|yellow", // regexp - Color.class, // type - Color::new, // transform - false, // useForSnippets - false // preferForRegexpMatch + "color", // name + "red|blue|yellow", // regexp + Color.class, // type + (@Nullable String name) -> new Color(requireNonNull(name)), // transform + false, // useForSnippets + false // preferForRegexpMatch )); } @Test - public void throws_exception_for_illegal_character_in_parameter_name() { - - final Executable testMethod = () -> new ParameterType<>( + void throws_exception_for_illegal_character_in_parameter_name() { + Executable testMethod = () -> new ParameterType<>( "(string)", ".*", String.class, - (Transformer) s -> s, + (@Nullable String s) -> s, false, false ); - final CucumberExpressionException thrownException = assertThrows(CucumberExpressionException.class, testMethod); - assertThat(thrownException.getMessage(), is(equalTo("Illegal character in parameter name {(string)}. Parameter names may not contain '{', '}', '(', ')', '\\' or '/'"))); + var exception = assertThrows(CucumberExpressionException.class, testMethod); + assertThat(exception).hasMessage("Illegal character in parameter name {(string)}. Parameter names may not contain '{', '}', '(', ')', '\\' or '/'"); } @Test - public void matches_CucumberExpression_parameters_with_custom_parameter_type() { - Expression expression = new CucumberExpression("I have a {color} ball", parameterTypeRegistry); - Object argumentValue = expression.match("I have a red ball").get(0).getValue(); - assertEquals(new Color("red"), argumentValue); + void matches_CucumberExpression_parameters_with_custom_parameter_type() { + var expression = new CucumberExpression("I have a {color} ball", parameterTypeRegistry); + var arguments = expression.match("I have a red ball"); + + assertThat(arguments).singleElement() + .extracting(Argument::getValue) + .isEqualTo(new Color("red")); } @Test - public void matches_CucumberExpression_parameters_with_multiple_capture_groups() { + void matches_CucumberExpression_parameters_with_multiple_capture_groups() { parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); parameterTypeRegistry.defineParameterType(new ParameterType<>( "coordinate", "(\\d+),\\s*(\\d+),\\s*(\\d+)", Coordinate.class, - new CaptureGroupTransformer() { - @Override - public Coordinate transform(String[] args) { - return new Coordinate( - parseInt(args[0]), - parseInt(args[1]), - parseInt(args[2])); - } - }, + (@Nullable String[] args) -> new Coordinate(parseInt(requireNonNull(args[0])), parseInt(requireNonNull(args[1])), parseInt(requireNonNull(args[2]))), false, false )); - Expression expression = new CucumberExpression("A {int} thick line from {coordinate} to {coordinate}", parameterTypeRegistry); - List> arguments = expression.match("A 5 thick line from 10,20,30 to 40,50,60"); - Integer thick = (Integer) arguments.get(0).getValue(); - Coordinate from = (Coordinate) arguments.get(1).getValue(); - Coordinate to = (Coordinate) arguments.get(2).getValue(); - assertEquals(Integer.valueOf(5), thick); - assertEquals(new Coordinate(10, 20, 30), from); - assertEquals(new Coordinate(40, 50, 60), to); + var expression = new CucumberExpression("A {int} thick line from {coordinate} to {coordinate}", parameterTypeRegistry); + var arguments = expression.match("A 5 thick line from 10,20,30 to 40,50,60"); + + assertThat(arguments) + .extracting(Argument::getValue) + .map(Object.class::cast) + .containsExactly( + 5, + new Coordinate(10, 20, 30), + new Coordinate(40, 50, 60) + ); } @Test - public void warns_when_CucumberExpression_parameters_with_multiple_capture_groups_has_a_transformer() { + void warns_when_CucumberExpression_parameters_with_multiple_capture_groups_has_a_transformer() { parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); parameterTypeRegistry.defineParameterType(new ParameterType<>( "coordinate", "(\\d+),\\s*(\\d+),\\s*(\\d+)", Coordinate.class, - new Transformer() { - @Override - public Coordinate transform(String args) { - throw new IllegalStateException(); - } + (@Nullable String arg) -> { + throw new IllegalStateException(); }, false, false )); - Expression expression = new CucumberExpression("A {int} thick line from {coordinate} to {coordinate}", parameterTypeRegistry); - List> arguments = expression.match("A 5 thick line from 10,20,30 to 40,50,60"); - - arguments.get(0).getValue(); - - final Executable testMethod = () -> arguments.get(1).getValue(); - - final CucumberExpressionException thrownException = assertThrows(CucumberExpressionException.class, testMethod); - assertThat("Unexpected message", thrownException.getMessage(), is(equalTo( + var expression = new CucumberExpression("A {int} thick line from {coordinate} to {coordinate}", parameterTypeRegistry); + var arguments = expression.match("A 5 thick line from 10,20,30 to 40,50,60"); + + assertDoesNotThrow(() -> { + requireNonNull(arguments).get(0).getValue(); + }); + var exception = assertThrows(CucumberExpressionException.class, () -> requireNonNull(arguments).get(1).getValue()); + assertThat(exception).hasMessage( "ParameterType {coordinate} was registered with a Transformer but has multiple capture groups [(\\d+),\\s*(\\d+),\\s*(\\d+)]. " + "Did you mean to use a CaptureGroupTransformer?" - ))); + ); } @Test - public void warns_when_anonymous_parameter_has_multiple_capture_groups() { + void warns_when_anonymous_parameter_has_multiple_capture_groups() { parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); Expression expression = new RegularExpression(Pattern.compile("^A (\\d+) thick line from ((\\d+),\\s*(\\d+),\\s*(\\d+)) to ((\\d+),\\s*(\\d+),\\s*(\\d+))$"), parameterTypeRegistry); List> arguments = expression.match("A 5 thick line from 10,20,30 to 40,50,60", Integer.class, Coordinate.class, Coordinate.class); - arguments.get(0).getValue(); - - final Executable testMethod = () -> arguments.get(1).getValue(); + assertNotNull(arguments); + assertDoesNotThrow(() -> { + arguments.get(0).getValue(); + }); - final CucumberExpressionException thrownException = assertThrows(CucumberExpressionException.class, testMethod); - assertThat("Unexpected message", thrownException.getMessage(), is(equalTo( + var exception = assertThrows(CucumberExpressionException.class, () -> arguments.get(1).getValue()); + assertThat(exception).hasMessage( "Anonymous ParameterType has multiple capture groups [(\\d+),\\s*(\\d+),\\s*(\\d+)]. " + "You can only use a single capture group in an anonymous ParameterType." - ))); + ); } @Test - public void matches_CucumberExpression_parameters_with_custom_parameter_type_using_optional_group() { + void matches_CucumberExpression_parameters_with_custom_parameter_type_using_optional_group() { parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); parameterTypeRegistry.defineParameterType(new ParameterType<>( "color", asList("red|blue|yellow", "(?:dark|light) (?:red|blue|yellow)"), Color.class, - Color::new, + (@Nullable String name) -> new Color(requireNonNull(name)), false, false )); - Expression expression = new CucumberExpression("I have a {color} ball", parameterTypeRegistry); - Object argumentValue = expression.match("I have a dark red ball").get(0).getValue(); - assertEquals(new Color("dark red"), argumentValue); + var expression = new CucumberExpression("I have a {color} ball", parameterTypeRegistry); + var match = expression.match("I have a dark red ball"); + + assertThat(match).singleElement() + .extracting(Argument::getValue) + .isEqualTo(new Color("dark red")); } @Test - public void defers_transformation_until_queried_from_argument() { + void defers_transformation_until_queried_from_argument() { parameterTypeRegistry.defineParameterType(new ParameterType<>( "throwing", "bad", CssColor.class, - new Transformer() { - @Override - public CssColor transform(String arg) { - throw new RuntimeException(String.format("Can't transform [%s]", arg)); - } + (@Nullable String arg) -> { + throw new RuntimeException(String.format("Can't transform [%s]", arg)); }, false, false )); - Expression expression = new CucumberExpression("I have a {throwing} parameter", parameterTypeRegistry); - List> arguments = expression.match("I have a bad parameter"); - try { - arguments.get(0).getValue(); - fail("should have failed"); - } catch (RuntimeException expected) { - assertEquals("ParameterType {throwing} failed to transform [bad] to " + CssColor.class, expected.getMessage()); - } + var expression = new CucumberExpression("I have a {throwing} parameter", parameterTypeRegistry); + var arguments = expression.match("I have a bad parameter"); + + var exception = assertThrows(RuntimeException.class, () -> requireNonNull(arguments).get(0).getValue()); + assertThat(exception).hasMessage("ParameterType {throwing} failed to transform [bad] to " + CssColor.class, exception.getMessage()); } @Test - public void conflicting_parameter_type_is_detected_for_type_name() { - try { - parameterTypeRegistry.defineParameterType(new ParameterType<>( - "color", - ".*", - CssColor.class, - CssColor::new, - false, - false - )); - fail("should have failed"); - } catch (DuplicateTypeNameException expected) { - assertEquals("There is already a parameter type with name color", expected.getMessage()); - } + void conflicting_parameter_type_is_detected_for_type_name() { + var exception = assertThrows(DuplicateTypeNameException.class, () -> + parameterTypeRegistry.defineParameterType(new ParameterType<>( + "color", + ".*", + CssColor.class, + (@Nullable String name) -> new CssColor(requireNonNull(name)), + false, + false + ))); + + assertThat(exception).hasMessage("There is already a parameter type with name color"); } @Test - public void conflicting_parameter_type_is_not_detected_for_type() { + void conflicting_parameter_type_is_not_detected_for_type() { parameterTypeRegistry.defineParameterType(new ParameterType<>( "whatever", ".*", Color.class, - Color::new, + (@Nullable String name) -> new Color(requireNonNull(name)), false, false )); } - ///// Conflicting parameter types - @Test - public void conflicting_parameter_type_is_not_detected_for_regexp() { + void conflicting_parameter_type_is_not_detected_for_regexp() { parameterTypeRegistry.defineParameterType(new ParameterType<>( "css-color", "red|blue|yellow", CssColor.class, - CssColor::new, + (@Nullable String name) -> new CssColor(requireNonNull(name)), false, false )); - assertEquals(new CssColor("blue"), new CucumberExpression("I have a {css-color} ball", parameterTypeRegistry).match("I have a blue ball").get(0).getValue()); - assertEquals(new Color("blue"), new CucumberExpression("I have a {color} ball", parameterTypeRegistry).match("I have a blue ball").get(0).getValue()); + var cssColorMatch = new CucumberExpression("I have a {css-color} ball", parameterTypeRegistry).match("I have a blue ball"); + assertThat(cssColorMatch).singleElement() + .extracting(Argument::getValue) + .isEqualTo(new CssColor("blue")); + + var colorMatch = new CucumberExpression("I have a {color} ball", parameterTypeRegistry).match("I have a blue ball"); + assertThat(colorMatch).singleElement() + .extracting(Argument::getValue) + .isEqualTo(new Color("blue")); + } @Test - public void matches_RegularExpression_arguments_with_custom_parameter_type_without_name() { + void matches_RegularExpression_arguments_with_custom_parameter_type_without_name() { parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); parameterTypeRegistry.defineParameterType(new ParameterType<>( - null, + "null", "red|blue|yellow", Color.class, - Color::new, + (@Nullable String name) -> new Color(requireNonNull(name)), false, false )); - Expression expression = new RegularExpression(compile("I have a (red|blue|yellow) ball"), parameterTypeRegistry); - Object argumentValue = expression.match("I have a red ball").get(0).getValue(); - assertEquals(new Color("red"), argumentValue); + var expression = new RegularExpression(compile("I have a (red|blue|yellow) ball"), parameterTypeRegistry); + var match = expression.match("I have a red ball"); + assertThat(match).singleElement() + .extracting(Argument::getValue) + .isEqualTo(new Color("red")); } - ///// RegularExpression - - public static class Color { - final String name; - - Color(String name) { - this.name = name; - } + private record Coordinate(int x, int y, int z) { - @Override - public int hashCode() { - return name.hashCode(); - } - - @Override - public boolean equals(Object obj) { - return obj instanceof Color && ((Color) obj).name.equals(name); - } } - public static class CssColor { - final String name; + record Color(String name) { - CssColor(String name) { - this.name = name; - } + } - @Override - public int hashCode() { - return name.hashCode(); - } + record CssColor(String name) { - @Override - public boolean equals(Object obj) { - return obj instanceof CssColor && ((CssColor) obj).name.equals(name); - } } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/EnumParameterTypeTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/EnumParameterTypeTest.java index 7826699ca..8ff3fb38a 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/EnumParameterTypeTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/EnumParameterTypeTest.java @@ -2,12 +2,11 @@ import org.junit.jupiter.api.Test; -import java.util.List; import java.util.Locale; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; -public class EnumParameterTypeTest { +class EnumParameterTypeTest { public enum Mood { happy, @@ -16,13 +15,15 @@ public enum Mood { } @Test - public void converts_to_enum() { - ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH); + void converts_to_enum() { + var registry = new ParameterTypeRegistry(Locale.ENGLISH); registry.defineParameterType(ParameterType.fromEnum(Mood.class)); - CucumberExpression expression = new CucumberExpression("I am {Mood}", registry); - List> args = expression.match("I am happy"); - assertEquals(Mood.happy, args.get(0).getValue()); + var expression = new CucumberExpression("I am {Mood}", registry); + var args = expression.match("I am happy"); + assertThat(args).singleElement() + .extracting(Argument::getValue) + .isEqualTo(Mood.happy); } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/ExpressionFactoryTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/ExpressionFactoryTest.java index 52169139e..934488074 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/ExpressionFactoryTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/ExpressionFactoryTest.java @@ -5,7 +5,6 @@ import java.util.Locale; -import static java.util.Collections.singleton; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsEqual.equalTo; diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/GenericParameterTypeTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/GenericParameterTypeTest.java index 29032298d..3458f3fbf 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/GenericParameterTypeTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/GenericParameterTypeTest.java @@ -1,5 +1,6 @@ package io.cucumber.cucumberexpressions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import java.util.List; @@ -7,7 +8,8 @@ import static java.util.Arrays.asList; import static java.util.Collections.singletonList; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; public class GenericParameterTypeTest { @@ -17,20 +19,16 @@ public void transforms_to_a_list_of_string() { parameterTypeRegistry.defineParameterType(new ParameterType<>( "stringlist", singletonList(".*"), - new TypeReference>() { - }.getType(), - new CaptureGroupTransformer>() { - @Override - public List transform(String... args) { - return asList(args[0].split(",")); - } - }, + new TypeReference>() {}.getType(), + (@Nullable String arg) -> asList(requireNonNull(arg).split(",")), false, false) ); - Expression expression = new CucumberExpression("I have {stringlist} yay", parameterTypeRegistry); - List> args = expression.match("I have three,blind,mice yay"); - assertEquals(asList("three", "blind", "mice"), args.get(0).getValue()); + var expression = new CucumberExpression("I have {stringlist} yay", parameterTypeRegistry); + var args = expression.match("I have three,blind,mice yay"); + assertThat(args).singleElement() + .extracting(Argument::getValue) + .isEqualTo(asList("three", "blind", "mice")); } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/KeyboardFriendlyDecimalFormatSymbolsTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/KeyboardFriendlyDecimalFormatSymbolsTest.java index 7c37f8a13..1b09366c3 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/KeyboardFriendlyDecimalFormatSymbolsTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/KeyboardFriendlyDecimalFormatSymbolsTest.java @@ -12,7 +12,6 @@ import static java.util.Comparator.comparing; import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toList; class KeyboardFriendlyDecimalFormatSymbolsTest { @@ -51,7 +50,7 @@ private static void listDecimalAndGroupingSeparators(Function entry.getKey().getKey())) - .forEach((entry) -> { + .forEach(entry -> { SimpleEntry characters = entry.getKey(); List locales = entry.getValue(); System.out.println(render(characters.getKey()) + " " + render(characters.getValue()) + " " + render(locales)); @@ -92,7 +91,7 @@ private static String render(List locales) { return locales.size() + ": " + locales.stream() .sorted(comparing(Locale::getDisplayName)) .map(Locale::getDisplayName) - .collect(toList()); + .toList(); } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/NumberParserTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/NumberParserTest.java index 5f3ca4ac3..5987c9e78 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/NumberParserTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/NumberParserTest.java @@ -29,12 +29,11 @@ void can_parse_float() { @Test void can_parse_double() { - assertEquals(1042.000000000000002, english.parseDouble("1,042.000000000000002"), 0); - assertEquals(1042.000000000000002, canadian.parseDouble("1,042.000000000000002"), 0); - - assertEquals(1042.000000000000002, german.parseDouble("1.042,000000000000002"), 0); - assertEquals(1042.000000000000002, canadianFrench.parseDouble("1.042,000000000000002"), 0); - assertEquals(1042.000000000000002, norwegian.parseDouble("1.042,000000000000002"), 0); + assertEquals(Double.parseDouble("1042.000000000000002"), english.parseDouble("1,042.000000000000002"), 0); + assertEquals(Double.parseDouble("1042.000000000000002"), canadian.parseDouble("1,042.000000000000002"), 0); + assertEquals(Double.parseDouble("1042.000000000000002"), german.parseDouble("1.042,000000000000002"), 0); + assertEquals(Double.parseDouble("1042.000000000000002"), canadianFrench.parseDouble("1.042,000000000000002"), 0); + assertEquals(Double.parseDouble("1042.000000000000002"), norwegian.parseDouble("1.042,000000000000002"), 0); } @Test diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/ParameterByTypeTransformerTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/ParameterByTypeTransformerTest.java index 39494b1d4..bb4dcedc9 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/ParameterByTypeTransformerTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/ParameterByTypeTransformerTest.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -16,7 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -public class ParameterByTypeTransformerTest { +class ParameterByTypeTransformerTest { static Stream objectMapperImplementations() { return Stream.of( @@ -27,19 +29,19 @@ static Stream objectMapperImplementations() { @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_null_to_null(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_null_to_null(final ParameterByTypeTransformer defaultTransformer) throws Throwable { assertNull(defaultTransformer.transform(null, Object.class)); } @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_null_to_optional(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_null_to_optional(final ParameterByTypeTransformer defaultTransformer) throws Throwable { assertEquals(Optional.empty(), defaultTransformer.transform(null, Optional.class)); } @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_null_to_optional_generic(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_null_to_optional_generic(final ParameterByTypeTransformer defaultTransformer) throws Throwable { Type optionalIntType = new TypeReference>() { }.getType(); @@ -48,14 +50,14 @@ public void should_convert_null_to_optional_generic(final ParameterByTypeTransfo @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_to_string(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_to_string(final ParameterByTypeTransformer defaultTransformer) throws Throwable { assertEquals("Barbara Liskov", defaultTransformer.transform("Barbara Liskov", String.class)); } @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_to_optional_string(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_to_optional_string(final ParameterByTypeTransformer defaultTransformer) throws Throwable { Type optionalStringType = new TypeReference>() { }.getType(); @@ -64,49 +66,49 @@ public void should_convert_to_optional_string(final ParameterByTypeTransformer d @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_to_object(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_to_object(final ParameterByTypeTransformer defaultTransformer) throws Throwable { assertEquals("Barbara Liskov", defaultTransformer.transform("Barbara Liskov", Object.class)); } @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_to_big_integer(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_to_big_integer(final ParameterByTypeTransformer defaultTransformer) throws Throwable { assertEquals(new BigInteger("10000008"), defaultTransformer.transform("10000008", BigInteger.class)); } @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_to_big_decimal(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_to_big_decimal(final ParameterByTypeTransformer defaultTransformer) throws Throwable { assertEquals(new BigDecimal("1.0000008"), defaultTransformer.transform("1.0000008", BigDecimal.class)); } @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_to_byte(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_to_byte(final ParameterByTypeTransformer defaultTransformer) throws Throwable { assertEquals(Byte.decode("42"), defaultTransformer.transform("42", Byte.class)); assertEquals(Byte.decode("42"), defaultTransformer.transform("42", byte.class)); } @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_to_short(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_to_short(final ParameterByTypeTransformer defaultTransformer) throws Throwable { assertEquals(Short.decode("42"), defaultTransformer.transform("42", Short.class)); assertEquals(Short.decode("42"), defaultTransformer.transform("42", short.class)); } @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_to_integer(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_to_integer(final ParameterByTypeTransformer defaultTransformer) throws Throwable { assertEquals(Integer.decode("42"), defaultTransformer.transform("42", Integer.class)); assertEquals(Integer.decode("42"), defaultTransformer.transform("42", int.class)); } @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_to_optional_integer(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_to_optional_integer(final ParameterByTypeTransformer defaultTransformer) throws Throwable { Type optionalIntType = new TypeReference>() { }.getType(); @@ -115,32 +117,33 @@ public void should_convert_to_optional_integer(final ParameterByTypeTransformer @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_to_long(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_to_long(final ParameterByTypeTransformer defaultTransformer) throws Throwable { assertEquals(Long.decode("42"), defaultTransformer.transform("42", Long.class)); assertEquals(Long.decode("42"), defaultTransformer.transform("42", long.class)); } @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_to_float(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_to_float(final ParameterByTypeTransformer defaultTransformer) throws Throwable { assertEquals(4.2f, defaultTransformer.transform("4.2", Float.class)); assertEquals(4.2f, defaultTransformer.transform("4.2", float.class)); } @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_to_double(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_to_double(final ParameterByTypeTransformer defaultTransformer) throws Throwable { assertEquals(4.2, defaultTransformer.transform("4.2", Double.class)); assertEquals(4.2, defaultTransformer.transform("4.2", double.class)); } @ParameterizedTest @MethodSource("objectMapperImplementations") - public void should_convert_to_enum(final ParameterByTypeTransformer defaultTransformer) throws Throwable { + void should_convert_to_enum(final ParameterByTypeTransformer defaultTransformer) throws Throwable { assertEquals(TestEnum.TEST, defaultTransformer.transform("TEST", TestEnum.class)); } - private static class TestJacksonDefaultTransformer implements ParameterByTypeTransformer { + @NullMarked + private static final class TestJacksonDefaultTransformer implements ParameterByTypeTransformer { ObjectMapper delegate = initMapper(); private static ObjectMapper initMapper() { @@ -150,7 +153,7 @@ private static ObjectMapper initMapper() { } @Override - public Object transform(String fromValue, Type toValueType) { + public Object transform(@Nullable String fromValue, Type toValueType) { TypeFactory typeFactory = delegate.getTypeFactory(); return delegate.convertValue(fromValue, typeFactory.constructType(toValueType)); } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeComparatorTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeComparatorTest.java index 0966f0545..a38e8cfae 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeComparatorTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeComparatorTest.java @@ -1,6 +1,7 @@ package io.cucumber.cucumberexpressions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.shadow.de.siegmar.fastcsv.util.Nullable; import java.util.ArrayList; import java.util.List; @@ -13,43 +14,42 @@ public class ParameterTypeComparatorTest { + @Test + public void sorts_parameter_types_by_preferential_then_name() { + SortedSet> set = new TreeSet<>(); + set.add(new ParameterType<>("c", "c", C.class, C::new, false, true)); + set.add(new ParameterType<>("a", "a", A.class, A::new, false, false)); + set.add(new ParameterType<>("d", "d", D.class, D::new, false, false)); + set.add(new ParameterType<>("b", "b", B.class, B::new, false, true)); + + List names = new ArrayList<>(); + for (ParameterType parameterType : set) { + names.add(parameterType.getName()); + } + assertEquals(asList("b", "c", "a", "d"), names); + } + public static class A { - A(String s) { + A(@Nullable String s) { assertNotNull(s); } } public static class B { - B(String s) { + B(@Nullable String s) { assertNotNull(s); } } public static class C { - C(String s) { + C(@Nullable String s) { assertNotNull(s); } } public static class D { - D(String s) { + D(@Nullable String s) { assertNotNull(s); } } - - @Test - public void sorts_parameter_types_by_preferential_then_name() { - SortedSet> set = new TreeSet<>(); - set.add(new ParameterType<>("c", "c", C.class, C::new, false, true)); - set.add(new ParameterType<>("a", "a", A.class, A::new, false, false)); - set.add(new ParameterType<>("d", "d", D.class, D::new, false, false)); - set.add(new ParameterType<>("b", "b", B.class, B::new, false, true)); - - List names = new ArrayList<>(); - for (ParameterType parameterType : set) { - names.add(parameterType.getName()); - } - assertEquals(asList("b", "c", "a", "d"), names); - } - } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeRegistryTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeRegistryTest.java index 987bfe363..2b42d2f0d 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeRegistryTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeRegistryTest.java @@ -2,41 +2,20 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.params.shadow.de.siegmar.fastcsv.util.Nullable; import java.math.BigDecimal; import java.util.Locale; import java.util.regex.Pattern; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsEqual.equalTo; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; public class ParameterTypeRegistryTest { private static final String CAPITALISED_WORD = "[A-Z]+\\w+"; - public static class Name { - Name(String s) { - assertNotNull(s); - } - } - - public static class Person { - Person(String s) { - assertNotNull(s); - } - } - - public static class Place { - Place(String s) { - assertNotNull(s); - } - } - private final ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH); @Test @@ -45,7 +24,7 @@ public void does_not_allow_more_than_one_preferential_parameter_type_for_each_re registry.defineParameterType(new ParameterType<>("name", CAPITALISED_WORD, Name.class, Name::new, false, true)); registry.defineParameterType(new ParameterType<>("person", CAPITALISED_WORD, Person.class, Person::new, false, false)); - final Executable testMethod = () -> registry.defineParameterType(new ParameterType<>( + Executable testMethod = () -> registry.defineParameterType(new ParameterType<>( "place", CAPITALISED_WORD, Place.class, @@ -54,66 +33,66 @@ public void does_not_allow_more_than_one_preferential_parameter_type_for_each_re true )); - final CucumberExpressionException thrownException = assertThrows(CucumberExpressionException.class, testMethod); - assertThat("Unexpected message", thrownException.getMessage(), is(equalTo("There can only be one preferential parameter type per regexp. The regexp /[A-Z]+\\w+/ is used for two preferential parameter types, {name} and {place}"))); + var exception = assertThrows(CucumberExpressionException.class, testMethod); + assertThat(exception).hasMessage("There can only be one preferential parameter type per regexp. The regexp /[A-Z]+\\w+/ is used for two preferential parameter types, {name} and {place}"); } @Test public void looks_up_preferential_parameter_type_by_regexp() { - ParameterType name = new ParameterType<>("name", CAPITALISED_WORD, Name.class, Name::new, false, false); - ParameterType person = new ParameterType<>("person", CAPITALISED_WORD, Person.class, Person::new, false, true); - ParameterType place = new ParameterType<>("place", CAPITALISED_WORD, Place.class, Place::new, false, false); + var name = new ParameterType<>("name", CAPITALISED_WORD, Name.class, Name::new, false, false); + var person = new ParameterType<>("person", CAPITALISED_WORD, Person.class, Person::new, false, true); + var place = new ParameterType<>("place", CAPITALISED_WORD, Place.class, Place::new, false, false); registry.defineParameterType(name); registry.defineParameterType(person); registry.defineParameterType(place); - assertSame(person, registry.lookupByRegexp(CAPITALISED_WORD, Pattern.compile("([A-Z]+\\w+) and ([A-Z]+\\w+)"), "Lisa and Bob")); + var parameter = registry.lookupByRegexp(CAPITALISED_WORD, Pattern.compile("([A-Z]+\\w+) and ([A-Z]+\\w+)"), "Lisa and Bob"); + assertThat(parameter).isSameAs(person); } @Test public void throws_ambiguous_exception_on_lookup_when_no_parameter_types_are_preferential() { - ParameterType name = new ParameterType<>("name", CAPITALISED_WORD, Name.class, Name::new, true, false); - ParameterType person = new ParameterType<>("person", CAPITALISED_WORD, Person.class, Person::new, true, false); - ParameterType place = new ParameterType<>("place", CAPITALISED_WORD, Place.class, Place::new, true, false); + var name = new ParameterType<>("name", CAPITALISED_WORD, Name.class, Name::new, true, false); + var person = new ParameterType<>("person", CAPITALISED_WORD, Person.class, Person::new, true, false); + var place = new ParameterType<>("place", CAPITALISED_WORD, Place.class, Place::new, true, false); registry.defineParameterType(name); registry.defineParameterType(person); registry.defineParameterType(place); - String expected = "" + - "Your Regular Expression /([A-Z]+\\w+) and ([A-Z]+\\w+)/\n" + - "matches multiple parameter types with regexp /[A-Z]+\\w+/:\n" + - " {name}\n" + - " {person}\n" + - " {place}\n" + - "\n" + - "I couldn't decide which one to use. You have two options:\n" + - "\n" + - "1) Use a Cucumber Expression instead of a Regular Expression. Try one of these:\n" + - " {name} and {name}\n" + - " {name} and {person}\n" + - " {name} and {place}\n" + - " {person} and {name}\n" + - " {person} and {person}\n" + - " {person} and {place}\n" + - " {place} and {name}\n" + - " {place} and {person}\n" + - " {place} and {place}\n" + - "\n" + - "2) Make one of the parameter types preferential and continue to use a Regular Expression.\n" + - "\n"; - - final Executable testMethod = () -> registry.lookupByRegexp(CAPITALISED_WORD, Pattern.compile("([A-Z]+\\w+) and ([A-Z]+\\w+)"), "Lisa and Bob"); - - final AmbiguousParameterTypeException thrownException = assertThrows(AmbiguousParameterTypeException.class, testMethod); - assertThat("Unexpected message", thrownException.getMessage(), is(equalTo(expected))); + String expected = """ + Your Regular Expression /([A-Z]+\\w+) and ([A-Z]+\\w+)/ + matches multiple parameter types with regexp /[A-Z]+\\w+/: + {name} + {person} + {place} + + I couldn't decide which one to use. You have two options: + + 1) Use a Cucumber Expression instead of a Regular Expression. Try one of these: + {name} and {name} + {name} and {person} + {name} and {place} + {person} and {name} + {person} and {person} + {person} and {place} + {place} and {name} + {place} and {person} + {place} and {place} + + 2) Make one of the parameter types preferential and continue to use a Regular Expression. + + """; + + Executable testMethod = () -> registry.lookupByRegexp(CAPITALISED_WORD, Pattern.compile("([A-Z]+\\w+) and ([A-Z]+\\w+)"), "Lisa and Bob"); + var exception = assertThrows(AmbiguousParameterTypeException.class, testMethod); + assertThat(exception).hasMessage(expected); } @Test public void does_not_allow_anonymous_parameter_type_to_be_registered() { + Executable testMethod = () -> registry.defineParameterType(new ParameterType<>("", ".*", Object.class, (Transformer) arg -> arg)); - final Executable testMethod = () -> registry.defineParameterType(new ParameterType<>("", ".*", Object.class, (Transformer) arg -> arg)); - - final DuplicateTypeNameException thrownException = assertThrows(DuplicateTypeNameException.class, testMethod); - assertThat("Unexpected message", thrownException.getMessage(), is(equalTo("The anonymous parameter type has already been defined"))); + var exception = assertThrows(DuplicateTypeNameException.class, testMethod); + assertThat(exception).hasMessage("The anonymous parameter type has already been defined"); } @Test @@ -121,38 +100,39 @@ public void parse_decimal_numbers_in_english() { ExpressionFactory factory = new ExpressionFactory(new ParameterTypeRegistry(Locale.ENGLISH)); Expression expression = factory.createExpression("{bigdecimal}"); - assertThat(expression.match(""), nullValue()); - assertThat(expression.match("."), nullValue()); - assertThat(expression.match(","), nullValue()); - assertThat(expression.match("-"), nullValue()); - assertThat(expression.match("E"), nullValue()); - assertThat(expression.match("1,"), nullValue()); - assertThat(expression.match(",1"), nullValue()); - assertThat(expression.match("1."), nullValue()); - - assertThat(expression.match("1").get(0).getValue(), is(BigDecimal.ONE)); - assertThat(expression.match("-1").get(0).getValue(), is(new BigDecimal("-1"))); - assertThat(expression.match("1.1").get(0).getValue(), is(new BigDecimal("1.1"))); - assertThat(expression.match("1,000").get(0).getValue(), is(new BigDecimal("1000"))); - assertThat(expression.match("1,000,0").get(0).getValue(), is(new BigDecimal("10000"))); - assertThat(expression.match("1,000.1").get(0).getValue(), is(new BigDecimal("1000.1"))); - assertThat(expression.match("1,000,10").get(0).getValue(), is(new BigDecimal("100010"))); - assertThat(expression.match("1,0.1").get(0).getValue(), is(new BigDecimal("10.1"))); - assertThat(expression.match("1,000,000.1").get(0).getValue(), is(new BigDecimal("1000000.1"))); - assertThat(expression.match("-1.1").get(0).getValue(), is(new BigDecimal("-1.1"))); - - assertThat(expression.match(".1").get(0).getValue(), is(new BigDecimal("0.1"))); - assertThat(expression.match("-.1").get(0).getValue(), is(new BigDecimal("-0.1"))); - assertThat(expression.match("-.10000001").get(0).getValue(), is(new BigDecimal("-0.10000001"))); - assertThat(expression.match("1E1").get(0).getValue(), is(new BigDecimal("1E1"))); // precision 1 with scale -1, can not be expressed as a decimal - assertThat(expression.match(".1E1").get(0).getValue(), is(new BigDecimal("1"))); - assertThat(expression.match("E1"), nullValue()); - assertThat(expression.match("-.1E-1").get(0).getValue(), is(new BigDecimal("-0.01"))); - assertThat(expression.match("-.1E-2").get(0).getValue(), is(new BigDecimal("-0.001"))); - assertThat(expression.match("-.1E+1"), nullValue()); - assertThat(expression.match("-.1E+2"), nullValue()); - assertThat(expression.match("-.1E1").get(0).getValue(), is(new BigDecimal("-1"))); - assertThat(expression.match("-.10E2").get(0).getValue(), is(new BigDecimal("-10"))); + assertThat(expression.match("")).isNull(); + assertThat(expression.match(".")).isNull(); + assertThat(expression.match(",")).isNull(); + assertThat(expression.match("-")).isNull(); + assertThat(expression.match("E")).isNull(); + assertThat(expression.match("1,")).isNull(); + assertThat(expression.match(",1")).isNull(); + assertThat(expression.match("1.")).isNull(); + + assertThat(expression.match("1")).singleElement().extracting(Argument::getValue).isEqualTo(BigDecimal.ONE); + assertThat(expression.match("-1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1")); + assertThat(expression.match("1.1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1.1")); + assertThat(expression.match("1,000")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000")); + assertThat(expression.match("1,000,0")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("10000")); + assertThat(expression.match("1,000.1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000.1")); + assertThat(expression.match("1,000,10")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("100010")); + assertThat(expression.match("1,0.1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("10.1")); + assertThat(expression.match("1,000,000.1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000000.1")); + assertThat(expression.match("-1.1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1.1")); + + assertThat(expression.match(".1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("0.1")); + assertThat(expression.match("-.1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-0.1")); + assertThat(expression.match("-.10000001")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-0.10000001")); + // precision 1 with scale -1, can not be expressed as a decimal + assertThat(expression.match("1E1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1E1")); + assertThat(expression.match(".1E1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1")); + assertThat(expression.match("E1")).isNull(); + assertThat(expression.match("-.1E-1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-0.01")); + assertThat(expression.match("-.1E-2")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-0.001")); + assertThat(expression.match("-.1E+1")).isNull(); + assertThat(expression.match("-.1E+2")).isNull(); + assertThat(expression.match("-.1E1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1")); + assertThat(expression.match("-.10E2")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-10")); } @Test @@ -160,10 +140,10 @@ public void parse_decimal_numbers_in_german() { ExpressionFactory factory = new ExpressionFactory(new ParameterTypeRegistry(Locale.GERMAN)); Expression expression = factory.createExpression("{bigdecimal}"); - assertThat(expression.match("1.000,1").get(0).getValue(), is(new BigDecimal("1000.1"))); - assertThat(expression.match("1.000.000,1").get(0).getValue(), is(new BigDecimal("1000000.1"))); - assertThat(expression.match("-1,1").get(0).getValue(), is(new BigDecimal("-1.1"))); - assertThat(expression.match("-,1E1").get(0).getValue(), is(new BigDecimal("-1"))); + assertThat(expression.match("1.000,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000.1")); + assertThat(expression.match("1.000.000,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000000.1")); + assertThat(expression.match("-1,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1.1")); + assertThat(expression.match("-,1E1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1")); } @Test @@ -171,10 +151,10 @@ public void parse_decimal_numbers_in_canadian_french() { ExpressionFactory factory = new ExpressionFactory(new ParameterTypeRegistry(Locale.CANADA_FRENCH)); Expression expression = factory.createExpression("{bigdecimal}"); - assertThat(expression.match("1.000,1").get(0).getValue(), is(new BigDecimal("1000.1"))); - assertThat(expression.match("1.000.000,1").get(0).getValue(), is(new BigDecimal("1000000.1"))); - assertThat(expression.match("-1,1").get(0).getValue(), is(new BigDecimal("-1.1"))); - assertThat(expression.match("-,1E1").get(0).getValue(), is(new BigDecimal("-1"))); + assertThat(expression.match("1.000,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000.1")); + assertThat(expression.match("1.000.000,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000000.1")); + assertThat(expression.match("-1,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1.1")); + assertThat(expression.match("-,1E1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1")); } @Test @@ -182,10 +162,28 @@ public void parse_decimal_numbers_in_norwegian() { ExpressionFactory factory = new ExpressionFactory(new ParameterTypeRegistry(Locale.forLanguageTag("no"))); Expression expression = factory.createExpression("{bigdecimal}"); - assertThat(expression.match("1.000,1").get(0).getValue(), is(new BigDecimal("1000.1"))); - assertThat(expression.match("1.000.000,1").get(0).getValue(), is(new BigDecimal("1000000.1"))); - assertThat(expression.match("-1,1").get(0).getValue(), is(new BigDecimal("-1.1"))); - assertThat(expression.match("-,1E1").get(0).getValue(), is(new BigDecimal("-1"))); + assertThat(expression.match("1.000,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000.1")); + assertThat(expression.match("1.000.000,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000000.1")); + assertThat(expression.match("-1,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1.1")); + assertThat(expression.match("-,1E1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1")); + } + + public static class Name { + Name(@Nullable String s) { + assertNotNull(s); + } + } + + public static class Person { + Person(@Nullable String s) { + assertNotNull(s); + } + } + + public static class Place { + Place(@Nullable String s) { + assertNotNull(s); + } } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/PatternCompilerProviderTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/PatternCompilerProviderTest.java index 5f6a9a567..8de1b8a02 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/PatternCompilerProviderTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/PatternCompilerProviderTest.java @@ -1,6 +1,6 @@ package io.cucumber.cucumberexpressions; -import org.junit.jupiter.api.AfterEach; +import org.jspecify.annotations.NullMarked; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; @@ -9,53 +9,45 @@ import java.util.Collections; import java.util.regex.Pattern; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.junit.jupiter.api.Assertions.assertSame; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -public class PatternCompilerProviderTest { +class PatternCompilerProviderTest { @BeforeEach - public void setUp() { - PatternCompilerProvider.service = null; - } - - @AfterEach - public void tearDown() { + void setUp() { PatternCompilerProvider.service = null; } @Test - public void use_default_compiler_if_none_registered() { - PatternCompilerProvider.findPatternCompiler(Collections.emptyIterator()); - assertSame(DefaultPatternCompiler.class, PatternCompilerProvider.service.getClass()); + void use_default_compiler_if_none_registered() { + PatternCompilerProvider.getCompiler(); + assertThat(PatternCompilerProvider.service) + .extracting(Object::getClass) + .isEqualTo(DefaultPatternCompiler.class); } @Test - public void use_found_pattern_compiler_if_one_provided() { - PatternCompiler compiler = getTestCompiler(); - PatternCompilerProvider.findPatternCompiler(Collections.singletonList(compiler).iterator()); - assertSame(compiler, PatternCompilerProvider.service); + void use_found_pattern_compiler_if_one_provided() { + PatternCompiler compiler = new TestPatternCompiler(); + PatternCompiler found = PatternCompilerProvider.findPatternCompiler(Collections.singletonList(compiler).iterator()); + assertThat(found).isSameAs(compiler); } @Test - public void throws_error_if_more_than_one_pattern_compiler() { - - final Executable testMethod = () -> PatternCompilerProvider.findPatternCompiler(Arrays.asList(new DefaultPatternCompiler(), getTestCompiler()).iterator()); - - final IllegalStateException thrownException = assertThrows(IllegalStateException.class, testMethod); - assertThat("Unexpected message", thrownException.getMessage(), is(equalTo("More than one PatternCompiler: [class io.cucumber.cucumberexpressions.DefaultPatternCompiler, class io.cucumber.cucumberexpressions.PatternCompilerProviderTest$1]"))); + void throws_error_if_more_than_one_pattern_compiler() { + Executable testMethod = () -> PatternCompilerProvider.findPatternCompiler(Arrays.asList(new DefaultPatternCompiler(), new TestPatternCompiler()).iterator()); + var exception = assertThrows(IllegalStateException.class, testMethod); + assertThat(exception).hasMessage("More than one PatternCompiler: [class io.cucumber.cucumberexpressions.DefaultPatternCompiler, class io.cucumber.cucumberexpressions.PatternCompilerProviderTest$TestPatternCompiler]"); } - private PatternCompiler getTestCompiler() { - return new PatternCompiler() { - @Override - public Pattern compile(String regexp, int flags) { - return null; - } - }; + @NullMarked + private static final class TestPatternCompiler implements PatternCompiler { + + @Override + public Pattern compile(String regexp, int flags) { + return Pattern.compile(regexp, flags); + } } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/RegularExpressionTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/RegularExpressionTest.java index 1aa4e809b..cb16d728e 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/RegularExpressionTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/RegularExpressionTest.java @@ -1,5 +1,7 @@ package io.cucumber.cucumberexpressions; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.ParameterizedTest; @@ -18,25 +20,26 @@ import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Collectors; import static java.nio.file.Files.newDirectoryStream; import static java.nio.file.Files.newInputStream; import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; import static java.util.regex.Pattern.compile; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; -public class RegularExpressionTest { +final class RegularExpressionTest { private final ParameterTypeRegistry parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); - private static List acceptance_tests_pass() throws IOException { + static List acceptance_tests_pass() throws IOException { List paths = new ArrayList<>(); - newDirectoryStream(Paths.get("..", "testdata", "regular-expression", "matching")).forEach(paths::add); + try (var directories = newDirectoryStream(Paths.get("..", "testdata", "regular-expression", "matching"))) { + directories.forEach(paths::add); + } paths.sort(Comparator.naturalOrder()); return paths; } @@ -50,232 +53,222 @@ void acceptance_tests_pass(@ConvertWith(Converter.class) Expectation expectation .map(Argument::getValue) .collect(Collectors.toList()); - assertThat(values, CustomMatchers.equalOrCloseTo(expectation.expected_args)); - } - - static class Expectation { - public String expression; - public String text; - public List expected_args; - } - - static class Converter implements ArgumentConverter { - Yaml yaml = new Yaml(); - - @Override - public Expectation convert(Object source, ParameterContext context) throws ArgumentConversionException { - try { - Path path = (Path) source; - InputStream inputStream = newInputStream(path); - return yaml.loadAs(inputStream, Expectation.class); - } catch (IOException e) { - throw new ArgumentConversionException("Could not load " + source, e); - } - } + assertThat(values).isEqualTo(expectation.expectedArgs); } @Test - public void documentation_match_arguments() { + void documentation_match_arguments() { Pattern expr = Pattern.compile("I have (\\d+) cukes? in my (\\w+) now"); Expression expression = new RegularExpression(expr, parameterTypeRegistry); List> match = expression.match("I have 7 cukes in my belly now"); - assertEquals(7, match.get(0).getValue()); - assertEquals("belly", match.get(1).getValue()); + assertThat(match).extracting(Argument::getValue).map(Object.class::cast).containsExactly(7, "belly"); } @Test - public void matches_positive_int() { - List match = match(compile("(\\d+)"), "22"); - assertEquals(singletonList(22), match); + void matches_positive_int() { + List match = match(compile("(\\d+)"), "22"); + assertThat(match).containsExactly(22); } @Test - public void matches_positive_int_with_hint() { - List match = match(compile("(\\d+)"), "22", Integer.class); - assertEquals(singletonList(22), match); + void matches_positive_int_with_hint() { + List match = match(compile("(\\d+)"), "22", Integer.class); + assertThat(match).containsExactly(22); } @Test - public void matches_positive_int_with_conflicting_type_hint() { - List match = match(compile("(\\d+)"), "22", String.class); - assertEquals(singletonList("22"), match); + void matches_positive_int_with_conflicting_type_hint() { + List match = match(compile("(\\d+)"), "22", String.class); + assertThat(match).containsExactly("22"); } @Test - public void matches_nested_capture_group_without_match() { - List match = match(compile("^a user( named \"([^\"]*)\")?$"), "a user"); - assertEquals(singletonList(null), match); + void matches_nested_capture_group_without_match() { + List match = match(compile("^a user( named \"([^\"]*)\")?$"), "a user"); + assertThat(match).containsExactly((Object) null); } @Test - public void matches_nested_capture_group_with_match() { - List match = match(compile("^a user( named \"([^\"]*)\")?$"), "a user named \"Charlie\""); - assertEquals(singletonList("Charlie"), match); + void matches_nested_capture_group_with_match() { + List match = match(compile("^a user( named \"([^\"]*)\")?$"), "a user named \"Charlie\""); + assertThat(match).containsExactly("Charlie"); } @Test - public void ignores_non_capturing_groups() { + void ignores_non_capturing_groups() { String expr = "(\\S+) ?(can|cannot)? (?:delete|cancel) the (\\d+)(?:st|nd|rd|th) (attachment|slide) ?(?:upload)?"; String step = "I can cancel the 1st slide upload"; - List match = match(compile(expr), step); - assertEquals(asList("I", "can", 1, "slide"), match); + List match = match(compile(expr), step); + assertThat(match).isEqualTo(asList("I", "can", 1, "slide")); } @Test - public void matches_capture_group_nested_in_optional_one() { + void matches_capture_group_nested_in_optional_one() { String regex = "^a (pre-commercial transaction |pre buyer fee model )?purchase(?: for \\$(\\d+))?$"; - assertEquals(asList(null, null), match(compile(regex), "a purchase")); - assertEquals(asList(null, 33), match(compile(regex), "a purchase for $33")); - assertEquals(asList("pre buyer fee model ", null), match(compile(regex), "a pre buyer fee model purchase")); + assertThat(match(Pattern.compile(regex), "a purchase")).containsExactly(null, null); + assertThat(match(Pattern.compile(regex), "a purchase for $33")).containsExactly(null, 33); + assertThat(match(Pattern.compile(regex), "a pre buyer fee model purchase")).containsExactly("pre buyer fee model ", null); } @Test - public void works_with_escaped_parenthesis() { + void works_with_escaped_parenthesis() { String expr = "Across the line\\(s\\)"; String step = "Across the line(s)"; - List match = match(compile(expr), step); - assertEquals(emptyList(), match); + List match = match(compile(expr), step); + assertThat(match).isEmpty(); } @Test - public void exposes_source_and_regexp() { + void exposes_source_and_regexp() { String regexp = "I have (\\d+) cukes? in my (.+) now"; - RegularExpression expression = new RegularExpression(Pattern.compile(regexp), - new ParameterTypeRegistry(Locale.ENGLISH)); - assertEquals(regexp, expression.getSource()); - assertEquals(regexp, expression.getRegexp().pattern()); + RegularExpression expression = new RegularExpression(Pattern.compile(regexp), new ParameterTypeRegistry(Locale.ENGLISH)); + assertThat(expression.getSource()).isEqualTo(regexp); + assertThat(expression.getRegexp().pattern()).isEqualTo(regexp); } @Test - public void uses_float_type_hint_when_group_doesnt_match_known_param_type() { - List match = match(compile("a (.*)"), "a 22", Float.class); - assertEquals(Float.class, match.get(0).getClass()); - assertEquals(22f, (Float) match.get(0), 0.00001); + void uses_float_type_hint_when_group_doesnt_match_known_param_type() { + List match = match(compile("a (.*)"), "a 22", Float.class); + assertThat(match.get(0).getClass()).isEqualTo(Float.class); + assertThat(match.get(0)).isEqualTo(22f); } @Test - public void uses_double_type_hint_when_group_doesnt_match_known_param_type() { - List match = match(compile("a (\\d\\d.\\d)"), "a 33.5", Double.class); - assertEquals(Double.class, match.get(0).getClass()); - assertEquals(33.5d, (Double) match.get(0), 0.00001); + void uses_double_type_hint_when_group_doesnt_match_known_param_type() { + List match = match(compile("a (\\d\\d.\\d)"), "a 33.5", Double.class); + assertThat(match.get(0).getClass()).isEqualTo(Double.class); + assertThat(match.get(0)).isEqualTo(33.5d); } @Test - public void matches_empty_string() { - List match = match(compile("^The value equals \"([^\"]*)\"$"), "The value equals \"\"", String.class); - assertEquals(String.class, match.get(0).getClass()); - assertEquals("", match.get(0)); + void matches_empty_string() { + List match = match(compile("^The value equals \"([^\"]*)\"$"), "The value equals \"\"", String.class); + assertThat(match.get(0).getClass()).isEqualTo(String.class); + assertThat(match.get(0)).isEqualTo(""); } @Test - public void uses_two_type_hints_to_resolve_anonymous_parameter_type() { - List match = match(compile("a (.*) and a (.*)"), "a 22 and a 33.5", Float.class, Double.class); + void uses_two_type_hints_to_resolve_anonymous_parameter_type() { + List match = match(compile("a (.*) and a (.*)"), "a 22 and a 33.5", Float.class, Double.class); - assertEquals(Float.class, match.get(0).getClass()); - assertEquals(22f, (Float) match.get(0), 0.00001); + assertThat(match.get(0).getClass()).isEqualTo(Float.class); + assertThat(match.get(0)).isEqualTo(22f); - assertEquals(Double.class, match.get(1).getClass()); - assertEquals(33.5d, (Double) match.get(1), 0.00001); + assertThat(match.get(1).getClass()).isEqualTo(Double.class); + assertThat(match.get(1)).isEqualTo(33.5d); } @Test - public void retains_all_content_captured_by_the_capture_group() { - List match = match(compile("a quote ([\"a-z ]+)"), "a quote \" and quote \"", String.class); - assertEquals(singletonList("\" and quote \""), match); + void retains_all_content_captured_by_the_capture_group() { + List match = match(compile("a quote ([\"a-z ]+)"), "a quote \" and quote \"", String.class); + assertThat(match).containsExactly("\" and quote \""); } @Test - public void uses_parameter_type_registry_when_parameter_type_is_defined() { + void uses_parameter_type_registry_when_parameter_type_is_defined() { parameterTypeRegistry.defineParameterType(new ParameterType<>( "test", "[\"a-z ]+", String.class, - new Transformer() { - @Override - public String transform(String s) { - return s.toUpperCase(); - } - } + (@Nullable String s) -> requireNonNull(s).toUpperCase(Locale.US) )); - List match = match(compile("a quote ([\"a-z ]+)"), "a quote \" and quote \"", String.class); - assertEquals(singletonList("\" AND QUOTE \""), match); + List match = match(compile("a quote ([\"a-z ]+)"), "a quote \" and quote \"", String.class); + assertThat(match).containsExactly("\" AND QUOTE \""); } @Test - public void ignores_type_hint_when_parameter_type_has_strong_type_hint() { + void ignores_type_hint_when_parameter_type_has_strong_type_hint() { parameterTypeRegistry.defineParameterType(new ParameterType<>( "test", "one|two|three", Integer.class, - new Transformer() { - @Override - public Integer transform(String s) { - return 42; - } - }, false, false, true + s -> 42, + false, + false, + true )); - assertEquals(asList(42), match(compile("(one|two|three)"), "one", String.class)); + assertThat(match(Pattern.compile("(one|two|three)"), "one", String.class)).containsExactly(42); } @Test - public void follows_type_hint_when_parameter_type_does_not_have_strong_type_hint() { + void follows_type_hint_when_parameter_type_does_not_have_strong_type_hint() { parameterTypeRegistry.defineParameterType(new ParameterType<>( "test", "one|two|three", Integer.class, - new Transformer() { - @Override - public Integer transform(String s) { - return 42; - } - }, false, false, false + s -> 42, + false, + false, + false )); - assertEquals(asList("one"), match(compile("(one|two|three)"), "one", String.class)); + assertThat(match(Pattern.compile("(one|two|three)"), "one", String.class)).containsExactly("one"); } @Test - public void matches_anonymous_parameter_type_with_hint() { - assertEquals(singletonList(0.22f), match(compile("(.*)"), "0.22", Float.class)); + void matches_anonymous_parameter_type_with_hint() { + assertThat(match(Pattern.compile("(.*)"), "0.22", Float.class)).containsExactly(0.22f); } @Test - public void matches_anonymous_parameter_type() { - assertEquals(singletonList("0.22"), match(compile("(.*)"), "0.22")); + void matches_anonymous_parameter_type() { + assertThat(match(Pattern.compile("(.*)"), "0.22")).containsExactly("0.22"); } @Test - public void matches_optional_boolean_capture_group() { + void matches_optional_boolean_capture_group() { Pattern pattern = compile("^(true|false)?$"); - assertEquals(singletonList(true), match(pattern, "true", Boolean.class)); - assertEquals(singletonList(false), match(pattern, "false", Boolean.class)); - assertEquals(singletonList(null), match(pattern, "", Boolean.class)); + assertThat(match(pattern, "true", Boolean.class)).containsExactly(true); + assertThat(match(pattern, "false", Boolean.class)).containsExactly(false); + assertThat(match(pattern, "", Boolean.class)).containsExactly((Object) null); } @Test - public void parameter_types_can_be_optional_when_used_in_regex() { + void parameter_types_can_be_optional_when_used_in_regex() { parameterTypeRegistry.defineParameterType(new ParameterType<>( "test", ".+", String.class, - new Transformer() { - @Override - public String transform(String s) { - return s; - } - } + (@Nullable String s) -> s )); - List match = match(compile("^text(?: (.+))? text2$"), "text text2", String.class); - assertEquals(singletonList(null), match); + List match = match(compile("^text(?: (.+))? text2$"), "text text2", String.class); + assertThat(match).containsExactly((Object) null); } - private List match(Pattern pattern, String text, Type... types) { + private List match(Pattern pattern, String text, Type... types) { RegularExpression regularExpression = new RegularExpression(pattern, parameterTypeRegistry); List> arguments = regularExpression.match(text, types); List values = new ArrayList<>(); - for (Argument argument : arguments) { + for (Argument argument : requireNonNull(arguments)) { values.add(argument.getValue()); } return values; } + + record Expectation(String expression, String text, List expectedArgs) { + } + + @NullMarked + static class Converter implements ArgumentConverter { + Yaml yaml = new Yaml(); + + @Override + public Expectation convert(@Nullable Object source, ParameterContext context) throws ArgumentConversionException { + if (source == null) { + throw new ArgumentConversionException("Could not load null"); + } + try { + Path path = (Path) source; + InputStream inputStream = newInputStream(path); + Map expectation = yaml.loadAs(inputStream, Map.class); + return new Expectation( + (String) requireNonNull(expectation.get("expression")), + (String) requireNonNull(expectation.get("text")), + (List) requireNonNull(expectation.get("expected_args"))); + } catch (IOException e) { + throw new ArgumentConversionException("Could not load " + source, e); + } + } + } + } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/TreeRegexpTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/TreeRegexpTest.java index 5b0ef95b0..03fe6c1eb 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/TreeRegexpTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/TreeRegexpTest.java @@ -8,6 +8,7 @@ import static java.util.Arrays.asList; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -28,6 +29,7 @@ public void exposes_group_source() { public void builds_tree() { TreeRegexp tr = new TreeRegexp("(a(b(c))(d))"); Group g = tr.match("abcd"); + requireNonNull(g); assertEquals("abcd", g.getChildren().get(0).getValue()); assertEquals("bc", g.getChildren().get(0).getChildren().get(0).getValue()); assertEquals("c", g.getChildren().get(0).getChildren().get(0).getChildren().get(0).getValue()); @@ -38,6 +40,7 @@ public void builds_tree() { public void ignores_question_mark_colon_non_capturing_group() { TreeRegexp tr = new TreeRegexp("a(?:b)(c)"); Group g = tr.match("abc"); + requireNonNull(g); assertEquals("abc", g.getValue()); assertEquals(1, g.getChildren().size()); } @@ -46,6 +49,7 @@ public void ignores_question_mark_colon_non_capturing_group() { public void ignores_question_mark_exclamation_mark_non_capturing_group() { TreeRegexp tr = new TreeRegexp("a(?!b)(.+)"); Group g = tr.match("aBc"); + requireNonNull(g); assertEquals("aBc", g.getValue()); assertEquals(1, g.getChildren().size()); } @@ -54,6 +58,7 @@ public void ignores_question_mark_exclamation_mark_non_capturing_group() { public void ignores_question_mark_equal_sign_non_capturing_group() { TreeRegexp tr = new TreeRegexp("a(?=b)(.+)"); Group g = tr.match("abc"); + requireNonNull(g); assertEquals("abc", g.getValue()); assertEquals(1, g.getChildren().size()); assertEquals("bc", g.getChildren().get(0).getValue()); @@ -63,6 +68,7 @@ public void ignores_question_mark_equal_sign_non_capturing_group() { public void ignores_question_mark_less_than_equal_sign_non_capturing_group() { TreeRegexp tr = new TreeRegexp("a(.+)(?<=c)$"); Group g = tr.match("abc"); + requireNonNull(g); assertEquals("abc", g.getValue()); assertEquals(1, g.getChildren().size()); assertEquals("bc", g.getChildren().get(0).getValue()); @@ -72,6 +78,7 @@ public void ignores_question_mark_less_than_equal_sign_non_capturing_group() { public void ignores_question_mark_less_than_exclamation_mark_non_capturing_group() { TreeRegexp tr = new TreeRegexp("a(.+)(?b)(c)$"); Group g = tr.match("abc"); + requireNonNull(g); assertEquals("abc", g.getValue()); assertEquals(1, g.getChildren().size()); assertEquals("c", g.getChildren().get(0).getValue()); @@ -90,6 +98,7 @@ public void ignores_question_mark_greater_then_non_capturing_group() { public void matches_named_capturing_group() { TreeRegexp tr = new TreeRegexp("a(?b)c$"); Group g = tr.match("abc"); + requireNonNull(g); assertEquals("abc", g.getValue()); assertEquals(1, g.getChildren().size()); assertEquals("b", g.getChildren().get(0).getValue()); @@ -99,6 +108,7 @@ public void matches_named_capturing_group() { public void matches_optional_group() { TreeRegexp tr = new TreeRegexp("^Something( with an optional argument)?"); Group g = tr.match("Something"); + requireNonNull(g); assertNull(g.getChildren().get(0).getValue()); } @@ -106,7 +116,7 @@ public void matches_optional_group() { public void matches_nested_groups() { TreeRegexp tr = new TreeRegexp("^A (\\d+) thick line from ((\\d+),\\s*(\\d+),\\s*(\\d+)) to ((\\d+),\\s*(\\d+),\\s*(\\d+))"); Group g = tr.match("A 5 thick line from 10,20,30 to 40,50,60"); - + requireNonNull(g); assertEquals("5", g.getChildren().get(0).getValue()); assertEquals("10,20,30", g.getChildren().get(1).getValue()); assertEquals("10", g.getChildren().get(1).getChildren().get(0).getValue()); @@ -122,6 +132,7 @@ public void matches_nested_groups() { public void captures_non_capturing_groups_with_capturing_groups_inside() { TreeRegexp tr = new TreeRegexp("the stdout(?: from \"(.*?)\")?"); Group g = tr.match("the stdout"); + requireNonNull(g); assertEquals("the stdout", g.getValue()); assertNull(g.getChildren().get(0).getValue()); assertEquals(1, g.getChildren().size()); @@ -131,6 +142,7 @@ public void captures_non_capturing_groups_with_capturing_groups_inside() { public void detects_multiple_non_capturing_groups() { TreeRegexp tr = new TreeRegexp("(?:a)(:b)(\\?c)(d)"); Group g = tr.match("a:b?cd"); + requireNonNull(g); assertEquals(3, g.getChildren().size()); } @@ -138,6 +150,7 @@ public void detects_multiple_non_capturing_groups() { public void works_with_escaped_backslash() { TreeRegexp tr = new TreeRegexp("foo\\\\(bar|baz)"); Group g = tr.match("foo\\bar"); + requireNonNull(g); assertEquals(1, g.getChildren().size()); } @@ -145,6 +158,7 @@ public void works_with_escaped_backslash() { public void works_with_slash_which_doesnt_need_escaping_in_java() { TreeRegexp tr = new TreeRegexp("^I go to '/(.+)'$"); Group g = tr.match("I go to '/hello'"); + requireNonNull(g); assertEquals(1, g.getChildren().size()); } @@ -152,6 +166,7 @@ public void works_with_slash_which_doesnt_need_escaping_in_java() { public void works_digit_and_word() { TreeRegexp tr = new TreeRegexp("^(\\d) (\\w+) (\\w+)$"); Group g = tr.match("2 you привет"); + requireNonNull(g); assertEquals(3, g.getChildren().size()); } @@ -159,6 +174,7 @@ public void works_digit_and_word() { public void captures_start_and_end() { TreeRegexp tr = new TreeRegexp("^the step \"([^\"]*)\" has status \"([^\"]*)\"$"); Group g = tr.match("the step \"a pending step\" has status \"pending\""); + requireNonNull(g); assertEquals(10, g.getChildren().get(0).getStart()); assertEquals(24, g.getChildren().get(0).getEnd()); assertEquals(38, g.getChildren().get(1).getStart()); @@ -169,6 +185,7 @@ public void captures_start_and_end() { public void doesnt_consider_parenthesis_in_character_class_as_group() { TreeRegexp tr = new TreeRegexp("^drawings: ([A-Z_, ()]+)$"); Group g = tr.match("drawings: FU(BAR)"); + requireNonNull(g); assertEquals("drawings: FU(BAR)", g.getValue()); assertEquals("FU(BAR)", g.getChildren().get(0).getValue()); assertEquals(0, g.getChildren().get(0).getChildren().size()); @@ -178,6 +195,7 @@ public void doesnt_consider_parenthesis_in_character_class_as_group() { public void works_with_flags() { TreeRegexp tr = new TreeRegexp(Pattern.compile("HELLO", Pattern.CASE_INSENSITIVE)); Group g = tr.match("hello"); + requireNonNull(g); assertEquals("hello", g.getValue()); } @@ -185,6 +203,7 @@ public void works_with_flags() { public void works_with_inline_flags() { TreeRegexp tr = new TreeRegexp(Pattern.compile("(?i)HELLO")); Group g = tr.match("hello"); + requireNonNull(g); assertEquals("hello", g.getValue()); assertEquals(0, g.getChildren().size()); } @@ -193,6 +212,7 @@ public void works_with_inline_flags() { public void works_with_non_capturing_inline_flags() { TreeRegexp tr = new TreeRegexp(Pattern.compile("(?i:HELLO)")); Group g = tr.match("hello"); + requireNonNull(g); assertEquals("hello", g.getValue()); assertEquals(0, g.getChildren().size()); } @@ -201,6 +221,7 @@ public void works_with_non_capturing_inline_flags() { public void empty_capturing_group() { TreeRegexp tr = new TreeRegexp(Pattern.compile("()")); Group g = tr.match(""); + requireNonNull(g); assertEquals("", g.getValue()); assertEquals(1, g.getChildren().size()); } @@ -209,6 +230,7 @@ public void empty_capturing_group() { public void empty_non_capturing_group() { TreeRegexp tr = new TreeRegexp(Pattern.compile("(?)")); Group g = tr.match(""); + requireNonNull(g); assertEquals("", g.getValue()); assertEquals(0, g.getChildren().size()); } @@ -217,6 +239,7 @@ public void empty_non_capturing_group() { public void empty_look_ahead() { TreeRegexp tr = new TreeRegexp(Pattern.compile("(?<=)")); Group g = tr.match(""); + requireNonNull(g); assertEquals("", g.getValue()); assertEquals(0, g.getChildren().size()); } @@ -230,7 +253,9 @@ public void uses_loaded_pattern_compiler_service() { PatternCompilerProvider.service = (re, flags) -> Pattern.compile(re + "[a-z]", flags); tr = new TreeRegexp(regexp); - assertEquals("1a", tr.match("1a").getValue()); + Group g = tr.match("1a"); + requireNonNull(g); + assertEquals("1a", g.getValue()); PatternCompilerProvider.service = null; } From 7827c6e0baed3a9d69218408b304cb326e061138 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 14 Nov 2025 03:16:59 +0100 Subject: [PATCH 2/9] Wrap match result in optional to avoid nullable in API --- .../CucumberExpression.java | 8 +-- .../cucumberexpressions/Expression.java | 9 ++- .../RegularExpression.java | 8 +-- .../CustomParameterTypeTest.java | 64 ++++++++++--------- 4 files changed, 48 insertions(+), 41 deletions(-) diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java index cd7da0022..3616199fb 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java @@ -1,11 +1,11 @@ package io.cucumber.cucumberexpressions; import org.apiguardian.api.API; -import org.jspecify.annotations.Nullable; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.Function; import java.util.regex.Pattern; @@ -132,10 +132,10 @@ private void assertNoNodeOfType(Node.Type nodeType, Node node, @Override - public @Nullable List> match(String text, Type... typeHints) { + public Optional>> match(String text, Type... typeHints) { final Group group = treeRegexp.match(text); if (group == null) { - return null; + return Optional.empty(); } List> parameterTypes = new ArrayList<>(this.parameterTypes); @@ -148,7 +148,7 @@ private void assertNoNodeOfType(Node.Type nodeType, Node node, } } - return Argument.build(group, parameterTypes); + return Optional.of(Argument.build(group, parameterTypes)); } @Override diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Expression.java b/java/src/main/java/io/cucumber/cucumberexpressions/Expression.java index 1c99abbfc..cb4be7ff5 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/Expression.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Expression.java @@ -1,16 +1,19 @@ package io.cucumber.cucumberexpressions; import org.apiguardian.api.API; -import org.jspecify.annotations.Nullable; import java.lang.reflect.Type; import java.util.List; +import java.util.Optional; import java.util.regex.Pattern; @API(status = API.Status.STABLE) public interface Expression { - - @Nullable List> match(String text, Type... typeHints); + + /** + * Matches a string to an expression. Empty if no match. + */ + Optional>> match(String text, Type... typeHints); Pattern getRegexp(); diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java b/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java index beff2fd20..ab3c5786c 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java @@ -1,11 +1,11 @@ package io.cucumber.cucumberexpressions; import org.apiguardian.api.API; -import org.jspecify.annotations.Nullable; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.regex.Pattern; import static io.cucumber.cucumberexpressions.ParameterType.createAnonymousParameterType; @@ -31,10 +31,10 @@ public final class RegularExpression implements Expression { } @Override - public @Nullable List> match(String text, Type... typeHints) { + public Optional>> match(String text, Type... typeHints) { final Group group = treeRegexp.match(text); if (group == null) { - return null; + return Optional.empty(); } final ParameterByTypeTransformer defaultTransformer = parameterTypeRegistry.getDefaultParameterTransformer(); @@ -70,7 +70,7 @@ public final class RegularExpression implements Expression { parameterTypes.add(parameterType); } - return Argument.build(group, parameterTypes); + return Optional.of(Argument.build(group, parameterTypes)); } @Override diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java index 0962986e1..1cc3aee3a 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java @@ -1,5 +1,7 @@ package io.cucumber.cucumberexpressions; +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.InstanceOfAssertFactories; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -7,6 +9,7 @@ import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.regex.Pattern; import static java.lang.Integer.parseInt; @@ -54,10 +57,7 @@ void throws_exception_for_illegal_character_in_parameter_name() { void matches_CucumberExpression_parameters_with_custom_parameter_type() { var expression = new CucumberExpression("I have a {color} ball", parameterTypeRegistry); var arguments = expression.match("I have a red ball"); - - assertThat(arguments).singleElement() - .extracting(Argument::getValue) - .isEqualTo(new Color("red")); + asserThatSingleArgumentValue(arguments).isEqualTo(new Color("red")); } @Test @@ -75,8 +75,10 @@ void matches_CucumberExpression_parameters_with_multiple_capture_groups() { var arguments = expression.match("A 5 thick line from 10,20,30 to 40,50,60"); assertThat(arguments) + .get() + .asInstanceOf(InstanceOfAssertFactories.LIST) + .map(Argument.class::cast) .extracting(Argument::getValue) - .map(Object.class::cast) .containsExactly( 5, new Coordinate(10, 20, 30), @@ -100,10 +102,8 @@ void warns_when_CucumberExpression_parameters_with_multiple_capture_groups_has_a var expression = new CucumberExpression("A {int} thick line from {coordinate} to {coordinate}", parameterTypeRegistry); var arguments = expression.match("A 5 thick line from 10,20,30 to 40,50,60"); - assertDoesNotThrow(() -> { - requireNonNull(arguments).get(0).getValue(); - }); - var exception = assertThrows(CucumberExpressionException.class, () -> requireNonNull(arguments).get(1).getValue()); + assertDoesNotThrow(() -> getArgumentValue(arguments, 0)); + var exception = assertThrows(CucumberExpressionException.class, () -> getArgumentValue(arguments, 1)); assertThat(exception).hasMessage( "ParameterType {coordinate} was registered with a Transformer but has multiple capture groups [(\\d+),\\s*(\\d+),\\s*(\\d+)]. " + "Did you mean to use a CaptureGroupTransformer?" @@ -114,15 +114,13 @@ void warns_when_CucumberExpression_parameters_with_multiple_capture_groups_has_a void warns_when_anonymous_parameter_has_multiple_capture_groups() { parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); Expression expression = new RegularExpression(Pattern.compile("^A (\\d+) thick line from ((\\d+),\\s*(\\d+),\\s*(\\d+)) to ((\\d+),\\s*(\\d+),\\s*(\\d+))$"), parameterTypeRegistry); - List> arguments = expression.match("A 5 thick line from 10,20,30 to 40,50,60", + var arguments = expression.match("A 5 thick line from 10,20,30 to 40,50,60", Integer.class, Coordinate.class, Coordinate.class); assertNotNull(arguments); - assertDoesNotThrow(() -> { - arguments.get(0).getValue(); - }); + assertDoesNotThrow(() -> getArgumentValue(arguments, 0)); - var exception = assertThrows(CucumberExpressionException.class, () -> arguments.get(1).getValue()); + var exception = assertThrows(CucumberExpressionException.class, () -> getArgumentValue(arguments, 1)); assertThat(exception).hasMessage( "Anonymous ParameterType has multiple capture groups [(\\d+),\\s*(\\d+),\\s*(\\d+)]. " + "You can only use a single capture group in an anonymous ParameterType." @@ -143,9 +141,7 @@ void matches_CucumberExpression_parameters_with_custom_parameter_type_using_opti var expression = new CucumberExpression("I have a {color} ball", parameterTypeRegistry); var match = expression.match("I have a dark red ball"); - assertThat(match).singleElement() - .extracting(Argument::getValue) - .isEqualTo(new Color("dark red")); + asserThatSingleArgumentValue(match).isEqualTo(new Color("dark red")); } @Test @@ -163,7 +159,7 @@ void defers_transformation_until_queried_from_argument() { var expression = new CucumberExpression("I have a {throwing} parameter", parameterTypeRegistry); var arguments = expression.match("I have a bad parameter"); - var exception = assertThrows(RuntimeException.class, () -> requireNonNull(arguments).get(0).getValue()); + var exception = assertThrows(RuntimeException.class, () -> getArgumentValue(arguments, 0)); assertThat(exception).hasMessage("ParameterType {throwing} failed to transform [bad] to " + CssColor.class, exception.getMessage()); } @@ -205,15 +201,11 @@ void conflicting_parameter_type_is_not_detected_for_regexp() { false )); - var cssColorMatch = new CucumberExpression("I have a {css-color} ball", parameterTypeRegistry).match("I have a blue ball"); - assertThat(cssColorMatch).singleElement() - .extracting(Argument::getValue) - .isEqualTo(new CssColor("blue")); + var cssColorArguments = new CucumberExpression("I have a {css-color} ball", parameterTypeRegistry).match("I have a blue ball"); + asserThatSingleArgumentValue(cssColorArguments).isEqualTo(new CssColor("blue")); - var colorMatch = new CucumberExpression("I have a {color} ball", parameterTypeRegistry).match("I have a blue ball"); - assertThat(colorMatch).singleElement() - .extracting(Argument::getValue) - .isEqualTo(new Color("blue")); + var colorArguments = new CucumberExpression("I have a {color} ball", parameterTypeRegistry).match("I have a blue ball"); + asserThatSingleArgumentValue(colorArguments).isEqualTo(new Color("blue")); } @@ -230,10 +222,22 @@ void matches_RegularExpression_arguments_with_custom_parameter_type_without_name )); var expression = new RegularExpression(compile("I have a (red|blue|yellow) ball"), parameterTypeRegistry); - var match = expression.match("I have a red ball"); - assertThat(match).singleElement() - .extracting(Argument::getValue) - .isEqualTo(new Color("red")); + var arguments = expression.match("I have a red ball"); + asserThatSingleArgumentValue(arguments).isEqualTo(new Color("red")); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static void getArgumentValue(Optional>> match, int index) { + match.ifPresent(arguments -> arguments.get(index).getValue()); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static AbstractObjectAssert asserThatSingleArgumentValue(Optional>> match) { + return assertThat(match).get() + .asInstanceOf(InstanceOfAssertFactories.LIST) + .map(Argument.class::cast) + .singleElement() + .extracting(Argument::getValue); } private record Coordinate(int x, int y, int z) { From d5334088a0fff8309fa9d58d5333d4cbb32f822a Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 14 Nov 2025 03:31:32 +0100 Subject: [PATCH 3/9] Wrap match result in optional to avoid nullable in API --- .../ParameterTypeRegistry.java | 12 +- .../CucumberExpressionGeneratorTest.java | 15 +-- .../CucumberExpressionTest.java | 26 +++-- .../EnumParameterTypeTest.java | 17 ++- .../GenericParameterTypeTest.java | 18 ++- .../ParameterTypeRegistryTest.java | 105 ++++++++++-------- .../RegularExpressionTest.java | 29 +++-- 7 files changed, 132 insertions(+), 90 deletions(-) diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/ParameterTypeRegistry.java b/java/src/main/java/io/cucumber/cucumberexpressions/ParameterTypeRegistry.java index 8cde5b6c1..a900a2242 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/ParameterTypeRegistry.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/ParameterTypeRegistry.java @@ -139,15 +139,13 @@ private ParameterTypeRegistry(ParameterByTypeTransformer defaultParameterTransfo } public void defineParameterType(ParameterType parameterType) { - if (parameterType.getName() != null) { - if (parameterTypeByName.containsKey(parameterType.getName())) { - if (parameterType.getName().isEmpty()) { - throw new DuplicateTypeNameException("The anonymous parameter type has already been defined"); - } - throw new DuplicateTypeNameException(String.format("There is already a parameter type with name %s", parameterType.getName())); + if (parameterTypeByName.containsKey(parameterType.getName())) { + if (parameterType.getName().isEmpty()) { + throw new DuplicateTypeNameException("The anonymous parameter type has already been defined"); } - parameterTypeByName.put(parameterType.getName(), parameterType); + throw new DuplicateTypeNameException(String.format("There is already a parameter type with name %s", parameterType.getName())); } + parameterTypeByName.put(parameterType.getName(), parameterType); for (String parameterTypeRegexp : parameterType.getRegexps()) { if (!parameterTypesByRegexp.containsKey(parameterTypeRegexp)) { diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionGeneratorTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionGeneratorTest.java index eaf540db0..49b3be0ff 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionGeneratorTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionGeneratorTest.java @@ -11,6 +11,7 @@ import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Optional; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -34,20 +35,20 @@ public void documents_expression_generation() { @Test public void generates_expression_for_no_args() { - assertExpression("hello", Collections.emptyList(), "hello"); + assertExpression("hello", Collections.emptyList(), "hello"); } @Test public void generates_expression_with_escaped_left_parenthesis() { assertExpression( - "\\(iii)", Collections.emptyList(), + "\\(iii)", Collections.emptyList(), "(iii)"); } @Test public void generates_expression_with_escaped_left_curly_brace() { assertExpression( - "\\{iii}", Collections.emptyList(), + "\\{iii}", Collections.emptyList(), "{iii}"); } @@ -131,7 +132,7 @@ public void does_not_suggest_parameter_type_when_surrounded_by_alphanum() { false )); assertExpression( - "I like muppets", Collections.emptyList(), + "I like muppets", Collections.emptyList(), "I like muppets"); } @@ -318,11 +319,11 @@ private void assertExpression(String expectedExpression, List expectedAr // Check that the generated expression matches the text CucumberExpression cucumberExpression = new CucumberExpression(generatedExpression.getSource(), parameterTypeRegistry); - List> match = cucumberExpression.match(text); - if (match == null) { + Optional>> match = cucumberExpression.match(text); + if (match.isEmpty()) { fail(String.format("Expected text '%s' to match generated expression '%s'", text, generatedExpression.getSource())); } - assertEquals(expectedArgumentNames.size(), match.size()); + assertEquals(expectedArgumentNames.size(), match.get().size()); } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java index 634c5bb40..1b942ec97 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java @@ -22,10 +22,12 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import static java.nio.file.Files.newDirectoryStream; @@ -60,8 +62,9 @@ static List acceptance_tests_pass() throws IOException { void acceptance_tests_pass(@ConvertWith(Converter.class) Expectation expectation) { if (expectation.exception == null) { CucumberExpression expression = new CucumberExpression(expectation.expression, parameterTypeRegistry); - List> match = expression.match(requireNonNull(expectation.text)); - List values = match == null ? null : match.stream() + Optional>> match = expression.match(requireNonNull(expectation.text)); + List values = match.isEmpty() ? null : match.stream() + .flatMap(Collection::stream) .map(Argument::getValue) .collect(Collectors.toList()); @@ -99,9 +102,9 @@ void matches_anonymous_parameter_type_with_hint() { void documents_match_arguments() { String expr = "I have {int} cuke(s)"; Expression expression = new CucumberExpression(expr, parameterTypeRegistry); - List> args = expression.match("I have 7 cukes"); + Optional>> args = expression.match("I have 7 cukes"); assertNotNull(args); - assertEquals(7, args.get(0).getValue()); + assertEquals(7, args.get().get(0).getValue()); } @Test @@ -179,16 +182,15 @@ private List match(String expr, String text, Locale locale, Type... typeHints @Nullable private List match(String expr, String text, ParameterTypeRegistry parameterTypeRegistry, Type... typeHints) { CucumberExpression expression = new CucumberExpression(expr, parameterTypeRegistry); - List> args = expression.match(text, typeHints); - if (args == null) { + Optional>> match = expression.match(text, typeHints); + if (match.isEmpty()) { return null; } else { - List list = new ArrayList<>(); - for (Argument arg : args) { - Object value = arg.getValue(); - list.add(value); - } - return list; + return match.stream() + .flatMap(Collection::stream) + .map(Argument::getValue) + .map(Object.class::cast) + .toList(); } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/EnumParameterTypeTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/EnumParameterTypeTest.java index 8ff3fb38a..98cc340a6 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/EnumParameterTypeTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/EnumParameterTypeTest.java @@ -1,8 +1,12 @@ package io.cucumber.cucumberexpressions; +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import java.util.List; import java.util.Locale; +import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -21,9 +25,16 @@ void converts_to_enum() { var expression = new CucumberExpression("I am {Mood}", registry); var args = expression.match("I am happy"); - assertThat(args).singleElement() - .extracting(Argument::getValue) - .isEqualTo(Mood.happy); + asserThatSingleArgumentValue(args).isEqualTo(Mood.happy); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static AbstractObjectAssert asserThatSingleArgumentValue(Optional>> match) { + return assertThat(match).get() + .asInstanceOf(InstanceOfAssertFactories.LIST) + .map(Argument.class::cast) + .singleElement() + .extracting(Argument::getValue); } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/GenericParameterTypeTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/GenericParameterTypeTest.java index 3458f3fbf..a95a46d90 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/GenericParameterTypeTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/GenericParameterTypeTest.java @@ -1,10 +1,13 @@ package io.cucumber.cucumberexpressions; +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.InstanceOfAssertFactories; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import java.util.List; import java.util.Locale; +import java.util.Optional; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -19,16 +22,23 @@ public void transforms_to_a_list_of_string() { parameterTypeRegistry.defineParameterType(new ParameterType<>( "stringlist", singletonList(".*"), - new TypeReference>() {}.getType(), + new TypeReference>() { + }.getType(), (@Nullable String arg) -> asList(requireNonNull(arg).split(",")), false, false) ); var expression = new CucumberExpression("I have {stringlist} yay", parameterTypeRegistry); var args = expression.match("I have three,blind,mice yay"); - assertThat(args).singleElement() - .extracting(Argument::getValue) - .isEqualTo(asList("three", "blind", "mice")); + asserThatSingleArgumentValue(args).isEqualTo(asList("three", "blind", "mice")); } + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static AbstractObjectAssert asserThatSingleArgumentValue(Optional>> match) { + return assertThat(match).get() + .asInstanceOf(InstanceOfAssertFactories.LIST) + .map(Argument.class::cast) + .singleElement() + .extracting(Argument::getValue); + } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeRegistryTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeRegistryTest.java index 2b42d2f0d..732e20a94 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeRegistryTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeRegistryTest.java @@ -1,11 +1,15 @@ package io.cucumber.cucumberexpressions; +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; -import org.junit.jupiter.params.shadow.de.siegmar.fastcsv.util.Nullable; import java.math.BigDecimal; +import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; @@ -89,7 +93,7 @@ public void throws_ambiguous_exception_on_lookup_when_no_parameter_types_are_pre @Test public void does_not_allow_anonymous_parameter_type_to_be_registered() { - Executable testMethod = () -> registry.defineParameterType(new ParameterType<>("", ".*", Object.class, (Transformer) arg -> arg)); + Executable testMethod = () -> registry.defineParameterType(new ParameterType<>("", ".*", Object.class, (@Nullable String arg) -> arg)); var exception = assertThrows(DuplicateTypeNameException.class, testMethod); assertThat(exception).hasMessage("The anonymous parameter type has already been defined"); @@ -100,39 +104,39 @@ public void parse_decimal_numbers_in_english() { ExpressionFactory factory = new ExpressionFactory(new ParameterTypeRegistry(Locale.ENGLISH)); Expression expression = factory.createExpression("{bigdecimal}"); - assertThat(expression.match("")).isNull(); - assertThat(expression.match(".")).isNull(); - assertThat(expression.match(",")).isNull(); - assertThat(expression.match("-")).isNull(); - assertThat(expression.match("E")).isNull(); - assertThat(expression.match("1,")).isNull(); - assertThat(expression.match(",1")).isNull(); - assertThat(expression.match("1.")).isNull(); - - assertThat(expression.match("1")).singleElement().extracting(Argument::getValue).isEqualTo(BigDecimal.ONE); - assertThat(expression.match("-1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1")); - assertThat(expression.match("1.1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1.1")); - assertThat(expression.match("1,000")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000")); - assertThat(expression.match("1,000,0")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("10000")); - assertThat(expression.match("1,000.1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000.1")); - assertThat(expression.match("1,000,10")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("100010")); - assertThat(expression.match("1,0.1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("10.1")); - assertThat(expression.match("1,000,000.1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000000.1")); - assertThat(expression.match("-1.1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1.1")); - - assertThat(expression.match(".1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("0.1")); - assertThat(expression.match("-.1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-0.1")); - assertThat(expression.match("-.10000001")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-0.10000001")); + assertThat(expression.match("")).isEmpty(); + assertThat(expression.match(".")).isEmpty(); + assertThat(expression.match(",")).isEmpty(); + assertThat(expression.match("-")).isEmpty(); + assertThat(expression.match("E")).isEmpty(); + assertThat(expression.match("1,")).isEmpty(); + assertThat(expression.match(",1")).isEmpty(); + assertThat(expression.match("1.")).isEmpty(); + + asserThatSingleArgumentValue(expression.match("1")).isEqualTo(BigDecimal.ONE); + asserThatSingleArgumentValue(expression.match("-1")).isEqualTo(new BigDecimal("-1")); + asserThatSingleArgumentValue(expression.match("1.1")).isEqualTo(new BigDecimal("1.1")); + asserThatSingleArgumentValue(expression.match("1,000")).isEqualTo(new BigDecimal("1000")); + asserThatSingleArgumentValue(expression.match("1,000,0")).isEqualTo(new BigDecimal("10000")); + asserThatSingleArgumentValue(expression.match("1,000.1")).isEqualTo(new BigDecimal("1000.1")); + asserThatSingleArgumentValue(expression.match("1,000,10")).isEqualTo(new BigDecimal("100010")); + asserThatSingleArgumentValue(expression.match("1,0.1")).isEqualTo(new BigDecimal("10.1")); + asserThatSingleArgumentValue(expression.match("1,000,000.1")).isEqualTo(new BigDecimal("1000000.1")); + asserThatSingleArgumentValue(expression.match("-1.1")).isEqualTo(new BigDecimal("-1.1")); + + asserThatSingleArgumentValue(expression.match(".1")).isEqualTo(new BigDecimal("0.1")); + asserThatSingleArgumentValue(expression.match("-.1")).isEqualTo(new BigDecimal("-0.1")); + asserThatSingleArgumentValue(expression.match("-.10000001")).isEqualTo(new BigDecimal("-0.10000001")); // precision 1 with scale -1, can not be expressed as a decimal - assertThat(expression.match("1E1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1E1")); - assertThat(expression.match(".1E1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1")); - assertThat(expression.match("E1")).isNull(); - assertThat(expression.match("-.1E-1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-0.01")); - assertThat(expression.match("-.1E-2")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-0.001")); - assertThat(expression.match("-.1E+1")).isNull(); - assertThat(expression.match("-.1E+2")).isNull(); - assertThat(expression.match("-.1E1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1")); - assertThat(expression.match("-.10E2")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-10")); + asserThatSingleArgumentValue(expression.match("1E1")).isEqualTo(new BigDecimal("1E1")); + asserThatSingleArgumentValue(expression.match(".1E1")).isEqualTo(new BigDecimal("1")); + assertThat(expression.match("E1")).isEmpty(); + asserThatSingleArgumentValue(expression.match("-.1E-1")).isEqualTo(new BigDecimal("-0.01")); + asserThatSingleArgumentValue(expression.match("-.1E-2")).isEqualTo(new BigDecimal("-0.001")); + assertThat(expression.match("-.1E+1")).isEmpty(); + assertThat(expression.match("-.1E+2")).isEmpty(); + asserThatSingleArgumentValue(expression.match("-.1E1")).isEqualTo(new BigDecimal("-1")); + asserThatSingleArgumentValue(expression.match("-.10E2")).isEqualTo(new BigDecimal("-10")); } @Test @@ -140,10 +144,10 @@ public void parse_decimal_numbers_in_german() { ExpressionFactory factory = new ExpressionFactory(new ParameterTypeRegistry(Locale.GERMAN)); Expression expression = factory.createExpression("{bigdecimal}"); - assertThat(expression.match("1.000,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000.1")); - assertThat(expression.match("1.000.000,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000000.1")); - assertThat(expression.match("-1,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1.1")); - assertThat(expression.match("-,1E1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1")); + asserThatSingleArgumentValue(expression.match("1.000,1")).isEqualTo(new BigDecimal("1000.1")); + asserThatSingleArgumentValue(expression.match("1.000.000,1")).isEqualTo(new BigDecimal("1000000.1")); + asserThatSingleArgumentValue(expression.match("-1,1")).isEqualTo(new BigDecimal("-1.1")); + asserThatSingleArgumentValue(expression.match("-,1E1")).isEqualTo(new BigDecimal("-1")); } @Test @@ -151,10 +155,10 @@ public void parse_decimal_numbers_in_canadian_french() { ExpressionFactory factory = new ExpressionFactory(new ParameterTypeRegistry(Locale.CANADA_FRENCH)); Expression expression = factory.createExpression("{bigdecimal}"); - assertThat(expression.match("1.000,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000.1")); - assertThat(expression.match("1.000.000,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000000.1")); - assertThat(expression.match("-1,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1.1")); - assertThat(expression.match("-,1E1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1")); + asserThatSingleArgumentValue(expression.match("1.000,1")).isEqualTo(new BigDecimal("1000.1")); + asserThatSingleArgumentValue(expression.match("1.000.000,1")).isEqualTo(new BigDecimal("1000000.1")); + asserThatSingleArgumentValue(expression.match("-1,1")).isEqualTo(new BigDecimal("-1.1")); + asserThatSingleArgumentValue(expression.match("-,1E1")).isEqualTo(new BigDecimal("-1")); } @Test @@ -162,10 +166,19 @@ public void parse_decimal_numbers_in_norwegian() { ExpressionFactory factory = new ExpressionFactory(new ParameterTypeRegistry(Locale.forLanguageTag("no"))); Expression expression = factory.createExpression("{bigdecimal}"); - assertThat(expression.match("1.000,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000.1")); - assertThat(expression.match("1.000.000,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("1000000.1")); - assertThat(expression.match("-1,1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1.1")); - assertThat(expression.match("-,1E1")).singleElement().extracting(Argument::getValue).isEqualTo(new BigDecimal("-1")); + asserThatSingleArgumentValue(expression.match("1.000,1")).isEqualTo(new BigDecimal("1000.1")); + asserThatSingleArgumentValue(expression.match("1.000.000,1")).isEqualTo(new BigDecimal("1000000.1")); + asserThatSingleArgumentValue(expression.match("-1,1")).isEqualTo(new BigDecimal("-1.1")); + asserThatSingleArgumentValue(expression.match("-,1E1")).isEqualTo(new BigDecimal("-1")); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static AbstractObjectAssert asserThatSingleArgumentValue(Optional>> match) { + return assertThat(match).get() + .asInstanceOf(InstanceOfAssertFactories.LIST) + .map(Argument.class::cast) + .singleElement() + .extracting(Argument::getValue); } public static class Name { diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/RegularExpressionTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/RegularExpressionTest.java index cb16d728e..01214e218 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/RegularExpressionTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/RegularExpressionTest.java @@ -1,5 +1,6 @@ package io.cucumber.cucumberexpressions; +import org.assertj.core.api.InstanceOfAssertFactories; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; @@ -17,10 +18,12 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -48,8 +51,9 @@ static List acceptance_tests_pass() throws IOException { @MethodSource void acceptance_tests_pass(@ConvertWith(Converter.class) Expectation expectation) { RegularExpression expression = new RegularExpression(Pattern.compile(expectation.expression), parameterTypeRegistry); - List> match = expression.match(expectation.text); - List values = match == null ? null : match.stream() + Optional>> match = expression.match(expectation.text); + List values = match.isEmpty() ? null : match.stream() + .flatMap(Collection::stream) .map(Argument::getValue) .collect(Collectors.toList()); @@ -60,8 +64,12 @@ void acceptance_tests_pass(@ConvertWith(Converter.class) Expectation expectation void documentation_match_arguments() { Pattern expr = Pattern.compile("I have (\\d+) cukes? in my (\\w+) now"); Expression expression = new RegularExpression(expr, parameterTypeRegistry); - List> match = expression.match("I have 7 cukes in my belly now"); - assertThat(match).extracting(Argument::getValue).map(Object.class::cast).containsExactly(7, "belly"); + Optional>> match = expression.match("I have 7 cukes in my belly now"); + assertThat(match).get() + .asInstanceOf(InstanceOfAssertFactories.LIST) + .map(Argument.class::cast) + .map(Argument::getValue) + .containsExactly(7, "belly"); } @Test @@ -236,15 +244,14 @@ void parameter_types_can_be_optional_when_used_in_regex() { private List match(Pattern pattern, String text, Type... types) { RegularExpression regularExpression = new RegularExpression(pattern, parameterTypeRegistry); - List> arguments = regularExpression.match(text, types); - List values = new ArrayList<>(); - for (Argument argument : requireNonNull(arguments)) { - values.add(argument.getValue()); - } - return values; + Optional>> match = regularExpression.match(text, types); + return match.stream() + .flatMap(Collection::stream) + .map(Argument::getValue) + .map(Object.class::cast) + .toList(); } - record Expectation(String expression, String text, List expectedArgs) { } From 1c96be2bae8394ea4eca3f136ab7b00f3a6f1085 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 14 Nov 2025 03:35:50 +0100 Subject: [PATCH 4/9] Simplify --- .../CucumberExpressionGeneratorTest.java | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionGeneratorTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionGeneratorTest.java index 49b3be0ff..7345b5cb3 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionGeneratorTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionGeneratorTest.java @@ -1,7 +1,7 @@ package io.cucumber.cucumberexpressions; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.shadow.de.siegmar.fastcsv.util.Nullable; import java.text.DateFormat; import java.text.ParseException; @@ -14,7 +14,9 @@ import java.util.Optional; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -76,7 +78,7 @@ public void generates_expression_for_numbers_with_symbols_and_currency() { @Test public void generates_expression_for_numbers_with_text_on_both_sides() { assertExpression( - "i18n", asList(), + "i18n", emptyList(), "i18n"); } @@ -114,7 +116,7 @@ public void numbers_only_second_argument_when_type_is_not_reserved_keyword() { "currency", "[A-Z]{3}", Currency.class, - (Transformer) Currency::getInstance + (@Nullable String s) -> Currency.getInstance(requireNonNull(s)) )); assertExpression( "I have a {currency} account and a {currency} account", asList("currency", "currency2"), @@ -127,7 +129,7 @@ public void does_not_suggest_parameter_type_when_surrounded_by_alphanum() { "direction", "(up|down)", String.class, - (Transformer) arg -> arg, + (@Nullable String arg) -> arg, true, false )); @@ -142,7 +144,7 @@ public void does_suggest_parameter_type_when_surrounded_by_space() { "direction", "(up|down)", String.class, - (Transformer) arg -> arg, + (@Nullable String arg) -> arg, true, false )); @@ -157,13 +159,13 @@ public void prefers_leftmost_match_when_there_is_overlap() { "right", "c d", String.class, - (Transformer) s -> s + (@Nullable String arg) -> arg )); parameterTypeRegistry.defineParameterType(new ParameterType<>( "left", "b c", String.class, - (Transformer) arg -> arg + (@Nullable String arg) -> arg )); assertExpression( "a {left} d e f g", singletonList("left"), @@ -176,13 +178,13 @@ public void prefers_widest_match_when_pos_is_same() { "airport", "[A-Z]{3}", String.class, - (Transformer) s -> s + (@Nullable String arg) -> arg )); parameterTypeRegistry.defineParameterType(new ParameterType<>( "leg", "[A-Z]{3}-[A-Z]{3}", String.class, - (Transformer) s -> s + (@Nullable String arg) -> arg )); assertExpression( "leg {leg}", singletonList("leg"), @@ -195,7 +197,7 @@ public void generates_all_combinations_of_expressions_when_several_parameter_typ "currency", "x", Currency.class, - (Transformer) Currency::getInstance, + (@Nullable String s) -> Currency.getInstance(requireNonNull(s)), true, true )); @@ -248,7 +250,7 @@ public void matches_parameter_types_with_optional_capture_groups() { "optional-flight", "(1st flight)?", String.class, - (Transformer) arg -> arg, + (@Nullable String arg) -> arg, true, false ); @@ -256,7 +258,7 @@ public void matches_parameter_types_with_optional_capture_groups() { "optional-hotel", "(1 hotel)?", String.class, - (Transformer) arg -> arg, + (@Nullable String arg) -> arg, true, false ); @@ -274,7 +276,7 @@ public void generates_at_most_256_expressions() { "my-type-" + i, "[a-z]", String.class, - (Transformer) arg -> arg, + (@Nullable String arg) -> arg, true, false ); @@ -291,7 +293,7 @@ public void prefers_expression_with_longest_non_empty_match() { "zero-or-more", "[a-z]*", String.class, - (Transformer) arg -> arg, + (@Nullable String arg) -> arg, true, false ); @@ -300,7 +302,7 @@ public void prefers_expression_with_longest_non_empty_match() { "exactly-one", "[a-z]", String.class, - (Transformer) arg -> arg, + (@Nullable String arg) -> arg, true, false ); From b62c52394c389b65beb08297b7b35438918208ec Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 14 Nov 2025 14:11:11 +0100 Subject: [PATCH 5/9] Remove duplicates --- .../cucumberexpressions/Assertions.java | 25 +++++++++++++++++++ .../CustomParameterTypeTest.java | 11 +------- .../EnumParameterTypeTest.java | 16 +----------- .../GenericParameterTypeTest.java | 13 +--------- .../ParameterTypeRegistryTest.java | 14 +---------- 5 files changed, 29 insertions(+), 50 deletions(-) create mode 100644 java/src/test/java/io/cucumber/cucumberexpressions/Assertions.java diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/Assertions.java b/java/src/test/java/io/cucumber/cucumberexpressions/Assertions.java new file mode 100644 index 000000000..cd21bcc61 --- /dev/null +++ b/java/src/test/java/io/cucumber/cucumberexpressions/Assertions.java @@ -0,0 +1,25 @@ +package io.cucumber.cucumberexpressions; + +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.InstanceOfAssertFactories; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +final class Assertions { + + private Assertions(){ + // utility class + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + static AbstractObjectAssert asserThatSingleArgumentValue(Optional>> match) { + return assertThat(match).get() + .asInstanceOf(InstanceOfAssertFactories.LIST) + .map(Argument.class::cast) + .singleElement() + .extracting(Argument::getValue); + } +} diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java index 1cc3aee3a..0677633dd 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java @@ -1,6 +1,5 @@ package io.cucumber.cucumberexpressions; -import org.assertj.core.api.AbstractObjectAssert; import org.assertj.core.api.InstanceOfAssertFactories; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; @@ -12,6 +11,7 @@ import java.util.Optional; import java.util.regex.Pattern; +import static io.cucumber.cucumberexpressions.Assertions.asserThatSingleArgumentValue; import static java.lang.Integer.parseInt; import static java.util.Arrays.asList; import static java.util.Objects.requireNonNull; @@ -230,15 +230,6 @@ void matches_RegularExpression_arguments_with_custom_parameter_type_without_name private static void getArgumentValue(Optional>> match, int index) { match.ifPresent(arguments -> arguments.get(index).getValue()); } - - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private static AbstractObjectAssert asserThatSingleArgumentValue(Optional>> match) { - return assertThat(match).get() - .asInstanceOf(InstanceOfAssertFactories.LIST) - .map(Argument.class::cast) - .singleElement() - .extracting(Argument::getValue); - } private record Coordinate(int x, int y, int z) { diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/EnumParameterTypeTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/EnumParameterTypeTest.java index 98cc340a6..56889497c 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/EnumParameterTypeTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/EnumParameterTypeTest.java @@ -1,14 +1,10 @@ package io.cucumber.cucumberexpressions; -import org.assertj.core.api.AbstractObjectAssert; -import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; -import java.util.List; import java.util.Locale; -import java.util.Optional; -import static org.assertj.core.api.Assertions.assertThat; +import static io.cucumber.cucumberexpressions.Assertions.asserThatSingleArgumentValue; class EnumParameterTypeTest { @@ -27,14 +23,4 @@ void converts_to_enum() { var args = expression.match("I am happy"); asserThatSingleArgumentValue(args).isEqualTo(Mood.happy); } - - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private static AbstractObjectAssert asserThatSingleArgumentValue(Optional>> match) { - return assertThat(match).get() - .asInstanceOf(InstanceOfAssertFactories.LIST) - .map(Argument.class::cast) - .singleElement() - .extracting(Argument::getValue); - } - } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/GenericParameterTypeTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/GenericParameterTypeTest.java index a95a46d90..c4f7b9d45 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/GenericParameterTypeTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/GenericParameterTypeTest.java @@ -1,18 +1,15 @@ package io.cucumber.cucumberexpressions; -import org.assertj.core.api.AbstractObjectAssert; -import org.assertj.core.api.InstanceOfAssertFactories; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import java.util.List; import java.util.Locale; -import java.util.Optional; +import static io.cucumber.cucumberexpressions.Assertions.asserThatSingleArgumentValue; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; -import static org.assertj.core.api.Assertions.assertThat; public class GenericParameterTypeTest { @@ -33,12 +30,4 @@ public void transforms_to_a_list_of_string() { asserThatSingleArgumentValue(args).isEqualTo(asList("three", "blind", "mice")); } - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private static AbstractObjectAssert asserThatSingleArgumentValue(Optional>> match) { - return assertThat(match).get() - .asInstanceOf(InstanceOfAssertFactories.LIST) - .map(Argument.class::cast) - .singleElement() - .extracting(Argument::getValue); - } } diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeRegistryTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeRegistryTest.java index 732e20a94..4bb8e154b 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeRegistryTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeRegistryTest.java @@ -1,17 +1,14 @@ package io.cucumber.cucumberexpressions; -import org.assertj.core.api.AbstractObjectAssert; -import org.assertj.core.api.InstanceOfAssertFactories; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import java.math.BigDecimal; -import java.util.List; import java.util.Locale; -import java.util.Optional; import java.util.regex.Pattern; +import static io.cucumber.cucumberexpressions.Assertions.asserThatSingleArgumentValue; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -171,15 +168,6 @@ public void parse_decimal_numbers_in_norwegian() { asserThatSingleArgumentValue(expression.match("-1,1")).isEqualTo(new BigDecimal("-1.1")); asserThatSingleArgumentValue(expression.match("-,1E1")).isEqualTo(new BigDecimal("-1")); } - - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private static AbstractObjectAssert asserThatSingleArgumentValue(Optional>> match) { - return assertThat(match).get() - .asInstanceOf(InstanceOfAssertFactories.LIST) - .map(Argument.class::cast) - .singleElement() - .extracting(Argument::getValue); - } public static class Name { Name(@Nullable String s) { From 7bd40bb16a2ec499a9da1a9a81f9d4d52b99c576 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 14 Nov 2025 17:45:41 +0100 Subject: [PATCH 6/9] Remove redundant AST class --- .../io/cucumber/cucumberexpressions/Ast.java | 145 ------------------ .../CucumberExpressionException.java | 2 - .../CucumberExpressionParser.java | 17 +- .../CucumberExpressionTokenizer.java | 1 - .../cucumber/cucumberexpressions/Located.java | 9 ++ .../io/cucumber/cucumberexpressions/Node.java | 2 +- .../cucumber/cucumberexpressions/Token.java | 134 ++++++++++++++++ .../CucumberExpressionTokenizerTest.java | 1 - 8 files changed, 152 insertions(+), 159 deletions(-) delete mode 100644 java/src/main/java/io/cucumber/cucumberexpressions/Ast.java create mode 100644 java/src/main/java/io/cucumber/cucumberexpressions/Located.java create mode 100644 java/src/main/java/io/cucumber/cucumberexpressions/Token.java diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java b/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java deleted file mode 100644 index 8e1c67c84..000000000 --- a/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java +++ /dev/null @@ -1,145 +0,0 @@ -package io.cucumber.cucumberexpressions; - -import org.jspecify.annotations.Nullable; - -import java.util.Objects; -import java.util.StringJoiner; - -import static java.util.Objects.requireNonNull; - -final class Ast { - - interface Located { - int start(); - - int 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 Type type; - final int start; - final int end; - - Token(String text, Type type, int start, int end) { - this.text = requireNonNull(text); - this.type = requireNonNull(type); - this.start = start; - this.end = end; - } - - static boolean canEscape(Integer token) { - if (Character.isWhitespace(token)) { - return true; - } - return switch (token) { - case (int) escapeCharacter, - (int) alternationCharacter, - (int) beginParameterCharacter, - (int) endParameterCharacter, - (int) beginOptionalCharacter, - (int) endOptionalCharacter -> true; - default -> false; - }; - } - - static Type typeOf(Integer token) { - if (Character.isWhitespace(token)) { - return Type.WHITE_SPACE; - } - return switch (token) { - case (int) alternationCharacter -> Type.ALTERNATION; - case (int) beginParameterCharacter -> Type.BEGIN_PARAMETER; - case (int) endParameterCharacter -> Type.END_PARAMETER; - case (int) beginOptionalCharacter -> Type.BEGIN_OPTIONAL; - case (int) endOptionalCharacter -> Type.END_OPTIONAL; - default -> Type.TEXT; - }; - } - - static boolean isEscapeCharacter(int token) { - return token == escapeCharacter; - } - - @Override - public int start() { - return start; - } - - @Override - public int end() { - return end; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - Token token = (Token) o; - return start == token.start && - end == token.end && - text.equals(token.text) && - type == token.type; - } - - @Override - public int hashCode() { - return Objects.hash(start, end, text, type); - } - - @Override - public String toString() { - return new StringJoiner(", ", "{", "}") - .add("\"type\": \"" + type + "\"") - .add("\"start\": " + start) - .add("\"end\": " + end) - .add("\"text\": \"" + text + "\"") - .toString(); - } - - enum Type { - START_OF_LINE, - END_OF_LINE, - WHITE_SPACE, - BEGIN_OPTIONAL("" + beginOptionalCharacter, "optional text"), - END_OPTIONAL("" + endOptionalCharacter, "optional text"), - BEGIN_PARAMETER("" + beginParameterCharacter, "a parameter"), - END_PARAMETER("" + endParameterCharacter, "a parameter"), - ALTERNATION("" + alternationCharacter, "alternation"), - TEXT; - - private final @Nullable String symbol; - private final @Nullable String purpose; - - Type() { - this(null, null); - } - - Type(@Nullable String symbol, @Nullable String purpose) { - this.symbol = symbol; - this.purpose = purpose; - } - - String purpose() { - return requireNonNull(purpose, name() + " does not have a purpose"); - } - - String symbol() { - return requireNonNull(symbol, name() + " does not have a symbol"); - } - } - - } - -} diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java index 011e708ad..586641ef5 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java @@ -1,7 +1,5 @@ package io.cucumber.cucumberexpressions; -import io.cucumber.cucumberexpressions.Ast.Located; -import io.cucumber.cucumberexpressions.Ast.Token; import org.apiguardian.api.API; @API(status = API.Status.STABLE) diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java index cbd80eda3..59e15b0ad 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java @@ -1,6 +1,5 @@ package io.cucumber.cucumberexpressions; -import io.cucumber.cucumberexpressions.Ast.Token; import org.apiguardian.api.API; import java.util.ArrayList; @@ -11,14 +10,14 @@ 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.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.Token.Type.ALTERNATION; +import static io.cucumber.cucumberexpressions.Token.Type.BEGIN_OPTIONAL; +import static io.cucumber.cucumberexpressions.Token.Type.BEGIN_PARAMETER; +import static io.cucumber.cucumberexpressions.Token.Type.END_OF_LINE; +import static io.cucumber.cucumberexpressions.Token.Type.END_OPTIONAL; +import static io.cucumber.cucumberexpressions.Token.Type.END_PARAMETER; +import static io.cucumber.cucumberexpressions.Token.Type.START_OF_LINE; +import static io.cucumber.cucumberexpressions.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; diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java index cfd1e1c07..1dd0dd9e4 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java @@ -1,6 +1,5 @@ package io.cucumber.cucumberexpressions; -import io.cucumber.cucumberexpressions.Ast.Token; import org.jspecify.annotations.Nullable; import java.util.ArrayList; diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Located.java b/java/src/main/java/io/cucumber/cucumberexpressions/Located.java new file mode 100644 index 000000000..a5bf23ee6 --- /dev/null +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Located.java @@ -0,0 +1,9 @@ +package io.cucumber.cucumberexpressions; + +interface Located { + + int start(); + + int end(); + +} diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Node.java b/java/src/main/java/io/cucumber/cucumberexpressions/Node.java index 5b26eb691..d84f7c8ab 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/Node.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Node.java @@ -11,7 +11,7 @@ import static org.apiguardian.api.API.Status.EXPERIMENTAL; @API(since = "18.1", status = EXPERIMENTAL) -public final class Node implements Ast.Located { +public final class Node implements Located { private final Type type; private final @Nullable List nodes; diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/Token.java b/java/src/main/java/io/cucumber/cucumberexpressions/Token.java new file mode 100644 index 000000000..0bf3ba953 --- /dev/null +++ b/java/src/main/java/io/cucumber/cucumberexpressions/Token.java @@ -0,0 +1,134 @@ +package io.cucumber.cucumberexpressions; + +import org.jspecify.annotations.Nullable; + +import java.util.Objects; +import java.util.StringJoiner; + +import static java.util.Objects.requireNonNull; + +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 Type type; + final int start; + final int end; + + Token(String text, Type type, int start, int end) { + this.text = requireNonNull(text); + this.type = requireNonNull(type); + this.start = start; + this.end = end; + } + + static boolean canEscape(Integer token) { + if (Character.isWhitespace(token)) { + return true; + } + return switch (token) { + case (int) escapeCharacter, + (int) alternationCharacter, + (int) beginParameterCharacter, + (int) endParameterCharacter, + (int) beginOptionalCharacter, + (int) endOptionalCharacter -> true; + default -> false; + }; + } + + static Type typeOf(Integer token) { + if (Character.isWhitespace(token)) { + return Type.WHITE_SPACE; + } + return switch (token) { + case (int) alternationCharacter -> Type.ALTERNATION; + case (int) beginParameterCharacter -> Type.BEGIN_PARAMETER; + case (int) endParameterCharacter -> Type.END_PARAMETER; + case (int) beginOptionalCharacter -> Type.BEGIN_OPTIONAL; + case (int) endOptionalCharacter -> Type.END_OPTIONAL; + default -> Type.TEXT; + }; + } + + static boolean isEscapeCharacter(int token) { + return token == escapeCharacter; + } + + @Override + public int start() { + return start; + } + + @Override + public int end() { + return end; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Token token = (Token) o; + return start == token.start && + end == token.end && + text.equals(token.text) && + type == token.type; + } + + @Override + public int hashCode() { + return Objects.hash(start, end, text, type); + } + + @Override + public String toString() { + return new StringJoiner(", ", "{", "}") + .add("\"type\": \"" + type + "\"") + .add("\"start\": " + start) + .add("\"end\": " + end) + .add("\"text\": \"" + text + "\"") + .toString(); + } + + enum Type { + START_OF_LINE, + END_OF_LINE, + WHITE_SPACE, + BEGIN_OPTIONAL("" + beginOptionalCharacter, "optional text"), + END_OPTIONAL("" + endOptionalCharacter, "optional text"), + BEGIN_PARAMETER("" + beginParameterCharacter, "a parameter"), + END_PARAMETER("" + endParameterCharacter, "a parameter"), + ALTERNATION("" + alternationCharacter, "alternation"), + TEXT; + + private final @Nullable String symbol; + private final @Nullable String purpose; + + Type() { + this(null, null); + } + + Type(@Nullable String symbol, @Nullable String purpose) { + this.symbol = symbol; + this.purpose = purpose; + } + + String purpose() { + return requireNonNull(purpose, name() + " does not have a purpose"); + } + + String symbol() { + return requireNonNull(symbol, name() + " does not have a symbol"); + } + } + +} diff --git a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java index 082645daf..5a1b8f2ec 100644 --- a/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java +++ b/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java @@ -1,6 +1,5 @@ package io.cucumber.cucumberexpressions; -import io.cucumber.cucumberexpressions.Ast.Token; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.extension.ParameterContext; From fdd3c4bdde02b5c0402b66de0c26a7a44acdcfc5 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 14 Nov 2025 17:52:14 +0100 Subject: [PATCH 7/9] Use switch expression --- .../CucumberExpressionParser.java | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java index 59e15b0ad..80e4d04a7 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java @@ -36,22 +36,13 @@ public final class CucumberExpressionParser { */ private static final Parser textParser = (expression, tokens, current) -> { Token token = tokens.get(current); - switch (token.type) { - case WHITE_SPACE: - case TEXT: - case END_PARAMETER: - case END_OPTIONAL: - return new Result(1, new Node(TEXT_NODE, token.start(), token.end(), token.text)); - case ALTERNATION: - throw createAlternationNotAllowedInOptional(expression, token); - case BEGIN_PARAMETER: - case START_OF_LINE: - case END_OF_LINE: - case BEGIN_OPTIONAL: - default: - // If configured correctly this will never happen - return new Result(0); - } + return switch (token.type) { + case WHITE_SPACE, TEXT, END_PARAMETER, END_OPTIONAL -> + new Result(1, new Node(TEXT_NODE, token.start(), token.end(), token.text)); + case ALTERNATION -> throw createAlternationNotAllowedInOptional(expression, token); + // If configured correctly this will never happen + default -> new Result(0); + }; }; /* From 8d128498f17ba11a167bb655865e04fee8585c2b Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 14 Nov 2025 17:52:33 +0100 Subject: [PATCH 8/9] Use switch expression --- .../cucumberexpressions/CucumberExpressionParser.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java index 80e4d04a7..0e0346d93 100644 --- a/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java +++ b/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java @@ -37,8 +37,10 @@ public final class CucumberExpressionParser { private static final Parser textParser = (expression, tokens, current) -> { Token token = tokens.get(current); return switch (token.type) { - case WHITE_SPACE, TEXT, END_PARAMETER, END_OPTIONAL -> - new Result(1, new Node(TEXT_NODE, token.start(), token.end(), token.text)); + case WHITE_SPACE, + TEXT, + END_PARAMETER, + END_OPTIONAL -> new Result(1, new Node(TEXT_NODE, token.start(), token.end(), token.text)); case ALTERNATION -> throw createAlternationNotAllowedInOptional(expression, token); // If configured correctly this will never happen default -> new Result(0); From 089fad2483fa41b81fdbc6e92f6b20f4528570d9 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sat, 15 Nov 2025 00:27:10 +0100 Subject: [PATCH 9/9] Update versions and polish --- java/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/pom.xml b/java/pom.xml index b6522dbc7..b5486e4b4 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -9,7 +9,7 @@ cucumber-expressions - 18.0.2-SNAPSHOT + 19.0.0-SNAPSHOT jar Cucumber Expressions Cucumber Expressions are simple patterns for matching Step Definitions with Gherkin steps