Copilot commented on code in PR #16042:
URL: https://github.com/apache/grails-core/pull/16042#discussion_r3664439766


##########
grails-core/src/main/groovy/org/grails/compiler/injection/GlobalGrailsClassInjectorTransformation.groovy:
##########
@@ -211,153 +231,391 @@ class GlobalGrailsClassInjectorTransformation 
implements ASTTransformation, Comp
         return targetDirectory
     }
 
+    /**
+     * Adds the compiled class to the {@code META-INF/grails.factories} entry 
for the supplied type
+     * when it is a concrete subtype of that type. Existing generated entries 
and matching
+     * project-source entries are preserved, and the resulting factory file is 
written to the
+     * compilation target directory.
+     *
+     * @param classNode the class being compiled
+     * @param superType the factory interface or superclass whose 
implementations are registered
+     * @param compilationTargetDirectory the compilation output directory 
containing the factory file
+     * @return {@code true} when {@code classNode} is a non-abstract subtype 
of {@code superType} and
+     *         was registered; {@code false} otherwise
+     */
     static boolean updateGrailsFactoriesWithType(ClassNode classNode, 
ClassNode superType, File compilationTargetDirectory) {
-        FactoriesFileWriter.updateFactoriesWithType(classNode, superType, 
compilationTargetDirectory,
-                'META-INF/grails.factories', 
['src/main/resources/META-INF/grails.factories'])
+        FactoriesFileWriter.updateFactoriesWithType(
+                classNode,
+                superType,
+                compilationTargetDirectory,
+                'META-INF/grails.factories',
+                ['src/main/resources/META-INF/grails.factories']
+        )
     }
 
-    static LinkedHashSet<String> pendingPluginClasses = []
-    static Collection<String> pluginExcludes = []
+    private static boolean updateGrailsFactoriesWithTypes(ClassNode classNode, 
Collection<ClassNode> superTypes, File compilationTargetDirectory) {
+        superTypes.any {
+            updateGrailsFactoriesWithType(classNode, it, 
compilationTargetDirectory)
+        }
+    }
 
