> match(String text, Type... typeHints) {
}
}
- return Argument.build(group, treeRegexp, parameterTypes);
+ return Argument.build(group, parameterTypes);
}
@Override
@@ -157,4 +177,5 @@ public String getSource() {
public Pattern getRegexp() {
return treeRegexp.pattern();
}
+
}
diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java
index 443abc0e03..ef2a3cfc81 100644
--- a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java
+++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java
@@ -1,9 +1,14 @@
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)
public class CucumberExpressionException extends RuntimeException {
+
CucumberExpressionException(String message) {
super(message);
}
@@ -11,4 +16,148 @@ public class CucumberExpressionException extends RuntimeException {
CucumberExpressionException(String message, Throwable cause) {
super(message, cause);
}
+
+ static CucumberExpressionException createMissingEndToken(String expression, Type beginToken, Type endToken,
+ Token current) {
+ return new CucumberExpressionException(message(
+ current.start(),
+ expression,
+ pointAt(current),
+ "The '" + beginToken.symbol() + "' does not have a matching '" + endToken.symbol() + "'",
+ "If you did not intend to use " + beginToken.purpose() + " you can use '\\" + beginToken
+ .symbol() + "' to escape the " + beginToken.purpose()));
+ }
+
+ static CucumberExpressionException createAlternationNotAllowedInOptional(String expression, Token current) {
+ return new CucumberExpressionException(message(
+ current.start,
+ expression,
+ pointAt(current),
+ "An alternation can not be used inside an optional",
+ "You can use '\\/' to escape the the '/'"
+ ));
+ }
+
+ static CucumberExpressionException createTheEndOfLineCanNotBeEscaped(String expression) {
+ int index = expression.codePointCount(0, expression.length()) - 1;
+ return new CucumberExpressionException(message(
+ index,
+ expression,
+ pointAt(index),
+ "The end of line can not be escaped",
+ "You can use '\\\\' to escape the the '\\'"
+ ));
+ }
+
+ static CucumberExpressionException createAlternativeMayNotBeEmpty(Node node, String expression) {
+ return new CucumberExpressionException(message(
+ node.start(),
+ expression,
+ pointAt(node),
+ "Alternative may not be empty",
+ "If you did not mean to use an alternative you can use '\\/' to escape the the '/'"));
+ }
+
+ static CucumberExpressionException createParameterIsNotAllowedInOptional(Node node, String expression) {
+ return new CucumberExpressionException(message(
+ node.start(),
+ expression,
+ pointAt(node),
+ "An optional may not contain a parameter type",
+ "If you did not mean to use an parameter type you can use '\\{' to escape the the '{'"));
+ }
+ static CucumberExpressionException createOptionalIsNotAllowedInOptional(Node node, String expression) {
+ return new CucumberExpressionException(message(
+ node.start(),
+ expression,
+ pointAt(node),
+ "An optional may not contain an other optional",
+ "If you did not mean to use an optional type you can use '\\(' to escape the the '('. For more complicated expressions consider using a regular expression instead."));
+ }
+
+ static CucumberExpressionException createOptionalMayNotBeEmpty(Node node, String expression) {
+ return new CucumberExpressionException(message(
+ node.start(),
+ expression,
+ pointAt(node),
+ "An optional must contain some text",
+ "If you did not mean to use an optional you can use '\\(' to escape the the '('"));
+ }
+
+ static CucumberExpressionException createAlternativeMayNotExclusivelyContainOptionals(Node node,
+ String expression) {
+ return new CucumberExpressionException(message(
+ node.start(),
+ expression,
+ pointAt(node),
+ "An alternative may not exclusively contain optionals",
+ "If you did not mean to use an optional you can use '\\(' to escape the the '('"));
+ }
+
+ private static String thisCucumberExpressionHasAProblemAt(int index) {
+ return "This Cucumber Expression has a problem at column " + (index + 1) + ":" + "\n";
+ }
+
+ static CucumberExpressionException createCantEscape(String expression, int index) {
+ return new CucumberExpressionException(message(
+ index,
+ expression,
+ pointAt(index),
+ "Only the characters '{', '}', '(', ')', '\\', '/' and whitespace can be escaped",
+ "If you did mean to use an '\\' you can use '\\\\' to escape it"));
+ }
+
+ static CucumberExpressionException createInvalidParameterTypeName(String name) {
+ return new CucumberExpressionException(
+ "Illegal character in parameter name {" + name + "}. Parameter names may not contain '{', '}', '(', ')', '\\' or '/'");
+ }
+
+ /**
+ * Not very clear, but this message has to be language independent
+ * Other languages have dedicated syntax for writing down regular expressions
+ *
+ * In java a regular expression has to start with {@code ^} and end with
+ * {@code $} to be recognized as one by Cucumber.
+ *
+ * @see ExpressionFactory
+ */
+ static CucumberExpressionException createInvalidParameterTypeName(Token token, String expression) {
+ return new CucumberExpressionException(message(
+ token.start(),
+ expression,
+ pointAt(token),
+ "Parameter names may not contain '{', '}', '(', ')', '\\' or '/'",
+ "Did you mean to use a regular expression?"));
+ }
+
+ static String message(int index, String expression, String pointer, String problem,
+ String solution) {
+ return thisCucumberExpressionHasAProblemAt(index) +
+ "\n" +
+ expression + "\n" +
+ pointer + "\n" +
+ problem + ".\n" +
+ solution;
+ }
+
+ static String pointAt(Located node) {
+ StringBuilder pointer = new StringBuilder(pointAt(node.start()));
+ if (node.start() + 1 < node.end()) {
+ for (int i = node.start() + 1; i < node.end() - 1; i++) {
+ pointer.append("-");
+ }
+ pointer.append("^");
+ }
+ return pointer.toString();
+ }
+
+ private static String pointAt(int index) {
+ StringBuilder pointer = new StringBuilder();
+ for (int i = 0; i < index; i++) {
+ pointer.append(" ");
+ }
+ pointer.append("^");
+ return pointer.toString();
+ }
+
}
diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionGenerator.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionGenerator.java
index 512f58e481..1cdd6fd3e4 100644
--- a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionGenerator.java
+++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionGenerator.java
@@ -85,17 +85,6 @@ private String escape(String s) {
.replaceAll("/", "\\\\/");
}
- /**
- * @param text the text (step) to generate an expression for
- * @return the first of the generated expressions
- * @deprecated use {@link #generateExpressions(String)}
- */
- @Deprecated
- public GeneratedExpression generateExpression(String text) {
- List generatedExpressions = generateExpressions(text);
- return generatedExpressions.get(0);
- }
-
private List createParameterTypeMatchers(String text) {
Collection> parameterTypes = parameterTypeRegistry.getParameterTypes();
List parameterTypeMatchers = new ArrayList<>();
diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java
new file mode 100644
index 0000000000..b77be40e91
--- /dev/null
+++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java
@@ -0,0 +1,311 @@
+package io.cucumber.cucumberexpressions;
+
+import io.cucumber.cucumberexpressions.Ast.Node;
+import io.cucumber.cucumberexpressions.Ast.Token;
+import io.cucumber.cucumberexpressions.Ast.Token.Type;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static io.cucumber.cucumberexpressions.Ast.Node.Type.ALTERNATION_NODE;
+import static io.cucumber.cucumberexpressions.Ast.Node.Type.ALTERNATIVE_NODE;
+import static io.cucumber.cucumberexpressions.Ast.Node.Type.EXPRESSION_NODE;
+import static io.cucumber.cucumberexpressions.Ast.Node.Type.OPTIONAL_NODE;
+import static io.cucumber.cucumberexpressions.Ast.Node.Type.PARAMETER_NODE;
+import static io.cucumber.cucumberexpressions.Ast.Node.Type.TEXT_NODE;
+import static io.cucumber.cucumberexpressions.Ast.Token.Type.ALTERNATION;
+import static io.cucumber.cucumberexpressions.Ast.Token.Type.BEGIN_OPTIONAL;
+import static io.cucumber.cucumberexpressions.Ast.Token.Type.BEGIN_PARAMETER;
+import static io.cucumber.cucumberexpressions.Ast.Token.Type.END_OF_LINE;
+import static io.cucumber.cucumberexpressions.Ast.Token.Type.END_OPTIONAL;
+import static io.cucumber.cucumberexpressions.Ast.Token.Type.END_PARAMETER;
+import static io.cucumber.cucumberexpressions.Ast.Token.Type.START_OF_LINE;
+import static io.cucumber.cucumberexpressions.Ast.Token.Type.WHITE_SPACE;
+import static io.cucumber.cucumberexpressions.CucumberExpressionException.createAlternationNotAllowedInOptional;
+import static io.cucumber.cucumberexpressions.CucumberExpressionException.createInvalidParameterTypeName;
+import static io.cucumber.cucumberexpressions.CucumberExpressionException.createMissingEndToken;
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
+final class CucumberExpressionParser {
+
+ /*
+ * text := whitespace | ')' | '}' | .
+ */
+ 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);
+ }
+ };
+
+ /*
+ * name := whitespace | .
+ */
+ 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);
+ }
+ };
+
+ /*
+ * parameter := '{' + name* + '}'
+ */
+ private static final Parser parameterParser = parseBetween(
+ PARAMETER_NODE,
+ BEGIN_PARAMETER,
+ END_PARAMETER,
+ singletonList(nameParser)
+ );
+
+ /*
+ * optional := '(' + option* + ')'
+ * option := optional | parameter | text
+ */
+ private static final Parser optionalParser;
+ static {
+ List parsers = new ArrayList<>();
+ optionalParser = parseBetween(
+ OPTIONAL_NODE,
+ BEGIN_OPTIONAL,
+ END_OPTIONAL,
+ parsers
+ );
+ parsers.addAll(asList(optionalParser, parameterParser, textParser));
+ }
+
+ /*
+ * alternation := alternative* + ( '/' + alternative* )+
+ */
+ private static final Parser alternativeSeparator = (expression, tokens, current) -> {
+ if (!lookingAt(tokens, current, ALTERNATION)) {
+ return new Result(0);
+ }
+ Token token = tokens.get(current);
+ return new Result(1, new Node(ALTERNATIVE_NODE, token.start(), token.end(), token.text));
+ };
+
+ private static final List alternativeParsers = asList(
+ alternativeSeparator,
+ optionalParser,
+ parameterParser,
+ textParser
+ );
+
+ /*
+ * alternation := (?<=left-boundary) + alternative* + ( '/' + alternative* )+ + (?=right-boundary)
+ * left-boundary := whitespace | } | ^
+ * right-boundary := whitespace | { | $
+ * alternative: = optional | parameter | text
+ */
+ private static final Parser alternationParser = (expression, tokens, current) -> {
+ int previous = current - 1;
+ if (!lookingAtAny(tokens, previous, START_OF_LINE, WHITE_SPACE, END_PARAMETER)) {
+ return new Result(0);
+ }
+
+ Result result = parseTokensUntil(expression, alternativeParsers, tokens, current, WHITE_SPACE, END_OF_LINE, BEGIN_PARAMETER);
+ int subCurrent = current + result.consumed;
+ if (result.ast.stream().noneMatch(astNode -> astNode.type() == ALTERNATIVE_NODE)) {
+ return new Result(0);
+ }
+
+ int start = tokens.get(current).start();
+ int end = tokens.get(subCurrent).start();
+ // Does not consume right hand boundary token
+ return new Result(result.consumed,
+ new Node(ALTERNATION_NODE, start, end, splitAlternatives(start, end, result.ast)));
+ };
+
+ /*
+ * cucumber-expression := ( alternation | optional | parameter | text )*
+ */
+ private static final Parser cucumberExpressionParser = parseBetween(
+ EXPRESSION_NODE,
+ START_OF_LINE,
+ END_OF_LINE,
+ asList(
+ alternationParser,
+ optionalParser,
+ parameterParser,
+ textParser
+ )
+ );
+
+ 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,
+ List parsers) {
+ return (expression, tokens, current) -> {
+ if (!lookingAt(tokens, current, beginToken)) {
+ return new Result(0);
+ }
+ int subCurrent = current + 1;
+ Result result = parseTokensUntil(expression, parsers, tokens, subCurrent, endToken, END_OF_LINE);
+ subCurrent += result.consumed;
+
+ // endToken not found
+ if (!lookingAt(tokens, subCurrent, endToken)) {
+ throw createMissingEndToken(expression, beginToken, endToken, tokens.get(current));
+ }
+ // consumes endToken
+ int start = tokens.get(current).start();
+ int end = tokens.get(subCurrent).end();
+ return new Result(subCurrent + 1 - current, new Node(type, start, end, result.ast));
+ };
+ }
+
+ private static Result parseTokensUntil(
+ String expression,
+ List parsers,
+ List tokens,
+ int startAt,
+ Type... endTokens) {
+ int current = startAt;
+ int size = tokens.size();
+ List ast = new ArrayList<>();
+ while (current < size) {
+ if (lookingAtAny(tokens, current, endTokens)) {
+ break;
+ }
+
+ Result result = parseToken(expression, parsers, tokens, current);
+ if (result.consumed == 0) {
+ // If configured correctly this will never happen
+ // Keep to avoid infinite loops
+ throw new IllegalStateException("No eligible parsers for " + tokens);
+ }
+ current += result.consumed;
+ ast.addAll(result.ast);
+ }
+ return new Result(current - startAt, ast);
+ }
+
+ private static Result parseToken(String expression, List parsers,
+ List tokens,
+ int startAt) {
+ for (Parser parser : parsers) {
+ Result result = parser.parse(expression, tokens, startAt);
+ if (result.consumed != 0) {
+ return result;
+ }
+ }
+ // If configured correctly this will never happen
+ throw new IllegalStateException("No eligible parsers for " + tokens);
+ }
+
+ private static boolean lookingAtAny(List tokens, int at, Type... tokenTypes) {
+ for (Type tokeType : tokenTypes) {
+ if (lookingAt(tokens, at, tokeType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean lookingAt(List tokens, int at, Type token) {
+ if (at < 0) {
+ // If configured correctly this will never happen
+ // Keep for completeness
+ return token == START_OF_LINE;
+ }
+ if (at >= tokens.size()) {
+ return token == END_OF_LINE;
+ }
+ return tokens.get(at).type == token;
+ }
+
+ private static List splitAlternatives(int start, int end, List alternation) {
+ List separators = new ArrayList<>();
+ List> alternatives = new ArrayList<>();
+ List alternative = new ArrayList<>();
+ for (Node n : alternation) {
+ if (ALTERNATIVE_NODE.equals(n.type())) {
+ separators.add(n);
+ alternatives.add(alternative);
+ alternative = new ArrayList<>();
+ } else {
+ alternative.add(n);
+ }
+ }
+ alternatives.add(alternative);
+
+ return createAlternativeNodes(start, end, separators, alternatives);
+ }
+
+ private static List createAlternativeNodes(int start, int end, List separators, List> alternatives) {
+ List nodes = new ArrayList<>();
+ for (int i = 0; i < alternatives.size(); i++) {
+ List n = alternatives.get(i);
+ if (i == 0) {
+ Node rightSeparator = separators.get(i);
+ nodes.add(new Node(ALTERNATIVE_NODE, start, rightSeparator.start(), n));
+ } else if (i == alternatives.size() - 1) {
+ Node leftSeparator = separators.get(i - 1);
+ nodes.add(new Node(ALTERNATIVE_NODE, leftSeparator.end(), end, n));
+ } else {
+ Node leftSeparator = separators.get(i - 1);
+ Node rightSeparator = separators.get(i);
+ nodes.add(new Node(ALTERNATIVE_NODE, leftSeparator.end(), rightSeparator.start(), n));
+ }
+ }
+ return nodes;
+ }
+
+}
diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java
new file mode 100644
index 0000000000..9a2a9df254
--- /dev/null
+++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java
@@ -0,0 +1,133 @@
+package io.cucumber.cucumberexpressions;
+
+import io.cucumber.cucumberexpressions.Ast.Token;
+import io.cucumber.cucumberexpressions.Ast.Token.Type;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.PrimitiveIterator.OfInt;
+
+import static io.cucumber.cucumberexpressions.CucumberExpressionException.createCantEscape;
+import static io.cucumber.cucumberexpressions.CucumberExpressionException.createTheEndOfLineCanNotBeEscaped;
+
+final class CucumberExpressionTokenizer {
+
+ List tokenize(String expression) {
+ List tokens = new ArrayList<>();
+ tokenizeImpl(expression).forEach(tokens::add);
+ return tokens;
+ }
+
+ private Iterable tokenizeImpl(String expression) {
+ return () -> new TokenIterator(expression);
+ }
+
+ private static class TokenIterator implements Iterator {
+
+ private final String expression;
+ private final OfInt codePoints;
+
+ private StringBuilder buffer = new StringBuilder();
+ private Type previousTokenType = null;
+ private Type currentTokenType = Type.START_OF_LINE;
+ private boolean treatAsText;
+ private int bufferStartIndex;
+ private int escaped;
+
+ TokenIterator(String expression) {
+ this.expression = expression;
+ this.codePoints = expression.codePoints().iterator();
+ }
+
+ private Token convertBufferToToken(Type tokenType) {
+ int escapeTokens = 0;
+ if (tokenType == Type.TEXT) {
+ escapeTokens = escaped;
+ escaped = 0;
+ }
+ int consumedIndex = bufferStartIndex + buffer.codePointCount(0, buffer.length()) + escapeTokens;
+ Token t = new Token(buffer.toString(), tokenType, bufferStartIndex, consumedIndex);
+ buffer = new StringBuilder();
+ this.bufferStartIndex = consumedIndex;
+ return t;
+ }
+
+ private void advanceTokenTypes() {
+ previousTokenType = currentTokenType;
+ currentTokenType = null;
+ }
+
+ private Type tokenTypeOf(Integer token, boolean treatAsText) {
+ if (!treatAsText) {
+ return Token.typeOf(token);
+ }
+ if (Token.canEscape(token)) {
+ return Type.TEXT;
+ }
+ throw createCantEscape(expression, bufferStartIndex + buffer.codePointCount(0, buffer.length()) + escaped);
+ }
+
+ private boolean shouldContinueTokenType(Type previousTokenType,
+ Type currentTokenType) {
+ return currentTokenType == previousTokenType
+ && (currentTokenType == Type.WHITE_SPACE || currentTokenType == Type.TEXT);
+ }
+
+ @Override
+ public boolean hasNext() {
+ return previousTokenType != Type.END_OF_LINE;
+ }
+
+ @Override
+ public Token next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ if (currentTokenType == Type.START_OF_LINE) {
+ Token token = convertBufferToToken(currentTokenType);
+ advanceTokenTypes();
+ return token;
+ }
+
+ while (codePoints.hasNext()) {
+ int codePoint = codePoints.nextInt();
+ if (!treatAsText && Token.isEscapeCharacter(codePoint)) {
+ escaped++;
+ treatAsText = true;
+ continue;
+ }
+ currentTokenType = tokenTypeOf(codePoint, treatAsText);
+ treatAsText = false;
+
+ if (previousTokenType == Type.START_OF_LINE ||
+ shouldContinueTokenType(previousTokenType, currentTokenType)) {
+ advanceTokenTypes();
+ buffer.appendCodePoint(codePoint);
+ } else {
+ Token t = convertBufferToToken(previousTokenType);
+ advanceTokenTypes();
+ buffer.appendCodePoint(codePoint);
+ return t;
+ }
+ }
+
+ if (buffer.length() > 0) {
+ Token token = convertBufferToToken(previousTokenType);
+ advanceTokenTypes();
+ return token;
+ }
+
+ currentTokenType = Type.END_OF_LINE;
+ if (treatAsText) {
+ throw createTheEndOfLineCanNotBeEscaped(expression);
+ }
+ Token token = convertBufferToToken(currentTokenType);
+ advanceTokenTypes();
+ return token;
+ }
+
+ }
+
+}
diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ExpressionFactory.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ExpressionFactory.java
index cb5049500a..6565bc861f 100644
--- a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ExpressionFactory.java
+++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ExpressionFactory.java
@@ -10,6 +10,9 @@
* Creates a {@link CucumberExpression} or {@link RegularExpression} from a {@link String}
* 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 `$` is considered a regular expression.
+ * Everything else is considered a Cucumber expression.
*/
@API(status = API.Status.STABLE)
public final class ExpressionFactory {
@@ -17,6 +20,8 @@ public final class ExpressionFactory {
private static final Pattern BEGIN_ANCHOR = Pattern.compile("^\\^.*");
private static final Pattern END_ANCHOR = Pattern.compile(".*\\$$");
private static final Pattern SCRIPT_STYLE_REGEXP = Pattern.compile("^/(.*)/$");
+ private static final Pattern PARAMETER_PATTERN = Pattern.compile("((?:\\\\){0,2})\\{([^}]*)\\}");
+
private final ParameterTypeRegistry parameterTypeRegistry;
public ExpressionFactory(ParameterTypeRegistry parameterTypeRegistry) {
@@ -38,7 +43,7 @@ private RegularExpression createRegularExpressionWithAnchors(String expressionSt
try {
return new RegularExpression(Pattern.compile(expressionString), parameterTypeRegistry);
} catch (PatternSyntaxException e) {
- if (CucumberExpression.PARAMETER_PATTERN.matcher(expressionString).find()) {
+ if (PARAMETER_PATTERN.matcher(expressionString).find()) {
throw new CucumberExpressionException("You cannot use anchors (^ or $) in Cucumber Expressions. Please remove them from " + expressionString, e);
}
throw e;
diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ParameterType.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ParameterType.java
index 938c7a5b2b..c3f8348073 100644
--- a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ParameterType.java
+++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ParameterType.java
@@ -12,7 +12,7 @@
@API(status = API.Status.STABLE)
public final class ParameterType implements Comparable> {
@SuppressWarnings("RegExpRedundantEscape") // Android can't parse unescaped braces
- private static final Pattern ILLEGAL_PARAMETER_NAME_PATTERN = Pattern.compile("([\\[\\]()$.|?*+])");
+ private static final Pattern ILLEGAL_PARAMETER_NAME_PATTERN = Pattern.compile("([{}()\\\\/])");
private static final Pattern UNESCAPE_PATTERN = Pattern.compile("(\\\\([\\[$.|?*+\\]]))");
private final String name;
@@ -25,11 +25,15 @@ public final class ParameterType implements Comparable> {
private final boolean useRegexpMatchAsStrongTypeHint;
static void checkParameterTypeName(String name) {
+ if (!isValidParameterTypeName(name)) {
+ throw CucumberExpressionException.createInvalidParameterTypeName(name);
+ }
+ }
+
+ static boolean isValidParameterTypeName(String name) {
String unescapedTypeName = UNESCAPE_PATTERN.matcher(name).replaceAll("$2");
Matcher matcher = ILLEGAL_PARAMETER_NAME_PATTERN.matcher(unescapedTypeName);
- if (matcher.find()) {
- throw new CucumberExpressionException(String.format("Illegal character '%s' in parameter name {%s}.", matcher.group(1), unescapedTypeName));
- }
+ return !matcher.find();
}
static ParameterType