This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch feat/publish-groovydoc-plugin
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 9115b7575364d29285db66d14a4202dda77188dd
Author: James Fredley <[email protected]>
AuthorDate: Sat Feb 21 11:12:02 2026 -0500

    feat: publish GroovydocEnhancerPlugin for use in end-user Grails apps
    
    Add grails-gradle-groovydoc as a published Gradle plugin module that
    enables Grails applications using modern Java features (17+) to generate
    accurate Groovydoc documentation. Without this plugin, Groovydoc fails
    to parse Java sources using sealed classes, records, pattern matching,
    and other post-Java 11 language features.
    
    Changes:
    - New module grails-gradle/groovydoc with GroovydocEnhancerPlugin
      published as org.apache.grails.gradle:grails-gradle-groovydoc
      with plugin ID org.apache.grails.gradle.groovydoc
    - Forge DefaultFeature adds the plugin to all generated Grails apps
    - Profile base/profile.yml applies the plugin via grails CLI generation
    - grails-bom updated with the new artifact for BOM version management
    - build-logic retains its own copy for the framework build (bootstrap)
    
    Assisted-by: Claude Code <[email protected]>
---
 grails-bom/build.gradle                            |   1 +
 .../forge/feature/groovydoc/GroovydocEnhancer.java |  88 +++++++++
 grails-gradle/gradle/publish-root-config.gradle    |   1 +
 grails-gradle/groovydoc/build.gradle               |  72 +++++++
 .../groovydoc/GroovydocEnhancerExtension.groovy    |  49 +++++
 .../groovydoc/GroovydocEnhancerPlugin.groovy       | 208 +++++++++++++++++++++
 grails-gradle/settings.gradle                      |   3 +
 grails-profiles/base/profile.yml                   |   3 +
 8 files changed, 425 insertions(+)

