Repository: quarkusio/quarkus
Issue: @Message template fails to use #each or #for

### Describe the bug

I'm using Qute to enable i18n on a project.

When parsing a string using `Qute.fmt`, passing a template directly renders the message just fine.

But when I reference its template via the message bundle namespace, such as
```java
Qute.fmt("{error:errorMessage(dates)}").data("dates", dates)).attribute('locale', locale).render()
```

I get the following error

```text
2022-02-15 19:54:04,757 ERROR [io.qua.dep.dev.IsolatedDevModeMain] (vert.x-worker-thread-0) Failed to start quarkus: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
	[error]: Build step io.quarkus.qute.deployment.QuteProcessor#processTemplateErrors threw an exception: io.quarkus.qute.TemplateException: Found template problems (1):
[1] Incorrect expression found: {it}
	- it is not a parameter of the message bundle method net.dummy.example.i18n.ErrorMessages#errorMessage()
	- at net.dummy.example.i18n.ErrorMessages#errorMessage():1
	at io.quarkus.qute.deployment.QuteProcessor.processTemplateErrors(QuteProcessor.java:200)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at io.quarkus.deployment.ExtensionLoader$2.execute(ExtensionLoader.java:882)
	at io.quarkus.builder.BuildContext.run(BuildContext.java:277)
	at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
	at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2449)
	at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1478)
	at java.base/java.lang.Thread.run(Thread.java:834)
	at org.jboss.threads.JBossThread.run(JBossThread.java:501)
	Suppressed: io.quarkus.qute.TemplateException: Incorrect expression found: {it}
	- it is not a parameter of the message bundle method net.dummy.example.i18n.ErrorMessages#errorMessage()
	- at net.dummy.example.i18n.ErrorMessages#errorMessage():1
		at io.quarkus.qute.deployment.QuteProcessor.processTemplateErrors(QuteProcessor.java:174)
		... 11 more
```

@Message("FROM i18n A booking for user {user} in the selected period {#each period}{it}{#if it_hasNext}, {/if}{/each} already exists.")

I have an interface like this:
```java
@MessageBundle(value = "error")
public interface ErrorMessages {

    @Message("Information in the selected period {#each dates}{it}{/each} already exists.")
    String errorMessage(List<String> dates);

}
```


### Expected behavior

I want it to return
`Information in the selected period 2022-02-15, 2022-02-16 already exists.`

### Actual behavior

_No response_

### How to Reproduce?

_No response_

### Output of `uname -a` or `ver`

_No response_

### Output of `java -version`

Oracle JDK 11

### GraalVM version (if different from Java)

_No response_

### Quarkus version or git rev

2.7.0.Final

### Build tool (ie. output of `mvnw --version` or `gradlew --version`)

Apache Maven 3.8.4 (9b656c72d54e5bacbed989b64718c159fe39b537)

### Additional information

_No response_

Changed files:
extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java
extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleExpressionValidationTest.java
extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/Item.java
extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java

diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java
index a226da57c02..2233a59a65d 100644
--- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java
+++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java
@@ -78,6 +78,7 @@
 import io.quarkus.qute.EvaluatedParams;
 import io.quarkus.qute.Expression;
 import io.quarkus.qute.Expression.Part;
+import io.quarkus.qute.Expressions;
 import io.quarkus.qute.Namespaces;
 import io.quarkus.qute.Resolver;
 import io.quarkus.qute.deployment.QuteProcessor.LookupConfig;
