Merge branch '3.3.x' into 3.4.x
This commit is contained in:
commit
3f00b08577
|
@ -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,248 +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(),
|
||||
enumSourceShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodParameterType());
|
||||
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<List<ArchRule>, Boolean> whenTrue(Supplier<List<ArchRule>> rules) {
|
||||
return (in) -> (!in) ? Collections.emptyList() : rules.get();
|
||||
}
|
||||
|
||||
private List<String> asDescriptions(List<ArchRule> 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<EvaluationResult> violations = getRules().get()
|
||||
.stream()
|
||||
.map((rule) -> rule.evaluate(javaClasses))
|
||||
.filter(EvaluationResult::hasViolation)
|
||||
.toList();
|
||||
JavaClasses javaClasses = new ClassFileImporter().importPaths(classFilesPaths());
|
||||
List<EvaluationResult> 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<Path> classFilesPaths() {
|
||||
return this.classes.getFiles().stream().map(File::toPath).toList();
|
||||
}
|
||||
|
||||
private Stream<EvaluationResult> evaluate(JavaClasses javaClasses) {
|
||||
return getRules().get().stream().map((rule) -> rule.evaluate(javaClasses));
|
||||
}
|
||||
|
||||
private void writeViolationReport(List<EvaluationResult> 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<JavaMethod> onlyHaveParametersThatWillNotCauseEagerInitialization() {
|
||||
DescribedPredicate<CanBeAnnotated> notAnnotatedWithLazy = DescribedPredicate
|
||||
.not(CanBeAnnotated.Predicates.annotatedWith("org.springframework.context.annotation.Lazy"));
|
||||
DescribedPredicate<JavaClass> 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(onlyInjectEnvironment())
|
||||
.andShould()
|
||||
.beStatic()
|
||||
.allowEmptyShould(true);
|
||||
}
|
||||
|
||||
private ArchCondition<JavaMethod> onlyInjectEnvironment() {
|
||||
return new ArchCondition<>("only inject Environment") {
|
||||
|
||||
@Override
|
||||
public void check(JavaMethod item, ConditionEvents events) {
|
||||
List<JavaParameter> parameters = item.getParameters();
|
||||
for (JavaParameter parameter : parameters) {
|
||||
if (!"org.springframework.core.env.Environment".equals(parameter.getType().getName())) {
|
||||
events.add(SimpleConditionEvent.violated(item,
|
||||
item.getDescription() + " should only inject Environment"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
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<ArchRule> 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<? super JavaMethod> 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<JavaMethod> conditional = item
|
||||
.getAnnotationOfType("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean");
|
||||
Map<String, Object> 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"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
private ArchRule enumSourceShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodParameterType() {
|
||||
return ArchRuleDefinition.methods()
|
||||
.that()
|
||||
.areAnnotatedWith("org.junit.jupiter.params.provider.EnumSource")
|
||||
.should(notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType())
|
||||
.allowEmptyShould(true);
|
||||
}
|
||||
|
||||
private ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType() {
|
||||
return new ArchCondition<>("not specify only a type that is the same as the method's parameter type") {
|
||||
|
||||
@Override
|
||||
public void check(JavaMethod item, ConditionEvents events) {
|
||||
JavaAnnotation<JavaMethod> conditional = item
|
||||
.getAnnotationOfType("org.junit.jupiter.params.provider.EnumSource");
|
||||
Map<String, Object> properties = conditional.getProperties();
|
||||
if (properties.size() == 1 && item.getParameterTypes().size() == 1) {
|
||||
conditional.get("value").ifPresent((value) -> {
|
||||
if (value.equals(item.getParameterTypes().get(0))) {
|
||||
events.add(SimpleConditionEvent.violated(item, conditional.getDescription()
|
||||
+ " should not specify only a value that is the same as the method's parameter type"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING);
|
||||
}
|
||||
|
||||
public void setClasses(FileCollection classes) {
|
||||
|
@ -360,9 +140,7 @@ public abstract class ArchitectureCheck extends DefaultTask {
|
|||
@Internal
|
||||
public abstract Property<Boolean> 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<String> getRuleDescriptions();
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,304 @@
|
|||
/*
|
||||
* 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<ArchRule> 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<ArchRule> standard() {
|
||||
List<ArchRule> rules = new ArrayList<>();
|
||||
rules.add(allPackagesShouldBeFreeOfTangles());
|
||||
rules.add(allBeanPostProcessorBeanMethodsShouldBeStaticAndNotCausePrematureInitialization());
|
||||
rules.add(allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveOnlyInjectEnvironment());
|
||||
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());
|
||||
rules.add(enumSourceShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodParameterType());
|
||||
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<JavaMethod> onlyHaveParametersThatWillNotCauseEagerInitialization() {
|
||||
return check("not have parameters that will cause eager initialization",
|
||||
ArchitectureRules::allBeanPostProcessorBeanMethodsShouldBeStaticAndNotCausePrematureInitialization);
|
||||
}
|
||||
|
||||
private static void allBeanPostProcessorBeanMethodsShouldBeStaticAndNotCausePrematureInitialization(JavaMethod item,
|
||||
ConditionEvents events) {
|
||||
DescribedPredicate<JavaParameter> notAnnotatedWithLazy = DescribedPredicate
|
||||
.not(CanBeAnnotated.Predicates.annotatedWith("org.springframework.context.annotation.Lazy"));
|
||||
DescribedPredicate<JavaClass> 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 allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveOnlyInjectEnvironment() {
|
||||
return methodsThatAreAnnotatedWith("org.springframework.context.annotation.Bean").and()
|
||||
.haveRawReturnType(assignableTo("org.springframework.beans.factory.config.BeanFactoryPostProcessor"))
|
||||
.should(onlyInjectEnvironment())
|
||||
.andShould()
|
||||
.beStatic()
|
||||
.allowEmptyShould(true);
|
||||
}
|
||||
|
||||
private static ArchCondition<JavaMethod> onlyInjectEnvironment() {
|
||||
return check("only inject Environment", ArchitectureRules::onlyInjectEnvironment);
|
||||
}
|
||||
|
||||
private static void onlyInjectEnvironment(JavaMethod item, ConditionEvents events) {
|
||||
if (item.getParameters().stream().anyMatch(ArchitectureRules::isNotEnvironment)) {
|
||||
events.add(SimpleConditionEvent.violated(item, item.getDescription() + " should only inject Environment"));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isNotEnvironment(JavaParameter parameter) {
|
||||
return !"org.springframework.core.env.Environment".equals(parameter.getType().getName());
|
||||
}
|
||||
|
||||
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<JavaCall<?>> resourceUtilsGetURL = hasJavaCallTarget(ownedByResourceUtils())
|
||||
.and(hasJavaCallTarget(hasNameOf("getURL")))
|
||||
.and(hasJavaCallTarget(hasRawStringParameterType()));
|
||||
DescribedPredicate<JavaCall<?>> 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<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType() {
|
||||
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");
|
||||
Map<String, Object> 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 ArchRule enumSourceShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodParameterType() {
|
||||
return ArchRuleDefinition.methods()
|
||||
.that()
|
||||
.areAnnotatedWith("org.junit.jupiter.params.provider.EnumSource")
|
||||
.should(notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType())
|
||||
.allowEmptyShould(true);
|
||||
}
|
||||
|
||||
private static ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType() {
|
||||
return check("not specify only a type that is the same as the method's parameter type",
|
||||
ArchitectureRules::notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType);
|
||||
}
|
||||
|
||||
private static void notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType(JavaMethod item,
|
||||
ConditionEvents events) {
|
||||
JavaAnnotation<JavaMethod> enumSourceAnnotation = item
|
||||
.getAnnotationOfType("org.junit.jupiter.params.provider.EnumSource");
|
||||
Map<String, Object> properties = enumSourceAnnotation.getProperties();
|
||||
if (properties.size() == 1 && item.getParameterTypes().size() == 1) {
|
||||
enumSourceAnnotation.get("value").ifPresent((value) -> {
|
||||
if (value.equals(item.getParameterTypes().get(0))) {
|
||||
events.add(SimpleConditionEvent.violated(item, enumSourceAnnotation.getDescription()
|
||||
+ " should not specify only a value that is the same as the method's parameter 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<HasOwner<JavaClass>> ownedByResourceUtils() {
|
||||
return With.owner(Predicates.type(ResourceUtils.class));
|
||||
}
|
||||
|
||||
private static DescribedPredicate<? super CodeUnitCallTarget> hasNameOf(String name) {
|
||||
return HasName.Predicates.name(name);
|
||||
}
|
||||
|
||||
private static DescribedPredicate<HasParameterTypes> hasRawStringParameterType() {
|
||||
return HasParameterTypes.Predicates.rawParameterTypes(String.class);
|
||||
}
|
||||
|
||||
private static DescribedPredicate<JavaCall<?>> hasJavaCallTarget(
|
||||
DescribedPredicate<? super CodeUnitCallTarget> predicate) {
|
||||
return JavaCall.Predicates.target(predicate);
|
||||
}
|
||||
|
||||
private static DescribedPredicate<JavaClass> notAssignableTo(String... typeNames) {
|
||||
return DescribedPredicate.not(assignableTo(typeNames));
|
||||
}
|
||||
|
||||
private static DescribedPredicate<JavaClass> assignableTo(String... typeNames) {
|
||||
DescribedPredicate<JavaClass> result = null;
|
||||
for (String typeName : typeNames) {
|
||||
DescribedPredicate<JavaClass> assignableTo = Predicates.assignableTo(typeName);
|
||||
result = (result != null) ? result.or(assignableTo) : assignableTo;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static <T> ArchCondition<T> check(String description, BiConsumer<T, ConditionEvents> 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";
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue