diff --git a/CHANGELOG.md b/CHANGELOG.md
index d4f650aa..1dbc8563 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,12 +13,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Changed
+* Only allow escape character (\) in front of (, ), or \. Throw error otherwise. ([#17](https://github.com/cucumber/tag-expressions/pull/17))
+
### Deprecated
### Removed
### Fixed
+* Document escaping. ([#16](https://github.com/cucumber/tag-expressions/issues/16), [#17](https://github.com/cucumber/tag-expressions/pull/17))
+* [Ruby] Empty expression evaluates to true
+
## [4.1.0] - 2021-10-08
### Added
diff --git a/README.md b/README.md
index 6bf01997..ff2ea9a3 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,16 @@ For more complex Tag Expressions you can use parenthesis for clarity, or to chan
(@smoke or @ui) and (not @slow)
+## Escaping
+
+If you need to use one of the reserved characters `(`, `)`, `\` or ` ` (whitespace) in a tag,
+you can escape it with a `\`. Examples:
+
+| Gherkin Tag | Escaped Tag Expression |
+| ------------- | ---------------------- |
+| @x(y) | @x\(y\) |
+| @x\y | @x\\y |
+
## Migrating from old style tags
Older versions of Cucumber used a different syntax for tags. The list below
diff --git a/go/.gitignore b/go/.gitignore
index 7b0ee7ae..df241b81 100644
--- a/go/.gitignore
+++ b/go/.gitignore
@@ -15,3 +15,4 @@ dist_compressed/
*.iml
# upx dist/cucumber-gherkin-openbsd-386 fails with a core dump
core.*.!usr!bin!upx-ucl
+.idea
diff --git a/go/Makefile b/go/Makefile
index f1e28128..c0b802b5 100644
--- a/go/Makefile
+++ b/go/Makefile
@@ -1,4 +1,5 @@
GO_SOURCE_FILES := $(wildcard *.go)
+TEST_FILES := $(wildcard ../testdata/*.yml)
default: .linted .tested
.PHONY: default
@@ -7,7 +8,7 @@ default: .linted .tested
gofmt -w $^
touch $@
-.tested: $(GO_SOURCE_FILES)
+.tested: $(GO_SOURCE_FILES) $(TEST_FILES)
go test ./...
touch $@
diff --git a/go/errors_test.go b/go/errors_test.go
new file mode 100644
index 00000000..de7134fa
--- /dev/null
+++ b/go/errors_test.go
@@ -0,0 +1,32 @@
+package tagexpressions
+
+import (
+ "fmt"
+ "gopkg.in/yaml.v3"
+ "io/ioutil"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+type ErrorTest struct {
+ Expression string `yaml:"expression"`
+ Error string `yaml:"error"`
+}
+
+func TestErrors(t *testing.T) {
+ contents, err := ioutil.ReadFile("../testdata/errors.yml")
+ require.NoError(t, err)
+ var tests []ErrorTest
+ err = yaml.Unmarshal(contents, &tests)
+ require.NoError(t, err)
+
+ for _, test := range tests {
+ name := fmt.Sprintf("fails to parse \"%s\" with \"%s\"", test.Expression, test.Error)
+ t.Run(name, func(t *testing.T) {
+ _, err := Parse(test.Expression)
+ require.Error(t, err)
+ require.Equal(t, test.Error, err.Error())
+ })
+ }
+}
diff --git a/go/evaluations_test.go b/go/evaluations_test.go
new file mode 100644
index 00000000..c098925a
--- /dev/null
+++ b/go/evaluations_test.go
@@ -0,0 +1,43 @@
+package tagexpressions
+
+import (
+ "fmt"
+ "gopkg.in/yaml.v3"
+ "io/ioutil"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+type Evaluation struct {
+ Expression string `yaml:"expression"`
+ Tests []Test `yaml:"tests"`
+}
+
+type Test struct {
+ Variables []string `yaml:"variables"`
+ Result bool `yaml:"result"`
+}
+
+func TestEvaluations(t *testing.T) {
+ contents, err := ioutil.ReadFile("../testdata/evaluations.yml")
+ require.NoError(t, err)
+ var evaluations []Evaluation
+ err = yaml.Unmarshal(contents, &evaluations)
+ require.NoError(t, err)
+
+ for _, evaluation := range evaluations {
+ for _, test := range evaluation.Tests {
+ variables := strings.Join(test.Variables, ", ")
+ name := fmt.Sprintf("evaluates [%s] to %t", variables, test.Result)
+ t.Run(name, func(t *testing.T) {
+ expression, err := Parse(evaluation.Expression)
+ require.NoError(t, err)
+
+ result := expression.Evaluate(test.Variables)
+ require.Equal(t, test.Result, result)
+ })
+ }
+ }
+}
diff --git a/go/parser.go b/go/parser.go
index 8759690a..ebac8fcf 100644
--- a/go/parser.go
+++ b/go/parser.go
@@ -2,7 +2,6 @@ package tagexpressions
import (
"bytes"
- "errors"
"fmt"
"strings"
"unicode"
@@ -17,7 +16,10 @@ type Evaluatable interface {
}
func Parse(infix string) (Evaluatable, error) {
- tokens := tokenize(infix)
+ tokens, err := tokenize(infix)
+ if err != nil {
+ return nil, err
+ }
if len(tokens) == 0 {
return &trueExpr{}, nil
}
@@ -27,13 +29,13 @@ func Parse(infix string) (Evaluatable, error) {
for _, token := range tokens {
if isUnary(token) {
- if err := check(expectedTokenType, OPERAND); err != nil {
+ if err := check(infix, expectedTokenType, OPERAND); err != nil {
return nil, err
}
operators.Push(token)
expectedTokenType = OPERAND
} else if isBinary(token) {
- if err := check(expectedTokenType, OPERATOR); err != nil {
+ if err := check(infix, expectedTokenType, OPERATOR); err != nil {
return nil, err
}
for operators.Len() > 0 &&
@@ -45,27 +47,27 @@ func Parse(infix string) (Evaluatable, error) {
operators.Push(token)
expectedTokenType = OPERAND
} else if "(" == token {
- if err := check(expectedTokenType, OPERAND); err != nil {
+ if err := check(infix, expectedTokenType, OPERAND); err != nil {
return nil, err
}
operators.Push(token)
expectedTokenType = OPERAND
} else if ")" == token {
- if err := check(expectedTokenType, OPERATOR); err != nil {
+ if err := check(infix, expectedTokenType, OPERATOR); err != nil {
return nil, err
}
for operators.Len() > 0 && operators.Peek() != "(" {
pushExpr(operators.Pop(), expressions)
}
if operators.Len() == 0 {
- return nil, errors.New("Syntax error. Unmatched )")
+ return nil, fmt.Errorf("Tag expression \"%s\" could not be parsed because of syntax error: Unmatched ).", infix)
}
if operators.Peek() == "(" {
operators.Pop()
}
expectedTokenType = OPERATOR
} else {
- if err := check(expectedTokenType, OPERAND); err != nil {
+ if err := check(infix, expectedTokenType, OPERAND); err != nil {
return nil, err
}
pushExpr(token, expressions)
@@ -75,7 +77,7 @@ func Parse(infix string) (Evaluatable, error) {
for operators.Len() > 0 {
if operators.Peek() == "(" {
- return nil, errors.New("Syntax error. Unmatched (")
+ return nil, fmt.Errorf("Tag expression \"%s\" could not be parsed because of syntax error: Unmatched (.", infix)
}
pushExpr(operators.Pop(), expressions)
}
@@ -97,51 +99,38 @@ var PREC = map[string]int{
"not": 2,
}
-func tokenize(expr string) []string {
+func tokenize(expr string) ([]string, error) {
var tokens []string
var token bytes.Buffer
- collectToken := func() {
- if token.Len() > 0 {
- tokens = append(tokens, token.String())
- token.Reset()
- }
- }
-
escaped := false
for _, c := range expr {
- if unicode.IsSpace(c) {
- collectToken()
- escaped = false
- continue
- }
-
- ch := string(c)
-
- switch ch {
- case "\\":
- if escaped {
- token.WriteString(ch)
+ if escaped {
+ if c == '(' || c == ')' || c == '\\' || unicode.IsSpace(c) {
+ token.WriteRune(c)
escaped = false
} else {
- escaped = true
+ return nil, fmt.Errorf("Tag expression \"%s\" could not be parsed because of syntax error: Illegal escape before \"%s\".", expr, string(c))
}
- case "(", ")":
- if escaped {
- token.WriteString(ch)
- escaped = false
- } else {
- collectToken()
- tokens = append(tokens, ch)
+ } else if c == '\\' {
+ escaped = true
+ } else if c == '(' || c == ')' || unicode.IsSpace(c) {
+ if token.Len() > 0 {
+ tokens = append(tokens, token.String())
+ token.Reset()
}
- default:
- token.WriteString(ch)
- escaped = false
+ if !unicode.IsSpace(c) {
+ tokens = append(tokens, string(c))
+ }
+ } else {
+ token.WriteRune(c)
}
}
+ if token.Len() > 0 {
+ tokens = append(tokens, token.String())
+ }
- collectToken()
- return tokens
+ return tokens, nil
}
func isUnary(token string) bool {
@@ -157,9 +146,9 @@ func isOp(token string) bool {
return ok
}
-func check(expectedTokenType, tokenType string) error {
+func check(infix, expectedTokenType, tokenType string) error {
if expectedTokenType != tokenType {
- return fmt.Errorf("Syntax error. Expected %s", expectedTokenType)
+ return fmt.Errorf("Tag expression \"%s\" could not be parsed because of syntax error: Expected %s.", infix, expectedTokenType)
}
return nil
}
@@ -198,17 +187,11 @@ func (l *literalExpr) Evaluate(variables []string) bool {
}
func (l *literalExpr) ToString() string {
- return strings.Replace(
- strings.Replace(
- strings.Replace(l.value, "\\", "\\\\", -1),
- "(",
- "\\(",
- -1,
- ),
- ")",
- "\\)",
- -1,
- )
+ s1 := l.value
+ s2 := strings.Replace(s1, "\\", "\\\\", -1)
+ s3 := strings.Replace(s2, "(", "\\(", -1)
+ s4 := strings.Replace(s3, ")", "\\)", -1)
+ return strings.Replace(s4, " ", "\\ ", -1)
}
type orExpr struct {
diff --git a/go/parser_test.go b/go/parser_test.go
deleted file mode 100644
index 3833919a..00000000
--- a/go/parser_test.go
+++ /dev/null
@@ -1,227 +0,0 @@
-package tagexpressions
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestParseForValidCases(t *testing.T) {
- cases := []struct {
- name string
- given string
- expected string
- }{
- {
- name: "test and",
- given: "a and b",
- expected: "( a and b )",
- },
- {
- name: "test or",
- given: "a or b",
- expected: "( a or b )",
- },
- {
- name: "test unary not",
- given: "not a",
- expected: "not ( a )",
- },
- {
- name: "test and & or",
- given: "( a and b ) or ( c and d )",
- expected: "( ( a and b ) or ( c and d ) )",
- },
- {
- name: "test and, or, not",
- given: "not a or b and not c or not d or e and f",
- expected: "( ( ( not ( a ) or ( b and not ( c ) ) ) or not ( d ) ) or ( e and f ) )",
- },
- {
- name: "test escaping",
- given: "not a\\(\\) or b and not c or not d or e and f",
- expected: "( ( ( not ( a\\(\\) ) or ( b and not ( c ) ) ) or not ( d ) ) or ( e and f ) )",
- },
- {
- name: "test escaping backslash",
- given: "(a\\\\ and b\\\\\\( and c\\\\\\) and d\\\\)",
- expected: "( ( ( a\\\\ and b\\\\\\( ) and c\\\\\\) ) and d\\\\ )",
- },
- {
- name: "test backslash",
- given: "(a\\ and \\b) and c\\",
- expected: "( ( a and b ) and c )",
- },
- }
-
- for _, tc := range cases {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
- actual, err := Parse(tc.given)
- require.NoError(t, err)
-
- actualStr := actual.ToString()
- require.Equal(t, tc.expected, actualStr)
-
- roundTripActual, err := Parse(actualStr)
- require.NoError(t, err)
-
- roundTripActualStr := roundTripActual.ToString()
- require.Equal(t, tc.expected, roundTripActualStr)
- })
- }
-}
-
-func TestParseForSyntaxErrors(t *testing.T) {
- cases := []struct {
- name string
- given string
- expected string
- }{
- {
- name: "no operators",
- given: "a b",
- expected: "Syntax error. Expected operator",
- },
- {
- name: "missing operator in binary expression",
- given: "@a @b or",
- expected: "Syntax error. Expected operator",
- },
- {
- name: "missing operator in unary expression",
- given: "@a and (@b not)",
- expected: "Syntax error. Expected operator",
- },
- {
- name: "missing operator between operands",
- given: "@a and (@b @c) or",
- expected: "Syntax error. Expected operator",
- },
- {
- name: "no operands",
- given: "or or",
- expected: "Syntax error. Expected operand",
- },
- {
- name: "missing operand",
- given: "@a and or",
- expected: "Syntax error. Expected operand",
- },
- {
- name: "unmatched closing parenthesis",
- given: "( a and b ) )",
- expected: "Syntax error. Unmatched )",
- },
- {
- name: "unmatched opening parenthesis",
- given: "( ( a and b )",
- expected: "Syntax error. Unmatched (",
- },
- }
-
- for _, tc := range cases {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
- _, err := Parse(tc.given)
- require.Error(t, err)
- require.Equal(t, tc.expected, err.Error())
- })
- }
-}
-
-func TestParseForEvaluationErrors(t *testing.T) {
- cases := []struct {
- name string
- given string
- expectations func(*testing.T, Evaluatable)
- }{
- {
- name: "evaluate not",
- given: "not x",
- expectations: func(t *testing.T, expr Evaluatable) {
- require.False(t, expr.Evaluate([]string{"x"}))
- require.True(t, expr.Evaluate([]string{"y"}))
- },
- },
- {
- name: "evaluate and",
- given: "x and y",
- expectations: func(t *testing.T, expr Evaluatable) {
- require.True(t, expr.Evaluate([]string{"x", "y"}))
- require.False(t, expr.Evaluate([]string{"y"}))
- require.False(t, expr.Evaluate([]string{"x"}))
- },
- },
- {
- name: "evaluate or",
- given: " x or(y) ",
- expectations: func(t *testing.T, expr Evaluatable) {
- require.False(t, expr.Evaluate([]string{}))
- require.True(t, expr.Evaluate([]string{"y"}))
- require.True(t, expr.Evaluate([]string{"x"}))
- },
- },
- {
- name: "evaluate expressions with escaped chars",
- given: " x\\(1\\) or(y\\(2\\)) ",
- expectations: func(t *testing.T, expr Evaluatable) {
- require.False(t, expr.Evaluate([]string{}))
- require.True(t, expr.Evaluate([]string{"y(2)"}))
- require.True(t, expr.Evaluate([]string{"x(1)"}))
- require.False(t, expr.Evaluate([]string{"y"}))
- require.False(t, expr.Evaluate([]string{"x"}))
- },
- },
- {
- name: "evaluate empty expressions to true",
- given: "",
- expectations: func(t *testing.T, expr Evaluatable) {
- require.True(t, expr.Evaluate([]string{}))
- require.True(t, expr.Evaluate([]string{"y"}))
- require.True(t, expr.Evaluate([]string{"x"}))
- },
- },
- {
- name: "evaluate expressions with escaped backslash",
- given: "x\\\\ or(y\\\\\\)) or(z\\\\)",
- expectations: func(t *testing.T, expr Evaluatable) {
- require.False(t, expr.Evaluate([]string{}))
- require.True(t, expr.Evaluate([]string{"x\\"}))
- require.True(t, expr.Evaluate([]string{"y\\)"}))
- require.True(t, expr.Evaluate([]string{"z\\"}))
- require.False(t, expr.Evaluate([]string{"x"}))
- require.False(t, expr.Evaluate([]string{"y)"}))
- require.False(t, expr.Evaluate([]string{"z"}))
- },
- },
- {
- name: "evaluate expressions with backslash",
- given: "\\x or y\\ or z\\",
- expectations: func(t *testing.T, expr Evaluatable) {
- require.False(t, expr.Evaluate([]string{}))
- require.True(t, expr.Evaluate([]string{"x"}))
- require.True(t, expr.Evaluate([]string{"y"}))
- require.True(t, expr.Evaluate([]string{"z"}))
- require.False(t, expr.Evaluate([]string{"\\x"}))
- require.False(t, expr.Evaluate([]string{"y\\"}))
- require.False(t, expr.Evaluate([]string{"z\\"}))
- },
- },
- }
-
- for _, tc := range cases {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
- expr, err := Parse(tc.given)
- require.NoError(t, err)
- tc.expectations(t, expr)
- })
- }
-}
diff --git a/go/parsing_test.go b/go/parsing_test.go
new file mode 100644
index 00000000..d0bcba32
--- /dev/null
+++ b/go/parsing_test.go
@@ -0,0 +1,38 @@
+package tagexpressions
+
+import (
+ "fmt"
+ "gopkg.in/yaml.v3"
+ "io/ioutil"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+type ParsingTest struct {
+ Expression string `yaml:"expression"`
+ Formatted string `yaml:"formatted"`
+}
+
+func TestParsing(t *testing.T) {
+ contents, err := ioutil.ReadFile("../testdata/parsing.yml")
+ require.NoError(t, err)
+ var tests []ParsingTest
+ err = yaml.Unmarshal(contents, &tests)
+ require.NoError(t, err)
+
+ for _, test := range tests {
+ name := fmt.Sprintf("parses \"%s\" into \"%s\"", test.Expression, test.Formatted)
+ t.Run(name, func(t *testing.T) {
+ expression, err := Parse(test.Expression)
+ require.NoError(t, err)
+
+ require.Equal(t, test.Formatted, expression.ToString())
+
+ expressionAgain, err := Parse(expression.ToString())
+ require.NoError(t, err)
+
+ require.Equal(t, test.Formatted, expressionAgain.ToString())
+ })
+ }
+}
diff --git a/java/pom.xml b/java/pom.xml
index 16f84e42..d7dd633f 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -55,6 +55,12 @@
junit-jupiter-params
test
+
+ org.yaml
+ snakeyaml
+ 1.29
+ test
+
diff --git a/java/src/main/java/io/cucumber/tagexpressions/TagExpressionParser.java b/java/src/main/java/io/cucumber/tagexpressions/TagExpressionParser.java
index 6b62a225..d1e7da6f 100644
--- a/java/src/main/java/io/cucumber/tagexpressions/TagExpressionParser.java
+++ b/java/src/main/java/io/cucumber/tagexpressions/TagExpressionParser.java
@@ -6,6 +6,8 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
public final class TagExpressionParser {
private static final Map ASSOC = new HashMap() {{
@@ -24,16 +26,16 @@ public final class TagExpressionParser {
private final String infix;
public static Expression parse(String infix) {
- return new TagExpressionParser(infix).parse();
+ return new TagExpressionParser(infix).parse();
}
private TagExpressionParser(String infix) {
- this.infix = infix;
+ this.infix = infix;
}
-
- private Expression parse() {
+
+ private Expression parse() {
List tokens = tokenize(infix);
- if(tokens.isEmpty()) return new True();
+ if (tokens.isEmpty()) return new True();
Deque operators = new ArrayDeque<>();
Deque expressions = new ArrayDeque<>();
@@ -49,7 +51,7 @@ private Expression parse() {
(ASSOC.get(token) == Assoc.LEFT && PREC.get(token) <= PREC.get(operators.peek()))
||
(ASSOC.get(token) == Assoc.RIGHT && PREC.get(token) < PREC.get(operators.peek())))
- ) {
+ ) {
pushExpr(pop(operators), expressions);
}
operators.push(token);
@@ -64,7 +66,7 @@ private Expression parse() {
pushExpr(pop(operators), expressions);
}
if (operators.size() == 0) {
- throw new TagExpressionException("Tag expression '%s' could not be parsed because of syntax error: unmatched )", this.infix);
+ throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Unmatched ).", this.infix);
}
if ("(".equals(operators.peek())) {
pop(operators);
@@ -79,7 +81,7 @@ private Expression parse() {
while (operators.size() > 0) {
if ("(".equals(operators.peek())) {
- throw new TagExpressionException("Tag expression '%s' could not be parsed because of syntax error: unmatched (", infix);
+ throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Unmatched (.", infix);
}
pushExpr(pop(operators), expressions);
}
@@ -89,43 +91,32 @@ private Expression parse() {
private static List tokenize(String expr) {
List tokens = new ArrayList<>();
-
boolean isEscaped = false;
- StringBuilder token = null;
+ StringBuilder token = new StringBuilder();
for (int i = 0; i < expr.length(); i++) {
char c = expr.charAt(i);
- if (ESCAPING_CHAR == c && !isEscaped) {
- isEscaped = true;
- } else {
- if (Character.isWhitespace(c)) { // skip
- if (null != token) { // end of token
- tokens.add(token.toString());
- token = null;
- }
+ if (isEscaped) {
+ if (c == '(' || c == ')' || c == '\\' || Character.isWhitespace(c)) {
+ token.append(c);
+ isEscaped = false;
} else {
- switch (c) {
- case '(':
- case ')':
- if (!isEscaped) {
- if (null != token) { // end of token
- tokens.add(token.toString());
- token = null;
- }
- tokens.add(String.valueOf(c));
- break;
- }
- default:
- if (null == token) { // start of token
- token = new StringBuilder();
- }
- token.append(c);
- break;
- }
+ throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Illegal escape before \"%s\".", expr, c);
+ }
+ } else if (c == ESCAPING_CHAR) {
+ isEscaped = true;
+ } else if (c == '(' || c == ')' || Character.isWhitespace(c)) {
+ if (token.length() > 0) {
+ tokens.add(token.toString());
+ token = new StringBuilder();
}
- isEscaped = false;
+ if (!Character.isWhitespace(c)) {
+ tokens.add(String.valueOf(c));
+ }
+ } else {
+ token.append(c);
}
}
- if (null != token) { // end of token
+ if (token.length() > 0) {
tokens.add(token.toString());
}
return tokens;
@@ -133,12 +124,13 @@ private static List tokenize(String expr) {
private void check(TokenType expectedTokenType, TokenType tokenType) {
if (expectedTokenType != tokenType) {
- throw new TagExpressionException("Tag expression '%s' could not be parsed because of syntax error: expected %s", infix, expectedTokenType.toString().toLowerCase());
+ throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Expected %s.", infix, expectedTokenType.toString().toLowerCase());
}
}
private T pop(Deque stack) {
- if (stack.isEmpty()) throw new TagExpressionException("Tag expression '%s' could not be parsed because of an empty stack", infix);
+ if (stack.isEmpty())
+ throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of an empty stack", infix);
return stack.pop();
}
@@ -183,7 +175,7 @@ private enum Assoc {
RIGHT
}
- private class Literal implements Expression {
+ private static class Literal implements Expression {
private final String value;
Literal(String value) {
@@ -197,11 +189,15 @@ public boolean evaluate(List variables) {
@Override
public String toString() {
- return value.replaceAll("\\\\", "\\\\\\\\").replaceAll("\\(", "\\\\(").replaceAll("\\)", "\\\\)");
+ return value
+ .replaceAll(Pattern.quote("\\"), Matcher.quoteReplacement("\\\\"))
+ .replaceAll(Pattern.quote("("), Matcher.quoteReplacement("\\("))
+ .replaceAll(Pattern.quote(")"), Matcher.quoteReplacement("\\)"))
+ .replaceAll("\\s", "\\\\ ");
}
}
- private class Or implements Expression {
+ private static class Or implements Expression {
private final Expression left;
private final Expression right;
@@ -221,7 +217,7 @@ public String toString() {
}
}
- private class And implements Expression {
+ private static class And implements Expression {
private final Expression left;
private final Expression right;
@@ -241,7 +237,7 @@ public String toString() {
}
}
- private class Not implements Expression {
+ private static class Not implements Expression {
private final Expression expr;
Not(Expression expr) {
@@ -259,10 +255,15 @@ public String toString() {
}
}
- private class True implements Expression {
+ private static class True implements Expression {
@Override
public boolean evaluate(List variables) {
return true;
}
+
+ @Override
+ public String toString() {
+ return "true";
+ }
}
}
diff --git a/java/src/test/java/io/cucumber/tagexpressions/ErrorsTest.java b/java/src/test/java/io/cucumber/tagexpressions/ErrorsTest.java
new file mode 100644
index 00000000..f641d431
--- /dev/null
+++ b/java/src/test/java/io/cucumber/tagexpressions/ErrorsTest.java
@@ -0,0 +1,30 @@
+package io.cucumber.tagexpressions;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+
+import static java.nio.file.Files.newInputStream;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ErrorsTest {
+
+ private static List