Repository: quarkusio/quarkus
Issue: Qute section helper type check is not complete

### Describe the bug

I'm writing the following Qute section helpers:

```java
@EngineConfiguration
public class QuteCode128Code extends QuteBarCode {

    public QuteCode128Code() {
        super("code128", Barcode::code128Img);
    }
}

public abstract class QuteBarCode implements SectionHelperFactory<QuteBarCode.CustomSectionHelper> {

    @FunctionalInterface
    public static interface BarCodeEncoder {
        String encode(String value, int width, int height);
    }

    private String name;
    private BarCodeEncoder encoder;

    public QuteBarCode(String name, BarCodeEncoder encoder) {
        this.name = name;
        this.encoder = encoder;
    }

    @Override
    public List<String> getDefaultAliases() {
        return List.of(name);
    }

    @Override
    public ParametersInfo getParameters() {
        return ParametersInfo.builder()
                .addParameter(Parameter.builder("value"))
                .addParameter(Parameter.builder("size").optional())
                .addParameter(Parameter.builder("width").optional())
                .addParameter(Parameter.builder("height").optional())
                .build();
    }

    @Override
    public Scope initializeBlock(Scope outerScope, BlockInfo block) {
        Util.declareBlock(block, "value", "size", "width", "height");
        return SectionHelperFactory.super.initializeBlock(outerScope, block);
    }

    @Override
    public CustomSectionHelper initialize(SectionInitContext context) {
        Util.requireParameter(context, "value");
        // FIXME: support compile-time type-checking when Qute supports it
        Map<String, Expression> params = Util.collectExpressions(context, "value", "size", "width", "height");
        if (context.hasParameter("size")) {
            if (params.containsKey("width") || params.containsKey("height")) {
                throw new TemplateException("Cannot set both size and (width or height): choose one the others");
            }
        }
        return new CustomSectionHelper(params, encoder);
    }

    static class CustomSectionHelper implements SectionHelper {

        private Map<String, Expression> params;
        private BarCodeEncoder encoder;

        public CustomSectionHelper(Map<String, Expression> params, BarCodeEncoder encoder) {
            this.params = params;
            this.encoder = encoder;
        }

        @Override
        public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
            return Futures.evaluateParams(params, context.resolutionContext())
                    .thenApply(values -> {
                        String value = Util.typecheckValue(values, "value", String.class);
                        Integer size = Util.typecheckValue(values, "size", Integer.class, 200);
                        Integer width = Util.typecheckValue(values, "width", Integer.class, size);
                        Integer height = Util.typecheckValue(values, "height", Integer.class, size);

                        return new SingleResultNode(encoder.encode(value, width, height));
                    });
        }
    }
}
```

And I get exceptions at runtime:

```
	Suppressed: io.quarkus.qute.TemplateException: A class annotated with @EngineConfiguration must implement one of the [io.quarkus.qute.SectionHelperFactory, io.quarkus.qute.ValueResolver, io.quarkus.qute.NamespaceResolver]: io.quarkiverse.renarde.barcode.runtime.QuteCode39Code
		at io.quarkus.qute.deployment.QuteProcessor.collectEngineConfigurations(QuteProcessor.java:2121)
```

This is because the code in `QuteProcessor` only checks direct super interfaces, and mine comes from the superclass:

```java
    private static boolean isImplementorOf(ClassInfo target, DotName interfaceName) {
        for (DotName name : target.asClass().interfaceNames()) {
            if (name.equals(interfaceName)) {
                return true;
            }
        }
        return false;
    }
```

I think we should have some better subtyping rules here, no?

