Copilot commented on code in PR #2632:
URL: https://github.com/apache/groovy/pull/2632#discussion_r3485767039


##########
subprojects/groovy-docgenerator/src/main/groovy/org/apache/groovy/docgenerator/JavaExtensionSourceSet.groovy:
##########
@@ -0,0 +1,522 @@
+/*
+ *  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
+ *
+ *    http://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.groovy.docgenerator
+
+import com.github.javaparser.JavaParser
+import com.github.javaparser.ParseProblemException
+import com.github.javaparser.ParseResult
+import com.github.javaparser.ParserConfiguration
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.ImportDeclaration
+import com.github.javaparser.ast.body.BodyDeclaration
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.body.MethodDeclaration
+import com.github.javaparser.ast.body.TypeDeclaration
+import com.github.javaparser.ast.comments.JavadocComment
+import com.github.javaparser.ast.type.ArrayType
+import com.github.javaparser.ast.type.ClassOrInterfaceType
+import com.github.javaparser.ast.type.IntersectionType
+import com.github.javaparser.ast.type.Type
+import com.github.javaparser.ast.type.TypeParameter
+import com.github.javaparser.ast.type.UnionType
+import com.github.javaparser.ast.type.VoidType
+import com.github.javaparser.ast.type.WildcardType
+import org.codehaus.groovy.control.ResolveVisitor
+
+/**
+ * JavaParser-backed view of the extension source files consumed by {@link 
MockSourceGenerator}.
+ */
+final class JavaExtensionSourceSet {
+    private final JavaParser parser = new JavaParser(
+            new 
ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.BLEEDING_EDGE)
+    )
+    private final List<ParsedUnit> units = []
+    private boolean dirty = true
+    private List<JavaExtensionMethod> cachedMethods = []
+    private Map<String, TypeDeclaration<?>> declarationsByFqcn = [:]
+    private Map<String, Set<String>> simpleNameIndex = [:].withDefault { new 
LinkedHashSet<String>() }
+    private Set<String> knownTypes = new LinkedHashSet<>()
+    private Map<CompilationUnit, JavaExtensionContext> contextsByUnit = [:]
+
+    /**
+     * Adds one extension source file to the in-memory model.
+     *
+     * @param file Java source file containing extension methods
+     */
+    void addSource(File file) {
+        if (!file?.exists()) return
+
+        ParseResult<CompilationUnit> result
+        try {
+            result = parser.parse(file)
+        } catch (ParseProblemException e) {
+            throw new IllegalStateException("Unable to parse ${file}: 
${e.message}", e)
+        }
+        if (!result.result.present) {
+            def details = result.problems.collect { it.toString() 
}.join(System.lineSeparator())
+            throw new IllegalStateException("Unable to parse 
${file}:${System.lineSeparator()}${details}")
+        }
+
+        units << new ParsedUnit(
+                file: file,
+                compilationUnit: result.result.get()
+        )
+        dirty = true
+    }
+
+    /**
+     * Returns all parsed extension methods, rebuilding cached structures only 
when
+     * sources have changed.
+     */
+    List<JavaExtensionMethod> getMethods() {
+        rebuildIfNeeded()
+        cachedMethods
+    }
+
+    /**
+     * Resolves receiver metadata for the supplied fully-qualified class name.
+     *
+     * @param fqcn receiver type name (arrays allowed)
+     * @return receiver metadata including primitive/interface markers
+     */
+    ReceiverTypeInfo typeInfoForFqcn(String fqcn) {
+        rebuildIfNeeded()
+        resolveTypeInfo(fqcn)
+    }
+
+    private ReceiverTypeInfo resolveTypeInfo(String fqcn) {
+        String base = stripArraySuffix(fqcn)
+        if (!base) {
+            return new ReceiverTypeInfo(canonicalName: fqcn, primitive: false, 
interfaceType: false)
+        }
+
+        if (PRIMITIVES.contains(base)) {
+            return new ReceiverTypeInfo(canonicalName: fqcn, primitive: true, 
interfaceType: false)
+        }
+
+        TypeDeclaration<?> declaration = declarationsByFqcn[base]
+        if (declaration instanceof ClassOrInterfaceDeclaration) {
+            return new ReceiverTypeInfo(canonicalName: fqcn, primitive: false, 
interfaceType: declaration.isInterface())
+        }
+
+        Class<?> resolvedClass = tryLoad(base)
+        new ReceiverTypeInfo(
+                canonicalName: fqcn,
+                primitive: false,
+                interfaceType: resolvedClass?.isInterface() ?: false
+        )
+    }
+
+    private void rebuildIfNeeded() {
+        if (!dirty) return
+
+        declarationsByFqcn = [:]
+        simpleNameIndex = [:].withDefault { new LinkedHashSet<String>() }
+        knownTypes = new LinkedHashSet<>()
+        contextsByUnit = [:]
+        cachedMethods = []
+
+        units.each { ParsedUnit unit ->
+            String packageName = unit.compilationUnit.packageDeclaration.map { 
it.nameAsString }.orElse('')
+            unit.compilationUnit.types.each { TypeDeclaration<?> declaration ->
+                collectTypes(declaration, packageName, null)
+            }
+        }
+
+        knownTypes.addAll(declarationsByFqcn.keySet())
+        declarationsByFqcn.keySet().each { String fqcn ->
+            simpleNameIndex[simpleNameOf(fqcn)] << fqcn
+        }
+
+        units.each { ParsedUnit unit ->
+            contextsByUnit[unit.compilationUnit] = new 
JavaExtensionContext(unit.compilationUnit, declarationsByFqcn, simpleNameIndex, 
knownTypes)
+        }
+
+        declarationsByFqcn.each { String fqcn, TypeDeclaration<?> declaration 
->
+            JavaExtensionContext context = 
contextsByUnit[declaration.findCompilationUnit().orElse(null)]
+            declaration.members.findAll {
+                it instanceof MethodDeclaration
+            }.each { MethodDeclaration method ->
+                cachedMethods << createMethodView(method, declaration, context)
+            }
+        }
+        dirty = false
+    }
+
+    private void collectTypes(TypeDeclaration<?> declaration, String 
packageName, String ownerFqcn) {
+        String fqcn = ownerFqcn ? ownerFqcn + '.' + declaration.nameAsString
+                : (packageName ? packageName + '.' + declaration.nameAsString 
: declaration.nameAsString)
+        declarationsByFqcn[fqcn] = declaration
+        declaration.members.findAll { BodyDeclaration member ->
+            member instanceof TypeDeclaration
+        }.each { TypeDeclaration nested ->
+            collectTypes(nested, packageName, fqcn)
+        }
+    }
+
+    private JavaExtensionMethod createMethodView(MethodDeclaration method, 
TypeDeclaration<?> declaringType, JavaExtensionContext context) {
+        def ownerTypeParameters = declaringType.respondsTo('getTypeParameters')
+                ? declaringType.typeParameters*.nameAsString
+                : []
+        def scopeTypeParameters = new 
LinkedHashSet<String>(ownerTypeParameters)
+        scopeTypeParameters.addAll(method.typeParameters*.nameAsString)
+
+        List<JavaExtensionParameter> parameters = method.parameters.collect { 
parameter ->
+            new JavaExtensionParameter(
+                    name: parameter.nameAsString,
+                    type: context.renderType(parameter.type, 
scopeTypeParameters, parameter.varArgs),
+                    varArgs: parameter.varArgs
+            )
+        }
+
+        String receiverTypeName = parameters
+                ? 
MockSourceGenerator.resolveJdkClassName(context.eraseType(method.parameters[0].type,
 scopeTypeParameters))
+                : null
+
+        new JavaExtensionMethod(
+                name: method.nameAsString,
+                declaringClassName: declaringType.nameAsString,
+                publicMethod: method.isPublic(),
+                staticMethod: method.isStatic(),
+                deprecated: method.annotations.any { 
context.resolveTypeName(it.nameAsString) == 'java.lang.Deprecated' },
+                receiverTypeName: receiverTypeName,
+                receiverTypeInfo: receiverTypeName ? 
resolveTypeInfo(receiverTypeName) : null,
+                returnType: context.renderType(method.type, 
scopeTypeParameters),
+                parameters: parameters,
+                typeParameters: method.typeParameters.collect {
+                    context.renderTypeParameter(it, scopeTypeParameters)
+                },
+                exceptions: method.thrownExceptions.collect {
+                    context.renderType(it, scopeTypeParameters)
+                },
+                javadoc: JavadocInfo.parse(method.javadocComment.orElse(null))
+        )
+    }
+
+    private static String stripArraySuffix(String name) {
+        name?.replaceAll(/(\[\])+$/, '')
+    }
+
+    private static String simpleNameOf(String fqcn) {
+        int dot = fqcn.lastIndexOf('.')
+        dot < 0 ? fqcn : fqcn.substring(dot + 1)
+    }
+
+    /**
+     * Attempts to load a type by canonical name, falling back to nested-class
+     * binary names ({@code Outer$Inner}) when needed.
+     *
+     * @param candidate candidate canonical type name
+     * @return loaded class, or {@code null} when resolution fails
+     */
+    static Class<?> tryLoad(String candidate) {
+        if (!candidate) return null
+        try {
+            return Class.forName(candidate)
+        } catch (Throwable ignored) {
+            int lastDot = candidate.lastIndexOf('.')
+            while (lastDot > 0) {
+                candidate = candidate.substring(0, lastDot) + '$' + 
candidate.substring(lastDot + 1)
+                try {
+                    return Class.forName(candidate)
+                } catch (Throwable ignoredAgain) {
+                    lastDot = candidate.lastIndexOf('.')
+                }
+            }
+            return null
+        }
+    }
+
+    private static final Set<String> PRIMITIVES = [
+            'boolean', 'byte', 'char', 'double', 'float', 'int', 'long', 
'short'
+    ] as Set<String>
+}
+
+/**
+ * Type rendering and name-resolution context derived from one compilation 
unit.
+ */
+final class JavaExtensionContext {
+    private static final Set<String> PRIMITIVES = [
+            'boolean', 'byte', 'char', 'double', 'float', 'int', 'long', 
'short'
+    ] as Set<String>
+
+    private final Map<String, String> explicitImports = [:]
+    private final List<String> wildcardImports = []
+    private final Map<String, TypeDeclaration<?>> declarationsByFqcn
+    private final Map<String, Set<String>> simpleNameIndex
+    private final Set<String> knownTypes
+    private final String packageName
+
+    /**
+     * Creates a rendering context with import/package/type indexes for one 
unit.
+     */
+    JavaExtensionContext(
+            CompilationUnit compilationUnit,
+            Map<String, TypeDeclaration<?>> declarationsByFqcn,
+            Map<String, Set<String>> simpleNameIndex,
+            Set<String> knownTypes
+    ) {
+        this.declarationsByFqcn = declarationsByFqcn
+        this.simpleNameIndex = simpleNameIndex
+        this.knownTypes = knownTypes
+        this.packageName = compilationUnit.packageDeclaration.map { 
it.nameAsString }.orElse('')
+
+        compilationUnit.imports.each { ImportDeclaration importDeclaration ->
+            if (importDeclaration.isStatic()) return
+            if (importDeclaration.asterisk) {
+                wildcardImports << importDeclaration.nameAsString
+            } else {
+                explicitImports[importDeclaration.name.identifier] = 
importDeclaration.nameAsString
+            }
+        }
+    }
+
+    /**
+     * Renders a JavaParser type to the canonical textual form expected by
+     * mock-source generation.
+     */
+    String renderType(Type type, Set<String> typeParameterNames = 
Collections.emptySet(), boolean varArgs = false) {
+        String rendered
+        if (type instanceof VoidType) {
+            rendered = 'void'
+        } else if (type instanceof ArrayType) {
+            rendered = renderType(type.componentType, typeParameterNames) + 
'[]'
+        } else if (type instanceof ClassOrInterfaceType) {
+            rendered = renderClassOrInterfaceType((ClassOrInterfaceType) type, 
typeParameterNames)
+        } else if (type instanceof WildcardType) {
+            def wildcard = (WildcardType) type
+            if (wildcard.extendedType.present) {
+                rendered = '? extends ' + 
renderType(wildcard.extendedType.get(), typeParameterNames)
+            } else if (wildcard.superType.present) {
+                rendered = '? super ' + renderType(wildcard.superType.get(), 
typeParameterNames)
+            } else {
+                rendered = '?'
+            }
+        } else if (type instanceof IntersectionType) {
+            rendered = ((IntersectionType) type).elements.collect { 
renderType(it, typeParameterNames) }.join(' & ')
+        } else if (type instanceof UnionType) {
+            rendered = ((UnionType) type).elements.collect { renderType(it, 
typeParameterNames) }.join(' | ')
+        } else {
+            rendered = type.toString()
+        }
+
+        if (varArgs && rendered.endsWith('[]')) {
+            rendered = rendered[0..-3] + '...'
+        }
+        rendered
+    }
+
+    /**
+     * Renders the erased type name used for receiver bucketing.
+     */
+    String eraseType(Type type, Set<String> typeParameterNames = 
Collections.emptySet()) {
+        if (type instanceof ArrayType) {
+            return eraseType(type.componentType, typeParameterNames) + '[]'
+        }
+        if (type instanceof ClassOrInterfaceType) {
+            def classType = (ClassOrInterfaceType) type
+            if (classType.scope.present) {
+                return rawScopedName(classType.scope.get(), 
typeParameterNames) + '.' + classType.nameAsString
+            }
+            return resolveSimpleName(classType.nameAsString, 
typeParameterNames)
+        }
+        type.toString()
+    }
+
+    /**
+     * Renders a type-parameter declaration with resolved bounds.
+     */
+    String renderTypeParameter(TypeParameter typeParameter, Set<String> 
inheritedTypeParameters) {
+        def scopeTypeParameters = new 
LinkedHashSet<String>(inheritedTypeParameters)
+        scopeTypeParameters << typeParameter.nameAsString
+
+        def rendered = new StringBuilder(typeParameter.nameAsString)
+        if (!typeParameter.typeBound.empty) {
+            rendered << ' extends ' << typeParameter.typeBound.collect {
+                renderType(it, scopeTypeParameters)
+            }.join(' & ')
+        }
+        rendered.toString()
+    }
+
+    /**
+     * Resolves a possibly-short type name against explicit imports, wildcard
+     * imports, same-package types, default imports, and known parsed symbols.
+     */
+    String resolveTypeName(String name, Set<String> typeParameterNames = 
Collections.emptySet()) {
+        if (!name) return name
+        if (PRIMITIVES.contains(name) || name == 'void' || 
typeParameterNames.contains(name)) return name
+
+        int dot = name.indexOf('.')
+        if (dot < 0) return resolveSimpleName(name, typeParameterNames)
+
+        String first = name.substring(0, dot)
+        if (!Character.isUpperCase(first.charAt(0)) && 
!explicitImports.containsKey(first)) {
+            return name
+        }
+
+        String resolvedFirst = resolveSimpleName(first, typeParameterNames)
+        resolvedFirst == first ? name : resolvedFirst + name.substring(dot)
+    }
+
+    private String renderClassOrInterfaceType(ClassOrInterfaceType type, 
Set<String> typeParameterNames) {
+        String name
+        if (type.scope.present) {
+            name = rawScopedName(type.scope.get(), typeParameterNames) + '.' + 
type.nameAsString
+        } else {
+            name = resolveSimpleName(type.nameAsString, typeParameterNames)
+        }
+
+        if (!type.typeArguments.present || type.typeArguments.get().empty) {
+            return name
+        }
+        name + '<' + type.typeArguments.get().collect { renderType(it, 
typeParameterNames) }.join(', ') + '>'
+    }
+
+    private String rawScopedName(ClassOrInterfaceType type, Set<String> 
typeParameterNames) {
+        if (type.scope.present) {
+            return rawScopedName(type.scope.get(), typeParameterNames) + '.' + 
type.nameAsString
+        }
+        resolveSimpleName(type.nameAsString, typeParameterNames)
+    }
+
+    private String resolveSimpleName(String name, Set<String> 
typeParameterNames) {
+        if (PRIMITIVES.contains(name) || name == 'void' || 
typeParameterNames.contains(name)) return name
+        if (explicitImports.containsKey(name)) return explicitImports[name]
+
+        if (packageName) {
+            String candidate = packageName + '.' + name
+            if (knownType(candidate)) return candidate
+        }
+
+        for (String importPrefix : wildcardImports) {
+            String candidate = importPrefix + '.' + name
+            if (knownType(candidate)) return candidate
+        }
+
+        for (String defaultImport : ResolveVisitor.DEFAULT_IMPORTS) {
+            String candidate = defaultImport + name
+            if (knownType(candidate)) return candidate
+        }
+
+        Set<String> matches = simpleNameIndex[name]
+        if (matches?.size() == 1) return matches.first()
+
+        name
+    }
+
+    private boolean knownType(String candidate) {
+        knownTypes.contains(candidate) || 
JavaExtensionSourceSet.tryLoad(candidate) != null
+    }

Review Comment:
   `knownType` calls `JavaExtensionSourceSet.tryLoad(candidate)` repeatedly for 
every unresolved type lookup. When parsing large extension sources (e.g. 
DefaultGroovyMethods), this can become unnecessarily expensive due to repeated 
`Class.forName` attempts. Consider caching successful loads by adding them to 
`knownTypes` so subsequent resolutions avoid reflection/exceptions.



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