Improve handling of annotation properties in architecture checks

Closes gh-47437
This commit is contained in:
Andy Wilkinson 2025-10-09 09:55:25 +01:00
parent 61acc52137
commit 3ba61c3fa7
9 changed files with 155 additions and 23 deletions

View File

@ -89,7 +89,7 @@ final class ArchitectureRules {
rules.add(noClassesShouldCallStringToUpperCaseWithoutLocale()); rules.add(noClassesShouldCallStringToUpperCaseWithoutLocale());
rules.add(noClassesShouldCallStringToLowerCaseWithoutLocale()); rules.add(noClassesShouldCallStringToLowerCaseWithoutLocale());
rules.add(conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType()); rules.add(conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType());
rules.add(enumSourceShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodParameterType()); rules.add(enumSourceShouldNotHaveValueThatIsTheSameAsTypeOfMethodsFirstParameter());
rules.add(allConfigurationPropertiesBindingBeanMethodsShouldBeStatic()); rules.add(allConfigurationPropertiesBindingBeanMethodsShouldBeStatic());
return List.copyOf(rules); return List.copyOf(rules);
} }
@ -222,7 +222,7 @@ final class ArchitectureRules {
JavaAnnotation<JavaMethod> conditionalAnnotation = item JavaAnnotation<JavaMethod> conditionalAnnotation = item
.getAnnotationOfType("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean"); .getAnnotationOfType("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean");
Map<String, Object> properties = conditionalAnnotation.getProperties(); Map<String, Object> properties = conditionalAnnotation.getProperties();
if (!properties.containsKey("type") && !properties.containsKey("name")) { if (!hasProperty("type", properties) && !hasProperty("name", properties)) {
conditionalAnnotation.get("value").ifPresent((value) -> { conditionalAnnotation.get("value").ifPresent((value) -> {
if (containsOnlySingleType((JavaType[]) value, item.getReturnType())) { if (containsOnlySingleType((JavaType[]) value, item.getReturnType())) {
addViolation(events, item, conditionalAnnotation.getDescription() addViolation(events, item, conditionalAnnotation.getDescription()
@ -233,16 +233,24 @@ final class ArchitectureRules {
}); });
} }
private static ArchRule enumSourceShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodParameterType() { private static boolean hasProperty(String name, Map<String, Object> properties) {
Object property = properties.get(name);
if (property == null) {
return false;
}
return !property.getClass().isArray() || ((Object[]) property).length > 0;
}
private static ArchRule enumSourceShouldNotHaveValueThatIsTheSameAsTypeOfMethodsFirstParameter() {
return ArchRuleDefinition.methods() return ArchRuleDefinition.methods()
.that() .that()
.areAnnotatedWith("org.junit.jupiter.params.provider.EnumSource") .areAnnotatedWith("org.junit.jupiter.params.provider.EnumSource")
.should(notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType()) .should(notHaveValueThatIsTheSameAsTheTypeOfTheMethodsFirstParameter())
.allowEmptyShould(true); .allowEmptyShould(true);
} }
private static ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType() { private static ArchCondition<? super JavaMethod> notHaveValueThatIsTheSameAsTheTypeOfTheMethodsFirstParameter() {
return check("not specify only a type that is the same as the method's parameter type", return check("not have a value that is the same as the type of the method's first parameter",
ArchitectureRules::notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType); ArchitectureRules::notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType);
} }
@ -250,15 +258,13 @@ final class ArchitectureRules {
ConditionEvents events) { ConditionEvents events) {
JavaAnnotation<JavaMethod> enumSourceAnnotation = item JavaAnnotation<JavaMethod> enumSourceAnnotation = item
.getAnnotationOfType("org.junit.jupiter.params.provider.EnumSource"); .getAnnotationOfType("org.junit.jupiter.params.provider.EnumSource");
Map<String, Object> properties = enumSourceAnnotation.getProperties(); enumSourceAnnotation.get("value").ifPresent((value) -> {
if (properties.size() == 1 && item.getParameterTypes().size() == 1) { JavaType parameterType = item.getParameterTypes().get(0);
enumSourceAnnotation.get("value").ifPresent((value) -> { if (value.equals(parameterType)) {
if (value.equals(item.getParameterTypes().get(0))) { addViolation(events, item, enumSourceAnnotation.getDescription()
addViolation(events, item, enumSourceAnnotation.getDescription() + " should not specify a value that is the same as the type of the method's first parameter");
+ " should not specify only a value that is the same as the method's parameter type"); }
} });
});
}
} }
private static ArchRule allConfigurationPropertiesBindingBeanMethodsShouldBeStatic() { private static ArchRule allConfigurationPropertiesBindingBeanMethodsShouldBeStatic() {

View File

@ -57,6 +57,8 @@ class ArchitectureCheckTests {
private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.9"; private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.9";
private static final String JUNIT_JUPITER = "org.junit.jupiter:junit-jupiter:5.12.0";
private static final String SPRING_INTEGRATION_JMX = "org.springframework.integration:spring-integration-jmx:6.5.1"; private static final String SPRING_INTEGRATION_JMX = "org.springframework.integration:spring-integration-jmx:6.5.1";
private GradleBuild gradleBuild; private GradleBuild gradleBuild;
@ -270,6 +272,29 @@ class ArchitectureCheckTests {
build(this.gradleBuild.withDependencies(SPRING_CONTEXT), task); build(this.gradleBuild.withDependencies(SPRING_CONTEXT), task);
} }
@Test
void whenEnumSourceValueIsInferredShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_TEST, "junit/enumsource/inferredfromparametertype");
build(this.gradleBuild.withDependencies(JUNIT_JUPITER), Task.CHECK_ARCHITECTURE_TEST);
}
@Test
void whenEnumSourceValueIsNotTheSameAsTypeOfMethodsFirstParameterShouldSucceedAndWriteEmptyReport()
throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_TEST, "junit/enumsource/valuenecessary");
build(this.gradleBuild.withDependencies(JUNIT_JUPITER), Task.CHECK_ARCHITECTURE_TEST);
}
@Test
void whenEnumSourceValueIsSameAsTypeOfMethodsFirstParameterShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_TEST, "junit/enumsource/sameasparametertype");
buildAndFail(this.gradleBuild.withDependencies(JUNIT_JUPITER), Task.CHECK_ARCHITECTURE_TEST,
"method <org.springframework.boot.build.architecture.junit.enumsource.sameasparametertype"
+ ".EnumSourceSameAsParameterType.exampleMethod(org.springframework.boot.build."
+ "architecture.junit.enumsource.sameasparametertype.EnumSourceSameAsParameterType$Example)>",
"should not have a value that is the same as the type of the method's first parameter");
}
private void prepareTask(Task task, String... sourceDirectories) throws IOException { private void prepareTask(Task task, String... sourceDirectories) throws IOException {
for (String sourceDirectory : sourceDirectories) { for (String sourceDirectory : sourceDirectories) {
FileSystemUtils.copyRecursively( FileSystemUtils.copyRecursively(

View File

@ -0,0 +1,34 @@
/*
* Copyright 2012-present 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.boot.build.architecture.junit.enumsource.inferredfromparametertype;
import org.junit.jupiter.params.provider.EnumSource;
class EnumSourceInferredFromParameterType {
@EnumSource
void exampleMethod(Example example) {
}
enum Example {
ONE, TWO, THREE
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2012-present 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.boot.build.architecture.junit.enumsource.sameasparametertype;
import org.junit.jupiter.params.provider.EnumSource;
class EnumSourceSameAsParameterType {
@EnumSource(Example.class)
void exampleMethod(Example example) {
}
enum Example {
ONE, TWO, THREE
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2012-present 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.boot.build.architecture.junit.enumsource.valuenecessary;
import org.junit.jupiter.params.provider.EnumSource;
class EnumSourceValueNecessary {
@EnumSource(Example.class)
void exampleMethod(String thing, Example example) {
}
enum Example {
ONE, TWO, THREE
}
}

View File

@ -112,7 +112,7 @@ class SbomEndpointWebExtensionTests {
} }
@ParameterizedTest @ParameterizedTest
@EnumSource(value = SbomType.class, names = "UNKNOWN", mode = Mode.EXCLUDE) @EnumSource(names = "UNKNOWN", mode = Mode.EXCLUDE)
void shouldAutodetectFormats(SbomType type) throws IOException { void shouldAutodetectFormats(SbomType type) throws IOException {
String content = getSbomContent(type); String content = getSbomContent(type);
assertThat(type.matches(content)).isTrue(); assertThat(type.matches(content)).isTrue();

View File

@ -45,10 +45,9 @@ class JacksonHttpMessageConvertersConfiguration {
static class MappingJackson2HttpMessageConverterConfiguration { static class MappingJackson2HttpMessageConverterConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(value = MappingJackson2HttpMessageConverter.class, @ConditionalOnMissingBean(ignoredType = {
ignoredType = { "org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter",
"org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter", "org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter" })
"org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter" })
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper) { MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
return new MappingJackson2HttpMessageConverter(objectMapper); return new MappingJackson2HttpMessageConverter(objectMapper);
} }

View File

@ -59,7 +59,7 @@ public class ErrorWebFluxAutoConfiguration {
} }
@Bean @Bean
@ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT) @ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
@Order(-1) @Order(-1)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes, public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes,
WebProperties webProperties, ObjectProvider<ViewResolver> viewResolvers, WebProperties webProperties, ObjectProvider<ViewResolver> viewResolvers,

View File

@ -62,8 +62,8 @@ class BatchDataSourceScriptDatabaseInitializerTests {
} }
@ParameterizedTest @ParameterizedTest
@EnumSource(value = DatabaseDriver.class, mode = Mode.EXCLUDE, names = { "CLICKHOUSE", "FIREBIRD", "INFORMIX", @EnumSource(mode = Mode.EXCLUDE, names = { "CLICKHOUSE", "FIREBIRD", "INFORMIX", "JTDS", "PHOENIX", "REDSHIFT",
"JTDS", "PHOENIX", "REDSHIFT", "TERADATA", "TESTCONTAINERS", "UNKNOWN" }) "TERADATA", "TESTCONTAINERS", "UNKNOWN" })
void batchSchemaCanBeLocated(DatabaseDriver driver) throws SQLException { void batchSchemaCanBeLocated(DatabaseDriver driver) throws SQLException {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
BatchProperties properties = new BatchProperties(); BatchProperties properties = new BatchProperties();