From 303c937a7c33fa9df5c28079c423ee071e87e410 Mon Sep 17 00:00:00 2001 From: Andrey Tyrin Date: Thu, 12 Jan 2023 20:03:06 +0100 Subject: Default Java constructor (#2795) --- .../psi/DefaultPsiToDocumentableTranslator.kt | 77 ++++++++++++++---- plugins/base/src/test/kotlin/model/JavaTest.kt | 13 ++-- .../signatures/InheritedAccessorsSignatureTest.kt | 48 ++++++++---- .../src/test/kotlin/signatures/SignatureTest.kt | 8 +- .../DefaultPsiToDocumentableTranslatorTest.kt | 90 +++++++++++++++++++++- 5 files changed, 196 insertions(+), 40 deletions(-) (limited to 'plugins/base/src') diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt index 574fb2e8..f67ee15c 100644 --- a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt @@ -16,14 +16,15 @@ import org.jetbrains.dokka.analysis.KotlinAnalysis import org.jetbrains.dokka.analysis.PsiDocumentableSource import org.jetbrains.dokka.analysis.from import org.jetbrains.dokka.base.DokkaBase -import org.jetbrains.dokka.base.translators.psi.parsers.JavaDocumentationParser import org.jetbrains.dokka.base.translators.psi.parsers.JavadocParser import org.jetbrains.dokka.base.translators.typeConstructorsBeingExceptions import org.jetbrains.dokka.base.translators.unquotedValue -import org.jetbrains.dokka.links.* +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.nextTarget +import org.jetbrains.dokka.links.withClass +import org.jetbrains.dokka.links.withEnumEntryExtra import org.jetbrains.dokka.model.* import org.jetbrains.dokka.model.AnnotationTarget -import org.jetbrains.dokka.model.Nullable import org.jetbrains.dokka.model.doc.DocumentationNode import org.jetbrains.dokka.model.doc.Param import org.jetbrains.dokka.model.properties.PropertyContainer @@ -45,7 +46,7 @@ import org.jetbrains.kotlin.utils.addToStdlib.safeAs import java.io.File class DefaultPsiToDocumentableTranslator( - context: DokkaContext + context: DokkaContext, ) : AsyncSourceToDocumentableTranslator { private val kotlinAnalysis: KotlinAnalysis = context.plugin().querySingle { kotlinAnalysis } @@ -94,8 +95,8 @@ class DefaultPsiToDocumentableTranslator( class DokkaPsiParser( private val sourceSetData: DokkaSourceSet, - facade: DokkaResolutionFacade, - private val logger: DokkaLogger + private val facade: DokkaResolutionFacade, + private val logger: DokkaLogger, ) { private val javadocParser = JavadocParser(logger, facade) private val syntheticDocProvider = SyntheticElementDocumentationProvider(javadocParser, facade) @@ -266,7 +267,7 @@ class DefaultPsiToDocumentableTranslator( } parsedFields + parsedSuperFields } - val source = PsiDocumentableSource(this).toSourceSetDependent() + val source = parseSources() val classlikes = async { innerClasses.asIterable().parallelMap { parseClasslike(it, dri) } } val visibility = getVisibility().toSourceSetDependent() val ancestors = (listOfNotNull(ancestry.superclass?.let { @@ -276,10 +277,16 @@ class DefaultPsiToDocumentableTranslator( JavaClassKindTypes.CLASS ) } - }) + ancestry.interfaces.map { TypeConstructorWithKind(it.typeConstructor, JavaClassKindTypes.INTERFACE) }).toSourceSetDependent() + }) + ancestry.interfaces.map { + TypeConstructorWithKind( + it.typeConstructor, + JavaClassKindTypes.INTERFACE + ) + }).toSourceSetDependent() val modifiers = getModifier().toSourceSetDependent() val implementedInterfacesExtra = ImplementedInterfaces(ancestry.allImplementedInterfaces().toSourceSetDependent()) + when { isAnnotationType -> DAnnotation( @@ -293,7 +300,7 @@ class DefaultPsiToDocumentableTranslator( classlikes = classlikes.await(), visibility = visibility, companion = null, - constructors = constructors.map { parseFunction(it, true) }, + constructors = parseConstructors(), generics = mapTypeParameters(dri), sourceSets = setOf(sourceSetData), isExpectActual = false, @@ -303,6 +310,7 @@ class DefaultPsiToDocumentableTranslator( .toAnnotations() ) ) + isEnum -> DEnum( dri = dri, name = name.orEmpty(), @@ -327,11 +335,12 @@ class DefaultPsiToDocumentableTranslator( expectPresentInSet = null, sources = source, functions = allFunctions.await(), - properties = fields.filter { it !is PsiEnumConstant }.map { parseField(it, accessors[it].orEmpty()) }, + properties = fields.filter { it !is PsiEnumConstant } + .map { parseField(it, accessors[it].orEmpty()) }, classlikes = classlikes.await(), visibility = visibility, companion = null, - constructors = constructors.map { parseFunction(it, true) }, + constructors = parseConstructors(), supertypes = ancestors, sourceSets = setOf(sourceSetData), isExpectActual = false, @@ -341,6 +350,7 @@ class DefaultPsiToDocumentableTranslator( .toAnnotations() ) ) + isInterface -> DInterface( dri = dri, name = name.orEmpty(), @@ -362,10 +372,11 @@ class DefaultPsiToDocumentableTranslator( .toAnnotations() ) ) + else -> DClass( dri = dri, name = name.orEmpty(), - constructors = constructors.map { parseFunction(it, true) }, + constructors = parseConstructors(), functions = allFunctions.await(), properties = allFields.await(), classlikes = classlikes.await(), @@ -390,14 +401,38 @@ class DefaultPsiToDocumentableTranslator( } } + private fun PsiClass.parseConstructors(): List { + val constructors = when { + isAnnotationType || isInterface -> emptyArray() + isEnum -> this.constructors + else -> this.constructors.takeIf { it.isNotEmpty() } ?: arrayOf(createDefaultConstructor()) + } + return constructors.map { parseFunction(it, true) } + } + + /** + * PSI doesn't return a default constructor if class doesn't contain an explicit one. + * This method create synthetic constructor + * Visibility modifier is preserved from the class. + */ + private fun PsiClass.createDefaultConstructor(): PsiMethod { + val psiElementFactory = JavaPsiFacade.getElementFactory(facade.project) + val signature = when (val classVisibility = getVisibility()) { + JavaVisibility.Default -> name.orEmpty() + else -> "${classVisibility.name} $name" + } + return psiElementFactory.createConstructor(signature, this) + } + private fun AncestryNode.exceptionInSupertypesOrNull(): ExceptionInSupertypes? = - typeConstructorsBeingExceptions().takeIf { it.isNotEmpty() }?.let { ExceptionInSupertypes(it.toSourceSetDependent()) } + typeConstructorsBeingExceptions().takeIf { it.isNotEmpty() } + ?.let { ExceptionInSupertypes(it.toSourceSetDependent()) } private fun parseFunction( psi: PsiMethod, isConstructor: Boolean = false, inheritedFrom: DRI? = null, - parentDRI: DRI? = null + parentDRI: DRI? = null, ): DFunction { val dri = parentDRI?.let { dri -> DRI.from(psi).copy(packageName = dri.packageName, classNames = dri.classNames) @@ -427,7 +462,7 @@ class DefaultPsiToDocumentableTranslator( }, documentation = docs.toSourceSetDependent(), expectPresentInSet = null, - sources = PsiDocumentableSource(psi).toSourceSetDependent(), + sources = psi.parseSources(), visibility = psi.getVisibility().toSourceSetDependent(), type = psi.returnType?.let { getBound(type = it) } ?: Void, generics = psi.mapTypeParameters(dri), @@ -450,6 +485,16 @@ class DefaultPsiToDocumentableTranslator( ) } + private fun PsiNamedElement.parseSources(): SourceSetDependent { + return when { + // `isPhysical` detects the virtual declarations without real sources. + // Otherwise, `PsiDocumentableSource` initialization will fail: non-physical declarations doesn't have `virtualFile`. + // This check protects from accidentally requesting sources for synthetic / virtual declarations. + isPhysical -> PsiDocumentableSource(this).toSourceSetDependent() + else -> emptyMap() + } + } + private fun PsiMethod.getDocumentation(): DocumentationNode = this.takeIf { it is SyntheticElement }?.let { syntheticDocProvider.getDocumentation(it) } ?: javadocParser.parseDocumentation(this) @@ -657,7 +702,7 @@ class DefaultPsiToDocumentableTranslator( name = psi.name, documentation = javadocParser.parseDocumentation(psi).toSourceSetDependent(), expectPresentInSet = null, - sources = PsiDocumentableSource(psi).toSourceSetDependent(), + sources = psi.parseSources(), visibility = psi.getVisibility(getter).toSourceSetDependent(), type = getBound(psi.type), receiver = null, diff --git a/plugins/base/src/test/kotlin/model/JavaTest.kt b/plugins/base/src/test/kotlin/model/JavaTest.kt index 47a25943..b3ee2726 100644 --- a/plugins/base/src/test/kotlin/model/JavaTest.kt +++ b/plugins/base/src/test/kotlin/model/JavaTest.kt @@ -13,7 +13,6 @@ import utils.AbstractModelTest import utils.assertNotNull import utils.name import kotlin.test.assertEquals -import org.jetbrains.dokka.links.Callable as DRICallable class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") { val configuration = dokkaConfiguration { @@ -48,7 +47,7 @@ class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") { ) { with((this / "java" / "Test").cast()) { name equals "Test" - children counts 1 + children counts 2 // default constructor and function with((this / "fn").cast()) { name equals "fn" val params = parameters.map { it.documentation.values.first().children.first() as Param } @@ -118,7 +117,7 @@ class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") { ) { with((this / "java" / "Test").cast()) { name equals "Test" - children counts 1 + children counts 2 // default constructor and function with((this / "arrayToString").cast()) { name equals "arrayToString" @@ -219,10 +218,10 @@ class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") { """, configuration = configuration ) { with((this / "java" / "InnerClass").cast()) { - children counts 1 + children counts 2 // default constructor and inner class with((this / "D").cast()) { name equals "D" - children counts 0 + children counts 1 // default constructor } } } @@ -239,7 +238,7 @@ class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") { ) { with((this / "java" / "Foo").cast()) { name equals "Foo" - children counts 1 + children counts 2 // default constructor and function with((this / "bar").cast()) { name equals "bar" @@ -263,7 +262,7 @@ class JavaTest : AbstractModelTest("/src/main/kotlin/java/Test.java", "java") { """, configuration = configuration ) { with((this / "java" / "Test").cast()) { - children counts 2 + children counts 3 // default constructor + 2 props with((this / "i").cast()) { getter equals null diff --git a/plugins/base/src/test/kotlin/signatures/InheritedAccessorsSignatureTest.kt b/plugins/base/src/test/kotlin/signatures/InheritedAccessorsSignatureTest.kt index a75c00fc..49a70f1c 100644 --- a/plugins/base/src/test/kotlin/signatures/InheritedAccessorsSignatureTest.kt +++ b/plugins/base/src/test/kotlin/signatures/InheritedAccessorsSignatureTest.kt @@ -62,11 +62,11 @@ class InheritedAccessorsSignatureTest : BaseAbstractTest() { writerPlugin.writer.renderedContent("root/test/-a/index.html").let { javaClassContent -> val signatures = javaClassContent.signature().toList() assertEquals( - 2, signatures.size, - "Expected 2 signatures: class signature and property" + 3, signatures.size, + "Expected 3 signatures: class signature, default constructor and property" ) - val property = signatures[1] + val property = signatures[2] property.match( "open var ", A("a"), ":", A("Int"), ignoreSpanWithTokenStyle = true @@ -109,9 +109,13 @@ class InheritedAccessorsSignatureTest : BaseAbstractTest() { writerPlugin.writer.renderedContent("root/test/-a/index.html").let { javaClassContent -> val signatures = javaClassContent.signature().toList() - assertEquals(2, signatures.size, "Expected 2 signatures: class signature and property") + assertEquals( + 3, + signatures.size, + "Expected 3 signatures: class signature, default constructor and property" + ) - val property = signatures[1] + val property = signatures[2] property.match( "open val ", A("a"), ":", A("Int"), ignoreSpanWithTokenStyle = true @@ -156,9 +160,13 @@ class InheritedAccessorsSignatureTest : BaseAbstractTest() { writerPlugin.writer.renderedContent("root/test/-a/index.html").let { javaClassContent -> val signatures = javaClassContent.signature().toList() - assertEquals(2, signatures.size, "Expected 2 signatures: class signature and setter") + assertEquals( + 3, + signatures.size, + "Expected 3 signatures: class signature, default constructor and setter" + ) - val setterFunction = signatures[1] + val setterFunction = signatures[2] setterFunction.match( "open fun ", A("setA"), "(", Parameters( Parameter("a: ", A("Int")) @@ -241,9 +249,13 @@ class InheritedAccessorsSignatureTest : BaseAbstractTest() { renderingStage = { _, _ -> writerPlugin.writer.renderedContent("root/test/-java-class/index.html").let { kotlinClassContent -> val signatures = kotlinClassContent.signature().toList() - assertEquals(2, signatures.size, "Expected to find two signatures: class and property") + assertEquals( + 3, + signatures.size, + "Expected to find 3 signatures: class, default constructor and property" + ) - val property = signatures[1] + val property = signatures[2] property.match( "open var ", A("variable"), ": ", Span("String"), ignoreSpanWithTokenStyle = true @@ -290,15 +302,19 @@ class InheritedAccessorsSignatureTest : BaseAbstractTest() { // test added to control changes writerPlugin.writer.renderedContent("root/test/-java-class/index.html").let { javaClassContent -> val signatures = javaClassContent.signature().toList() - assertEquals(3, signatures.size, "Expected to find 3 signatures: class and two accessors") + assertEquals( + 4, + signatures.size, + "Expected to find 4 signatures: class, default constructor and two accessors" + ) - val getter = signatures[1] + val getter = signatures[2] getter.match( "fun ", A("getVariable"), "(): ", Span("String"), ignoreSpanWithTokenStyle = true ) - val setter = signatures[2] + val setter = signatures[3] setter.match( "fun ", A("setVariable"), "(", Parameters( Parameter("value: ", Span("String")) @@ -367,9 +383,13 @@ class InheritedAccessorsSignatureTest : BaseAbstractTest() { writerPlugin.writer.renderedContent("root/test/-java-class/index.html").let { javaClassContent -> val signatures = javaClassContent.signature().toList() - assertEquals(2, signatures.size, "Expected 2 signatures: class signature and property") + assertEquals( + 3, + signatures.size, + "Expected 3 signatures: class signature, default constructor and property" + ) - val property = signatures[1] + val property = signatures[2] property.match( "protected open var ", A("protectedGetterAndProtectedSetter"), ":", A("Int"), ignoreSpanWithTokenStyle = true diff --git a/plugins/base/src/test/kotlin/signatures/SignatureTest.kt b/plugins/base/src/test/kotlin/signatures/SignatureTest.kt index 77c92d9c..f017c815 100644 --- a/plugins/base/src/test/kotlin/signatures/SignatureTest.kt +++ b/plugins/base/src/test/kotlin/signatures/SignatureTest.kt @@ -976,9 +976,13 @@ class SignatureTest : BaseAbstractTest() { writerPlugin.writer.renderedContent("root/test/-java-class/index.html").let { kotlinClassContent -> val signatures = kotlinClassContent.signature().toList() - assertEquals(2, signatures.size, "Expected 2 signatures: class signature and property") + assertEquals( + 3, + signatures.size, + "Expected 3 signatures: class signature, default constructor and property" + ) - val property = signatures[1] + val property = signatures[2] property.match( "open var ", A("property"), ":", A("Int"), ignoreSpanWithTokenStyle = true diff --git a/plugins/base/src/test/kotlin/translators/DefaultPsiToDocumentableTranslatorTest.kt b/plugins/base/src/test/kotlin/translators/DefaultPsiToDocumentableTranslatorTest.kt index 711b9c02..537a4bfc 100644 --- a/plugins/base/src/test/kotlin/translators/DefaultPsiToDocumentableTranslatorTest.kt +++ b/plugins/base/src/test/kotlin/translators/DefaultPsiToDocumentableTranslatorTest.kt @@ -8,7 +8,6 @@ import org.jetbrains.dokka.links.PointingToDeclaration import org.jetbrains.dokka.model.* import org.jetbrains.dokka.model.doc.* import org.jetbrains.dokka.plugability.DokkaPlugin -import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import utils.assertNotNull @@ -741,4 +740,93 @@ class DefaultPsiToDocumentableTranslatorTest : BaseAbstractTest() { } } } + + @Test + fun `should have public default constructor in public class`() { + testInline( + """ + |/src/main/java/test/A.java + |package test; + |public class A { + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.findClasslike(packageName = "test", "A") as DClass + + assertEquals(1, testedClass.constructors.size, "Expect 1 default constructor") + assertTrue( + testedClass.constructors.first().parameters.isEmpty(), + "Expect default constructor doesn't have params" + ) + assertEquals(JavaVisibility.Public, testedClass.constructors.first().visibility()) + } + } + } + + @Test + fun `should have package-private default constructor in package-private class`() { + testInline( + """ + |/src/main/java/test/A.java + |package test; + |class A { + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.findClasslike(packageName = "test", "A") as DClass + + assertEquals(1, testedClass.constructors.size, "Expect 1 default constructor") + assertEquals(JavaVisibility.Default, testedClass.constructors.first().visibility()) + } + } + } + + @Test + fun `should have private default constructor in private nested class`() { + testInline( + """ + |/src/main/java/test/A.java + |package test; + |class A { + | private static class PrivateNested{} + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val parentClass = module.findClasslike(packageName = "test", "A") as DClass + val testedClass = parentClass.classlikes.single { it.name == "PrivateNested" } as DClass + + assertEquals(1, testedClass.constructors.size, "Expect 1 default constructor") + assertEquals(JavaVisibility.Private, testedClass.constructors.first().visibility()) + } + } + } + + @Test + fun `should not have a default constructor because have explicit private`() { + testInline( + """ + |/src/main/java/test/A.java + |package test; + |public class A { + | private A(){} + |} + """.trimIndent(), + configuration + ) { + documentablesMergingStage = { module -> + val testedClass = module.findClasslike(packageName = "test", "A") as DClass + + assertEquals(1, testedClass.constructors.size, "Expect 1 declared constructor") + assertEquals(JavaVisibility.Private, testedClass.constructors.first().visibility()) + } + } + } } + +private fun DFunction.visibility() = visibility.values.first() -- cgit