You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/06/26 19:58:08 UTC
(groovy) 01/01: GROOVY-12096: Replace qdox with javaparser
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch GROOVY-12096
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 6ffa1a16efc1c8f3fc9530073ce787b475351295
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Jun 27 04:57:30 2026 +0900
GROOVY-12096: Replace qdox with javaparser
---
build.gradle | 4 +-
gradle/verification-metadata.xml | 7 -
.../tools/stubgenerator/JavaParserCategory.groovy | 53 ++
.../tools/stubgenerator/JavaParserSourceSet.groovy | 691 +++++++++++++++++++++
.../groovy/tools/stubgenerator/QDoxCategory.groovy | 145 -----
.../groovy/tools/stubgenerator/StubTestCase.groovy | 27 +-
subprojects/groovy-docgenerator/build.gradle | 2 +-
.../docgenerator/JavaExtensionSourceSet.groovy | 453 ++++++++++++++
.../groovy/docgenerator/MockSourceGenerator.groovy | 102 ++-
.../docgenerator/MockSourceGeneratorTest.groovy | 116 ++++
versions.properties | 1 -
11 files changed, 1372 insertions(+), 229 deletions(-)
diff --git a/build.gradle b/build.gradle
index 6b2ae87426..87b5c3a3ad 100644
--- a/build.gradle
+++ b/build.gradle
@@ -127,7 +127,7 @@ dependencies {
testImplementation projects.groovyDateutil
testImplementation projects.groovyTestJunit6 // for groovy.junit6.plugin.ForkedJvm
testImplementation "net.jcip:jcip-annotations:${versions.jcipAnnotations}"
- testImplementation "com.thoughtworks.qdox:qdox:${versions.qdox}"
+ testImplementation "com.github.javaparser:javaparser-core:${versions.javaParser}"
testImplementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
testImplementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${versions.jackson}"
@@ -147,8 +147,6 @@ dependencies {
tools "org.codehaus.plexus:plexus-utils:4.0.3"
// end
tools "org.ow2.asm:asm:${versions.asm}"
- tools "com.thoughtworks.qdox:qdox:${versions.qdox}"
-
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0'
testRuntimeOnly(project(':')) {
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index f50996fb25..1ce132ad91 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -174,7 +174,6 @@
<trusted-key id="74CEFDCEADFB95278EDEDF71E5B3BB879D1EC979" group="me.sunlan"/>
<trusted-key id="76E94E8FF0AB5AF3B6F8366972FEFD1572EB75E1" group="org.spockframework"/>
<trusted-key id="7B79ADD11F8A779FE90FD3D0893A028475557671" group="com.gradle"/>
- <trusted-key id="7E6557B314B189FD4EA5B680FE66BCA17E67AE6F" group="com.thoughtworks.qdox" name="qdox" version="2.2.0"/>
<trusted-key id="82F94BBDF95C247BBD21396B9A0B94DEC0FFA7EE" group="org.webjars" name="jquery" version="3.7.1"/>
<trusted-key id="8446B9E902A3F3DDEE711FDB8E26FF248BC7AEAD" group="com.github.jnr"/>
<trusted-key id="84789D24DF77A32433CE1F079EB80E92EB2135B1" group="^org[.]apache($|([.].*))" regex="true"/>
@@ -840,12 +839,6 @@
<sha512 value="cd57377d61d66ac2ab6ab90483252385fe2bbb9e13dee07913362a64ffe97b16082df981dbe469df3f67ad946827be6f4355d63003a393bda62a0fc8b54a590b" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
</component>
- <component group="com.thoughtworks.qdox" name="qdox" version="2.2.0">
- <artifact name="qdox-2.2.0.jar">
- <pgp value="7E6557B314B189FD4EA5B680FE66BCA17E67AE6F"/>
- <sha512 value="3fec9532bd0fcbe695ac6c4c3ae54bdd83b35ec031ea81ac853a49d548d6c14bfab92e626545b90564f8424cc22d9c2c682615cf0a461273b60f127184095758" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
- </artifact>
- </component>
<component group="com.thoughtworks.xstream" name="xstream" version="1.4.18">
<artifact name="xstream-1.4.18.jar">
<sha512 value="141c0aa3bd2e0d82cd37e6c2403f09f447aa51344fdf76c2f64b0952b1297ac99156b672628f310b4208d2387f047e62afefead519ba9eee37a7495850814b19" origin="Generated by Gradle"/>
diff --git a/src/test/groovy/org/codehaus/groovy/tools/stubgenerator/JavaParserCategory.groovy b/src/test/groovy/org/codehaus/groovy/tools/stubgenerator/JavaParserCategory.groovy
new file mode 100644
index 0000000000..f8c4759af2
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/tools/stubgenerator/JavaParserCategory.groovy
@@ -0,0 +1,53 @@
+/*
+ * 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.codehaus.groovy.tools.stubgenerator
+
+import groovy.transform.CompileStatic
+
+/**
+ * Category methods that keep stub-generator assertions concise while using JavaParser-backed views.
+ */
+class JavaParserCategory {
+
+ /**
+ * Access a parsed class by fully qualified name.
+ */
+ static JavaSourceClass getAt(JavaSourceClass[] self, String className) {
+ def clazz = self.find { JavaSourceClass jc -> jc.fullyQualifiedName == className }
+ assert clazz, "No stub class found for name $className, among ${self.collect { it.fullyQualifiedName }}"
+ clazz
+ }
+
+ /**
+ * Access one or several parsed methods by name.
+ */
+ static getAt(JavaSourceMethod[] self, String methodName) {
+ getAt(self.toList(), methodName)
+ }
+
+ static getAt(List<JavaSourceMethod> self, String methodName) {
+ def methods = self.findAll { JavaSourceMethod method -> method.name == methodName }
+ methods.size() == 1 ? methods[0] : methods
+ }
+
+ @CompileStatic
+ static List<JavaSourceMethod> getMethods(JavaSourceClass self) {
+ self.methods
+ }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/tools/stubgenerator/JavaParserSourceSet.groovy b/src/test/groovy/org/codehaus/groovy/tools/stubgenerator/JavaParserSourceSet.groovy
new file mode 100644
index 0000000000..79ae962562
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/tools/stubgenerator/JavaParserSourceSet.groovy
@@ -0,0 +1,691 @@
+/*
+ * 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.codehaus.groovy.tools.stubgenerator
+
+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.ConstructorDeclaration
+import com.github.javaparser.ast.body.EnumDeclaration
+import com.github.javaparser.ast.body.FieldDeclaration
+import com.github.javaparser.ast.body.MethodDeclaration
+import com.github.javaparser.ast.body.RecordDeclaration
+import com.github.javaparser.ast.body.TypeDeclaration
+import com.github.javaparser.ast.expr.AnnotationExpr
+import com.github.javaparser.ast.expr.ArrayInitializerExpr
+import com.github.javaparser.ast.expr.BinaryExpr
+import com.github.javaparser.ast.expr.BooleanLiteralExpr
+import com.github.javaparser.ast.expr.CastExpr
+import com.github.javaparser.ast.expr.CharLiteralExpr
+import com.github.javaparser.ast.expr.ClassExpr
+import com.github.javaparser.ast.expr.DoubleLiteralExpr
+import com.github.javaparser.ast.expr.EnclosedExpr
+import com.github.javaparser.ast.expr.Expression
+import com.github.javaparser.ast.expr.FieldAccessExpr
+import com.github.javaparser.ast.expr.IntegerLiteralExpr
+import com.github.javaparser.ast.expr.LongLiteralExpr
+import com.github.javaparser.ast.expr.MarkerAnnotationExpr
+import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.expr.NameExpr
+import com.github.javaparser.ast.expr.NormalAnnotationExpr
+import com.github.javaparser.ast.expr.NullLiteralExpr
+import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr
+import com.github.javaparser.ast.expr.StringLiteralExpr
+import com.github.javaparser.ast.expr.TextBlockLiteralExpr
+import com.github.javaparser.ast.expr.UnaryExpr
+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
+
+import static groovy.io.FileType.FILES
+
+/**
+ * JavaParser-backed source set used by stub-generator tests to inspect generated Java stubs.
+ */
+final class JavaParserSourceSet {
+ private final JavaParser parser = new JavaParser(
+ new ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.BLEEDING_EDGE)
+ )
+ private final List<ParsedUnit> units = []
+ private boolean dirty = true
+ private List<JavaSourceClass> cachedClasses = []
+
+ void addSourceTree(File root) {
+ if (!root?.exists()) return
+
+ root.eachFileRecurse(FILES) { File file ->
+ if (!file.name.endsWith('.java')) 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,
+ text: file.getText('UTF-8'),
+ compilationUnit: result.result.get()
+ )
+ dirty = true
+ }
+ }
+
+ JavaSourceClass[] getClasses() {
+ rebuildIfNeeded()
+ cachedClasses as JavaSourceClass[]
+ }
+
+ private void rebuildIfNeeded() {
+ if (!dirty) return
+
+ Map<TypeDeclaration<?>, String> fqcnByDeclaration = [:]
+ List<TypeRecord> orderedTypes = []
+ units.each { ParsedUnit unit ->
+ String packageName = unit.compilationUnit.packageDeclaration.map { it.nameAsString }.orElse('')
+ unit.compilationUnit.types.each { TypeDeclaration<?> declaration ->
+ collectTypes(unit, declaration, packageName, null, fqcnByDeclaration, orderedTypes)
+ }
+ }
+
+ Map<String, Set<String>> simpleNameIndex = [:].withDefault { new LinkedHashSet<String>() }
+ Set<String> knownTypes = new LinkedHashSet<>(fqcnByDeclaration.values())
+ knownTypes.each { String fqcn ->
+ simpleNameIndex[simpleNameOf(fqcn)] << fqcn
+ }
+
+ Map<CompilationUnit, JavaSourceContext> contextsByUnit = [:]
+ units.each { ParsedUnit unit ->
+ Set<String> unitTypes = fqcnByDeclaration.findAll { TypeDeclaration<?> declaration, String ignored ->
+ declaration.findCompilationUnit().orElse(null) == unit.compilationUnit
+ }.values() as Set<String>
+ contextsByUnit[unit.compilationUnit] = new JavaSourceContext(unit, fqcnByDeclaration, simpleNameIndex, knownTypes, unitTypes)
+ }
+
+ cachedClasses = orderedTypes.collect { TypeRecord record ->
+ new JavaSourceClass(record.fqcn, record.declaration, contextsByUnit[record.unit.compilationUnit])
+ }
+ dirty = false
+ }
+
+ private static void collectTypes(
+ ParsedUnit unit,
+ TypeDeclaration<?> declaration,
+ String packageName,
+ String ownerFqcn,
+ Map<TypeDeclaration<?>, String> fqcnByDeclaration,
+ List<TypeRecord> orderedTypes
+ ) {
+ String fqcn = ownerFqcn ? ownerFqcn + '.' + declaration.nameAsString
+ : (packageName ? packageName + '.' + declaration.nameAsString : declaration.nameAsString)
+ fqcnByDeclaration[declaration] = fqcn
+ orderedTypes << new TypeRecord(unit: unit, declaration: declaration, fqcn: fqcn)
+
+ declaration.members.findAll { BodyDeclaration member -> member instanceof TypeDeclaration }
+ .each { TypeDeclaration nested ->
+ collectTypes(unit, nested, packageName, fqcn, fqcnByDeclaration, orderedTypes)
+ }
+ }
+
+ private static String simpleNameOf(String fqcn) {
+ int dot = fqcn.lastIndexOf('.')
+ dot < 0 ? fqcn : fqcn.substring(dot + 1)
+ }
+}
+
+final class JavaSourceClass {
+ final String fullyQualifiedName
+ final JavaSourceFile source
+ final List<JavaSourceMethod> methods
+ final List<JavaSourceConstructor> constructors
+ final List<JavaSourceAnnotation> annotations
+
+ private final TypeDeclaration<?> declaration
+ private final JavaSourceContext context
+ private final Map<String, JavaSourceField> fieldsByName
+
+ JavaSourceClass(String fullyQualifiedName, TypeDeclaration<?> declaration, JavaSourceContext context) {
+ this.fullyQualifiedName = fullyQualifiedName
+ this.declaration = declaration
+ this.context = context
+ this.source = context.source
+ this.methods = declaration.members.findAll { it instanceof MethodDeclaration }
+ .collect { new JavaSourceMethod((MethodDeclaration) it, this, context) }
+ this.constructors = declaration.members.findAll { it instanceof ConstructorDeclaration }
+ .collect { new JavaSourceConstructor((ConstructorDeclaration) it, this, context) }
+ this.annotations = declaration.annotations.collect { new JavaSourceAnnotation(it, context) }
+ this.fieldsByName = [:]
+ declaration.members.findAll { it instanceof FieldDeclaration }.each { FieldDeclaration field ->
+ field.variables.each { variable ->
+ fieldsByName[variable.nameAsString] = new JavaSourceField(variable.nameAsString, variable.initializer.map {
+ context.renderExpression(it)
+ }.orElse(null))
+ }
+ }
+ }
+
+ JavaSourceField getFieldByName(String fieldName) {
+ fieldsByName[fieldName]
+ }
+
+ List<String> getImports() {
+ source.imports
+ }
+
+ List<String> getInterfaces() {
+ if (declaration instanceof ClassOrInterfaceDeclaration) {
+ def classOrInterface = (ClassOrInterfaceDeclaration) declaration
+ def types = classOrInterface.isInterface() ? classOrInterface.extendedTypes : classOrInterface.implementedTypes
+ return types.collect { context.renderType(it, typeParameterNames()) }
+ }
+ if (declaration instanceof EnumDeclaration) {
+ return ((EnumDeclaration) declaration).implementedTypes.collect { context.renderType(it, typeParameterNames()) }
+ }
+ if (declaration instanceof RecordDeclaration) {
+ return ((RecordDeclaration) declaration).implementedTypes.collect { context.renderType(it, typeParameterNames()) }
+ }
+ []
+ }
+
+ String getBaseClass() {
+ if (declaration instanceof ClassOrInterfaceDeclaration) {
+ def classOrInterface = (ClassOrInterfaceDeclaration) declaration
+ if (classOrInterface.isInterface()) return null
+ if (!classOrInterface.extendedTypes.empty) {
+ return context.renderType(classOrInterface.extendedTypes[0], typeParameterNames())
+ }
+ return 'java.lang.Object'
+ }
+ if (declaration instanceof EnumDeclaration) {
+ return 'java.lang.Enum'
+ }
+ if (declaration instanceof RecordDeclaration) {
+ return 'java.lang.Record'
+ }
+ null
+ }
+
+ Set<String> typeParameterNames() {
+ declaration.respondsTo('getTypeParameters')
+ ? declaration.typeParameters*.nameAsString as Set<String>
+ : Collections.emptySet()
+ }
+}
+
+final class JavaSourceMethod {
+ final String name
+ final List<JavaSourceAnnotation> annotations
+
+ private final MethodDeclaration declaration
+ private final JavaSourceClass owner
+ private final JavaSourceContext context
+
+ JavaSourceMethod(MethodDeclaration declaration, JavaSourceClass owner, JavaSourceContext context) {
+ this.declaration = declaration
+ this.owner = owner
+ this.context = context
+ this.name = declaration.nameAsString
+ this.annotations = declaration.annotations.collect { new JavaSourceAnnotation(it, context) }
+ }
+
+ String getSignature() {
+ def scopeTypeParameters = new LinkedHashSet<String>(owner.typeParameterNames())
+ scopeTypeParameters.addAll(declaration.typeParameters*.nameAsString)
+
+ def parts = []
+ if (!declaration.modifiers.empty) {
+ parts << declaration.modifiers.collect { it.keyword.asString() }.join(' ')
+ }
+ if (!declaration.typeParameters.empty) {
+ parts << '<' + declaration.typeParameters.collect {
+ context.renderTypeParameter(it, scopeTypeParameters)
+ }.join(', ') + '>'
+ }
+ parts << context.renderType(declaration.type, scopeTypeParameters)
+ parts << "${declaration.nameAsString}(" + declaration.parameters.collect {
+ context.renderParameter(it, scopeTypeParameters)
+ }.join(', ') + ')'
+
+ String signature = parts.findAll { it }.join(' ')
+ if (!declaration.thrownExceptions.empty) {
+ signature += ' throws ' + declaration.thrownExceptions.collect {
+ context.renderType(it, scopeTypeParameters)
+ }.join(', ')
+ }
+ signature
+ }
+}
+
+final class JavaSourceConstructor {
+ private final ConstructorDeclaration declaration
+ private final JavaSourceClass owner
+ private final JavaSourceContext context
+
+ JavaSourceConstructor(ConstructorDeclaration declaration, JavaSourceClass owner, JavaSourceContext context) {
+ this.declaration = declaration
+ this.owner = owner
+ this.context = context
+ }
+
+ @Override
+ String toString() {
+ def scopeTypeParameters = new LinkedHashSet<String>(owner.typeParameterNames())
+ scopeTypeParameters.addAll(declaration.typeParameters*.nameAsString)
+
+ def parts = []
+ if (!declaration.modifiers.empty) {
+ parts << declaration.modifiers.collect { it.keyword.asString() }.join(' ')
+ }
+ parts << owner.fullyQualifiedName + '(' + declaration.parameters.collect {
+ context.renderType(it.type, scopeTypeParameters, it.varArgs)
+ }.join(',') + ')'
+
+ String signature = parts.findAll { it }.join(' ')
+ if (!declaration.thrownExceptions.empty) {
+ signature += ' throws ' + declaration.thrownExceptions.collect {
+ context.renderType(it, scopeTypeParameters)
+ }.join(', ')
+ }
+ signature
+ }
+}
+
+final class JavaSourceAnnotation {
+ private final AnnotationExpr expression
+ private final JavaSourceContext context
+
+ JavaSourceAnnotation(AnnotationExpr expression, JavaSourceContext context) {
+ this.expression = expression
+ this.context = context
+ }
+
+ String getType() {
+ context.resolveTypeName(expression.nameAsString)
+ }
+
+ Object getProperty(String name) {
+ if (name == 'type') return getType()
+ if (name == 'class') return getClass()
+ if (name == 'metaClass') return GroovySystem.metaClassRegistry.getMetaClass(getClass())
+
+ annotationProperty(name)
+ }
+
+ private JavaSourceAnnotationValue annotationProperty(String name) {
+ Expression value = null
+ if (expression instanceof NormalAnnotationExpr) {
+ value = ((NormalAnnotationExpr) expression).pairs.find { it.nameAsString == name }?.value
+ } else if (expression instanceof SingleMemberAnnotationExpr && name == 'value') {
+ value = ((SingleMemberAnnotationExpr) expression).memberValue
+ }
+ value != null ? new JavaSourceAnnotationValue(value, context) : null
+ }
+
+ String getNamedParameter(String name) {
+ annotationProperty(name)?.toString()
+ }
+
+ @Override
+ String toString() {
+ context.renderAnnotation(expression)
+ }
+}
+
+final class JavaSourceAnnotationValue {
+ private final Expression expression
+ private final JavaSourceContext context
+
+ JavaSourceAnnotationValue(Expression expression, JavaSourceContext context) {
+ this.expression = expression
+ this.context = context
+ }
+
+ JavaSourceAnnotationValue getParameterValue() {
+ this
+ }
+
+ Object getProperty(String name) {
+ if (name == 'parameterValue') return getParameterValue()
+ if (name == 'value') return getValue()
+ if (name == 'class') return getClass()
+ if (name == 'metaClass') return GroovySystem.metaClassRegistry.getMetaClass(getClass())
+
+ expression instanceof AnnotationExpr
+ ? new JavaSourceAnnotation((AnnotationExpr) expression, context).getProperty(name)
+ : null
+ }
+
+ String getValue() {
+ if (expression instanceof StringLiteralExpr) {
+ return ((StringLiteralExpr) expression).asString()
+ }
+ if (expression instanceof TextBlockLiteralExpr) {
+ return ((TextBlockLiteralExpr) expression).asString()
+ }
+ context.renderExpression(expression)
+ }
+
+ @Override
+ String toString() {
+ context.renderExpression(expression)
+ }
+}
+
+final class JavaSourceField {
+ final String name
+ final String initializationExpression
+
+ JavaSourceField(String name, String initializationExpression) {
+ this.name = name
+ this.initializationExpression = initializationExpression
+ }
+}
+
+final class JavaSourceFile {
+ final String text
+ final List<String> imports
+
+ JavaSourceFile(String text, List<String> imports) {
+ this.text = text
+ this.imports = imports
+ }
+
+ @Override
+ String toString() {
+ text
+ }
+}
+
+final class JavaSourceContext {
+ private static final Set<String> PRIMITIVES = [
+ 'boolean', 'byte', 'char', 'double', 'float', 'int', 'long', 'short'
+ ] as Set<String>
+
+ final JavaSourceFile source
+ final String packageName
+
+ private final Map<String, String> explicitImports = [:]
+ private final List<String> wildcardImports = []
+ private final Map<TypeDeclaration<?>, String> fqcnByDeclaration
+ private final Map<String, Set<String>> simpleNameIndex
+ private final Set<String> knownTypes
+ private final Set<String> unitTypes
+
+ JavaSourceContext(
+ ParsedUnit unit,
+ Map<TypeDeclaration<?>, String> fqcnByDeclaration,
+ Map<String, Set<String>> simpleNameIndex,
+ Set<String> knownTypes,
+ Set<String> unitTypes
+ ) {
+ this.packageName = unit.compilationUnit.packageDeclaration.map { it.nameAsString }.orElse('')
+ this.source = new JavaSourceFile(unit.text, collectImports(unit.compilationUnit))
+ this.fqcnByDeclaration = fqcnByDeclaration
+ this.simpleNameIndex = simpleNameIndex
+ this.knownTypes = knownTypes
+ this.unitTypes = unitTypes
+
+ unit.compilationUnit.imports.each { ImportDeclaration importDeclaration ->
+ if (importDeclaration.isStatic()) return
+ if (importDeclaration.asterisk) {
+ wildcardImports << importDeclaration.nameAsString
+ } else {
+ explicitImports[importDeclaration.name.identifier] = importDeclaration.nameAsString
+ }
+ }
+ }
+
+ String renderParameter(parameter, Set<String> typeParameterNames) {
+ "${renderType(parameter.type, typeParameterNames, parameter.varArgs)} ${parameter.nameAsString}"
+ }
+
+ 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
+ }
+
+ 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()
+ }
+
+ String renderExpression(Expression expression) {
+ if (expression instanceof StringLiteralExpr ||
+ expression instanceof TextBlockLiteralExpr ||
+ expression instanceof IntegerLiteralExpr ||
+ expression instanceof LongLiteralExpr ||
+ expression instanceof DoubleLiteralExpr ||
+ expression instanceof BooleanLiteralExpr ||
+ expression instanceof CharLiteralExpr ||
+ expression instanceof NullLiteralExpr) {
+ return expression.toString()
+ }
+ if (expression instanceof NameExpr) {
+ return resolveTypeName(((NameExpr) expression).nameAsString)
+ }
+ if (expression instanceof FieldAccessExpr) {
+ def fieldAccess = (FieldAccessExpr) expression
+ return renderExpression(fieldAccess.scope) + '.' + fieldAccess.nameAsString
+ }
+ if (expression instanceof ClassExpr) {
+ return renderType(((ClassExpr) expression).type) + '.class'
+ }
+ if (expression instanceof AnnotationExpr) {
+ return renderAnnotation((AnnotationExpr) expression)
+ }
+ if (expression instanceof ArrayInitializerExpr) {
+ return '{' + ((ArrayInitializerExpr) expression).values.collect {
+ renderExpression(it)
+ }.join(', ') + '}'
+ }
+ if (expression instanceof EnclosedExpr) {
+ return '(' + renderExpression(((EnclosedExpr) expression).inner) + ')'
+ }
+ if (expression instanceof CastExpr) {
+ def cast = (CastExpr) expression
+ return '(' + renderType(cast.type) + ') ' + renderExpression(cast.expression)
+ }
+ if (expression instanceof UnaryExpr) {
+ def unary = (UnaryExpr) expression
+ return unary.operator.asString() + renderExpression(unary.expression)
+ }
+ if (expression instanceof BinaryExpr) {
+ def binary = (BinaryExpr) expression
+ return renderExpression(binary.left) + ' ' + binary.operator.asString() + ' ' + renderExpression(binary.right)
+ }
+ if (expression instanceof MethodCallExpr) {
+ def call = (MethodCallExpr) expression
+ def prefix = call.scope.map { renderExpression(it) + '.' }.orElse('')
+ return prefix + call.nameAsString + '(' + call.arguments.collect { renderExpression(it) }.join(', ') + ')'
+ }
+ expression.toString()
+ }
+
+ String renderAnnotation(AnnotationExpr annotation) {
+ String typeName = resolveTypeName(annotation.nameAsString)
+ if (annotation instanceof MarkerAnnotationExpr) {
+ return "@${typeName}"
+ }
+ if (annotation instanceof SingleMemberAnnotationExpr) {
+ return "@${typeName}(" + renderExpression(((SingleMemberAnnotationExpr) annotation).memberValue) + ')'
+ }
+ def normal = (NormalAnnotationExpr) annotation
+ "@${typeName}(" + normal.pairs.collect {
+ "${it.nameAsString} = ${renderExpression(it.value)}"
+ }.join(', ') + ')'
+ }
+
+ 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
+ }
+
+ Set<String> localMatches = unitTypes.findAll { String candidate -> candidate.endsWith('.' + name) || candidate == name } as Set<String>
+ if (localMatches.size() == 1) return localMatches.first()
+
+ 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) || tryLoad(candidate) != null
+ }
+
+ private 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 List<String> collectImports(CompilationUnit compilationUnit) {
+ compilationUnit.imports.findAll { !it.isStatic() }.collect { ImportDeclaration importDeclaration ->
+ importDeclaration.asterisk ? importDeclaration.nameAsString + '.*' : importDeclaration.nameAsString
+ }
+ }
+}
+
+final class ParsedUnit {
+ File file
+ String text
+ CompilationUnit compilationUnit
+}
+
+final class TypeRecord {
+ ParsedUnit unit
+ TypeDeclaration<?> declaration
+ String fqcn
+}
diff --git a/src/test/groovy/org/codehaus/groovy/tools/stubgenerator/QDoxCategory.groovy b/src/test/groovy/org/codehaus/groovy/tools/stubgenerator/QDoxCategory.groovy
deleted file mode 100644
index 99265ecc1a..0000000000
--- a/src/test/groovy/org/codehaus/groovy/tools/stubgenerator/QDoxCategory.groovy
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * 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.codehaus.groovy.tools.stubgenerator
-
-import com.thoughtworks.qdox.model.JavaClass
-import com.thoughtworks.qdox.model.JavaMethod
-import groovy.transform.CompileStatic
-
-/**
- * Category methods for the QDox library to simplify the writing assertions to check the generated stubs.
- */
-class QDoxCategory {
-
- /**
- * Given a <code>JavaClass[]</code> array,
- * like provided by the <code>getClasses()</code> method (or <code>classes</code> property) in <code>StubTestCase</code>,
- * you can access a specific class by its fully qualified name, using the subscript notation:
- *
- * <pre><code>
- * classes['com.bar.Rectangle']
- * </code></pre>
- *
- * @param self an array of <code>JavaClass[]</code>
- * @param className the name of the class to find
- * @return a <code>JavaClass</code>
- */
- static JavaClass getAt(JavaClass[] self, String className) {
- def clazz = self.find { JavaClass jc -> jc.fullyQualifiedName == className }
- assert clazz, "No stub class found for name $className, among ${self.collect { it.fullyQualifiedName }}"
- return clazz
- }
-
- /**
- * Find one or several methods from a <code>JavaMethod[]</code> array.
- *
- * Usage:
- *
- * <pre><code>
- * classes['com.bar.Rectangle'].methods['area']
- * </code></pre>
- *
- * @param self an array of <code>JavaMethod</code>
- * @param methodName the name of the method(s) to find
- * @return a single <code>JavaMethod</code> if only one of that name exists, or a <code>JavaMethod[]</code> array
- */
- static getAt(JavaMethod[] self, String methodName) {
- def methods = self.findAll { JavaMethod jc -> jc.name == methodName }
- if (methods.size() == 1) {
- return methods[0]
- } else {
- return methods
- }
- }
-
- @CompileStatic
- static JavaMethod[] getMethods(JavaClass self) {
- self.methods as JavaMethod[]
- }
-
- /**
- * Get a normalized string of the signature of a method, including its modifiers.
- * <br><br>
- * Usage:
- *
- * <pre><code>
- * // an area() method
- * assert classes['Rectangle'].methods['area'].signature == "public double area()"
- * // a constructor
- * assert classes['Rectangle'].methods['Rectangle'].signature == "public Rectangle(double x, double y)"
- * </code></pre>
- *
- * @param self a method
- * @return a normalized strings representing the signature
- */
- static String getSignature(JavaMethod self) {
- self.getDeclarationSignature(true)
- }
-
- /**
- * Lists the interfaces implemented by a class.
- * <br><br>
- * Usage:
- *
- * <pre><code>
- * assert classes['Rectangle'].interfaces == ['groovy.lang.GroovyObject', 'Shape']
- * </code></pre>
- *
- * @param self a <code>JavaClass</code>
- * @return a list of fully qualified interface names
- */
- @CompileStatic
- static List<String> getInterfaces(JavaClass self) {
- self.interfaces*.fullyQualifiedName
- }
-
- /**
- * Get the name of the base class of that class.
- * <br><br>
- * Usage:
- *
- * <pre><code>
- * assert classes['Rectangle'].baseClass == 'java.lang.Object'
- * </code></pre>
- *
- * @param self the class for which we want to know the base class
- * @return the fully qualified name of the parent class
- */
- static String getBaseClass(JavaClass self) {
- self.superJavaClass.fullyQualifiedName
- }
-
- /**
- * Shortcut to get the imports from the source associated with the class.
- * <br><br>
- * Usage:
- *
- * <pre><code>
- * assert classes['Rectangle'].imports == ['java.lang.*', 'java.io.*',
- * 'java.net.*', 'java.util.*',
- * 'groovy.lang.*', 'groovy.util.*']
- * </code></pre>
- *
- * @param self the class
- * @return the list of fully qualified imports
- */
- static List<String> getImports(JavaClass self) {
- self.source.imports
- }
-}
diff --git a/src/test/groovy/org/codehaus/groovy/tools/stubgenerator/StubTestCase.groovy b/src/test/groovy/org/codehaus/groovy/tools/stubgenerator/StubTestCase.groovy
index 6c1b1b8e30..8454388b87 100644
--- a/src/test/groovy/org/codehaus/groovy/tools/stubgenerator/StubTestCase.groovy
+++ b/src/test/groovy/org/codehaus/groovy/tools/stubgenerator/StubTestCase.groovy
@@ -18,8 +18,6 @@
*/
package org.codehaus.groovy.tools.stubgenerator
-import com.thoughtworks.qdox.JavaProjectBuilder
-import com.thoughtworks.qdox.model.JavaClass
import groovy.test.GroovyTestCase
import junit.framework.TestCase
import org.codehaus.groovy.control.CompilationFailedException
@@ -50,9 +48,9 @@ import static org.junit.jupiter.api.Assertions.fail
* existing tests which use this approach, e.g. <code>DuplicateMethodAdditionInStubsTest</code>.</li>
* </ul>
* From within the <code>verifyStubs()</code> method, you can make various assertions on the stubs.
- * QDox is used for parsing the Java sources (both the generated stub Java sources, as well as the original Java source,
+ * JavaParser is used for parsing the Java sources (both the generated stub Java sources, as well as the original Java source,
* but not the Groovy sources).
- * The execution of the <code>verifyStubs()</code> method is done with the <code>QDoxCategory</code> applied,
+ * The execution of the <code>verifyStubs()</code> method is done with the <code>JavaParserCategory</code> applied,
* which provides various useful shortcuts for quickly checking the structure of your stubs.
* <p>
* Please have a look at the existing samples to see what kind of asserts can be done.
@@ -64,7 +62,7 @@ abstract class StubTestCase {
protected File sourceRootPath
- protected JavaProjectBuilder qdox = new JavaProjectBuilder()
+ protected JavaParserSourceSet parsedSources = new JavaParserSourceSet()
protected GroovyClassLoader loader
protected CompilerConfiguration config
@@ -166,9 +164,9 @@ abstract class StubTestCase {
try {
compile(sources)
- // use QDox for parsing the Java stubs and Java sources
- qdox.addSourceTree(sourceRootPath)
- qdox.addSourceTree(stubDir)
+ // use JavaParser for parsing the Java stubs and Java sources
+ parsedSources.addSourceTree(sourceRootPath)
+ parsedSources.addSourceTree(stubDir)
if (debug) {
println ">>> Stubs generated"
@@ -179,8 +177,8 @@ abstract class StubTestCase {
}
}
- println ">>> QDox canonical sources"
- qdox.classes.each { JavaClass jc ->
+ println ">>> JavaParser canonical sources"
+ parsedSources.classes.each { JavaSourceClass jc ->
println " -> " + jc.fullyQualifiedName
println jc.source
}
@@ -191,7 +189,7 @@ abstract class StubTestCase {
compileError = t
} finally {
try {
- use (QDoxCategory) {
+ use (JavaParserCategory) {
verifyStubs()
}
} catch (AssertionError | RuntimeException e) {
@@ -277,10 +275,10 @@ abstract class StubTestCase {
* Method providing a useful shortcut for the subclasses, so that you can use <code>classes</code>
* from within the <code>verifyStubs()</code> method, to access all the stubs.
*
- * @return an array of QDox' <code>JavaClass</code>es.
+ * @return an array of parsed Java classes.
*/
- protected JavaClass[] getClasses() {
- qdox.classes as JavaClass[]
+ protected JavaSourceClass[] getClasses() {
+ parsedSources.classes as JavaSourceClass[]
}
/**
@@ -335,4 +333,3 @@ abstract class StubTestCase {
}
}
}
-
diff --git a/subprojects/groovy-docgenerator/build.gradle b/subprojects/groovy-docgenerator/build.gradle
index 82cd67eb1d..4f321573a9 100644
--- a/subprojects/groovy-docgenerator/build.gradle
+++ b/subprojects/groovy-docgenerator/build.gradle
@@ -24,7 +24,7 @@ dependencies {
implementation rootProject
implementation projects.groovyTemplates
implementation projects.groovyGroovydoc
- implementation "com.thoughtworks.qdox:qdox:${versions.qdox}"
+ implementation "com.github.javaparser:javaparser-core:${versions.javaParser}"
testImplementation projects.groovyTest
}
diff --git a/subprojects/groovy-docgenerator/src/main/groovy/org/apache/groovy/docgenerator/JavaExtensionSourceSet.groovy b/subprojects/groovy-docgenerator/src/main/groovy/org/apache/groovy/docgenerator/JavaExtensionSourceSet.groovy
new file mode 100644
index 0000000000..31f68028a2
--- /dev/null
+++ b/subprojects/groovy-docgenerator/src/main/groovy/org/apache/groovy/docgenerator/JavaExtensionSourceSet.groovy
@@ -0,0 +1,453 @@
+/*
+ * 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 = [:]
+
+ 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
+ }
+
+ List<JavaExtensionMethod> getMethods() {
+ rebuildIfNeeded()
+ cachedMethods
+ }
+
+ 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)
+ }
+
+ 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>
+}
+
+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
+
+ 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
+ }
+ }
+ }
+
+ 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
+ }
+
+ 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()
+ }
+
+ 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()
+ }
+
+ 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
+ }
+}
+
+final class JavaExtensionMethod {
+ String name
+ String declaringClassName
+ boolean publicMethod
+ boolean staticMethod
+ boolean deprecated
+ String receiverTypeName
+ ReceiverTypeInfo receiverTypeInfo
+ String returnType
+ List<JavaExtensionParameter> parameters = []
+ List<String> typeParameters = []
+ List<String> exceptions = []
+ JavadocInfo javadoc
+}
+
+final class JavaExtensionParameter {
+ String type
+ String name
+ boolean varArgs
+}
+
+final class ReceiverTypeInfo {
+ String canonicalName
+ boolean primitive
+ boolean interfaceType
+}
+
+final class JavadocInfo {
+ String description = ''
+ List<JavadocTagInfo> tags = []
+
+ static JavadocInfo parse(JavadocComment comment) {
+ if (comment == null) return new JavadocInfo()
+
+ def descriptionLines = []
+ def tags = []
+ JavadocTagInfo currentTag = null
+
+ comment.content.readLines().collect {
+ it.replaceFirst(/^\s*\* ?/, '')
+ }.each { String line ->
+ if (line.startsWith('@')) {
+ def matcher = (line =~ /^@(\S+)\s*(.*)$/)
+ if (matcher.matches()) {
+ currentTag = new JavadocTagInfo(name: matcher[0][1], value: matcher[0][2])
+ tags << currentTag
+ }
+ } else if (currentTag != null) {
+ currentTag.value = currentTag.value ? currentTag.value + '\n' + line : line
+ } else {
+ descriptionLines << line
+ }
+ }
+
+ new JavadocInfo(
+ description: descriptionLines.join('\n').trim(),
+ tags: tags
+ )
+ }
+}
+
+final class JavadocTagInfo {
+ String name
+ String value
+}
+
+final class ParsedUnit {
+ File file
+ CompilationUnit compilationUnit
+}
diff --git a/subprojects/groovy-docgenerator/src/main/groovy/org/apache/groovy/docgenerator/MockSourceGenerator.groovy b/subprojects/groovy-docgenerator/src/main/groovy/org/apache/groovy/docgenerator/MockSourceGenerator.groovy
index daf011ed24..16dd2de705 100644
--- a/subprojects/groovy-docgenerator/src/main/groovy/org/apache/groovy/docgenerator/MockSourceGenerator.groovy
+++ b/subprojects/groovy-docgenerator/src/main/groovy/org/apache/groovy/docgenerator/MockSourceGenerator.groovy
@@ -18,11 +18,6 @@
*/
package org.apache.groovy.docgenerator
-import com.thoughtworks.qdox.JavaProjectBuilder
-import com.thoughtworks.qdox.model.JavaClass
-import com.thoughtworks.qdox.model.JavaMethod
-import com.thoughtworks.qdox.model.JavaParameter
-import com.thoughtworks.qdox.model.JavaType
import groovy.cli.internal.CliBuilderInternal
import org.codehaus.groovy.runtime.DefaultGroovyMethods
@@ -77,29 +72,22 @@ class MockSourceGenerator {
* Generates mock source files for all supported receiver types.
*/
void generateAll() {
- def builder = new JavaProjectBuilder()
- sourceFiles.each {
- if (it.exists()) {
- builder.addSource(it.newReader())
- }
+ def sourceSet = new JavaExtensionSourceSet()
+ sourceFiles.each { File sourceFile ->
+ sourceSet.addSource(sourceFile)
}
// Bucket methods by receiver FQCN (generics stripped, single-letter type
// variables coerced to java.lang.Object[] as docgenerator historically did).
- def byReceiver = new LinkedHashMap<String, List<JavaMethod>>()
- def typeOf = new LinkedHashMap<String, JavaType>()
- builder.sources.each { source ->
- source.classes.each { JavaClass aClass ->
- aClass.methods.each { JavaMethod method ->
- if (!method.public || !method.static) return
- if (method.parameters.empty) return
- if (method.annotations.any { it.type.fullyQualifiedName == 'java.lang.Deprecated' }) return
- def raw = method.parameters[0].type.toString()
- def fqcn = resolveJdkClassName(raw)
- byReceiver.computeIfAbsent(fqcn) { [] } << method
- typeOf.putIfAbsent(fqcn, method.parameters[0].type)
- }
- }
+ def byReceiver = new LinkedHashMap<String, List<JavaExtensionMethod>>()
+ def typeOf = new LinkedHashMap<String, ReceiverTypeInfo>()
+ sourceSet.methods.each { JavaExtensionMethod method ->
+ if (!method.publicMethod || !method.staticMethod) return
+ if (method.parameters.empty) return
+ if (method.deprecated) return
+ def fqcn = method.receiverTypeName
+ byReceiver.computeIfAbsent(fqcn) { [] } << method
+ typeOf.putIfAbsent(fqcn, method.receiverTypeInfo)
}
// Group nested receivers under their outer class so we emit one file per
@@ -110,23 +98,23 @@ class MockSourceGenerator {
if (outer) {
// Make sure the outer exists as an owner even if it has no direct methods.
def parent = topLevelOwners.computeIfAbsent(outer) {
- new Owner(fqcn: outer, type: typeOf[outer])
+ new Owner(fqcn: outer, typeInfo: typeOf[outer] ?: sourceSet.typeInfoForFqcn(outer))
}
- parent.nested[fqcn] = new Owner(fqcn: fqcn, type: typeOf[fqcn], methods: methods)
+ parent.nested[fqcn] = new Owner(fqcn: fqcn, typeInfo: typeOf[fqcn], methods: methods)
} else {
def owner = topLevelOwners.computeIfAbsent(fqcn) {
- new Owner(fqcn: fqcn, type: typeOf[fqcn])
+ new Owner(fqcn: fqcn, typeInfo: typeOf[fqcn])
}
owner.methods.addAll(methods)
- if (!owner.type) owner.type = typeOf[fqcn]
+ if (!owner.typeInfo) owner.typeInfo = typeOf[fqcn]
}
}
def manifestEntries = []
topLevelOwners.values().each { owner ->
- def mockPkg = mockPackageFor(owner.fqcn, owner.type)
- def mockName = mockClassNameFor(owner.fqcn, owner.type)
- def displayPkg = displayPackageFor(owner.fqcn, owner.type)
+ def mockPkg = mockPackageFor(owner.fqcn, owner.typeInfo)
+ def mockName = mockClassNameFor(owner.fqcn, owner.typeInfo)
+ def displayPkg = displayPackageFor(owner.fqcn, owner.typeInfo)
def displayName = displayNameFor(owner.fqcn)
if (mockPkg != displayPkg || mockName != displayName) {
manifestEntries << "${mockPkg}\t${mockName}\t${displayPkg}\t${displayName}"
@@ -137,24 +125,24 @@ class MockSourceGenerator {
}
/** Where the generated HTML page should live after rewriting (historical URL). */
- static String displayPackageFor(String fqcn, JavaType type) {
+ static String displayPackageFor(String fqcn, ReceiverTypeInfo typeInfo) {
def base = stripArraySuffix(fqcn)
if (base in PRIMITIVE_BASES) return 'primitives-and-primitive-arrays'
- if (type?.isPrimitive()) return 'primitives-and-primitive-arrays'
+ if (typeInfo?.primitive) return 'primitives-and-primitive-arrays'
def dot = base.lastIndexOf('.')
dot < 0 ? '' : base[0..<dot]
}
private static class Owner {
String fqcn
- JavaType type
- List<JavaMethod> methods = []
+ ReceiverTypeInfo typeInfo
+ List<JavaExtensionMethod> methods = []
Map<String, Owner> nested = [:]
}
private void emitMock(Owner owner) {
- def pkg = mockPackageFor(owner.fqcn, owner.type)
- def mockName = mockClassNameFor(owner.fqcn, owner.type)
+ def pkg = mockPackageFor(owner.fqcn, owner.typeInfo)
+ def mockName = mockClassNameFor(owner.fqcn, owner.typeInfo)
def displayName = displayNameFor(owner.fqcn)
def dir = new File(outputDir, pkg.replace('.', '/'))
@@ -163,7 +151,7 @@ class MockSourceGenerator {
def sb = new StringBuilder()
sb << "package ${pkg};\n\n"
appendClassJavadoc(sb, displayName, owner.fqcn, '')
- def kw = classKeyword(owner.type)
+ def kw = classKeyword(owner.typeInfo)
sb << "public ${kw} ${mockName} {\n"
owner.methods.each { sb << serializeMethod(it, kw == 'interface', ' ') }
owner.nested.values().each { nested ->
@@ -179,7 +167,7 @@ class MockSourceGenerator {
def mockName = simpleMockName(owner.fqcn)
sb << '\n'
appendClassJavadoc(sb, displayName, owner.fqcn, indent)
- def kw = classKeyword(owner.type)
+ def kw = classKeyword(owner.typeInfo)
sb << "${indent}public static ${kw} ${mockName} {\n"
owner.methods.each { sb << serializeMethod(it, kw == 'interface', indent + ' ') }
sb << "${indent}}\n"
@@ -194,11 +182,11 @@ class MockSourceGenerator {
sb << "${indent} */\n"
}
- private static String classKeyword(JavaType type) {
- (type instanceof JavaClass && ((JavaClass) type).isInterface()) ? 'interface' : 'class'
+ private static String classKeyword(ReceiverTypeInfo typeInfo) {
+ typeInfo?.interfaceType ? 'interface' : 'class'
}
- private String serializeMethod(JavaMethod m, boolean ownerIsInterface, String indent) {
+ private String serializeMethod(JavaExtensionMethod m, boolean ownerIsInterface, String indent) {
def sb = new StringBuilder()
def javadoc = copyJavadoc(m)
if (javadoc) {
@@ -207,18 +195,18 @@ class MockSourceGenerator {
sb << "${indent} */\n"
}
sb << indent << 'public '
- boolean isStatic = m.declaringClass.name.endsWith('StaticMethods') || m.declaringClass.name.endsWith('StaticExtensions')
+ boolean isStatic = m.declaringClassName.endsWith('StaticMethods') || m.declaringClassName.endsWith('StaticExtensions')
if (isStatic) sb << 'static '
if (m.typeParameters) {
- sb << '<' << m.typeParameters.collect { it.toString() }.join(', ') << '> '
+ sb << '<' << m.typeParameters.join(', ') << '> '
}
- sb << (m.returns ? m.returns.genericCanonicalName : 'void') << ' '
+ sb << (m.returnType ?: 'void') << ' '
sb << m.name << '('
def params = isStatic ? m.parameters.toList() : (m.parameters.size() > 1 ? m.parameters.toList()[1..-1] : [])
- sb << params.collect { JavaParameter p -> "${p.type.genericCanonicalName} ${p.name}" }.join(', ')
+ sb << params.collect { JavaExtensionParameter p -> "${p.type} ${p.name}" }.join(', ')
sb << ')'
if (m.exceptions) {
- sb << ' throws ' << m.exceptions.collect { it.genericCanonicalName }.join(', ')
+ sb << ' throws ' << m.exceptions.join(', ')
}
if (ownerIsInterface && !isStatic) {
sb << ';\n\n'
@@ -229,8 +217,8 @@ class MockSourceGenerator {
sb.toString()
}
- private static String bodyFor(JavaMethod m) {
- def rt = m.returns?.toString()
+ private static String bodyFor(JavaExtensionMethod m) {
+ def rt = m.returnType
if (!rt || rt == 'void') return ''
'throw new UnsupportedOperationException();'
}
@@ -241,12 +229,12 @@ class MockSourceGenerator {
* {@code {@link Recv#m(X,Y)}} so groovydoc's link resolver can resolve them
* against the mock source tree.
*/
- private static String copyJavadoc(JavaMethod m) {
- def text = m.comment ?: ''
- def allTags = m.tags
+ private static String copyJavadoc(JavaExtensionMethod m) {
+ def text = m.javadoc?.description ?: ''
+ def allTags = m.javadoc?.tags ?: []
def paramTags = allTags.findAll { it.name == 'param' }
def otherTags = allTags.findAll { it.name != 'param' }
- boolean isStatic = m.declaringClass.name.endsWith('StaticMethods') || m.declaringClass.name.endsWith('StaticExtensions')
+ boolean isStatic = m.declaringClassName.endsWith('StaticMethods') || m.declaringClassName.endsWith('StaticExtensions')
def remainingParams = isStatic ? paramTags : (paramTags.size() > 1 ? paramTags.drop(1) : [])
def sb = new StringBuilder(rewriteLinks(text))
if (sb.length() > 0) sb << '\n'
@@ -276,21 +264,21 @@ class MockSourceGenerator {
}
/** Package the mock file lives in, derived from the receiver's FQCN. */
- static String mockPackageFor(String fqcn, JavaType type) {
+ static String mockPackageFor(String fqcn, ReceiverTypeInfo typeInfo) {
def base = stripArraySuffix(fqcn)
if (base in PRIMITIVE_BASES) return PRIMITIVES_PKG
- if (type?.isPrimitive()) return PRIMITIVES_PKG
+ if (typeInfo?.primitive) return PRIMITIVES_PKG
def dot = base.lastIndexOf('.')
dot < 0 ? '' : base[0..<dot]
}
/** Legal Java identifier used as the mock class name for a top-level receiver. */
- static String mockClassNameFor(String fqcn, JavaType type) {
+ static String mockClassNameFor(String fqcn, ReceiverTypeInfo typeInfo) {
def base = stripArraySuffix(fqcn)
def dims = arrayDimensions(fqcn)
def simple = (base.lastIndexOf('.') < 0) ? base : base[(base.lastIndexOf('.') + 1)..-1]
def name
- if (base in PRIMITIVE_BASES || (type?.isPrimitive())) {
+ if (base in PRIMITIVE_BASES || (typeInfo?.primitive)) {
name = 'Primitive' + simple.capitalize()
} else {
name = simple
diff --git a/subprojects/groovy-docgenerator/src/test/groovy/org/apache/groovy/docgenerator/MockSourceGeneratorTest.groovy b/subprojects/groovy-docgenerator/src/test/groovy/org/apache/groovy/docgenerator/MockSourceGeneratorTest.groovy
new file mode 100644
index 0000000000..31985126ae
--- /dev/null
+++ b/subprojects/groovy-docgenerator/src/test/groovy/org/apache/groovy/docgenerator/MockSourceGeneratorTest.groovy
@@ -0,0 +1,116 @@
+/*
+ * 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 org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+
+class MockSourceGeneratorTest {
+
+ @Test
+ void generatesMockSourcesFromJavaParserModel(@TempDir File tempDir) {
+ def sourceDir = new File(tempDir, 'src').tap { mkdirs() }
+ def outputDir = new File(tempDir, 'out').tap { mkdirs() }
+
+ def instanceExtensions = new File(sourceDir, 'demo/SampleMethods.java')
+ instanceExtensions.parentFile.mkdirs()
+ instanceExtensions.text = '''\
+ package demo;
+
+ import java.util.List;
+
+ public class SampleMethods {
+ /**
+ * See {@link #append(List, String)}.
+ *
+ * @param self receiver
+ * @param suffix suffix
+ * @return appended value
+ */
+ public static String append(List<String> self, String suffix) {
+ return suffix;
+ }
+
+ @Deprecated
+ public static String skip(String self) {
+ return self;
+ }
+ }
+ '''.stripIndent()
+
+ def staticExtensions = new File(sourceDir, 'demo/SampleStaticMethods.java')
+ staticExtensions.parentFile.mkdirs()
+ staticExtensions.text = '''\
+ package demo;
+
+ import java.io.IOException;
+ import java.util.Map;
+
+ public class SampleStaticMethods {
+ /**
+ * Combines entries.
+ *
+ * @param self receiver
+ * @param other other entry
+ * @return combined entry
+ * @throws IOException when broken
+ */
+ public static <T extends Map.Entry<String, Integer>> T combine(Map.Entry<String, Integer> self, T other) throws IOException {
+ return other;
+ }
+
+ /**
+ * Counts values.
+ *
+ * @param self values
+ * @return value count
+ */
+ public static int count(int[] self) {
+ return self.length;
+ }
+ }
+ '''.stripIndent()
+
+ new MockSourceGenerator([instanceExtensions, staticExtensions], outputDir).generateAll()
+
+ def listMock = new File(outputDir, 'java/util/List.java').text
+ assert listMock.contains('public interface List {')
+ assert listMock.contains('See {@link List#append(String)}.')
+ assert listMock.contains('@param suffix suffix')
+ assert !listMock.contains('@param self receiver')
+ assert listMock.contains('public java.lang.String append(java.lang.String suffix);')
+
+ def mapMock = new File(outputDir, 'java/util/Map.java').text
+ assert mapMock.contains('public interface Map {')
+ assert mapMock.contains('public static interface Entry {')
+ assert mapMock.contains('public static <T extends java.util.Map.Entry<java.lang.String, java.lang.Integer>> T combine(java.util.Map.Entry<java.lang.String, java.lang.Integer> self, T other) throws java.io.IOException { throw new UnsupportedOperationException(); }')
+ assert mapMock.contains('@param self receiver')
+ assert mapMock.contains('@param other other entry')
+
+ def primitiveMock = new File(outputDir, 'primitives/PrimitiveIntArray.java').text
+ assert primitiveMock.contains('@displayName int[]')
+ assert primitiveMock.contains('public class PrimitiveIntArray {')
+ assert primitiveMock.contains('public static int count(int[] self) { throw new UnsupportedOperationException(); }')
+
+ assert !new File(outputDir, 'java/lang/String.java').exists()
+
+ def manifest = new File(outputDir, MockSourceGenerator.MANIFEST_FILE).text
+ assert manifest.contains('primitives\tPrimitiveIntArray\tprimitives-and-primitive-arrays\tint[]')
+ }
+}
diff --git a/versions.properties b/versions.properties
index f97de0813f..e7fa310aeb 100644
--- a/versions.properties
+++ b/versions.properties
@@ -61,7 +61,6 @@ mavenResolverProvider=4.0.0-rc-5
mavenResolverSupplier=2.0.18
openbeans=1.0.2
picocli=4.7.7
-qdox=2.2.0
slf4j=2.0.18
# running with Groovy 6 can be allowed with -Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true
reactor=3.8.6