diff options
-rw-r--r-- | src/Kotlin/Diagnostics.kt | 40 | ||||
-rw-r--r-- | src/Kotlin/DocumentationBuilder.kt | 326 | ||||
-rw-r--r-- | src/Kotlin/DocumentationContext.kt | 18 | ||||
-rw-r--r-- | src/Kotlin/DocumentationNodeBuilder.kt | 15 | ||||
-rw-r--r-- | src/main.kt | 19 | ||||
-rw-r--r-- | test/src/TestAPI.kt | 18 | ||||
-rw-r--r-- | test/src/model/PropertyTest.kt | 28 |
7 files changed, 378 insertions, 86 deletions
diff --git a/src/Kotlin/Diagnostics.kt b/src/Kotlin/Diagnostics.kt deleted file mode 100644 index 5548bc01..00000000 --- a/src/Kotlin/Diagnostics.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.jetbrains.dokka - -import org.jetbrains.jet.lang.descriptors.* -import org.jetbrains.jet.lang.resolve.name.* -import org.jetbrains.jet.lang.resolve.BindingContext - -fun DocumentationContext.checkResolveChildren(node: DocumentationNode) { - if (node.kind != DocumentationNode.Kind.Module && node.kind != DocumentationNode.Kind.Package) { - // TODO: we don't resolve packages and modules for now - - val parentScope = getResolutionScope(node) - for (item in node.details + node.members) { - val symbolName = item.name - val symbol: DeclarationDescriptor? = when (item.kind) { - DocumentationNode.Kind.Modifier -> continue // do not resolve modifiers, they are not names - DocumentationNode.Kind.Receiver -> continue // what is receiver's name in platform? - DocumentationNode.Kind.Parameter -> parentScope.getLocalVariable(Name.guess(symbolName)) - DocumentationNode.Kind.Function -> parentScope.getFunctions(Name.guess(symbolName)).firstOrNull() - DocumentationNode.Kind.Property -> parentScope.getProperties(Name.guess(symbolName)).firstOrNull() - DocumentationNode.Kind.Constructor -> parentScope.getFunctions(Name.guess(symbolName)).firstOrNull() - else -> parentScope.getClassifier(Name.guess(symbolName)) - } - - if (symbol == null) - println("WARNING: Cannot resolve $item in ${path(node)}") - } - } - - for (reference in node.allReferences().filterNot { it.kind == DocumentationReference.Kind.Owner }) { - checkResolveChildren(reference.to) - } -} - -fun path(node: DocumentationNode): String { - val owner = node.owner - if (owner != null) - return "$node in ${path(owner)}" - else - return "$node" -}
\ No newline at end of file diff --git a/src/Kotlin/DocumentationBuilder.kt b/src/Kotlin/DocumentationBuilder.kt new file mode 100644 index 00000000..211697cd --- /dev/null +++ b/src/Kotlin/DocumentationBuilder.kt @@ -0,0 +1,326 @@ +package org.jetbrains.dokka + +import org.jetbrains.jet.lang.descriptors.* +import org.jetbrains.dokka.DocumentationNode.Kind +import org.jetbrains.jet.lang.types.TypeProjection +import org.jetbrains.jet.lang.types.Variance +import org.jetbrains.jet.lang.types.JetType +import org.jetbrains.jet.lang.types.lang.KotlinBuiltIns +import org.jetbrains.jet.lang.resolve.BindingContext +import org.jetbrains.jet.lang.resolve.name.Name +import org.jetbrains.jet.lang.resolve.scopes.JetScope +import org.jetbrains.jet.lang.psi.JetFile +import org.jetbrains.jet.lang.resolve.name.FqName + +class DocumentationBuilder(val context: BindingContext, val options: DocumentationOptions) { + val descriptorToNode = hashMapOf<DeclarationDescriptor, DocumentationNode>() + val nodeToDescriptor = hashMapOf<DocumentationNode, DeclarationDescriptor>() + val links = hashMapOf<DocumentationNode, DeclarationDescriptor>() + val packages = hashMapOf<FqName, DocumentationNode>() + + fun parseDocumentation(descriptor: DeclarationDescriptor): Content { + val docText = context.getDocumentationElements(descriptor).map { it.extractText() }.join("\n") + val tree = MarkdownProcessor.parse(docText) + //println(tree.toTestString()) + val content = tree.toContent() + return content + } + + fun link(node: DocumentationNode, descriptor: DeclarationDescriptor) { + links.put(node, descriptor) + } + + fun register(descriptor: DeclarationDescriptor, node: DocumentationNode) { + descriptorToNode.put(descriptor, node) + nodeToDescriptor.put(node, descriptor) + } + + fun DocumentationNode<T>(descriptor: T, kind: Kind): DocumentationNode where T : DeclarationDescriptor, T : Named { + val doc = parseDocumentation(descriptor) + val node = DocumentationNode(descriptor.getName().asString(), doc, kind) + if (descriptor is MemberDescriptor) { + if (descriptor !is ConstructorDescriptor) { + node.appendModality(descriptor) + } + node.appendVisibility(descriptor) + } + return node + } + + fun DocumentationNode.append(child: DocumentationNode, kind: DocumentationReference.Kind) { + addReferenceTo(child, kind) + when (kind) { + DocumentationReference.Kind.Detail -> child.addReferenceTo(this, DocumentationReference.Kind.Owner) + DocumentationReference.Kind.Member -> child.addReferenceTo(this, DocumentationReference.Kind.Owner) + DocumentationReference.Kind.Owner -> child.addReferenceTo(this, DocumentationReference.Kind.Member) + } + } + + fun DocumentationNode.appendModality(descriptor: MemberDescriptor) { + val modifier = descriptor.getModality().name().toLowerCase() + val node = DocumentationNode(modifier, Content.Empty, DocumentationNode.Kind.Modifier) + append(node, DocumentationReference.Kind.Detail) + } + + fun DocumentationNode.appendVisibility(descriptor: DeclarationDescriptorWithVisibility) { + val modifier = descriptor.getVisibility().toString() + val node = DocumentationNode(modifier, Content.Empty, DocumentationNode.Kind.Modifier) + append(node, DocumentationReference.Kind.Detail) + } + + fun DocumentationNode.appendSupertypes(descriptor: ClassDescriptor) { + val superTypes = descriptor.getTypeConstructor().getSupertypes() + for (superType in superTypes) { + if (superType.toString() != "Any") + appendType(superType, DocumentationNode.Kind.Supertype) + } + } + + fun DocumentationNode.appendProjection(projection: TypeProjection, kind: DocumentationNode.Kind = DocumentationNode.Kind.Type) { + val prefix = when (projection.getProjectionKind()) { + Variance.IN_VARIANCE -> "in " + Variance.OUT_VARIANCE -> "out " + else -> "" + } + appendType(projection.getType(), kind, prefix) + } + + fun DocumentationNode.appendType(jetType: JetType?, kind: DocumentationNode.Kind = DocumentationNode.Kind.Type, prefix: String = "") { + if (jetType == null) + return + val classifierDescriptor = jetType.getConstructor().getDeclarationDescriptor() + val name = when (classifierDescriptor) { + is Named -> prefix + classifierDescriptor.getName().asString() + if (jetType.isNullable()) "?" else "" + else -> "<anonymous>" + } + val node = DocumentationNode(name, Content.Empty, kind) + if (classifierDescriptor != null) + link(node, classifierDescriptor) + + append(node, DocumentationReference.Kind.Detail) + for (typeArgument in jetType.getArguments()) + node.appendProjection(typeArgument) + } + + val visibleToDocumentation = setOf(Visibilities.INTERNAL, Visibilities.PROTECTED, Visibilities.PUBLIC) + + fun DocumentationNode.appendChild(descriptor: DeclarationDescriptor, kind: DocumentationReference.Kind) { + // do not include generated code + if (descriptor is CallableMemberDescriptor && descriptor.getKind() != CallableMemberDescriptor.Kind.DECLARATION) + return + + if (options.includeNonPublic + || descriptor !is MemberDescriptor + || descriptor.getVisibility() in visibleToDocumentation) { + append(descriptor.build(), kind) + } + } + + fun DocumentationNode.appendChildren(descriptors: Iterable<DeclarationDescriptor>, kind: DocumentationReference.Kind) { + descriptors.forEach { descriptor -> appendChild(descriptor, kind) } + } + + fun DocumentationNode.appendFiles(sourceFiles : List<JetFile>) { + for (sourceFile in sourceFiles) { + val fragment = context.getPackageFragment(sourceFile)!! + val packageNode = packages.getOrPut(fragment.fqName) { + val packageNode = DocumentationNode(fragment.fqName.asString(), Content.Empty, Kind.Package) + append(packageNode, DocumentationReference.Kind.Member) + packageNode + } + packageNode.appendChildren(fragment.getMemberScope().getAllDescriptors(), DocumentationReference.Kind.Member) + } + } + + fun DeclarationDescriptor.build(): DocumentationNode = when (this) { + is ClassDescriptor -> build() + is ConstructorDescriptor -> build() + is ScriptDescriptor -> build() + is FunctionDescriptor -> build() + is PropertyDescriptor -> build() + is PropertyGetterDescriptor -> build() + is PropertySetterDescriptor -> build() + is TypeParameterDescriptor -> build() + is ValueParameterDescriptor -> build() + is ReceiverParameterDescriptor -> build() + else -> throw IllegalStateException("Descriptor $this is not known") + } + + fun ScriptDescriptor.build(): DocumentationNode = getClassDescriptor().build() + fun ClassDescriptor.build(): DocumentationNode { + val kind = when (getKind()) { + ClassKind.OBJECT -> Kind.Object + ClassKind.CLASS_OBJECT -> Kind.Object + ClassKind.TRAIT -> Kind.Interface + ClassKind.ENUM_CLASS -> Kind.Enum + ClassKind.ENUM_ENTRY -> Kind.EnumItem + else -> Kind.Class + } + val node = DocumentationNode(this, kind) + node.appendSupertypes(this) + if (getKind() != ClassKind.OBJECT) { + node.appendChildren(getTypeConstructor().getParameters(), DocumentationReference.Kind.Detail) + node.appendChildren(getConstructors(), DocumentationReference.Kind.Member) + val classObjectDescriptor = getClassObjectDescriptor() + if (classObjectDescriptor != null) + node.appendChild(classObjectDescriptor, DocumentationReference.Kind.Member) + } + node.appendChildren(getDefaultType().getMemberScope().getAllDescriptors(), DocumentationReference.Kind.Member) + register(this, node) + return node + } + + + fun ConstructorDescriptor.build(): DocumentationNode { + val node = DocumentationNode(this, Kind.Constructor) + node.appendChildren(getValueParameters(), DocumentationReference.Kind.Detail) + register(this, node) + return node + } + + fun FunctionDescriptor.build(): DocumentationNode { + val node = DocumentationNode(this, Kind.Function) + + node.appendChildren(getTypeParameters(), DocumentationReference.Kind.Detail) + getExtensionReceiverParameter()?.let { node.appendChild(it, DocumentationReference.Kind.Detail) } + node.appendChildren(getValueParameters(), DocumentationReference.Kind.Detail) + node.appendType(getReturnType()) + register(this, node) + return node + + } + + fun PropertyDescriptor.build(): DocumentationNode { + val node = DocumentationNode(this, Kind.Property) + node.appendType(getReturnType()) + node.appendChildren(getTypeParameters(), DocumentationReference.Kind.Detail) + getGetter()?.let { + if (!it.isDefault()) + node.appendChild(it, DocumentationReference.Kind.Member) + } + getSetter()?.let { + if (!it.isDefault()) + node.appendChild(it, DocumentationReference.Kind.Member) + } + + register(this, node) + return node + } + + fun ValueParameterDescriptor.build(): DocumentationNode { + val node = DocumentationNode(this, Kind.Parameter) + node.appendType(getType()) + return node + } + + fun TypeParameterDescriptor.build(): DocumentationNode { + val doc = parseDocumentation(this) + val name = getName().asString() + val prefix = when (getVariance()) { + Variance.IN_VARIANCE -> "in " + Variance.OUT_VARIANCE -> "out " + else -> "" + } + + val node = DocumentationNode(prefix + name, doc, DocumentationNode.Kind.TypeParameter) + + val builtIns = KotlinBuiltIns.getInstance() + for (constraint in getUpperBounds()) { + if (constraint == builtIns.getDefaultBound()) + continue + val constraintNode = DocumentationNode(constraint.toString(), Content.Empty, DocumentationNode.Kind.UpperBound) + node.append(constraintNode, DocumentationReference.Kind.Detail) + } + + for (constraint in getLowerBounds()) { + if (builtIns.isNothing(constraint)) + continue + val constraintNode = DocumentationNode(constraint.toString(), Content.Empty, DocumentationNode.Kind.LowerBound) + node.append(constraintNode, DocumentationReference.Kind.Detail) + } + return node + } + + fun ReceiverParameterDescriptor.build(): DocumentationNode { + val node = DocumentationNode(this, Kind.Receiver) + node.appendType(getType()) + return node + } + + /** + * Generates cross-references for documentation such as extensions for a type, inheritors, etc + * + * $receiver: [DocumentationContext] for node/descriptor resolutions + * $node: [DocumentationNode] to visit + */ + public fun resolveReferences(node: DocumentationNode) { + node.details(DocumentationNode.Kind.Receiver).forEach { detail -> + val receiverType = detail.detail(DocumentationNode.Kind.Type) + val descriptor = links[receiverType] + if (descriptor != null) { + val typeNode = descriptorToNode[descriptor] + // if typeNode is null, extension is to external type like in a library + // should we create dummy node here? + typeNode?.addReferenceTo(node, DocumentationReference.Kind.Extension) + } + } + node.details(DocumentationNode.Kind.Supertype).forEach { detail -> + val descriptor = links[detail] + if (descriptor != null) { + val typeNode = descriptorToNode[descriptor] + typeNode?.addReferenceTo(node, DocumentationReference.Kind.Inheritor) + } + } + node.details.forEach { detail -> + val descriptor = links[detail] + if (descriptor != null) { + val typeNode = descriptorToNode[descriptor] + if (typeNode != null) { + detail.addReferenceTo(typeNode, DocumentationReference.Kind.Link) + } + } + } + + resolveContentLinks(node, node.doc) + + for (child in node.members) { + resolveReferences(child) + } + for (child in node.details) { + resolveReferences(child) + } + } + + fun getResolutionScope(node: DocumentationNode): JetScope { + val descriptor = nodeToDescriptor[node] ?: throw IllegalArgumentException("Node is not known to this context") + return context.getResolutionScope(descriptor) + } + + fun resolveContentLinks(node: DocumentationNode, content: ContentNode) { + val snapshot = content.children.toList() + for (child in snapshot) { + if (child is ContentExternalLink) { + val referenceText = child.href + if (Name.isValidIdentifier(referenceText)) { + val scope = getResolutionScope(node) + val symbolName = Name.guess(referenceText) + val symbol = scope.getLocalVariable(symbolName) ?: + scope.getProperties(symbolName).firstOrNull() ?: + scope.getFunctions(symbolName).firstOrNull() ?: + scope.getClassifier(symbolName) + + if (symbol != null) { + val targetNode = descriptorToNode[symbol] + val contentLink = if (targetNode != null) ContentNodeLink(targetNode) else ContentExternalLink("#") + + val index = content.children.indexOf(child) + content.children.remove(index) + contentLink.children.addAll(child.children) + content.children.add(index, contentLink) + } + } + } + resolveContentLinks(node, child) + } + } +}
\ No newline at end of file diff --git a/src/Kotlin/DocumentationContext.kt b/src/Kotlin/DocumentationContext.kt index 95c3ded1..b13f08ea 100644 --- a/src/Kotlin/DocumentationContext.kt +++ b/src/Kotlin/DocumentationContext.kt @@ -47,16 +47,14 @@ fun BindingContext.createDocumentationModule(name: String, packages: Set<FqName>, options: DocumentationOptions = DocumentationOptions()): DocumentationModule { val documentationModule = DocumentationModule(name) - val context = DocumentationContext(this) - val visitor = DocumentationNodeBuilder(context) - for (packageName in packages) { - val pkg = module.getPackage(packageName) - pkg!!.accept(DocumentationBuildingVisitor(this, options, visitor), documentationModule) + val builder = DocumentationBuilder(this, options) + with(builder) { + for (packageName in packages) { + val pkg = module.getPackage(packageName) + if (pkg != null) + documentationModule.appendChild(pkg, DocumentationReference.Kind.Member) + } } - - context.resolveReferences(documentationModule) - - // TODO: Uncomment for resolve verification - // checkResolveChildren(documentationModule) + builder.resolveReferences(documentationModule) return documentationModule } diff --git a/src/Kotlin/DocumentationNodeBuilder.kt b/src/Kotlin/DocumentationNodeBuilder.kt index 8f77a3d6..a2686b00 100644 --- a/src/Kotlin/DocumentationNodeBuilder.kt +++ b/src/Kotlin/DocumentationNodeBuilder.kt @@ -18,6 +18,7 @@ import org.jetbrains.jet.lang.descriptors.ClassKind import org.jetbrains.jet.lang.types.lang.KotlinBuiltIns import org.jetbrains.jet.lang.types.TypeProjection import org.jetbrains.jet.lang.types.Variance +import org.jetbrains.dokka.DocumentationNode.Kind class DocumentationNodeBuilder(val context: DocumentationContext) : DeclarationDescriptorVisitorEmptyBodies<DocumentationNode, DocumentationNode>() { @@ -110,12 +111,12 @@ class DocumentationNodeBuilder(val context: DocumentationContext) : DeclarationD descriptor!! val doc = context.parseDocumentation(descriptor) val node = DocumentationNode(descriptor.getName().asString(), doc, when (descriptor.getKind()) { - ClassKind.OBJECT -> org.jetbrains.dokka.DocumentationNode.Kind.Object - ClassKind.CLASS_OBJECT -> org.jetbrains.dokka.DocumentationNode.Kind.Object - ClassKind.TRAIT -> org.jetbrains.dokka.DocumentationNode.Kind.Interface - ClassKind.ENUM_CLASS -> org.jetbrains.dokka.DocumentationNode.Kind.Enum - ClassKind.ENUM_ENTRY -> org.jetbrains.dokka.DocumentationNode.Kind.EnumItem - else -> DocumentationNode.Kind.Class + ClassKind.OBJECT -> Kind.Object + ClassKind.CLASS_OBJECT -> Kind.Object + ClassKind.TRAIT -> Kind.Interface + ClassKind.ENUM_CLASS -> Kind.Enum + ClassKind.ENUM_ENTRY -> Kind.EnumItem + else -> Kind.Class }) reference(data!!, node, DocumentationReference.Kind.Member) addModality(descriptor, node) @@ -182,7 +183,7 @@ class DocumentationNodeBuilder(val context: DocumentationContext) : DeclarationD override fun visitConstructorDescriptor(descriptor: ConstructorDescriptor?, data: DocumentationNode?): DocumentationNode? { descriptor!! val doc = context.parseDocumentation(descriptor) - val node = DocumentationNode("<constructor>", doc, DocumentationNode.Kind.Constructor) + val node = DocumentationNode("<init>", doc, DocumentationNode.Kind.Constructor) reference(data!!, node, DocumentationReference.Kind.Member) addVisibility(descriptor, node) diff --git a/src/main.kt b/src/main.kt index 970bbd8c..923b2ea8 100644 --- a/src/main.kt +++ b/src/main.kt @@ -6,6 +6,8 @@ import org.jetbrains.jet.cli.common.messages.* import org.jetbrains.jet.cli.common.arguments.* import org.jetbrains.jet.utils.PathUtil import java.io.File +import org.jetbrains.jet.lang.resolve.name.FqName +import org.jetbrains.dokka.DocumentationNode.Kind class DokkaArguments { Argument(value = "src", description = "Source file or directory (allows many paths separated by the system path separator)") @@ -49,13 +51,18 @@ public fun main(args: Array<String>) { print("Analysing sources and libraries... ") val startAnalyse = System.currentTimeMillis() - val documentation = environment.withContext<DocumentationModule> { environment, module, context -> - val packageSet = environment.getSourceFiles().map { file -> - context.getPackageFragment(file)!!.fqName - }.toSet() - context.createDocumentationModule(arguments.moduleName, module, packageSet) + val documentation = environment.withContext { environment, module, context -> + val documentationModule = DocumentationModule("test") + val options = DocumentationOptions() + val documentationBuilder = DocumentationBuilder(context, options) + with(documentationBuilder) { + documentationModule.appendFiles(environment.getSourceFiles()) + } + documentationBuilder.resolveReferences(documentationModule) + documentationModule } + val timeAnalyse = System.currentTimeMillis() - startAnalyse println("done in ${timeAnalyse / 1000} secs") @@ -64,7 +71,7 @@ public fun main(args: Array<String>) { val locationService = FoldersLocationService(arguments.outputDir) val templateService = HtmlTemplateService.default("/dokka/styles/style.css") - val formatter = KotlinWebsiteFormatService(locationService, signatureGenerator) + val formatter = HtmlFormatService(locationService, signatureGenerator, templateService) val generator = FileGenerator(signatureGenerator, locationService, formatter) print("Building pages... ") generator.buildPage(documentation) diff --git a/test/src/TestAPI.kt b/test/src/TestAPI.kt index 309654e5..fcff75e9 100644 --- a/test/src/TestAPI.kt +++ b/test/src/TestAPI.kt @@ -27,18 +27,22 @@ public fun verifyModel(vararg files: String, verifier: (DocumentationModule) -> addSources(files.toList()) } - val documentation = environment.withContext<DocumentationModule> { environment, module, context -> - val packageSet = environment.getSourceFiles().map { file -> - context.getPackageFragment(file)!!.fqName - }.toSet() + val options = DocumentationOptions(includeNonPublic = true) - context.createDocumentationModule("test", module, packageSet, DocumentationOptions(includeNonPublic = true)) + val documentation = environment.withContext { environment, module, context -> + val documentationModule = DocumentationModule("test") + val documentationBuilder = DocumentationBuilder(context, options) + with(documentationBuilder) { + documentationModule.appendFiles(environment.getSourceFiles()) + } + documentationBuilder.resolveReferences(documentationModule) + documentationModule } verifier(documentation) Disposer.dispose(environment) } -fun StringBuilder.appendChildren(node: ContentNode) : StringBuilder { +fun StringBuilder.appendChildren(node: ContentNode): StringBuilder { for (child in node.children) { val childText = child.toTestString() append(childText) @@ -46,7 +50,7 @@ fun StringBuilder.appendChildren(node: ContentNode) : StringBuilder { return this } -fun StringBuilder.appendNode(node: ContentNode) : StringBuilder { +fun StringBuilder.appendNode(node: ContentNode): StringBuilder { when (node) { is ContentText -> { append(node.text) diff --git a/test/src/model/PropertyTest.kt b/test/src/model/PropertyTest.kt index ed49bd6b..cac62194 100644 --- a/test/src/model/PropertyTest.kt +++ b/test/src/model/PropertyTest.kt @@ -58,18 +58,10 @@ public class PropertyTest { assertEquals(DocumentationNode.Kind.Property, kind) assertEquals(Content.Empty, doc) assertEquals(3, details.count()) - with(details.elementAt(0)) { - assertEquals(DocumentationNode.Kind.Type, kind) - assertEquals("String", name) - } - with(details.elementAt(1)) { - assertEquals(DocumentationNode.Kind.Modifier, kind) - assertEquals("final", name) - } - with(details.elementAt(2)) { - assertEquals(DocumentationNode.Kind.Modifier, kind) - assertEquals("internal", name) - } + assertEquals("String", detail(DocumentationNode.Kind.Type).name) + val modifiers = details(DocumentationNode.Kind.Modifier).map { it.name } + assertTrue("final" in modifiers) + assertTrue("internal" in modifiers) assertTrue(links.none()) assertEquals(2, members.count()) @@ -77,6 +69,9 @@ public class PropertyTest { assertEquals("<get-property>", name) assertEquals(DocumentationNode.Kind.Function, kind) assertEquals(Content.Empty, doc) + val get_modifiers = details(DocumentationNode.Kind.Modifier).map { it.name } + assertTrue("final" in get_modifiers) + assertTrue("internal" in get_modifiers) assertEquals("String", detail(DocumentationNode.Kind.Type).name) assertTrue(links.none()) assertTrue(members.none()) @@ -86,10 +81,11 @@ public class PropertyTest { assertEquals(DocumentationNode.Kind.Function, kind) assertEquals(Content.Empty, doc) assertEquals(4, details.count()) - assertEquals("Unit", details.elementAt(0).name) - assertEquals("final", details.elementAt(1).name) - assertEquals("internal", details.elementAt(2).name) - with(details.elementAt(3)) { + assertEquals("Unit", detail(DocumentationNode.Kind.Type).name) + val set_modifiers = details(DocumentationNode.Kind.Modifier).map { it.name } + assertTrue("final" in set_modifiers) + assertTrue("internal" in set_modifiers) + with(detail(DocumentationNode.Kind.Parameter)) { assertEquals("value", name) assertEquals(DocumentationNode.Kind.Parameter, kind) assertEquals(Content.Empty, doc) |