Skip to content

Commit 76f4d9b

Browse files
authored
Merge pull request #33882 from Ladicek/prevent-removing-final-from-records
ArC: prevent removing the final flag from records
2 parents 853fc9c + 540b809 commit 76f4d9b

File tree

7 files changed

+248
-7
lines changed

7 files changed

+248
-7
lines changed

independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -727,9 +727,12 @@ static void validateBean(BeanInfo bean, List<Throwable> errors, Consumer<Bytecod
727727
}
728728
if (Modifier.isFinal(beanClass.flags()) && classifier != null) {
729729
// Client proxies and subclasses require a non-final class
730-
if (bean.getDeployment().transformUnproxyableClasses) {
731-
bytecodeTransformerConsumer
732-
.accept(new BytecodeTransformer(beanClass.name().toString(), new FinalClassTransformFunction()));
730+
if (beanClass.isRecord()) {
731+
errors.add(new DeploymentException(String.format(
732+
"%s bean must not be a record, because records are always final: %s", classifier, bean)));
733+
} else if (bean.getDeployment().transformUnproxyableClasses) {
734+
bytecodeTransformerConsumer.accept(
735+
new BytecodeTransformer(beanClass.name().toString(), new FinalClassTransformFunction()));
733736
} else if (failIfNotProxyable) {
734737
errors.add(new DeploymentException(String.format("%s bean must not be final: %s", classifier, bean)));
735738
} else {
@@ -819,10 +822,13 @@ static void validateBean(BeanInfo bean, List<Throwable> errors, Consumer<Bytecod
819822
// null for primitive or array types, but those are covered above
820823
if (returnTypeClass != null && bean.getScope().isNormal() && !Modifier.isInterface(returnTypeClass.flags())) {
821824
if (Modifier.isFinal(returnTypeClass.flags())) {
822-
if (bean.getDeployment().transformUnproxyableClasses) {
823-
bytecodeTransformerConsumer
824-
.accept(new BytecodeTransformer(returnTypeClass.name().toString(),
825-
new FinalClassTransformFunction()));
825+
if (returnTypeClass.isRecord()) {
826+
errors.add(new DeploymentException(String.format(
827+
"%s must not have a type that is a record, because records are always final: %s",
828+
classifier, bean)));
829+
} else if (bean.getDeployment().transformUnproxyableClasses) {
830+
bytecodeTransformerConsumer.accept(
831+
new BytecodeTransformer(returnTypeClass.name().toString(), new FinalClassTransformFunction()));
826832
} else if (failIfNotProxyable) {
827833
errors.add(new DeploymentException(
828834
String.format("%s must not have a return type that is final: %s", classifier, bean)));

independent-projects/arc/tests/pom.xml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,41 @@
8989
</build>
9090

9191
<profiles>
92+
<profile>
93+
<id>java-17</id>
94+
<activation>
95+
<jdk>[17,)</jdk>
96+
</activation>
97+
98+
<properties>
99+
<maven.compiler.target>17</maven.compiler.target>
100+
<maven.compiler.source>17</maven.compiler.source>
101+
<maven.compiler.release>17</maven.compiler.release>
102+
</properties>
103+
<build>
104+
<plugins>
105+
<plugin>
106+
<groupId>org.codehaus.mojo</groupId>
107+
<artifactId>build-helper-maven-plugin</artifactId>
108+
<executions>
109+
<execution>
110+
<id>add-java17-directory</id>
111+
<phase>generate-test-sources</phase>
112+
<goals>
113+
<goal>add-test-source</goal>
114+
</goals>
115+
<configuration>
116+
<sources>
117+
<source>src/test/java17</source>
118+
</sources>
119+
</configuration>
120+
</execution>
121+
</executions>
122+
</plugin>
123+
</plugins>
124+
</build>
125+
</profile>
126+
92127
<profile>
93128
<id>kotlin-tests</id>
94129
<activation>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.quarkus.arc.test.java17.records;
2+
3+
import io.quarkus.arc.Arc;
4+
import io.quarkus.arc.test.ArcTestContainer;
5+
import jakarta.enterprise.context.Dependent;
6+
import jakarta.enterprise.inject.spi.DeploymentException;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.extension.RegisterExtension;
9+
10+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
11+
import static org.junit.jupiter.api.Assertions.assertNotNull;
12+
import static org.junit.jupiter.api.Assertions.assertTrue;
13+
14+
public class DependentRecordTest {
15+
@RegisterExtension
16+
public ArcTestContainer container = new ArcTestContainer(DependentRecord.class);
17+
18+
@Test
19+
public void test() {
20+
assertNotNull(Arc.container().select(DependentRecord.class).get());
21+
}
22+
23+
@Dependent
24+
record DependentRecord() {
25+
}
26+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.quarkus.arc.test.java17.records;
2+
3+
import io.quarkus.arc.test.ArcTestContainer;
4+
import io.quarkus.arc.test.interceptors.targetclass.mixed.AroundInvokeOnTargetClassAndOutsideAndManySuperclassesWithOverridesTest;
5+
import jakarta.annotation.Priority;
6+
import jakarta.enterprise.context.ApplicationScoped;
7+
import jakarta.enterprise.context.Dependent;
8+
import jakarta.enterprise.inject.spi.DeploymentException;
9+
import jakarta.interceptor.AroundInvoke;
10+
import jakarta.interceptor.Interceptor;
11+
import jakarta.interceptor.InterceptorBinding;
12+
import jakarta.interceptor.InvocationContext;
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.extension.RegisterExtension;
15+
16+
import java.lang.annotation.Documented;
17+
import java.lang.annotation.ElementType;
18+
import java.lang.annotation.Retention;
19+
import java.lang.annotation.RetentionPolicy;
20+
import java.lang.annotation.Target;
21+
22+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
23+
import static org.junit.jupiter.api.Assertions.assertNotNull;
24+
import static org.junit.jupiter.api.Assertions.assertTrue;
25+
26+
public class InterceptedRecordTest {
27+
@RegisterExtension
28+
public ArcTestContainer container = ArcTestContainer.builder()
29+
.beanClasses(DependentRecord.class, MyInterceptorBinding.class, MyInterceptor.class)
30+
.shouldFail()
31+
.build();
32+
33+
@Test
34+
public void trigger() {
35+
Throwable error = container.getFailure();
36+
assertNotNull(error);
37+
assertInstanceOf(DeploymentException.class, error);
38+
assertTrue(error.getMessage().contains("records are always final"));
39+
}
40+
41+
@Dependent
42+
record DependentRecord() {
43+
@MyInterceptorBinding
44+
String hello() {
45+
return "hello";
46+
}
47+
}
48+
49+
@Target({ ElementType.TYPE, ElementType.METHOD })
50+
@Retention(RetentionPolicy.RUNTIME)
51+
@Documented
52+
@InterceptorBinding
53+
@interface MyInterceptorBinding {
54+
}
55+
56+
@MyInterceptorBinding
57+
@Interceptor
58+
@Priority(1)
59+
static class MyInterceptor {
60+
@AroundInvoke
61+
Object intercept(InvocationContext ctx) throws Exception {
62+
return "intercepted: " + ctx.proceed();
63+
}
64+
}
65+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.quarkus.arc.test.java17.records;
2+
3+
import io.quarkus.arc.test.ArcTestContainer;
4+
import jakarta.enterprise.context.ApplicationScoped;
5+
import jakarta.enterprise.context.Dependent;
6+
import jakarta.enterprise.inject.Produces;
7+
import jakarta.enterprise.inject.spi.DeploymentException;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.extension.RegisterExtension;
10+
11+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
12+
import static org.junit.jupiter.api.Assertions.assertNotNull;
13+
import static org.junit.jupiter.api.Assertions.assertTrue;
14+
15+
public class NormalScopedRecordProducerTest {
16+
@RegisterExtension
17+
public ArcTestContainer container = ArcTestContainer.builder()
18+
.beanClasses(Producer.class)
19+
.shouldFail()
20+
.build();
21+
22+
@Test
23+
public void trigger() {
24+
Throwable error = container.getFailure();
25+
assertNotNull(error);
26+
assertInstanceOf(DeploymentException.class, error);
27+
assertTrue(error.getMessage().contains("records are always final"));
28+
}
29+
30+
@Dependent
31+
static class Producer {
32+
@Produces
33+
@ApplicationScoped
34+
MyRecord produce() {
35+
return new MyRecord();
36+
}
37+
}
38+
39+
record MyRecord() {
40+
}
41+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.quarkus.arc.test.java17.records;
2+
3+
import io.quarkus.arc.test.ArcTestContainer;
4+
import jakarta.enterprise.context.ApplicationScoped;
5+
import jakarta.enterprise.inject.spi.DeploymentException;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.RegisterExtension;
8+
9+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
10+
import static org.junit.jupiter.api.Assertions.assertNotNull;
11+
import static org.junit.jupiter.api.Assertions.assertTrue;
12+
13+
public class NormalScopedRecordTest {
14+
@RegisterExtension
15+
public ArcTestContainer container = ArcTestContainer.builder()
16+
.beanClasses(NormalScopedRecord.class)
17+
.shouldFail()
18+
.build();
19+
20+
@Test
21+
public void trigger() {
22+
Throwable error = container.getFailure();
23+
assertNotNull(error);
24+
assertInstanceOf(DeploymentException.class, error);
25+
assertTrue(error.getMessage().contains("records are always final"));
26+
}
27+
28+
@ApplicationScoped
29+
record NormalScopedRecord() {
30+
}
31+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.quarkus.arc.test.java17.records;
2+
3+
import io.quarkus.arc.Arc;
4+
import io.quarkus.arc.test.ArcTestContainer;
5+
import jakarta.enterprise.context.ApplicationScoped;
6+
import jakarta.enterprise.context.Dependent;
7+
import jakarta.enterprise.inject.Produces;
8+
import jakarta.enterprise.inject.spi.DeploymentException;
9+
import jakarta.inject.Singleton;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
14+
import static org.junit.jupiter.api.Assertions.assertNotNull;
15+
import static org.junit.jupiter.api.Assertions.assertTrue;
16+
17+
public class SingletonRecordProducerTest {
18+
@RegisterExtension
19+
public ArcTestContainer container = new ArcTestContainer(Producer.class);
20+
21+
@Test
22+
public void test() {
23+
assertNotNull(Arc.container().select(MyRecord.class).get());
24+
}
25+
26+
@Dependent
27+
static class Producer {
28+
@Produces
29+
@Singleton
30+
MyRecord produce() {
31+
return new MyRecord();
32+
}
33+
}
34+
35+
record MyRecord() {
36+
}
37+
}

0 commit comments

Comments
 (0)