blob: 59a3cb951f2b0b583dffa9acb47c7e8b1ea2486b [file] [log] [blame]
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea
import com.google.common.html.HtmlEscapers
import com.intellij.codeInsight.documentation.DocumentationManagerUtil
import com.intellij.codeInsight.javadoc.JavaDocInfoGeneratorFactory
import com.intellij.lang.documentation.AbstractDocumentationProvider
import com.intellij.lang.documentation.DocumentationMarkup.*
import com.intellij.lang.java.JavaDocumentationProvider
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.IndexNotReadyException
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.asJava.LightClassUtil
import org.jetbrains.kotlin.asJava.elements.KtLightDeclaration
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.idea.DocumentationURLs.LATE_INITIALIZED_PROPERTIES_AND_VARIABLES_URL
import org.jetbrains.kotlin.idea.DocumentationURLs.TAIL_RECURSIVE_FUNCTIONS_URL
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
import org.jetbrains.kotlin.idea.core.completion.DeclarationLookupObject
import org.jetbrains.kotlin.idea.decompiler.navigation.SourceNavigationHelper
import org.jetbrains.kotlin.idea.kdoc.*
import org.jetbrains.kotlin.idea.kdoc.KDocRenderer.appendKDocContent
import org.jetbrains.kotlin.idea.kdoc.KDocRenderer.appendKDocSection
import org.jetbrains.kotlin.idea.kdoc.KDocTemplate.DescriptionBodyTemplate
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.resolve.frontendService
import org.jetbrains.kotlin.idea.util.isRunningInCidrIde
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
import org.jetbrains.kotlin.kdoc.psi.api.KDoc
import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.renderer.AnnotationArgumentsRenderingPolicy
import org.jetbrains.kotlin.renderer.ClassifierNamePolicy
import org.jetbrains.kotlin.renderer.DescriptorRenderer
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.deprecation.DeprecationResolver
import org.jetbrains.kotlin.resolve.deprecation.deprecatedByAnnotationReplaceWithExpression
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.utils.addToStdlib.constant
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
private object DocumentationURLs {
const val LATE_INITIALIZED_PROPERTIES_AND_VARIABLES_URL =
"https://kotlinlang.org/docs/reference/properties.html#late-initialized-properties-and-variables"
const val TAIL_RECURSIVE_FUNCTIONS_URL =
"https://kotlinlang.org/docs/reference/functions.html#tail-recursive-functions"
}
class HtmlClassifierNamePolicy(val base: ClassifierNamePolicy) : ClassifierNamePolicy {
override fun renderClassifier(classifier: ClassifierDescriptor, renderer: DescriptorRenderer): String {
if (DescriptorUtils.isAnonymousObject(classifier)) {
val supertypes = classifier.typeConstructor.supertypes
return buildString {
append("<anonymous object")
if (supertypes.isNotEmpty()) {
append(" : ")
supertypes.joinTo(this) {
val ref = it.constructor.declarationDescriptor
if (ref != null)
renderClassifier(ref, renderer)
else
"<ERROR CLASS>"
}
}
append(">")
}
}
val name = base.renderClassifier(classifier, renderer)
if (classifier.isBoringBuiltinClass())
return name
return buildString {
val ref = classifier.fqNameUnsafe.toString()
DocumentationManagerUtil.createHyperlink(this, ref, name, true)
}
}
}
class WrapValueParameterHandler(val base: DescriptorRenderer.ValueParametersHandler) : DescriptorRenderer.ValueParametersHandler {
override fun appendBeforeValueParameters(parameterCount: Int, builder: StringBuilder) {
base.appendBeforeValueParameters(parameterCount, builder)
}
override fun appendBeforeValueParameter(
parameter: ValueParameterDescriptor,
parameterIndex: Int,
parameterCount: Int,
builder: StringBuilder
) {
builder.append("\n ")
base.appendBeforeValueParameter(parameter, parameterIndex, parameterCount, builder)
}
override fun appendAfterValueParameter(
parameter: ValueParameterDescriptor,
parameterIndex: Int,
parameterCount: Int,
builder: StringBuilder
) {
if (parameterIndex != parameterCount - 1) {
builder.append(",")
}
}
override fun appendAfterValueParameters(parameterCount: Int, builder: StringBuilder) {
if (parameterCount > 0) {
builder.appendln()
}
base.appendAfterValueParameters(parameterCount, builder)
}
}
class KotlinQuickDocumentationProvider : AbstractDocumentationProvider() {
override fun getCustomDocumentationElement(editor: Editor, fil: PsiFile, contextElement: PsiElement?): PsiElement? {
return if (contextElement.isModifier()) contextElement else null
}
override fun getQuickNavigateInfo(element: PsiElement?, originalElement: PsiElement?): String? {
return if (element == null) null else getText(element, originalElement, true)
}
override fun generateDoc(element: PsiElement, originalElement: PsiElement?): String? {
return getText(element, originalElement, false)
}
override fun getDocumentationElementForLink(psiManager: PsiManager, link: String, context: PsiElement?): PsiElement? {
val navElement = context?.navigationElement as? KtElement ?: return null
val bindingContext = navElement.analyze(BodyResolveMode.PARTIAL)
val contextDescriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, navElement] ?: return null
val descriptors = resolveKDocLink(
bindingContext, navElement.getResolutionFacade(),
contextDescriptor, null, link.split('.')
)
val target = descriptors.firstOrNull() ?: return null
return DescriptorToSourceUtilsIde.getAnyDeclaration(psiManager.project, target)
}
override fun getDocumentationElementForLookupItem(psiManager: PsiManager, `object`: Any?, element: PsiElement?): PsiElement? {
if (`object` is DeclarationLookupObject) {
`object`.psiElement?.let { return it }
`object`.descriptor?.let { descriptor ->
return DescriptorToSourceUtilsIde.getAnyDeclaration(psiManager.project, descriptor)
}
}
return null
}
companion object {
private val LOG = Logger.getInstance(KotlinQuickDocumentationProvider::class.java)
private val DESCRIPTOR_RENDERER = DescriptorRenderer.HTML.withOptions {
classifierNamePolicy = HtmlClassifierNamePolicy(ClassifierNamePolicy.SHORT)
valueParametersHandler = WrapValueParameterHandler(valueParametersHandler)
annotationArgumentsRenderingPolicy = AnnotationArgumentsRenderingPolicy.UNLESS_EMPTY
renderCompanionObjectName = true
withDefinedIn = false
eachAnnotationOnNewLine = true
boldOnlyForNamesInHtml = true
}
private fun renderEnumSpecialFunction(element: KtClass, functionDescriptor: FunctionDescriptor, quickNavigation: Boolean): String {
val kdoc = run {
val declarationDescriptor = element.resolveToDescriptorIfAny()
val enumDescriptor = declarationDescriptor?.getSuperClassNotAny() ?: return@run null
val enumDeclaration =
DescriptorToSourceUtilsIde.getAnyDeclaration(element.project, enumDescriptor) as? KtDeclaration ?: return@run null
val enumSource = SourceNavigationHelper.getNavigationElement(enumDeclaration)
val functionName = functionDescriptor.fqNameSafe.shortName().asString()
return@run enumSource.findDescendantOfType<KDoc> {
it.getChildrenOfType<KDocSection>().any { it.findTagByName(functionName) != null }
}
}
val section = kdoc?.getDefaultSection()
return buildString {
insert(KDocTemplate()) {
definition {
renderDefinition(functionDescriptor, DESCRIPTOR_RENDERER)
}
if (!quickNavigation && section != null) {
description {
insert(DescriptionBodyTemplate.Kotlin()) {
content {
appendKDocContent(section)
}
sections {
appendKDocSection(section)
}
}
}
}
}
}
}
private fun renderEnum(element: KtClass, originalElement: PsiElement?, quickNavigation: Boolean): String? {
val referenceExpression = originalElement?.getNonStrictParentOfType<KtReferenceExpression>()
if (referenceExpression != null) {
// When caret on special enum function (e.g SomeEnum.values<caret>())
// element is not an KtReferenceExpression, but KtClass of enum
// so reference extracted from originalElement
val context = referenceExpression.analyze(BodyResolveMode.PARTIAL)
(context[BindingContext.REFERENCE_TARGET, referenceExpression]
?: context[BindingContext.REFERENCE_TARGET, referenceExpression.getChildOfType<KtReferenceExpression>()])?.let {
if (it is FunctionDescriptor) // To protect from Some<caret>Enum.values()
return renderEnumSpecialFunction(element, it, quickNavigation)
}
}
return renderKotlinDeclaration(element, quickNavigation)
}
private fun getText(element: PsiElement, originalElement: PsiElement?, quickNavigation: Boolean): String? {
// INRE is already fixed in 191 in CtrlMouseHandler.updateOnPsiChanges(), so after abandoning 183 branch try-catch can be removed.
// BUNCH: 183
return try {
getTextImpl(element, originalElement, quickNavigation)
} catch (_: IndexNotReadyException) {
DumbService.getInstance(element.project)
.showDumbModeNotification("Element information is not available during index update")
null
}
}
private fun getTextImpl(element: PsiElement, originalElement: PsiElement?, quickNavigation: Boolean): String? {
if (element is PsiWhiteSpace) {
val itElement = findElementWithText(originalElement, "it")
val itReference = itElement?.getParentOfType<KtNameReferenceExpression>(false)
if (itReference != null) {
return getTextImpl(itReference, originalElement, quickNavigation)
}
}
if (element is KtTypeReference) {
val declaration = element.parent
if (declaration is KtCallableDeclaration && declaration.receiverTypeReference == element) {
val thisElement = findElementWithText(originalElement, "this")
if (thisElement != null) {
return getTextImpl(declaration, originalElement, quickNavigation)
}
}
}
if (element is KtClass && element.isEnum()) {
// When caret on special enum function (e.g SomeEnum.values<caret>())
// element is not an KtReferenceExpression, but KtClass of enum
return renderEnum(element, originalElement, quickNavigation)
} else if (element is KtEnumEntry && !quickNavigation) {
val ordinal = element.containingClassOrObject?.getBody()?.run { getChildrenOfType<KtEnumEntry>().indexOf(element) }
return buildString {
insert(buildKotlinDeclaration(element, quickNavigation)) {
definition {
it.inherit()
ordinal?.let {
append("<br>Enum constant ordinal: $ordinal")
}
}
}
}
} else if (element is KtDeclaration) {
return renderKotlinDeclaration(element, quickNavigation)
} else if (element is KtNameReferenceExpression && element.getReferencedName() == "it") {
return renderKotlinImplicitLambdaParameter(element, quickNavigation)
} else if (element is KtLightDeclaration<*, *>) {
val origin = element.kotlinOrigin ?: return null
return renderKotlinDeclaration(origin, quickNavigation)
} else if (element.isModifier()) {
when (element.text) {
KtTokens.LATEINIT_KEYWORD.value -> {
return "lateinit allows initializing a ${a(
LATE_INITIALIZED_PROPERTIES_AND_VARIABLES_URL,
"non-null property outside of a constructor"
)}"
}
KtTokens.TAILREC_KEYWORD.value -> {
return "tailrec marks a function as ${a(TAIL_RECURSIVE_FUNCTIONS_URL, "tail-recursive")} " +
"(allowing the compiler to replace recursion with iteration)"
}
}
}
if (quickNavigation) {
val referenceExpression = originalElement?.getNonStrictParentOfType<KtReferenceExpression>()
if (referenceExpression != null) {
val context = referenceExpression.analyze(BodyResolveMode.PARTIAL)
val declarationDescriptor = context[BindingContext.REFERENCE_TARGET, referenceExpression]
if (declarationDescriptor != null) {
return mixKotlinToJava(declarationDescriptor, element, originalElement)
}
}
} else {
// This element was resolved to non-kotlin element, it will be rendered with own provider
}
return null
}
private fun renderKotlinDeclaration(declaration: KtExpression, quickNavigation: Boolean) = buildString {
insert(buildKotlinDeclaration(declaration, quickNavigation)) {}
}
private fun buildKotlinDeclaration(declaration: KtExpression, quickNavigation: Boolean): KDocTemplate {
val context = declaration.analyze(BodyResolveMode.PARTIAL)
val declarationDescriptor = context[BindingContext.DECLARATION_TO_DESCRIPTOR, declaration]
if (declarationDescriptor == null) {
LOG.info("Failed to find descriptor for declaration " + declaration.getElementTextWithContext())
return KDocTemplate.NoDocTemplate().apply {
error {
append("No documentation available")
}
}
}
return buildKotlin(context, declarationDescriptor, quickNavigation, declaration)
}
private fun renderKotlinImplicitLambdaParameter(element: KtReferenceExpression, quickNavigation: Boolean): String? {
val context = element.analyze(BodyResolveMode.PARTIAL)
val target = element.mainReference.resolveToDescriptors(context).singleOrNull() as? ValueParameterDescriptor? ?: return null
return renderKotlin(context, target, quickNavigation, element)
}
private fun renderKotlin(
context: BindingContext,
declarationDescriptor: DeclarationDescriptor,
quickNavigation: Boolean,
ktElement: KtElement
) = buildString {
insert(buildKotlin(context, declarationDescriptor, quickNavigation, ktElement)) {}
}
private fun buildKotlin(
context: BindingContext,
declarationDescriptor: DeclarationDescriptor,
quickNavigation: Boolean,
ktElement: KtElement
): KDocTemplate {
@Suppress("NAME_SHADOWING")
var declarationDescriptor = declarationDescriptor
if (declarationDescriptor is ValueParameterDescriptor) {
val property = context[BindingContext.VALUE_PARAMETER_AS_PROPERTY, declarationDescriptor]
if (property != null) {
declarationDescriptor = property
}
}
val deprecationProvider = ktElement.getResolutionFacade().frontendService<DeprecationResolver>()
return KDocTemplate().apply {
definition {
renderDefinition(declarationDescriptor, DESCRIPTOR_RENDERER)
}
insertDeprecationInfo(declarationDescriptor, deprecationProvider)
if (!quickNavigation) {
description {
val comment = declarationDescriptor.findKDoc { DescriptorToSourceUtilsIde.getAnyDeclaration(ktElement.project, it) }
if (comment != null) {
insert(DescriptionBodyTemplate.Kotlin()) {
content {
appendKDocContent(comment)
}
sections {
if (comment is KDocSection) appendKDocSection(comment)
}
}
} else if (declarationDescriptor is CallableDescriptor) { // If we couldn't find KDoc, try to find javadoc in one of super's
insert(DescriptionBodyTemplate.FromJava()) {
body = extractJavaDescription(declarationDescriptor)
}
}
}
}
}
}
private fun StringBuilder.renderDefinition(descriptor: DeclarationDescriptor, renderer: DescriptorRenderer) {
if (!DescriptorUtils.isLocal(descriptor)) {
val containingDeclaration = descriptor.containingDeclaration
if (containingDeclaration != null) {
val fqName = containingDeclaration.fqNameSafe
if (!fqName.isRoot) {
DocumentationManagerUtil.createHyperlink(this, fqName.asString(), fqName.asString(), false)
}
val fileName =
descriptor
.safeAs<DeclarationDescriptorWithSource>()
?.source
?.containingFile
?.name
?.takeIf { containingDeclaration is PackageFragmentDescriptor }
if (fileName != null) {
if (!fqName.isRoot) {
append(" ")
}
wrap("<font color=\"808080\"><i>", "</i></font>") {
append(fileName)
}
}
if (fileName != null || !fqName.isRoot) {
append("<br>")
}
}
}
append(renderer.render(descriptor))
}
private fun extractJavaDescription(declarationDescriptor: DeclarationDescriptor): String {
val psi = declarationDescriptor.findPsi() as? KtFunction ?: return ""
val lightElement =
LightClassUtil.getLightClassMethod(psi) // Light method for super's scan in javadoc info gen
val javaDocInfoGenerator = JavaDocInfoGeneratorFactory.create(psi.project, lightElement)
val builder = StringBuilder()
if (javaDocInfoGenerator.generateDocInfoCore(builder, false)) {
val renderedJava = builder.toString()
return renderedJava.removeRange(
renderedJava.indexOf(DEFINITION_START),
renderedJava.indexOf(DEFINITION_END)
) // Cut off light method signature
}
return ""
}
private fun KDocTemplate.insertDeprecationInfo(
declarationDescriptor: DeclarationDescriptor,
deprecationResolver: DeprecationResolver
) {
val deprecationInfo = deprecationResolver.getDeprecations(declarationDescriptor).firstOrNull() ?: return
deprecation {
deprecationInfo.message?.let { message ->
append(SECTION_HEADER_START)
append("Deprecated:")
append(SECTION_SEPARATOR)
append(message.htmlEscape())
append(SECTION_END)
}
deprecationInfo.deprecatedByAnnotationReplaceWithExpression()?.let { replaceWith ->
append(SECTION_HEADER_START)
append("Replace with:")
append(SECTION_SEPARATOR)
wrapTag("code") { append(replaceWith.htmlEscape()) }
append(SECTION_END)
}
}
}
private fun String.htmlEscape(): String = HtmlEscapers.htmlEscaper().escape(this)
private inline fun StringBuilder.wrap(prefix: String, postfix: String, crossinline body: () -> Unit) {
this.append(prefix)
body()
this.append(postfix)
}
private inline fun StringBuilder.wrapTag(tag: String, crossinline body: () -> Unit) {
wrap("<$tag>", "</$tag>", body)
}
private fun a(url: String, text: String): String {
return """<a href="$url">$text</a>"""
}
private fun mixKotlinToJava(
declarationDescriptor: DeclarationDescriptor,
element: PsiElement,
originalElement: PsiElement?
): String? {
if (isRunningInCidrIde) return null // no Java support in CIDR
val originalInfo = JavaDocumentationProvider().getQuickNavigateInfo(element, originalElement)
if (originalInfo != null) {
val renderedDecl = constant { DESCRIPTOR_RENDERER.withOptions { withDefinedIn = false } }.render(declarationDescriptor)
return "$renderedDecl<br/>Java declaration:<br/>$originalInfo"
}
return null
}
private fun findElementWithText(element: PsiElement?, text: String): PsiElement? {
return when {
element == null -> null
element.text == text -> element
element.prevLeaf()?.text == text -> element.prevLeaf()
else -> null
}
}
private fun PsiElement?.isModifier() =
this != null && parent is KtModifierList && KtTokens.MODIFIER_KEYWORDS_ARRAY.firstOrNull { it.value == text } != null
}
}