Skip to content

Commit

Permalink
Merge pull request quarkusio#15234 from mkouba/issue-15165
Browse files Browse the repository at this point in the history
Qute type checks - fix multiple nested helper hints
  • Loading branch information
gsmet authored Feb 22, 2021
2 parents ee1ec35 + fa256d0 commit 56240a5
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -557,15 +557,15 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI
if (root.isTypeInfo()) {
// E.g. |org.acme.Item|
match.setValues(root.asTypeInfo().rawClass, root.asTypeInfo().resolvedType);
if (root.asTypeInfo().hint != null) {
processHints(templateAnalysis, root.asTypeInfo().hint, match, index, expression, generatedIdsToMatches,
if (root.asTypeInfo().hasHints()) {
processHints(templateAnalysis, root.asTypeInfo().hints, match, index, expression, generatedIdsToMatches,
incorrectExpressions);
}
} else {
if (root.isProperty() && root.asProperty().hint != null) {
if (root.isProperty() && root.asProperty().hasHints()) {
// Root is not a type info but a property with hint
// E.g. 'it<loop#123>' and 'STATUS<when#123>'
if (processHints(templateAnalysis, root.asProperty().hint, match, index, expression,
if (processHints(templateAnalysis, root.asProperty().hints, match, index, expression,
generatedIdsToMatches, incorrectExpressions)) {
// In some cases it's necessary to reset the iterator
iterator = parts.iterator();
Expand Down Expand Up @@ -687,13 +687,10 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI
clazz = index.getClassByName(type.name());
}
match.setValues(clazz, type);
if (info.isProperty()) {
String hint = info.asProperty().hint;
if (hint != null) {
// For example a loop section needs to validate the type of an element
processHints(templateAnalysis, hint, match, index, expression, generatedIdsToMatches,
incorrectExpressions);
}
if (info.isProperty() && info.asProperty().hasHints()) {
// For example a loop section needs to validate the type of an element
processHints(templateAnalysis, info.asProperty().hints, match, index, expression, generatedIdsToMatches,
incorrectExpressions);
}
}
} else {
Expand Down Expand Up @@ -1278,49 +1275,51 @@ private static Type resolveType(AnnotationTarget member, Match match, IndexView

/**
* @param templateAnalysis
* @param helperHint
* @param helperHints
* @param match
* @param index
* @param expression
* @param generatedIdsToMatches
* @param incorrectExpressions
* @return {@code true} if it is necessary to reset the type info part iterator
*/
static boolean processHints(TemplateAnalysis templateAnalysis, String helperHint, Match match, IndexView index,
static boolean processHints(TemplateAnalysis templateAnalysis, List<String> helperHints, Match match, IndexView index,
Expression expression, Map<Integer, Match> generatedIdsToMatches,
BuildProducer<IncorrectExpressionBuildItem> incorrectExpressions) {
if (helperHint == null || helperHint.isEmpty()) {
if (helperHints == null || helperHints.isEmpty()) {
return false;
}
if (helperHint.equals(LoopSectionHelper.Factory.HINT_ELEMENT)) {
// Iterable<Item>, Stream<Item> => Item
// Map<String,Long> => Entry<String,Long>
processLoopElementHint(match, index, expression, incorrectExpressions);
} else if (helperHint.startsWith(LoopSectionHelper.Factory.HINT_PREFIX)) {
Expression valueExpr = findExpression(helperHint, LoopSectionHelper.Factory.HINT_PREFIX, templateAnalysis);
if (valueExpr != null) {
Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId());
if (valueExprMatch != null) {
match.setValues(valueExprMatch.clazz, valueExprMatch.type);
for (String helperHint : helperHints) {
if (helperHint.equals(LoopSectionHelper.Factory.HINT_ELEMENT)) {
// Iterable<Item>, Stream<Item> => Item
// Map<String,Long> => Entry<String,Long>
processLoopElementHint(match, index, expression, incorrectExpressions);
} else if (helperHint.startsWith(LoopSectionHelper.Factory.HINT_PREFIX)) {
Expression valueExpr = findExpression(helperHint, LoopSectionHelper.Factory.HINT_PREFIX, templateAnalysis);
if (valueExpr != null) {
Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId());
if (valueExprMatch != null) {
match.setValues(valueExprMatch.clazz, valueExprMatch.type);
}
}
}
} else if (helperHint.startsWith(WhenSectionHelper.Factory.HINT_PREFIX)) {
// If a value expression resolves to an enum we attempt to use the enum type to validate the enum constant
// This basically transforms the type info "ON<when:12345>" into something like "|org.acme.Status|.ON"
Expression valueExpr = findExpression(helperHint, WhenSectionHelper.Factory.HINT_PREFIX, templateAnalysis);
if (valueExpr != null) {
Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId());
if (valueExprMatch != null && valueExprMatch.clazz.isEnum()) {
match.setValues(valueExprMatch.clazz, valueExprMatch.type);
return true;
} else if (helperHint.startsWith(WhenSectionHelper.Factory.HINT_PREFIX)) {
// If a value expression resolves to an enum we attempt to use the enum type to validate the enum constant
// This basically transforms the type info "ON<when:12345>" into something like "|org.acme.Status|.ON"
Expression valueExpr = findExpression(helperHint, WhenSectionHelper.Factory.HINT_PREFIX, templateAnalysis);
if (valueExpr != null) {
Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId());
if (valueExprMatch != null && valueExprMatch.clazz.isEnum()) {
match.setValues(valueExprMatch.clazz, valueExprMatch.type);
return true;
}
}
}
} else if (helperHint.startsWith(SetSectionHelper.Factory.HINT_PREFIX)) {
Expression valueExpr = findExpression(helperHint, SetSectionHelper.Factory.HINT_PREFIX, templateAnalysis);
if (valueExpr != null) {
Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId());
if (valueExprMatch != null) {
match.setValues(valueExprMatch.clazz, valueExprMatch.type);
} else if (helperHint.startsWith(SetSectionHelper.Factory.HINT_PREFIX)) {
Expression valueExpr = findExpression(helperHint, SetSectionHelper.Factory.HINT_PREFIX, templateAnalysis);
if (valueExpr != null) {
Match valueExprMatch = generatedIdsToMatches.get(valueExpr.getGeneratedId());
if (valueExprMatch != null) {
match.setValues(valueExprMatch.clazz, valueExprMatch.type);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jboss.jandex.ArrayType;
import org.jboss.jandex.ClassInfo;
Expand Down Expand Up @@ -210,11 +212,27 @@ public String toString() {

static abstract class HintInfo extends Info {

final String hint;
static final Pattern HINT_PATTERN = Pattern.compile("\\<[a-zA-Z_0-9#-]+\\>");

public HintInfo(String value, Expression.Part part, String hint) {
// <loop#1>, <set#10><loop-element>, etc.
final List<String> hints;

HintInfo(String value, Expression.Part part, String hintStr) {
super(value, part);
this.hint = hint;
if (hintStr != null) {
List<String> found = new ArrayList<>();
Matcher m = HINT_PATTERN.matcher(hintStr);
while (m.find()) {
found.add(m.group());
}
this.hints = found;
} else {
this.hints = Collections.emptyList();
}
}

boolean hasHints() {
return !hints.isEmpty();
}

}
Expand All @@ -224,7 +242,7 @@ static class TypeInfo extends HintInfo {
final Type resolvedType;
final ClassInfo rawClass;

public TypeInfo(String value, Expression.Part part, String hint, Type resolvedType, ClassInfo rawClass) {
TypeInfo(String value, Expression.Part part, String hint, Type resolvedType, ClassInfo rawClass) {
super(value, part, hint);
this.resolvedType = resolvedType;
this.rawClass = rawClass;
Expand All @@ -246,7 +264,7 @@ static class PropertyInfo extends HintInfo {

final String name;

public PropertyInfo(String name, Expression.Part part, String hint) {
PropertyInfo(String name, Expression.Part part, String hint) {
super(name, part, hint);
this.name = name;
}
Expand All @@ -267,7 +285,7 @@ static class VirtualMethodInfo extends Info {

final String name;

public VirtualMethodInfo(String value, Expression.VirtualMethodPart part) {
VirtualMethodInfo(String value, Expression.VirtualMethodPart part) {
super(value, part);
this.name = part.getName();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.quarkus.qute.deployment;

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;

import org.junit.jupiter.api.Test;

public class TypeInfosTest {

@Test
public void testHintPattern() {
assertHints("<loop-element>", "<loop-element>");
assertHints("<set#10><loop-element>", "<set#10>", "<loop-element>");
assertHints("<set#10><loop#4><any_other>", "<set#10>", "<loop#4>", "<any_other>");
assertHints("<set#10>loop-element>", "<set#10>");
}

private void assertHints(String hintStr, String... expectedHints) {
Matcher m = TypeInfos.HintInfo.HINT_PATTERN.matcher(hintStr);
List<String> hints = new ArrayList<>();
while (m.find()) {
hints.add(m.group());
}
assertEquals(Arrays.asList(expectedHints), hints);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,22 @@ public class TypeSafeLoopTest {
+ "::"
+ "{#for item in items}{#each item.tags('foo')}{it}{/each}{/for}"
+ "::"
+ "{fooList.get(0).tags.size}={#each fooList.get(0).tags}{it}{/each}"), "templates/foo.html"));
+ "{fooList.get(0).tags.size}={#each fooList.get(0).tags}{it}{/each}"), "templates/foo.html")
.addAsResource(new StringAsset("{@java.util.List<io.quarkus.qute.deployment.Foo> list}"
+ "{#for foo in list}"
+ "{#let name=foo.name}"
+ "{#for char in name.toCharArray}"
+ "{char}:"
+ "{/for}"
+ "{/let}"
+ "{/for}"), "templates/nested.html"));

@Inject
Template foo;

@Inject
Template nested;

@Test
public void testValidation() {
List<Foo> foos = Collections.singletonList(new Foo("bravo", 10l));
Expand All @@ -55,6 +66,13 @@ public void testValidation() {
foo.data("list", foos, "fooList", myFoos, "items", items).render());
}

@Test
public void testNestedHintsValidation() {
List<Foo> foos = Collections.singletonList(new Foo("boom", 10l));
assertEquals("b:o:o:m:",
nested.data("list", foos).render());
}

static class Item {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
// Execute the template with the params as the root context object
TemplateImpl tagTemplate = (TemplateImpl) templateSupplier.get();
tagTemplate.root
.resolve(context.resolutionContext().createChild(evaluatedParams, extendingBlocks))
.resolve(context.resolutionContext().createChild(Mapper.wrap(evaluatedParams), extendingBlocks))
.whenComplete((resultNode, t2) -> {
if (t2 != null) {
result.completeExceptionally(t2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import static io.quarkus.qute.Parameter.EMPTY;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -81,7 +81,13 @@ private Iterator<?> extractIterator(Object it) {
} else if (it instanceof Integer) {
return IntStream.rangeClosed(1, (Integer) it).iterator();
} else if (it.getClass().isArray()) {
return Arrays.stream((Object[]) it).iterator();
int length = Array.getLength(it);
List<Object> elements = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
// The val is automatically wrapped for primitive types
elements.add(Array.get(it, i));
}
return elements.iterator();
} else {
throw new TemplateException(String.format(
"Loop section error in template %s on line %s: [%s] resolved to [%s] which is not iterable",
Expand Down Expand Up @@ -130,17 +136,15 @@ public Scope initializeBlock(Scope previousScope, BlockInfo block) {
if (iterable == null) {
iterable = ValueResolvers.THIS;
}
// foo.items
// > |org.acme.Foo|.items<loop-element>
// foo.items becomes |org.acme.Foo|.items<loop-element>
previousScope.setLastPartHint(HINT_ELEMENT);
Expression iterableExpr = block.addExpression(ITERABLE, iterable);
previousScope.setLastPartHint(null);

String alias = block.getParameters().get(ALIAS);

if (iterableExpr.hasTypeInfo()) {
// it.name
// > it<loop#123>.name
// it.name becomes it<loop#123>.name
alias = alias.equals(Parameter.EMPTY) ? DEFAULT_ALIAS : alias;
Scope newScope = new Scope(previousScope);
newScope.putBinding(alias, alias + HINT_PREFIX + iterableExpr.getGeneratedId() + ">");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.qute;

import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

Expand All @@ -19,4 +20,33 @@ default CompletionStage<Object> getAsync(String key) {
return CompletableFuture.completedFuture(get(key));
}

/**
*
* @param key
* @return {@code true} if the mapper should be applied to the specified key
*/
default boolean appliesTo(String key) {
return true;
}

/**
*
* @param map
* @return a mapper that wraps the given map
*/
static Mapper wrap(Map<String, ?> map) {
return new Mapper() {

@Override
public boolean appliesTo(String key) {
return map.containsKey(key);
}

@Override
public Object get(String key) {
return map.get(key);
}
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
result.completeExceptionally(t);
} else {
// Execute the main block with the params as the current context object
context.execute(context.resolutionContext().createChild(r, null)).whenComplete((r2, t2) -> {
context.execute(context.resolutionContext().createChild(Mapper.wrap(r), null)).whenComplete((r2, t2) -> {
if (t2 != null) {
result.completeExceptionally(t2);
} else {
Expand Down Expand Up @@ -71,6 +71,7 @@ public Scope initializeBlock(Scope previousScope, BlockInfo block) {
for (Entry<String, String> entry : block.getParameters().entrySet()) {
Expression expr = block.addExpression(entry.getKey(), entry.getValue());
if (expr.hasTypeInfo()) {
// item.name becomes item<set#1>.name
newScope.putBinding(entry.getKey(), entry.getKey() + HINT_PREFIX + expr.getGeneratedId() + ">");
} else {
newScope.putBinding(entry.getKey(), null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
// Execute the nested content first and make it accessible via the "nested-content" key
evaluatedParams.put(NESTED_CONTENT,
context.execute(
context.resolutionContext().createChild(new HashMap<>(evaluatedParams), null)));
context.resolutionContext().createChild(Mapper.wrap(evaluatedParams), null)));
}
try {
// Execute the template with the params as the root context object
TemplateImpl tagTemplate = (TemplateImpl) templateSupplier.get();
tagTemplate.root.resolve(context.resolutionContext().createChild(evaluatedParams, null))
tagTemplate.root.resolve(context.resolutionContext().createChild(Mapper.wrap(evaluatedParams), null))
.whenComplete((resultNode, t2) -> {
if (t2 != null) {
result.completeExceptionally(t2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,10 @@ public static ValueResolver mapperResolver() {
return new ValueResolver() {

public boolean appliesTo(EvalContext context) {
return context.getBase() instanceof Mapper;
if (context.getBase() instanceof Mapper && context.getParams().isEmpty()) {
return ((Mapper) context.getBase()).appliesTo(context.getName());
}
return false;
}

@Override
Expand Down
Loading

0 comments on commit 56240a5

Please sign in to comment.