Skip to content
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

Qute type-safe - basic support of extension methods with type params #16271

Merged
merged 1 commit into from
Apr 8, 2021
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
3 changes: 3 additions & 0 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1383,6 +1383,9 @@ TIP: A map value can be also accessed directly: `{map.myKey}`. Use the bracket n
* `get(index)`: Returns the element at the specified position in a list
** `{list.get(0)}`

* `reversed`: Returns a reversed iterator over a list
** `{#for r in recordsList.reversed}`

TIP: A list element can be accessed directly: `{list.10}` or `{list[10]}`.

===== Numbers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem;
import io.quarkus.arc.processor.Annotations;
import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.arc.processor.InjectionPointInfo;
Expand Down Expand Up @@ -651,6 +652,7 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI
}

AnnotationTarget member = null;
TemplateExtensionMethodBuildItem extensionMethod = null;

if (!match.isPrimitive()) {
Set<String> membersUsed = implicitClassToMembersUsed.get(match.type().name());
Expand Down Expand Up @@ -679,9 +681,12 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI

if (member == null) {
// Then try to find an etension method
member = findTemplateExtensionMethod(info, match.type(), templateExtensionMethods, expression,
extensionMethod = findTemplateExtensionMethod(info, match.type(), templateExtensionMethods, expression,
index,
templateIdToPathFun, results);
if (extensionMethod != null) {
member = extensionMethod.getMethod();
}
}

if (member == null) {
Expand All @@ -707,7 +712,7 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI
match.clearValues();
break;
} else {
Type type = resolveType(member, match, index);
Type type = resolveType(member, match, index, extensionMethod);
ClassInfo clazz = null;
if (type.kind() == Type.Kind.CLASS || type.kind() == Type.Kind.PARAMETERIZED_TYPE) {
clazz = index.getClassByName(type.name());
Expand Down Expand Up @@ -1268,7 +1273,8 @@ private Object translate(JsonObject testData) {
return map;
}

private static Type resolveType(AnnotationTarget member, Match match, IndexView index) {
private static Type resolveType(AnnotationTarget member, Match match, IndexView index,
TemplateExtensionMethodBuildItem extensionMethod) {
Type matchType;
if (member.kind() == Kind.FIELD) {
matchType = member.asField().type();
Expand All @@ -1283,17 +1289,64 @@ private static Type resolveType(AnnotationTarget member, Match match, IndexView
Set<Type> closure = Types.getTypeClosure(match.clazz, Types.buildResolvedMap(
match.getParameterizedTypeArguments(), match.getTypeParameters(),
new HashMap<>(), index), index);
DotName declaringClassName = member.kind() == Kind.METHOD ? member.asMethod().declaringClass().name()
: member.asField().declaringClass().name();

DotName declaringClassName = null;
Type extensionMatchBase = null;
if (member.kind() == Kind.METHOD) {
MethodInfo method = member.asMethod();
List<TypeVariable> typeParameters = method.typeParameters();
if (extensionMethod != null && !extensionMethod.hasNamespace() && !typeParameters.isEmpty()) {
// Special handling for extension methods with type parameters
// For example "static <T> Iterator<T> reversed(List<T> list)"
// 1. identify the type used to match the base object; List<T>
// 2. resolve this type; List<String>
// 3. if needed apply to the return type; Iterator<String>
List<Type> params = method.parameters();
Set<AnnotationInstance> attributeAnnotations = Annotations.getAnnotations(Kind.METHOD_PARAMETER,
ExtensionMethodGenerator.TEMPLATE_ATTRIBUTE, method.annotations());
if (attributeAnnotations.isEmpty()) {
extensionMatchBase = params.get(0);
} else {
for (int i = 0; i < params.size(); i++) {
int position = i;
if (attributeAnnotations.stream()
.noneMatch(a -> a.target().asMethodParameter().position() == position)) {
// The first parameter that is not annotated with @TemplateAttribute is used to match the base object
extensionMatchBase = params.get(i);
break;
}
}
}
if (extensionMatchBase != null && Types.containsTypeVariable(extensionMatchBase)) {
declaringClassName = extensionMatchBase.name();
}
} else {
declaringClassName = method.declaringClass().name();
}
} else if (member.kind() == Kind.FIELD) {
declaringClassName = member.asField().declaringClass().name();
}
// Then find the declaring type with resolved type variables
Type declaringType = closure.stream()
.filter(t -> t.name().equals(declaringClassName)).findAny()
.orElse(null);
Type declaringType = null;
if (declaringClassName != null) {
for (Type type : closure) {
if (type.name().equals(declaringClassName)) {
declaringType = type;
break;
}
}
}
if (declaringType != null
&& declaringType.kind() == org.jboss.jandex.Type.Kind.PARAMETERIZED_TYPE) {
List<TypeVariable> typeParameters;
if (extensionMatchBase != null) {
typeParameters = extensionMethod.getMethod().typeParameters();
} else {
typeParameters = index.getClassByName(declaringType.name()).typeParameters();
}
matchType = Types.resolveTypeParam(matchType,
Types.buildResolvedMap(declaringType.asParameterizedType().arguments(),
index.getClassByName(declaringType.name()).typeParameters(),
typeParameters,
Collections.emptyMap(),
index),
index);
Expand Down Expand Up @@ -1482,7 +1535,7 @@ void autoExtractType() {
}
}

private static AnnotationTarget findTemplateExtensionMethod(Info info, Type matchType,
private static TemplateExtensionMethodBuildItem findTemplateExtensionMethod(Info info, Type matchType,
List<TemplateExtensionMethodBuildItem> templateExtensionMethods, Expression expression, IndexView index,
Function<String, String> templateIdToPathFun, Map<String, Match> results) {
if (!info.isProperty() && !info.isVirtualMethod()) {
Expand Down Expand Up @@ -1563,7 +1616,7 @@ private static AnnotationTarget findTemplateExtensionMethod(Info info, Type matc
continue;
}
}
return extensionMethod.getMethod();
return extensionMethod;
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.quarkus.qute.deployment.extensions;

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

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.inject.Inject;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

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

public class CollectionTemplateExtensionsTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class));

@Inject
Engine engine;

@Test
public void testListGetByIndex() {
assertEquals("true=true=NOT_FOUND",
engine.parse("{@java.util.List<Boolean> list}{list.0.booleanValue}={list[0]}={list[100]}")
.data("list", Collections.singletonList(true)).render());
}

@Test
public void testListReversed() {
List<String> names = new ArrayList<>();
names.add("alpha");
names.add("bravo");
names.add("charlie");
assertEquals("CHARLIE::BRAVO::ALPHA::",
engine.parse("{@java.util.List<String> list}{#each list.reversed}{it.toUpperCase}::{/each}").data("list", names)
.render());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -91,12 +90,6 @@ public void testPriority() {
assertEquals("bravo::baz", engine.getTemplate("priority").data("foo", new Foo("baz", 10l)).render());
}

@Test
public void testListGetByIndex() {
assertEquals("true=true=NOT_FOUND",
engine.parse("{list.0}={list[0]}={list[100]}").data("list", Collections.singletonList(true)).render());
}

@Test
public void testMatchTypeAssignability() {
assertEquals("20",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.quarkus.qute.runtime.extensions;

import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

import javax.enterprise.inject.Vetoed;

Expand All @@ -11,17 +13,35 @@
@TemplateExtension
public class CollectionTemplateExtensions {

static Object get(List<?> list, int index) {
static <T> T get(List<T> list, int index) {
return list.get(index);
}

@SuppressWarnings("unchecked")
@TemplateExtension(matchRegex = "\\d{1,10}")
static Object getByIndex(List<?> list, String index) {
static <T> T getByIndex(List<T> list, String index) {
int idx = Integer.parseInt(index);
if (idx >= list.size()) {
// Be consistent with property resolvers
return Result.NOT_FOUND;
return (T) Result.NOT_FOUND;
}
return list.get(idx);
}

static <T> Iterator<T> reversed(List<T> list) {
ListIterator<T> it = list.listIterator(list.size());
return new Iterator<T>() {

@Override
public boolean hasNext() {
return it.hasPrevious();
}

@Override
public T next() {
return it.previous();
}
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ static Object map(Map map, String name) {
}
}

static Object get(Map<?, ?> map, Object key) {
static <V> V get(Map<?, V> map, Object key) {
return map.get(key);
}

Expand Down