-    protected static void generatePluginXml(ClassNode pluginClassNode, String 
pluginVersion, Set<String> transformedClasses, File pluginXmlFile) {
+    /**
+     * Creates or updates the generated {@code META-INF/grails-plugin.xml} 
descriptor and carries
+     * forward artefact classes collected during compilation.
+     *
+     * @param pluginClassNode the compiled plugin descriptor class, or {@code 
null} when none was found
+     * @param pluginVersion the plugin version, or {@code null} when no 
concrete plugin descriptor
+     *                        is being generated
+     * @param transformedClassNames the artefact classes transformed in the 
current source unit
+     * @param pluginXmlFile the generated plugin descriptor file
+     */
+    protected static void generatePluginXml(
+            @Nullable ClassNode pluginClassNode,
+            @Nullable String pluginVersion,
+            Set<String> transformedClassNames,
+            File pluginXmlFile
+    ) {
+        // first check if plugin.xml exists
+        pluginXmlFile.parentFile.mkdirs()
         def pluginXmlExists = pluginXmlFile.exists()
-        LinkedHashSet<String> pluginClasses = []
-        pluginClasses.addAll(transformedClasses)
-        pluginClasses.addAll(pendingPluginClasses)
-
-        // if the class being transformed is a *GrailsPlugin class then if it 
doesn't exist create it
-        if (pluginClassNode && !pluginClassNode.isAbstract()) {
+        def pluginClasses = [] as LinkedHashSet<String>
+        pluginClasses.addAll(transformedClassNames)
+        pluginClasses.addAll(pendingPluginClassNames)
+
+        // Create or update grails-plugin.xml when a concrete plugin class is 
present; otherwise,
+        // update an existing descriptor or defer resource names until the 
descriptor is compiled.
+        if (pluginClassNode && !pluginClassNode.abstract) {
+            if (!pluginVersion) {
+                throw new IllegalStateException(
+                        "Unable to generate '${pluginXmlFile}' because plugin 
class '${pluginClassNode.name}' " +
+                                'does not define a plugin version.'
+                )
+            }
             if (!pluginXmlExists) {
+                // The plugin descriptor is being compiled for the first time.
                 writePluginXml(pluginClassNode, pluginVersion, pluginXmlFile, 
pluginClasses)
             } else {
-                // otherwise if the file does exist, update it with the plugin 
name
+                // Refresh the existing descriptor with the current plugin 
metadata and resources.
                 updatePluginXml(pluginClassNode, pluginVersion, pluginXmlFile, 
pluginClasses)
             }
         } else if (pluginXmlExists) {
-            // if the class isn't the *GrailsPlugin class then only update the 
plugin.xml if it already exists
+            // Add resources from this source unit to the existing descriptor.
             updatePluginXml(null, pluginVersion, pluginXmlFile, pluginClasses)
         } else {
-            // otherwise add it to a list of pending classes to populated when 
the plugin.xml is created
-            pendingPluginClasses.addAll(transformedClasses)
+            // Defer these resource names until a source unit compiles the 
plugin descriptor.
+            pendingPluginClassNames.addAll(transformedClassNames)
         }
     }
 
+    /**
+     * Writes a new plugin descriptor from the plugin class metadata and 
supplied artefact classes.
+     *
+     * @param pluginClassNode the plugin descriptor class
+     * @param pluginVersion the required plugin version when {@code 
pluginClassNode} is present
+     * @param pluginXml the output descriptor file
+     * @param artefactClassNames artefact class names to include as resources
+     */
     @CompileDynamic
-    static void writePluginXml(ClassNode pluginClassNode, String 
pluginVersion, File pluginXml, Collection<String> artefactClasses) {
+    static void writePluginXml(
+            @Nullable ClassNode pluginClassNode,
+            String pluginVersion,
+            File pluginXml,
+            Collection<String> artefactClassNames
+    ) {
         if (pluginClassNode) {
-            PluginAstReader pluginAstReader = new PluginAstReader()
-            def info = pluginAstReader.readPluginInfo(pluginClassNode)
-
+            def pluginInfo = new 
PluginAstReader().readPluginInfo(pluginClassNode)
             pluginXml.withWriter(StandardCharsets.UTF_8.name()) { Writer 
writer ->
-                def mkp = new MarkupBuilder(writer)
+                def markupBuilder = new MarkupBuilder(writer)
                 def pluginName = 
GrailsNameUtils.getLogicalPropertyName(pluginClassNode.name, 'GrailsPlugin')
-
-                def pluginProperties = info.getProperties()
-                def excludes = pluginProperties.get('pluginExcludes')
-                if (excludes instanceof List) {
-                    pluginExcludes.clear()
-                    pluginExcludes.addAll(excludes)
+                def pluginProperties = pluginInfo.properties
+                def pluginExcludes = pluginProperties.get('pluginExcludes')
+                if (pluginExcludes instanceof List) {
+                    pluginExcludePatterns.clear()
+                    pluginExcludePatterns.addAll(pluginExcludes)
                 }
 
-                def grailsVersion = pluginProperties['grailsVersion'] ?: 
getClass().getPackage().getImplementationVersion() + ' > *'
-                mkp.plugin(name: pluginName, version: pluginVersion, 
grailsVersion: grailsVersion) {
+                // if the plugin class doesn't define a grailsVersion, use the 
version of the grails-core jar
+                def grailsVersion = pluginProperties['grailsVersion'] ?:
+                        
GlobalGrailsClassInjectorTransformation.package.implementationVersion + ' > *'
+
+                markupBuilder.plugin(name: pluginName, version: pluginVersion, 
grailsVersion: grailsVersion) {
                     type(pluginClassNode.name)
 
-                    for (entry in pluginProperties) {
+                    for (def entry : pluginProperties) {
                         delegate."$entry.key"(entry.value)
                     }
 
-                    // if there are pending classes to add to the plugin.xml 
add those
-                    if (artefactClasses) {
-                        def antPathMatcher = new AntPathMatcher()
+                    // if there are pending class names to add to the 
plugin.xml - add them as resources
+                    if (artefactClassNames) {
                         resources {
-                            for (String cn in artefactClasses) {
-                                if (!pluginExcludes.any() { String exc -> 
antPathMatcher.match(exc, cn.replace('.', '/')) }) {
-                                    resource(cn)
+                            for (def artefactClassName : artefactClassNames) {
+                                if 
(!isResourceExcludedByPlugin(artefactClassName)) {
+                                    resource(artefactClassName)
                                 }
                             }
                         }
                     }
                 }
             }
 
-            pendingPluginClasses.clear()
+            pendingPluginClassNames.clear()
         }
     }
 
-    @CompileDynamic
-    static void updatePluginXml(ClassNode pluginClassNode, String 
pluginVersion, File pluginXmlFile, Collection<String> artefactClasses) {
-        if (!artefactClasses) return
-
+    /**
+     * Updates an existing plugin descriptor with plugin metadata and newly 
discovered artefact
+     * resources. If the descriptor cannot be read or written, it is recreated.
+     *
+     * @param pluginClassNode the plugin descriptor class, or {@code null} 
when only resources are updated
+     * @param pluginVersion the plugin version, or {@code null} when only 
resources are updated
+     * @param pluginXmlFile the existing plugin descriptor file
+     * @param artefactClassNames artefact class names to add as resources
+     */
+    static void updatePluginXml(
+            @Nullable ClassNode pluginClassNode,
+            @Nullable String pluginVersion,
+            File pluginXmlFile,
+            Collection<String> artefactClassNames
+    ) {
+        if (!artefactClassNames) return
         try {
-            XmlSlurper xmlSlurper = IOUtils.createXmlSlurper()
-
-            def pluginXml = xmlSlurper.parse(pluginXmlFile)
+            def pluginXml = IOUtils.createXmlSlurper().parse(pluginXmlFile)
             if (pluginClassNode) {
-                def pluginName = 
GrailsNameUtils.getLogicalPropertyName(pluginClassNode.name, 'GrailsPlugin')
-                pluginXml.@name = pluginName
-                pluginXml.@version = pluginVersion
-                pluginXml.type = pluginClassNode.name
-
-                PluginAstReader pluginAstReader = new PluginAstReader()
-                def info = pluginAstReader.readPluginInfo(pluginClassNode)
-
-                def pluginProperties = info.getProperties()
-                def grailsVersion = pluginProperties['grailsVersion'] ?: 
getClass().getPackage().getImplementationVersion() + ' > *'
-                pluginXml.@grailsVersion = grailsVersion
-                for (entry in pluginProperties) {
-                    pluginXml."$entry.key" = entry.value
+                def pluginProperties = 
writePluginXmlProperties(pluginClassNode, pluginVersion, pluginXml)
+                def pluginExcludes = pluginProperties.get('pluginExcludes')
+                if (pluginExcludes instanceof List) {
+                    pluginExcludePatterns.clear()
+                    pluginExcludePatterns.addAll(pluginExcludes as 
List<String>)
                 }
+            }
+            writePluginXmlResources(pluginXml, artefactClassNames)
+            handleExcludes(pluginXml)
 
-                def excludes = pluginProperties.get('pluginExcludes')
-                if (excludes instanceof List) {
-                    pluginExcludes.clear()
-                    pluginExcludes.addAll(excludes)
-                }
+            pluginXmlFile.withWriter(StandardCharsets.UTF_8.name()) {
+                createMarkup(pluginXml).writeTo(it)
             }
 
-            def resources = pluginXml.resources
+            pendingPluginClassNames.clear()
 
-            for (String cn in artefactClasses) {
-                if (!resources.resource.find { it.text() == cn }) {
-                    resources.appendNode {
-                        resource(cn)
-                    }
+        } catch (IOException | ParserConfigurationException | SAXException e) {
+            // Invalid or unreadable description; recreate it
+            log.warn('Failed to update existing file {}. Recreating it 
instead...', pluginXmlFile.absolutePath, e)
+            writePluginXml(pluginClassNode, pluginVersion, pluginXmlFile, 
artefactClassNames)
+        }

Review Comment:
   In updatePluginXml, the catch branch logs that the descriptor is being 
recreated, but calls writePluginXml(...) even when pluginClassNode is null. 
writePluginXml is a no-op in that case, so a malformed plugin.xml remains 
malformed and pendingPluginClassNames is not cleared. This can leave plugin 
discovery broken and can cause pending resources to leak into later 
compilations.



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

Reply via email to