jdaugherty commented on code in PR #16005:
URL: https://github.com/apache/grails-core/pull/16005#discussion_r3895767090
##########
gradle/docs-dependencies.gradle:
##########
@@ -54,14 +48,54 @@ def configureGroovyDoc =
tasks.register('configureGroovyDoc') {
if (testContainersVersion) {
links << [packages: 'org.testcontainers.', href:
"https://javadoc.io/doc/org.testcontainers/testcontainers/${testContainersVersion}/"]
}
- def springVersion = resolveProjectVersion('spring-core')
- if (springVersion) {
- links << [packages: 'org.springframework.core.', href:
"https://docs.spring.io/spring-framework/docs/${springVersion}/javadoc-api/"]
- }
+ // Groovydoc resolves 'links' first-match-wins in declaration order,
so the more
+ // specific org.springframework.boot. entry must come before the
broader
+ // org.springframework. entry or every Boot class link resolves
against the
+ // Framework javadocs instead, where the Boot classes don't exist.
def springBootVersion = resolveProjectVersion('spring-boot')
if (springBootVersion) {
links << [packages: 'org.springframework.boot.', href:
"https://docs.spring.io/spring-boot/docs/${springBootVersion}/api/"]
Review Comment:
This URL scheme was retired at Boot 3.4, so with the version this branch
resolves (3.5.16) it 404s:
| URL | status |
| --- | --- |
|
`docs.spring.io/spring-boot/docs/3.2.12/api/org/springframework/boot/SpringApplication.html`
| 200 |
|
`docs.spring.io/spring-boot/docs/3.4.0/api/org/springframework/boot/SpringApplication.html`
| 404 |
|
`docs.spring.io/spring-boot/3.5.16/api/java/org/springframework/boot/SpringApplication.html`
| 200 |
So the href wants to be
`https://docs.spring.io/spring-boot/${springBootVersion}/api/java/`.
The entry predates this PR, but this is the change that moves it above the
framework entry specifically so Boot types resolve here — worth having it point
somewhere that exists. (The `spring-framework/docs/${version}/javadoc-api/`
entry below is fine; verified 200 on 6.2.12.)
##########
gradle/docs-dependencies.gradle:
##########
@@ -54,14 +48,54 @@ def configureGroovyDoc =
tasks.register('configureGroovyDoc') {
if (testContainersVersion) {
links << [packages: 'org.testcontainers.', href:
"https://javadoc.io/doc/org.testcontainers/testcontainers/${testContainersVersion}/"]
}
- def springVersion = resolveProjectVersion('spring-core')
- if (springVersion) {
- links << [packages: 'org.springframework.core.', href:
"https://docs.spring.io/spring-framework/docs/${springVersion}/javadoc-api/"]
- }
+ // Groovydoc resolves 'links' first-match-wins in declaration order,
so the more
+ // specific org.springframework.boot. entry must come before the
broader
+ // org.springframework. entry or every Boot class link resolves
against the
+ // Framework javadocs instead, where the Boot classes don't exist.
def springBootVersion = resolveProjectVersion('spring-boot')
if (springBootVersion) {
links << [packages: 'org.springframework.boot.', href:
"https://docs.spring.io/spring-boot/docs/${springBootVersion}/api/"]
}
+ def springVersion = resolveProjectVersion('spring-core')
+ if (springVersion) {
+ links << [packages: 'org.springframework.', href:
"https://docs.spring.io/spring-framework/docs/${springVersion}/javadoc-api/"]
+ }
+ def hibernateVersion = resolveProjectVersion('hibernate-core')
+ if (hibernateVersion) {
+ def shortVersion = hibernateVersion.split('\\.').take(2).join('.')
+ links << [packages: 'org.hibernate.', href:
"https://docs.jboss.org/hibernate/orm/${shortVersion}/javadocs/"]
+ }
+ def jakartaValidationVersion =
resolveProjectVersion('jakarta.validation-api')
+ if (jakartaValidationVersion) {
+ def specVersion =
jakartaValidationVersion.split('\\.').take(2).join('.')
+ links << [packages: 'jakarta.validation.', href:
"https://jakarta.ee/specifications/bean-validation/${specVersion}/apidocs/"]
+ }
+ def jakartaPersistenceVersion =
resolveProjectVersion('jakarta.persistence-api')
+ if (jakartaPersistenceVersion) {
+ def specVersion =
jakartaPersistenceVersion.split('\\.').take(2).join('.')
+ links << [packages: 'jakarta.persistence.', href:
"https://jakarta.ee/specifications/persistence/${specVersion}/apidocs/"]
+ }
+ def jakartaServletVersion =
resolveProjectVersion('jakarta.servlet-api')
+ if (jakartaServletVersion) {
+ def servletSpecVersion =
jakartaServletVersion.split('\\.').take(2).join('.')
+ // The platform apidocs are versioned by Jakarta EE Platform
release, not by the
+ // Servlet spec version, so the two can't be derived from one
another directly.
+ def servletSpecToPlatformVersion = ['5.0': '9', '6.0': '10',
'6.1': '11']
+ def platformVersion =
servletSpecToPlatformVersion[servletSpecVersion]
+ if (platformVersion) {
+ links << [packages: 'jakarta.servlet.', href:
"https://jakarta.ee/specifications/platform/${platformVersion}/apidocs/"]
+ }
+ }
+ // No external link is registered for
org.grails.datastore./grails.gorm./org.grails.gorm.:
+ // since the data modules merged into grails-core in 7.0, those
classes are generated
+ // directly in this repo's own aggregate Groovydoc and resolve
locally. Mapping them to
+ // the older, standalone gorm.grails.org site would misdirect those
in-repo links.
+ def groovyVersion = resolveProjectVersion('groovy')
+ if (groovyVersion) {
+ links << [packages: 'groovy.', href:
"https://docs.groovy-lang.org/${groovyVersion}/html/gapi/"]
+ links << [packages: 'org.apache.groovy.', href:
"https://docs.groovy-lang.org/${groovyVersion}/html/gapi/"]
+ links << [packages: 'org.codehaus.groovy.', href:
"https://docs.groovy-lang.org/${groovyVersion}/html/gapi/"]
+ }
if (it.ext.has('groovydocLinks')) {
Review Comment:
The curated link list never reaches the Groovydoc tasks, so none of the
entries added or reordered above actually take effect.
`GroovydocEnhancerPlugin.resolveLinks(Groovydoc gdoc)` reads
`gdoc.ext.groovydocLinks` off the **Groovydoc** task, but inside this `doLast`
block `it` is the `configureGroovyDoc` task — so line 102 stores the list as an
extra property on `configureGroovyDoc` itself, and the `has()` check here only
ever sees what this same task set on an earlier run.
Reproduced on this branch:
```
./gradlew :grails-spring:groovydoc
grep -rho "href='https://[^']*'" grails-spring/build/docs/groovydoc | wc -l
# 0
```
Zero external links in the output. Every external reference comes out as a
dangling relative link instead — 20 distinct missing targets in that one small
module, e.g.
`href='../../../org.springframework.context.ApplicationContext.html'`. The
published docs show the same thing:
`org/grails/web/servlet/mvc/GrailsWebRequest.html` on
grails.apache.org/docs/latest/api links `HttpServletRequest` to
`../../../../../jakarta.servlet.http.HttpServletRequest.html`, which doesn't
exist.
Setting the same list on the Groovydoc task instead does produce external
links, which confirms the mechanism works and the wiring is the only thing
missing.
One thing worth knowing before restructuring this, because it changes what
this PR can claim: Groovy's `links` only feeds
`SimpleGroovyClassDoc.getDocUrl`, i.e. doc-comment references, `@see`, and
unresolved string types. Signature types (superclass, interfaces, parameters,
return types) go through `dolink` in groovydoc's `classDocName.html` template,
which builds `classDoc.relativeRootPath + t.fullPathName + '.html'`
unconditionally and never consults `links`. In the `:grails-spring` output only
1 of the 21 Spring references switched to an external URL once the wiring was
corrected. So fixing this recovers a real but small slice; the bulk of the
broken links needs a different fix.
##########
build-logic/docs-core/src/test/groovy/org/grails/doc/gradle/GroovydocLinkAuditorSpec.groovy:
##########
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.grails.doc.gradle
+
+import spock.lang.Specification
+import spock.lang.TempDir
+
+class GroovydocLinkAuditorSpec extends Specification {
Review Comment:
Non-blocking, and pre-existing: nothing runs these. `build-logic` is an
included build, and no workflow invokes its `build` or `test` — a root
`./gradlew build` only pulls the plugin jars out of it. The three specs already
in this project have the same problem.
Verified they pass locally:
```
cd build-logic && ../gradlew :grails-docs-core:test --tests
'*GroovydocLinkAuditorSpec*'
# 3 passed
```
Just flagging that the coverage this adds won't gate anything until
build-logic's tests are wired into CI.
##########
gradle/docs-dependencies.gradle:
##########
@@ -54,14 +48,54 @@ def configureGroovyDoc =
tasks.register('configureGroovyDoc') {
if (testContainersVersion) {
links << [packages: 'org.testcontainers.', href:
"https://javadoc.io/doc/org.testcontainers/testcontainers/${testContainersVersion}/"]
}
- def springVersion = resolveProjectVersion('spring-core')
- if (springVersion) {
- links << [packages: 'org.springframework.core.', href:
"https://docs.spring.io/spring-framework/docs/${springVersion}/javadoc-api/"]
- }
+ // Groovydoc resolves 'links' first-match-wins in declaration order,
so the more
+ // specific org.springframework.boot. entry must come before the
broader
+ // org.springframework. entry or every Boot class link resolves
against the
+ // Framework javadocs instead, where the Boot classes don't exist.
def springBootVersion = resolveProjectVersion('spring-boot')
if (springBootVersion) {
links << [packages: 'org.springframework.boot.', href:
"https://docs.spring.io/spring-boot/docs/${springBootVersion}/api/"]
}
+ def springVersion = resolveProjectVersion('spring-core')
+ if (springVersion) {
+ links << [packages: 'org.springframework.', href:
"https://docs.spring.io/spring-framework/docs/${springVersion}/javadoc-api/"]
+ }
+ def hibernateVersion = resolveProjectVersion('hibernate-core')
+ if (hibernateVersion) {
+ def shortVersion = hibernateVersion.split('\\.').take(2).join('.')
+ links << [packages: 'org.hibernate.', href:
"https://docs.jboss.org/hibernate/orm/${shortVersion}/javadocs/"]
+ }
+ def jakartaValidationVersion =
resolveProjectVersion('jakarta.validation-api')
+ if (jakartaValidationVersion) {
+ def specVersion =
jakartaValidationVersion.split('\\.').take(2).join('.')
+ links << [packages: 'jakarta.validation.', href:
"https://jakarta.ee/specifications/bean-validation/${specVersion}/apidocs/"]
+ }
+ def jakartaPersistenceVersion =
resolveProjectVersion('jakarta.persistence-api')
+ if (jakartaPersistenceVersion) {
+ def specVersion =
jakartaPersistenceVersion.split('\\.').take(2).join('.')
+ links << [packages: 'jakarta.persistence.', href:
"https://jakarta.ee/specifications/persistence/${specVersion}/apidocs/"]
Review Comment:
The Jakarta Persistence apidocs are published as *modular* javadoc, so the
module name is a path segment. Groovydoc builds the href as `href +
type.replace('.', '/') + '.html'` (`SimpleGroovyClassDoc.buildUrl`), which
lands on:
- `.../persistence/3.1/apidocs/jakarta/persistence/Entity.html` → 404
-
`.../persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/Entity.html`
→ 200
Same for 3.2. Appending `jakarta.persistence/` to the href fixes it.
Checked the neighbours while I was there:
`bean-validation/3.1/apidocs/jakarta/validation/Validator.html` and
`platform/11/apidocs/jakarta/servlet/http/HttpServletRequest.html` are both
flat and both 200, so those two entries are correct as written. Hibernate
`orm/{5.6,6.6,7.1}/javadocs/org/hibernate/Session.html` and
`docs.groovy-lang.org/4.0.33/html/gapi/groovy/lang/MetaClass.html` are 200 as
well.
##########
build-logic/docs-core/src/main/groovy/org/grails/doc/gradle/GroovydocLinkAuditor.groovy:
##########
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.grails.doc.gradle
+
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * Scans generated Groovydoc HTML for malformed inner-class links, where a
+ * slash-separated path segment (produced for an inner class, e.g.
+ * {@code Outer/Inner.html}) should have been the dotted Groovydoc naming
+ * convention ({@code Outer.Inner.html}). Kept free of the Gradle API so it
+ * can be unit tested directly - {@code build-logic/docs-core} deliberately
+ * keeps Gradle classes off the test compile classpath.
+ *
+ * <p>This intentionally does not audit the navigation links
(deprecated-list.html,
+ * help-doc.html, etc.): the bottom nav bar that {@code groovy-groovydoc}
emits on
+ * package-summary pages omits the relative-root prefix that every other
occurrence
+ * carries, which is a pre-existing quirk of that template rather than a
content
+ * issue this repository can fix - checking for it would flag hundreds of
instances
+ * of the same known, low-impact tool behavior on every doc build.</p>
+ */
+class GroovydocLinkAuditor {
+
+ // Inner class path issues like Query/Order.Direction.html ->
Query.Order.Direction.html.
+ private static final Pattern INNER_CLASS_LINK_PATTERN =
Review Comment:
Scope question on the audit as a whole.
"0 violations on a full `aggregateGroovydoc` run" isn't evidence the docs
are clean — this pattern only matches slash-separated inner-class paths, and
that shape doesn't appear to be emitted by Groovy 4.0.x. Published inner-class
links are correctly dotted (`org/grails/datastore/mapping/query/Query.html`
links its nested types as `Query.Criterion.html` etc.), and running this exact
regex over a locally generated `:grails-spring:groovydoc` tree matches nothing
— while that same tree contains 20 distinct hrefs pointing at files that were
never generated (`org.springframework.beans.MutablePropertyValues.html`,
`org.apache.commons.logging.Log.html`, …).
So the task as written can essentially only report clean, which makes it
weak insurance against exactly the class of breakage this PR is about.
`findViolationsInFile` already does the right thing structurally — resolve
the href relative to the page's own directory and check the filesystem.
Generalizing that to *every* relative `href` (flag when the resolved target
doesn't exist, skipping absolute/anchor-only hrefs) would catch the phantom
inner-class case, the dangling external-type case, and anything else, with less
code than the current special-casing. That would also give the task something
to say about whether the link config above is doing its job.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]