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/27 12:36:55 UTC
(groovy) branch GROOVY-12096 updated (a7fe588e27 -> bce296220c)
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a change to branch GROOVY-12096
in repository https://gitbox.apache.org/repos/asf/groovy.git
discard a7fe588e27 GROOVY-12096: Replace qdox with javaparser
new bce296220c GROOVY-12096: Replace qdox with javaparser
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (a7fe588e27)
\
N -- N -- N refs/heads/GROOVY-12096 (bce296220c)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../org/apache/groovy/docgenerator/JavaExtensionSourceSet.groovy | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
(groovy) 01/01: GROOVY-12096: Replace qdox with javaparser
Posted by su...@apache.org.
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 bce296220c0cf3f1b904d48490b9d0720472e5f2
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 | 597 ++++++++++++++++++
.../groovy/docgenerator/MockSourceGenerator.groovy | 102 ++-
.../docgenerator/JavaExtensionSourceSetTest.groovy | 225 +++++++
.../docgenerator/MockSourceGeneratorTest.groovy | 173 ++++++
versions.properties | 1 -
12 files changed, 1798 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..dc39b96db3
--- /dev/null
+++ b/subprojects/groovy-docgenerator/src/main/groovy/org/apache/groovy/docgenerator/JavaExtensionSourceSet.groovy
@@ -0,0 +1,597 @@
+/*
+ * 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 = newSimpleNameIndex()
+ 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 (JavaExtensionContext.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
+
+ resetCaches()
+ indexDeclaredTypes()
+ indexKnownTypesBySimpleName()
+ createContextsByUnit()
+ collectMethodViews()
+ dirty = false
+ }
+
+ private void resetCaches() {
+ declarationsByFqcn = [:]
+ simpleNameIndex = newSimpleNameIndex()
+ knownTypes = new LinkedHashSet<>()
+ contextsByUnit = [:]
+ cachedMethods = []
+ }
+
+ private void indexDeclaredTypes() {
+ units.each { ParsedUnit unit ->
+ String packageName = packageNameOf(unit.compilationUnit)
+ unit.compilationUnit.types.each { TypeDeclaration<?> declaration ->
+ collectTypes(declaration, packageName, null)
+ }
+ }
+ }
+
+ private void indexKnownTypesBySimpleName() {
+ knownTypes.addAll(declarationsByFqcn.keySet())
+ declarationsByFqcn.keySet().each { String fqcn ->
+ simpleNameIndex[simpleNameOf(fqcn)] << fqcn
+ }
+ }
+
+ private void createContextsByUnit() {
+ units.each { ParsedUnit unit ->
+ contextsByUnit[unit.compilationUnit] = new JavaExtensionContext(unit.compilationUnit, simpleNameIndex, knownTypes)
+ }
+ }
+
+ private void collectMethodViews() {
+ declarationsByFqcn.each { String fqcn, TypeDeclaration<?> declaration ->
+ JavaExtensionContext context = contextsByUnit[declaration.findCompilationUnit().orElse(null)]
+ methodDeclarationsOf(declaration).each { MethodDeclaration method ->
+ cachedMethods << createMethodView(method, declaration, context)
+ }
+ }
+ }
+
+ private void collectTypes(TypeDeclaration<?> declaration, String packageName, String ownerFqcn) {
+ String fqcn = qualifiedTypeName(declaration.nameAsString, packageName, ownerFqcn)
+ declarationsByFqcn[fqcn] = declaration
+ nestedTypeDeclarationsOf(declaration).each { TypeDeclaration<?> nested ->
+ collectTypes(nested, packageName, fqcn)
+ }
+ }
+
+ private JavaExtensionMethod createMethodView(MethodDeclaration method, TypeDeclaration<?> declaringType, JavaExtensionContext context) {
+ def scopeTypeParameters = scopeTypeParametersOf(declaringType, method)
+ List<JavaExtensionParameter> parameters = renderParameters(method, context, scopeTypeParameters)
+ String receiverTypeName = resolveReceiverTypeName(method, context, scopeTypeParameters, parameters)
+
+ 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: renderExceptions(method, context, scopeTypeParameters),
+ javadoc: JavadocInfo.parse(method.javadocComment.orElse(null))
+ )
+ }
+
+ private static List<JavaExtensionParameter> renderParameters(MethodDeclaration method, JavaExtensionContext context, Set<String> scopeTypeParameters) {
+ method.parameters.collect { parameter ->
+ new JavaExtensionParameter(
+ name: parameter.nameAsString,
+ type: context.renderType(parameter.type, scopeTypeParameters, parameter.varArgs),
+ varArgs: parameter.varArgs
+ )
+ }
+ }
+
+ private static List<String> renderExceptions(MethodDeclaration method, JavaExtensionContext context, Set<String> scopeTypeParameters) {
+ method.thrownExceptions.collect { context.renderType(it, scopeTypeParameters) }
+ }
+
+ private static String resolveReceiverTypeName(MethodDeclaration method, JavaExtensionContext context, Set<String> scopeTypeParameters, List<JavaExtensionParameter> parameters) {
+ if (parameters.empty) return null
+ MockSourceGenerator.resolveJdkClassName(context.eraseType(method.parameters[0].type, scopeTypeParameters))
+ }
+
+ private static Set<String> scopeTypeParametersOf(TypeDeclaration<?> declaringType, MethodDeclaration method) {
+ def scopeTypeParameters = new LinkedHashSet<String>(ownerTypeParametersOf(declaringType))
+ scopeTypeParameters.addAll(method.typeParameters*.nameAsString)
+ scopeTypeParameters
+ }
+
+ private static List<String> ownerTypeParametersOf(TypeDeclaration<?> declaringType) {
+ declaringType.respondsTo('getTypeParameters') ? declaringType.typeParameters*.nameAsString : []
+ }
+
+ private static String packageNameOf(CompilationUnit compilationUnit) {
+ compilationUnit.packageDeclaration.map { it.nameAsString }.orElse('')
+ }
+
+ private static String qualifiedTypeName(String typeName, String packageName, String ownerFqcn) {
+ ownerFqcn ? ownerFqcn + '.' + typeName : (packageName ? packageName + '.' + typeName : typeName)
+ }
+
+ private static List<TypeDeclaration<?>> nestedTypeDeclarationsOf(TypeDeclaration<?> declaration) {
+ declaration.members.findAll { BodyDeclaration member -> member instanceof TypeDeclaration } as List<TypeDeclaration<?>>
+ }
+
+ private static List<MethodDeclaration> methodDeclarationsOf(TypeDeclaration<?> declaration) {
+ declaration.members.findAll { BodyDeclaration member -> member instanceof MethodDeclaration } as List<MethodDeclaration>
+ }
+
+ 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 Map<String, Set<String>> newSimpleNameIndex() {
+ [:].withDefault { new LinkedHashSet<String>() }
+ }
+}
+
+/**
+ * Type rendering and name-resolution context derived from one compilation unit.
+ */
+final class JavaExtensionContext {
+ public static final Set<String> PRIMITIVES = Set.of('boolean', 'byte', 'char', 'double', 'float', 'int', 'long', 'short')
+ private static final List<String> DEFAULT_IMPORT_LIST = List.of(ResolveVisitor.DEFAULT_IMPORTS)
+ private static final String VOID_TYPE = 'void'
+
+ private final Map<String, String> explicitImports = [:]
+ private final List<String> wildcardImports = []
+ 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, Set<String>> simpleNameIndex,
+ Set<String> knownTypes
+ ) {
+ this.simpleNameIndex = simpleNameIndex
+ this.knownTypes = knownTypes
+ this.packageName = packageNameOf(compilationUnit)
+
+ 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_TYPE
+ } 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) {
+ rendered = renderWildcardType((WildcardType) type, typeParameterNames)
+ } else if (type instanceof IntersectionType) {
+ rendered = renderTypeList(((IntersectionType) type).elements, typeParameterNames, ' & ')
+ } else if (type instanceof UnionType) {
+ rendered = renderTypeList(((UnionType) type).elements, typeParameterNames, ' | ')
+ } else {
+ rendered = type.toString()
+ }
+
+ toVarArgIfNeeded(rendered, varArgs)
+ }
+
+ /**
+ * 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) {
+ return eraseClassOrInterfaceType((ClassOrInterfaceType) type, 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 || isIntrinsicName(name, typeParameterNames)) return name
+
+ int dot = name.indexOf('.')
+ if (dot < 0) return resolveSimpleName(name, typeParameterNames)
+
+ String first = name.substring(0, dot)
+ if (!startsWithUppercase(first) && !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 + '<' + renderTypeList(type.typeArguments.get(), typeParameterNames, ', ') + '>'
+ }
+
+ 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 (isIntrinsicName(name, typeParameterNames)) return name
+ if (explicitImports.containsKey(name)) return explicitImports[name]
+
+ resolveInCurrentPackage(name)
+ ?: resolveFromImportPrefixes(name, wildcardImports)
+ ?: resolveFromImportPrefixes(name, DEFAULT_IMPORT_LIST)
+ ?: uniquelyMatchedSimpleName(name)
+ ?: name
+ }
+
+ private boolean knownType(String candidate) {
+ if (knownTypes.contains(candidate)) return true
+ if (JavaExtensionSourceSet.tryLoad(candidate) != null) {
+ knownTypes.add(candidate)
+ return true
+ }
+ false
+ }
+
+ private String eraseClassOrInterfaceType(ClassOrInterfaceType classType, Set<String> typeParameterNames) {
+ if (classType.scope.present) {
+ return rawScopedName(classType.scope.get(), typeParameterNames) + '.' + classType.nameAsString
+ }
+ resolveSimpleName(classType.nameAsString, typeParameterNames)
+ }
+
+ private String renderWildcardType(WildcardType wildcard, Set<String> typeParameterNames) {
+ if (wildcard.extendedType.present) {
+ return '? extends ' + renderType(wildcard.extendedType.get(), typeParameterNames)
+ }
+ if (wildcard.superType.present) {
+ return '? super ' + renderType(wildcard.superType.get(), typeParameterNames)
+ }
+ '?'
+ }
+
+ private String resolveInCurrentPackage(String name) {
+ if (!packageName) return null
+ String candidate = qualify(packageName, name)
+ knownType(candidate) ? candidate : null
+ }
+
+ private String resolveFromImportPrefixes(String name, Iterable<String> prefixes) {
+ for (String prefix : prefixes) {
+ String candidate = qualify(prefix, name)
+ if (knownType(candidate)) return candidate
+ }
+ null
+ }
+
+ private String uniquelyMatchedSimpleName(String name) {
+ Set<String> matches = simpleNameIndex[name]
+ matches?.size() == 1 ? matches.first() : null
+ }
+
+ private String renderTypeList(Iterable<Type> types, Set<String> typeParameterNames, String separator) {
+ types.collect { renderType(it, typeParameterNames) }.join(separator)
+ }
+
+ private static boolean isIntrinsicName(String name, Set<String> typeParameterNames) {
+ PRIMITIVES.contains(name) || name == VOID_TYPE || typeParameterNames.contains(name)
+ }
+
+ private static boolean startsWithUppercase(String name) {
+ name && Character.isUpperCase(name.charAt(0))
+ }
+
+ private static String qualify(String prefix, String simpleName) {
+ if (!prefix) return simpleName
+ prefix.endsWith('.') ? prefix + simpleName : prefix + '.' + simpleName
+ }
+
+ private static String toVarArgIfNeeded(String rendered, boolean varArgs) {
+ if (!varArgs || !rendered.endsWith('[]')) return rendered
+ rendered[0..-3] + '...'
+ }
+
+ private static String packageNameOf(CompilationUnit compilationUnit) {
+ compilationUnit.packageDeclaration.map { it.nameAsString }.orElse('')
+ }
+}
+
+/**
+ * Serializable view of one extension method consumed by {@link MockSourceGenerator}.
+ */
+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
+}
+
+/**
+ * Serializable view of one method parameter.
+ */
+final class JavaExtensionParameter {
+ String type
+ String name
+ boolean varArgs
+}
+
+/**
+ * Receiver metadata used to decide mock type shape and output location.
+ */
+final class ReceiverTypeInfo {
+ String canonicalName
+ boolean primitive
+ boolean interfaceType
+}
+
+/**
+ * Parsed representation of method javadoc text and tags.
+ */
+final class JavadocInfo {
+ String description = ''
+ List<JavadocTagInfo> tags = []
+
+ /**
+ * Parses raw javadoc comments into plain description text and tag entries.
+ *
+ * @param comment raw javadoc comment node
+ * @return parsed javadoc representation
+ */
+ 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
+ )
+ }
+}
+
+/**
+ * Serializable javadoc tag entry.
+ */
+final class JavadocTagInfo {
+ String name
+ String value
+}
+
+/**
+ * Parsed source unit paired with its originating file.
+ */
+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/JavaExtensionSourceSetTest.groovy b/subprojects/groovy-docgenerator/src/test/groovy/org/apache/groovy/docgenerator/JavaExtensionSourceSetTest.groovy
new file mode 100644
index 0000000000..b2fd283f55
--- /dev/null
+++ b/subprojects/groovy-docgenerator/src/test/groovy/org/apache/groovy/docgenerator/JavaExtensionSourceSetTest.groovy
@@ -0,0 +1,225 @@
+/*
+ * 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.StaticJavaParser
+import com.github.javaparser.ast.NodeList
+import com.github.javaparser.ast.type.IntersectionType
+import com.github.javaparser.ast.type.Type
+import com.github.javaparser.ast.type.UnionType
+import com.github.javaparser.ast.type.VoidType
+import com.github.javaparser.ast.type.WildcardType
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+
+import static org.junit.jupiter.api.Assertions.assertThrows
+
+class JavaExtensionSourceSetTest {
+
+ @Test
+ void buildsMethodViewsAndTypeInfo(@TempDir File tempDir) {
+ def sourceSet = new JavaExtensionSourceSet()
+ sourceSet.addSource(null)
+ sourceSet.addSource(new File(tempDir, 'missing/Nope.java'))
+
+ def complex = new File(tempDir, 'demo/ComplexMethods.java')
+ complex.parentFile.mkdirs()
+ complex.text = '''\
+ package demo;
+
+ import java.io.IOException;
+ import java.util.List;
+
+ public class ComplexMethods {
+ public static void noReceiver() {}
+
+ @Deprecated
+ public static String oldOne(String self) {
+ return self;
+ }
+
+ public interface Marker {}
+
+ public static class Nested {
+ public static String nested(Marker self) {
+ return self.toString();
+ }
+ }
+
+ /**
+ * Description.
+ * @param self receiver
+ * @param values values
+ * @return first value
+ */
+ public static <T extends Number & Comparable<T>> T first(List<? extends Number> self, T... values) throws IOException {
+ return values[0];
+ }
+ }
+ '''.stripIndent()
+
+ def plain = new File(tempDir, 'PlainMethods.java')
+ plain.text = '''\
+ public class PlainMethods {
+ public static int ping(int self) {
+ return self;
+ }
+ }
+ '''.stripIndent()
+
+ sourceSet.addSource(complex)
+ sourceSet.addSource(plain)
+
+ def methods = sourceSet.methods
+ assert methods*.name.containsAll(['noReceiver', 'oldOne', 'nested', 'first', 'ping'])
+
+ def noReceiver = methods.find { it.name == 'noReceiver' }
+ assert noReceiver.receiverTypeName == null
+ assert noReceiver.receiverTypeInfo == null
+ assert noReceiver.returnType == 'void'
+
+ def first = methods.find { it.name == 'first' }
+ assert first.receiverTypeName == 'java.util.List'
+ assert first.receiverTypeInfo.interfaceType
+ assert first.parameters*.name == ['self', 'values']
+ assert first.parameters[1].varArgs
+ assert first.parameters[1].type == 'T'
+ assert first.typeParameters == ['T extends java.lang.Number & java.lang.Comparable<T>']
+ assert first.exceptions == ['java.io.IOException']
+
+ def deprecatedMethod = methods.find { it.name == 'oldOne' }
+ assert deprecatedMethod.deprecated
+
+ assert sourceSet.methods.size() == methods.size()
+
+ assert sourceSet.typeInfoForFqcn('demo.ComplexMethods.Marker').interfaceType
+ assert !sourceSet.typeInfoForFqcn('demo.ComplexMethods').interfaceType
+ assert sourceSet.typeInfoForFqcn('java.util.List').interfaceType
+ assert sourceSet.typeInfoForFqcn('int').primitive
+ assert sourceSet.typeInfoForFqcn('[]').canonicalName == '[]'
+ assert sourceSet.typeInfoForFqcn(null).canonicalName == null
+ assert !sourceSet.typeInfoForFqcn('does.not.Exist').interfaceType
+ }
+
+ @Test
+ void handlesBrokenSourcesAndClassLoading(@TempDir File tempDir) {
+ def broken = new File(tempDir, 'Broken.java')
+ broken.bytes = [0, 1, 2] as byte[]
+
+ def sourceSet = new JavaExtensionSourceSet()
+ def error = assertThrows(IllegalStateException) {
+ sourceSet.addSource(broken)
+ }
+ assert error.message.contains('Unable to parse')
+
+ assert JavaExtensionSourceSet.tryLoad(null) == null
+ assert JavaExtensionSourceSet.tryLoad('') == null
+ assert JavaExtensionSourceSet.tryLoad('java.util.Map.Entry') == java.util.Map.Entry
+ assert JavaExtensionSourceSet.tryLoad('no.such.Type') == null
+ }
+
+ @Test
+ void resolvesContextTypesAcrossImportsAndBounds() {
+ def unit = StaticJavaParser.parse('''\
+ package demo.pkg;
+
+ import foo.bar;
+ import java.time.*;
+ import java.util.List;
+ import java.util.Map;
+ import static java.util.Collections.emptyList;
+
+ class LocalType {}
+ class Another {}
+ '''.stripIndent())
+
+ Map<String, Set<String>> simpleNameIndex = [
+ Unique : ['x.y.Unique'] as Set<String>,
+ Ambiguous: ['a.b.Ambiguous', 'c.d.Ambiguous'] as Set<String>
+ ]
+ Set<String> knownTypes = ['demo.pkg.LocalType', 'java.time.LocalDate'] as Set<String>
+
+ def context = new JavaExtensionContext(unit, simpleNameIndex, knownTypes)
+
+ assert context.resolveTypeName(null) == null
+ assert context.resolveTypeName('int') == 'int'
+ assert context.resolveTypeName('void') == 'void'
+ assert context.resolveTypeName('T', ['T'] as Set<String>) == 'T'
+ assert context.resolveTypeName('Map.Entry') == 'java.util.Map.Entry'
+ assert context.resolveTypeName('bar.Baz') == 'foo.bar.Baz'
+ assert context.resolveTypeName('demo.inner.Name') == 'demo.inner.Name'
+ assert context.resolveTypeName('LocalType') == 'demo.pkg.LocalType'
+ assert context.resolveTypeName('LocalDate') == 'java.time.LocalDate'
+ assert context.resolveTypeName('String') == 'java.lang.String'
+ assert context.resolveTypeName('Unique') == 'x.y.Unique'
+ assert context.resolveTypeName('Ambiguous') == 'Ambiguous'
+ assert context.resolveTypeName('Another') == 'Another'
+ assert context.resolveTypeName('Unknown.Type') == 'Unknown.Type'
+
+ assert context.renderType(new VoidType()) == 'void'
+ assert context.renderType(StaticJavaParser.parseType('String[]'), Collections.emptySet(), true) == 'java.lang.String...'
+ assert context.renderType(StaticJavaParser.parseType('Map')) == 'java.util.Map'
+ assert context.renderType(StaticJavaParser.parseType('Map.Entry<String, Integer>')) == 'java.util.Map.Entry<java.lang.String, java.lang.Integer>'
+ assert context.renderType(new WildcardType().setExtendedType(StaticJavaParser.parseClassOrInterfaceType('Number'))) == '? extends java.lang.Number'
+ assert context.renderType(new WildcardType().setSuperType(StaticJavaParser.parseClassOrInterfaceType('Integer'))) == '? super java.lang.Integer'
+ assert context.renderType(new WildcardType()) == '?'
+
+ Type intersection = new IntersectionType(NodeList.nodeList(
+ StaticJavaParser.parseType('Runnable'),
+ StaticJavaParser.parseType('java.io.Closeable')
+ ))
+ assert context.renderType(intersection) == 'java.lang.Runnable & java.io.Closeable'
+
+ Type union = new UnionType(NodeList.nodeList(
+ StaticJavaParser.parseType('java.io.IOException'),
+ StaticJavaParser.parseType('RuntimeException')
+ ))
+ assert context.renderType(union) == 'java.io.IOException | java.lang.RuntimeException'
+ assert context.renderType(StaticJavaParser.parseType('int')) == 'int'
+
+ assert context.eraseType(StaticJavaParser.parseType('List<String>[]')) == 'java.util.List[]'
+ assert context.eraseType(StaticJavaParser.parseType('Map.Entry<String, Integer>')) == 'java.util.Map.Entry'
+
+ def holder = StaticJavaParser.parse('class Holder<T extends Number & Comparable<T>, U> {}').types[0]
+ assert context.renderTypeParameter(holder.typeParameters[0], [] as Set<String>) == 'T extends java.lang.Number & java.lang.Comparable<T>'
+ assert context.renderTypeParameter(holder.typeParameters[1], [] as Set<String>) == 'U'
+ }
+
+ @Test
+ void parsesJavadocInfoWithMalformedAndMultilineTags() {
+ def declaration = StaticJavaParser.parseBodyDeclaration('''\
+ /**
+ * Summary line.
+ * @
+ * @since
+ * 1.0
+ * @param value first line
+ * second line
+ */
+ void sample() {}
+ '''.stripIndent())
+ def info = JavadocInfo.parse(declaration.javadocComment.orElse(null))
+
+ assert info.description == 'Summary line.'
+ assert info.tags*.name == ['since', 'param']
+ assert info.tags[0].value == '1.0'
+ assert info.tags[1].value.trim() == 'value first line\nsecond line'
+ assert JavadocInfo.parse(null).description == ''
+ }
+}
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..a87b630a36
--- /dev/null
+++ b/subprojects/groovy-docgenerator/src/test/groovy/org/apache/groovy/docgenerator/MockSourceGeneratorTest.groovy
@@ -0,0 +1,173 @@
+/*
+ * 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[]')
+ }
+
+ @Test
+ void coversUtilityAndCliPaths(@TempDir File tempDir) {
+ assert MockSourceGenerator.rewriteLinks('See {@link #noop()}.') == 'See {@link #noop()}.'
+ assert MockSourceGenerator.rewriteLinks('See {@link #touch(String)}.') == 'See {@link String#touch()}.'
+ assert MockSourceGenerator.resolveJdkClassName('A') == 'java.lang.Object'
+ assert MockSourceGenerator.resolveJdkClassName('B[]') == 'java.lang.Object[]'
+ assert MockSourceGenerator.resolveJdkClassName('java.lang.String') == 'java.lang.String'
+
+ assert MockSourceGenerator.displayPackageFor('NoPackage', null) == ''
+ assert MockSourceGenerator.mockPackageFor('NoPackage', null) == ''
+ assert MockSourceGenerator.mockPackageFor('demo.Value', new ReceiverTypeInfo(primitive: true)) == MockSourceGenerator.PRIMITIVES_PKG
+ assert MockSourceGenerator.mockClassNameFor('demo.Value', new ReceiverTypeInfo(primitive: true)) == 'PrimitiveValue'
+ assert MockSourceGenerator.mockClassNameFor('demo.Value[][]', null) == 'ValueArrayArray'
+ assert MockSourceGenerator.outerClassFqcn('demo.lower.name') == null
+ assert MockSourceGenerator.outerClassFqcn('demo.Outer') == null
+ assert MockSourceGenerator.outerClassFqcn('demo.Outer.Inner') == 'demo.Outer'
+
+ def generator = new MockSourceGenerator([], tempDir)
+ def serializeMethod = MockSourceGenerator.getDeclaredMethod('serializeMethod', JavaExtensionMethod, boolean, String)
+ serializeMethod.accessible = true
+ def rendered = serializeMethod.invoke(generator, new JavaExtensionMethod(
+ name: 'touch',
+ declaringClassName: 'TouchMethods',
+ parameters: [new JavaExtensionParameter(type: 'java.lang.String', name: 'self')],
+ returnType: null,
+ javadoc: null
+ ), false, '')
+ assert !rendered.contains('/**')
+ assert rendered.contains('public void touch() { }')
+
+ def bodyFor = MockSourceGenerator.getDeclaredMethod('bodyFor', JavaExtensionMethod)
+ bodyFor.accessible = true
+ assert bodyFor.invoke(null, new JavaExtensionMethod(returnType: null)) == ''
+ assert bodyFor.invoke(null, new JavaExtensionMethod(returnType: 'void')) == ''
+ assert bodyFor.invoke(null, new JavaExtensionMethod(returnType: 'int')) == 'throw new UnsupportedOperationException();'
+
+ def sourceFile = new File(tempDir, 'demo/CliStaticMethods.java')
+ sourceFile.parentFile.mkdirs()
+ sourceFile.text = '''\
+ package demo;
+
+ public class CliStaticMethods {
+ public static int inc(int self) {
+ return self + 1;
+ }
+ }
+ '''.stripIndent()
+
+ def outputDir = new File(tempDir, 'cli-out')
+
+ MockSourceGenerator.main('--help')
+ MockSourceGenerator.main('--outputDir', outputDir.absolutePath, sourceFile.absolutePath, 'org.codehaus.groovy.runtime.DefaultGroovyMethods')
+
+ assert new File(outputDir, MockSourceGenerator.MANIFEST_FILE).exists()
+ assert new File(outputDir, 'primitives/PrimitiveInt.java').exists()
+ }
+}
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