diff --git a/grails-bom/build.gradle b/grails-bom/build.gradle
index 260239fced..74042673f0 100644
--- a/grails-bom/build.gradle
+++ b/grails-bom/build.gradle
@@ -44,6 +44,7 @@ ext {
     // TODO: It should be possible to pull these build names using 
includedBuild, but I haven't found a way to do so
     gradleBuildProjects = [
             'grails-gradle-plugins':'org.apache.grails',
+            'grails-gradle-groovydoc':'org.apache.grails.gradle',
             'grails-gradle-model':'org.apache.grails.gradle',
             'grails-gradle-common':'org.apache.grails.gradle',
             'grails-gradle-tasks':'org.apache.grails',
diff --git 
a/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/groovydoc/GroovydocEnhancer.java
 
b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/groovydoc/GroovydocEnhancer.java
new file mode 100644
index 0000000000..d040c44662
--- /dev/null
+++ 
b/grails-forge/grails-forge-core/src/main/java/org/grails/forge/feature/groovydoc/GroovydocEnhancer.java
@@ -0,0 +1,88 @@
+/*
+ *  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.forge.feature.groovydoc;
+
+import io.micronaut.core.annotation.NonNull;
+import jakarta.inject.Singleton;
+import org.grails.forge.application.ApplicationType;
+import org.grails.forge.application.generator.GeneratorContext;
+import org.grails.forge.build.dependencies.Dependency;
+import org.grails.forge.build.gradle.GradlePlugin;
+import org.grails.forge.feature.Category;
+import org.grails.forge.feature.DefaultFeature;
+import org.grails.forge.feature.Feature;
+import org.grails.forge.options.Options;
+
+import java.util.Set;
+
+@Singleton
+public class GroovydocEnhancer implements DefaultFeature {
+
+    @NonNull
+    @Override
+    public String getName() {
+        return "groovydoc-enhancer";
+    }
+
+    @Override
+    public String getTitle() {
+        return "Groovydoc Enhancer";
+    }
+
+    @NonNull
+    @Override
+    public String getDescription() {
+        return "Enables Groovydoc generation for projects using modern Java 
features (17+). " +
+                "Without this plugin, Groovydoc fails to parse Java sources 
that use sealed classes, " +
+                "records, pattern matching, and other post-Java 11 language 
features.";
+    }
+
+    @Override
+    public boolean supports(ApplicationType applicationType) {
+        return true;
+    }
+
+    @Override
+    public boolean isVisible() {
+        return false;
+    }
+
+    @Override
+    public String getCategory() {
+        return Category.DOCUMENTATION;
+    }
+
+    @Override
+    public boolean shouldApply(ApplicationType applicationType, Options 
options, Set<Feature> selectedFeatures) {
+        return true;
+    }
+
+    @Override
+    public void apply(GeneratorContext generatorContext) {
+        generatorContext.addBuildscriptDependency(Dependency.builder()
+                .groupId("org.apache.grails.gradle")
+                .artifactId("grails-gradle-groovydoc")
+                .buildSrc());
+
+        generatorContext.addBuildPlugin(GradlePlugin.builder()
+                .id("org.apache.grails.gradle.groovydoc")
+                .useApplyPlugin(true)
+                .build());
+    }
+}
diff --git a/grails-gradle/gradle/publish-root-config.gradle 
b/grails-gradle/gradle/publish-root-config.gradle
index 4282dbfe20..dba8346e06 100644
--- a/grails-gradle/gradle/publish-root-config.gradle
+++ b/grails-gradle/gradle/publish-root-config.gradle
@@ -26,6 +26,7 @@ group = 'this.will.be.overridden'
 def publishedProjects = [
         'grails-gradle-bom',
         'grails-gradle-common',
+        'grails-gradle-groovydoc',
         'grails-gradle-model',
         'grails-gradle-plugins',
         'grails-gradle-tasks',
diff --git a/grails-gradle/groovydoc/build.gradle 
b/grails-gradle/groovydoc/build.gradle
new file mode 100644
index 0000000000..105c3d522c
--- /dev/null
+++ b/grails-gradle/groovydoc/build.gradle
@@ -0,0 +1,72 @@
+/*
+ *  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.
+ */
+
+plugins {
+    id 'groovy'
+    id 'java-gradle-plugin'
+    id 'org.apache.grails.buildsrc.properties'
+    id 'org.apache.grails.buildsrc.compile'
+    id 'org.apache.grails.buildsrc.publish'
+    id 'org.apache.grails.buildsrc.sbom'
+    id 'org.apache.grails.gradle.grails-code-style'
+}
+
+version = projectVersion
+group = 'org.apache.grails.gradle'
+
+ext {
+    pomTitle = 'Grails Gradle Groovydoc Enhancer Plugin'
+    pomDescription = 'A Gradle plugin that enhances Groovydoc generation to 
support modern Java source levels, allowing Grails applications using Java 17+ 
features to generate accurate Groovydoc documentation'
+    pomMavenPublicationName = 'pluginMaven'
+}
+
+dependencies {
+    implementation platform(project(':grails-gradle-bom'))
+
+    // compile with the Groovy version provided by Gradle
+    // to ensure build compatibility with Gradle, currently Groovy 3.0.x
+    // see: https://docs.gradle.org/current/userguide/compatibility.html#groovy
+    compileOnly "org.codehaus.groovy:groovy"
+
+    // Testing - Gradle TestKit is auto-added by java-gradle-plugin
+    testImplementation('org.spockframework:spock-core') { transitive = false }
+    testImplementation 'org.codehaus.groovy:groovy-test-junit5'
+    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
+}
+
+configurations {
+    testCompileClasspath.exclude group: 'org.apache.groovy', module: 'groovy'
+    testRuntimeClasspath.exclude group: 'org.apache.groovy', module: 'groovy'
+}
+
+gradlePlugin {
+    plugins {
+        groovydoc {
+            displayName = 'Grails Groovydoc Enhancer Plugin'
+            description = 'Enhances Groovydoc generation with Java source 
level support for modern Java features (17+)'
+            id = 'org.apache.grails.gradle.groovydoc'
+            implementationClass = 
'org.apache.grails.gradle.groovydoc.GroovydocEnhancerPlugin'
+        }
+    }
+}
+
+apply {
+    from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle')
+    from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
+}
diff --git 
a/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerExtension.groovy
 
b/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerExtension.groovy
new file mode 100644
index 0000000000..eaeab01036
--- /dev/null
+++ 
b/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerExtension.groovy
@@ -0,0 +1,49 @@
+/*
+ *  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.apache.grails.gradle.groovydoc
+
+import javax.inject.Inject
+
+import groovy.transform.CompileStatic
+
+import org.gradle.api.Project
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.Property
+
+@CompileStatic
+class GroovydocEnhancerExtension {
+
+    final Property<String> javaVersion
+
+    final Property<Boolean> javaVersionEnabled
+
+    final Property<Boolean> useAntBuilder
+
+    final Property<String> footer
+
+    @Inject
+    GroovydocEnhancerExtension(ObjectFactory objects, Project project) {
+        javaVersion = objects.property(String).convention(
+                project.provider { "JAVA_${project.findProperty('javaVersion') 
?: '17'}" as String }
+        )
+        javaVersionEnabled = objects.property(Boolean).convention(true)
+        useAntBuilder = objects.property(Boolean).convention(true)
+        footer = objects.property(String).convention('')
+    }
+}
diff --git 
a/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerPlugin.groovy
 
b/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerPlugin.groovy
new file mode 100644
index 0000000000..219ab89159
--- /dev/null
+++ 
b/grails-gradle/groovydoc/src/main/groovy/org/apache/grails/gradle/groovydoc/GroovydocEnhancerPlugin.groovy
@@ -0,0 +1,208 @@
+/*
+ *  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.apache.grails.gradle.groovydoc
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.attributes.Bundling
+import org.gradle.api.attributes.Category
+import org.gradle.api.attributes.Usage
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.SourceSetContainer
+import org.gradle.api.tasks.javadoc.Groovydoc
+
+/**
+ * A Gradle plugin that enhances Groovydoc generation to support modern Java
+ * source levels. Gradle's built-in {@link Groovydoc} task does not expose
+ * the {@code javaVersion} parameter (added in Groovy 4.0.27 via
+ * <a 
href="https://issues.apache.org/jira/browse/GROOVY-11668";>GROOVY-11668</a>),
+ * so projects using Java 17+ features (sealed classes, records, etc.) fail
+ * to generate Groovydoc.
+ *
+ * <p>This plugin replaces the built-in task execution with a direct AntBuilder
+ * invocation that passes the {@code javaVersion} parameter, enabling accurate
+ * Groovydoc generation for modern Java source levels.</p>
+ *
+ * <p>Configure via the {@code groovydocEnhancer} extension:</p>
+ * <pre>
+ * groovydocEnhancer {
+ *     javaVersion = 'JAVA_17'       // Java source level for parsing
+ *     javaVersionEnabled = true      // set false for Groovy < 4.0.27
+ *     useAntBuilder = true           // set false when Gradle adds native 
support
+ *     footer = '&lt;p&gt;My Footer&lt;/p&gt;'
+ * }
+ * </pre>
+ *
+ * @since 7.0.8
+ * @see GroovydocEnhancerExtension
+ * @see <a 
href="https://github.com/gradle/gradle/issues/33659";>gradle#33659</a>
+ */
+@CompileStatic
+class GroovydocEnhancerPlugin implements Plugin<Project> {
+
+    @Override
+    void apply(Project project) {
+        GroovydocEnhancerExtension extension = project.extensions.create(
+                'groovydocEnhancer',
+                GroovydocEnhancerExtension,
+                project
+        )
+        registerDocumentationConfiguration(project)
+        configureGroovydocDefaults(project, extension)
+        configureAntBuilderExecution(project, extension)
+    }
+
+    private static void registerDocumentationConfiguration(Project project) {
+        if (project.configurations.names.contains('documentation')) {
+            return
+        }
+        project.configurations.register('documentation') {
+            it.canBeConsumed = false
+            it.canBeResolved = true
+            it.attributes {
+                it.attribute(Category.CATEGORY_ATTRIBUTE, 
project.objects.named(Category, Category.LIBRARY))
+                it.attribute(Bundling.BUNDLING_ATTRIBUTE, 
project.objects.named(Bundling, Bundling.EXTERNAL))
+                it.attribute(Usage.USAGE_ATTRIBUTE, 
project.objects.named(Usage, Usage.JAVA_RUNTIME))
+            }
+        }
+    }
+
+    @CompileDynamic
+    private static void configureGroovydocDefaults(Project project, 
GroovydocEnhancerExtension extension) {
+        project.tasks.withType(Groovydoc).configureEach {
+            it.includeAuthor.set(false)
+            it.includeMainForScripts.set(false)
+            it.processScripts.set(false)
+            it.noTimestamp = true
+            it.noVersionStamp = false
+            def footerValue = extension.footer.getOrElse('')
+            if (footerValue) {
+                it.footer = footerValue
+            }
+            if (project.configurations.names.contains('documentation')) {
+                it.groovyClasspath = 
project.configurations.getByName('documentation')
+            }
+        }
+    }
+
+    @CompileDynamic
+    private static void configureAntBuilderExecution(Project project, 
GroovydocEnhancerExtension extension) {
+        project.tasks.withType(Groovydoc).configureEach { gdoc ->
+            if (!extension.useAntBuilder.get()) {
+                return
+            }
+
+            gdoc.actions.clear()
+            gdoc.doLast {
+                def destDir = gdoc.destinationDir.tap { it.mkdirs() }
+                def sourceDirs = resolveSourceDirectories(gdoc, project)
+                if (sourceDirs.isEmpty()) {
+                    project.logger.lifecycle(
+                            'Skipping groovydoc for {}: no source directories 
found',
+                            gdoc.name
+                    )
+                    return
+                }
+
+                def docConfig = 
project.configurations.findByName('documentation')
+                if (!docConfig) {
+                    project.logger.warn(
+                            'Skipping groovydoc for {}: \'documentation\' 
configuration not found',
+                            gdoc.name
+                    )
+                    return
+                }
+
+                project.ant.taskdef(
+                        name: 'groovydoc',
+                        classname: 'org.codehaus.groovy.ant.Groovydoc',
+                        classpath: docConfig.asPath
+                )
+
+                def links = resolveLinks(gdoc)
+                def sourcepath = sourceDirs
+                        .collect { it.absolutePath }
+                        .join(File.pathSeparator)
+
+                def antArgs = [
+                        destdir: destDir.absolutePath,
+                        sourcepath: sourcepath,
+                        packagenames: '**.*',
+                        windowtitle: gdoc.windowTitle ?: '',
+                        doctitle: gdoc.docTitle ?: '',
+                        footer: gdoc.footer ?: '',
+                        access: 
resolveGroovydocProperty(gdoc.access)?.name()?.toLowerCase() ?: 'protected',
+                        author: resolveGroovydocProperty(gdoc.includeAuthor) 
as String,
+                        noTimestamp: 
resolveGroovydocProperty(gdoc.noTimestamp) as String,
+                        noVersionStamp: 
resolveGroovydocProperty(gdoc.noVersionStamp) as String,
+                        processScripts: 
resolveGroovydocProperty(gdoc.processScripts) as String,
+                        includeMainForScripts: 
resolveGroovydocProperty(gdoc.includeMainForScripts) as String
+                ]
+
+                if (extension.javaVersionEnabled.get()) {
+                    antArgs.put('javaVersion', extension.javaVersion.get())
+                }
+
+                project.ant.groovydoc(antArgs) {
+                    for (var l in links) {
+                        link(packages: l.packages, href: l.href)
+                    }
+                }
+            }
+        }
+    }
+
+    @CompileDynamic
+    private static List<File> resolveSourceDirectories(Groovydoc gdoc, Project 
project) {
+        if (gdoc.ext.has('groovydocSourceDirs') && 
gdoc.ext.groovydocSourceDirs) {
+            return (gdoc.ext.groovydocSourceDirs as List<File>)
+                    .findAll { it.exists() }
+                    .unique()
+        }
+
+        List<File> sourceDirs = []
+        def sourceSets = project.extensions.findByType(SourceSetContainer)
+        if (sourceSets) {
+            def mainSS = sourceSets.findByName('main')
+            if (mainSS) {
+                sourceDirs.addAll(mainSS.groovy.srcDirs.findAll { it.exists() 
})
+                sourceDirs.addAll(mainSS.java.srcDirs.findAll { it.exists() })
+            }
+        }
+        sourceDirs.unique()
+    }
+
+    @CompileDynamic
+    private static List<Map<String, String>> resolveLinks(Groovydoc gdoc) {
+        if (gdoc.ext.has('groovydocLinks')) {
+            return gdoc.ext.groovydocLinks as List<Map<String, String>>
+        }
+        []
+    }
+
+    static Object resolveGroovydocProperty(Object value) {
+        if (value instanceof Provider) {
+            return ((Provider) value).getOrNull()
+        }
+        value
+    }
+}
diff --git a/grails-gradle/settings.gradle b/grails-gradle/settings.gradle
index 7d0b8a0a1b..64a7bccebf 100644
--- a/grails-gradle/settings.gradle
+++ b/grails-gradle/settings.gradle
@@ -74,3 +74,6 @@ project(':grails-gradle-model').projectDir = file('model')
 
 include 'grails-gradle-tasks'
 project(':grails-gradle-tasks').projectDir = file('tasks')
+
+include 'grails-gradle-groovydoc'
+project(':grails-gradle-groovydoc').projectDir = file('groovydoc')
diff --git a/grails-profiles/base/profile.yml b/grails-profiles/base/profile.yml
index f1b9b54e29..4d42ea9d40 100644
--- a/grails-profiles/base/profile.yml
+++ b/grails-profiles/base/profile.yml
@@ -30,7 +30,10 @@ build:
         - eclipse
         - idea
         - org.apache.grails.gradle.grails-app
+        - org.apache.grails.gradle.groovydoc
 dependencies:
+    - scope: build
+      coords: "org.apache.grails.gradle:grails-gradle-groovydoc"
     - scope: build
       coords: "org.apache.grails:grails-gradle-plugins"
     - scope: developmentOnly

Reply via email to