Skip to content

Commit

Permalink
ArC: add validation for sealed types
Browse files Browse the repository at this point in the history
  • Loading branch information
Ladicek committed Oct 7, 2024
1 parent 9832943 commit a0fb50f
Show file tree
Hide file tree
Showing 9 changed files with 480 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.objectweb.asm.Opcodes;

import io.quarkus.arc.processor.BeanDeployment.SkippedClass;
import io.quarkus.arc.processor.BuiltinBean.ValidatorContext;
import io.quarkus.arc.processor.InjectionPointInfo.TypeAndQualifiers;
import io.quarkus.arc.processor.Types.TypeClosure;
import io.quarkus.gizmo.ClassTransformer;
Expand Down Expand Up @@ -485,7 +486,7 @@ static void resolveInjectionPoint(BeanDeployment deployment, InjectionTargetInfo
}
BuiltinBean builtinBean = BuiltinBean.resolve(injectionPoint);
if (builtinBean != null) {
builtinBean.validate(target, injectionPoint, errors::add);
builtinBean.getValidator().validate(new ValidatorContext(deployment, target, injectionPoint, errors::add));
// Skip built-in beans
return;
}
Expand Down Expand Up @@ -827,7 +828,7 @@ static void validateBean(BeanInfo bean, List<Throwable> errors, Consumer<Bytecod
classifier = "Intercepted";
failIfNotProxyable = true;
}
if (Modifier.isFinal(beanClass.flags()) && classifier != null) {
if (beanClass.isFinal() && classifier != null) {
// Client proxies and subclasses require a non-final class
if (beanClass.isRecord()) {
errors.add(new DeploymentException(String.format(
Expand All @@ -841,6 +842,13 @@ static void validateBean(BeanInfo bean, List<Throwable> errors, Consumer<Bytecod
bean.getDeployment().deferUnproxyableErrorToRuntime(bean);
}
}
if (beanClass.isSealed() && classifier != null) {
if (failIfNotProxyable) {
errors.add(new DeploymentException(String.format("%s bean must not be sealed: %s", classifier, bean)));
} else {
bean.getDeployment().deferUnproxyableErrorToRuntime(bean);
}
}
if (bean.getDeployment().strictCompatibility && classifier != null) {
validateNonStaticFinalMethods(bean, beanClass, bean.getDeployment().getBeanArchiveIndex(),
classifier, errors, failIfNotProxyable);
Expand Down Expand Up @@ -924,7 +932,7 @@ static void validateBean(BeanInfo bean, List<Throwable> errors, Consumer<Bytecod
ClassInfo returnTypeClass = getClassByName(bean.getDeployment().getBeanArchiveIndex(), type);
// null for primitive or array types, but those are covered above
if (returnTypeClass != null && bean.getScope().isNormal() && !Modifier.isInterface(returnTypeClass.flags())) {
if (Modifier.isFinal(returnTypeClass.flags())) {
if (returnTypeClass.isFinal()) {
if (returnTypeClass.isRecord()) {
errors.add(new DeploymentException(String.format(
"%s must not have a type that is a record, because records are always final: %s",
Expand Down Expand Up @@ -986,6 +994,14 @@ static void validateBean(BeanInfo bean, List<Throwable> errors, Consumer<Bytecod
}
}
}
if (returnTypeClass != null && bean.getScope().isNormal() && returnTypeClass.isSealed()) {
if (failIfNotProxyable) {
errors.add(new DeploymentException(
String.format("%s must not have a return type that is sealed: %s", classifier, bean)));
} else {
bean.getDeployment().deferUnproxyableErrorToRuntime(bean);
}
}
} else if (bean.isSynthetic()) {
// synth beans can accidentally be defined with a non-existing scope, throw exception in such case
DotName scopeName = bean.getScope().getDotName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,6 @@ public boolean matches(InjectionPointInfo injectionPoint) {
return matcher.test(injectionPoint, rawTypeDotNames);
}

void validate(InjectionTargetInfo injectionTarget, InjectionPointInfo injectionPoint, Consumer<Throwable> errors) {
validator.validate(injectionTarget, injectionPoint, errors);
}

DotName[] getRawTypeDotNames() {
return rawTypeDotNames;
}
Expand All @@ -117,6 +113,10 @@ Generator getGenerator() {
return generator;
}

Validator getValidator() {
return validator;
}

public static boolean resolvesTo(InjectionPointInfo injectionPoint) {
return resolve(injectionPoint) != null;
}
Expand Down Expand Up @@ -170,13 +170,28 @@ interface Generator {

}

public static class ValidatorContext {
final BeanDeployment beanDeployment;
final InjectionTargetInfo injectionTarget;
final InjectionPointInfo injectionPoint;
final Consumer<Throwable> errors;

public ValidatorContext(BeanDeployment beanDeployment, InjectionTargetInfo injectionTarget,
InjectionPointInfo injectionPoint, Consumer<Throwable> errors) {
this.beanDeployment = beanDeployment;
this.injectionTarget = injectionTarget;
this.injectionPoint = injectionPoint;
this.errors = errors;
}
}

@FunctionalInterface
interface Validator {

Validator NOOP = (it, ip, e) -> {
Validator NOOP = ctx -> {
};

void validate(InjectionTargetInfo injectionTarget, InjectionPointInfo injectionPoint, Consumer<Throwable> errors);
void validate(ValidatorContext context);

}

Expand Down Expand Up @@ -449,117 +464,118 @@ private static ResultHandle loadInvokerTargetBean(InvokerInfo invoker, BytecodeC
bytecode.load(invoker.targetBean.getIdentifier()));
}

private static void validateInstance(InjectionTargetInfo injectionTarget, InjectionPointInfo injectionPoint,
Consumer<Throwable> errors) {
if (injectionPoint.getType().kind() != Kind.PARAMETERIZED_TYPE) {
errors.accept(
new DefinitionException("An injection point of raw type jakarta.enterprise.inject.Instance is defined: "
+ injectionPoint.getTargetInfo()));
} else if (injectionPoint.getRequiredType().kind() == Kind.WILDCARD_TYPE) {
errors.accept(
new DefinitionException("Wildcard is not a legal type argument for jakarta.enterprise.inject.Instance: " +
injectionPoint.getTargetInfo()));
} else if (injectionPoint.getRequiredType().kind() == Kind.TYPE_VARIABLE) {
errors.accept(new DefinitionException(
"Type variable is not a legal type argument for jakarta.enterprise.inject.Instance: " +
injectionPoint.getTargetInfo()));
private static void validateInstance(ValidatorContext ctx) {
if (ctx.injectionPoint.getType().kind() != Kind.PARAMETERIZED_TYPE) {
ctx.errors.accept(new DefinitionException(
"An injection point of raw type jakarta.enterprise.inject.Instance is defined: "
+ ctx.injectionPoint.getTargetInfo()));
} else if (ctx.injectionPoint.getRequiredType().kind() == Kind.WILDCARD_TYPE) {
ctx.errors.accept(new DefinitionException(
"Wildcard is not a legal type argument for jakarta.enterprise.inject.Instance: "
+ ctx.injectionPoint.getTargetInfo()));
} else if (ctx.injectionPoint.getRequiredType().kind() == Kind.TYPE_VARIABLE) {
ctx.errors.accept(new DefinitionException(
"Type variable is not a legal type argument for jakarta.enterprise.inject.Instance: "
+ ctx.injectionPoint.getTargetInfo()));
}
}

private static void validateList(InjectionTargetInfo injectionTarget, InjectionPointInfo injectionPoint,
Consumer<Throwable> errors) {
if (injectionPoint.getType().kind() != Kind.PARAMETERIZED_TYPE) {
errors.accept(
new DefinitionException("An injection point of raw type is defined: " + injectionPoint.getTargetInfo()));
private static void validateList(ValidatorContext ctx) {
if (ctx.injectionPoint.getType().kind() != Kind.PARAMETERIZED_TYPE) {
ctx.errors.accept(new DefinitionException(
"An injection point of raw type is defined: " + ctx.injectionPoint.getTargetInfo()));
} else {
// Note that at this point we can be sure that the required type is List<>
Type typeParam = injectionPoint.getType().asParameterizedType().arguments().get(0);
Type typeParam = ctx.injectionPoint.getType().asParameterizedType().arguments().get(0);
if (typeParam.kind() == Type.Kind.WILDCARD_TYPE) {
if (injectionPoint.isSynthetic()) {
errors.accept(
new DefinitionException(
"Wildcard is not a legal type argument for a synthetic @All List<?> injection point used in: "
+ injectionTarget.toString()));
if (ctx.injectionPoint.isSynthetic()) {
ctx.errors.accept(new DefinitionException(
"Wildcard is not a legal type argument for a synthetic @All List<?> injection point used in: "
+ ctx.injectionTarget.toString()));
return;
}
ClassInfo declaringClass;
if (injectionPoint.isField()) {
declaringClass = injectionPoint.getAnnotationTarget().asField().declaringClass();
if (ctx.injectionPoint.isField()) {
declaringClass = ctx.injectionPoint.getAnnotationTarget().asField().declaringClass();
} else {
declaringClass = injectionPoint.getAnnotationTarget().asMethodParameter().method().declaringClass();
declaringClass = ctx.injectionPoint.getAnnotationTarget().asMethodParameter().method().declaringClass();
}
if (isKotlinClass(declaringClass)) {
errors.accept(
new DefinitionException(
"kotlin.collections.List cannot be used together with the @All qualifier, please use MutableList or java.util.List instead: "
+ injectionPoint.getTargetInfo()));
ctx.errors.accept(new DefinitionException(
"kotlin.collections.List cannot be used together with the @All qualifier, please use MutableList or java.util.List instead: "
+ ctx.injectionPoint.getTargetInfo()));
} else {
errors.accept(
new DefinitionException(
"Wildcard is not a legal type argument for: " + injectionPoint.getTargetInfo()));
ctx.errors.accept(new DefinitionException(
"Wildcard is not a legal type argument for: " + ctx.injectionPoint.getTargetInfo()));
}
} else if (typeParam.kind() == Type.Kind.TYPE_VARIABLE) {
errors.accept(new DefinitionException(
"Type variable is not a legal type argument for: " + injectionPoint.getTargetInfo()));
ctx.errors.accept(new DefinitionException(
"Type variable is not a legal type argument for: " + ctx.injectionPoint.getTargetInfo()));
}
}
}

private static void validateInjectionPoint(InjectionTargetInfo injectionTarget, InjectionPointInfo injectionPoint,
Consumer<Throwable> errors) {
if (injectionTarget.kind() != TargetKind.BEAN || !BuiltinScope.DEPENDENT.is(injectionTarget.asBean().getScope())) {
String msg = injectionPoint.getTargetInfo();
private static void validateInjectionPoint(ValidatorContext ctx) {
if (ctx.injectionTarget.kind() != TargetKind.BEAN
|| !BuiltinScope.DEPENDENT.is(ctx.injectionTarget.asBean().getScope())) {
String msg = ctx.injectionPoint.getTargetInfo();
if (msg.isBlank()) {
msg = injectionTarget.toString();
msg = ctx.injectionTarget.toString();
}
errors.accept(new DefinitionException("Only @Dependent beans can access metadata about an injection point: "
+ msg));
ctx.errors.accept(new DefinitionException(
"Only @Dependent beans can access metadata about an injection point: " + msg));
}
}

private static void validateBean(InjectionTargetInfo injectionTarget, InjectionPointInfo injectionPoint,
Consumer<Throwable> errors) {
if (injectionTarget.kind() != InjectionTargetInfo.TargetKind.BEAN) {
errors.accept(new DefinitionException("Only beans can access bean metadata"));
private static void validateBean(ValidatorContext ctx) {
if (ctx.injectionTarget.kind() != InjectionTargetInfo.TargetKind.BEAN) {
ctx.errors.accept(new DefinitionException("Only beans can access bean metadata"));
}
}

private static void validateInterceptedBean(InjectionTargetInfo injectionTarget, InjectionPointInfo injectionPoint,
Consumer<Throwable> errors) {
if (injectionTarget.kind() != InjectionTargetInfo.TargetKind.BEAN || !injectionTarget.asBean().isInterceptor()) {
errors.accept(new DefinitionException("Only interceptors can access intercepted bean metadata"));
private static void validateInterceptedBean(ValidatorContext ctx) {
if (ctx.injectionTarget.kind() != InjectionTargetInfo.TargetKind.BEAN
|| !ctx.injectionTarget.asBean().isInterceptor()) {
ctx.errors.accept(new DefinitionException("Only interceptors can access intercepted bean metadata"));
}
}

private static void validateEventMetadata(InjectionTargetInfo injectionTarget, InjectionPointInfo injectionPoint,
Consumer<Throwable> errors) {
if (injectionTarget.kind() != TargetKind.OBSERVER) {
errors.accept(new DefinitionException("EventMetadata can be only injected into an observer method: "
+ injectionPoint.getTargetInfo()));
private static void validateEventMetadata(ValidatorContext ctx) {
if (ctx.injectionTarget.kind() != TargetKind.OBSERVER) {
ctx.errors.accept(new DefinitionException(
"EventMetadata can be only injected into an observer method: " + ctx.injectionPoint.getTargetInfo()));
}
}

private static void validateInterceptionProxy(InjectionTargetInfo injectionTarget,
InjectionPointInfo injectionPoint, Consumer<Throwable> errors) {
if (injectionTarget.kind() != TargetKind.BEAN
|| (!injectionTarget.asBean().isProducerMethod() && !injectionTarget.asBean().isSynthetic())
|| injectionTarget.asBean().getInterceptionProxy() == null) {
errors.accept(new DefinitionException(
private static void validateInterceptionProxy(ValidatorContext ctx) {
if (ctx.injectionTarget.kind() != TargetKind.BEAN
|| (!ctx.injectionTarget.asBean().isProducerMethod() && !ctx.injectionTarget.asBean().isSynthetic())
|| ctx.injectionTarget.asBean().getInterceptionProxy() == null) {
ctx.errors.accept(new DefinitionException(
"InterceptionProxy can only be injected into a producer method or a synthetic bean"));
}
if (injectionPoint.getType().kind() != Kind.PARAMETERIZED_TYPE) {
errors.accept(new DefinitionException("InterceptionProxy must be a parameterized type"));
if (ctx.injectionPoint.getType().kind() != Kind.PARAMETERIZED_TYPE) {
ctx.errors.accept(new DefinitionException("InterceptionProxy must be a parameterized type"));
}
Type interceptionProxyType = injectionPoint.getType().asParameterizedType().arguments().get(0);
Type interceptionProxyType = ctx.injectionPoint.getType().asParameterizedType().arguments().get(0);
if (interceptionProxyType.kind() != Kind.CLASS && interceptionProxyType.kind() != Kind.PARAMETERIZED_TYPE) {
errors.accept(new DefinitionException(
ctx.errors.accept(new DefinitionException(
"Type argument of InterceptionProxy may only be a class or parameterized type"));
}
if (!injectionTarget.asBean().getProviderType().equals(interceptionProxyType)) {
String msg = injectionTarget.asBean().isProducerMethod()
if (!ctx.injectionTarget.asBean().getProviderType().equals(interceptionProxyType)) {
String msg = ctx.injectionTarget.asBean().isProducerMethod()
? "Type argument of InterceptionProxy must be equal to the return type of the producer method"
: "Type argument of InterceptionProxy must be equal to the bean provider type";
errors.accept(new DefinitionException(msg));
ctx.errors.accept(new DefinitionException(msg));
}
ClassInfo clazz = getClassByName(ctx.beanDeployment.getBeanArchiveIndex(), interceptionProxyType.name());
if (clazz != null) {
if (clazz.isRecord()) {
ctx.errors.accept(new DefinitionException("Cannot build InterceptionProxy for a record"));
}
if (clazz.isSealed()) {
ctx.errors.accept(new DefinitionException("Cannot build InterceptionProxy for a sealed type"));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.quarkus.arc.test.records;

import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import jakarta.annotation.Priority;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.inject.spi.DefinitionException;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InterceptorBinding;
import jakarta.interceptor.InvocationContext;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.InterceptionProxy;
import io.quarkus.arc.test.ArcTestContainer;

public class InterceptedRecordProducerTest {
@RegisterExtension
public ArcTestContainer container = ArcTestContainer.builder()
.beanClasses(Producer.class, MyInterceptorBinding.class, MyInterceptor.class)
.shouldFail()
.build();

@Test
public void trigger() {
Throwable error = container.getFailure();
assertNotNull(error);
assertInstanceOf(DefinitionException.class, error);
assertTrue(error.getMessage().contains("Cannot build InterceptionProxy for a record"));
}

@Dependent
static class Producer {
@Produces
@Dependent
DependentRecord produce(InterceptionProxy<DependentRecord> proxy) {
return proxy.create(new DependentRecord());
}
}

record DependentRecord() {
@MyInterceptorBinding
String hello() {
return "hello";
}
}

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@InterceptorBinding
@interface MyInterceptorBinding {
}

@MyInterceptorBinding
@Interceptor
@Priority(1)
static class MyInterceptor {
@AroundInvoke
Object intercept(InvocationContext ctx) throws Exception {
return "intercepted: " + ctx.proceed();
}
}
}
Loading

0 comments on commit a0fb50f

Please sign in to comment.