Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ArC: add validation for sealed types #43747

Merged
merged 1 commit into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading