Skip to content

Qute: fix build time validation and generated value resolver for getters #47497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3123,9 +3123,9 @@ private static AnnotationTarget findProperty(String name, ClassInfo clazz, JavaM
if (method.returnType().kind() != org.jboss.jandex.Type.Kind.VOID
&& config.filter().test(method)
&& (method.name().equals(name)
|| ValueResolverGenerator.getPropertyName(method.name()).equals(name))) {
|| isMatchingGetter(method, name, clazz, config))) {
// Skip void, non-public, static and synthetic methods
// Method name must match (exact or getter)
// Method name must match (exact or getter if no exact match exists)
return method;
}
}
Expand Down Expand Up @@ -3154,7 +3154,7 @@ private static AnnotationTarget findProperty(String name, ClassInfo clazz, JavaM
for (MethodInfo method : interfaceClassInfo.methods()) {
if (config.filter().test(method)
&& (method.name().equals(name)
|| ValueResolverGenerator.getPropertyName(method.name()).equals(name))) {
|| isMatchingGetter(method, name, clazz, config))) {
return method;
}
}
Expand All @@ -3165,6 +3165,55 @@ private static AnnotationTarget findProperty(String name, ClassInfo clazz, JavaM
return null;
}

private static boolean isMatchingGetter(MethodInfo method, String name, ClassInfo clazz, JavaMemberLookupConfig config) {
if (ValueResolverGenerator.isGetterName(method.name(), method.returnType())) {
// isActive -> active, hasImage -> image
String propertyName = ValueResolverGenerator.getPropertyName(method.name());
if (propertyName.equals(name)) {
if (config.declaredMembersOnly()) {
return clazz.methods().stream().noneMatch(m -> m.name().equals(name));
} else {
Set<DotName> interfaceNames = new HashSet<>();
while (clazz != null) {
if (interfaceNames != null) {
addInterfaces(clazz, config.index(), interfaceNames);
}
for (MethodInfo m : clazz.methods()) {
if (m.returnType().kind() != org.jboss.jandex.Type.Kind.VOID
&& config.filter().test(m)
&& m.name().equals(name)) {
// Exact match exists
return false;
}
}
DotName superName = clazz.superName();
if (superName == null) {
clazz = null;
} else {
clazz = config.index().getClassByName(clazz.superName());
}
}
for (DotName interfaceName : interfaceNames) {
ClassInfo interfaceClassInfo = config.index().getClassByName(interfaceName);
if (interfaceClassInfo != null) {
for (MethodInfo m : interfaceClassInfo.methods()) {
if (m.isDefault()
&& m.returnType().kind() != org.jboss.jandex.Type.Kind.VOID
&& config.filter().test(m)
&& (m.name().equals(name))) {
// Exact default method match exists
return false;
}
}
}
}
return true;
}
}
}
return false;
}

private static void addInterfaces(ClassInfo clazz, IndexView index, Set<DotName> interfaceNames) {
if (clazz == null) {
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.quarkus.qute.deployment.typesafe.getters;

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.inject.Inject;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.Template;
import io.quarkus.test.QuarkusUnitTest;

public class TypesafeGettersValidationTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addClasses(SomeBean.class, SomeInterface.class)
.addAsResource(new StringAsset("""
{@io.quarkus.qute.deployment.typesafe.getters.TypesafeGettersValidationTest$SomeBean some}
{some.image.length}::{some.hasImage}::{some.hasImage('bar')}::{some.png}::{some.hasPng('bar')}
"""), "templates/some.html"));

@Inject
Template some;

@Test
public void testValidation() {
assertEquals("3::true::true::ping::false", some.data("some", new SomeBean("bar")).render().strip());
}

public static class SomeBean implements SomeInterface {

private final String image;

SomeBean(String image) {
this.image = image;
}

public String image() {
return image;
}

public boolean hasImage() {
return image != null;
}

public boolean hasImage(String val) {
return image.equals(val);
}

}

public interface SomeInterface {

default String png() {
return "ping";
}

default boolean hasPng(String val) {
return png().equals(val);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,10 @@ private boolean implementResolve(ClassCreator valueResolver, String clazzName, C
matchingNames.add(method.name());
}
String propertyName = isGetterName(method.name(), method.returnType()) ? getPropertyName(method.name()) : null;
if (propertyName != null && matchedNames.add(propertyName)) {
if (propertyName != null
// No method with exact name match exists
&& noParamMethods.stream().noneMatch(mk -> mk.name.equals(propertyName))
&& matchedNames.add(propertyName)) {
matchingNames.add(propertyName);
}
if (matchingNames.isEmpty()) {
Expand Down Expand Up @@ -1107,7 +1110,7 @@ public static boolean isSynthetic(int mod) {
return (mod & 0x00001000) != 0;
}

static boolean isGetterName(String name, Type returnType) {
public static boolean isGetterName(String name, Type returnType) {
if (name.startsWith(GET_PREFIX)) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class SimpleGeneratorTest {
public static void init() throws IOException {
TestClassOutput classOutput = new TestClassOutput();
Index index = index(MyService.class, PublicMyService.class, BaseService.class, MyItem.class, String.class,
CompletionStage.class, List.class, MyEnum.class, StringBuilder.class);
CompletionStage.class, List.class, MyEnum.class, StringBuilder.class, SomeBean.class, SomeInterface.class);
ClassInfo myServiceClazz = index.getClassByName(DotName.createSimple(MyService.class.getName()));
ValueResolverGenerator generator = ValueResolverGenerator.builder().setIndex(index).setClassOutput(classOutput)
.addClass(myServiceClazz)
Expand All @@ -51,6 +51,7 @@ public static void init() throws IOException {
.addClass(index.getClassByName(List.class))
.addClass(index.getClassByName(MyEnum.class))
.addClass(index.getClassByName(StringBuilder.class), stringBuilderTemplateData())
.addClass(index.getClassByName(SomeBean.class))
.build();

generator.generate();
Expand Down Expand Up @@ -150,6 +151,11 @@ public void testWithEngine() throws Exception {
assertEquals("one", engine.parse("{MyEnum:valueOf('ONE').name}").render());
assertEquals("10", engine.parse("{io_quarkus_qute_generator_MyService:getDummy(5)}").render());
assertEquals("foo", engine.parse("{builder.append('foo')}").data("builder", new StringBuilder()).render());

// Exact match takes precedence over the getter
assertEquals("bar::true::true::ping::false",
engine.parse("{some.image}::{some.hasImage}::{some.hasImage('bar')}::{some.png}::{some.hasPng('bar')}")
.data("some", new SomeBean("bar")).render());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.quarkus.qute.generator;

import io.quarkus.qute.TemplateData;

@TemplateData
public class SomeBean implements SomeInterface {

private final String image;

SomeBean(String image) {
this.image = image;
}

public String image() {
return image;
}

public boolean hasImage() {
return image != null;
}

public boolean hasImage(String val) {
return image.equals(val);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.quarkus.qute.generator;

public interface SomeInterface {

default String png() {
return "ping";
}

default boolean hasPng(String val) {
return png().equals(val);
}

}
Loading