[ 
https://issues.apache.org/jira/browse/GROOVY-12096?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18092009#comment-18092009
 ] 

ASF GitHub Bot commented on GROOVY-12096:
-----------------------------------------

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.





> Replace qdox with javaparser
> ----------------------------
>
>                 Key: GROOVY-12096
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12096
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Daniel Sun
>            Priority: Major
>
> QDox status:
> {code:java}
> 2026: Teams relying on QDox should probably migrate to JavaParser, or Spoon, 
> Rewrite, or Roaster which all have active development still. We're not going 
> to make further changes to QDox nor respond to new issues or PRs. {code}
>  
> https://github.com/paul-hammant/qdox



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to