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