Skip to content

Commit f670074

Browse files
committed
Qute: fix nested literal separator in a virtual method parameter
- fix the parser if a virtual method parameter contains a string literal with nested literal separator, such as "Hello '" - fixes #47092
1 parent 46892f9 commit f670074

File tree

4 files changed

+98
-13
lines changed

4 files changed

+98
-13
lines changed

extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TypeInfos.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,13 @@ public boolean isInfixNotationSupported() {
221221
}
222222

223223
@Override
224-
public boolean isLiteralSeparator(char candidate) {
225-
return candidate == '<' || candidate == '>';
224+
public boolean isLiteralSeparatorStart(char candidate) {
225+
return candidate == '<';
226+
}
227+
228+
@Override
229+
public boolean isLiteralSeparatorEnd(char startSeparator, char candidate) {
230+
return candidate == '>';
226231
}
227232

228233
};

extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/StringTemplateExtensionsTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ public void testTemplateExtensions() {
9393
assertEquals("Hello fool!",
9494
hello.data("name", "fool")
9595
.render());
96+
97+
// https://github.com/quarkusio/quarkus/issues/47092
98+
assertEquals("Quteiscool!",
99+
engine.parse("{str:builder('Qute').append(\"is\").append(\"cool!\")}")
100+
.render());
101+
assertEquals("Qute's cool!",
102+
engine.parse("{str:builder('Qute').append(\"'s\").append(\" cool!\")}")
103+
.render());
104+
assertEquals("\"Qute\" is cool!",
105+
engine.parse("{str:builder('\"Qute\" ').append('is').append(\" cool!\")}")
106+
.render());
107+
assertEquals("Hello '!",
108+
engine.parse("{str:concat(\"Hello '\",\"!\")}")
109+
.render());
110+
96111
}
97112

98113
}

