Repository: quarkusio/quarkus
Issue: Qute unable to find template extension methods with @TemplateAttribute annotation

### Describe the bug

I have updated my Quarkus app from 2.3.1.Final to 2.5.0.Final. Since then, Qute Template Extension no longer recognizes methods marked with an @TemplateAttribute annotation and throws the following error when launching from the app. I can confirm that the error happens from version 2.4.0.Final.

I have built a small project with which the error can be reproduced: https://github.com/andlinger/quarkus-qute-namespace

In the [Extensions](https://github.com/andlinger/quarkus-qute-namespace/blob/main/quarkus/src/main/java/io/i15s/quarkus/namespace/Extensions.java) class you can find two methods. `formatCountryEnglish` works perfectly. As soon as `formatCountry` is used, Quarkus produces the exception attached below.

```
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: {base:formatCountry("SE")}
	- no matching namespace [base] extension method found
	- at HtmlResource/test.html:5
	at io.quarkus.qute.deployment.QuteProcessor.processTemplateErrors(QuteProcessor.java:194)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at io.quarkus.deployment.ExtensionLoader$2.execute(ExtensionLoader.java:887)
	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:833)
	at org.jboss.threads.JBossThread.run(JBossThread.java:501)
	Suppressed: io.quarkus.qute.TemplateException: Incorrect expression found: {base:formatCountry("SE")}
	- no matching namespace [base] extension method found
	- at HtmlResource/test.html:5
		at io.quarkus.qute.deployment.QuteProcessor.processTemplateErrors(QuteProcessor.java:168)
		... 11 more

	at io.quarkus.runner.bootstrap.AugmentActionImpl.runAugment(AugmentActionImpl.java:330)
	at io.quarkus.runner.bootstrap.AugmentActionImpl.createInitialRuntimeApplication(AugmentActionImpl.java:252)
	at io.quarkus.runner.bootstrap.AugmentActionImpl.createInitialRuntimeApplication(AugmentActionImpl.java:60)
	at io.quarkus.deployment.dev.IsolatedDevModeMain.firstStart(IsolatedDevModeMain.java:93)
	at io.quarkus.deployment.dev.IsolatedDevModeMain.accept(IsolatedDevModeMain.java:450)
	at io.quarkus.deployment.dev.IsolatedDevModeMain.accept(IsolatedDevModeMain.java:67)
	at io.quarkus.bootstrap.app.CuratedApplication.runInCl(CuratedApplication.java:149)
	at io.quarkus.bootstrap.app.CuratedApplication.runInAugmentClassLoader(CuratedApplication.java:105)
	at io.quarkus.deployment.dev.DevModeMain.start(DevModeMain.java:145)
	at io.quarkus.deployment.dev.DevModeMain.main(DevModeMain.java:63)
Caused by: 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: {base:formatCountry("SE")}
	- no matching namespace [base] extension method found
	- at HtmlResource/test.html:5
	at io.quarkus.qute.deployment.QuteProcessor.processTemplateErrors(QuteProcessor.java:194)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at io.quarkus.deployment.ExtensionLoader$2.execute(ExtensionLoader.java:887)
	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:833)
	at org.jboss.threads.JBossThread.run(JBossThread.java:501)
	Suppressed: io.quarkus.qute.TemplateException: Incorrect expression found: {base:formatCountry("SE")}
	- no matching namespace [base] extension method found
	- at HtmlResource/test.html:5
		at io.quarkus.qute.deployment.QuteProcessor.processTemplateErrors(QuteProcessor.java:168)
		... 11 more

	at io.quarkus.builder.Execution.run(Execution.java:116)
	at io.quarkus.builder.BuildExecutionBuilder.execute(BuildExecutionBuilder.java:79)
	at io.quarkus.deployment.QuarkusAugmentor.run(QuarkusAugmentor.java:161)
	at io.quarkus.runner.bootstrap.AugmentActionImpl.runAugment(AugmentActionImpl.java:328)
	... 9 more
Caused by: io.quarkus.qute.TemplateException: Found template problems (1):
[1] Incorrect expression found: {base:formatCountry("SE")}
	- no matching namespace [base] extension method found
	- at HtmlResource/test.html:5
	at io.quarkus.qute.deployment.QuteProcessor.processTemplateErrors(QuteProcessor.java:194)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at io.quarkus.deployment.ExtensionLoader$2.execute(ExtensionLoader.java:887)
	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:833)
	at org.jboss.threads.JBossThread.run(JBossThread.java:501)
	Suppressed: io.quarkus.qute.TemplateException: Incorrect expression found: {base:formatCountry("SE")}
	- no matching namespace [base] extension method found
	- at HtmlResource/test.html:5
		at io.quarkus.qute.deployment.QuteProcessor.processTemplateErrors(QuteProcessor.java:168)
		... 11 more
```

### Expected behavior

_No response_

### Actual behavior

_No response_

### How to Reproduce?

_No response_

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

_No response_

### Output of `java -version`

_No response_

### GraalVM version (if different from Java)

_No response_

### Quarkus version or git rev

_No response_

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

_No response_

### Additional information

_No response_

Changed files:
independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java
independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java
extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java
extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TemplateExtensionAttributeTest.java

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 00873e95b7f..3594df36ea8 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
@@ -111,6 +111,7 @@
 import io.quarkus.qute.generator.ExtensionMethodGenerator;
 import io.quarkus.qute.generator.ExtensionMethodGenerator.NamespaceResolverCreator;
 import io.quarkus.qute.generator.ExtensionMethodGenerator.NamespaceResolverCreator.ResolveCreator;
+import io.quarkus.qute.generator.ExtensionMethodGenerator.Param;
 import io.quarkus.qute.generator.ValueResolverGenerator;
 import io.quarkus.qute.runtime.ContentTypes;
 import io.quarkus.qute.runtime.EngineProducer;
@@ -1729,31 +1730,26 @@ private static TemplateExtensionMethodBuildItem findTemplateExtensionMethod(Info
                 // Name does not match
                 continue;
             }
-            List<Type> parameters = extensionMethod.getMethod().parameters();
-            int realParamSize = parameters.size();
-            if (!extensionMethod.hasNamespace()) {
-                realParamSize -= 1;
-            }
-            if (TemplateExtension.ANY.equals(extensionMethod.getMatchName())) {
-                realParamSize -= 1;
-            }
-            if (realParamSize > 0 && !info.isVirtualMethod()) {
+
+            List<Param> evaluatedParams = extensionMethod.getParams().evaluated();
+            if (evaluatedParams.size() > 0 && !info.isVirtualMethod()) {
                 // If method accepts additional params the info must be a virtual method
                 continue;
             }
             if (info.isVirtualMethod()) {
                 // For virtual method validate the number of params and attempt to validate the parameter types if available
                 VirtualMethodPart virtualMethod = info.part.asVirtualMethod();
+
                 boolean isVarArgs = ValueResolverGenerator.isVarArgs(extensionMethod.getMethod());
-                int lastParamIdx = parameters.size() - 1;
+                int lastParamIdx = evaluatedParams.size() - 1;
 
                 if (isVarArgs) {
                     // For varargs methods match the minimal number of params
-                    if ((realParamSize - 1) > virtualMethod.getParameters().size()) {
+                    if ((evaluatedParams.size() - 1) > virtualMethod.getParameters().size()) {
                         continue;
                     }
                 } else {
-                    if (virtualMethod.getParameters().size() != realParamSize) {
+                    if (virtualMethod.getParameters().size() != evaluatedParams.size()) {
                         // Check number of parameters; some of params of the extension method must be ignored
                         continue;
                     }
@@ -1761,8 +1757,7 @@ private static TemplateExtensionMethodBuildItem findTemplateExtensionMethod(Info
 
                 // Check parameter types if available
                 boolean matches = true;
-                // Skip base and name param if needed
-                int idx = parameters.size() - realParamSize;
+                int idx = 0;
 
                 for (Expression param : virtualMethod.getParameters()) {
 
@@ -1772,9 +1767,9 @@ private static TemplateExtensionMethodBuildItem findTemplateExtensionMethod(Info
                         Type paramType;
                         if (isVarArgs && (idx >= lastParamIdx)) {
                             // Replace the type for varargs methods
-                            paramType = parameters.get(lastParamIdx).asArrayType().component();
+                            paramType = evaluatedParams.get(lastParamIdx).type.asArrayType().component();
                         } else {
-                            paramType = parameters.get(idx);
+                            paramType = evaluatedParams.get(idx).type;
                         }
                         if (!Types.isAssignableFrom(paramType,
                                 result.type, index)) {
diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java
index 0495521b5b4..0bd918fff92 100644
--- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java
+++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateExtensionMethodBuildItem.java
@@ -9,6 +9,8 @@
 import io.quarkus.builder.item.MultiBuildItem;
 import io.quarkus.qute.Namespaces;
 import io.quarkus.qute.TemplateExtension;
+import io.quarkus.qute.generator.ExtensionMethodGenerator;
+import io.quarkus.qute.generator.ExtensionMethodGenerator.Parameters;
 
 /**
  * Represents a template extension method.
@@ -24,6 +26,7 @@ public final class TemplateExtensionMethodBuildItem extends MultiBuildItem {
     private final Type matchType;
     private final int priority;
     private final String namespace;
+    private final Parameters params;
 
     public TemplateExtensionMethodBuildItem(MethodInfo method, String matchName, String matchRegex, Type matchType,
             int priority, String namespace) {
@@ -34,6 +37,7 @@ public TemplateExtensionMethodBuildItem(MethodInfo method, String matchName, Str
         this.priority = priority;
         this.namespace = (namespace != null && !namespace.isEmpty()) ? Namespaces.requireValid(namespace) : namespace;
         this.matchPattern = (matchRegex == null || matchRegex.isEmpty()) ? null : Pattern.compile(matchRegex);
+        this.params = new ExtensionMethodGenerator.Parameters(method, matchPattern != null || matchesAny(), hasNamespace());
     }
 
     public MethodInfo getMethod() {
@@ -68,11 +72,19 @@ boolean matchesName(String name) {
         if (matchPattern != null) {
             return matchPattern.matcher(name).matches();
         }
-        return TemplateExtension.ANY.equals(matchName) ? true : matchName.equals(name);
+        return matchesAny() ? true : matchName.equals(name);
     }
 
-    boolean hasNamespace() {
+    boolean matchesAny() {
+        return TemplateExtension.ANY.equals(matchName);
+    }
+
+    public boolean hasNamespace() {
         return namespace != null && !namespace.isEmpty();
     }
 
+    public Parameters getParams() {
+        return params;
+    }
+
 }
diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TemplateExtensionAttributeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TemplateExtensionAttributeTest.java
index e669dc943f6..b5b6873dc63 100644
--- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TemplateExtensionAttributeTest.java
+++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/extensions/TemplateExtensionAttributeTest.java
@@ -6,6 +6,7 @@
 
 import javax.inject.Inject;
 
+import org.jboss.shrinkwrap.api.asset.StringAsset;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
@@ -19,6 +20,9 @@ public class TemplateExtensionAttributeTest {
     @RegisterExtension
     static final QuarkusUnitTest config = new QuarkusUnitTest()
             .withApplicationRoot((jar) -> jar
+                    .addAsResource(
+                            new StringAsset("{ping:transform('Foo')}"),
+                            "templates/foo.txt")
                     .addClasses(Extensions.class));
 
     @Inject
@@ -34,6 +38,8 @@ public void testTemplateExtensions() {
                 engine.parse("{foo.myAttr}").render());
         assertEquals("OK",
                 engine.parse("{attr:ping}").instance().setAttribute("myAttribute", "OK").render());
+        assertEquals("foo::cs",
+                engine.getTemplate("foo").instance().setAttribute("locale", "cs").render());
     }
 
     @TemplateExtension
@@ -54,4 +60,13 @@ static String ping(@TemplateAttribute Object myAttribute) {
 
     }
 
+    @TemplateExtension(namespace = "ping")
+    public static class NamespaceExtensions {
+
+        static String transform(@TemplateAttribute("locale") Object loc, String val) {
+            return val.toLowerCase() + "::" + loc.toString();
+        }
+
+    }
+
 }
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java
index 7d8dbda3527..b66e07cf916 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java
@@ -107,7 +107,7 @@
     String namespace() default "";
 
     /**
-     * Used to annotated a template extension method parameter that should be obtained via
+     * Used to annotate a template extension method parameter that should be obtained via
      * {@link TemplateInstance#getAttribute(String)}. The parameter type must be {@link java.lang.Object}.
      * 
      * <pre>
diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java
index 508b75e9ab5..4faedb00254 100644
--- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java
+++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java
@@ -670,11 +670,11 @@ static Type box(Primitive primitive) {
         }
     }
 
-    static class Parameters implements Iterable<Param> {
+    public static final class Parameters implements Iterable<Param> {
 
         final List<Param> params;
 
-        Parameters(MethodInfo method, boolean matchAnyOrRegex, boolean hasNamespace) {
+        public Parameters(MethodInfo method, boolean matchAnyOrRegex, boolean hasNamespace) {
             List<Type> parameters = method.parameters();
             Map<Integer, String> attributeParamNames = new HashMap<>();
             for (AnnotationInstance annotation : method.annotations()) {
@@ -721,20 +721,20 @@ static class Parameters implements Iterable<Param> {
             if (matchAnyOrRegex) {
                 Param nameParam = getFirst(ParamKind.NAME);
                 if (nameParam == null || !nameParam.type.name().equals(DotNames.STRING)) {
-                    throw new IllegalStateException(
+                    throw new TemplateException(
                             "Template extension method declared on " + method.declaringClass().name()
                                     + " must accept at least one string parameter to match the name: " + method);
                 }
             }
             if (!hasNamespace && getFirst(ParamKind.BASE) == null) {
-                throw new IllegalStateException(
+                throw new TemplateException(
                         "Template extension method declared on " + method.declaringClass().name()
                                 + " must accept at least one parameter to match the base object: " + method);
             }
 
             for (Param param : params) {
                 if (param.kind == ParamKind.ATTR && !param.type.name().equals(DotNames.OBJECT)) {
-                    throw new IllegalStateException(
+                    throw new TemplateException(
                             "Template extension method parameter annotated with @TemplateAttribute declared on "
                                     + method.declaringClass().name()
                                     + " must be of type java.lang.Object: " + method);
@@ -742,7 +742,7 @@ static class Parameters implements Iterable<Param> {
             }
         }
 
-        String[] parameterTypesAsStringArray() {
+        public String[] parameterTypesAsStringArray() {
             String[] types = new String[params.size()];
             for (int i = 0; i < params.size(); i++) {
                 types[i] = params.get(i).type.name().toString();
@@ -750,7 +750,7 @@ String[] parameterTypesAsStringArray() {
             return types;
         }
 
-        Param getFirst(ParamKind kind) {
+        public Param getFirst(ParamKind kind) {
             for (Param param : params) {
                 if (param.kind == kind) {
                     return param;
@@ -759,15 +759,15 @@ Param getFirst(ParamKind kind) {
             return null;
         }
 
-        Param get(int index) {
+        public Param get(int index) {
             return params.get(index);
         }
 
-        int size() {
+        public int size() {
             return params.size();
         }
 
-        boolean needsEvaluation() {
+        public boolean needsEvaluation() {
             for (Param param : params) {
                 if (param.kind == ParamKind.EVAL) {
                     return true;
@@ -776,7 +776,7 @@ boolean needsEvaluation() {
             return false;
         }
 
-        List<Param> evaluated() {
+        public List<Param> evaluated() {
             if (params.isEmpty()) {
                 return Collections.emptyList();
             }
@@ -796,12 +796,12 @@ public Iterator<Param> iterator() {
 
     }
 
-    static class Param {
+    public static final class Param {
 
-        final String name;
-        final Type type;
-        final int position;
-        final ParamKind kind;
+        public final String name;
+        public final Type type;
+        public final int position;
+        public final ParamKind kind;
 
         public Param(String name, Type type, int position, ParamKind paramKind) {
             this.name = name;