Browse Source

Improve checking of auto-configuration

3.3.x
Andy Wilkinson 8 months ago committed by Phillip Webb
parent
commit
5464812f80
  1. 135
      buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationClass.java
  2. 66
      buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationImportsTask.java
  3. 122
      buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java
  4. 212
      buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/CheckAutoConfigurationClasses.java
  5. 126
      buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/CheckAutoConfigurationImports.java

135
buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationClass.java

@ -0,0 +1,135 @@ @@ -0,0 +1,135 @@
/*
* Copyright 2025 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.autoconfigure;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.springframework.asm.AnnotationVisitor;
import org.springframework.asm.ClassReader;
import org.springframework.asm.ClassVisitor;
import org.springframework.asm.SpringAsmInfo;
import org.springframework.asm.Type;
/**
* An {@code @AutoConfiguration} class.
*
* @param name name of the auto-configuration class
* @param before values of the {@code before} attribute
* @param beforeName values of the {@code beforeName} attribute
* @param after values of the {@code after} attribute
* @param afterName values of the {@code afterName} attribute
* @author Andy Wilkinson
*/
public record AutoConfigurationClass(String name, List<String> before, List<String> beforeName, List<String> after,
List<String> afterName) {
private AutoConfigurationClass(String name, Map<String, List<String>> attributes) {
this(name, attributes.getOrDefault("before", Collections.emptyList()),
attributes.getOrDefault("beforeName", Collections.emptyList()),
attributes.getOrDefault("after", Collections.emptyList()),
attributes.getOrDefault("afterName", Collections.emptyList()));
}
static AutoConfigurationClass of(File classFile) {
try (FileInputStream input = new FileInputStream(classFile)) {
ClassReader classReader = new ClassReader(input);
AutoConfigurationClassVisitor visitor = new AutoConfigurationClassVisitor();
classReader.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES);
return visitor.autoConfigurationClass;
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private static final class AutoConfigurationClassVisitor extends ClassVisitor {
private AutoConfigurationClass autoConfigurationClass;
private String name;
private AutoConfigurationClassVisitor() {
super(SpringAsmInfo.ASM_VERSION);
}
@Override
public void visit(int version, int access, String name, String signature, String superName,
String[] interfaces) {
this.name = Type.getObjectType(name).getClassName();
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
String annotationClassName = Type.getType(descriptor).getClassName();
if ("org.springframework.boot.autoconfigure.AutoConfiguration".equals(annotationClassName)) {
return new AutoConfigurationAnnotationVisitor();
}
return null;
}
private final class AutoConfigurationAnnotationVisitor extends AnnotationVisitor {
private Map<String, List<String>> attributes = new HashMap<>();
private static final Set<String> INTERESTING_ATTRIBUTES = Set.of("before", "beforeName", "after",
"afterName");
private AutoConfigurationAnnotationVisitor() {
super(SpringAsmInfo.ASM_VERSION);
}
@Override
public void visitEnd() {
AutoConfigurationClassVisitor.this.autoConfigurationClass = new AutoConfigurationClass(
AutoConfigurationClassVisitor.this.name, this.attributes);
}
@Override
public AnnotationVisitor visitArray(String attributeName) {
if (INTERESTING_ATTRIBUTES.contains(attributeName)) {
return new AnnotationVisitor(SpringAsmInfo.ASM_VERSION) {
@Override
public void visit(String name, Object value) {
if (value instanceof Type type) {
value = type.getClassName();
}
AutoConfigurationAnnotationVisitor.this.attributes
.computeIfAbsent(attributeName, (n) -> new ArrayList<>())
.add(Objects.toString(value));
}
};
}
return null;
}
}
}
}

66
buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationImportsTask.java

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
/*
* Copyright 2025 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.autoconfigure;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.util.List;
import org.gradle.api.DefaultTask;
import org.gradle.api.Task;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.SkipWhenEmpty;
/**
* A {@link Task} that uses a project's auto-configuration imports.
*
* @author Andy Wilkinson
*/
public abstract class AutoConfigurationImportsTask extends DefaultTask {
static final String IMPORTS_FILE = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";
private FileCollection sourceFiles = getProject().getObjects().fileCollection();
@InputFiles
@SkipWhenEmpty
@PathSensitive(PathSensitivity.RELATIVE)
public FileTree getSource() {
return this.sourceFiles.getAsFileTree().matching((filter) -> filter.include(IMPORTS_FILE));
}
public void setSource(Object source) {
this.sourceFiles = getProject().getObjects().fileCollection().from(source);
}
protected List<String> loadImports() {
File importsFile = getSource().getSingleFile();
try {
return Files.readAllLines(importsFile.toPath());
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}

122
buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java

@ -16,31 +16,20 @@ @@ -16,31 +16,20 @@
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.Map;
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.gradle.api.tasks.TaskProvider;
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.optional.OptionalDependenciesPlugin;
/**
* {@link Plugin} for projects that define auto-configuration. When applied, the plugin
@ -70,14 +59,16 @@ public class AutoConfigurationPlugin implements Plugin<Project> { @@ -70,14 +59,16 @@ public class AutoConfigurationPlugin implements Plugin<Project> {
*/
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);
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> {
Configuration annotationProcessors = project.getConfigurations()
.getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME);
SourceSet main = project.getExtensions()
.getByType(JavaPluginExtension.class)
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
annotationProcessors.getDependencies()
.add(project.getDependencies()
.project(Collections.singletonMap("path",
@ -87,10 +78,6 @@ public class AutoConfigurationPlugin implements Plugin<Project> { @@ -87,10 +78,6 @@ public class AutoConfigurationPlugin implements Plugin<Project> {
.project(Collections.singletonMap("path",
":spring-boot-project:spring-boot-tools:spring-boot-configuration-processor")));
project.getTasks().register("autoConfigurationMetadata", AutoConfigurationMetadata.class, (task) -> {
SourceSet main = project.getExtensions()
.getByType(JavaPluginExtension.class)
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
task.setSourceSet(main);
task.dependsOn(main.getClassesTaskName());
task.getOutputFile()
@ -99,74 +86,37 @@ public class AutoConfigurationPlugin implements Plugin<Project> { @@ -99,74 +86,37 @@ public class AutoConfigurationPlugin implements Plugin<Project> {
.add(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME, task.getOutputFile(),
(artifact) -> artifact.builtBy(task));
});
project.getTasks()
.register("checkAutoConfigurationImports", CheckAutoConfigurationImports.class, (task) -> {
task.setSource(main.getResources());
task.setClasspath(main.getOutput().getClassesDirs());
task.setDescription("Checks the %s file of the main source set."
.formatted(AutoConfigurationImportsTask.IMPORTS_FILE));
});
Configuration requiredClasspath = project.getConfigurations()
.create("autoConfigurationRequiredClasspath")
.extendsFrom(project.getConfigurations().getByName(main.getImplementationConfigurationName()),
project.getConfigurations().getByName(main.getRuntimeOnlyConfigurationName()));
requiredClasspath.getDependencies()
.add(project.getDependencies()
.project(Map.of("path", ":spring-boot-project:spring-boot-autoconfigure")));
TaskProvider<CheckAutoConfigurationClasses> checkAutoConfigurationClasses = project.getTasks()
.register("checkAutoConfigurationClasses", CheckAutoConfigurationClasses.class, (task) -> {
task.setSource(main.getResources());
task.setClasspath(main.getOutput().getClassesDirs());
task.setRequiredDependencies(requiredClasspath);
task.setDescription("Checks the auto-configuration classes of the main source set.");
});
project.getPlugins()
.withType(ArchitecturePlugin.class, (plugin) -> configureArchitecturePluginTasks(project));
});
}
private void configureArchitecturePluginTasks(Project project) {
project.getTasks().configureEach((task) -> {
if ("checkArchitectureMain".equals(task.getName()) && task instanceof ArchitectureCheck architectureCheck) {
configureCheckArchitectureMain(project, architectureCheck);
}
.withType(OptionalDependenciesPlugin.class,
(plugin) -> checkAutoConfigurationClasses.configure((check) -> {
Configuration optionalClasspath = project.getConfigurations()
.create("autoConfigurationOptionalClassPath")
.extendsFrom(project.getConfigurations()
.getByName(OptionalDependenciesPlugin.OPTIONAL_CONFIGURATION_NAME));
check.setOptionalDependencies(optionalClasspath);
}));
});
}
private void configureCheckArchitectureMain(Project project, ArchitectureCheck architectureCheck) {
SourceSet main = project.getExtensions()
.getByType(JavaPluginExtension.class)
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
File resourcesDirectory = main.getOutput().getResourcesDir();
architectureCheck.dependsOn(main.getProcessResourcesTaskName());
architectureCheck.getInputs()
.files(resourcesDirectory)
.optional()
.withPathSensitivity(PathSensitivity.RELATIVE);
architectureCheck.getRules()
.add(allClassesAnnotatedWithAutoConfigurationShouldBeListedInAutoConfigurationImports(
autoConfigurationImports(project, resourcesDirectory)));
}
private ArchRule allClassesAnnotatedWithAutoConfigurationShouldBeListedInAutoConfigurationImports(
Provider<AutoConfigurationImports> imports) {
return ArchRuleDefinition.classes()
.that()
.areAnnotatedWith("org.springframework.boot.autoconfigure.AutoConfiguration")
.should(beListedInAutoConfigurationImports(imports))
.allowEmptyShould(true);
}
private ArchCondition<JavaClass> beListedInAutoConfigurationImports(Provider<AutoConfigurationImports> 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> 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 record AutoConfigurationImports(Path importsFile, List<String> imports) {
}
}

212
buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/CheckAutoConfigurationClasses.java

@ -0,0 +1,212 @@ @@ -0,0 +1,212 @@
/*
* Copyright 2012-2025 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.autoconfigure;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Stream;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.VerificationException;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
/**
* Task to check a project's {@code @AutoConfiguration} classes.
*
* @author Andy Wilkinson
*/
public abstract class CheckAutoConfigurationClasses extends AutoConfigurationImportsTask {
private FileCollection classpath = getProject().getObjects().fileCollection();
private FileCollection optionalDependencies = getProject().getObjects().fileCollection();
private FileCollection requiredDependencies = getProject().getObjects().fileCollection();
private SetProperty<String> optionalDependencyClassNames = getProject().getObjects().setProperty(String.class);
private SetProperty<String> requiredDependencyClassNames = getProject().getObjects().setProperty(String.class);
public CheckAutoConfigurationClasses() {
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
this.optionalDependencyClassNames.set(getProject().provider(() -> classNamesOf(this.optionalDependencies)));
this.requiredDependencyClassNames.set(getProject().provider(() -> classNamesOf(this.requiredDependencies)));
}
private static List<String> classNamesOf(FileCollection classpath) {
return classpath.getFiles().stream().flatMap((file) -> {
try (JarFile jarFile = new JarFile(file)) {
return Collections.list(jarFile.entries())
.stream()
.filter((entry) -> !entry.isDirectory())
.map(JarEntry::getName)
.filter((entryName) -> entryName.endsWith(".class"))
.map((entryName) -> entryName.substring(0, entryName.length() - ".class".length()))
.map((entryName) -> entryName.replace("/", "."));
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}).toList();
}
@Classpath
public FileCollection getClasspath() {
return this.classpath;
}
public void setClasspath(Object classpath) {
this.classpath = getProject().getObjects().fileCollection().from(classpath);
}
@Classpath
public FileCollection getOptionalDependencies() {
return this.optionalDependencies;
}
public void setOptionalDependencies(Object classpath) {
this.optionalDependencies = getProject().getObjects().fileCollection().from(classpath);
}
@Classpath
public FileCollection getRequiredDependencies() {
return this.requiredDependencies;
}
public void setRequiredDependencies(Object classpath) {
this.requiredDependencies = getProject().getObjects().fileCollection().from(classpath);
}
@OutputDirectory
public abstract DirectoryProperty getOutputDirectory();
@TaskAction
void execute() {
Map<String, List<String>> problems = new TreeMap<>();
Set<String> optionalOnlyClassNames = new HashSet<>(this.optionalDependencyClassNames.get());
Set<String> requiredClassNames = this.requiredDependencyClassNames.get();
optionalOnlyClassNames.removeAll(requiredClassNames);
classFiles().forEach((classFile) -> {
AutoConfigurationClass autoConfigurationClass = AutoConfigurationClass.of(classFile);
if (autoConfigurationClass != null) {
check(autoConfigurationClass, optionalOnlyClassNames, requiredClassNames, problems);
}
});
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
writeReport(problems, outputFile);
if (!problems.isEmpty()) {
throw new VerificationException(
"Auto-configuration class check failed. See '%s' for details".formatted(outputFile));
}
}
private List<File> classFiles() {
List<File> classFiles = new ArrayList<>();
for (File root : this.classpath.getFiles()) {
try (Stream<Path> files = Files.walk(root.toPath())) {
files.forEach((file) -> {
if (Files.isRegularFile(file) && file.getFileName().toString().endsWith(".class")) {
classFiles.add(file.toFile());
}
});
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
return classFiles;
}
private void check(AutoConfigurationClass autoConfigurationClass, Set<String> optionalOnlyClassNames,
Set<String> requiredClassNames, Map<String, List<String>> problems) {
if (!autoConfigurationClass.name().endsWith("AutoConfiguration")) {
problems.computeIfAbsent(autoConfigurationClass.name(), (name) -> new ArrayList<>())
.add("Name of a class annotated with @AutoConfiguration should end with AutoConfiguration");
}
autoConfigurationClass.before().forEach((before) -> {
if (optionalOnlyClassNames.contains(before)) {
problems.computeIfAbsent(autoConfigurationClass.name(), (name) -> new ArrayList<>())
.add("before '%s' is from an optional dependency and should be declared in beforeName"
.formatted(before));
}
});
autoConfigurationClass.beforeName().forEach((beforeName) -> {
if (!optionalOnlyClassNames.contains(beforeName)) {
String problem = requiredClassNames.contains(beforeName)
? "beforeName '%s' is from a required dependency and should be declared in before"
.formatted(beforeName)
: "beforeName '%s' not found".formatted(beforeName);
problems.computeIfAbsent(autoConfigurationClass.name(), (name) -> new ArrayList<>()).add(problem);
}
});
autoConfigurationClass.after().forEach((after) -> {
if (optionalOnlyClassNames.contains(after)) {
problems.computeIfAbsent(autoConfigurationClass.name(), (name) -> new ArrayList<>())
.add("after '%s' is from an optional dependency and should be declared in afterName"
.formatted(after));
}
});
autoConfigurationClass.afterName().forEach((afterName) -> {
if (!optionalOnlyClassNames.contains(afterName)) {
String problem = requiredClassNames.contains(afterName)
? "afterName '%s' is from a required dependency and should be declared in after"
.formatted(afterName)
: "afterName '%s' not found".formatted(afterName);
problems.computeIfAbsent(autoConfigurationClass.name(), (name) -> new ArrayList<>()).add(problem);
}
});
}
private void writeReport(Map<String, List<String>> problems, File outputFile) {
outputFile.getParentFile().mkdirs();
StringBuilder report = new StringBuilder();
if (!problems.isEmpty()) {
report.append("Found auto-configuration class problems:%n".formatted());
problems.forEach((className, classProblems) -> {
report.append(" - %s:%n".formatted(className));
classProblems.forEach((problem) -> report.append(" - %s%n".formatted(problem)));
});
}
try {
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}

126
buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/CheckAutoConfigurationImports.java

@ -0,0 +1,126 @@ @@ -0,0 +1,126 @@
/*
* Copyright 2025 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.autoconfigure;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.VerificationException;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
/**
* Task to check the contents of a project's
* {@code META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports}
* file.
*
* @author Andy Wilkinson
*/
public abstract class CheckAutoConfigurationImports extends AutoConfigurationImportsTask {
private FileCollection classpath = getProject().getObjects().fileCollection();
public CheckAutoConfigurationImports() {
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
}
@Classpath
public FileCollection getClasspath() {
return this.classpath;
}
public void setClasspath(Object classpath) {
this.classpath = getProject().getObjects().fileCollection().from(classpath);
}
@OutputDirectory
public abstract DirectoryProperty getOutputDirectory();
@TaskAction
void execute() {
File importsFile = getSource().getSingleFile();
check(importsFile);
}
private void check(File importsFile) {
List<String> imports = loadImports();
List<String> problems = new ArrayList<>();
for (String imported : imports) {
File classFile = find(imported);
if (classFile == null) {
problems.add("'%s' was not found".formatted(imported));
}
else if (!correctlyAnnotated(classFile)) {
problems.add("'%s' is not annotated with @AutoConfiguration".formatted(imported));
}
}
List<String> sortedValues = new ArrayList<>(imports);
Collections.sort(sortedValues);
if (!sortedValues.equals(imports)) {
problems.add("Entries should be sorted alphabetically");
}
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
writeReport(importsFile, problems, outputFile);
if (!problems.isEmpty()) {
throw new VerificationException("%s check failed. See '%s' for details"
.formatted(AutoConfigurationImportsTask.IMPORTS_FILE, outputFile));
}
}
private File find(String className) {
for (File root : this.classpath.getFiles()) {
String classFilePath = className.replace(".", "/") + ".class";
File classFile = new File(root, classFilePath);
if (classFile.isFile()) {
return classFile;
}
}
return null;
}
private boolean correctlyAnnotated(File classFile) {
return AutoConfigurationClass.of(classFile) != null;
}
private void writeReport(File importsFile, List<String> problems, File outputFile) {
outputFile.getParentFile().mkdirs();
StringBuilder report = new StringBuilder();
if (!problems.isEmpty()) {
report.append("Found problems in '%s':%n".formatted(importsFile));
problems.forEach((problem) -> report.append(" - %s%n".formatted(problem)));
}
try {
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
Loading…
Cancel
Save