Skip to content

Commit

Permalink
Merge pull request #16271 from mkouba/qute-extension-methods-type-params
Browse files Browse the repository at this point in the history
Qute type-safe - basic support of extension methods with type params
  • Loading branch information
mkouba authored Apr 8, 2021
2 parents b7011a8 + b033089 commit d818d27
Showing 6 changed files with 137 additions and 22 deletions.
3 changes: 3 additions & 0 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
@@ -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());
@@ -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) {
@@ -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());
@@ -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();
@@ -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);
@@ -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()) {
@@ -1563,7 +1616,7 @@ private static AnnotationTarget findTemplateExtensionMethod(Info info, Type matc
continue;
}
}
return extensionMethod.getMethod();
return extensionMethod;
}
return null;
}
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
@@ -5,7 +5,6 @@

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

@@ -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",
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;

@@ -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
@@ -37,7 +37,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);
}

0 comments on commit d818d27

Please sign in to comment.