diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index e96bbbe6df310b..f75a326bb6ec53 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -12,6 +12,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -49,6 +50,7 @@ import io.quarkus.arc.processor.BeanRegistrar.RegistrationContext; import io.quarkus.arc.processor.BuildExtension.BuildContext; import io.quarkus.arc.processor.BuildExtension.Key; +import io.quarkus.arc.processor.Types.TypeClosure; import io.quarkus.arc.processor.bcextensions.ExtensionsEntryPoint; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.ResultHandle; @@ -79,6 +81,7 @@ public class BeanDeployment { private final List beans; private volatile Map> beansByType; + private final List skippedClasses; private final List interceptors; private final List decorators; @@ -218,6 +221,7 @@ public class BeanDeployment { this.interceptors = new CopyOnWriteArrayList<>(); this.decorators = new CopyOnWriteArrayList<>(); this.beans = new CopyOnWriteArrayList<>(); + this.skippedClasses = new CopyOnWriteArrayList<>(); this.observers = new CopyOnWriteArrayList<>(); this.assignabilityCheck = new AssignabilityCheck(getBeanArchiveIndex(), applicationIndex); @@ -271,9 +275,11 @@ void registerScopes() { BeanRegistrar.RegistrationContext registerBeans(List beanRegistrars) { List injectionPoints = new ArrayList<>(); - this.beans.addAll( - findBeans(initBeanDefiningAnnotations(beanDefiningAnnotations.values(), stereotypes.keySet()), observers, - injectionPoints, jtaCapabilities)); + BeanDiscoveryResult beanDiscoveryResult = findBeans( + initBeanDefiningAnnotations(beanDefiningAnnotations.values(), stereotypes.keySet()), observers, + injectionPoints, jtaCapabilities); + this.beans.addAll(beanDiscoveryResult.beans); + this.skippedClasses.addAll(beanDiscoveryResult.skippedClasses); // Note that we use unmodifiable views because the underlying collections may change in the next phase // E.g. synthetic beans are added and unused interceptors removed buildContext.putInternal(Key.BEANS, Collections.unmodifiableList(beans)); @@ -941,7 +947,23 @@ static ScopeInfo getValidScope(Set scopes, AnnotationTarget target) { } } - private List findBeans(Collection beanDefiningAnnotations, List observers, + record BeanDiscoveryResult(List beans, List skippedClasses) { + } + + /** + * A class found during discovery but skipped for a specific reason. + */ + record SkippedClass(ClassInfo clazz, SkippedReason reason) { + } + + enum SkippedReason { + EXCLUDED_TYPE, + VETOED, + NO_BEAN_DEF_ANNOTATION, + NO_BEAN_CONSTRUCTOR + } + + private BeanDiscoveryResult findBeans(Collection beanDefiningAnnotations, List observers, List injectionPoints, boolean jtaCapabilities) { Set beanClasses = new HashSet<>(); @@ -956,6 +978,9 @@ private List findBeans(Collection beanDefiningAnnotations, Li .map(StereotypeInfo::getName) .collect(Collectors.toSet()); + List skipped = new ArrayList<>(); + List noBeanDefiningAnnotationClasses = new ArrayList<>(); + Set seenClasses = new HashSet<>(); // If needed use the specialized immutable index to discover beans @@ -978,6 +1003,7 @@ private List findBeans(Collection beanDefiningAnnotations, Li } if (isExcluded(beanClass)) { + skipped.add(new SkippedClass(beanClass, SkippedReason.EXCLUDED_TYPE)); continue; } @@ -997,18 +1023,21 @@ private List findBeans(Collection beanDefiningAnnotations, Li // in strict compatibility mode, the bean needs to have either no args ctor or some with @Inject // note that we perform validation (for multiple ctors for instance) later in the cycle if (strictCompatibility && numberOfConstructorsWithInject == 0) { + skipped.add(new SkippedClass(beanClass, SkippedReason.NO_BEAN_CONSTRUCTOR)); continue; } // without strict compatibility, a bean without no-arg constructor needs to have either a constructor // annotated with @Inject or a single constructor if (numberOfConstructorsWithInject == 0 && numberOfConstructorsWithoutInject != 1) { + skipped.add(new SkippedClass(beanClass, SkippedReason.NO_BEAN_CONSTRUCTOR)); continue; } } if (isVetoed(beanClass)) { // Skip vetoed bean classes + skipped.add(new SkippedClass(beanClass, SkippedReason.VETOED)); continue; } @@ -1032,9 +1061,12 @@ private List findBeans(Collection beanDefiningAnnotations, Li if (annotationStore.hasAnyAnnotation(beanClass, beanDefiningAnnotations)) { hasBeanDefiningAnnotation = true; beanClasses.add(beanClass); + } else { + noBeanDefiningAnnotationClasses.add(beanClass); } - // non-inherited methods + // non-inherited methods: producers and disposers + // Impl.note: we cannot optimize with beanClass.hasAnnotation() as the annotations can be added with a transformer for (MethodInfo method : beanClass.methods()) { if (method.isSynthetic()) { continue; @@ -1216,10 +1248,10 @@ private List findBeans(Collection beanDefiningAnnotations, Li for (MethodInfo producerMethod : producerMethods) { BeanInfo declaringBean = beanClassToBean.get(producerMethod.declaringClass()); if (declaringBean != null) { - Set beanTypes = Types.getProducerMethodTypeClosure(producerMethod, this); - DisposerInfo disposer = findDisposer(beanTypes, declaringBean, producerMethod, disposers); + TypeClosure typeClosure = Types.getProducerMethodTypeClosure(producerMethod, this); + DisposerInfo disposer = findDisposer(typeClosure.types(), declaringBean, producerMethod, disposers); unusedDisposers.remove(disposer); - BeanInfo producerMethodBean = Beans.createProducerMethod(beanTypes, producerMethod, declaringBean, this, + BeanInfo producerMethodBean = Beans.createProducerMethod(typeClosure, producerMethod, declaringBean, this, disposer, injectionPointTransformer); if (producerMethodBean != null) { beans.add(producerMethodBean); @@ -1231,8 +1263,8 @@ private List findBeans(Collection beanDefiningAnnotations, Li for (FieldInfo producerField : producerFields) { BeanInfo declaringBean = beanClassToBean.get(producerField.declaringClass()); if (declaringBean != null) { - Set beanTypes = Types.getProducerFieldTypeClosure(producerField, this); - DisposerInfo disposer = findDisposer(beanTypes, declaringBean, producerField, disposers); + TypeClosure typeClosure = Types.getProducerFieldTypeClosure(producerField, this); + DisposerInfo disposer = findDisposer(typeClosure.types(), declaringBean, producerField, disposers); unusedDisposers.remove(disposer); BeanInfo producerFieldBean = Beans.createProducerField(producerField, declaringBean, this, disposer); @@ -1281,7 +1313,18 @@ private List findBeans(Collection beanDefiningAnnotations, Li LOGGER.logf(Level.TRACE, "Created %s", bean); } } - return beans; + + for (Iterator it = noBeanDefiningAnnotationClasses.iterator(); it.hasNext();) { + ClassInfo clazz = it.next(); + if (beanClasses.contains(clazz)) { + // Bean class is not annotated with a bean defining annotation but declares observer, producer, disposer.. + continue; + } + skipped.add(new SkippedClass(clazz, SkippedReason.NO_BEAN_DEF_ANNOTATION)); + } + + LOGGER.debugf("Found %s beans, skipped %s classes", beans.size(), skipped.size()); + return new BeanDiscoveryResult(beans, skipped); } private boolean isVetoed(ClassInfo beanClass) { @@ -1670,6 +1713,26 @@ public Set getQualifierNonbindingMembers(DotName name) { return qualifierNonbindingMembers.getOrDefault(name, Collections.emptySet()); } + /** + * Returns the set of skipped classes that match the given required type. + * + * @param type + * @return set of skipped classes + */ + List findSkippedClassesMatching(Type type) { + List skipped = new ArrayList<>(); + for (SkippedClass skippedClass : skippedClasses) { + Set types = Types.getClassUnrestrictedTypeClosure(skippedClass.clazz, this); + for (Type beanType : types) { + if (beanResolver.matches(type, beanType)) { + skipped.add(skippedClass); + break; + } + } + } + return skipped; + } + @Override public String toString() { return "BeanDeployment [name=" + name + "]"; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java index 705d47b85e9b37..14aea5e82c65b3 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java @@ -19,6 +19,7 @@ import java.util.function.Consumer; import java.util.stream.Collectors; +import jakarta.enterprise.inject.Typed; import jakarta.enterprise.inject.spi.DefinitionException; import jakarta.enterprise.inject.spi.DeploymentException; import jakarta.enterprise.inject.spi.InterceptionType; @@ -55,6 +56,7 @@ public class BeanInfo implements InjectionTargetInfo { protected final ScopeInfo scope; protected final Set types; + protected final Set unrestrictedTypes; protected final Set qualifiers; @@ -97,10 +99,10 @@ public class BeanInfo implements InjectionTargetInfo { BeanInfo(AnnotationTarget target, BeanDeployment beanDeployment, ScopeInfo scope, Set types, Set qualifiers, List injections, BeanInfo declaringBean, DisposerInfo disposer, boolean alternative, List stereotypes, String name, boolean isDefaultBean, String targetPackageName, - Integer priority) { + Integer priority, Set unrestrictedTypes) { this(null, null, target, beanDeployment, scope, types, qualifiers, injections, declaringBean, disposer, alternative, stereotypes, name, isDefaultBean, null, null, Collections.emptyMap(), true, false, - targetPackageName, priority, null); + targetPackageName, priority, null, unrestrictedTypes); } BeanInfo(ClassInfo implClazz, Type providerType, AnnotationTarget target, BeanDeployment beanDeployment, ScopeInfo scope, @@ -108,7 +110,8 @@ public class BeanInfo implements InjectionTargetInfo { DisposerInfo disposer, boolean alternative, List stereotypes, String name, boolean isDefaultBean, Consumer creatorConsumer, Consumer destroyerConsumer, Map params, boolean isRemovable, - boolean forceApplicationClass, String targetPackageName, Integer priority, String identifier) { + boolean forceApplicationClass, String targetPackageName, Integer priority, String identifier, + Set unrestrictedTypes) { this.target = Optional.ofNullable(target); if (implClazz == null && target != null) { @@ -126,6 +129,7 @@ public class BeanInfo implements InjectionTargetInfo { for (Type type : types) { Beans.analyzeType(type, beanDeployment); } + this.unrestrictedTypes = unrestrictedTypes != null ? unrestrictedTypes : types; Beans.addImplicitQualifiers(qualifiers); this.qualifiers = qualifiers; this.injections = injections; @@ -249,6 +253,15 @@ public Set getTypes() { return types; } + /** + * + * @return the unrestricted set of bean types + * @see Typed + */ + public Set getUnrestrictedTypes() { + return unrestrictedTypes; + } + public boolean hasType(DotName typeName) { for (Type type : types) { if (type.name().equals(typeName)) { @@ -1173,7 +1186,7 @@ Builder targetPackageName(String name) { BeanInfo build() { return new BeanInfo(implClazz, providerType, target, beanDeployment, scope, types, qualifiers, injections, declaringBean, disposer, alternative, stereotypes, name, isDefaultBean, creatorConsumer, - destroyerConsumer, params, removable, forceApplicationClass, targetPackageName, priority, identifier); + destroyerConsumer, params, removable, forceApplicationClass, targetPackageName, priority, identifier, null); } public Builder forceApplicationClass(boolean forceApplicationClass) { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanResolverImpl.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanResolverImpl.java index 2a446d9bf252f3..fb4fb175d05bb7 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanResolverImpl.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanResolverImpl.java @@ -131,6 +131,22 @@ List findTypeMatching(Type type) { return resolved.isEmpty() ? Collections.emptyList() : resolved; } + List findUnrestrictedTypeMatching(TypeAndQualifiers typeAndQualifiers) { + List resolved = new ArrayList<>(); + for (BeanInfo b : beanDeployment.getBeans()) { + if (!Beans.hasQualifiers(b, typeAndQualifiers.qualifiers)) { + continue; + } + for (Type type : b.getUnrestrictedTypes()) { + if (matches(typeAndQualifiers.type, type)) { + resolved.add(b); + break; + } + } + } + return resolved.isEmpty() ? Collections.emptyList() : resolved; + } + Collection potentialBeans(Type type) { if ((type.kind() == CLASS || type.kind() == PARAMETERIZED_TYPE) && !type.name().equals(DotNames.OBJECT)) { return beanDeployment.getBeansByRawType(type.name()); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java index 3c363818a805ba..fb439049f29b2a 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java @@ -36,7 +36,9 @@ import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Opcodes; +import io.quarkus.arc.processor.BeanDeployment.SkippedClass; import io.quarkus.arc.processor.InjectionPointInfo.TypeAndQualifiers; +import io.quarkus.arc.processor.Types.TypeClosure; import io.quarkus.gizmo.ClassTransformer; import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.MethodCreator; @@ -78,7 +80,7 @@ private static ScopeInfo inheritScope(ClassInfo beanClass, BeanDeployment beanDe return null; } - static BeanInfo createProducerMethod(Set beanTypes, MethodInfo producerMethod, BeanInfo declaringBean, + static BeanInfo createProducerMethod(TypeClosure typeClosure, MethodInfo producerMethod, BeanInfo declaringBean, BeanDeployment beanDeployment, DisposerInfo disposer, InjectionPointModifier transformer) { Set qualifiers = new HashSet<>(); @@ -190,8 +192,9 @@ static BeanInfo createProducerMethod(Set beanTypes, MethodInfo producerMet List injections = Injection.forBean(producerMethod, declaringBean, beanDeployment, transformer, Injection.BeanType.PRODUCER_METHOD); - BeanInfo bean = new BeanInfo(producerMethod, beanDeployment, scope, beanTypes, qualifiers, injections, declaringBean, - disposer, isAlternative, stereotypes, name, isDefaultBean, null, priority); + BeanInfo bean = new BeanInfo(producerMethod, beanDeployment, scope, typeClosure.types(), qualifiers, injections, + declaringBean, + disposer, isAlternative, stereotypes, name, isDefaultBean, null, priority, typeClosure.unrestrictedTypes()); for (Injection injection : injections) { injection.init(bean); } @@ -202,7 +205,7 @@ static BeanInfo createProducerField(FieldInfo producerField, BeanInfo declaringB DisposerInfo disposer) { Set qualifiers = new HashSet<>(); List scopes = new ArrayList<>(); - Set types = Types.getProducerFieldTypeClosure(producerField, beanDeployment); + TypeClosure typeClosure = Types.getProducerFieldTypeClosure(producerField, beanDeployment); Integer priority = null; boolean isAlternative = false; boolean isDefaultBean = false; @@ -302,8 +305,10 @@ static BeanInfo createProducerField(FieldInfo producerField, BeanInfo declaringB + "its scope must be @Dependent: " + producerField); } - BeanInfo bean = new BeanInfo(producerField, beanDeployment, scope, types, qualifiers, Collections.emptyList(), - declaringBean, disposer, isAlternative, stereotypes, name, isDefaultBean, null, priority); + BeanInfo bean = new BeanInfo(producerField, beanDeployment, scope, typeClosure.types(), qualifiers, + Collections.emptyList(), + declaringBean, disposer, isAlternative, stereotypes, name, isDefaultBean, null, priority, + typeClosure.unrestrictedTypes()); return bean; } @@ -460,21 +465,56 @@ static void resolveInjectionPoint(BeanDeployment deployment, InjectionTargetInfo List resolved = deployment.beanResolver.resolve(injectionPoint.getTypeAndQualifiers()); BeanInfo selected = null; if (resolved.isEmpty()) { - List typeMatching = deployment.beanResolver.findTypeMatching(injectionPoint.getRequiredType()); StringBuilder message = new StringBuilder("Unsatisfied dependency for type "); addStandardErroneousDependencyMessage(target, injectionPoint, message); + + List typeMatching = deployment.beanResolver.findTypeMatching(injectionPoint.getRequiredType()); if (!typeMatching.isEmpty()) { - message.append("\n\tThe following beans match by type, but none have matching qualifiers:"); - for (BeanInfo beanInfo : typeMatching) { - message.append("\n\t\t- "); - message.append("Bean [class="); - message.append(beanInfo.getImplClazz()); - message.append(", qualifiers="); - message.append(beanInfo.getQualifiers()); - message.append("]"); + message.append("\n\tThe following beans match by type, but none has matching qualifiers:"); + for (BeanInfo bean : typeMatching) { + message.append("\n\t- "); + message.append(bean); } + message.append("\n\n"); } + + List unrestrictedTypeMatching = deployment.beanResolver + .findUnrestrictedTypeMatching(injectionPoint.getTypeAndQualifiers()); + if (!unrestrictedTypeMatching.isEmpty()) { + message.append("\n\tThe following beans match by type excluded by the @Typed annotation:"); + for (BeanInfo bean : unrestrictedTypeMatching) { + message.append("\n\t- "); + message.append(bean); + } + message.append("\n\n"); + } + + List skippedClasses = deployment.findSkippedClassesMatching(injectionPoint.getRequiredType()); + if (!skippedClasses.isEmpty()) { + message.append("\n\tThe following classes match by type, but have been skipped during discovery:"); + for (SkippedClass skippedClass : skippedClasses) { + message.append("\n\t- "); + message.append(skippedClass.clazz().name()); + switch (skippedClass.reason()) { + case EXCLUDED_TYPE: + message.append(" was excluded in config"); + break; + case VETOED: + message.append(" was annotated with @Vetoed"); + break; + case NO_BEAN_DEF_ANNOTATION: + message.append(" has no bean defining annotation (scope, stereotype, etc.)"); + break; + case NO_BEAN_CONSTRUCTOR: + message.append(" does not declare a valid bean constructor"); + default: + break; + } + } + message.append("\n\n"); + } + errors.add(new UnsatisfiedResolutionException(message.toString())); } else if (resolved.size() > 1) { // Try to resolve the ambiguity @@ -502,7 +542,7 @@ private static void addStandardErroneousDependencyMessage(InjectionTargetInfo ta if (injectionPoint.isSynthetic()) { message.append("\n\t- synthetic injection point"); } else { - message.append("\n\t- java member: "); + message.append("\n\t- injection target: "); message.append(injectionPoint.getTargetInfo()); } message.append("\n\t- declared on "); @@ -1250,7 +1290,7 @@ void processAnnotation(AnnotationInstance annotation, BeanInfo create() { Set qualifiers = new HashSet<>(); List scopes = new ArrayList<>(); - Set types = Types.getClassBeanTypeClosure(beanClass, beanDeployment); + TypeClosure typeClosure = Types.getClassBeanTypeClosure(beanClass, beanDeployment); List stereotypes = new ArrayList<>(); Collection annotations = beanDeployment.getAnnotations(beanClass); @@ -1307,8 +1347,9 @@ BeanInfo create() { List injections = Injection.forBean(beanClass, null, beanDeployment, transformer, Injection.BeanType.MANAGED_BEAN); - BeanInfo bean = new BeanInfo(beanClass, beanDeployment, scope, types, qualifiers, - injections, null, null, isAlternative, stereotypes, name, isDefaultBean, null, priority); + BeanInfo bean = new BeanInfo(beanClass, beanDeployment, scope, typeClosure.types(), qualifiers, + injections, null, null, isAlternative, stereotypes, name, isDefaultBean, null, priority, + typeClosure.unrestrictedTypes()); for (Injection injection : injections) { injection.init(bean); } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorInfo.java index df3dce0e2919d9..f9e08bbb44bda6 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/DecoratorInfo.java @@ -23,7 +23,7 @@ public class DecoratorInfo extends BeanInfo implements Comparable Set decoratedTypes, List injections, int priority) { super(target, beanDeployment, BuiltinScope.DEPENDENT.getInfo(), Sets.singletonHashSet(Type.create(target.asClass().name(), Kind.CLASS)), new HashSet<>(), injections, - null, null, false, Collections.emptyList(), null, false, null, priority); + null, null, false, Collections.emptyList(), null, false, null, priority, null); this.delegateInjectionPoint = delegateInjectionPoint; this.decoratedTypes = decoratedTypes; } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Decorators.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Decorators.java index bd514684b43b8e..5571482a0c8632 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Decorators.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Decorators.java @@ -19,6 +19,8 @@ import org.jboss.jandex.Type; import org.jboss.logging.Logger; +import io.quarkus.arc.processor.Types.TypeClosure; + final class Decorators { static final Logger LOGGER = Logger.getLogger(Decorators.class); @@ -69,7 +71,8 @@ static DecoratorInfo createDecorator(ClassInfo decoratorClass, BeanDeployment be } // The set includes all bean types which are Java interfaces, except for java.io.Serializable - Set types = Types.getClassBeanTypeClosure(decoratorClass, beanDeployment); + TypeClosure typeClosure = Types.getClassBeanTypeClosure(decoratorClass, beanDeployment); + Set types = typeClosure.types(); Set decoratedTypes = new HashSet<>(); for (Type type : types) { if (type.name().equals(DotNames.SERIALIZABLE)) { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java index 828ecdbc573d3f..f2ef90f7c26c09 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java @@ -64,7 +64,8 @@ public class InterceptorInfo extends BeanInfo implements Comparable injections, int priority) { super(target, beanDeployment, BuiltinScope.DEPENDENT.getInfo(), Sets.singletonHashSet(Type.create(target.asClass().name(), Kind.CLASS)), new HashSet<>(), injections, - null, null, false, Collections.emptyList(), null, false, null, priority); + null, null, false, Collections.emptyList(), null, false, null, priority, null); this.bindings = bindings; this.interceptionType = null; this.creatorClass = null; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Types.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Types.java index 1673865997502b..c1ce6e80c604b7 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Types.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Types.java @@ -406,7 +406,7 @@ static Type getArrayElementType(ArrayType array) { return elementType; } - static Set getProducerMethodTypeClosure(MethodInfo producerMethod, BeanDeployment beanDeployment) { + static TypeClosure getProducerMethodTypeClosure(MethodInfo producerMethod, BeanDeployment beanDeployment) { Set types; Set unrestrictedBeanTypes = new HashSet<>(); Type returnType = producerMethod.returnType(); @@ -420,7 +420,7 @@ static Set getProducerMethodTypeClosure(MethodInfo producerMethod, BeanDep types = new HashSet<>(); types.add(returnType); types.add(OBJECT_TYPE); - return types; + return new TypeClosure(types); } else { ClassInfo returnTypeClassInfo = getClassByName(beanDeployment.getBeanArchiveIndex(), returnType); if (returnTypeClassInfo == null) { @@ -439,12 +439,11 @@ static Set getProducerMethodTypeClosure(MethodInfo producerMethod, BeanDep throw new IllegalArgumentException("Unsupported return type"); } } - return restrictBeanTypes(types, unrestrictedBeanTypes, beanDeployment.getAnnotations(producerMethod), - beanDeployment.getBeanArchiveIndex(), - producerMethod); + return new TypeClosure(restrictBeanTypes(types, unrestrictedBeanTypes, beanDeployment.getAnnotations(producerMethod), + beanDeployment.getBeanArchiveIndex(), producerMethod), unrestrictedBeanTypes); } - static Set getProducerFieldTypeClosure(FieldInfo producerField, BeanDeployment beanDeployment) { + static TypeClosure getProducerFieldTypeClosure(FieldInfo producerField, BeanDeployment beanDeployment) { Set types; Set unrestrictedBeanTypes = new HashSet<>(); Type fieldType = producerField.type(); @@ -458,6 +457,7 @@ static Set getProducerFieldTypeClosure(FieldInfo producerField, BeanDeploy types = new HashSet<>(); types.add(fieldType); types.add(OBJECT_TYPE); + return new TypeClosure(types); } else { ClassInfo fieldClassInfo = getClassByName(beanDeployment.getBeanArchiveIndex(), producerField.type()); if (fieldClassInfo == null) { @@ -475,12 +475,11 @@ static Set getProducerFieldTypeClosure(FieldInfo producerField, BeanDeploy throw new IllegalArgumentException("Unsupported return type"); } } - return restrictBeanTypes(types, unrestrictedBeanTypes, beanDeployment.getAnnotations(producerField), - beanDeployment.getBeanArchiveIndex(), - producerField); + return new TypeClosure(restrictBeanTypes(types, unrestrictedBeanTypes, beanDeployment.getAnnotations(producerField), + beanDeployment.getBeanArchiveIndex(), producerField), unrestrictedBeanTypes); } - static Set getClassBeanTypeClosure(ClassInfo classInfo, BeanDeployment beanDeployment) { + static TypeClosure getClassBeanTypeClosure(ClassInfo classInfo, BeanDeployment beanDeployment) { Set types; Set unrestrictedBeanTypes = new HashSet<>(); List typeParameters = classInfo.typeParameters(); @@ -490,9 +489,28 @@ static Set getClassBeanTypeClosure(ClassInfo classInfo, BeanDeployment bea types = getTypeClosure(classInfo, null, buildResolvedMap(typeParameters, typeParameters, Collections.emptyMap(), beanDeployment.getBeanArchiveIndex()), beanDeployment, null, unrestrictedBeanTypes); } - return restrictBeanTypes(types, unrestrictedBeanTypes, beanDeployment.getAnnotations(classInfo), - beanDeployment.getBeanArchiveIndex(), - classInfo); + return new TypeClosure(restrictBeanTypes(types, unrestrictedBeanTypes, beanDeployment.getAnnotations(classInfo), + beanDeployment.getBeanArchiveIndex(), classInfo), unrestrictedBeanTypes); + } + + record TypeClosure(Set types, Set unrestrictedTypes) { + + TypeClosure(Set types) { + this(types, types); + } + } + + static Set getClassUnrestrictedTypeClosure(ClassInfo classInfo, BeanDeployment beanDeployment) { + Set types; + Set unrestrictedBeanTypes = new HashSet<>(); + List typeParameters = classInfo.typeParameters(); + if (typeParameters.isEmpty()) { + types = getTypeClosure(classInfo, null, Collections.emptyMap(), beanDeployment, null, unrestrictedBeanTypes); + } else { + types = getTypeClosure(classInfo, null, buildResolvedMap(typeParameters, typeParameters, + Collections.emptyMap(), beanDeployment.getBeanArchiveIndex()), beanDeployment, null, unrestrictedBeanTypes); + } + return types; } static Map resolveDecoratedTypeParams(ClassInfo decoratedTypeClass, DecoratorInfo decorator) { diff --git a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/TypesTest.java b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/TypesTest.java index da29c9dea820c1..4b3ee2f3e8baff 100644 --- a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/TypesTest.java +++ b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/TypesTest.java @@ -60,7 +60,7 @@ public void testGetTypeClosure() throws IOException { resolvedTypeVariables.clear(); // Foo, Object Set fooTypes = Types.getClassBeanTypeClosure(fooClass, - dummyDeployment); + dummyDeployment).types(); assertEquals(2, fooTypes.size()); for (Type t : fooTypes) { if (t.kind().equals(Kind.PARAMETERIZED_TYPE)) { @@ -83,18 +83,20 @@ public void testGetTypeClosure() throws IOException { final String wildcardInTypeHierarchy = "eagleProducer"; assertDoesNotThrow( () -> verifyEagleTypes( - Types.getProducerMethodTypeClosure(producerClass.method(wildcardInTypeHierarchy), dummyDeployment))); + Types.getProducerMethodTypeClosure(producerClass.method(wildcardInTypeHierarchy), dummyDeployment) + .types())); assertDoesNotThrow( () -> verifyEagleTypes( - Types.getProducerFieldTypeClosure(producerClass.field(wildcardInTypeHierarchy), dummyDeployment))); + Types.getProducerFieldTypeClosure(producerClass.field(wildcardInTypeHierarchy), dummyDeployment) + .types())); // now do the same for a non-producer scenario with Eagle class assertDoesNotThrow( () -> verifyEagleTypes(Types.getClassBeanTypeClosure(index.getClassByName(DotName.createSimple(Eagle.class)), - dummyDeployment))); + dummyDeployment).types())); // raw type bean Set rawBeanTypes = Types.getClassBeanTypeClosure(index.getClassByName(DotName.createSimple(MyRawBean.class)), - dummyDeployment); + dummyDeployment).types(); assertEquals(rawBeanTypes.size(), 5); assertContainsType(MyRawBean.class, rawBeanTypes); assertContainsType(MyBean.class, rawBeanTypes); diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java index 5a06041446b7a9..9aa839dd4eead3 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java @@ -19,6 +19,7 @@ import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.CompositeIndex; import org.jboss.jandex.DotName; import org.jboss.jandex.Index; @@ -88,11 +89,12 @@ public static class Builder { private final List beanDeploymentValidators; private boolean shouldFail = false; private boolean removeUnusedBeans = false; - private final List> exclusions; + private final List> removalExclusions; private AlternativePriorities alternativePriorities; private final List buildCompatibleExtensions; private boolean strictCompatibility = false; private boolean optimizeContexts = false; + private final List> excludeTypes; public Builder() { resourceReferenceProviders = new ArrayList<>(); @@ -109,8 +111,9 @@ public Builder() { injectionsPointsTransformers = new ArrayList<>(); observerTransformers = new ArrayList<>(); beanDeploymentValidators = new ArrayList<>(); - exclusions = new ArrayList<>(); + removalExclusions = new ArrayList<>(); buildCompatibleExtensions = new ArrayList<>(); + excludeTypes = new ArrayList<>(); } public Builder resourceReferenceProviders(Class... resourceReferenceProviders) { @@ -190,7 +193,7 @@ public Builder removeUnusedBeans(boolean value) { } public Builder addRemovalExclusion(Predicate exclusion) { - this.exclusions.add(exclusion); + this.removalExclusions.add(exclusion); return this; } @@ -219,6 +222,11 @@ public Builder optimizeContexts(boolean value) { return this; } + public Builder excludeType(Predicate predicate) { + this.excludeTypes.add(predicate); + return this; + } + public ArcTestContainer build() { return new ArcTestContainer(this); } @@ -229,6 +237,7 @@ public ArcTestContainer build() { private final List> beanClasses; private final List> additionalClasses; + private final List> excludeTypes; private final List> resourceAnnotations; @@ -247,7 +256,7 @@ public ArcTestContainer build() { private final AtomicReference buildFailure; private final boolean removeUnusedBeans; - private final List> exclusions; + private final List> removalExclusions; private final AlternativePriorities alternativePriorities; @@ -274,11 +283,12 @@ public ArcTestContainer(Class... beanClasses) { this.buildFailure = new AtomicReference(null); this.shouldFail = false; this.removeUnusedBeans = false; - this.exclusions = Collections.emptyList(); + this.removalExclusions = Collections.emptyList(); this.alternativePriorities = null; this.buildCompatibleExtensions = Collections.emptyList(); this.strictCompatibility = false; this.optimizeContexts = false; + this.excludeTypes = Collections.emptyList(); } public ArcTestContainer(Builder builder) { @@ -299,11 +309,12 @@ public ArcTestContainer(Builder builder) { this.buildFailure = new AtomicReference(null); this.shouldFail = builder.shouldFail; this.removeUnusedBeans = builder.removeUnusedBeans; - this.exclusions = builder.exclusions; + this.removalExclusions = builder.removalExclusions; this.alternativePriorities = builder.alternativePriorities; this.buildCompatibleExtensions = builder.buildCompatibleExtensions; this.strictCompatibility = builder.strictCompatibility; this.optimizeContexts = builder.optimizeContexts; + this.excludeTypes = builder.excludeTypes; } // this is where we start Arc, we operate on a per-method basis @@ -437,6 +448,7 @@ private ClassLoader init(ExtensionContext context) { injectionPointsTransformers.forEach(builder::addInjectionPointTransformer); observerTransformers.forEach(builder::addObserverTransformer); beanDeploymentValidators.forEach(builder::addBeanDeploymentValidator); + excludeTypes.forEach(builder::addExcludeType); builder.setOutput(new ResourceOutput() { @Override @@ -461,7 +473,7 @@ public void writeResource(Resource resource) throws IOException { } }); builder.setRemoveUnusedBeans(removeUnusedBeans); - for (Predicate exclusion : exclusions) { + for (Predicate exclusion : removalExclusions) { builder.addRemovalExclusion(exclusion); } builder.setAlternativePriorities(alternativePriorities); @@ -495,6 +507,10 @@ public void writeResource(Resource resource) throws IOException { throw new RuntimeException(e); } } + } finally { + if (shouldFail && buildFailure.get() == null) { + throw new AssertionError("The container was expected to fail!"); + } } return old; } diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/finalmethod/FinalMethodIllegalWhenNotInjectedTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/finalmethod/FinalMethodIllegalWhenNotInjectedTest.java index d82d2a136e4374..944e81e291af4b 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/finalmethod/FinalMethodIllegalWhenNotInjectedTest.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/clientproxy/finalmethod/FinalMethodIllegalWhenNotInjectedTest.java @@ -12,11 +12,11 @@ import io.quarkus.arc.test.ArcTestContainer; public class FinalMethodIllegalWhenNotInjectedTest { + @RegisterExtension public ArcTestContainer container = ArcTestContainer.builder() .beanClasses(Moo.class) .strictCompatibility(true) - .shouldFail() .build(); @Test diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchByRestrictedTypeTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchByRestrictedTypeTest.java new file mode 100644 index 00000000000000..6bfc7399becb67 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchByRestrictedTypeTest.java @@ -0,0 +1,51 @@ +package io.quarkus.arc.test.injection.unsatisfied; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.enterprise.inject.Typed; +import jakarta.enterprise.inject.UnsatisfiedResolutionException; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.test.ArcTestContainer; + +public class UnsatisfiedMatchByRestrictedTypeTest { + + @RegisterExtension + ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(FooService.class, Consumer.class) + .shouldFail() + .build(); + + @Test + public void testExceptionThrown() { + Throwable error = container.getFailure(); + assertThat(error).rootCause().isInstanceOf(UnsatisfiedResolutionException.class) + .hasMessageContaining("The following beans match by type excluded by the @Typed annotation") + .hasMessageContaining( + "io.quarkus.arc.test.injection.unsatisfied.UnsatisfiedMatchByRestrictedTypeTest$FooService"); + } + + @Singleton + static class Consumer { + + @Inject + FooService foo; + + } + + @Typed(Comparable.class) + @Singleton + static class FooService implements Comparable { + + @Override + public int compareTo(String o) { + return 0; + } + + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchByTypeTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchByTypeTest.java new file mode 100644 index 00000000000000..938de304979c7f --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchByTypeTest.java @@ -0,0 +1,46 @@ +package io.quarkus.arc.test.injection.unsatisfied; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.enterprise.inject.UnsatisfiedResolutionException; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.test.ArcTestContainer; +import io.quarkus.arc.test.MyQualifier; + +public class UnsatisfiedMatchByTypeTest { + + @RegisterExtension + ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(FooService.class, Consumer.class, MyQualifier.class) + .shouldFail() + .build(); + + @Test + public void testExceptionThrown() { + Throwable error = container.getFailure(); + assertThat(error).rootCause().isInstanceOf(UnsatisfiedResolutionException.class) + .hasMessageContaining("The following beans match by type, but none has matching qualifiers") + .hasMessageContaining("io.quarkus.arc.test.injection.unsatisfied.UnsatisfiedMatchByTypeTest$FooService"); + + } + + @Singleton + static class Consumer { + + @Inject + @MyQualifier + FooService foo; + + } + + @Singleton + static class FooService { + + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchExcludedTypeTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchExcludedTypeTest.java new file mode 100644 index 00000000000000..5ad60d48c0977d --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchExcludedTypeTest.java @@ -0,0 +1,47 @@ +package io.quarkus.arc.test.injection.unsatisfied; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.enterprise.inject.UnsatisfiedResolutionException; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.jboss.jandex.DotName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.test.ArcTestContainer; + +public class UnsatisfiedMatchExcludedTypeTest { + + @RegisterExtension + ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(FooService.class, Consumer.class) + .excludeType(c -> c.name().equals(DotName.createSimple(FooService.class))) + .shouldFail() + .build(); + + @Test + public void testExceptionThrown() { + Throwable error = container.getFailure(); + assertThat(error).rootCause().isInstanceOf(UnsatisfiedResolutionException.class) + .hasMessageContaining("The following classes match by type, but have been skipped during discovery") + .hasMessageContaining( + "io.quarkus.arc.test.injection.unsatisfied.UnsatisfiedMatchExcludedTypeTest$FooService was excluded in config"); + + } + + @Singleton + static class Consumer { + + @Inject + FooService foo; + + } + + @Singleton + static class FooService { + + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchNoBeanConstructorTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchNoBeanConstructorTest.java new file mode 100644 index 00000000000000..9b8c7f52653339 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchNoBeanConstructorTest.java @@ -0,0 +1,49 @@ +package io.quarkus.arc.test.injection.unsatisfied; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.enterprise.inject.UnsatisfiedResolutionException; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.test.ArcTestContainer; + +public class UnsatisfiedMatchNoBeanConstructorTest { + + @RegisterExtension + ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(FooService.class, Consumer.class) + .shouldFail() + .build(); + + @Test + public void testExceptionThrown() { + Throwable error = container.getFailure(); + assertThat(error).rootCause().isInstanceOf(UnsatisfiedResolutionException.class) + .hasMessageContaining("The following classes match by type, but have been skipped during discovery") + .hasMessageContaining( + "io.quarkus.arc.test.injection.unsatisfied.UnsatisfiedMatchNoBeanConstructorTest$FooService does not declare a valid bean constructor"); + + } + + @Singleton + static class Consumer { + + @Inject + FooService foo; + + } + + static class FooService { + + public FooService(boolean flag) { + } + + public FooService(int score) { + } + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchNoBeanDefininAnnotationTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchNoBeanDefininAnnotationTest.java new file mode 100644 index 00000000000000..66f5f548f10661 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchNoBeanDefininAnnotationTest.java @@ -0,0 +1,44 @@ +package io.quarkus.arc.test.injection.unsatisfied; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.enterprise.inject.UnsatisfiedResolutionException; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.test.ArcTestContainer; + +public class UnsatisfiedMatchNoBeanDefininAnnotationTest { + + @RegisterExtension + ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(FooService.class, Consumer.class) + .shouldFail() + .build(); + + @Test + public void testExceptionThrown() { + Throwable error = container.getFailure(); + assertThat(error).rootCause().isInstanceOf(UnsatisfiedResolutionException.class) + .hasMessageContaining("The following classes match by type, but have been skipped during discovery") + .hasMessageContaining( + "io.quarkus.arc.test.injection.unsatisfied.UnsatisfiedMatchNoBeanDefininAnnotationTest$FooService has no bean defining annotation"); + + } + + @Singleton + static class Consumer { + + @Inject + FooService foo; + + } + + static class FooService { + + } + +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchVetoedTypeTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchVetoedTypeTest.java new file mode 100644 index 00000000000000..a70eccffdd8ec9 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/injection/unsatisfied/UnsatisfiedMatchVetoedTypeTest.java @@ -0,0 +1,47 @@ +package io.quarkus.arc.test.injection.unsatisfied; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.enterprise.inject.UnsatisfiedResolutionException; +import jakarta.enterprise.inject.Vetoed; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.test.ArcTestContainer; + +public class UnsatisfiedMatchVetoedTypeTest { + + @RegisterExtension + ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(FooService.class, Consumer.class) + .shouldFail() + .build(); + + @Test + public void testExceptionThrown() { + Throwable error = container.getFailure(); + assertThat(error).rootCause().isInstanceOf(UnsatisfiedResolutionException.class) + .hasMessageContaining("The following classes match by type, but have been skipped during discovery") + .hasMessageContaining( + "io.quarkus.arc.test.injection.unsatisfied.UnsatisfiedMatchVetoedTypeTest$FooService was annotated with @Vetoed"); + + } + + @Singleton + static class Consumer { + + @Inject + FooService foo; + + } + + @Vetoed + @Singleton + static class FooService { + + } + +}