@@ -316,8 +317,8 @@ void validateMessageBundleMethods(TemplatesAnalysisBuildItem templatesAnalysis,
         for (TemplateAnalysis analysis : templatesAnalysis.getAnalysis()) {
             MessageBundleMethodBuildItem messageBundleMethod = bundleMethods.get(analysis.id);
             if (messageBundleMethod != null) {
+                // All top-level expressions without a namespace should be mapped to a param
                 Set<String> usedParamNames = new HashSet<>();
-                // All top-level expressions without namespace map to a param
                 Set<String> paramNames = IntStream.range(0, messageBundleMethod.getMethod().parameters().size())
                         .mapToObj(idx -> getParameterName(messageBundleMethod.getMethod(), idx)).collect(Collectors.toSet());
                 for (Expression expression : analysis.expressions) {
@@ -342,8 +343,12 @@ private void validateExpression(BuildProducer<IncorrectExpressionBuildItem> inco
             return;
         }
         if (!expression.hasNamespace()) {
-            String name = expression.getParts().get(0).getName();
-            if (!paramNames.contains(name)) {
+            Expression.Part firstPart = expression.getParts().get(0);
+            String name = firstPart.getName();
+            // Skip expressions that have type info derived from a parent section, e.g "it<loop#3>" and "foo<set#3>"
+            if (firstPart.getTypeInfo() == null || (firstPart.getTypeInfo().startsWith("" + Expressions.TYPE_INFO_SEPARATOR)
+                    && !paramNames.contains(name))) {
+                // Expression has no type info or type info that does not match a method parameter
                 incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
                         name + " is not a parameter of the message bundle method "
                                 + messageBundleMethod.getMethod().declaringClass().name() + "#"
@@ -815,7 +820,7 @@ private String generateImplementation(ClassInfo defaultBundleInterface, String d
         return generatedName.replace('/', '.');
     }
 
-    private String getParameterName(MethodInfo method, int position) {
+    static String getParameterName(MethodInfo method, int position) {
         String name = method.parameterName(position);
         AnnotationInstance paramAnnotation = Annotations
                 .find(Annotations.getParameterAnnotations(method.annotations()).stream()
diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
index 50dd9d684b6..7751d806c61 100644
--- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
+++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
@@ -453,8 +453,8 @@ public void beforeParsing(ParserHelper parserHelper) {
                     MethodInfo method = messageBundleMethod.getMethod();
                     for (ListIterator<Type> it = method.parameters().listIterator(); it.hasNext();) {
                         Type paramType = it.next();
-                        parserHelper.addParameter(method.parameterName(it.previousIndex()),
-                                JandexUtil.getBoxedTypeName(paramType));
+                        String name = MessageBundleProcessor.getParameterName(method, it.previousIndex());
+                        parserHelper.addParameter(name, JandexUtil.getBoxedTypeName(paramType));
                     }
                 }
             }
diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/Item.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/Item.java
index 23b0464cb71..b71ef2116af 100644
--- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/Item.java
+++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/Item.java
@@ -1,5 +1,7 @@
 package io.quarkus.qute.deployment.i18n;
 
+import java.util.List;
+
 public class Item {
 
     private String name;
@@ -19,4 +21,8 @@ public Integer getAge() {
         return age;
     }
 
+    public List<String> getNames() {
+        return List.of(name);
+    }
+
 }
diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleExpressionValidationTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleExpressionValidationTest.java
index d730e1a6faa..9cb0d8f9096 100644
--- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleExpressionValidationTest.java
+++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/i18n/MessageBundleExpressionValidationTest.java
@@ -19,6 +19,7 @@ public class MessageBundleExpressionValidationTest {
             .withApplicationRoot((jar) -> jar
                     .addClasses(WrongBundle.class, Item.class)
                     .addAsResource(new StringAsset(
+                            // foo is not a parameter of WrongBundle.hello()
                             "hello=Hallo {foo}!"),
                             "messages/msg_de.properties"))
             .assertException(t -> {
@@ -34,10 +35,12 @@ public class MessageBundleExpressionValidationTest {
                 if (te == null) {
                     fail("No template exception thrown: " + t);
                 }
-                assertTrue(te.getMessage().contains("Found template problems (3)"), te.getMessage());
+                assertTrue(te.getMessage().contains("Found template problems (5)"), te.getMessage());
                 assertTrue(te.getMessage().contains("item.foo"), te.getMessage());
                 assertTrue(te.getMessage().contains("bar"), te.getMessage());
                 assertTrue(te.getMessage().contains("foo"), te.getMessage());
+                assertTrue(te.getMessage().contains("baf"), te.getMessage());
+                assertTrue(te.getMessage().contains("it.baz"), te.getMessage());
             });
 
     @Test
@@ -48,7 +51,8 @@ public void testValidation() {
     @MessageBundle
     public interface WrongBundle {
 
-        @Message("Hello {item.foo} {bar}") // -> there is no foo property and bar is not a parameter 
+        // item has no "foo" property, "bar" and "baf" are not parameters
+        @Message("Hello {item.foo} {bar} {#each item.names}{it}{it.baz}{baf}{/each}")
         String hello(Item item);
 
     }