independent-projects/qute/core/src/main/java/io/quarkus/qute/Expressions.java

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public static List<String> splitParts(String value, SplitConfig splitConfig) {
7272
if (value == null || value.isEmpty()) {
7373
return Collections.emptyList();
7474
}
75-
boolean literal = false;
75+
char literal = 0;
7676
char separator = 0;
7777
byte infix = 0;
7878
byte brackets = 0;
@@ -83,7 +83,7 @@ public static List<String> splitParts(String value, SplitConfig splitConfig) {
8383
if (splitConfig.isSeparator(c)) {
8484
// Adjacent separators may be ignored
8585
if (separator == 0 || separator != c) {
86-
if (!literal && brackets == 0 && infix == 0) {
86+
if (literal == 0 && brackets == 0 && infix == 0) {
8787
if (splitConfig.shouldPrependSeparator(c)) {
8888
buffer.append(c);
8989
}
@@ -99,11 +99,15 @@ public static List<String> splitParts(String value, SplitConfig splitConfig) {
9999
}
100100
}
101101
} else {
102-
if (splitConfig.isLiteralSeparator(c)) {
103-
literal = !literal;
102+
if (literal == 0
103+
&& splitConfig.isLiteralSeparatorStart(c)) {
104+
literal = c;
105+
} else if (literal != 0
106+
&& splitConfig.isLiteralSeparatorEnd(literal, c)) {
107+
literal = 0;
104108
}
105109
// Non-separator char
106-
if (!literal) {
110+
if (literal == 0) {
107111
// Not inside a string/type literal
108112
if (brackets == 0 && c == ' ' && splitConfig.isInfixNotationSupported()) {
109113
// Infix supported, blank space and not inside a virtual method
@@ -212,16 +216,27 @@ public boolean isInfixNotationSupported() {
212216
}
213217

214218
@Override
215-
public boolean isLiteralSeparator(char candidate) {
216-
return SplitConfig.super.isLiteralSeparator(candidate) || candidate == '<' || candidate == '>';
219+
public boolean isLiteralSeparatorStart(char candidate) {
220+
return SplitConfig.super.isLiteralSeparatorStart(candidate)
221+
// We need this in order to support things like {@com.foo.Bar<? extends org.acme.Baz, String> bar}
222+
// where a space should not be treated as a separator
223+
|| candidate == '<';
224+
}
225+
226+
@Override
227+
public boolean isLiteralSeparatorEnd(char startSeparator, char candidate) {
228+
if (startSeparator == '<') {
229+
return candidate == '>';
230+
}
231+
return SplitConfig.super.isLiteralSeparatorEnd(startSeparator, candidate);
217232
}
218233

219234
};
220235

221236
private static final SplitConfig TYPE_INFO_SPLIT_CONFIG = new DefaultSplitConfig() {
222237

223238
@Override
224-
public boolean isLiteralSeparator(char candidate) {
239+
public boolean isLiteralSeparatorStart(char candidate) {
225240
return candidate == TYPE_INFO_SEPARATOR || LiteralSupport.isStringLiteralSeparator(candidate);
226241
}
227242
};
@@ -247,12 +262,36 @@ public boolean shouldAppendSeparator(char candidate) {
247262

248263
public interface SplitConfig {
249264

265+
/**
266+
*
267+
* @param candidate
268+
* @return {@code true} if the characted should be treated as a "part" separator
269+
*/
250270
boolean isSeparator(char candidate);
251271

252-
default boolean isLiteralSeparator(char candidate) {
272+
/**
273+
* A "part" separator used inside a literal must be ignored.
274+
*
275+
* @param candidate
276+
* @return {@code true} if the characted should be treated as a "literal" start separator
277+
*/
278+
default boolean isLiteralSeparatorStart(char candidate) {
253279
return LiteralSupport.isStringLiteralSeparator(candidate);
254280
}
255281

282+
/**
283+
*
284+
* @param startSeparator
285+
* @param candidate
286+
* @return {@code true} if the characted should be treated as a "literal" end separator
287+
*/
288+
default boolean isLiteralSeparatorEnd(char startSeparator, char candidate) {
289+
if (isLiteralSeparatorStart(startSeparator)) {
290+
return startSeparator == candidate;
291+
}
292+
return false;
293+
}
294+
256295
default boolean shouldPrependSeparator(char candidate) {
257296
return false;
258297
}

independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.junit.jupiter.api.Test;
2323

24+
import io.quarkus.qute.Expression.Part;
2425
import io.quarkus.qute.TemplateException.Builder;
2526
import io.quarkus.qute.TemplateLocator.TemplateLocation;
2627
import io.quarkus.qute.TemplateNode.Origin;
@@ -460,13 +461,38 @@ public void testSectionParameterWithNestedSingleQuotationMark() {
460461
assertSectionParams(engine, "{#let id=\"'Foo \"}", Map.of("id", "\"'Foo \""));
461462
assertSectionParams(engine, "{#let id=\"'Foo ' \"}", Map.of("id", "\"'Foo ' \""));
462463
assertSectionParams(engine, "{#let id=\"'Foo ' \" bar='baz'}", Map.of("id", "\"'Foo ' \"", "bar", "'baz'"));
463-
assertSectionParams(engine, "{#let my=bad id=(\"'Foo ' \" + 1) bar='baz'}",
464-
Map.of("my", "bad", "id", "(\"'Foo ' \" + 1)", "bar", "'baz'"));
464+
assertSectionParams(engine, "{#let my=bad id=(foo + 1) bar='baz'}",
465+
Map.of("my", "bad", "id", "(foo + 1)", "bar", "'baz'"));
465466
assertSectionParams(engine, "{#let id = 'Foo'}", Map.of("id", "'Foo'"));
466467
assertSectionParams(engine, "{#let id= 'Foo'}", Map.of("id", "'Foo'"));
467468
assertSectionParams(engine, "{#let my = (bad or not) id=1}", Map.of("my", "(bad or not)", "id", "1"));
468469
assertSectionParams(engine, "{#let my= (bad or not) id=1}", Map.of("my", "(bad or not)", "id", "1"));
470+
}
471+
472+
@Test
473+
public void testVirtualMethodWithNestedLiteralSeparator() {
474+
Engine engine = Engine.builder().addDefaults().build();
475+
List<Part> parts = engine.parse("{foo('Bar \"!')}").findExpression(e -> true).getParts();
476+
assertVirtualMethodParam(parts.get(0), "foo", "Bar \"!");
477+
478+
parts = engine.parse("{foo(\"Bar '!\")}").findExpression(e -> true).getParts();
479+
assertVirtualMethodParam(parts.get(0), "foo", "Bar '!");
480+
481+
parts = engine.parse("{foo(\"Bar '!\").baz(1)}").findExpression(e -> true).getParts();
482+
assertVirtualMethodParam(parts.get(0), "foo", "Bar '!");
483+
assertVirtualMethodParam(parts.get(1), "baz", "1");
484+
485+
parts = engine.parse("{str:builder('Qute').append(\"is '\").append(\"cool!\")}").findExpression(e -> true).getParts();
486+
assertVirtualMethodParam(parts.get(0), "builder", "Qute");
487+
assertVirtualMethodParam(parts.get(1), "append", "is '");
488+
assertVirtualMethodParam(parts.get(2), "append", "cool!");
489+
}
469490

491+
private void assertVirtualMethodParam(Part part, String name, String literal) {
492+
assertTrue(part.isVirtualMethod());
493+
assertEquals(name, part.getName());
494+
assertTrue(part.asVirtualMethod().getParameters().get(0).isLiteral());
495+
assertEquals(literal, part.asVirtualMethod().getParameters().get(0).getLiteral().toString());
470496
}
471497

472498
@Test

0 commit comments

Comments
 (0)