Skip to content

docs: fix broken external and internal links in Groovydocs#16005

Open
borinquenkid wants to merge 3 commits into
7.0.xfrom
fix/groovydoc-links
Open

docs: fix broken external and internal links in Groovydocs#16005
borinquenkid wants to merge 3 commits into
7.0.xfrom
fix/groovydoc-links

Conversation

@borinquenkid

Copy link
Copy Markdown
Member

Continues #15327 by @sanjana2505006 — rebases onto current 7.0.x and addresses @jdaugherty's outstanding review feedback:

  • Removes the stray TEST_FAILURES.md that was accidentally committed (flagged in docs: fix broken external and internal links in Groovydocs #15327, never addressed).
  • Decouples auditGroovydocLinks from the default docs build 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.
  • Extracts the link-violation-detection logic into a Gradle-API-free GroovydocLinkAuditor class with unit tests (build-logic/docs-core deliberately keeps Gradle classes off the test compile classpath, so the previous DefaultTask-only implementation had no path to test coverage).

Verified against a full aggregateGroovydoc run on current 7.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

sanjana2505006 and others added 3 commits July 17, 2026 16:34
… 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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:auditGroovydocLinks task (backed by AuditGroovydocLinksTask) to audit generated API docs.
  • Updates Groovydoc external link configuration/resolution to use Gradle’s resolutionResult and expands mapped external Javadoc roots (Spring, Hibernate, Jakarta, GORM, Groovy).
  • Adds a Gradle-API-free GroovydocLinkAuditor with 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.

Comment thread grails-doc/build.gradle
Comment on lines +129 to +132
def auditGroovydocLinks = tasks.register('auditGroovydocLinks', AuditGroovydocLinksTask) {
dependsOn combinedGroovydoc
apiDocsDir = combinedGroovydoc.get().destinationDir
}
Comment on lines +62 to +67
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()
}
}
@testlens-app

testlens-app Bot commented Jul 17, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 978a571
▶️ Tests: 15168 executed
⚪️ Checks: 52/52 completed


Learn more about TestLens at testlens.app.

@jdaugherty jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread grails-doc/build.gradle

def auditGroovydocLinks = tasks.register('auditGroovydocLinks', AuditGroovydocLinksTask) {
dependsOn combinedGroovydoc
apiDocsDir = combinedGroovydoc.get().destinationDir

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants