From 142a3983a09aaee0344cd007dd13db16385675dd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 6 Jul 2023 11:08:23 +0100 Subject: [PATCH] Check that AutoConfiguration classes are listed in imports file Closes gh-36253 --- .../build/architecture/ArchitectureCheck.java | 31 ++++-- .../architecture/ArchitecturePlugin.java | 1 + .../AutoConfigurationPlugin.java | 99 +++++++++++++++++-- 3 files changed, 119 insertions(+), 12 deletions(-) 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 34335360f2e..9c436d744f8 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 @@ -23,7 +23,6 @@ import java.nio.file.Files; import java.nio.file.StandardOpenOption; import java.util.List; import java.util.stream.Collectors; -import java.util.stream.Stream; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.JavaClass; @@ -46,9 +45,12 @@ import org.gradle.api.Task; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileTree; +import org.gradle.api.provider.ListProperty; import org.gradle.api.tasks.IgnoreEmptyDirectories; +import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.PathSensitive; import org.gradle.api.tasks.PathSensitivity; @@ -66,17 +68,21 @@ public abstract class ArchitectureCheck extends DefaultTask { public ArchitectureCheck() { getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName())); + getRules().addAll(allPackagesShouldBeFreeOfTangles(), + allBeanPostProcessorBeanMethodsShouldBeStaticAndHaveParametersThatWillNotCausePrematureInitialization(), + allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters(), + noClassesShouldCallStepVerifierStepVerifyComplete(), + noClassesShouldConfigureDefaultStepVerifierTimeout()); + getRuleDescriptions() + .set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).collect(Collectors.toList()))); } @TaskAction void checkArchitecture() throws IOException { JavaClasses javaClasses = new ClassFileImporter() .importPaths(this.classes.getFiles().stream().map(File::toPath).collect(Collectors.toList())); - List violations = Stream.of(allPackagesShouldBeFreeOfTangles(), - allBeanPostProcessorBeanMethodsShouldBeStaticAndHaveParametersThatWillNotCausePrematureInitialization(), - allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters(), - noClassesShouldCallStepVerifierStepVerifyComplete(), - noClassesShouldConfigureDefaultStepVerifierTimeout()) + List violations = getRules().get() + .stream() .map((rule) -> rule.evaluate(javaClasses)) .filter(EvaluationResult::hasViolation) .collect(Collectors.toList()); @@ -196,7 +202,20 @@ public abstract class ArchitectureCheck extends DefaultTask { return this.classes.getAsFileTree(); } + @Optional + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract DirectoryProperty getResourcesDirectory(); + @OutputDirectory public abstract DirectoryProperty getOutputDirectory(); + @Internal + public abstract ListProperty getRules(); + + @Input + // The rules themselves can't be an input as they aren't serializable so we use their + // descriptions instead + abstract ListProperty getRuleDescriptions(); + } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java index 102ddf72e00..afeaf631170 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java @@ -50,6 +50,7 @@ public class ArchitecturePlugin implements Plugin { .register("checkArchitecture" + StringUtils.capitalize(sourceSet.getName()), ArchitectureCheck.class, (task) -> { task.setClasses(sourceSet.getOutput().getClassesDirs()); + task.getResourcesDirectory().set(sourceSet.getOutput().getResourcesDir()); task.setDescription("Checks the architecture of the classes of the " + sourceSet.getName() + " source set."); task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java index f3e751e48b6..73c4ab1aacd 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java @@ -17,29 +17,50 @@ package org.springframework.boot.build.autoconfigure; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; +import java.util.List; import java.util.concurrent.Callable; +import com.tngtech.archunit.core.domain.JavaClass; +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 org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.SourceSet; import org.springframework.boot.build.DeployedPlugin; +import org.springframework.boot.build.architecture.ArchitectureCheck; +import org.springframework.boot.build.architecture.ArchitecturePlugin; import org.springframework.boot.build.context.properties.ConfigurationPropertiesPlugin; /** * {@link Plugin} for projects that define auto-configuration. When applied, the plugin - * applies the {@link DeployedPlugin}. Additionally, it reacts to the presence of the - * {@link JavaPlugin} by: + * applies the {@link DeployedPlugin}. Additionally, when the {@link JavaPlugin} is + * applied it: * *
    - *
  • Applying the {@link ConfigurationPropertiesPlugin}. - *
  • Adding a dependency on the auto-configuration annotation processor. - *
  • Defining a task that produces metadata describing the auto-configuration. The - * metadata is made available as an artifact in the + *
  • Applies the {@link ConfigurationPropertiesPlugin}. + *
  • Adds a dependency on the auto-configuration annotation processor. + *
  • Defines a task that produces metadata describing the auto-configuration. The + * metadata is made available as an artifact in the {@code autoConfigurationMetadata} + * configuration. + *
  • Reacts to the {@link ArchitecturePlugin} being applied and: + *
      + *
    • Adds a rule to the {@code checkArchitectureMain} task to verify that all + * {@code AutoConfiguration} classes are listed in the {@code AutoConfiguration.imports} + * file. + *
    *
* * @author Andy Wilkinson @@ -52,6 +73,8 @@ public class AutoConfigurationPlugin implements Plugin { */ public static final String AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME = "autoConfigurationMetadata"; + private static final String AUTO_CONFIGURATION_IMPORTS_PATH = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"; + @Override public void apply(Project project) { project.getPlugins().apply(DeployedPlugin.class); @@ -80,7 +103,71 @@ public class AutoConfigurationPlugin implements Plugin { project.provider((Callable) task::getOutputFile), (artifact) -> artifact.builtBy(task)); }); + project.getPlugins().withType(ArchitecturePlugin.class, (architecturePlugin) -> { + project.getTasks().named("checkArchitectureMain", ArchitectureCheck.class).configure((task) -> { + SourceSet main = project.getExtensions() + .getByType(JavaPluginExtension.class) + .getSourceSets() + .getByName(SourceSet.MAIN_SOURCE_SET_NAME); + File resourcesDirectory = main.getOutput().getResourcesDir(); + task.dependsOn(main.getProcessResourcesTaskName()); + task.getInputs().files(resourcesDirectory).optional().withPathSensitivity(PathSensitivity.RELATIVE); + task.getRules() + .add(allClassesAnnotatedWithAutoConfigurationShouldBeListedInAutoConfigurationImports( + autoConfigurationImports(project, resourcesDirectory))); + }); + }); }); } + private ArchRule allClassesAnnotatedWithAutoConfigurationShouldBeListedInAutoConfigurationImports( + Provider imports) { + return ArchRuleDefinition.classes() + .that() + .areAnnotatedWith("org.springframework.boot.autoconfigure.AutoConfiguration") + .should(beListedInAutoConfigurationImports(imports)) + .allowEmptyShould(true); + } + + private ArchCondition beListedInAutoConfigurationImports(Provider imports) { + return new ArchCondition("be listed in " + AUTO_CONFIGURATION_IMPORTS_PATH) { + + @Override + public void check(JavaClass item, ConditionEvents events) { + AutoConfigurationImports autoConfigurationImports = imports.get(); + if (!autoConfigurationImports.imports.contains(item.getName())) { + events.add(SimpleConditionEvent.violated(item, + item.getName() + " was not listed in " + autoConfigurationImports.importsFile)); + } + } + + }; + } + + private Provider autoConfigurationImports(Project project, File resourcesDirectory) { + Path importsFile = new File(resourcesDirectory, AUTO_CONFIGURATION_IMPORTS_PATH).toPath(); + return project.provider(() -> { + try { + return new AutoConfigurationImports(project.getProjectDir().toPath().relativize(importsFile), + Files.readAllLines(importsFile)); + } + catch (IOException ex) { + throw new RuntimeException("Failed to read AutoConfiguration.imports", ex); + } + }); + } + + private static final class AutoConfigurationImports { + + private final Path importsFile; + + private final List imports; + + private AutoConfigurationImports(Path importsFile, List imports) { + this.importsFile = importsFile; + this.imports = imports; + } + + } + }