diff --git a/src/main/java/pl/allegro/tech/opel/OpelParser.java b/src/main/java/pl/allegro/tech/opel/OpelParser.java index 89a5b20..15d6a17 100644 --- a/src/main/java/pl/allegro/tech/opel/OpelParser.java +++ b/src/main/java/pl/allegro/tech/opel/OpelParser.java @@ -7,10 +7,14 @@ import org.parboiled.annotations.SuppressSubnodes; import java.math.BigDecimal; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @BuildParseTree public class OpelParser extends BaseParser { + private final Pattern escapeCharacterPattern = Pattern.compile("\\\\."); + final ImplicitConversion implicitConversion; final OpelNodeFactory nodeFactory; @@ -343,14 +347,36 @@ protected Rule fromStringLiteral(String string) { return String(string).label("'" + string + "'"); } + /** + * Escapes special characters in string literals. + * Explicitly converts \t and \n to their corresponding whitespace characters. + * All other escaped characters are left as they are. + */ protected String escapeString(String string) { + Matcher matcher = escapeCharacterPattern.matcher(string); StringBuilder result = new StringBuilder(); - for (int i = 0; i < string.length(); i++) { - if (string.charAt(i) == '\\') { - i++; + while (matcher.find()) { + String replacement; + switch (matcher.group()) { + case "\\t": + replacement = "\t"; + break; + case "\\r": + replacement = "\r"; + break; + case "\\n": + replacement = "\n"; + break; + case "\\\\": + replacement = "\\\\"; + break; + default: + replacement = matcher.group().substring(1); + break; } - result.append(string.charAt(i)); + matcher.appendReplacement(result, replacement); } + matcher.appendTail(result); return result.toString(); } diff --git a/src/test/groovy/pl/allegro/tech/opel/OpelEngineIntegrationSpec.groovy b/src/test/groovy/pl/allegro/tech/opel/OpelEngineIntegrationSpec.groovy index d266861..f0a5961 100644 --- a/src/test/groovy/pl/allegro/tech/opel/OpelEngineIntegrationSpec.groovy +++ b/src/test/groovy/pl/allegro/tech/opel/OpelEngineIntegrationSpec.groovy @@ -7,7 +7,6 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.ExecutionException import static OpelEngineBuilder.create -import static pl.allegro.tech.opel.TestUtil.constFunctionReturning import static pl.allegro.tech.opel.TestUtil.functions class OpelEngineIntegrationSpec extends Specification { @@ -74,9 +73,9 @@ xyz'""" def 'should evaluate expression with whitespaces "#input"'() { given: def engine = create() - .withImplicitConversion(String, BigDecimal, { string -> new BigDecimal(string) }) - .withImplicitConversion(BigDecimal, String, { decimal -> decimal.toPlainString() }) - .build() + .withImplicitConversion(String, BigDecimal, { string -> new BigDecimal(string) }) + .withImplicitConversion(BigDecimal, String, { decimal -> decimal.toPlainString() }) + .build() expect: engine.eval(input).get() == expResult @@ -109,9 +108,9 @@ xyz'""" def 'should evaluate expression which starts with whitespace(s): "#input"'() { given: def engine = create() - .withImplicitConversion(String, BigDecimal, { string -> new BigDecimal(string) }) - .withImplicitConversion(BigDecimal, String, { decimal -> decimal.toPlainString() }) - .build() + .withImplicitConversion(String, BigDecimal, { string -> new BigDecimal(string) }) + .withImplicitConversion(BigDecimal, String, { decimal -> decimal.toPlainString() }) + .build() expect: engine.eval(input).get() == expResult @@ -305,4 +304,23 @@ xyz'""" counter1 == 1 counter2 == 0 } + + @Unroll + def 'should not treat \\n and \\t characters as escaped but as whitespace control characters'() { + given: + def engine = create().build() + + expect: + engine.eval(input).get() == expResult + + where: + input || expResult + /'Guns\'n\'Roses'.startsWith('Guns\'n\'R')/ || true //\' is an escaped character + /'\\slash\\'.startsWith('\\')/ || true //\\ is an escaped character + /'pest'.startsWith('\pest')/ || true //\p is an escaped character + /'test'.startsWith('\test')/ || false //\t is replaced by tab character + /'nest'.startsWith('\nest')/ || false //\n is replaced by newline character + /'rest'.startsWith('\rest')/ || false //\r is replaced by RF character + /'rust'.startsWith('\r\nust')/ || false //\r\n is replaced with newline character + } }