From ef51f7e2466e28db0943d528b6b489aefd098c0d Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Tue, 30 Dec 2014 16:34:08 +0100 Subject: annotations work in progress --- test/data/functions/annotatedFunction.kt | 2 ++ test/data/functions/functionWithAnnotatedParam.kt | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 test/data/functions/annotatedFunction.kt create mode 100644 test/data/functions/functionWithAnnotatedParam.kt (limited to 'test/data/functions') diff --git a/test/data/functions/annotatedFunction.kt b/test/data/functions/annotatedFunction.kt new file mode 100644 index 00000000..11c19672 --- /dev/null +++ b/test/data/functions/annotatedFunction.kt @@ -0,0 +1,2 @@ +inline fun f() { +} diff --git a/test/data/functions/functionWithAnnotatedParam.kt b/test/data/functions/functionWithAnnotatedParam.kt new file mode 100644 index 00000000..8922f765 --- /dev/null +++ b/test/data/functions/functionWithAnnotatedParam.kt @@ -0,0 +1,2 @@ +fun function([noinline] notInlined: () -> Unit) { +} -- cgit From 69dd2988ec98a9fa027fcc805f28efbe8758d476 Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Tue, 30 Dec 2014 18:47:03 +0100 Subject: support and render annotation parameters --- src/Kotlin/DocumentationBuilder.kt | 25 +++++++++++- src/Kotlin/KotlinLanguageService.kt | 8 ++++ src/Model/DocumentationNode.kt | 2 + .../annotatedClassWithAnnotationParameters.kt | 1 + test/data/classes/javaAnnotationClass.kt | 5 +++ test/data/format/annotationParams.kt | 1 + test/data/format/annotationParams.md | 12 ++++++ .../annotatedFunctionWithAnnotationParameters.kt | 1 + test/src/TestAPI.kt | 3 ++ test/src/format/MarkdownFormatTest.kt | 6 +++ test/src/model/ClassTest.kt | 44 ++++++++++++++++++++++ test/src/model/FunctionTest.kt | 23 +++++++++++ 12 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 test/data/classes/annotatedClassWithAnnotationParameters.kt create mode 100644 test/data/classes/javaAnnotationClass.kt create mode 100644 test/data/format/annotationParams.kt create mode 100644 test/data/format/annotationParams.md create mode 100644 test/data/functions/annotatedFunctionWithAnnotationParameters.kt (limited to 'test/data/functions') diff --git a/src/Kotlin/DocumentationBuilder.kt b/src/Kotlin/DocumentationBuilder.kt index 57f8572f..c00580c5 100644 --- a/src/Kotlin/DocumentationBuilder.kt +++ b/src/Kotlin/DocumentationBuilder.kt @@ -9,6 +9,9 @@ import org.jetbrains.jet.lang.resolve.lazy.* import org.jetbrains.jet.lang.descriptors.annotations.Annotated import org.jetbrains.jet.lang.descriptors.annotations.AnnotationDescriptor import org.jetbrains.jet.lang.resolve.DescriptorUtils +import org.jetbrains.jet.lang.resolve.constants.CompileTimeConstant +import com.intellij.openapi.util.text.StringUtil +import org.jetbrains.jet.lang.descriptors.impl.EnumEntrySyntheticClassDescriptor public data class DocumentationOptions(val includeNonPublic: Boolean = false) @@ -290,10 +293,30 @@ class DocumentationBuilder(val session: ResolveSession, val options: Documentati fun AnnotationDescriptor.build(): DocumentationNode { val annotationClass = getType().getConstructor().getDeclarationDescriptor() val node = DocumentationNode(annotationClass.getName().asString(), Content.Empty, DocumentationNode.Kind.Annotation) - // TODO handle parameters + val arguments = getAllValueArguments().toList().sortBy { it.first.getIndex() } + arguments.forEach { + val valueNode = it.second.build() + if (valueNode != null) { + val paramNode = DocumentationNode(it.first.getName().asString(), Content.Empty, DocumentationNode.Kind.Parameter) + paramNode.append(valueNode, DocumentationReference.Kind.Detail) + node.append(paramNode, DocumentationReference.Kind.Detail) + } + } return node } + fun CompileTimeConstant.build(): DocumentationNode? { + val value = getValue() + val valueString = when(value) { + is String -> + "\"" + StringUtil.escapeStringCharacters(value) + "\"" + is EnumEntrySyntheticClassDescriptor -> + value.getContainingDeclaration().getName().asString() + "." + value.getName() + else -> value?.toString() + } + return if (valueString != null) DocumentationNode(valueString, Content.Empty, DocumentationNode.Kind.Value) else null + } + /** * Generates cross-references for documentation such as extensions for a type, inheritors, etc * diff --git a/src/Kotlin/KotlinLanguageService.kt b/src/Kotlin/KotlinLanguageService.kt index 0538ba74..92d5bf1d 100644 --- a/src/Kotlin/KotlinLanguageService.kt +++ b/src/Kotlin/KotlinLanguageService.kt @@ -172,6 +172,14 @@ class KotlinLanguageService : LanguageService { private fun ContentNode.renderAnnotation(node: DocumentationNode) { identifier(node.name) + val parameters = node.details(DocumentationNode.Kind.Parameter) + if (!parameters.isEmpty()) { + symbol("(") + renderList(parameters) { + text(it.detail(DocumentationNode.Kind.Value).name) + } + symbol(")") + } text(" ") } diff --git a/src/Model/DocumentationNode.kt b/src/Model/DocumentationNode.kt index 0698a5d0..caae77a8 100644 --- a/src/Model/DocumentationNode.kt +++ b/src/Model/DocumentationNode.kt @@ -90,6 +90,8 @@ public open class DocumentationNode(val name: String, Module Annotation + + Value } } diff --git a/test/data/classes/annotatedClassWithAnnotationParameters.kt b/test/data/classes/annotatedClassWithAnnotationParameters.kt new file mode 100644 index 00000000..1af97e75 --- /dev/null +++ b/test/data/classes/annotatedClassWithAnnotationParameters.kt @@ -0,0 +1 @@ +deprecated("should no longer be used") class Foo() {} diff --git a/test/data/classes/javaAnnotationClass.kt b/test/data/classes/javaAnnotationClass.kt new file mode 100644 index 00000000..c5f5cac4 --- /dev/null +++ b/test/data/classes/javaAnnotationClass.kt @@ -0,0 +1,5 @@ +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy + +Retention(RetentionPolicy.SOURCE) +public annotation class throws() diff --git a/test/data/format/annotationParams.kt b/test/data/format/annotationParams.kt new file mode 100644 index 00000000..ee5b524a --- /dev/null +++ b/test/data/format/annotationParams.kt @@ -0,0 +1 @@ +inlineOptions(InlineOption.LOCAL_CONTINUE_AND_BREAK) fun f() {} diff --git a/test/data/format/annotationParams.md b/test/data/format/annotationParams.md new file mode 100644 index 00000000..6bbdc0e5 --- /dev/null +++ b/test/data/format/annotationParams.md @@ -0,0 +1,12 @@ +[test](out.md) / [](out.md) / [f](out.md) + + +# f + + +``` +inlineOptions([InlineOption.LOCAL_CONTINUE_AND_BREAK]) fun f(): Unit +``` + + + diff --git a/test/data/functions/annotatedFunctionWithAnnotationParameters.kt b/test/data/functions/annotatedFunctionWithAnnotationParameters.kt new file mode 100644 index 00000000..ee5b524a --- /dev/null +++ b/test/data/functions/annotatedFunctionWithAnnotationParameters.kt @@ -0,0 +1 @@ +inlineOptions(InlineOption.LOCAL_CONTINUE_AND_BREAK) fun f() {} diff --git a/test/src/TestAPI.kt b/test/src/TestAPI.kt index cc09f001..af1b8e52 100644 --- a/test/src/TestAPI.kt +++ b/test/src/TestAPI.kt @@ -7,6 +7,7 @@ import org.jetbrains.dokka.* import org.jetbrains.jet.lang.descriptors.DeclarationDescriptor import java.io.File import kotlin.test.assertEquals +import com.intellij.openapi.application.PathManager public fun verifyModel(vararg files: String, verifier: (DocumentationModule) -> Unit) { val messageCollector = object : MessageCollector { @@ -27,6 +28,8 @@ public fun verifyModel(vararg files: String, verifier: (DocumentationModule) -> } val environment = AnalysisEnvironment(messageCollector) { + val stringRoot = PathManager.getResourceRoot(javaClass(), "/java/lang/String.class") + addClasspath(File(stringRoot)) addSources(files.toList()) } diff --git a/test/src/format/MarkdownFormatTest.kt b/test/src/format/MarkdownFormatTest.kt index a1fc7ac1..08267d32 100644 --- a/test/src/format/MarkdownFormatTest.kt +++ b/test/src/format/MarkdownFormatTest.kt @@ -31,4 +31,10 @@ public class MarkdownFormatTest { markdownService.appendNodes(tempLocation, output, model.members.single().members) } } + + Test fun annotationParams() { + verifyOutput("test/data/format/annotationParams.kt", ".md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } } diff --git a/test/src/model/ClassTest.kt b/test/src/model/ClassTest.kt index 257d73eb..ae82a4f1 100644 --- a/test/src/model/ClassTest.kt +++ b/test/src/model/ClassTest.kt @@ -179,4 +179,48 @@ public class ClassTest { } } } + + Test fun annotatedClassWithAnnotationParameters() { + verifyModel("test/data/classes/annotatedClassWithAnnotationParameters.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(1, annotations.count()) + with(annotations[0]) { + assertEquals("deprecated", name) + assertEquals(Content.Empty, content) + assertEquals(DocumentationNode.Kind.Annotation, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(DocumentationNode.Kind.Parameter, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(DocumentationNode.Kind.Value, kind) + assertEquals("\"should no longer be used\"", name) + } + } + } + } + } + } + + Test fun javaAnnotationClass() { + verifyModel("test/data/classes/javaAnnotationClass.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(1, annotations.count()) + with(annotations[0]) { + assertEquals("Retention", name) + assertEquals(Content.Empty, content) + assertEquals(DocumentationNode.Kind.Annotation, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(DocumentationNode.Kind.Parameter, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(DocumentationNode.Kind.Value, kind) + assertEquals("RetentionPolicy.SOURCE", name) + } + } + } + } + } + } } diff --git a/test/src/model/FunctionTest.kt b/test/src/model/FunctionTest.kt index d3d7843a..bf7471ea 100644 --- a/test/src/model/FunctionTest.kt +++ b/test/src/model/FunctionTest.kt @@ -163,4 +163,27 @@ Documentation""", content.description.toTestString()) } } } + + Test fun annotatedFunctionWithAnnotationParameters() { + verifyModel("test/data/functions/annotatedFunctionWithAnnotationParameters.kt") { model -> + with(model.members.single().members.single()) { + assertEquals(1, annotations.count()) + with(annotations[0]) { + assertEquals("inlineOptions", name) + assertEquals(Content.Empty, content) + assertEquals(DocumentationNode.Kind.Annotation, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(DocumentationNode.Kind.Parameter, kind) + assertEquals(1, details.count()) + with(details[0]) { + assertEquals(DocumentationNode.Kind.Value, kind) + assertEquals("[InlineOption.LOCAL_CONTINUE_AND_BREAK]", name) + } + } + } + } + } + } } + -- cgit From 4b0dcee83efbdb77ae5e389ee04c309c52446153 Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Fri, 9 Jan 2015 19:48:44 +0100 Subject: generate ExternalClass nodes to hold extension functions and properties for classes from other packages --- src/Formats/StructuredFormatService.kt | 11 ++++++-- src/Kotlin/DocumentationBuilder.kt | 37 ++++++++++++++++++++++++++- src/Model/DocumentationNode.kt | 1 + test/data/format/extensions.class.md | 16 ++++++++++++ test/data/format/extensions.kt | 19 ++++++++++++++ test/data/format/extensions.package.md | 18 +++++++++++++ test/data/functions/functionWithReceiver.kt | 8 +++++- test/data/properties/propertyWithReceiver.kt | 2 ++ test/src/format/MarkdownFormatTest.kt | 9 +++++++ test/src/model/FunctionTest.kt | 38 +++++++++++++++++----------- test/src/model/PropertyTest.kt | 13 ++++++++++ 11 files changed, 153 insertions(+), 19 deletions(-) create mode 100644 test/data/format/extensions.class.md create mode 100644 test/data/format/extensions.kt create mode 100644 test/data/format/extensions.package.md create mode 100644 test/data/properties/propertyWithReceiver.kt (limited to 'test/data/functions') diff --git a/src/Formats/StructuredFormatService.kt b/src/Formats/StructuredFormatService.kt index b75f39d1..cb510f80 100644 --- a/src/Formats/StructuredFormatService.kt +++ b/src/Formats/StructuredFormatService.kt @@ -178,10 +178,15 @@ public abstract class StructuredFormatService(val locationService: LocationServi for ((breadcrumbs, items) in breakdownByLocation) { appendLine(to, breadcrumbs) appendLine(to) - appendLocation(location, to, items) + appendLocation(location, to, items.filter { it.kind != DocumentationNode.Kind.ExternalClass }) } for (node in nodes) { + if (node.kind == DocumentationNode.Kind.ExternalClass) { + appendSection(location, "Extensions for ${node.name}", node.members, node, to) + continue + } + appendSection(location, "Packages", node.members(DocumentationNode.Kind.Package), node, to) appendSection(location, "Types", node.members.filter { it.kind in setOf( @@ -191,6 +196,7 @@ public abstract class StructuredFormatService(val locationService: LocationServi DocumentationNode.Kind.Object, DocumentationNode.Kind.AnnotationClass) }, node, to) + appendSection(location, "Extensions for External Classes", node.members(DocumentationNode.Kind.ExternalClass), node, to) appendSection(location, "Constructors", node.members(DocumentationNode.Kind.Constructor), node, to) appendSection(location, "Properties", node.members(DocumentationNode.Kind.Property), node, to) appendSection(location, "Functions", node.members(DocumentationNode.Kind.Function), node, to) @@ -210,7 +216,8 @@ public abstract class StructuredFormatService(val locationService: LocationServi DocumentationNode.Kind.Function, DocumentationNode.Kind.PropertyAccessor, DocumentationNode.Kind.ClassObjectProperty, - DocumentationNode.Kind.ClassObjectFunction + DocumentationNode.Kind.ClassObjectFunction, + DocumentationNode.Kind.ExternalClass ) }, node, to) appendSection(location, "Extensions", node.extensions, node, to) diff --git a/src/Kotlin/DocumentationBuilder.kt b/src/Kotlin/DocumentationBuilder.kt index c00580c5..99c81760 100644 --- a/src/Kotlin/DocumentationBuilder.kt +++ b/src/Kotlin/DocumentationBuilder.kt @@ -15,6 +15,12 @@ import org.jetbrains.jet.lang.descriptors.impl.EnumEntrySyntheticClassDescriptor public data class DocumentationOptions(val includeNonPublic: Boolean = false) +private fun isSamePackage(descriptor1: DeclarationDescriptor, descriptor2: DeclarationDescriptor): Boolean { + val package1 = DescriptorUtils.getParentOfType(descriptor1, javaClass()) + val package2 = DescriptorUtils.getParentOfType(descriptor2, javaClass()) + return package1 != null && package2 != null && package1.fqName == package2.fqName +} + class DocumentationBuilder(val session: ResolveSession, val options: DocumentationOptions) { val visibleToDocumentation = setOf(Visibilities.INTERNAL, Visibilities.PROTECTED, Visibilities.PUBLIC) val descriptorToNode = hashMapOf() @@ -141,6 +147,22 @@ class DocumentationBuilder(val session: ResolveSession, val options: Documentati descriptors.forEach { descriptor -> appendChild(descriptor, kind) } } + fun DocumentationNode.getParentForPackageMember(descriptor: DeclarationDescriptor, + externalClassNodes: MutableMap): DocumentationNode { + if (descriptor is CallableMemberDescriptor) { + val extensionClassDescriptor = descriptor.getExtensionClassDescriptor() + if (extensionClassDescriptor != null && !isSamePackage(descriptor, extensionClassDescriptor)) { + val fqName = DescriptorUtils.getFqNameFromTopLevelClass(extensionClassDescriptor) + return externalClassNodes.getOrPut(fqName, { + val newNode = DocumentationNode(fqName.asString(), Content.Empty, Kind.ExternalClass) + append(newNode, DocumentationReference.Kind.Member) + newNode + }) + } + } + return this + } + fun DocumentationNode.appendFragments(fragments: Collection) { val descriptors = hashMapOf>() for ((name, parts) in fragments.groupBy { it.fqName }) { @@ -149,7 +171,11 @@ class DocumentationBuilder(val session: ResolveSession, val options: Documentati for ((packageName, declarations) in descriptors) { println(" package $packageName: ${declarations.count()} nodes") val packageNode = DocumentationNode(packageName, Content.Empty, Kind.Package) - packageNode.appendChildren(declarations, DocumentationReference.Kind.Member) + val externalClassNodes = hashMapOf() + declarations.forEach { descriptor -> + val parent = packageNode.getParentForPackageMember(descriptor, externalClassNodes) + parent.appendChild(descriptor, DocumentationReference.Kind.Member) + } append(packageNode, DocumentationReference.Kind.Member) } } @@ -205,6 +231,15 @@ class DocumentationBuilder(val session: ResolveSession, val options: Documentati private fun DeclarationDescriptor.inClassObject() = getContainingDeclaration().let { it is ClassDescriptor && it.getKind() == ClassKind.CLASS_OBJECT } + fun CallableMemberDescriptor.getExtensionClassDescriptor(): ClassifierDescriptor? { + val extensionReceiver = getExtensionReceiverParameter() + if (extensionReceiver != null) { + val type = extensionReceiver.getType() + return type.getConstructor().getDeclarationDescriptor() as? ClassDescriptor + } + return null + } + fun FunctionDescriptor.build(): DocumentationNode { val node = DocumentationNode(this, if (inClassObject()) Kind.ClassObjectFunction else Kind.Function) diff --git a/src/Model/DocumentationNode.kt b/src/Model/DocumentationNode.kt index caae77a8..5f9aabab 100644 --- a/src/Model/DocumentationNode.kt +++ b/src/Model/DocumentationNode.kt @@ -89,6 +89,7 @@ public open class DocumentationNode(val name: String, Module + ExternalClass Annotation Value diff --git a/test/data/format/extensions.class.md b/test/data/format/extensions.class.md new file mode 100644 index 00000000..a9747756 --- /dev/null +++ b/test/data/format/extensions.class.md @@ -0,0 +1,16 @@ +[test](out.md) / [foo](out.md) / [String](out.md) + + +### Extensions for String + + +| [fn](out.md) | `fun String.fn(): Unit` +`fun String.fn(x: Int): Unit` +Function with receiver + + | +| [foobar](out.md) | `val String.foobar: Int` +Property with receiver. + + | + diff --git a/test/data/format/extensions.kt b/test/data/format/extensions.kt new file mode 100644 index 00000000..6f2eff9d --- /dev/null +++ b/test/data/format/extensions.kt @@ -0,0 +1,19 @@ +package foo + +/** + * Function with receiver + */ +fun String.fn() { +} + +/** + * Function with receiver + */ +fun String.fn(x: Int) { +} + +/** + * Property with receiver. + */ +val String.foobar: Int + get() = size() * 2 diff --git a/test/data/format/extensions.package.md b/test/data/format/extensions.package.md new file mode 100644 index 00000000..13f40457 --- /dev/null +++ b/test/data/format/extensions.package.md @@ -0,0 +1,18 @@ +[test](out.md) / [foo](out.md) + + +# foo + + +``` +package foo +``` + + + + +### Extensions for External Classes + + +| [String](out.md) | `` | + diff --git a/test/data/functions/functionWithReceiver.kt b/test/data/functions/functionWithReceiver.kt index 663c3e56..c8473251 100644 --- a/test/data/functions/functionWithReceiver.kt +++ b/test/data/functions/functionWithReceiver.kt @@ -2,4 +2,10 @@ * Function with receiver */ fun String.fn() { -} \ No newline at end of file +} + +/** + * Function with receiver + */ +fun String.fn(x: Int) { +} diff --git a/test/data/properties/propertyWithReceiver.kt b/test/data/properties/propertyWithReceiver.kt new file mode 100644 index 00000000..e282f6bd --- /dev/null +++ b/test/data/properties/propertyWithReceiver.kt @@ -0,0 +1,2 @@ +val String.foobar: Int + get() = size() * 2 diff --git a/test/src/format/MarkdownFormatTest.kt b/test/src/format/MarkdownFormatTest.kt index 08267d32..3d32743f 100644 --- a/test/src/format/MarkdownFormatTest.kt +++ b/test/src/format/MarkdownFormatTest.kt @@ -37,4 +37,13 @@ public class MarkdownFormatTest { markdownService.appendNodes(tempLocation, output, model.members.single().members) } } + + Test fun extensions() { + verifyOutput("test/data/format/extensions.kt", ".package.md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members) + } + verifyOutput("test/data/format/extensions.kt", ".class.md") { model, output -> + markdownService.appendNodes(tempLocation, output, model.members.single().members) + } + } } diff --git a/test/src/model/FunctionTest.kt b/test/src/model/FunctionTest.kt index bf7471ea..299f33a8 100644 --- a/test/src/model/FunctionTest.kt +++ b/test/src/model/FunctionTest.kt @@ -21,23 +21,32 @@ public class FunctionTest { Test fun functionWithReceiver() { verifyModel("test/data/functions/functionWithReceiver.kt") { model -> with(model.members.single().members.single()) { - assertEquals("fn", name) - assertEquals(DocumentationNode.Kind.Function, kind) - assertEquals("Function with receiver", content.summary.toTestString()) - assertEquals(4, details.count()) - assertEquals("internal", details.elementAt(0).name) - assertEquals("final", details.elementAt(1).name) - with(details.elementAt(2)) { - assertEquals("", name) - assertEquals(DocumentationNode.Kind.Receiver, kind) - assertEquals(Content.Empty, content) - assertEquals("String", details.single().name) + assertEquals("String", name) + assertEquals(DocumentationNode.Kind.ExternalClass, kind) + assertEquals(2, members.count()) + with(members[0]) { + assertEquals("fn", name) + assertEquals(DocumentationNode.Kind.Function, kind) + assertEquals("Function with receiver", content.summary.toTestString()) + assertEquals(4, details.count()) + assertEquals("internal", details.elementAt(0).name) + assertEquals("final", details.elementAt(1).name) + with(details.elementAt(2)) { + assertEquals("", name) + assertEquals(DocumentationNode.Kind.Receiver, kind) + assertEquals(Content.Empty, content) + assertEquals("String", details.single().name) + assertTrue(members.none()) + assertTrue(links.none()) + } + assertEquals("Unit", details.elementAt(3).name) assertTrue(members.none()) assertTrue(links.none()) } - assertEquals("Unit", details.elementAt(3).name) - assertTrue(members.none()) - assertTrue(links.none()) + with(members[1]) { + assertEquals("fn", name) + assertEquals(DocumentationNode.Kind.Function, kind) + } } } } @@ -186,4 +195,3 @@ Documentation""", content.description.toTestString()) } } } - diff --git a/test/src/model/PropertyTest.kt b/test/src/model/PropertyTest.kt index 0bf9714d..14c43f78 100644 --- a/test/src/model/PropertyTest.kt +++ b/test/src/model/PropertyTest.kt @@ -112,4 +112,17 @@ public class PropertyTest { } } } + + Test fun propertyWithReceiver() { + verifyModel("test/data/properties/propertyWithReceiver.kt") { model -> + with(model.members.single().members.single()) { + assertEquals("String", name) + assertEquals(DocumentationNode.Kind.ExternalClass, kind) + with(members.single()) { + assertEquals("foobar", name) + assertEquals(DocumentationNode.Kind.Property, kind) + } + } + } + } } -- cgit