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
This commit is contained in:
Moritz Halbritter
2026-10-07 11:50:24 +02:00
parent 7217681ed1
commit 111dc0c15a
4 changed files with 115 additions and 3 deletions
@@ -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));
});
}
@@ -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<String> 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);
@@ -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);
}
}
}
@@ -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"))