From 111dc0c15ad3b557635d6e9331f30b6b7d2c3256 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 7 Oct 2026 11:24:15 +0200 Subject: [PATCH] Run javadoc macro check as part of antora checkAntoraJavadocMacros was never wired into the build and its classpath, implementation, is not resolvable. Broken javadoc: links went unnoticed. Use runtimeClasspath, make antora depend on the check and add lombok, which the docs link to. Also fix rejection of no-argument method links and the method signature in failure messages. Closes gh-52051 --- .../boot/build/AntoraConventions.java | 6 +- .../boot/build/antora/CheckJavadocMacros.java | 4 +- .../build/antora/CheckJavadocMacrosTests.java | 106 ++++++++++++++++++ documentation/spring-boot-docs/build.gradle | 2 + 4 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 buildSrc/src/test/java/org/springframework/boot/build/antora/CheckJavadocMacrosTests.java diff --git a/buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java index 43369d8a12a..030c0bf2519 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java @@ -103,8 +103,10 @@ public class AntoraConventions { task.getOutputDirectory().set(project.getLayout().getBuildDirectory().dir(task.getName())); }); project.getPlugins().withType(JavaPlugin.class, (java) -> { - Configuration implementation = project.getConfigurations().getByName("implementation"); - checkAntoraJavadocMacros.configure((macrosTask) -> macrosTask.setClasspath(implementation)); + Configuration runtimeClasspath = project.getConfigurations() + .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + checkAntoraJavadocMacros.configure((macrosTask) -> macrosTask.setClasspath(runtimeClasspath)); + tasks.withType(AntoraTask.class).configureEach((antora) -> antora.dependsOn(checkAntoraJavadocMacros)); }); } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/CheckJavadocMacros.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/CheckJavadocMacros.java index f5dc18891b7..663b8b47025 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/antora/CheckJavadocMacros.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/CheckJavadocMacros.java @@ -53,6 +53,7 @@ import org.springframework.asm.FieldVisitor; import org.springframework.asm.MethodVisitor; import org.springframework.asm.SpringAsmInfo; import org.springframework.asm.Type; +import org.springframework.util.StringUtils; import org.springframework.util.function.ThrowingConsumer; /** @@ -358,7 +359,7 @@ public abstract class CheckJavadocMacros extends DefaultTask { @Override public String toString() { - return this.name + "(" + String.join(", ", this.arguments + ")"); + return this.name + "(" + String.join(", ", this.arguments) + ")"; } static MethodAnchor of(String anchor, Origin origin) { @@ -369,6 +370,7 @@ public abstract class CheckJavadocMacros extends DefaultTask { String name = anchor.substring(0, openingIndex); List arguments = Stream.of(anchor.substring(openingIndex + 1, anchor.length() - 1).split(",")) .map(String::trim) + .filter(StringUtils::hasText) .map((argument) -> argument.endsWith("...") ? argument.replace("...", "[]") : argument) .toList(); return new MethodAnchor(name, arguments, origin); diff --git a/buildSrc/src/test/java/org/springframework/boot/build/antora/CheckJavadocMacrosTests.java b/buildSrc/src/test/java/org/springframework/boot/build/antora/CheckJavadocMacrosTests.java new file mode 100644 index 00000000000..4feef6d13d4 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/antora/CheckJavadocMacrosTests.java @@ -0,0 +1,106 @@ +/* + * 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.antora; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; + +import org.gradle.api.Project; +import org.gradle.api.tasks.VerificationException; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link CheckJavadocMacros}. + * + * @author Moritz Halbritter + */ +class CheckJavadocMacrosTests { + + @TempDir + private File temp; + + @Test + void shouldAcceptClass() throws IOException { + check("javadoc:java.lang.Object[]"); + } + + @Test + void shouldAcceptMethodWithoutArguments() throws IOException { + check("javadoc:java.lang.Object#hashCode()[]"); + } + + @Test + void shouldAcceptMethodWithArguments() throws IOException { + check("javadoc:java.lang.String#valueOf(int)[]"); + } + + @Test + void shouldRejectMissingClass() { + assertThatExceptionOfType(VerificationException.class).isThrownBy(() -> check("javadoc:java.lang.Missing[]")); + assertThat(failureReport()).contains("class java.lang.Missing does not exist"); + } + + @Test + void shouldRejectMissingMethodWithoutArguments() { + assertThatExceptionOfType(VerificationException.class) + .isThrownBy(() -> check("javadoc:java.lang.Object#missing()[]")); + assertThat(failureReport()).contains("method missing() does not exist"); + } + + @Test + void shouldRejectMethodWithWrongArguments() { + assertThatExceptionOfType(VerificationException.class) + .isThrownBy(() -> check("javadoc:java.lang.Object#hashCode(int)[]")); + assertThat(failureReport()).contains("method hashCode(int) does not exist"); + } + + @Test + void shouldRejectMethodWithWrongMultipleArguments() { + assertThatExceptionOfType(VerificationException.class) + .isThrownBy(() -> check("javadoc:java.lang.Object#hashCode(int, java.lang.String)[]")); + assertThat(failureReport()).contains("method hashCode(int, java.lang.String) does not exist"); + } + + private void check(String content) throws IOException { + File source = new File(this.temp, "source"); + source.mkdirs(); + Files.writeString(new File(source, "test.adoc").toPath(), content); + Project project = ProjectBuilder.builder().withProjectDir(this.temp).build(); + CheckJavadocMacros task = project.getTasks().register("checkJavadocMacros", CheckJavadocMacros.class).get(); + task.setSource(project.files(source)); + task.setClasspath(project.files()); + task.getOutputDirectory().set(new File(this.temp, "output")); + task.checkJavadocMacros(); + } + + private String failureReport() { + try { + return Files.readString(new File(this.temp, "output/failure-report.txt").toPath()); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + +} diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index 240591d6abe..c4d44a39426 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -101,6 +101,8 @@ dependencies { resolvedBom(project(path: ":platform:spring-boot-dependencies", configuration: "resolvedBom")) + runtimeOnly("org.projectlombok:lombok") + springApplicationExample(platform(project(":platform:spring-boot-dependencies"))) springApplicationExample(project(":starter:spring-boot-starter-webmvc"))