diff options
8 files changed, 353 insertions, 2 deletions
diff --git a/core/src/main/kotlin/DokkaGenerator.kt b/core/src/main/kotlin/DokkaGenerator.kt index d598c773..72161322 100644 --- a/core/src/main/kotlin/DokkaGenerator.kt +++ b/core/src/main/kotlin/DokkaGenerator.kt @@ -164,7 +164,7 @@ class DokkaGenerator( } - private class DokkaMessageCollector(private val logger: DokkaLogger) : MessageCollector { + class DokkaMessageCollector(private val logger: DokkaLogger) : MessageCollector { override fun clear() { seenErrors = false } diff --git a/plugins/base/src/main/kotlin/DokkaBase.kt b/plugins/base/src/main/kotlin/DokkaBase.kt index 1376cd8e..2be784ba 100644 --- a/plugins/base/src/main/kotlin/DokkaBase.kt +++ b/plugins/base/src/main/kotlin/DokkaBase.kt @@ -22,6 +22,8 @@ import org.jetbrains.dokka.base.transformers.pages.merger.FallbackPageMergerStra import org.jetbrains.dokka.base.transformers.pages.merger.PageMerger import org.jetbrains.dokka.base.transformers.pages.merger.PageMergerStrategy import org.jetbrains.dokka.base.transformers.pages.merger.SameMethodNamePageMergerStrategy +import org.jetbrains.dokka.base.transformers.pages.samples.DefaultSamplesTransformer +import org.jetbrains.dokka.base.transformers.pages.samples.SamplesTransformer import org.jetbrains.dokka.base.transformers.pages.sourcelinks.SourceLinksTransformer import org.jetbrains.dokka.base.translators.descriptors.DefaultDescriptorToDocumentableTranslator import org.jetbrains.dokka.base.translators.documentables.DefaultDocumentableToPageTranslator @@ -37,6 +39,7 @@ class DokkaBase : DokkaPlugin() { val externalLocationProviderFactory by extensionPoint<ExternalLocationProviderFactory>() val outputWriter by extensionPoint<OutputWriter>() val htmlPreprocessors by extensionPoint<PageTransformer>() + val samplesTransformer by extensionPoint<SamplesTransformer>() val descriptorToDocumentableTranslator by extending(isFallback = true) { CoreExtensions.descriptorToDocumentableTranslator providing ::DefaultDescriptorToDocumentableTranslator @@ -128,6 +131,10 @@ class DokkaBase : DokkaPlugin() { htmlPreprocessors with RootCreator } + val defaultSamplesTransformer by extending(isFallback = true) { + samplesTransformer providing ::DefaultSamplesTransformer + } + val sourceLinksTransformer by extending { htmlPreprocessors providing ::SourceLinksTransformer order { after(rootCreator) } } diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt index c2960694..1365fecb 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt @@ -10,6 +10,7 @@ import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.plugability.plugin import org.jetbrains.dokka.plugability.query +import org.jetbrains.dokka.plugability.querySingle import java.io.File open class HtmlRenderer( @@ -18,7 +19,8 @@ open class HtmlRenderer( private val pageList = mutableListOf<String>() - override val preprocessors = context.plugin<DokkaBase>().query { htmlPreprocessors } + override val preprocessors = context.plugin<DokkaBase>().query { htmlPreprocessors } + + context.plugin<DokkaBase>().querySingle { samplesTransformer } override fun FlowContent.wrapGroup( node: ContentGroup, diff --git a/plugins/base/src/main/kotlin/transformers/pages/samples/DefaultSamplesTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/samples/DefaultSamplesTransformer.kt new file mode 100644 index 00000000..a391b534 --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/samples/DefaultSamplesTransformer.kt @@ -0,0 +1,38 @@ +package org.jetbrains.dokka.base.transformers.pages.samples + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.kotlin.idea.kdoc.resolveKDocSampleLink +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtDeclarationWithBody +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + +class DefaultSamplesTransformer(context: DokkaContext) : SamplesTransformer(context) { + + override fun processBody(psiElement: PsiElement): String { + val text = processSampleBody(psiElement).trim { it == '\n' || it == '\r' }.trimEnd() + val lines = text.split("\n") + val indent = lines.filter(String::isNotBlank).map { it.takeWhile(Char::isWhitespace).count() }.min() ?: 0 + return lines.joinToString("\n") { it.drop(indent) } + } + + private fun processSampleBody(psiElement: PsiElement): String = when (psiElement) { + is KtDeclarationWithBody -> { + val bodyExpression = psiElement.bodyExpression + when (bodyExpression) { + is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") + else -> bodyExpression!!.text + } + } + else -> psiElement.text + } + + override fun processImports(psiElement: PsiElement): String { + val psiFile = psiElement.containingFile + return when(val text = psiFile.safeAs<KtFile>()?.importList?.text) { + is String -> text + else -> "" + } + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/pages/samples/KotlinWebsiteSamplesTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/samples/KotlinWebsiteSamplesTransformer.kt new file mode 100644 index 00000000..c099644f --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/samples/KotlinWebsiteSamplesTransformer.kt @@ -0,0 +1,196 @@ +package org.jetbrains.dokka.base.transformers.pages.samples + +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.impl.source.tree.LeafPsiElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.allChildren +import org.jetbrains.kotlin.psi.psiUtil.prevLeaf +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.resolve.ImportPath +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import java.io.PrintWriter +import java.io.StringWriter + +// TODO Inspect below class for any bugs. Big chunk of was ripped from 0.10.1 +class KotlinWebsiteSamplesTransformer(context: DokkaContext): SamplesTransformer(context) { + + private class SampleBuilder : KtTreeVisitorVoid() { + val builder = StringBuilder() + val text: String + get() = builder.toString() + + val errors = mutableListOf<ConvertError>() + + data class ConvertError(val e: Exception, val text: String, val loc: String) + + fun KtValueArgument.extractStringArgumentValue() = + (getArgumentExpression() as KtStringTemplateExpression) + .entries.joinToString("") { it.text } + + + fun convertAssertPrints(expression: KtCallExpression) { + val (argument, commentArgument) = expression.valueArguments + builder.apply { + append("println(") + append(argument.text) + append(") // ") + append(commentArgument.extractStringArgumentValue()) + } + } + + fun convertAssertTrueFalse(expression: KtCallExpression, expectedResult: Boolean) { + val (argument) = expression.valueArguments + builder.apply { + expression.valueArguments.getOrNull(1)?.let { + append("// ${it.extractStringArgumentValue()}") + val ws = expression.prevLeaf { it is PsiWhiteSpace } + append(ws?.text ?: "\n") + } + append("println(\"") + append(argument.text) + append(" is \${") + append(argument.text) + append("}\") // $expectedResult") + } + } + + fun convertAssertFails(expression: KtCallExpression) { + val valueArguments = expression.valueArguments + + val funcArgument: KtValueArgument + val message: KtValueArgument? + + if (valueArguments.size == 1) { + message = null + funcArgument = valueArguments.first() + } else { + message = valueArguments.first() + funcArgument = valueArguments.last() + } + + builder.apply { + val argument = funcArgument.extractFunctionalArgumentText() + append(argument.lines().joinToString(separator = "\n") { "// $it" }) + append(" // ") + if (message != null) { + append(message.extractStringArgumentValue()) + } + append(" will fail") + } + } + + private fun KtValueArgument.extractFunctionalArgumentText(): String { + return if (getArgumentExpression() is KtLambdaExpression) + PsiTreeUtil.findChildOfType(this, KtBlockExpression::class.java)?.text ?: "" + else + text + } + + fun convertAssertFailsWith(expression: KtCallExpression) { + val (funcArgument) = expression.valueArguments + val (exceptionType) = expression.typeArguments + builder.apply { + val argument = funcArgument.extractFunctionalArgumentText() + append(argument.lines().joinToString(separator = "\n") { "// $it" }) + append(" // will fail with ") + append(exceptionType.text) + } + } + + override fun visitCallExpression(expression: KtCallExpression) { + when (expression.calleeExpression?.text) { + "assertPrints" -> convertAssertPrints(expression) + "assertTrue" -> convertAssertTrueFalse(expression, expectedResult = true) + "assertFalse" -> convertAssertTrueFalse(expression, expectedResult = false) + "assertFails" -> convertAssertFails(expression) + "assertFailsWith" -> convertAssertFailsWith(expression) + else -> super.visitCallExpression(expression) + } + } + + private fun reportProblemConvertingElement(element: PsiElement, e: Exception) { + val text = element.text + val document = PsiDocumentManager.getInstance(element.project).getDocument(element.containingFile) + + val lineInfo = if (document != null) { + val lineNumber = document.getLineNumber(element.startOffset) + "$lineNumber, ${element.startOffset - document.getLineStartOffset(lineNumber)}" + } else { + "offset: ${element.startOffset}" + } + errors += ConvertError(e, text, lineInfo) + } + + override fun visitElement(element: PsiElement) { + if (element is LeafPsiElement) + builder.append(element.text) + + element.acceptChildren(object : PsiElementVisitor() { + override fun visitElement(element: PsiElement) { + try { + element.accept(this@SampleBuilder) + } catch (e: Exception) { + try { + reportProblemConvertingElement(element, e) + } finally { + builder.append(element.text) //recover + } + } + } + }) + } + + } + + private fun PsiElement.buildSampleText(): String { + val sampleBuilder = SampleBuilder() + this.accept(sampleBuilder) + + sampleBuilder.errors.forEach { + val sw = StringWriter() + val pw = PrintWriter(sw) + it.e.printStackTrace(pw) + + this@KotlinWebsiteSamplesTransformer.context.logger.error("${containingFile.name}: (${it.loc}): Exception thrown while converting \n```\n${it.text}\n```\n$sw") + } + return sampleBuilder.text + } + + val importsToIgnore = arrayOf("samples.*", "samples.Sample").map { ImportPath.fromString(it) } + + override fun processImports(psiElement: PsiElement): String { + val psiFile = psiElement.containingFile + return when(val text = psiFile.safeAs<KtFile>()?.importList) { + is KtImportList -> text.let { + it.allChildren.filter { + it !is KtImportDirective || it.importPath !in importsToIgnore + }.joinToString(separator = "\n") { it.text } + } + else -> "" + } + } + + override fun processBody(psiElement: PsiElement): String { + val text = processSampleBody(psiElement).trim { it == '\n' || it == '\r' }.trimEnd() + val lines = text.split("\n") + val indent = lines.filter(String::isNotBlank).map { it.takeWhile(Char::isWhitespace).count() }.min() ?: 0 + return lines.joinToString("\n") { it.drop(indent) } + } + + private fun processSampleBody(psiElement: PsiElement) = when (psiElement) { + is KtDeclarationWithBody -> { + val bodyExpression = psiElement.bodyExpression + val bodyExpressionText = bodyExpression!!.buildSampleText() + when (bodyExpression) { + is KtBlockExpression -> bodyExpressionText.removeSurrounding("{", "}") + else -> bodyExpressionText + } + } + else -> psiElement.buildSampleText() + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/pages/samples/SamplesTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/samples/SamplesTransformer.kt new file mode 100644 index 00000000..a3a6b99c --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/pages/samples/SamplesTransformer.kt @@ -0,0 +1,102 @@ +package org.jetbrains.dokka.base.transformers.pages.samples + +import com.intellij.psi.PsiElement +import org.jetbrains.dokka.DokkaGenerator +import org.jetbrains.dokka.EnvironmentAndFacade +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.AnalysisEnvironment +import org.jetbrains.dokka.analysis.DokkaResolutionFacade +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.doc.Sample +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.pages.PageTransformer +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.utils.PathUtil +import org.jetbrains.kotlin.idea.kdoc.resolveKDocSampleLink +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import java.io.File + +abstract class SamplesTransformer(val context: DokkaContext) : PageTransformer { + + abstract fun processBody(psiElement: PsiElement): String + abstract fun processImports(psiElement: PsiElement): String + + override fun invoke(input: RootPageNode): RootPageNode { + + val analysis = setUpAnalysis(context) + + return input.transformContentPagesTree { page -> + page.documentable?.documentation?.map?.entries?.fold(page) { acc, entry -> + entry.value.children.filterIsInstance<Sample>().fold(acc) { acc, sample -> + acc.modified(content = acc.content.addSample(page, entry.key, sample.name, analysis)) + } + } ?: page + } + } + + private fun setUpAnalysis(context: DokkaContext) = context.configuration.passesConfigurations.map { + it.platformData to AnalysisEnvironment(DokkaGenerator.DokkaMessageCollector(context.logger), it.analysisPlatform).run { + if (analysisPlatform == Platform.jvm) { + addClasspath(PathUtil.getJdkClassesRootsFromCurrentJre()) + } + it.classpath.forEach { addClasspath(File(it)) } + + addSources(it.samples.map { it }) + + loadLanguageVersionSettings(it.languageVersion, it.apiVersion) + + val environment = createCoreEnvironment() + val (facade, _) = createResolutionFacade(environment) + EnvironmentAndFacade(environment, facade) + } + }.toMap() + + private fun ContentNode.addSample(contentPage: ContentPage, platform: PlatformData, fqName: String, analysis: Map<PlatformData, EnvironmentAndFacade>): ContentNode { + val facade = analysis[platform]?.facade ?: + return this.also { context.logger.warn("Cannot resolve facade for platform ${platform.name}")} + val psiElement = fqNameToPsiElement(facade, fqName) ?: + return this.also { context.logger.warn("Cannot find PsiElement corresponding to $fqName") } + val imports = processImports(psiElement) // TODO: Process somehow imports. Maybe just attach them at the top of each body + val body = processBody(psiElement) + val node = platformHintedContentCode(platform, contentPage.dri, body, "kotlin") + return this.safeAs<ContentGroup>()?.run { copy( + children = children.indexOfFirst { contentNode -> + contentNode.safeAs<ContentHeader>()?.children?.firstOrNull()?.safeAs<ContentText>()?.text == "Sample" + }.takeIf { it != -1 }?.let { children.apply { this.safeAs<MutableList<ContentNode>>()?.add(it+1, node) } } ?: children.also { context.logger.warn("Not found Sample block in ${contentPage.dri}")} + ) } ?: this.also { context.logger.warn("ContentPage ${contentPage.dri} cannot be cast to ContentGroup") } + } + + private fun fqNameToPsiElement(resolutionFacade: DokkaResolutionFacade, functionName: String): PsiElement? { + val packageName = functionName.takeWhile { it != '.' } + val descriptor = resolutionFacade.resolveSession.getPackageFragment(FqName(packageName)) ?: + return null.also { context.logger.warn("Cannot find descriptor for package $packageName") } + val symbol = resolveKDocSampleLink(BindingContext.EMPTY, resolutionFacade, descriptor, functionName.split(".")).firstOrNull() ?: + return null.also { context.logger.warn("Unresolved function $functionName in @sample") } + return DescriptorToSourceUtils.descriptorToDeclaration(symbol) + } + + private fun platformHintedContentCode(platformData: PlatformData, dri: Set<DRI>, content: String, language: String) = + PlatformHintedContent( + inner = ContentCode( + children = listOf( + ContentText( + text = content, + dci = DCI(dri, ContentKind.BriefComment), + platforms = setOf(platformData), + style = emptySet(), + extra = PropertyContainer.empty() + ) + ), + language = language, + extra = PropertyContainer.empty(), + dci = DCI(dri, ContentKind.Source), + platforms = setOf(platformData), + style = emptySet() + ), + platforms = setOf(platformData) + ) +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt b/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt index b19f83d3..7a93f0b9 100644 --- a/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt +++ b/plugins/base/src/main/kotlin/transformers/pages/sourcelinks/SourceLinksTransformer.kt @@ -2,6 +2,7 @@ package org.jetbrains.dokka.base.transformers.pages.sourcelinks import com.intellij.psi.PsiElement import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.util.PsiTreeUtil import org.jetbrains.dokka.DokkaConfiguration import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.DescriptorDocumentableSource @@ -11,6 +12,8 @@ import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.transformers.pages.PageTransformer import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithSource +import org.jetbrains.kotlin.kdoc.psi.api.KDoc +import org.jetbrains.kotlin.psi.KtDeclaration import org.jetbrains.kotlin.resolve.source.getPsi import org.jetbrains.kotlin.utils.addToStdlib.cast import org.jetbrains.kotlin.utils.addToStdlib.safeAs diff --git a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt index 8942991e..87f47ac8 100644 --- a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt +++ b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt @@ -7,6 +7,9 @@ import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder.Doc import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.* import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.DFunction +import org.jetbrains.dokka.model.doc.Property +import org.jetbrains.dokka.model.doc.TagWrapper import org.jetbrains.dokka.model.properties.WithExtraProperties import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.utilities.DokkaLogger |