Detect use of deprecated API

This commit is a best effort attempt at identifying the members that
code generation invokes and might be deprecated. It introduces
a CodeWarnings helper class that records warnings, with special
handling for `@Deprecated`.

See gh-29597
This commit is contained in:
Stéphane Nicoll
2023-10-10 14:52:18 +02:00
parent 35372e5e72
commit ea66883d79
12 changed files with 614 additions and 3 deletions

View File

@@ -26,6 +26,7 @@ import java.util.function.Supplier;
import javax.lang.model.element.Modifier;
import javax.xml.parsers.DocumentBuilderFactory;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.GeneratedMethod;
@@ -52,6 +53,7 @@ import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy;
import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy.Implementation;
import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy.One;
import org.springframework.beans.testfixture.beans.factory.aot.TestHierarchy.Two;
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedBean;
import org.springframework.core.ResolvableType;
import org.springframework.core.test.io.support.MockSpringFactoriesLoader;
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
@@ -66,6 +68,7 @@ import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Tests for {@link BeanDefinitionMethodGenerator} and
@@ -740,6 +743,32 @@ class BeanDefinitionMethodGeneratorTests {
});
}
@Nested
@SuppressWarnings("deprecation")
class DeprecationTests {
private static final TestCompiler TEST_COMPILER = TestCompiler.forSystem()
.withCompilerOptions("-Xlint:all", "-Xlint:-rawtypes", "-Werror");
@Test
void generateBeanDefinitionMethodWithDeprecatedTargetClass() {
RootBeanDefinition beanDefinition = new RootBeanDefinition(DeprecatedBean.class);
RegisteredBean registeredBean = registerBean(beanDefinition);
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(
methodGeneratorFactory, registeredBean, null,
Collections.emptyList());
MethodReference method = generator.generateBeanDefinitionMethod(
generationContext, beanRegistrationsCode);
compileAndCheckWarnings(method);
}
private void compileAndCheckWarnings(MethodReference methodReference) {
assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, methodReference,
((instanceSupplier, compiled) -> {})));
}
}
private void testBeanDefinitionMethodInCurrentFile(Class<?> targetType, RootBeanDefinition beanDefinition) {
RegisteredBean registeredBean = registerBean(new RootBeanDefinition(beanDefinition));
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(
@@ -764,6 +793,10 @@ class BeanDefinitionMethodGeneratorTests {
}
private void compile(MethodReference method, BiConsumer<RootBeanDefinition, Compiled> result) {
compile(TestCompiler.forSystem(), method, result);
}
private void compile(TestCompiler testCompiler, MethodReference method, BiConsumer<RootBeanDefinition, Compiled> result) {
this.beanRegistrationsCode.getTypeBuilder().set(type -> {
CodeBlock methodInvocation = method.toInvokeCodeBlock(ArgumentCodeGenerator.none(),
this.beanRegistrationsCode.getClassName());
@@ -775,7 +808,7 @@ class BeanDefinitionMethodGeneratorTests {
.addCode("return $L;", methodInvocation).build());
});
this.generationContext.writeGeneratedContent();
TestCompiler.forSystem().with(this.generationContext).compile(compiled ->
testCompiler.with(this.generationContext).compile(compiled ->
result.accept((RootBeanDefinition) compiled.getInstance(Supplier.class).get(), compiled));
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans.factory.aot;
import java.util.function.Consumer;
import javax.lang.model.element.Modifier;
import org.junit.jupiter.api.Test;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder;
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedBean;
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedForRemovalBean;
import org.springframework.core.test.tools.Compiled;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.MethodSpec.Builder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CodeWarnings}.
*
* @author Stephane Nicoll
*/
class CodeWarningsTests {
private static final TestCompiler TEST_COMPILER = TestCompiler.forSystem()
.withCompilerOptions("-Xlint:all", "-Werror");
private final CodeWarnings codeWarnings;
private final TestGenerationContext generationContext;
CodeWarningsTests() {
this.codeWarnings = new CodeWarnings();
this.generationContext = new TestGenerationContext();
}
@Test
void registerNoWarningDoesNotIncludeAnnotation() {
compile(method -> {
this.codeWarnings.suppress(method);
method.addStatement("$T bean = $S", String.class, "Hello");
}, compiled -> assertThat(compiled.getSourceFile()).doesNotContain("@SuppressWarnings"));
}
@Test
@SuppressWarnings("deprecation")
void registerWarningSuppressesIt() {
this.codeWarnings.register("deprecation");
compile(method -> {
this.codeWarnings.suppress(method);
method.addStatement("$T bean = new $T()", DeprecatedBean.class, DeprecatedBean.class);
}, compiled -> assertThat(compiled.getSourceFile())
.contains("@SuppressWarnings(\"deprecation\")"));
}
@Test
@SuppressWarnings({ "deprecation", "removal" })
void registerSeveralWarningsSuppressesThem() {
this.codeWarnings.register("deprecation");
this.codeWarnings.register("removal");
compile(method -> {
this.codeWarnings.suppress(method);
method.addStatement("$T bean = new $T()", DeprecatedBean.class, DeprecatedBean.class);
method.addStatement("$T another = new $T()", DeprecatedForRemovalBean.class, DeprecatedForRemovalBean.class);
}, compiled -> assertThat(compiled.getSourceFile())
.contains("@SuppressWarnings({ \"deprecation\", \"removal\" })"));
}
@Test
@SuppressWarnings("deprecation")
void detectDeprecationOnAnnotatedElementWithDeprecated() {
this.codeWarnings.detectDeprecation(DeprecatedBean.class);
assertThat(this.codeWarnings.getWarnings()).containsExactly("deprecation");
}
@Test
@SuppressWarnings("removal")
void detectDeprecationOnAnnotatedElementWithDeprecatedForRemoval() {
this.codeWarnings.detectDeprecation(DeprecatedForRemovalBean.class);
assertThat(this.codeWarnings.getWarnings()).containsExactly("removal");
}
@Test
void toStringIncludeWarnings() {
this.codeWarnings.register("deprecation");
this.codeWarnings.register("rawtypes");
assertThat(this.codeWarnings).hasToString("CodeWarnings[deprecation, rawtypes]");
}
private void compile(Consumer<Builder> method,
Consumer<Compiled> result) {
DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder();
this.generationContext.getGeneratedClasses().addForFeature("TestCode", typeBuilder);
typeBuilder.set(type -> {
type.addModifiers(Modifier.PUBLIC);
Builder methodBuilder = MethodSpec.methodBuilder("apply")
.addModifiers(Modifier.PUBLIC);
method.accept(methodBuilder);
type.addMethod(methodBuilder.build());
});
this.generationContext.writeGeneratedContent();
TEST_COMPILER.with(this.generationContext).compile(result);
}
}

View File

@@ -23,6 +23,8 @@ import java.util.function.Supplier;
import javax.lang.model.element.Modifier;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.GeneratedClass;
@@ -44,6 +46,12 @@ import org.springframework.beans.testfixture.beans.factory.generator.InnerCompon
import org.springframework.beans.testfixture.beans.factory.generator.InnerComponentConfiguration.EnvironmentAwareComponent;
import org.springframework.beans.testfixture.beans.factory.generator.InnerComponentConfiguration.NoDependencyComponent;
import org.springframework.beans.testfixture.beans.factory.generator.SimpleConfiguration;
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedBean;
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedConstructor;
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedForRemovalBean;
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedForRemovalConstructor;
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedForRemovalMemberConfiguration;
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedMemberConfiguration;
import org.springframework.beans.testfixture.beans.factory.generator.factory.NumberHolder;
import org.springframework.beans.testfixture.beans.factory.generator.factory.NumberHolderFactoryBean;
import org.springframework.beans.testfixture.beans.factory.generator.factory.SampleFactory;
@@ -57,6 +65,7 @@ import org.springframework.javapoet.ParameterizedTypeName;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Tests for {@link InstanceSupplierCodeGenerator}.
@@ -261,6 +270,108 @@ class InstanceSupplierCodeGeneratorTests {
.satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT));
}
@Nested
@SuppressWarnings("deprecation")
class DeprecationTests {
private static final TestCompiler TEST_COMPILER = TestCompiler.forSystem()
.withCompilerOptions("-Xlint:all", "-Xlint:-rawtypes", "-Werror");
@Test
@Disabled("Need to move to a separate method so that the warning can be suppressed")
void generateWhenTargetClassIsDeprecated() {
compileAndCheckWarnings(new RootBeanDefinition(DeprecatedBean.class));
}
@Test
void generateWhenTargetConstructorIsDeprecated() {
compileAndCheckWarnings(new RootBeanDefinition(DeprecatedConstructor.class));
}
@Test
void generateWhenTargetFactoryMethodIsDeprecated() {
BeanDefinition beanDefinition = BeanDefinitionBuilder
.rootBeanDefinition(String.class)
.setFactoryMethodOnBean("deprecatedString", "config").getBeanDefinition();
beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder
.genericBeanDefinition(DeprecatedMemberConfiguration.class).getBeanDefinition());
compileAndCheckWarnings(beanDefinition);
}
@Test
void generateWhenTargetFactoryMethodParameterIsDeprecated() {
BeanDefinition beanDefinition = BeanDefinitionBuilder
.rootBeanDefinition(String.class)
.setFactoryMethodOnBean("deprecatedParameter", "config").getBeanDefinition();
beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder
.genericBeanDefinition(DeprecatedMemberConfiguration.class).getBeanDefinition());
beanFactory.registerBeanDefinition("parameter", new RootBeanDefinition(DeprecatedBean.class));
compileAndCheckWarnings(beanDefinition);
}
@Test
void generateWhenTargetFactoryMethodReturnTypeIsDeprecated() {
BeanDefinition beanDefinition = BeanDefinitionBuilder
.rootBeanDefinition(DeprecatedBean.class)
.setFactoryMethodOnBean("deprecatedReturnType", "config").getBeanDefinition();
beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder
.genericBeanDefinition(DeprecatedMemberConfiguration.class).getBeanDefinition());
compileAndCheckWarnings(beanDefinition);
}
private void compileAndCheckWarnings(BeanDefinition beanDefinition) {
assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, beanDefinition,
((instanceSupplier, compiled) -> {})));
}
}
@Nested
@SuppressWarnings("removal")
class DeprecationForRemovalTests {
private static final TestCompiler TEST_COMPILER = TestCompiler.forSystem()
.withCompilerOptions("-Xlint:all", "-Xlint:-rawtypes", "-Werror");
@Test
@Disabled("Need to move to a separate method so that the warning can be suppressed")
void generateWhenTargetClassIsDeprecatedForRemoval() {
compileAndCheckWarnings(new RootBeanDefinition(DeprecatedForRemovalBean.class));
}
@Test
void generateWhenTargetConstructorIsDeprecatedForRemoval() {
compileAndCheckWarnings(new RootBeanDefinition(DeprecatedForRemovalConstructor.class));
}
@Test
void generateWhenTargetFactoryMethodIsDeprecatedForRemoval() {
BeanDefinition beanDefinition = BeanDefinitionBuilder
.rootBeanDefinition(String.class)
.setFactoryMethodOnBean("deprecatedString", "config").getBeanDefinition();
beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder
.genericBeanDefinition(DeprecatedForRemovalMemberConfiguration.class).getBeanDefinition());
compileAndCheckWarnings(beanDefinition);
}
@Test
void generateWhenTargetFactoryMethodParameterIsDeprecatedForRemoval() {
BeanDefinition beanDefinition = BeanDefinitionBuilder
.rootBeanDefinition(String.class)
.setFactoryMethodOnBean("deprecatedParameter", "config").getBeanDefinition();
beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder
.genericBeanDefinition(DeprecatedForRemovalMemberConfiguration.class).getBeanDefinition());
beanFactory.registerBeanDefinition("parameter", new RootBeanDefinition(DeprecatedForRemovalBean.class));
compileAndCheckWarnings(beanDefinition);
}
private void compileAndCheckWarnings(BeanDefinition beanDefinition) {
assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, beanDefinition,
((instanceSupplier, compiled) -> {})));
}
}
private ReflectionHints getReflectionHints() {
return this.generationContext.getRuntimeHints().reflection();
}
@@ -285,6 +396,11 @@ class InstanceSupplierCodeGeneratorTests {
}
private void compile(BeanDefinition beanDefinition, BiConsumer<InstanceSupplier<?>, Compiled> result) {
compile(TestCompiler.forSystem(), beanDefinition, result);
}
private void compile(TestCompiler testCompiler, BeanDefinition beanDefinition,
BiConsumer<InstanceSupplier<?>, Compiled> result) {
DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(this.beanFactory);
freshBeanFactory.registerBeanDefinition("testBean", beanDefinition);
@@ -306,8 +422,8 @@ class InstanceSupplierCodeGeneratorTests {
.addStatement("return $L", generatedCode).build());
});
this.generationContext.writeGeneratedContent();
TestCompiler.forSystem().with(this.generationContext).compile(compiled ->
result.accept((InstanceSupplier<?>) compiled.getInstance(Supplier.class).get(), compiled));
testCompiler.with(this.generationContext).compile(compiled -> result.accept(
(InstanceSupplier<?>) compiled.getInstance(Supplier.class).get(), compiled));
}
}