diff --git a/tag-expressions/CHANGELOG.md b/tag-expressions/CHANGELOG.md index 4fe49063f8..ed6b05c325 100644 --- a/tag-expressions/CHANGELOG.md +++ b/tag-expressions/CHANGELOG.md @@ -20,6 +20,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +* [Go], [JavaScript], [Java], [Ruby] Support backslash-escape in tag expressions + ([#1778](https://github.com/cucumber/common/pull/1778) [yusuke-noda]) + ## [4.0.2] - 2021-09-13 Note: some issues while releasing 4.0.1 prevent us to release it again. @@ -178,3 +181,4 @@ Note: some issues while releasing 4.0.1 prevent us to release it again. [ehuelsmann]: https://github.com/ehuelsmann [link89]: https://github.com/link89 [luke-hill]: https://github.com/luke-hill +[yusuke-noda]: https://github.com/yusuke-noda diff --git a/tag-expressions/go/parser.go b/tag-expressions/go/parser.go index a0f8880043..8759690ab4 100644 --- a/tag-expressions/go/parser.go +++ b/tag-expressions/go/parser.go @@ -112,6 +112,7 @@ func tokenize(expr string) []string { for _, c := range expr { if unicode.IsSpace(c) { collectToken() + escaped = false continue } @@ -119,7 +120,12 @@ func tokenize(expr string) []string { switch ch { case "\\": - escaped = true + if escaped { + token.WriteString(ch) + escaped = false + } else { + escaped = true + } case "(", ")": if escaped { token.WriteString(ch) @@ -130,6 +136,7 @@ func tokenize(expr string) []string { } default: token.WriteString(ch) + escaped = false } } @@ -192,7 +199,12 @@ func (l *literalExpr) Evaluate(variables []string) bool { func (l *literalExpr) ToString() string { return strings.Replace( - strings.Replace(l.value, "(", "\\(", -1), + strings.Replace( + strings.Replace(l.value, "\\", "\\\\", -1), + "(", + "\\(", + -1, + ), ")", "\\)", -1, diff --git a/tag-expressions/go/parser_test.go b/tag-expressions/go/parser_test.go index 6e175a7b4e..3833919a04 100644 --- a/tag-expressions/go/parser_test.go +++ b/tag-expressions/go/parser_test.go @@ -42,6 +42,16 @@ func TestParseForValidCases(t *testing.T) { 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 { @@ -176,6 +186,32 @@ func TestParseForEvaluationErrors(t *testing.T) { 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 { diff --git a/tag-expressions/java/src/main/java/io/cucumber/tagexpressions/TagExpressionParser.java b/tag-expressions/java/src/main/java/io/cucumber/tagexpressions/TagExpressionParser.java index 9131c4dbd7..6b62a2259a 100644 --- a/tag-expressions/java/src/main/java/io/cucumber/tagexpressions/TagExpressionParser.java +++ b/tag-expressions/java/src/main/java/io/cucumber/tagexpressions/TagExpressionParser.java @@ -94,7 +94,7 @@ private static List tokenize(String expr) { StringBuilder token = null; for (int i = 0; i < expr.length(); i++) { char c = expr.charAt(i); - if (ESCAPING_CHAR == c) { + if (ESCAPING_CHAR == c && !isEscaped) { isEscaped = true; } else { if (Character.isWhitespace(c)) { // skip @@ -197,7 +197,7 @@ public boolean evaluate(List variables) { @Override public String toString() { - return value.replaceAll("\\(", "\\\\(").replaceAll("\\)", "\\\\)"); + return value.replaceAll("\\\\", "\\\\\\\\").replaceAll("\\(", "\\\\(").replaceAll("\\)", "\\\\)"); } } diff --git a/tag-expressions/java/src/test/java/io/cucumber/tagexpressions/TagExpressionParserParameterizedTest.java b/tag-expressions/java/src/test/java/io/cucumber/tagexpressions/TagExpressionParserParameterizedTest.java index d76042ce9e..6fa107821a 100644 --- a/tag-expressions/java/src/test/java/io/cucumber/tagexpressions/TagExpressionParserParameterizedTest.java +++ b/tag-expressions/java/src/test/java/io/cucumber/tagexpressions/TagExpressionParserParameterizedTest.java @@ -18,7 +18,11 @@ static Stream data() { arguments("not a", "not ( a )"), arguments("( a and b ) or ( c and d )", "( ( a and b ) or ( c and d ) )"), arguments("not a or b and not c or not d or e and f", "( ( ( not ( a ) or ( b and not ( c ) ) ) or not ( d ) ) or ( e and f ) )"), - arguments("not a\\(1\\) or b and not c or not d or e and f", "( ( ( not ( a\\(1\\) ) or ( b and not ( c ) ) ) or not ( d ) ) or ( e and f ) )") + arguments("not a\\(1\\) or b and not c or not d or e and f", "( ( ( not ( a\\(1\\) ) or ( b and not ( c ) ) ) or not ( d ) ) or ( e and f ) )"), + arguments("a\\\\ and b", "( a\\\\ and b )"), + arguments("\\a and b\\ and c\\", "( ( a and b ) and c )"), + arguments("a\\\\\\( and b\\\\\\)", "( a\\\\\\( and b\\\\\\) )"), + arguments("(a and \\b)", "( a and b )") ); } diff --git a/tag-expressions/java/src/test/java/io/cucumber/tagexpressions/TagExpressionParserTest.java b/tag-expressions/java/src/test/java/io/cucumber/tagexpressions/TagExpressionParserTest.java index d097dfc429..3396d12695 100644 --- a/tag-expressions/java/src/test/java/io/cucumber/tagexpressions/TagExpressionParserTest.java +++ b/tag-expressions/java/src/test/java/io/cucumber/tagexpressions/TagExpressionParserTest.java @@ -48,4 +48,22 @@ void errors_when_there_are_only_operators() { assertThat("Unexpected message", thrownException.getMessage(), is(equalTo("Tag expression 'or or' could not be parsed because of syntax error: expected operand"))); } + @Test + void evaluates_expr_with_escaped_backslash() { + Expression expr = TagExpressionParser.parse("@a\\\\ or (@b\\\\\\)) or (@c\\\\)"); + assertFalse(expr.evaluate(asList("@a @b) @c".split(" ")))); + assertTrue(expr.evaluate(asList("@a\\".split(" ")))); + assertTrue(expr.evaluate(asList("@b\\)".split(" ")))); + assertTrue(expr.evaluate(asList("@c\\".split(" ")))); + } + + @Test + void evaluates_expr_with_backslash() { + Expression expr = TagExpressionParser.parse("@\\a or @b\\ or @c\\"); + assertFalse(expr.evaluate(asList("@\\a @b\\ @c\\".split(" ")))); + assertTrue(expr.evaluate(asList("@a".split(" ")))); + assertTrue(expr.evaluate(asList("@b".split(" ")))); + assertTrue(expr.evaluate(asList("@c".split(" ")))); + } + } diff --git a/tag-expressions/javascript/src/index.ts b/tag-expressions/javascript/src/index.ts index 5f2c407b48..fc4797d21c 100644 --- a/tag-expressions/javascript/src/index.ts +++ b/tag-expressions/javascript/src/index.ts @@ -84,7 +84,7 @@ function tokenize(expr: string): string[] { let token for (let i = 0; i < expr.length; i++) { const c = expr.charAt(i) - if ('\\' === c) { + if ('\\' === c && !isEscaped) { isEscaped = true } else { if (/\s/.test(c)) { @@ -171,7 +171,7 @@ class Literal implements Node { } public toString() { - return this.value.replace(/\(/g, '\\(').replace(/\)/g, '\\)') + return this.value.replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)') } } diff --git a/tag-expressions/javascript/test/tag_expression_parser_test.ts b/tag-expressions/javascript/test/tag_expression_parser_test.ts index 1987ca2815..ef08ec1b7a 100644 --- a/tag-expressions/javascript/test/tag_expression_parser_test.ts +++ b/tag-expressions/javascript/test/tag_expression_parser_test.ts @@ -17,6 +17,13 @@ describe('TagExpressionParser', () => { 'not a\\(\\) or b and not c or not d or e and f', '( ( ( not ( a\\(\\) ) or ( b and not ( c ) ) ) or not ( d ) ) or ( e and f ) )', ], + ['a\\\\ and b', '( a\\\\ and b )'], + ['\\a and b', '( a and b )'], + ['a\\ and b', '( a and b )'], + ['a and b\\', '( a and b )'], + ['( a and b\\\\)', '( a and b\\\\ )'], + ['a\\\\\\( and b\\\\\\)', '( a\\\\\\( and b\\\\\\) )'], + ['(a and \\b)', '( a and b )'], // a or not b ] tests.forEach(function (inOut) { @@ -89,5 +96,27 @@ describe('TagExpressionParser', () => { assert.strictEqual(expr.evaluate(['y']), true) assert.strictEqual(expr.evaluate(['x']), true) }) + + it('evaluates expressions with escaped backslash', function () { + const expr = parse('x\\\\ or(y\\\\\\)) or(z\\\\)') + assert.strictEqual(expr.evaluate([]), false) + assert.strictEqual(expr.evaluate(['x\\']), true) + assert.strictEqual(expr.evaluate(['y\\)']), true) + assert.strictEqual(expr.evaluate(['z\\']), true) + assert.strictEqual(expr.evaluate(['x']), false) + assert.strictEqual(expr.evaluate(['y)']), false) + assert.strictEqual(expr.evaluate(['z']), false) + }) + + it('evaluates expressions with backslash', function () { + const expr = parse('\\x or y\\ or z\\') + assert.strictEqual(expr.evaluate([]), false) + assert.strictEqual(expr.evaluate(['x']), true) + assert.strictEqual(expr.evaluate(['y']), true) + assert.strictEqual(expr.evaluate(['z']), true) + assert.strictEqual(expr.evaluate(['\\x']), false) + assert.strictEqual(expr.evaluate(['y\\']), false) + assert.strictEqual(expr.evaluate(['z\\']), false) + }) }) }) diff --git a/tag-expressions/ruby/lib/cucumber/tag_expressions/expressions.rb b/tag-expressions/ruby/lib/cucumber/tag_expressions/expressions.rb index 1f7db270a0..9e3c0d6759 100644 --- a/tag-expressions/ruby/lib/cucumber/tag_expressions/expressions.rb +++ b/tag-expressions/ruby/lib/cucumber/tag_expressions/expressions.rb @@ -3,7 +3,7 @@ module TagExpressions # Literal expression node class Literal def initialize(value) - @value = value.gsub(/\\\(/, '(').gsub(/\\\)/, ')') + @value = value end def evaluate(variables) @@ -11,7 +11,7 @@ def evaluate(variables) end def to_s - @value.gsub(/\(/, '\\(').gsub(/\)/, '\\)') + @value.gsub(/\\/, "\\\\\\\\").gsub(/\(/, "\\(").gsub(/\)/, "\\)") end end diff --git a/tag-expressions/ruby/lib/cucumber/tag_expressions/parser.rb b/tag-expressions/ruby/lib/cucumber/tag_expressions/parser.rb index 44600e4d81..a9f855c8d3 100644 --- a/tag-expressions/ruby/lib/cucumber/tag_expressions/parser.rb +++ b/tag-expressions/ruby/lib/cucumber/tag_expressions/parser.rb @@ -53,7 +53,36 @@ def precedence(token) end def tokens(infix_expression) - infix_expression.gsub(/(? 0 + result.push(token) + token = "" + end + else + if (ch == '(' || ch == ')') && !escaped + if token.length > 0 + result.push(token) + token = "" + end + result.push(ch) + else + token = token + ch + end + end + escaped = false + end + end + if token.length > 0 + result.push(token) + end + result end def process_tokens!(infix_expression) diff --git a/tag-expressions/ruby/spec/expressions_spec.rb b/tag-expressions/ruby/spec/expressions_spec.rb index 45c3045d8c..8fa8a8ddf8 100644 --- a/tag-expressions/ruby/spec/expressions_spec.rb +++ b/tag-expressions/ruby/spec/expressions_spec.rb @@ -52,4 +52,33 @@ include_examples 'expression node', infix_expression, data end end + + describe Cucumber::TagExpressions::Or do + context '#evaluate' do + infix_expression = 'x\\\\ or (y\\\\\\)) or (z\\\\)' + data = [[%w(), false], + [%w(x), false], + [%w(y\)), false], + [%w(z), false], + [%w(x\\), true], + [%w(y\\\)), true], + [%w(z\\), true]] + include_examples 'expression node', infix_expression, data + end + end + + describe Cucumber::TagExpressions::Or do + context '#evaluate' do + infix_expression = '\\x or y\\ or z\\' + data = [[%w(), false], + [%w(\\x), false], + [%w(y\\), false], + [%w(z\\), false], + [%w(x), true], + [%w(y), true], + [%w(z), true]] + include_examples 'expression node', infix_expression, data + end + end + end diff --git a/tag-expressions/ruby/spec/parser_spec.rb b/tag-expressions/ruby/spec/parser_spec.rb index b7389fc7f0..c77cf694d6 100644 --- a/tag-expressions/ruby/spec/parser_spec.rb +++ b/tag-expressions/ruby/spec/parser_spec.rb @@ -9,7 +9,11 @@ ['not a or b and not c or not d or e and f', '( ( ( not ( a ) or ( b and not ( c ) ) ) or not ( d ) ) or ( e and f ) )'], ['not a\\(\\) or b and not c or not d or e and f', - '( ( ( not ( a\\(\\) ) or ( b and not ( c ) ) ) or not ( d ) ) or ( e and f ) )'] + '( ( ( not ( a\\(\\) ) or ( b and not ( c ) ) ) or not ( d ) ) or ( e and f ) )'], + ['a\\\\ and b', '( a\\\\ and b )'], + ['\\a and b\\ and c\\', '( ( a and b ) and c )'], + ['(a and b\\\\)', '( a and b\\\\ )'], + ['a\\\\\\( and b\\\\\\)', '( a\\\\\\( and b\\\\\\) )'] ] error_test_data = [