@ -21,32 +21,39 @@ import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.parentOfType
import com.intellij.util.concurrency.annotations.RequiresReadLock
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.symbols.KaClassLikeSymbol
import org.jetbrains.kotlin.asJava.findFacadeClass
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.allConstructors
import org.jetbrains.kotlin.psi.psiUtil.containingClass
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
internal const val DESKTOP _PREVIEW _ANNOTATION _FQN = " androidx.compose.desktop.ui.tooling.preview.Preview "
internal const val DESKTOP _PREVIEW _ANNOTATION _FQN =
" androidx.compose.desktop.ui.tooling.preview.Preview "
internal const val COMPOSABLE _FQ _NAME = " androidx.compose.runtime.Composable "
private val ComposableAnnotationClassId = ClassId . topLevel ( FqName ( COMPOSABLE _FQ _NAME ) )
private val DesktopPreviewAnnotationClassId =
ClassId . topLevel ( FqName ( DESKTOP _PREVIEW _ANNOTATION _FQN ) )
/ * *
* Utils based on functions from AOSP , taken from
* tools / adt / idea / compose - designer / src / com / android / tools / idea / compose / preview / util / PreviewElement . kt
* /
/ * *
* Returns whether a `@Composable` [ PREVIEW _ANNOTATION _FQN ] is defined in a valid location , which can be either :
* Returns whether a `@Composable` [ DESKTOP _PREVIEW _ANNOTATION _FQN ] is defined in a valid location ,
* which can be either :
* 1. Top - level functions
* 2. Non - nested functions defined in top - level classes that have a default ( no parameter ) constructor
*
* 2. Non - nested functions defined in top - level classes that have a default ( no parameter )
* constructor
* /
private fun KtNamedFunction . isValidPreviewLocation ( ) : Boolean {
if ( valueParameters . size > 0 ) return false
if ( valueParameters . isNotEmpty ( ) ) return false
if ( receiverTypeReference != null ) return false
if ( isTopLevel ) return true
@ -55,7 +62,8 @@ private fun KtNamedFunction.isValidPreviewLocation(): Boolean {
// This is not a nested method
val containingClass = containingClass ( )
if ( containingClass != null ) {
// We allow functions that are not top level defined in top level classes that have a default (no parameter) constructor.
// We allow functions that are not top level defined in top level classes that have a
// default (no parameter) constructor.
if ( containingClass . isTopLevel ( ) && containingClass . hasDefaultConstructor ( ) ) {
return true
}
@ -64,84 +72,67 @@ private fun KtNamedFunction.isValidPreviewLocation(): Boolean {
return false
}
/ * *
* Computes the qualified name of the class containing this [ KtNamedFunction ] .
*
* For functions defined within a Kotlin class , returns the qualified name of that class . For top - level functions , returns the JVM name of
* the Java facade class generated instead .
*
* For functions defined within a Kotlin class , returns the qualified name of that class . For
* top - level functions , returns the JVM name of the Java facade class generated instead .
* /
internal fun KtNamedFunction . getClassName ( ) : String ? =
if ( isTopLevel ) ( ( parent as ? KtFile ) ?. findFacadeClass ( ) ) ?. qualifiedName else parentOfType < KtClass > ( ) ?. getQualifiedName ( )
if ( isTopLevel ) ( ( parent as ? KtFile ) ?. findFacadeClass ( ) ) ?. qualifiedName
else parentOfType < KtClass > ( ) ?. getQualifiedName ( )
/** Computes the qualified name for a Kotlin Class. Returns null if the class is a kotlin built-in. */
private fun KtClass . getQualifiedName ( ) : String ? {
val classDescriptor = analyze ( BodyResolveMode . PARTIAL ) . get ( BindingContext . CLASS , this ) ?: return null
return if ( KotlinBuiltIns . isUnderKotlinPackage ( classDescriptor ) || classDescriptor . kind != ClassKind . CLASS ) {
null
} else {
classDescriptor . fqNameSafe . asString ( )
/ * *
* Computes the qualified name for a Kotlin Class . Returns null if the class is a kotlin built - in .
* /
private fun KtClass . getQualifiedName ( ) : String ? =
analyze ( this ) {
val classSymbol = symbol
return when {
classSymbol !is KaClassLikeSymbol -> null
classSymbol . classId . isKotlinPackage ( ) -> null
else -> classSymbol . classId ?. asFqNameString ( )
}
}
}
private fun ClassId ?. isKotlinPackage ( ) =
this != null && startsWith ( org . jetbrains . kotlin . builtins . StandardNames . BUILT _INS _PACKAGE _NAME )
private fun KtClass . hasDefaultConstructor ( ) =
allConstructors . isEmpty ( ) . or ( allConstructors . any { it . valueParameters . isEmpty ( ) } )
/ * *
* Determines whether this [ KtAnnotationEntry ] has the specified qualified name .
* Careful : this does * not * currently take into account Kotlin type aliases ( https : //kotlinlang.org/docs/reference/type-aliases.html).
* Fortunately , type aliases are extremely uncommon for simple annotation types .
* /
private fun KtAnnotationEntry . fqNameMatches ( fqName : String ) : Boolean {
// For inspiration, see IDELightClassGenerationSupport.KtUltraLightSupportImpl.findAnnotation in the Kotlin plugin.
val shortName = shortName ?. asString ( ) ?: return false
return fqName . endsWith ( shortName ) && fqName == getQualifiedName ( )
}
/ * *
* Computes the qualified name of this [ KtAnnotationEntry ] .
* Prefer to use [ fqNameMatches ] , which checks the short name first and thus has better performance .
* /
private fun KtAnnotationEntry . getQualifiedName ( ) : String ? =
analyze ( BodyResolveMode . PARTIAL ) . get ( BindingContext . ANNOTATION , this ) ?. fqName ?. asString ( )
internal fun KtNamedFunction . composePreviewFunctionFqn ( ) = " ${getClassName()} . ${name} "
@RequiresReadLock
internal fun KtNamedFunction . isValidComposablePreviewFunction ( ) : Boolean {
fun isValidComposablePreviewImpl ( ) : Boolean {
if ( !is ValidPreviewLocation ( ) ) return false
var hasComposableAnnotation = false
var hasPreviewAnnotation = false
val annotationIt = annotationEntries . iterator ( )
while ( annotationIt . hasNext ( ) && ! ( hasComposableAnnotation && hasPreviewAnnotation ) ) {
val annotation = annotationIt . next ( )
hasComposableAnnotation = hasComposableAnnotation || annotation . fqNameMatches ( COMPOSABLE _FQ _NAME )
hasPreviewAnnotation = hasPreviewAnnotation || annotation . fqNameMatches ( DESKTOP _PREVIEW _ANNOTATION _FQN )
}
fun isValidComposablePreviewImpl ( ) : Boolean =
analyze ( this ) {
if ( !is ValidPreviewLocation ( ) ) return false
return hasComposableAnnotation && hasPreviewAnnotation
}
val mySymbol = symbol
val hasComposableAnnotation = mySymbol . annotations . contains ( ComposableAnnotationClassId )
val hasPreviewAnnotation =
mySymbol . annotations . contains ( DesktopPreviewAnnotationClassId )
return CachedValuesManager . getCachedValue ( this ) {
cachedResult ( isValidComposablePreviewImpl ( ) )
}
return hasComposableAnnotation && hasPreviewAnnotation
}
return CachedValuesManager . getCachedValue ( this ) { cachedResult ( isValidComposablePreviewImpl ( ) ) }
}
// based on AndroidComposePsiUtils.kt from AOSP
internal fun KtNamedFunction . isComposableFunction ( ) : Boolean {
return CachedValuesManager . getCachedValue ( this ) {
cachedResult ( annotationEntries . any { it . fqNameMatches ( COMPOSABLE _FQ _NAME ) } )
internal fun KtNamedFunction . isComposableFunction ( ) : Boolean =
CachedValuesManager . getCachedValue ( this ) {
val hasComposableAnnotation =
analyze ( this ) { symbol . annotations . contains ( ComposableAnnotationClassId ) }
cachedResult ( hasComposableAnnotation )
}
}
private fun < T > KtNamedFunction . cachedResult ( value : T ) =
CachedValueProvider . Result . create (
// TODO: see if we can handle alias imports without ruining performance.
value ,
this . containingKtFile ,
ProjectRootModificationTracker . getInstance ( project )
ProjectRootModificationTracker . getInstance ( project ) ,
)