Skip to content
Open
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 @@ -26,7 +26,6 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
import java.util.stream.Stream;
Expand Down Expand Up @@ -60,6 +59,8 @@
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.VerificationException;

import org.springframework.boot.build.architecture.ArchitectureCheckAnnotations.Annotation;

/**
* {@link Task} that checks for architecture problems.
*
Expand All @@ -72,27 +73,26 @@
*/
public abstract class ArchitectureCheck extends DefaultTask {

static final String CONDITIONAL_ON_CLASS = "ConditionalOnClass";

static final String DEPRECATED_CONFIGURATION_PROPERTY = "DeprecatedConfigurationProperty";

private static final String CONDITIONAL_ON_CLASS_ANNOTATION = "org.springframework.boot.autoconfigure.condition.ConditionalOnClass";

private static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.context.properties.DeprecatedConfigurationProperty";

private FileCollection classes;

public ArchitectureCheck() {
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
getAnnotationClasses().convention(Map.of(CONDITIONAL_ON_CLASS, CONDITIONAL_ON_CLASS_ANNOTATION,
DEPRECATED_CONFIGURATION_PROPERTY, DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION));
getAnnotationClasses().convention(ArchitectureCheckAnnotations.asMap());
getRules().addAll(getProhibitObjectsRequireNonNull().convention(true)
.map(whenTrue(ArchitectureRules::noClassesShouldCallObjectsRequireNonNull)));
getRules().addAll(ArchitectureRules.standard());
getRules().addAll(whenMainSources(() -> ArchitectureRules
.beanMethods(annotationClassFor(CONDITIONAL_ON_CLASS, CONDITIONAL_ON_CLASS_ANNOTATION))));
getRules().addAll(whenMainSources(() -> ArchitectureRules.configurationProperties(
annotationClassFor(DEPRECATED_CONFIGURATION_PROPERTY, DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION))));
getRules().addAll(whenMainSources(() -> ArchitectureRules.beanMethods(
ArchitectureCheckAnnotations.classFor(getAnnotationClasses().get(), Annotation.CONDITIONAL_ON_CLASS))));
getRules().addAll(whenMainSources(() -> ArchitectureRules.conditionalOnMissingBean(ArchitectureCheckAnnotations
.classFor(getAnnotationClasses().get(), Annotation.CONDITIONAL_ON_MISSING_BEAN))));
getRules().addAll(whenMainSources(() -> ArchitectureRules.configurationProperties(ArchitectureCheckAnnotations
.classFor(getAnnotationClasses().get(), Annotation.CONFIGURATION_PROPERTIES))));
getRules()
.addAll(whenMainSources(() -> ArchitectureRules.configurationPropertiesBinding(ArchitectureCheckAnnotations
.classFor(getAnnotationClasses().get(), Annotation.CONFIGURATION_PROPERTIES_BINDING))));
getRules().addAll(
whenMainSources(() -> ArchitectureRules.configurationPropertiesDeprecation(ArchitectureCheckAnnotations
.classFor(getAnnotationClasses().get(), Annotation.DEPRECATED_CONFIGURATION_PROPERTY))));
getRuleDescriptions().set(getRules().map(this::asDescriptions));
}

Expand All @@ -110,10 +110,6 @@ private List<String> asDescriptions(List<ArchRule> rules) {
return rules.stream().map(ArchRule::getDescription).toList();
}

private String annotationClassFor(String name, String defaultValue) {
return getAnnotationClasses().get().getOrDefault(name, defaultValue);
}

@TaskAction
void checkArchitecture() throws Exception {
withCompileClasspath(() -> {
Expand Down Expand Up @@ -204,7 +200,7 @@ final FileTree getInputClasses() {
@Input // Use descriptions as input since rules aren't serializable
abstract ListProperty<String> getRuleDescriptions();

@Input
@Internal
abstract MapProperty<String, String> getAnnotationClasses();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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;

import java.util.Map;

/**
* Annotations used in architecture checks, which can be overridden in architecture rule
* tests.
*
* @author Scott Frederick
*/
public final class ArchitectureCheckAnnotations {

enum Annotation {

CONDITIONAL_ON_CLASS, CONDITIONAL_ON_MISSING_BEAN, CONFIGURATION_PROPERTIES, DEPRECATED_CONFIGURATION_PROPERTY,
CONFIGURATION_PROPERTIES_BINDING

}

private ArchitectureCheckAnnotations() {
}

private static final Map<String, String> annotationNameToClassName = Map.of(Annotation.CONDITIONAL_ON_CLASS.name(),
"org.springframework.boot.autoconfigure.condition.ConditionalOnClass",
Annotation.CONDITIONAL_ON_MISSING_BEAN.name(),
"org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean",
Annotation.CONFIGURATION_PROPERTIES.name(),
"org.springframework.boot.context.properties.ConfigurationProperties",
Annotation.DEPRECATED_CONFIGURATION_PROPERTY.name(),
"org.springframework.boot.context.properties.DeprecatedConfigurationProperty",
Annotation.CONFIGURATION_PROPERTIES_BINDING.name(),
"org.springframework.boot.context.properties.ConfigurationPropertiesBinding");

static Map<String, String> asMap() {
return annotationNameToClassName;
}

static String classFor(Map<String, String> annotationProperty, Annotation annotation) {
String name = annotation.name();
return annotationProperty.getOrDefault(name, asMap().get(name));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,32 @@ static List<ArchRule> standard() {
rules.add(noClassesShouldLoadResourcesUsingResourceUtils());
rules.add(noClassesShouldCallStringToUpperCaseWithoutLocale());
rules.add(noClassesShouldCallStringToLowerCaseWithoutLocale());
rules.add(conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType());
rules.add(enumSourceShouldNotHaveValueThatIsTheSameAsTypeOfMethodsFirstParameter());
rules.add(classLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute());
rules.add(methodLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute());
rules.add(conditionsShouldNotBePublic());
rules.add(allConfigurationPropertiesBindingBeanMethodsShouldBeStatic());
return List.copyOf(rules);
}

static List<ArchRule> beanMethods(String annotationName) {
static List<ArchRule> beanMethods(String annotationClass) {
return List.of(allBeanMethodsShouldReturnNonPrivateType(),
allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(annotationName));
allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(annotationClass));
}

static List<ArchRule> configurationProperties(String annotationName) {
return List.of(allDeprecatedConfigurationPropertiesShouldIncludeSince(annotationName));
static List<ArchRule> conditionalOnMissingBean(String annotationClass) {
return List
.of(conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType(annotationClass));
}

static List<ArchRule> configurationProperties(String annotationClass) {
return List.of(classLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass),
methodLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass));
}

static List<ArchRule> configurationPropertiesBinding(String annotationClass) {
return List.of(allConfigurationPropertiesBindingBeanMethodsShouldBeStatic(annotationClass));
}

static List<ArchRule> configurationPropertiesDeprecation(String annotationClass) {
return List.of(allDeprecatedConfigurationPropertiesShouldIncludeSince(annotationClass));
}

private static ArchRule allBeanMethodsShouldReturnNonPrivateType() {
Expand Down Expand Up @@ -247,16 +257,17 @@ private static ArchRule noClassesShouldCallStringToLowerCaseWithoutLocale() {
.because(shouldUse("String.toLowerCase(Locale.ROOT)"));
}

private static ArchRule conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType() {
return methodsThatAreAnnotatedWith("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean")
.should(notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType())
private static ArchRule conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType(
String annotation) {
return methodsThatAreAnnotatedWith(annotation)
.should(notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType(annotation))
.allowEmptyShould(true);
}

private static ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType() {
private static ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType(
String annotation) {
return check("not specify only a type that is the same as the method's return type", (item, events) -> {
JavaAnnotation<JavaMethod> conditionalAnnotation = item
.getAnnotationOfType("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean");
JavaAnnotation<JavaMethod> conditionalAnnotation = item.getAnnotationOfType(annotation);
Map<String, Object> properties = conditionalAnnotation.getProperties();
if (!hasProperty("type", properties) && !hasProperty("name", properties)) {
conditionalAnnotation.get("value").ifPresent((value) -> {
Expand All @@ -274,7 +285,7 @@ private static boolean hasProperty(String name, Map<String, Object> properties)
if (property == null) {
return false;
}
return !property.getClass().isArray() || ((Object[]) property).length > 0;
return (property.getClass().isArray()) ? ((Object[]) property).length > 0 : !property.toString().isEmpty();
}

private static ArchRule enumSourceShouldNotHaveValueThatIsTheSameAsTypeOfMethodsFirstParameter() {
Expand Down Expand Up @@ -303,33 +314,37 @@ private static void notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType(Jav
});
}

private static ArchRule classLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute() {
private static ArchRule classLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(
String annotationClass) {
return ArchRuleDefinition.classes()
.that()
.areAnnotatedWith("org.springframework.boot.context.properties.ConfigurationProperties")
.should(notSpecifyOnlyPrefixAttributeOfConfigurationProperties())
.areAnnotatedWith(annotationClass)
.should(notSpecifyOnlyPrefixAttributeOfConfigurationProperties(annotationClass))
.allowEmptyShould(true);
}

private static ArchRule methodLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute() {
private static ArchRule methodLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(
String annotationClass) {
return ArchRuleDefinition.methods()
.that()
.areAnnotatedWith("org.springframework.boot.context.properties.ConfigurationProperties")
.should(notSpecifyOnlyPrefixAttributeOfConfigurationProperties())
.areAnnotatedWith(annotationClass)
.should(notSpecifyOnlyPrefixAttributeOfConfigurationProperties(annotationClass))
.allowEmptyShould(true);
}

private static ArchCondition<? super HasAnnotations<?>> notSpecifyOnlyPrefixAttributeOfConfigurationProperties() {
return check("not specify only prefix attribute of @ConfigurationProperties",
ArchitectureRules::notSpecifyOnlyPrefixAttributeOfConfigurationProperties);
private static ArchCondition<? super HasAnnotations<?>> notSpecifyOnlyPrefixAttributeOfConfigurationProperties(
String annotationClass) {
return check("not specify only prefix attribute of @ConfigurationProperties", (item,
events) -> notSpecifyOnlyPrefixAttributeOfConfigurationProperties(annotationClass, item, events));
}

private static void notSpecifyOnlyPrefixAttributeOfConfigurationProperties(HasAnnotations<?> item,
ConditionEvents events) {
JavaAnnotation<?> configurationPropertiesAnnotation = item
.getAnnotationOfType("org.springframework.boot.context.properties.ConfigurationProperties");
private static void notSpecifyOnlyPrefixAttributeOfConfigurationProperties(String annotationClass,
HasAnnotations<?> item, ConditionEvents events) {
JavaAnnotation<?> configurationPropertiesAnnotation = item.getAnnotationOfType(annotationClass);
Map<String, Object> properties = configurationPropertiesAnnotation.getProperties();
if (properties.size() == 1 && properties.containsKey("prefix")) {
if (hasProperty("prefix", properties) && !hasProperty("value", properties)
&& properties.get("ignoreInvalidFields").equals(false)
&& properties.get("ignoreUnknownFields").equals(true)) {
addViolation(events, item, configurationPropertiesAnnotation.getDescription()
+ " should specify implicit 'value' attribute other than explicit 'prefix' attribute");
}
Expand All @@ -349,9 +364,9 @@ private static ArchRule conditionsShouldNotBePublic() {
.allowEmptyShould(true);
}

private static ArchRule allConfigurationPropertiesBindingBeanMethodsShouldBeStatic() {
private static ArchRule allConfigurationPropertiesBindingBeanMethodsShouldBeStatic(String annotationClass) {
return methodsThatAreAnnotatedWith("org.springframework.context.annotation.Bean").and()
.areAnnotatedWith("org.springframework.boot.context.properties.ConfigurationPropertiesBinding")
.areAnnotatedWith(annotationClass)
.should()
.beStatic()
.allowEmptyShould(true);
Expand Down
Loading
Loading