Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
import java.util.stream.Stream;
Expand All @@ -42,6 +43,7 @@
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Classpath;
Expand Down Expand Up @@ -70,19 +72,27 @@
*/
public abstract class ArchitectureCheck extends DefaultTask {

static final String CONDITIONAL_ON_CLASS = "ConditionalOnClass";

static final String DEPRECATED_CONFIGURATION_PROPERTY = "DeprecatedConfigurationProperty";

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

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

private FileCollection classes;

public ArchitectureCheck() {
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
getConditionalOnClassAnnotation().convention(CONDITIONAL_ON_CLASS_ANNOTATION);
getAnnotationClasses().convention(Map.of(CONDITIONAL_ON_CLASS, CONDITIONAL_ON_CLASS_ANNOTATION,
DEPRECATED_CONFIGURATION_PROPERTY, DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION));
getRules().addAll(getProhibitObjectsRequireNonNull().convention(true)
.map(whenTrue(ArchitectureRules::noClassesShouldCallObjectsRequireNonNull)));
getRules().addAll(ArchitectureRules.standard());
getRules().addAll(whenMainSources(() -> List
.of(ArchitectureRules.allBeanMethodsShouldReturnNonPrivateType(), ArchitectureRules
.allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(getConditionalOnClassAnnotation().get()))));
getRules().addAll(whenMainSources(() -> ArchitectureRules
.beanMethods(annotationClassFor(CONDITIONAL_ON_CLASS, CONDITIONAL_ON_CLASS_ANNOTATION))));
getRules().addAll(whenMainSources(() -> ArchitectureRules.configurationProperties(
annotationClassFor(DEPRECATED_CONFIGURATION_PROPERTY, DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION))));
getRuleDescriptions().set(getRules().map(this::asDescriptions));
}

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

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

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

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,16 @@ static List<ArchRule> standard() {
return List.copyOf(rules);
}

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

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

private static ArchRule allBeanMethodsShouldReturnNonPrivateType() {
return methodsThatAreAnnotatedWith("org.springframework.context.annotation.Bean").should(check(
"not return types declared with the %s modifier, as such types are incompatible with Spring AOT processing"
.formatted(JavaModifier.PRIVATE),
Expand All @@ -115,7 +124,7 @@ static ArchRule allBeanMethodsShouldReturnNonPrivateType() {
.allowEmptyShould(true);
}

static ArchRule allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(String annotationName) {
private static ArchRule allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(String annotationName) {
return methodsThatAreAnnotatedWith("org.springframework.context.annotation.Bean").should()
.notBeAnnotatedWith(annotationName)
.because("@ConditionalOnClass on @Bean methods is ineffective - it doesn't prevent "
Expand Down Expand Up @@ -348,6 +357,20 @@ private static ArchRule allConfigurationPropertiesBindingBeanMethodsShouldBeStat
.allowEmptyShould(true);
}

private static ArchRule allDeprecatedConfigurationPropertiesShouldIncludeSince(String annotationName) {
return methodsThatAreAnnotatedWith(annotationName)
.should(check("include a non-empty 'since' attribute", (method, events) -> {
JavaAnnotation<JavaMethod> annotation = method.getAnnotationOfType(annotationName);
Map<String, Object> properties = annotation.getProperties();
Object since = properties.get("since");
if (!(since instanceof String) || ((String) since).isEmpty()) {
addViolation(events, method, annotation.getDescription()
+ " should include a non-empty 'since' attribute of @DeprecatedConfigurationProperty");
}
}))
.allowEmptyShould(true);
}

private static boolean containsOnlySingleType(JavaType[] types, JavaType type) {
return types.length == 1 && type.equals(types[0]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ void check() throws IOException {
ConfigurationPropertiesAnalyzer analyzer = new ConfigurationPropertiesAnalyzer(getSource().getFiles());
Report report = new Report(this.projectDir);
analyzer.analyzeSort(report);
analyzer.analyzeDeprecationSince(report);
File reportFile = getReportLocation().get().getAsFile();
report.write(reportFile);
if (report.hasProblems()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ void check() throws IOException {
Report report = new Report(this.projectDir);
analyzer.analyzeSort(report);
analyzer.analyzePropertyDescription(report, getExclusions().get());
analyzer.analyzeDeprecationSince(report);
File reportFile = getReportLocation().get().getAsFile();
report.write(reportFile);
if (report.hasProblems()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,26 @@ private boolean isDescribed(Map<String, Object> property) {
return property.get("description") != null;
}

void analyzeDeprecationSince(Report report) throws IOException {
for (File source : this.sources) {
report.registerAnalysis(source, analyzeDeprecationSince(source));
}
}

@SuppressWarnings("unchecked")
private Analysis analyzeDeprecationSince(File source) throws IOException {
Analysis analysis = new Analysis("The following properties are deprecated without a 'since' version:");
Map<String, Object> json = readJsonContent(source);
List<Map<String, Object>> properties = (List<Map<String, Object>>) json.get("properties");
properties.stream().filter((property) -> property.containsKey("deprecation")).forEach((property) -> {
Map<String, Object> deprecation = (Map<String, Object>) property.get("deprecation");
if (!deprecation.containsKey("since")) {
analysis.addItem(property.get("name").toString());
}
});
return analysis;
}

private Map<String, Object> readJsonContent(File source) throws IOException {
return this.objectMapperSupplier.obtain().readValue(source, new TypeReference<Map<String, Object>>() {
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

import org.gradle.api.tasks.SourceSet;
import org.gradle.testkit.runner.BuildResult;
Expand All @@ -41,6 +44,7 @@
import org.junit.jupiter.params.provider.EnumSource;

import org.springframework.boot.build.architecture.annotations.TestConditionalOnClass;
import org.springframework.boot.build.architecture.annotations.TestDeprecatedConfigurationProperty;
import org.springframework.util.ClassUtils;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
Expand Down Expand Up @@ -316,6 +320,16 @@ void whenConditionalOnClassUsedOnBeanMethodsWithTestSourcesShouldSucceedAndWrite
build(gradleBuild, Task.CHECK_ARCHITECTURE_TEST);
}

@Test
void whenDeprecatedConfigurationPropertyIsMissingSinceShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties", "annotations");
GradleBuild gradleBuild = this.gradleBuild.withDependencies(SPRING_CONTEXT)
.withDeprecatedConfigurationPropertyAnnotation(TestDeprecatedConfigurationProperty.class.getName());
buildAndFail(gradleBuild, Task.CHECK_ARCHITECTURE_MAIN,
"should include a non-empty 'since' attribute of @DeprecatedConfigurationProperty",
"DeprecatedConfigurationPropertySince.getProperty");
}

private void prepareTask(Task task, String... sourceDirectories) throws IOException {
for (String sourceDirectory : sourceDirectories) {
FileSystemUtils.copyRecursively(
Expand Down Expand Up @@ -348,7 +362,12 @@ private void buildAndFail(GradleBuild gradleBuild, Task task, String... messages
try {
BuildResult buildResult = gradleBuild.buildAndFail(task.toString());
assertThat(buildResult.taskPaths(TaskOutcome.FAILED)).as(buildResult.getOutput()).contains(":" + task);
assertThat(task.getFailureReport(gradleBuild.getProjectDir())).contains(messages);
try {
assertThat(task.getFailureReport(gradleBuild.getProjectDir())).contains(messages);
}
catch (NoSuchFileException ex) {
throw new AssertionError("Expected failure report not found\n" + buildResult.getOutput());
}
}
catch (UnexpectedBuildSuccess ex) {
throw new AssertionError("Expected build to fail but it succeeded\n" + ex.getBuildResult().getOutput(), ex);
Expand Down Expand Up @@ -410,9 +429,18 @@ GradleBuild withProhibitObjectsRequireNonNull(Boolean prohibitObjectsRequireNonN
return this;
}

GradleBuild withConditionalOnClassAnnotation(String annotationName) {
GradleBuild withConditionalOnClassAnnotation(String annotationClass) {
for (Task task : Task.values()) {
configureTask(task, (configuration) -> configuration
.withAnnotation(ArchitectureCheck.CONDITIONAL_ON_CLASS, annotationClass));
}
return this;
}

GradleBuild withDeprecatedConfigurationPropertyAnnotation(String annotationClass) {
for (Task task : Task.values()) {
configureTask(task, (configuration) -> configuration.withConditionalOnClassAnnotation(annotationName));
configureTask(task, (configuration) -> configuration
.withAnnotation(ArchitectureCheck.DEPRECATED_CONFIGURATION_PROPERTY, annotationClass));
}
return this;
}
Expand Down Expand Up @@ -454,18 +482,18 @@ private GradleRunner prepareRunner(String... arguments) throws IOException {
for (String dependency : this.dependencies) {
buildFile.append("\n implementation ").append(StringUtils.quote(dependency));
}
buildFile.append("}\n");
buildFile.append("\n}\n\n");
}
this.taskConfigurations.forEach((task, configuration) -> {
buildFile.append(task).append(" {");
if (configuration.conditionalOnClassAnnotation() != null) {
buildFile.append("\n conditionalOnClassAnnotation = ")
.append(StringUtils.quote(configuration.conditionalOnClassAnnotation()));
}
if (configuration.prohibitObjectsRequireNonNull() != null) {
buildFile.append("\n prohibitObjectsRequireNonNull = ")
.append(configuration.prohibitObjectsRequireNonNull());
}
if (configuration.annotations() != null && !configuration.annotations().isEmpty()) {
buildFile.append("\n annotationClasses = ")
.append(toGroovyMapString(configuration.annotations()));
}
buildFile.append("\n}\n");
});
Files.writeString(this.projectDir.resolve("build.gradle"), buildFile, StandardCharsets.UTF_8);
Expand All @@ -475,15 +503,31 @@ private GradleRunner prepareRunner(String... arguments) throws IOException {
.withPluginClasspath();
}

private record TaskConfiguration(Boolean prohibitObjectsRequireNonNull, String conditionalOnClassAnnotation) {
static String toGroovyMapString(Map<String, String> map) {
return map.entrySet()
.stream()
.map((entry) -> "'" + entry.getKey() + "' : '" + entry.getValue() + "'")
.collect(Collectors.joining(", ", "[", "]"));
}

private TaskConfiguration withConditionalOnClassAnnotation(String annotationName) {
return new TaskConfiguration(this.prohibitObjectsRequireNonNull, annotationName);
private record TaskConfiguration(Boolean prohibitObjectsRequireNonNull, Map<String, String> annotations) {

public TaskConfiguration {
if (annotations == null) {
annotations = new HashMap<>();
}
}

private TaskConfiguration withProhibitObjectsRequireNonNull(Boolean prohibitObjectsRequireNonNull) {
return new TaskConfiguration(prohibitObjectsRequireNonNull, this.conditionalOnClassAnnotation);
return new TaskConfiguration(prohibitObjectsRequireNonNull, this.annotations);
}

private TaskConfiguration withAnnotation(String name, String annotationClass) {
Map<String, String> map = new HashMap<>(this.annotations);
map.put(name, annotationClass);
return new TaskConfiguration(this.prohibitObjectsRequireNonNull, map);
}

}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.build.architecture.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* {@code @DeprecatedConfigurationProperty} analogue for architecture checks.
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestDeprecatedConfigurationProperty {

/**
* The reason for the deprecation.
* @return the deprecation reason
*/
String reason() default "";

/**
* The field that should be used instead (if any).
* @return the replacement field
*/
String replacement() default "";

/**
* The version in which the property became deprecated.
* @return the version
*/
String since() default "";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.build.architecture.configurationproperties;

import org.springframework.boot.build.architecture.annotations.TestDeprecatedConfigurationProperty;

public class DeprecatedConfigurationPropertySince {

private String property;

@TestDeprecatedConfigurationProperty(reason = "no longer used")
@Deprecated
public String getProperty() {
return this.property;
}

public void setProperty(String property) {
this.property = property;
}

}
Loading