aboutsummaryrefslogtreecommitdiff
path: root/plugins/base
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/base')
-rw-r--r--plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt43
-rw-r--r--plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt63
-rw-r--r--plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt51
-rw-r--r--plugins/base/src/test/kotlin/model/ClassesTest.kt19
-rw-r--r--plugins/base/src/test/kotlin/model/JavaTest.kt25
5 files changed, 159 insertions, 42 deletions
diff --git a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt
index dfa7b480..4f292ca1 100644
--- a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt
+++ b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt
@@ -147,7 +147,8 @@ private class DokkaDescriptorVisitor(
sourceSets = setOf(sourceSet),
extra = PropertyContainer.withAll(
descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(),
- descriptor.getAnnotations().toSourceSetDependent().toAnnotations()
+ descriptor.getAnnotations().toSourceSetDependent().toAnnotations(),
+ ImplementedInterfaces(info.interfaces)
)
)
}
@@ -173,7 +174,8 @@ private class DokkaDescriptorVisitor(
sourceSets = setOf(sourceSet),
extra = PropertyContainer.withAll(
descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(),
- descriptor.getAnnotations().toSourceSetDependent().toAnnotations()
+ descriptor.getAnnotations().toSourceSetDependent().toAnnotations(),
+ ImplementedInterfaces(info.interfaces)
)
)
}
@@ -201,7 +203,8 @@ private class DokkaDescriptorVisitor(
sourceSets = setOf(sourceSet),
extra = PropertyContainer.withAll(
descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(),
- descriptor.getAnnotations().toSourceSetDependent().toAnnotations()
+ descriptor.getAnnotations().toSourceSetDependent().toAnnotations(),
+ ImplementedInterfaces(info.interfaces)
)
)
}
@@ -284,7 +287,8 @@ private class DokkaDescriptorVisitor(
sourceSets = setOf(sourceSet),
extra = PropertyContainer.withAll(
descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(),
- descriptor.getAnnotations().toSourceSetDependent().toAnnotations()
+ descriptor.getAnnotations().toSourceSetDependent().toAnnotations(),
+ ImplementedInterfaces(info.interfaces)
)
)
}
@@ -322,14 +326,14 @@ private class DokkaDescriptorVisitor(
)
}
- fun CallableMemberDescriptor.createDRI(wasOverriden: Boolean = false): Pair<DRI, Boolean> =
+ fun CallableMemberDescriptor.createDRI(wasOverridenBy: DRI? = null): Pair<DRI, DRI?> =
if (kind == CallableMemberDescriptor.Kind.DECLARATION || overriddenDescriptors.isEmpty())
- Pair(DRI.from(this), wasOverriden)
+ Pair(DRI.from(this), wasOverridenBy)
else
- overriddenDescriptors.first().createDRI(true)
+ overriddenDescriptors.first().createDRI(DRI.from(this))
override fun visitFunctionDescriptor(descriptor: FunctionDescriptor, parent: DRIWithPlatformInfo): DFunction {
- val (dri, isInherited) = descriptor.createDRI()
+ val (dri, inheritedFrom) = descriptor.createDRI()
val isExpect = descriptor.isExpect
val actual = descriptor.createSources()
@@ -352,7 +356,7 @@ private class DokkaDescriptorVisitor(
type = descriptor.returnType!!.toBound(),
sourceSets = setOf(sourceSet),
extra = PropertyContainer.withAll(
- InheritedFunction(isInherited),
+ InheritedFunction(inheritedFrom),
descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(),
descriptor.getAnnotations().toSourceSetDependent().toAnnotations()
)
@@ -550,8 +554,17 @@ private class DokkaDescriptorVisitor(
getDocumentation()?.toSourceSetDependent() ?: emptyMap()
private fun ClassDescriptor.resolveClassDescriptionData(): ClassInfo {
+ val superClasses = hashSetOf<ClassDescriptor>()
+
+ fun processSuperClasses(supers: List<ClassDescriptor>) {
+ supers.forEach {
+ superClasses.add(it)
+ processSuperClasses(it.getSuperInterfaces() + it.getAllSuperclassesWithoutAny())
+ }
+ }
+ processSuperClasses(getSuperInterfaces() + getAllSuperclassesWithoutAny())
return ClassInfo(
- (getSuperInterfaces() + getAllSuperclassesWithoutAny()).map { DRI.from(it) },
+ superClasses.map { Supertype(DRI.from(it), it.kind == ClassKind.INTERFACE) }.toList(),
resolveDescriptorData()
)
}
@@ -726,7 +739,13 @@ private class DokkaDescriptorVisitor(
private fun ValueArgument.childrenAsText() = this.safeAs<KtValueArgument>()?.children?.map {it.text }.orEmpty()
- private data class ClassInfo(val supertypes: List<DRI>, val docs: SourceSetDependent<DocumentationNode>)
+ private data class ClassInfo(private val allSupertypes: List<Supertype>, val docs: SourceSetDependent<DocumentationNode>){
+ val supertypes: List<DRI>
+ get() = allSupertypes.map { it.dri }
+
+ val interfaces: List<DRI>
+ get() = allSupertypes.filter { it.isInterface }.map { it.dri }
+ }
private fun Visibility.toDokkaVisibility(): org.jetbrains.dokka.model.Visibility = when (this) {
Visibilities.PUBLIC -> KotlinVisibility.Public
@@ -740,6 +759,8 @@ private class DokkaDescriptorVisitor(
"${this.enumClassId.relativeClassName.asString()}.${this.enumEntryName.identifier}"
private fun fallbackPackageName(): String = "[${sourceSet.displayName} root]"// TODO: error-prone, find a better way to do it
+
+ private data class Supertype(val dri: DRI, val isInterface: Boolean)
}
private fun DRI.withPackageFallbackTo(fallbackPackage: String): DRI {
diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt
index 6ec5c4f5..8b397859 100644
--- a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt
+++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt
@@ -1,18 +1,20 @@
package org.jetbrains.dokka.base.translators.psi
-import com.intellij.icons.AllIcons.Nodes.Static
-import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute
import com.intellij.lang.jvm.JvmModifier
+import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute
import com.intellij.lang.jvm.types.JvmReferenceType
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.*
import com.intellij.psi.impl.source.PsiClassReferenceType
import com.intellij.psi.impl.source.PsiImmediateClassType
-import com.intellij.psi.impl.source.tree.java.PsiArrayInitializerMemberValueImpl
import org.jetbrains.dokka.links.DRI
import org.jetbrains.dokka.links.nextTarget
import org.jetbrains.dokka.links.withClass
import org.jetbrains.dokka.model.*
+import org.jetbrains.dokka.model.doc.DocumentationLink
+import org.jetbrains.dokka.model.doc.DocumentationNode
+import org.jetbrains.dokka.model.doc.Param
+import org.jetbrains.dokka.model.doc.Text
import org.jetbrains.dokka.model.properties.PropertyContainer
import org.jetbrains.dokka.plugability.DokkaContext
import org.jetbrains.dokka.transformers.sources.SourceToDocumentableTranslator
@@ -27,9 +29,9 @@ import org.jetbrains.kotlin.load.java.propertyNamesBySetMethodName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
import org.jetbrains.kotlin.resolve.DescriptorUtils
+import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import java.io.File
-import java.lang.ClassValue
object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
@@ -120,23 +122,24 @@ object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
fun parseClasslike(psi: PsiClass, parent: DRI): DClasslike = with(psi) {
val dri = parent.withClass(name.toString())
- val ancestorsSet = hashSetOf<DRI>()
+ val ancestorsSet = hashSetOf<Ancestor>()
val superMethodsKeys = hashSetOf<Int>()
- val superMethods = mutableListOf<PsiMethod>()
+ val superMethods = mutableListOf<Pair<PsiMethod, DRI>>()
methods.forEach { superMethodsKeys.add(it.hash) }
fun parseSupertypes(superTypes: Array<PsiClassType>) {
superTypes.forEach { type ->
(type as? PsiClassType)?.takeUnless { type.shouldBeIgnored }?.resolve()?.let {
+ val definedAt = DRI.from(it)
it.methods.forEach { method ->
val hash = method.hash
if (!method.isConstructor && !superMethodsKeys.contains(hash) &&
method.getVisibility() != Visibilities.PRIVATE
) {
superMethodsKeys.add(hash)
- superMethods.add(method)
+ superMethods.add(Pair(method, definedAt))
}
}
- ancestorsSet.add(DRI.from(it))
+ ancestorsSet.add(Ancestor(DRI.from(it), it.isInterface))
parseSupertypes(it.superTypes)
}
}
@@ -145,12 +148,13 @@ object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
val (regularFunctions, accessors) = splitFunctionsAndAccessors()
val documentation = javadocParser.parseDocumentation(this).toSourceSetDependent()
val allFunctions = regularFunctions.mapNotNull { if (!it.isConstructor) parseFunction(it) else null } +
- superMethods.map { parseFunction(it, isInherited = true) }
+ superMethods.map { parseFunction(it.first, inheritedFrom = it.second) }
val source = PsiDocumentableSource(this).toSourceSetDependent()
val classlikes = innerClasses.map { parseClasslike(it, dri) }
val visibility = getVisibility().toSourceSetDependent()
- val ancestors = ancestorsSet.toList().toSourceSetDependent()
+ val ancestors = ancestorsSet.toList().map { it.dri }.toSourceSetDependent()
val modifiers = getModifier().toSourceSetDependent()
+ val implementedInterfacesExtra = ImplementedInterfaces(ancestorsSet.filter { it.isInterface }.map { it.dri }.toList())
return when {
isAnnotationType ->
DAnnotation(
@@ -167,8 +171,8 @@ object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
constructors.map { parseFunction(it, true) },
mapTypeParameters(dri),
setOf(sourceSetData),
- PropertyContainer.empty<DAnnotation>() + annotations.toList().getAnnotations()
- .toSourceSetDependent().toAnnotations()
+ PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations())
)
isEnum -> DEnum(
dri,
@@ -183,8 +187,8 @@ object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
emptyList(),
emptyList(),
setOf(sourceSetData),
- PropertyContainer.empty<DEnumEntry>() + entry.annotations.toList().getAnnotations()
- .toSourceSetDependent().toAnnotations()
+ PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations())
)
},
documentation,
@@ -198,8 +202,8 @@ object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
constructors.map { parseFunction(it, true) },
ancestors,
setOf(sourceSetData),
- PropertyContainer.empty<DEnum>() + annotations.toList().getAnnotations().toSourceSetDependent()
- .toAnnotations()
+ PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations())
)
isInterface -> DInterface(
dri,
@@ -215,8 +219,8 @@ object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
mapTypeParameters(dri),
ancestors,
setOf(sourceSetData),
- PropertyContainer.empty<DInterface>() + annotations.toList().getAnnotations().toSourceSetDependent()
- .toAnnotations()
+ PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations())
)
else -> DClass(
dri,
@@ -234,8 +238,8 @@ object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
null,
modifiers,
setOf(sourceSetData),
- PropertyContainer.empty<DClass>() + annotations.toList().getAnnotations().toSourceSetDependent()
- .toAnnotations()
+ PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations())
)
}
}
@@ -243,9 +247,10 @@ object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
private fun parseFunction(
psi: PsiMethod,
isConstructor: Boolean = false,
- isInherited: Boolean = false
+ inheritedFrom: DRI? = null
): DFunction {
val dri = DRI.from(psi)
+ val docs = javadocParser.parseDocumentation(psi).toSourceSetDependent()
return DFunction(
dri,
if (isConstructor) "<init>" else psi.name,
@@ -254,13 +259,13 @@ object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
DParameter(
dri.copy(target = dri.target.nextTarget()),
psiParameter.name,
- javadocParser.parseDocumentation(psiParameter).toSourceSetDependent(),
+ DocumentationNode(docs.entries.mapNotNull { it.value.children.filterIsInstance<Param>().firstOrNull { it.root.children.firstIsInstanceOrNull<DocumentationLink>()?.children?.firstIsInstanceOrNull<Text>()?.body == psiParameter.name } }).toSourceSetDependent(),
null,
getBound(psiParameter.type),
setOf(sourceSetData)
)
},
- javadocParser.parseDocumentation(psi).toSourceSetDependent(),
+ docs,
null,
PsiDocumentableSource(psi).toSourceSetDependent(),
psi.getVisibility().toSourceSetDependent(),
@@ -271,9 +276,9 @@ object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
setOf(sourceSetData),
psi.additionalExtras().let {
PropertyContainer.withAll(
- InheritedFunction(isInherited),
+ InheritedFunction(inheritedFrom),
it.toSourceSetDependent().toAdditionalModifiers(),
- (psi.annotations.toList().getAnnotations() + it.getAnnotations()).toSourceSetDependent()
+ (psi.annotations.toList().toListOfAnnotations() + it.toListOfAnnotations()).toSourceSetDependent()
.toAnnotations()
)
}
@@ -291,7 +296,7 @@ object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
).toSet()
- private fun Set<ExtraModifiers>.getAnnotations() = map {
+ private fun Set<ExtraModifiers>.toListOfAnnotations() = map {
if (it !is ExtraModifiers.JavaOnlyModifiers.Static)
Annotations.Annotation(DRI("kotlin.jvm", it.name.toLowerCase().capitalize()), emptyMap())
else
@@ -398,14 +403,14 @@ object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
psi.additionalExtras().let {
PropertyContainer.withAll<DProperty>(
it.toSourceSetDependent().toAdditionalModifiers(),
- (psi.annotations.toList().getAnnotations() + it.getAnnotations()).toSourceSetDependent()
+ (psi.annotations.toList().toListOfAnnotations() + it.toListOfAnnotations()).toSourceSetDependent()
.toAnnotations()
)
}
)
}
- private fun Collection<PsiAnnotation>.getAnnotations() =
+ private fun Collection<PsiAnnotation>.toListOfAnnotations() =
filter { it !is KtLightAbstractAnnotation }.mapNotNull { it.toAnnotation() }
private fun JvmAnnotationAttribute.toValue(): AnnotationParameterValue = when (this) {
@@ -436,4 +441,6 @@ object DefaultPsiToDocumentableTranslator : SourceToDocumentableTranslator {
DRI.from(it)
}
}
+
+ private data class Ancestor(val dri: DRI, val isInterface: Boolean)
}
diff --git a/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt b/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt
index 335d834e..f66f88db 100644
--- a/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt
+++ b/plugins/base/src/test/kotlin/content/params/ContentForParamsTest.kt
@@ -1,8 +1,18 @@
package content.params
import matchers.content.*
+import org.jetbrains.dokka.Platform
+import org.jetbrains.dokka.model.DFunction
+import org.jetbrains.dokka.model.Documentable
+import org.jetbrains.dokka.model.SourceSetData
+import org.jetbrains.dokka.model.doc.DocumentationNode
+import org.jetbrains.dokka.model.doc.Param
+import org.jetbrains.dokka.model.doc.Text
import org.jetbrains.dokka.pages.ContentPage
+import org.jetbrains.dokka.pages.MemberPageNode
+import org.jetbrains.dokka.pages.dfs
import org.jetbrains.dokka.testApi.testRunner.AbstractCoreTest
+import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.junit.jupiter.api.Test
import utils.*
@@ -558,4 +568,45 @@ class ContentForParamsTest : AbstractCoreTest() {
}
}
}
+
+ @Test
+ fun javaDocCommentWithDocumentedParameters(){
+ testInline(
+ """
+ |/src/main/java/test/Main.java
+ |package test
+ | public class Main {
+ |
+ | /**
+ | * comment to function
+ | * @param first comment to first param
+ | * @param second comment to second param
+ | */
+ | public void sample(String first, String second) {
+ |
+ | }
+ | }
+ """.trimIndent(), testConfiguration
+ ){
+ pagesTransformationStage = {
+ module ->
+ val sampleFunction = module.dfs {
+ it is MemberPageNode && it.dri.first().toString() == "test/Main/sample/#java.lang.String#java.lang.String/PointingToDeclaration/"
+ } as MemberPageNode
+ val forJvm = (sampleFunction.documentable as DFunction).parameters.mapNotNull {
+ val jvm = it.documentation.keys.first { it.platform == Platform.jvm }
+ it.documentation[jvm]
+ }
+
+ assert(forJvm.size == 2)
+ val (first, second) = forJvm.map { it.paramsDescription() }
+ assert(first == "comment to first param")
+ assert(second == "comment to second param")
+ }
+ }
+ }
+
+ private fun DocumentationNode.paramsDescription(): String =
+ children.firstIsInstanceOrNull<Param>()?.root?.children?.firstIsInstanceOrNull<Text>()?.body.orEmpty()
+
} \ No newline at end of file
diff --git a/plugins/base/src/test/kotlin/model/ClassesTest.kt b/plugins/base/src/test/kotlin/model/ClassesTest.kt
index 5616a6c3..9121f9ae 100644
--- a/plugins/base/src/test/kotlin/model/ClassesTest.kt
+++ b/plugins/base/src/test/kotlin/model/ClassesTest.kt
@@ -1,6 +1,7 @@
package model
import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.links.sureClassNames
import org.jetbrains.dokka.model.*
import org.jetbrains.dokka.model.KotlinModifier.*
import org.junit.jupiter.api.Assertions.assertNull
@@ -425,7 +426,7 @@ class ClassesTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "class
"""@Suppress("abc") class Foo() {}"""
) {
with((this / "classes" / "Foo").cast<DClass>()) {
- with(extra[Annotations]!!.content.entries.single().value.firstOrNull().assertNotNull("annotations")) {
+ with(extra[Annotations]?.content?.firstOrNull().assertNotNull("annotations")) {
dri.toString() equals "kotlin/Suppress///PointingToDeclaration/"
(params["names"].assertNotNull("param") as ArrayValue).value equals listOf(StringValue("\"abc\""))
}
@@ -483,4 +484,20 @@ class ClassesTest : AbstractModelTest("/src/main/kotlin/classes/Test.kt", "class
}
}
}
+
+ @Test fun allImplementedInterfaces() {
+ inlineModelTest(
+ """
+ | interface Highest { }
+ | open class HighestImpl: Highest { }
+ | interface Lower { }
+ | interface LowerImplInterface: Lower { }
+ | class Tested : HighestImpl(), LowerImplInterface { }
+ """.trimIndent()
+ ){
+ with((this / "classes" / "Tested").cast<DClass>()){
+ extra[ImplementedInterfaces]?.interfaces?.map { it.sureClassNames }?.sorted() equals listOf("Highest", "Lower", "LowerImplInterface").sorted()
+ }
+ }
+ }
} \ No newline at end of file
diff --git a/plugins/base/src/test/kotlin/model/JavaTest.kt b/plugins/base/src/test/kotlin/model/JavaTest.kt
index 8f52fcc8..77fcc666 100644
--- a/plugins/base/src/test/kotlin/model/JavaTest.kt
+++ b/plugins/base/src/test/kotlin/model/JavaTest.kt
@@ -2,16 +2,22 @@ package model
import org.jetbrains.dokka.base.transformers.documentables.InheritorsInfo
import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.links.sureClassNames
import org.jetbrains.dokka.model.*
+import org.jetbrains.dokka.model.doc.Param
+import org.jetbrains.dokka.model.doc.Text
+import org.jetbrains.dokka.pages.dfs
+import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import utils.AbstractModelTest
import utils.assertNotNull
+import utils.docs
import utils.name
class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") {
- @Test //todo params in comments
+ @Test
fun function() {
inlineModelTest(
"""
@@ -30,12 +36,27 @@ class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") {
children counts 1
with((this / "fn").cast<DFunction>()) {
name equals "fn"
- this
+ val params = parameters.map { it.documentation.values.first().children.first() as Param }
+ params.mapNotNull { it.root.children.firstIsInstanceOrNull<Text>()?.body } equals listOf("is String parameter", "is int parameter")
}
}
}
}
+ @Test fun allImplementedInterfacesInJava() {
+ inlineModelTest(
+ """
+ |interface Highest { }
+ |interface Lower extends Highest { }
+ |class Extendable { }
+ |class Tested extends Extendable implements Lower { }
+ """){
+ with((this / "java" / "Tested").cast<DClass>()){
+ extra[ImplementedInterfaces]?.interfaces?.map { it.sureClassNames }?.sorted() equals listOf("Highest", "Lower").sorted()
+ }
+ }
+ }
+
//@Test fun function() {
// verifyJavaPackageMember("testdata/java/member.java", defaultModelConfig) { cls ->
// assertEquals("Test", cls.name)