Skip to content

Commit 2ab6bad

Browse files
authored
[Java] Improve expression creation performance (#185, #187)
1 parent a6b1c83 commit 2ab6bad

File tree

4 files changed

+34
-13
lines changed

4 files changed

+34
-13
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
9+
### Fixed
10+
- [Java] Improve expression creation performance ([#187](https://github.com/cucumber/cucumber-expressions/pull/187), [#189](https://github.com/cucumber/cucumber-expressions/pull/189))
911

1012
## [16.1.0] - 2022-11-28
1113
### Added

java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
@API(status = API.Status.STABLE)
2626
public final class CucumberExpression implements Expression {
27-
private static final Pattern ESCAPE_PATTERN = Pattern.compile("([\\\\^\\[({$.|?*+})\\]])");
27+
private static final Pattern ESCAPE_PATTERN = Pattern.compile("[\\\\^\\[({$.|?*+})\\]]");
2828

2929
private final List<ParameterType<?>> parameterTypes = new ArrayList<>();
3030
private final String source;
@@ -62,7 +62,7 @@ private String rewriteToRegex(Node node) {
6262
}
6363

6464
private static String escapeRegex(String text) {
65-
return ESCAPE_PATTERN.matcher(text).replaceAll("\\\\$1");
65+
return ESCAPE_PATTERN.matcher(text).replaceAll("\\\\$0");
6666
}
6767

6868
private String rewriteOptional(Node node) {

java/src/main/java/io/cucumber/cucumberexpressions/ExpressionFactory.java

+24-10
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,13 @@
1111
* using heuristics. This is particularly useful for languages that don't have a
1212
* literal syntax for regular expressions. In Java, a regular expression has to be represented as a String.
1313
*
14-
* A string that starts with `^` and/or ends with `$` is considered a regular expression.
14+
* A string that starts with `^` and/or ends with `$` (or written in script style, i.e. starting with `/`
15+
* and ending with `/`) is considered a regular expression.
1516
* Everything else is considered a Cucumber expression.
1617
*/
1718
@API(status = API.Status.STABLE)
1819
public final class ExpressionFactory {
1920

20-
private static final Pattern BEGIN_ANCHOR = Pattern.compile("^\\^.*");
21-
private static final Pattern END_ANCHOR = Pattern.compile(".*\\$$");
22-
private static final Pattern SCRIPT_STYLE_REGEXP = Pattern.compile("^/(.*)/$");
2321
private static final Pattern PARAMETER_PATTERN = Pattern.compile("((?:\\\\){0,2})\\{([^}]*)\\}");
2422

2523
private final ParameterTypeRegistry parameterTypeRegistry;
@@ -29,14 +27,30 @@ public ExpressionFactory(ParameterTypeRegistry parameterTypeRegistry) {
2927
}
3028

3129
public Expression createExpression(String expressionString) {
32-
if (BEGIN_ANCHOR.matcher(expressionString).find() || END_ANCHOR.matcher(expressionString).find()) {
33-
return createRegularExpressionWithAnchors(expressionString);
30+
/* This method is called often (typically about number_of_steps x
31+
* nbr_test_scenarios), thus performance is more important than
32+
* readability here.
33+
* Consequently, we check the first and last expressionString
34+
* characters to determine whether we need to create a
35+
* RegularExpression or a CucumberExpression (because character
36+
* matching is faster than startsWith/endsWith and regexp matching).
37+
*/
38+
int length = expressionString.length();
39+
if (length == 0) {
40+
return new CucumberExpression(expressionString, this.parameterTypeRegistry);
3441
}
35-
Matcher m = SCRIPT_STYLE_REGEXP.matcher(expressionString);
36-
if (m.find()) {
37-
return new RegularExpression(Pattern.compile(m.group(1)), parameterTypeRegistry);
42+
43+
int lastCharIndex = length - 1;
44+
char firstChar = expressionString.charAt(0);
45+
char lastChar = expressionString.charAt(lastCharIndex);
46+
47+
if (firstChar == '^' || lastChar == '$') {
48+
return this.createRegularExpressionWithAnchors(expressionString);
49+
} else if (firstChar == '/' && lastChar == '/') {
50+
return new RegularExpression(Pattern.compile(expressionString.substring(1, lastCharIndex)), this.parameterTypeRegistry);
3851
}
39-
return new CucumberExpression(expressionString, parameterTypeRegistry);
52+
53+
return new CucumberExpression(expressionString, this.parameterTypeRegistry);
4054
}
4155

4256
private RegularExpression createRegularExpressionWithAnchors(String expressionString) {

java/src/test/java/io/cucumber/cucumberexpressions/ExpressionFactoryTest.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313
import static org.junit.jupiter.api.Assertions.assertThrows;
1414

1515
public class ExpressionFactoryTest {
16-
16+
17+
@Test
18+
public void creates_cucumber_expression_for_empty() {
19+
assertCucumberExpression("");
20+
}
21+
1722
@Test
1823
public void creates_cucumber_expression_by_default() {
1924
assertCucumberExpression("strings are cukexp by default");

0 commit comments

Comments
 (0)