Skip to content

Commit

Permalink
Update Java supertype processing (#3997)
Browse files Browse the repository at this point in the history
  • Loading branch information
juliamcclellan authored Jan 29, 2025
1 parent ac6e7f1 commit af87c48
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,11 @@ internal class DokkaPsiParser(
* - superMethods
* - superFieldsKeys
* - superKeys
*
* First processes the list of [PsiClassType]s to add their methods and fields to the maps mentioned above,
* then filters the list to return a pair of the optional superclass type and a list of interface types.
*/
/**
* Caution! This method mutates
* - superMethodsKeys
* - superMethods
* - superFieldsKeys
* - superKeys
*/
fun Array<PsiClassType>.getSuperTypesPsiClasses(): List<Pair<PsiClass, JavaClassKindTypes>> {
fun List<PsiClassType>.getSuperclassAndInterfaces(): Pair<PsiClassType?, List<PsiClassType>> {
forEach { type ->
type.resolve()?.let {
val definedAt = DRI.from(it)
Expand All @@ -137,38 +133,61 @@ internal class DokkaPsiParser(
}
}
}
return filter { !it.shouldBeIgnored }.mapNotNull { supertypePsi ->
val supertypesToKinds = filter { !it.shouldBeIgnored }.mapNotNull { supertypePsi ->
supertypePsi.resolve()?.let { supertypePsiClass ->
val javaClassKind = when {
supertypePsiClass.isInterface -> JavaClassKindTypes.INTERFACE
else -> JavaClassKindTypes.CLASS
}
supertypePsiClass to javaClassKind
supertypePsi to javaClassKind
}
}
val (superclassPairs, interfacePairs) =
supertypesToKinds.partition { it.second == JavaClassKindTypes.CLASS }
return superclassPairs.firstOrNull()?.first to interfacePairs.map { it.first}
}

fun traversePsiClassForAncestorsAndInheritedMembers(psiClass: PsiClass): AncestryNode {
val (classes, interfaces) = psiClass.superTypes.getSuperTypesPsiClasses()
.partition { it.second == JavaClassKindTypes.CLASS }
/**
* Creates an [AncestryNode] for the [type] given the list of all [supertypes].
*
* Also processes all super methods and fields using the getSuperclassAndInterfaces function defined above.
*/
fun createAncestryNode(type: GenericTypeConstructor, supertypes: List<PsiClassType>): AncestryNode {
fun createAncestryNodeForPsiClassType(psiClassType: PsiClassType): AncestryNode {
return createAncestryNode(
type = GenericTypeConstructor(
DRI.from(psiClassType.resolve()!!),
psiClassType.parameters.map { getProjection(it) }
),
supertypes = psiClassType.superTypes.filterIsInstance<PsiClassType>()
)
}

val (superclass, interfaces) = supertypes.getSuperclassAndInterfaces()
return AncestryNode(
typeConstructor = GenericTypeConstructor(
DRI.from(psiClass),
psiClass.typeParameters.map { typeParameter ->
TypeParameter(
dri = DRI.from(typeParameter),
name = typeParameter.name.orEmpty(),
extra = typeParameter.annotations()
)
}
),
superclass = classes.singleOrNull()?.first?.let(::traversePsiClassForAncestorsAndInheritedMembers),
interfaces = interfaces.map { traversePsiClassForAncestorsAndInheritedMembers(it.first) }
typeConstructor = type,
superclass = superclass?.let(::createAncestryNodeForPsiClassType),
interfaces = interfaces.map { createAncestryNodeForPsiClassType(it) }
)
}

val ancestry: AncestryNode = traversePsiClassForAncestorsAndInheritedMembers(this)
// Creates the AncestryNode for this class. The AncestryNodes for this class's supertypes will be done using
// PsiClassTypes, not PsiClasses. This is important because the type parameters used in the class hierarchy
// should reflect the usage in the extends/implements clause, not the type parameters in the supertype
// class definitions, which the PsiClasses would use.
val ancestry = createAncestryNode(
type = GenericTypeConstructor(
DRI.from(this),
typeParameters.map { typeParameter ->
TypeParameter(
dri = DRI.from(typeParameter),
name = typeParameter.name.orEmpty(),
extra = typeParameter.annotations()
)
}
),
supertypes = superTypes.toList()
)

val (regularFunctions, accessors) = splitFunctionsAndAccessors(psi.fields, psi.methods)
val (regularSuperFunctions, superAccessors) = splitFunctionsAndAccessors(
Expand Down
58 changes: 58 additions & 0 deletions dokka-subprojects/plugin-base/src/test/kotlin/model/JavaTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,26 @@ class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") {
}
}

@Test fun allImplementedInterfacesWithGenericsInJava() {
inlineModelTest(
"""
|interface Highest<H> { }
|interface Lower<L> extends Highest<L> { }
|class Extendable { }
|class Tested<T> extends Extendable implements Lower<T> { }
""", configuration = configuration){
with((this / "java" / "Tested").cast<DClass>()){
val implementedInterfaces = extra[ImplementedInterfaces]?.interfaces?.entries?.single()?.value!!
implementedInterfaces.map { it.dri.sureClassNames }.sorted() equals listOf("Highest", "Lower").sorted()
for (implementedInterface in implementedInterfaces) {
// The type parameter T from Tested should be used for each interface, not the type parameters in
// the interface definitions.
assertEquals((implementedInterface.projections.single() as TypeParameter).name, "T")
}
}
}
}

@Test fun multipleClassInheritanceWithInterface() {
inlineModelTest(
"""
Expand All @@ -94,6 +114,25 @@ class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") {
}
}

@Test
fun interfaceWithGeneric() {
inlineModelTest(
"""
|interface Bar<T> {}
|public class Foo implements Bar<String> {}
""", configuration = configuration
) {
with((this / "java" / "Foo").cast<DClass>()) {
val interfaceType = supertypes.values.flatten().single()
assertEquals(interfaceType.kind, JavaClassKindTypes.INTERFACE)
assertEquals(interfaceType.typeConstructor.dri.classNames, "Bar")
// The interface type should be Bar<String>, and not use Bar<T> like the interface definition
val generic = interfaceType.typeConstructor.projections.single() as GenericTypeConstructor
assertEquals(generic.dri.classNames, "String")
}
}
}

@Test
fun superClass() {
inlineModelTest(
Expand All @@ -110,6 +149,25 @@ class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") {
}
}

@Test
fun superclassWithGeneric() {
inlineModelTest(
"""
|class Bar<T> {}
|public class Foo extends Bar<String> {}
""", configuration = configuration
) {
with((this / "java" / "Foo").cast<DClass>()) {
val superclassType = supertypes.values.flatten().single()
assertEquals(superclassType.kind, JavaClassKindTypes.CLASS)
assertEquals(superclassType.typeConstructor.dri.classNames, "Bar")
// The superclass type should be Bar<String>, and not use Bar<T> like the class definition
val generic = superclassType.typeConstructor.projections.single() as GenericTypeConstructor
assertEquals(generic.dri.classNames, "String")
}
}
}

@Test
fun arrayType() {
inlineModelTest(
Expand Down

0 comments on commit af87c48

Please sign in to comment.