docs: fix broken external and internal links in Groovydocs#16005
docs: fix broken external and internal links in Groovydocs#16005borinquenkid wants to merge 3 commits into
Conversation
… coverage jdaugherty flagged that wiring auditGroovydocLinks as an unconditional dependency of the docs task would fail the build the moment any violation is found - and the author's own investigation on this PR noted ~30 residual "phantom" link reports that aren't real errors, so merging as-is would have broken every future docs build. Keep the task registered and runnable on demand (./gradlew :grails-doc:auditGroovydocLinks) without wiring it into docsTask.dependsOn. Also extract the violation-detection logic into a Gradle-API-free GroovydocLinkAuditor so it's unit testable - build-logic/docs-core deliberately keeps Gradle classes off the test compile classpath (see docs-core/build.gradle), so the previous DefaultTask-only implementation had no path to test coverage. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR continues the effort to stabilize and validate generated Groovydocs by (1) improving external link mapping used during Groovydoc generation and (2) introducing a dedicated, testable Groovydoc link audit implementation that can be run on demand rather than as part of every docs build.
Changes:
- Registers a new
:grails-doc:auditGroovydocLinkstask (backed byAuditGroovydocLinksTask) to audit generated API docs. - Updates Groovydoc external link configuration/resolution to use Gradle’s
resolutionResultand expands mapped external Javadoc roots (Spring, Hibernate, Jakarta, GORM, Groovy). - Adds a Gradle-API-free
GroovydocLinkAuditorwith Spock unit tests.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| grails-doc/build.gradle | Adds registration for the auditGroovydocLinks task wired to combinedGroovydoc. |
| gradle/docs-dependencies.gradle | Updates dependency version resolution and expands external Javadoc link roots used by Groovydoc. |
| build-logic/docs-core/src/test/groovy/org/grails/doc/gradle/GroovydocLinkAuditorSpec.groovy | Adds unit tests for link violation detection. |
| build-logic/docs-core/src/main/groovy/org/grails/doc/gradle/GroovydocLinkAuditor.groovy | Introduces the Gradle-API-free auditor implementation. |
| build-logic/docs-core/src/main/groovy/org/grails/doc/gradle/AuditGroovydocLinksTask.groovy | Adds the Gradle task wrapper that runs the auditor and fails on violations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def auditGroovydocLinks = tasks.register('auditGroovydocLinks', AuditGroovydocLinksTask) { | ||
| dependsOn combinedGroovydoc | ||
| apiDocsDir = combinedGroovydoc.get().destinationDir | ||
| } |
| NAV_LINK_PATTERNS.each { Pattern pattern, String replacement -> | ||
| Matcher matcher = pattern.matcher(content) | ||
| if (matcher.find()) { | ||
| violations << "Malformed nav link in ${file.name}: ${matcher.group(0)}".toString() | ||
| } | ||
| } |
✅ All tests passed ✅🏷️ Commit: 978a571 Learn more about TestLens at testlens.app. |
jdaugherty
left a comment
There was a problem hiding this comment.
I think there's more work to do on this one.
| def springVersion = resolveProjectVersion('spring-core') | ||
| if (springVersion) { | ||
| links << [packages: 'org.springframework.core.', href: "https://docs.spring.io/spring-framework/docs/${springVersion}/javadoc-api/"] | ||
| links << [packages: 'org.springframework.', href: "https://docs.spring.io/spring-framework/docs/${springVersion}/javadoc-api/"] |
There was a problem hiding this comment.
Groovydoc resolves links first-match-wins in list order (SimpleGroovyClassDoc.getDocUrl iterates the links and returns on the first type.startsWith(prefix) hit). Broadening this entry to org.springframework. while it sits before the org.springframework.boot. entry means every Spring Boot class reference now matches here first and links into docs.spring.io/spring-framework/..., where the Boot packages don't exist — the Boot entry below becomes dead code and all Boot links 404.
Most-specific-first fixes it: move the org.springframework.boot. entry above this one.
| } | ||
| def jakartaValidationVersion = resolveProjectVersion('jakarta.validation-api') | ||
| if (jakartaValidationVersion) { | ||
| links << [packages: 'jakarta.validation.', href: "https://jakarta.ee/specifications/bean-validation/3.0/apidocs/"] |
There was a problem hiding this comment.
Minor: these three Jakarta links gate on the resolved artifact version but hardcode the spec version in the URL (3.0, 3.1, platform/10), so they'll silently go stale on the next spec bump. Deriving the URL segment from the resolved version (as done for Hibernate above) would keep them consistent.
| if (jakartaServletVersion) { | ||
| links << [packages: 'jakarta.servlet.', href: "https://jakarta.ee/specifications/platform/10/apidocs/"] | ||
| } | ||
| links << [packages: 'org.grails.datastore.', href: "https://gorm.grails.org/latest/api/"] |
There was a problem hiding this comment.
Question on these three: since the data modules merged into this repo in 7.0, org.grails.datastore.* / grails.gorm.* classes are generated in this repo's own aggregate groovydoc. gorm.grails.org/latest/api/ is the standalone pre-merge GORM site — is that the intended target, and are we sure these prefixes don't hijack references that should resolve locally within the combined docs?
There was a problem hiding this comment.
Actually, we host these directly now on our website, so we shoudlnt' refer to the older docs link
| links << [packages: 'org.grails.datastore.', href: "https://gorm.grails.org/latest/api/"] | ||
| links << [packages: 'grails.gorm.', href: "https://gorm.grails.org/latest/api/"] | ||
| links << [packages: 'org.grails.gorm.', href: "https://gorm.grails.org/latest/api/"] | ||
| links << [packages: 'groovy.', href: "https://docs.groovy-lang.org/latest/html/gapi/"] |
There was a problem hiding this comment.
docs.groovy-lang.org/latest is Groovy 5 while 7.0.x ships Groovy 4, so these links will point at the wrong major version's API docs. The rest of this block pins resolved versions — GroovySystem.version is available here to build the versioned URL (https://docs.groovy-lang.org/docs/groovy-${GroovySystem.version}/html/gapi/).
| class GroovydocLinkAuditor { | ||
|
|
||
| // Patterns common to Groovydoc navigation failures | ||
| private static final Map<Pattern, String> NAV_LINK_PATTERNS = [ |
There was a problem hiding this comment.
These nav-link patterns can never match real Groovydoc output. The templates in groovy-groovydoc (checked 4.0.24, what 7.0.x uses) emit nav links double-quoted with a legitimate relative prefix: href="${classDoc.relativeRootPath}deprecated-list.html". So the single-quoted patterns are dead code — and if the quoting were "fixed", they'd flag every nested class page's perfectly valid nav links, since the prefix is correct there. That also means a "0 violations" run partly reflects this vacuity rather than clean docs.
The inner-class check below is the genuinely valid part: class cross-reference links are single-quoted in the templates, which is where the Outer/Inner.html vs Outer.Inner.html phantom-link bug lives, and the only-flag-if-the-dotted-file-exists guard is a smart false-positive suppressor.
Suggest dropping these patterns (or reworking them to detect links whose resolved target doesn't exist, like the inner-class check does). Two related smells that go away with them: the map values (the replacement strings) are never read anywhere, and findViolationsInFile only records the first match per pattern per file, so the "Found N malformed links" total in the task's exception would undercount.
| @TaskAction | ||
| void auditLinks() { | ||
| File apiDir = apiDocsDir.get().asFile | ||
| if (!apiDir.exists()) { |
There was a problem hiding this comment.
This guard is unreachable: with @InputDirectory, Gradle fails input validation before the @TaskAction runs when the directory is missing.
| if (!violations.isEmpty()) { | ||
| violations.take(10).each { logger.error(it) } | ||
| if (violations.size() > 10) logger.error("... and ${violations.size() - 10} more") | ||
| throw new org.gradle.api.GradleException("Found ${violations.size()} malformed links in Groovydoc. Please fix the source issue rather than patching. See logs for details.") |
There was a problem hiding this comment.
Import org.gradle.api.GradleException instead of the inline FQCN — it's already on the compile classpath here, and the project convention is explicit imports.
|
|
||
| def auditGroovydocLinks = tasks.register('auditGroovydocLinks', AuditGroovydocLinksTask) { | ||
| dependsOn combinedGroovydoc | ||
| apiDocsDir = combinedGroovydoc.get().destinationDir |
There was a problem hiding this comment.
combinedGroovydoc.get() realizes the task eagerly and requires the explicit dependsOn above. Provider wiring keeps it lazy and carries the dependency implicitly:
apiDocsDir = project.layout.dir(combinedGroovydoc.map { it.destinationDir })(and then the dependsOn line can go.)
Continues #15327 by @sanjana2505006 — rebases onto current 7.0.x and addresses @jdaugherty's outstanding review feedback:
TEST_FAILURES.mdthat was accidentally committed (flagged in docs: fix broken external and internal links in Groovydocs #15327, never addressed).auditGroovydocLinksfrom the defaultdocsbuild so it can't fail every future docs build on a false positive. jdaugherty's last comment on docs: fix broken external and internal links in Groovydocs #15327: "The audit should be a task we run, but it shouldn't be run by default unless we're going to capture it in the workflow summary of the build." It remains available on demand:./gradlew :grails-doc:auditGroovydocLinks.GroovydocLinkAuditorclass with unit tests (build-logic/docs-coredeliberately keeps Gradle classes off the test compile classpath, so the previousDefaultTask-only implementation had no path to test coverage).Verified against a full
aggregateGroovydocrun on current7.0.x(all 60+ modules): 0 violations — the ~30 phantom-link false positives Sanjana observed earlier in the PR's life (from the old Python-script-based approach) no longer reproduce with the current native-Groovy audit logic.Closes #15327.
Co-Authored-By: Sanjana sanju250506@gmail.com