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 checks - fix multiple nested helper hints #15234

Merged
merged 1 commit into from
Feb 22, 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
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