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 1fda908
Show file tree
Hide file tree
Showing 9 changed files with 483 additions and 107 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

Large diffs are not rendered by default.

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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.quarkus.arc.test.sealed;

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

import jakarta.enterprise.context.Dependent;

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

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

public class DependentSealedTest {
@RegisterExtension
public ArcTestContainer container = new ArcTestContainer(MyDependent.class);

@Test
public void test() {
assertNotNull(Arc.container().select(MyDependent.class).get());
}

@Dependent
static sealed class MyDependent permits MyDependentSubclass {
}

static final class MyDependentSubclass extends MyDependent {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.quarkus.arc.test.sealed;

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 InterceptedSealedProducerTest {
@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 sealed type"));
}

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

static sealed class DependentSealed permits DependentSealedSubclass {
@MyInterceptorBinding
String hello() {
return "hello";
}
}

static final class DependentSealedSubclass extends DependentSealed {
}

@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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.quarkus.arc.test.sealed;

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.spi.DeploymentException;
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.test.ArcTestContainer;

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

@Test
public void trigger() {
Throwable error = container.getFailure();
assertNotNull(error);
assertInstanceOf(DeploymentException.class, error);
assertTrue(error.getMessage().contains("must not be sealed"));
}

@Dependent
static sealed class DependentSealed permits DependentSealedSubclass {
@MyInterceptorBinding
String hello() {
return "hello";
}
}

static final class DependentSealedSubclass extends DependentSealed {
}

@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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.quarkus.arc.test.sealed;

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 jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.inject.spi.DeploymentException;

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

import io.quarkus.arc.test.ArcTestContainer;

public class NormalScopedSealedProducerTest {
@RegisterExtension
public ArcTestContainer container = ArcTestContainer.builder()
.beanClasses(Producer.class)
.shouldFail()
.build();

@Test
public void trigger() {
Throwable error = container.getFailure();
assertNotNull(error);
assertInstanceOf(DeploymentException.class, error);
assertTrue(error.getMessage().contains("must not have a return type that is sealed"));
}

@Dependent
static class Producer {
@Produces
@ApplicationScoped
MySealed produce() {
return new MySealed();
}
}

static sealed class MySealed permits MySealedSubclass {
}

static final class MySealedSubclass extends MySealed {
}
}
Loading

0 comments on commit 1fda908

Please sign in to comment.