From 432681734f6c10d1df8b6f5385b91d6811908572 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 24 Jan 2025 17:40:07 -0800 Subject: [PATCH] Polish ArchitectureCheck Extract rules to a new class and use more helper methods. --- .../build/architecture/ArchitectureCheck.java | 266 +++-------------- .../build/architecture/ArchitectureRules.java | 272 ++++++++++++++++++ 2 files changed, 310 insertions(+), 228 deletions(-) create mode 100644 buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java index b44e9e4a24b..be13c429bae 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java @@ -18,41 +18,22 @@ package org.springframework.boot.build.architecture; import java.io.File; import java.io.IOException; -import java.net.URLDecoder; -import java.net.URLEncoder; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.function.Supplier; -import java.util.stream.Collectors; +import java.util.stream.Stream; -import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.core.domain.JavaAnnotation; -import com.tngtech.archunit.core.domain.JavaCall; -import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaClass.Predicates; import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.domain.JavaMethod; -import com.tngtech.archunit.core.domain.JavaParameter; -import com.tngtech.archunit.core.domain.JavaType; -import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; -import com.tngtech.archunit.core.domain.properties.HasName; -import com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With; -import com.tngtech.archunit.core.domain.properties.HasParameterTypes; import com.tngtech.archunit.core.importer.ClassFileImporter; -import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; -import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.archunit.lang.EvaluationResult; -import com.tngtech.archunit.lang.SimpleConditionEvent; -import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; -import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.Task; +import org.gradle.api.Transformer; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileTree; @@ -69,8 +50,6 @@ import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.SkipWhenEmpty; import org.gradle.api.tasks.TaskAction; -import org.springframework.util.ResourceUtils; - /** * {@link Task} that checks for architecture problems. * @@ -78,6 +57,7 @@ import org.springframework.util.ResourceUtils; * @author Yanming Zhou * @author Scott Frederick * @author Ivan Malutin + * @author Phillip Webb */ public abstract class ArchitectureCheck extends DefaultTask { @@ -85,216 +65,48 @@ public abstract class ArchitectureCheck extends DefaultTask { public ArchitectureCheck() { getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName())); - getProhibitObjectsRequireNonNull().convention(true); - getRules().addAll(allPackagesShouldBeFreeOfTangles(), - allBeanPostProcessorBeanMethodsShouldBeStaticAndHaveParametersThatWillNotCausePrematureInitialization(), - allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters(), - noClassesShouldCallStepVerifierStepVerifyComplete(), - noClassesShouldConfigureDefaultStepVerifierTimeout(), noClassesShouldCallCollectorsToList(), - noClassesShouldCallURLEncoderWithStringEncoding(), noClassesShouldCallURLDecoderWithStringEncoding(), - noClassesShouldLoadResourcesUsingResourceUtils(), noClassesShouldCallStringToUpperCaseWithoutLocale(), - noClassesShouldCallStringToLowerCaseWithoutLocale(), - conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType()); - getRules().addAll(getProhibitObjectsRequireNonNull() - .map((prohibit) -> prohibit ? noClassesShouldCallObjectsRequireNonNull() : Collections.emptyList())); - getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList())); + getRules().addAll(getProhibitObjectsRequireNonNull().convention(true) + .map(whenTrue(ArchitectureRules::noClassesShouldCallObjectsRequireNonNull))); + getRules().addAll(ArchitectureRules.standard()); + getRuleDescriptions().set(getRules().map(this::asDescriptions)); + } + + private Transformer, Boolean> whenTrue(Supplier> rules) { + return (in) -> (!in) ? Collections.emptyList() : rules.get(); + } + + private List asDescriptions(List rules) { + return rules.stream().map(ArchRule::getDescription).toList(); } @TaskAction void checkArchitecture() throws IOException { - JavaClasses javaClasses = new ClassFileImporter() - .importPaths(this.classes.getFiles().stream().map(File::toPath).toList()); - List violations = getRules().get() - .stream() - .map((rule) -> rule.evaluate(javaClasses)) - .filter(EvaluationResult::hasViolation) - .toList(); + JavaClasses javaClasses = new ClassFileImporter().importPaths(classFilesPaths()); + List violations = evaluate(javaClasses).filter(EvaluationResult::hasViolation).toList(); File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile(); - outputFile.getParentFile().mkdirs(); + writeViolationReport(violations, outputFile); if (!violations.isEmpty()) { - StringBuilder report = new StringBuilder(); - for (EvaluationResult violation : violations) { - report.append(violation.getFailureReport()); - report.append(String.format("%n")); - } - Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING); throw new GradleException("Architecture check failed. See '" + outputFile + "' for details."); } - else { - outputFile.createNewFile(); + } + + private List classFilesPaths() { + return this.classes.getFiles().stream().map(File::toPath).toList(); + } + + private Stream evaluate(JavaClasses javaClasses) { + return getRules().get().stream().map((rule) -> rule.evaluate(javaClasses)); + } + + private void writeViolationReport(List violations, File outputFile) throws IOException { + outputFile.getParentFile().mkdirs(); + StringBuilder report = new StringBuilder(); + for (EvaluationResult violation : violations) { + report.append(violation.getFailureReport()); + report.append(String.format("%n")); } - } - - private ArchRule allPackagesShouldBeFreeOfTangles() { - return SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles(); - } - - private ArchRule allBeanPostProcessorBeanMethodsShouldBeStaticAndHaveParametersThatWillNotCausePrematureInitialization() { - return ArchRuleDefinition.methods() - .that() - .areAnnotatedWith("org.springframework.context.annotation.Bean") - .and() - .haveRawReturnType(Predicates.assignableTo("org.springframework.beans.factory.config.BeanPostProcessor")) - .should(onlyHaveParametersThatWillNotCauseEagerInitialization()) - .andShould() - .beStatic() - .allowEmptyShould(true); - } - - private ArchCondition onlyHaveParametersThatWillNotCauseEagerInitialization() { - DescribedPredicate notAnnotatedWithLazy = DescribedPredicate - .not(CanBeAnnotated.Predicates.annotatedWith("org.springframework.context.annotation.Lazy")); - DescribedPredicate notOfASafeType = DescribedPredicate - .not(Predicates.assignableTo("org.springframework.beans.factory.ObjectProvider") - .or(Predicates.assignableTo("org.springframework.context.ApplicationContext")) - .or(Predicates.assignableTo("org.springframework.core.env.Environment"))); - return new ArchCondition<>("not have parameters that will cause eager initialization") { - - @Override - public void check(JavaMethod item, ConditionEvents events) { - item.getParameters() - .stream() - .filter(notAnnotatedWithLazy) - .filter((parameter) -> notOfASafeType.test(parameter.getRawType())) - .forEach((parameter) -> events.add(SimpleConditionEvent.violated(parameter, - parameter.getDescription() + " will cause eager initialization as it is " - + notAnnotatedWithLazy.getDescription() + " and is " - + notOfASafeType.getDescription()))); - } - - }; - } - - private ArchRule allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters() { - return ArchRuleDefinition.methods() - .that() - .areAnnotatedWith("org.springframework.context.annotation.Bean") - .and() - .haveRawReturnType( - Predicates.assignableTo("org.springframework.beans.factory.config.BeanFactoryPostProcessor")) - .should(haveNoParameters()) - .andShould() - .beStatic() - .allowEmptyShould(true); - } - - private ArchCondition haveNoParameters() { - return new ArchCondition<>("have no parameters") { - - @Override - public void check(JavaMethod item, ConditionEvents events) { - List parameters = item.getParameters(); - if (!parameters.isEmpty()) { - events - .add(SimpleConditionEvent.violated(item, item.getDescription() + " should have no parameters")); - } - } - - }; - } - - private ArchRule noClassesShouldCallStringToLowerCaseWithoutLocale() { - return ArchRuleDefinition.noClasses() - .should() - .callMethod(String.class, "toLowerCase") - .because("String.toLowerCase(Locale.ROOT) should be used instead"); - } - - private ArchRule noClassesShouldCallStringToUpperCaseWithoutLocale() { - return ArchRuleDefinition.noClasses() - .should() - .callMethod(String.class, "toUpperCase") - .because("String.toUpperCase(Locale.ROOT) should be used instead"); - } - - private ArchRule noClassesShouldCallStepVerifierStepVerifyComplete() { - return ArchRuleDefinition.noClasses() - .should() - .callMethod("reactor.test.StepVerifier$Step", "verifyComplete") - .because("it can block indefinitely and expectComplete().verify(Duration) should be used instead"); - } - - private ArchRule noClassesShouldConfigureDefaultStepVerifierTimeout() { - return ArchRuleDefinition.noClasses() - .should() - .callMethod("reactor.test.StepVerifier", "setDefaultTimeout", "java.time.Duration") - .because("expectComplete().verify(Duration) should be used instead"); - } - - private ArchRule noClassesShouldCallCollectorsToList() { - return ArchRuleDefinition.noClasses() - .should() - .callMethod(Collectors.class, "toList") - .because("java.util.stream.Stream.toList() should be used instead"); - } - - private ArchRule noClassesShouldCallURLEncoderWithStringEncoding() { - return ArchRuleDefinition.noClasses() - .should() - .callMethod(URLEncoder.class, "encode", String.class, String.class) - .because("java.net.URLEncoder.encode(String s, Charset charset) should be used instead"); - } - - private ArchRule noClassesShouldCallURLDecoderWithStringEncoding() { - return ArchRuleDefinition.noClasses() - .should() - .callMethod(URLDecoder.class, "decode", String.class, String.class) - .because("java.net.URLDecoder.decode(String s, Charset charset) should be used instead"); - } - - private ArchRule noClassesShouldLoadResourcesUsingResourceUtils() { - return ArchRuleDefinition.noClasses() - .should() - .callMethodWhere(JavaCall.Predicates.target(With.owner(Predicates.type(ResourceUtils.class))) - .and(JavaCall.Predicates.target(HasName.Predicates.name("getURL"))) - .and(JavaCall.Predicates.target(HasParameterTypes.Predicates.rawParameterTypes(String.class))) - .or(JavaCall.Predicates.target(With.owner(Predicates.type(ResourceUtils.class))) - .and(JavaCall.Predicates.target(HasName.Predicates.name("getFile"))) - .and(JavaCall.Predicates.target(HasParameterTypes.Predicates.rawParameterTypes(String.class))))) - .because("org.springframework.boot.io.ApplicationResourceLoader should be used instead"); - } - - private List noClassesShouldCallObjectsRequireNonNull() { - return List.of( - ArchRuleDefinition.noClasses() - .should() - .callMethod(Objects.class, "requireNonNull", Object.class, String.class) - .because("org.springframework.utils.Assert.notNull(Object, String) should be used instead"), - ArchRuleDefinition.noClasses() - .should() - .callMethod(Objects.class, "requireNonNull", Object.class, Supplier.class) - .because("org.springframework.utils.Assert.notNull(Object, Supplier) should be used instead")); - } - - private ArchRule conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType() { - return ArchRuleDefinition.methods() - .that() - .areAnnotatedWith("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean") - .should(notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType()) - .allowEmptyShould(true); - } - - private ArchCondition notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType() { - return new ArchCondition<>("not specify only a type that is the same as the method's return type") { - - @Override - public void check(JavaMethod item, ConditionEvents events) { - JavaAnnotation conditional = item - .getAnnotationOfType("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean"); - Map properties = conditional.getProperties(); - if (!properties.containsKey("type") && !properties.containsKey("name")) { - conditional.get("value").ifPresent((value) -> { - JavaType[] types = (JavaType[]) value; - if (types.length == 1 && item.getReturnType().equals(types[0])) { - events.add(SimpleConditionEvent.violated(item, conditional.getDescription() - + " should not specify only a value that is the same as the method's return type")); - } - }); - } - } - - }; + Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); } public void setClasses(FileCollection classes) { @@ -328,9 +140,7 @@ public abstract class ArchitectureCheck extends DefaultTask { @Internal public abstract Property getProhibitObjectsRequireNonNull(); - @Input - // The rules themselves can't be an input as they aren't serializable so we use - // their descriptions instead + @Input // Use descriptions as input since rules aren't serializable abstract ListProperty getRuleDescriptions(); } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java new file mode 100644 index 00000000000..bd34b1a59db --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java @@ -0,0 +1,272 @@ +/* + * Copyright 2024 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.net.URLDecoder; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.AccessTarget.CodeUnitCallTarget; +import com.tngtech.archunit.core.domain.JavaAnnotation; +import com.tngtech.archunit.core.domain.JavaCall; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClass.Predicates; +import com.tngtech.archunit.core.domain.JavaMethod; +import com.tngtech.archunit.core.domain.JavaParameter; +import com.tngtech.archunit.core.domain.JavaType; +import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; +import com.tngtech.archunit.core.domain.properties.HasName; +import com.tngtech.archunit.core.domain.properties.HasOwner; +import com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With; +import com.tngtech.archunit.core.domain.properties.HasParameterTypes; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; +import com.tngtech.archunit.lang.syntax.elements.ClassesShould; +import com.tngtech.archunit.lang.syntax.elements.GivenMethodsConjunction; +import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition; + +import org.springframework.util.ResourceUtils; + +/** + * Factory used to create {@link ArchRule architecture rules}. + * + * @author Andy Wilkinson + * @author Yanming Zhou + * @author Scott Frederick + * @author Ivan Malutin + * @author Phillip Webb + */ +final class ArchitectureRules { + + private ArchitectureRules() { + } + + static List noClassesShouldCallObjectsRequireNonNull() { + return List.of( + noClassesShould().callMethod(Objects.class, "requireNonNull", Object.class, String.class) + .because(shouldUse("org.springframework.utils.Assert.notNull(Object, String)")), + noClassesShould().callMethod(Objects.class, "requireNonNull", Object.class, Supplier.class) + .because(shouldUse("org.springframework.utils.Assert.notNull(Object, Supplier)"))); + } + + static List standard() { + List rules = new ArrayList<>(); + rules.add(allPackagesShouldBeFreeOfTangles()); + rules.add(allBeanPostProcessorBeanMethodsShouldBeStaticAndNotCausePrematureInitialization()); + rules.add(allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters()); + rules.add(noClassesShouldCallStepVerifierStepVerifyComplete()); + rules.add(noClassesShouldConfigureDefaultStepVerifierTimeout()); + rules.add(noClassesShouldCallCollectorsToList()); + rules.add(noClassesShouldCallURLEncoderWithStringEncoding()); + rules.add(noClassesShouldCallURLDecoderWithStringEncoding()); + rules.add(noClassesShouldLoadResourcesUsingResourceUtils()); + rules.add(noClassesShouldCallStringToUpperCaseWithoutLocale()); + rules.add(noClassesShouldCallStringToLowerCaseWithoutLocale()); + rules.add(conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType()); + return List.copyOf(rules); + } + + private static ArchRule allPackagesShouldBeFreeOfTangles() { + return SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles(); + } + + private static ArchRule allBeanPostProcessorBeanMethodsShouldBeStaticAndNotCausePrematureInitialization() { + return methodsThatAreAnnotatedWith("org.springframework.context.annotation.Bean").and() + .haveRawReturnType(assignableTo("org.springframework.beans.factory.config.BeanPostProcessor")) + .should(onlyHaveParametersThatWillNotCauseEagerInitialization()) + .andShould() + .beStatic() + .allowEmptyShould(true); + } + + private static ArchCondition onlyHaveParametersThatWillNotCauseEagerInitialization() { + return check("not have parameters that will cause eager initialization", + ArchitectureRules::allBeanPostProcessorBeanMethodsShouldBeStaticAndNotCausePrematureInitialization); + } + + private static void allBeanPostProcessorBeanMethodsShouldBeStaticAndNotCausePrematureInitialization(JavaMethod item, + ConditionEvents events) { + DescribedPredicate notAnnotatedWithLazy = DescribedPredicate + .not(CanBeAnnotated.Predicates.annotatedWith("org.springframework.context.annotation.Lazy")); + DescribedPredicate notOfASafeType = notAssignableTo( + "org.springframework.beans.factory.ObjectProvider", "org.springframework.context.ApplicationContext", + "org.springframework.core.env.Environment"); + item.getParameters() + .stream() + .filter(notAnnotatedWithLazy) + .filter((parameter) -> notOfASafeType.test(parameter.getRawType())) + .forEach((parameter) -> events.add(SimpleConditionEvent.violated(parameter, + parameter.getDescription() + " will cause eager initialization as it is " + + notAnnotatedWithLazy.getDescription() + " and is " + notOfASafeType.getDescription()))); + } + + private static ArchRule allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters() { + return methodsThatAreAnnotatedWith("org.springframework.context.annotation.Bean").and() + .haveRawReturnType(assignableTo("org.springframework.beans.factory.config.BeanFactoryPostProcessor")) + .should(haveNoParameters()) + .andShould() + .beStatic() + .allowEmptyShould(true); + } + + private static ArchRule noClassesShouldCallStepVerifierStepVerifyComplete() { + return noClassesShould().callMethod("reactor.test.StepVerifier$Step", "verifyComplete") + .because("it can block indefinitely and " + shouldUse("expectComplete().verify(Duration)")); + } + + private static ArchRule noClassesShouldConfigureDefaultStepVerifierTimeout() { + return noClassesShould().callMethod("reactor.test.StepVerifier", "setDefaultTimeout", "java.time.Duration") + .because(shouldUse("expectComplete().verify(Duration)")); + } + + private static ArchRule noClassesShouldCallCollectorsToList() { + return noClassesShould().callMethod(Collectors.class, "toList") + .because(shouldUse("java.util.stream.Stream.toList()")); + } + + private static ArchRule noClassesShouldCallURLEncoderWithStringEncoding() { + return noClassesShould().callMethod(URLEncoder.class, "encode", String.class, String.class) + .because(shouldUse("java.net.URLEncoder.encode(String s, Charset charset)")); + } + + private static ArchRule noClassesShouldCallURLDecoderWithStringEncoding() { + return noClassesShould().callMethod(URLDecoder.class, "decode", String.class, String.class) + .because(shouldUse("java.net.URLDecoder.decode(String s, Charset charset)")); + } + + private static ArchRule noClassesShouldLoadResourcesUsingResourceUtils() { + DescribedPredicate> resourceUtilsGetURL = hasJavaCallTarget(ownedByResourceUtils()) + .and(hasJavaCallTarget(hasNameOf("getURL"))) + .and(hasJavaCallTarget(hasRawStringParameterType())); + DescribedPredicate> resourceUtilsGetFile = hasJavaCallTarget(ownedByResourceUtils()) + .and(hasJavaCallTarget(hasNameOf("getFile"))) + .and(hasJavaCallTarget(hasRawStringParameterType())); + return noClassesShould().callMethodWhere(resourceUtilsGetURL.or(resourceUtilsGetFile)) + .because(shouldUse("org.springframework.boot.io.ApplicationResourceLoader")); + } + + private static ArchRule noClassesShouldCallStringToUpperCaseWithoutLocale() { + return noClassesShould().callMethod(String.class, "toUpperCase") + .because(shouldUse("String.toUpperCase(Locale.ROOT)")); + } + + private static ArchRule noClassesShouldCallStringToLowerCaseWithoutLocale() { + return noClassesShould().callMethod(String.class, "toLowerCase") + .because(shouldUse("String.toLowerCase(Locale.ROOT)")); + } + + private static ArchRule conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType() { + return methodsThatAreAnnotatedWith("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean") + .should(notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType()) + .allowEmptyShould(true); + } + + private static ArchCondition notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType() { + return check("not specify only a type that is the same as the method's return type", (item, events) -> { + JavaAnnotation conditionalAnnotation = item + .getAnnotationOfType("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean"); + Map properties = conditionalAnnotation.getProperties(); + if (!properties.containsKey("type") && !properties.containsKey("name")) { + conditionalAnnotation.get("value").ifPresent((value) -> { + if (containsOnlySingleType((JavaType[]) value, item.getReturnType())) { + events.add(SimpleConditionEvent.violated(item, conditionalAnnotation.getDescription() + + " should not specify only a value that is the same as the method's return type")); + } + }); + } + }); + } + + private static boolean containsOnlySingleType(JavaType[] types, JavaType type) { + return types.length == 1 && type.equals(types[0]); + } + + private static ClassesShould noClassesShould() { + return ArchRuleDefinition.noClasses().should(); + } + + private static GivenMethodsConjunction methodsThatAreAnnotatedWith(String annotation) { + return ArchRuleDefinition.methods().that().areAnnotatedWith(annotation); + } + + private static DescribedPredicate> ownedByResourceUtils() { + return With.owner(Predicates.type(ResourceUtils.class)); + } + + private static DescribedPredicate hasNameOf(String name) { + return HasName.Predicates.name(name); + } + + private static DescribedPredicate hasRawStringParameterType() { + return HasParameterTypes.Predicates.rawParameterTypes(String.class); + } + + private static DescribedPredicate> hasJavaCallTarget( + DescribedPredicate predicate) { + return JavaCall.Predicates.target(predicate); + } + + private static DescribedPredicate notAssignableTo(String... typeNames) { + return DescribedPredicate.not(assignableTo(typeNames)); + } + + private static DescribedPredicate assignableTo(String... typeNames) { + DescribedPredicate result = null; + for (String typeName : typeNames) { + DescribedPredicate assignableTo = Predicates.assignableTo(typeName); + result = (result != null) ? result.or(assignableTo) : assignableTo; + } + return result; + } + + private static ArchCondition haveNoParameters() { + return check("have no parameters", ArchitectureRules::haveNoParameters); + } + + private static void haveNoParameters(JavaMethod item, ConditionEvents events) { + List parameters = item.getParameters(); + if (!parameters.isEmpty()) { + events.add(SimpleConditionEvent.violated(item, item.getDescription() + " should have no parameters")); + } + } + + private static ArchCondition check(String description, BiConsumer check) { + return new ArchCondition<>(description) { + + @Override + public void check(T item, ConditionEvents events) { + check.accept(item, events); + } + + }; + } + + private static String shouldUse(String string) { + return string + " should be used instead"; + } + +}