### 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:
extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/resolver/CustomResolversTest.java
extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java
extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypesTest.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/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java
index ce524f97723..e4effe48efb 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
@@ -464,6 +464,7 @@ TemplatesAnalysisBuildItem analyzeTemplates(List<TemplatePathBuildItem> template
             TemplateFilePathsBuildItem filePaths, List<CheckedTemplateBuildItem> checkedTemplates,
             List<MessageBundleMethodBuildItem> messageBundleMethods, List<TemplateGlobalBuildItem> globals, QuteConfig config,
             Optional<EngineConfigurationsBuildItem> engineConfigurations,
+            BeanArchiveIndexBuildItem beanArchiveIndex,
             BuildProducer<CheckedFragmentValidationBuildItem> checkedFragmentValidations) {
         long start = System.nanoTime();
 
@@ -490,7 +491,8 @@ TemplatesAnalysisBuildItem analyzeTemplates(List<TemplatePathBuildItem> template
         // Register additional section factories
         if (engineConfigurations.isPresent()) {
             Collection<ClassInfo> sectionFactories = engineConfigurations.get().getConfigurations().stream()
-                    .filter(c -> isImplementorOf(c, Names.SECTION_HELPER_FACTORY)).collect(Collectors.toList());
+                    .filter(c -> Types.isImplementorOf(c, Names.SECTION_HELPER_FACTORY, beanArchiveIndex.getIndex()))
+                    .collect(Collectors.toList());
             // Use the deployment class loader - it can load application classes; it's non-persistent and isolated
             ClassLoader tccl = Thread.currentThread().getContextClassLoader();
             for (ClassInfo factoryClass : sectionFactories) {
@@ -2078,11 +2080,11 @@ CustomTemplateLocatorPatternsBuildItem validateAndCollectCustomTemplateLocatorLo
         // Collect TemplateLocators annotated with io.quarkus.qute.Locate
         for (AnnotationInstance locate : beanArchiveIndex.getIndex().getAnnotations(Names.LOCATE)) {
             AnnotationTarget locateTarget = locate.target();
-            if (isTargetClass(locateTarget)) {
-                if (isNotTemplateLocatorImpl(locateTarget)) {
-                    reportFoundInvalidTarget(validationErrors, locateTarget);
-                } else {
+            if (locateTarget.kind() == Kind.CLASS) {
+                if (Types.isImplementorOf(locateTarget.asClass(), Names.TEMPLATE_LOCATOR, beanArchiveIndex.getIndex())) {
                     addLocationRegExToLocators(locationPatterns, locate.value(), locateTarget, validationErrors);
+                } else {
+                    reportFoundInvalidTarget(validationErrors, locateTarget);
                 }
             }
         }
@@ -2090,14 +2092,14 @@ CustomTemplateLocatorPatternsBuildItem validateAndCollectCustomTemplateLocatorLo
         // Collect TemplateLocators annotated with multiple 'io.quarkus.qute.Locate'
         for (AnnotationInstance locates : beanArchiveIndex.getIndex().getAnnotations(Names.LOCATES)) {
             AnnotationTarget locatesTarget = locates.target();
-            if (isTargetClass(locatesTarget)) {
-                if (isNotTemplateLocatorImpl(locatesTarget)) {
-                    reportFoundInvalidTarget(validationErrors, locatesTarget);
-                } else {
+            if (locatesTarget.kind() == Kind.CLASS) {
+                if (Types.isImplementorOf(locatesTarget.asClass(), Names.TEMPLATE_LOCATOR, beanArchiveIndex.getIndex())) {
                     // locates.value() is array of 'io.quarkus.qute.Locate'
                     for (AnnotationInstance locate : locates.value().asNestedArray()) {
                         addLocationRegExToLocators(locationPatterns, locate.value(), locatesTarget, validationErrors);
                     }
+                } else {
+                    reportFoundInvalidTarget(validationErrors, locatesTarget);
                 }
             }
         }
@@ -2111,9 +2113,16 @@ void collectEngineConfigurations(
             BuildProducer<EngineConfigurationsBuildItem> engineConfig,
             BuildProducer<ValidationErrorBuildItem> validationErrors) {
 
+        Collection<AnnotationInstance> engineConfigAnnotations = beanArchiveIndex.getIndex()
+                .getAnnotations(Names.ENGINE_CONFIGURATION);
+        if (engineConfigAnnotations.isEmpty()) {
+            return;
+        }
+
         List<ClassInfo> engineConfigClasses = new ArrayList<>();
+        IndexView index = beanArchiveIndex.getIndex();
 
-        for (AnnotationInstance annotation : beanArchiveIndex.getIndex().getAnnotations(Names.ENGINE_CONFIGURATION)) {
+        for (AnnotationInstance annotation : engineConfigAnnotations) {
             AnnotationTarget target = annotation.target();
             if (target.kind() == Kind.CLASS) {
                 ClassInfo targetClass = target.asClass();
@@ -2125,7 +2134,7 @@ void collectEngineConfigurations(
                                     new TemplateException(String.format(
                                             "Only top-level and static nested classes may be annotated with @%s: %s",
                                             EngineConfiguration.class.getSimpleName(), targetClass.name()))));
-                } else if (isImplementorOf(targetClass, Names.SECTION_HELPER_FACTORY)) {
+                } else if (Types.isImplementorOf(targetClass, Names.SECTION_HELPER_FACTORY, index)) {
                     if (targetClass.hasNoArgsConstructor()) {
                         engineConfigClasses.add(targetClass);
                     } else {
@@ -2135,8 +2144,8 @@ void collectEngineConfigurations(
                                                 "A class annotated with @%s that also implements io.quarkus.qute.SectionHelperFactory must declare a no-args constructor: %s",
                                                 EngineConfiguration.class.getSimpleName(), targetClass.name()))));
                     }
-                } else if (isImplementorOf(targetClass, Names.VALUE_RESOLVER)
-                        || isImplementorOf(targetClass, Names.NAMESPACE_RESOLVER)) {
+                } else if (Types.isImplementorOf(targetClass, Names.VALUE_RESOLVER, index)
+                        || Types.isImplementorOf(targetClass, Names.NAMESPACE_RESOLVER, index)) {
                     engineConfigClasses.add(targetClass);
                 } else {
                     validationErrors.produce(
@@ -2151,10 +2160,7 @@ void collectEngineConfigurations(
                 }
             }
         }
-
-        if (!engineConfigClasses.isEmpty()) {
-            engineConfig.produce(new EngineConfigurationsBuildItem(engineConfigClasses));
-        }
+        engineConfig.produce(new EngineConfigurationsBuildItem(engineConfigClasses));
     }
 
     private void addLocationRegExToLocators(Collection<Pattern> locationToLocators,
@@ -2181,30 +2187,6 @@ private void reportFoundInvalidTarget(BuildProducer<ValidationErrorBuildItem> va
                                 locateTarget.asClass().name().toString()))));
     }
 
-    private boolean isTargetClass(AnnotationTarget locateTarget) {
-        return locateTarget.kind() == Kind.CLASS;
-    }
-
-    private boolean isNotTemplateLocatorImpl(AnnotationTarget locateTarget) {
-        boolean targetImplTemplateLocator = false;
-        for (DotName interfaceName : locateTarget.asClass().interfaceNames()) {
-            if (interfaceName.equals(Names.TEMPLATE_LOCATOR)) {
-                targetImplTemplateLocator = true;
-                break;
-            }
-        }
-        return !targetImplTemplateLocator;
-    }
-
-    private static boolean isImplementorOf(ClassInfo target, DotName interfaceName) {
-        for (DotName name : target.asClass().interfaceNames()) {
-            if (name.equals(interfaceName)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     @BuildStep
     TemplateVariantsBuildItem collectTemplateVariants(List<TemplatePathBuildItem> templatePaths) throws IOException {
         Set<String> allPaths = templatePaths.stream().map(TemplatePathBuildItem::getPath).collect(Collectors.toSet());
diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java
index 4cb99a9c95e..362d2aed769 100644
--- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java
+++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Types.java
@@ -215,4 +215,27 @@ static Type box(Primitive primitive) {
         }
     }
 
+    static boolean isImplementorOf(ClassInfo target, DotName interfaceName, IndexView index) {
+        if (target.interfaceNames().contains(interfaceName)) {
+            // Direct implementor
+            return true;
+        }
+        DotName superName = target.superName();
+        if (superName != null && !superName.equals(DotName.OBJECT_NAME)) {
+            ClassInfo superClass = index.getClassByName(superName);
+            if (superClass != null && isImplementorOf(superClass, interfaceName, index)) {
+                // Superclass is implementor
+                return true;
+            }
+        }
+        for (DotName name : target.interfaceNames()) {
+            ClassInfo interfaceClass = index.getClassByName(name);
+            if (interfaceClass != null && isImplementorOf(interfaceClass, interfaceName, index)) {
+                // Superinterface is implementor
+                return true;
+            }
+        }
+        return false;
+    }
+
 }
diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypesTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypesTest.java
index 764a7635b1b..1be95694e93 100644
--- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypesTest.java
+++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/TypesTest.java
@@ -7,6 +7,7 @@
 import java.io.InputStream;
 import java.io.Serializable;
 import java.math.BigDecimal;
+import java.util.concurrent.CompletionStage;
 
 import org.jboss.jandex.ArrayType;
 import org.jboss.jandex.ClassType;
@@ -18,6 +19,8 @@
 import org.jboss.jandex.Type;
 import org.junit.jupiter.api.Test;
 
+import io.quarkus.qute.EvalContext;
+import io.quarkus.qute.ValueResolver;
 import io.quarkus.qute.deployment.Types.AssignabilityCheck;
 
 public class TypesTest {
@@ -59,6 +62,19 @@ public void testIsAssignableFrom() throws IOException {
         assertFalse(assignabilityCheck.isAssignableFrom(byteArrayType, intArrayType));
     }
 
+    @Test
+    public void testIsImplementorOf() throws IOException {
+        Index index = index(MyResolver1.class, MyResolver2.class, MyResolver3.class, MyResolver4.class, CoolResolver.class,
+                BaseResolver.class, String.class);
+
+        assertTrue(Types.isImplementorOf(index.getClassByName(MyResolver1.class), Names.VALUE_RESOLVER, index));
+        assertTrue(Types.isImplementorOf(index.getClassByName(MyResolver2.class), Names.VALUE_RESOLVER, index));
+        assertTrue(Types.isImplementorOf(index.getClassByName(MyResolver3.class), Names.VALUE_RESOLVER, index));
+        assertTrue(Types.isImplementorOf(index.getClassByName(MyResolver4.class), Names.VALUE_RESOLVER, index));
+        assertTrue(Types.isImplementorOf(index.getClassByName(CoolResolver.class), Names.VALUE_RESOLVER, index));
+        assertFalse(Types.isImplementorOf(index.getClassByName(String.class), Names.VALUE_RESOLVER, index));
+    }
+
     private static Index index(Class<?>... classes) throws IOException {
         Indexer indexer = new Indexer();
         for (Class<?> clazz : classes) {
@@ -70,4 +86,42 @@ private static Index index(Class<?>... classes) throws IOException {
         return indexer.complete();
     }
 
+    public static class MyResolver1 implements ValueResolver {
+
+        @Override
+        public CompletionStage<Object> resolve(EvalContext context) {
+            return null;
+        }
+
+    }
+
+    public static class MyResolver2 extends BaseResolver {
+
+    }
+
+    public static class BaseResolver implements ValueResolver {
+
+        @Override
+        public CompletionStage<Object> resolve(EvalContext context) {
+            return null;
+        }
+    }
+
+    public interface CoolResolver extends ValueResolver {
+
+    }
+
+    public static class MyResolver3 implements CoolResolver {
+
+        @Override
+        public CompletionStage<Object> resolve(EvalContext context) {
+            return null;
+        }
+
+    }
+
+    public static class MyResolver4 extends MyResolver3 {
+
+    }
+
 }
diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/resolver/CustomResolversTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/resolver/CustomResolversTest.java
index d6bf336e3d3..25090d0aec4 100644
--- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/resolver/CustomResolversTest.java
+++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/engineconfigurations/resolver/CustomResolversTest.java
@@ -22,8 +22,9 @@ public class CustomResolversTest {
 
     @RegisterExtension
     static final QuarkusUnitTest config = new QuarkusUnitTest()
-            .withApplicationRoot(root -> root.addClasses(CustomValueResolver.class, CustomNamespaceResolver.class)
-                    .addAsResource(new StringAsset("{bool.foo}::{custom:bar}"), "templates/foo.html"));
+            .withApplicationRoot(
+                    root -> root.addClasses(CustomValueResolver.class, CustomNamespaceResolver.class, ResolverBase.class)
+                            .addAsResource(new StringAsset("{bool.foo}::{custom:bar}"), "templates/foo.html"));
 
     @Inject
     Template foo;
@@ -34,7 +35,7 @@ public void testResolvers() {
     }
 
     @EngineConfiguration
-    static class CustomValueResolver implements ValueResolver {
+    static class CustomValueResolver extends ResolverBase {
 
         @Override
         public boolean appliesTo(EvalContext context) {
@@ -48,6 +49,10 @@ public CompletionStage<Object> resolve(EvalContext context) {
 
     }
 
+    static abstract class ResolverBase implements ValueResolver {
+
+    }
+
     @EngineConfiguration
     static class CustomNamespaceResolver implements NamespaceResolver {