From 4a6828b0f022dc727edf5b2c82fe90a4474b64fa Mon Sep 17 00:00:00 2001 From: Ben Trengrove Date: Tue, 10 Dec 2024 15:56:32 +1100 Subject: [PATCH] Upgrade kotlin metadata and match lint to androidx --- .../google/accompanist/BundleInsideHelper.kt | 5 + gradle/libs.versions.toml | 2 +- permissions-lint/build.gradle.kts | 4 - .../permissions/lint/util/ComposableUtils.kt | 304 ++++++++---------- .../lint/util/KotlinMetadataUtils.kt | 101 +++--- .../permissions/lint/util/Names.kt | 148 +++++++++ .../permissions/lint/util/PsiUtils.kt | 53 +++ 7 files changed, 393 insertions(+), 224 deletions(-) create mode 100644 permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/Names.kt create mode 100644 permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/PsiUtils.kt diff --git a/build-logic/convention/src/main/kotlin/com/google/accompanist/BundleInsideHelper.kt b/build-logic/convention/src/main/kotlin/com/google/accompanist/BundleInsideHelper.kt index 28a0ef6ae..f367cd3ce 100644 --- a/build-logic/convention/src/main/kotlin/com/google/accompanist/BundleInsideHelper.kt +++ b/build-logic/convention/src/main/kotlin/com/google/accompanist/BundleInsideHelper.kt @@ -32,6 +32,11 @@ import org.gradle.api.tasks.TaskProvider import org.gradle.kotlin.dsl.named import org.gradle.kotlin.dsl.register +/** + * Originally from https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt + * Small modifications based on gradle version + */ + /** Allow java and Android libraries to bundle other projects inside the project jar/aar. */ object BundleInsideHelper { val CONFIGURATION_NAME = "bundleInside" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 97ed82010..cb778b1a1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,7 +52,7 @@ kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = " kotlin-stdlibJdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } -kotlin-metadataJvm = "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.3.0" +kotlin-metadataJvm = { module = "org.jetbrains.kotlin:kotlin-metadata-jvm", version.ref = "kotlin" } kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } diff --git a/permissions-lint/build.gradle.kts b/permissions-lint/build.gradle.kts index ccf180d12..e23554900 100644 --- a/permissions-lint/build.gradle.kts +++ b/permissions-lint/build.gradle.kts @@ -23,10 +23,6 @@ plugins { id(libs.plugins.android.lint.get().pluginId) } -kotlin { - explicitApi() -} - lint { htmlReport = true htmlOutput = file("lint-report.html") diff --git a/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/ComposableUtils.kt b/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/ComposableUtils.kt index 4538092d8..46a9c7ca3 100644 --- a/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/ComposableUtils.kt +++ b/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/ComposableUtils.kt @@ -18,12 +18,14 @@ package com.google.accompanist.permissions.lint.util +// FILE COPIED FROM: +// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/lint/common/src/main/java/androidx/compose/lint/ComposableUtils.kt + import com.intellij.lang.java.JavaLanguage import com.intellij.psi.PsiMethod import com.intellij.psi.PsiParameter import com.intellij.psi.impl.compiled.ClsParameterImpl import com.intellij.psi.impl.light.LightParameter -import kotlinx.metadata.jvm.annotations import org.jetbrains.kotlin.psi.KtAnnotated import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.KtProperty @@ -34,6 +36,7 @@ import org.jetbrains.uast.UAnonymousClass import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UDeclaration import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression import org.jetbrains.uast.ULambdaExpression import org.jetbrains.uast.UMethod import org.jetbrains.uast.UParameter @@ -44,141 +47,167 @@ import org.jetbrains.uast.getContainingUClass import org.jetbrains.uast.getParameterForArgument import org.jetbrains.uast.toUElement import org.jetbrains.uast.withContainingElements +import kotlin.metadata.jvm.annotations -// FILE COPIED FROM: -// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/lint/common/src/main/java/androidx/compose/lint/ComposableUtils.kt +/** + * Returns whether this [UCallExpression] is directly invoked within the body of a Composable + * function or lambda without being `remember`ed. + */ +public fun UCallExpression.isNotRemembered(): Boolean = isNotRememberedWithKeys() /** - * Returns whether this [UCallExpression] is invoked within the body of a Composable function or - * lambda. + * Returns whether this [UCallExpression] is directly invoked within the body of a Composable + * function or lambda without being `remember`ed, or whether it is invoked inside a `remember call + * without the provided [keys][keyClassNames]. + * - Returns true if this [UCallExpression] is directly invoked inside a Composable function or + * lambda without being `remember`ed + * - Returns true if this [UCallExpression] is invoked inside a call to `remember`, but without all + * of the provided [keys][keyClassNames] being used as key parameters to `remember` + * - Returns false if this [UCallExpression] is correctly `remember`ed with the provided + * [keys][keyClassNames], or is not called inside a `remember` block, and is not called inside a + * Composable function or lambda * - * This searches parent declarations until we find a lambda expression or a function, and looks - * to see if these are Composable. + * @param keyClassNames [Name]s representing the expected classes that should be used as a key + * parameter to the `remember` call */ -public fun UCallExpression.isInvokedWithinComposable(): Boolean { +public fun UCallExpression.isNotRememberedWithKeys(vararg keyClassNames: Name): Boolean { + val visitor = ComposableBodyVisitor(this) + // The nearest method or lambda expression that contains this call expression + val boundaryElement = visitor.parentUElements().last() + // Check if the nearest lambda expression is actually a call to remember + val rememberCall: UCallExpression? = + (boundaryElement.uastParent as? UCallExpression)?.takeIf { + it.methodName == Names.Runtime.Remember.shortName && + it.resolve()?.isInPackageName(Names.Runtime.PackageName) == true + } + return if (rememberCall == null) { + visitor.isComposable() + } else { + val parameterTypes = + rememberCall.valueArguments.mapNotNull { it.getExpressionType()?.canonicalText } + !keyClassNames.all { parameterTypes.contains(it.javaFqn) } + } +} + +/** + * Returns whether this [UExpression] is invoked within the body of a Composable function or lambda. + * + * This searches parent declarations until we find a lambda expression or a function, and looks to + * see if these are Composable. + */ +fun UExpression.isInvokedWithinComposable(): Boolean { return ComposableBodyVisitor(this).isComposable() } // TODO: https://youtrack.jetbrains.com/issue/KT-45406 // KotlinUMethodWithFakeLightDelegate.hasAnnotation() (for reified functions for example) // doesn't find annotations, so just look at the annotations directly. -/** - * Returns whether this method is @Composable or not - */ -public val PsiMethod.isComposable: Boolean - get() = annotations.any { it.qualifiedName == Composable.javaFqn } +/** Returns whether this method is @Composable or not */ +val PsiMethod.isComposable + get() = annotations.any { it.qualifiedName == Names.Runtime.Composable.javaFqn } -/** - * Returns whether this variable's type is @Composable or not - */ -public val UVariable.isComposable: Boolean +/** Returns whether this variable's type is @Composable or not */ +val UVariable.isComposable: Boolean get() { // Annotation on the lambda - val annotationOnLambda = when (val initializer = uastInitializer) { - is ULambdaExpression -> { - val source = initializer.sourcePsi - if (source is KtFunction) { - // Anonymous function, val foo = @Composable fun() {} - source.hasComposableAnnotation - } else { - // Lambda, val foo = @Composable {} - initializer.findAnnotation(Composable.javaFqn) != null + val annotationOnLambda = + when (val initializer = uastInitializer) { + is ULambdaExpression -> { + val source = initializer.sourcePsi + if (source is KtFunction) { + // Anonymous function, val foo = @Composable fun() {} + source.hasComposableAnnotation + } else { + // Lambda, val foo = @Composable {} + initializer.findAnnotation(Names.Runtime.Composable.javaFqn) != null + } } + else -> false } - else -> false - } // Annotation on the type, foo: @Composable () -> Unit = { } val annotationOnType = typeReference?.isComposable == true return annotationOnLambda || annotationOnType } -/** - * Returns whether this parameter's type is @Composable or not - */ +/** Returns whether this parameter's type is @Composable or not */ private val PsiParameter.isComposable: Boolean - get() = when { - // The parameter is in a class file. Currently type annotations aren't currently added to - // the underlying type (https://youtrack.jetbrains.com/issue/KT-45307), so instead we use - // the metadata annotation. - this is ClsParameterImpl || - // In some cases when a method is defined in bytecode and the call fails to resolve - // to the ClsMethodImpl, we will instead get a LightParameter. Note that some Kotlin - // declarations too will also appear as a LightParameter, so we can check to see if - // the source language is Java, which means that this is a LightParameter for - // bytecode, as opposed to for a Kotlin declaration. - // https://youtrack.jetbrains.com/issue/KT-46883 - (this is LightParameter && this.language is JavaLanguage) -> { - // Find the containing method, so we can get metadata from the containing class - val containingMethod = getParentOfType(true) - val kmFunction = containingMethod!!.toKmFunction() + get() = + when { + // The parameter is in a class file. Currently type annotations aren't currently added + // to + // the underlying type (https://youtrack.jetbrains.com/issue/KT-45307), so instead we + // use + // the metadata annotation. + this is ClsParameterImpl || + // In some cases when a method is defined in bytecode and the call fails to resolve + // to the ClsMethodImpl, we will instead get a LightParameter. Note that some Kotlin + // declarations too will also appear as a LightParameter, so we can check to see if + // the source language is Java, which means that this is a LightParameter for + // bytecode, as opposed to for a Kotlin declaration. + // https://youtrack.jetbrains.com/issue/KT-46883 + (this is LightParameter && this.language is JavaLanguage) -> { + // Find the containing method, so we can get metadata from the containing class + val containingMethod = getParentOfType(true) + val kmFunction = containingMethod!!.toKmFunction() - val kmValueParameter = kmFunction?.valueParameters?.find { - it.name == name - } + val kmValueParameter = kmFunction?.valueParameters?.find { it.name == name } - kmValueParameter?.type?.annotations?.find { - it.className == Composable.kmClassName - } != null + kmValueParameter?.type?.annotations?.find { + it.className == Names.Runtime.Composable.kmClassName + } != null + } + // The parameter is in a source declaration + else -> (toUElement() as? UParameter)?.typeReference?.isComposable == true } - // The parameter is in a source declaration - else -> (toUElement() as UParameter).typeReference!!.isComposable - } -/** - * Returns whether this lambda expression is @Composable or not - */ -public val ULambdaExpression.isComposable: Boolean - get() = when (val lambdaParent = uastParent) { - // Function call with a lambda parameter - is UCallExpression -> { - val parameter = lambdaParent.getParameterForArgument(this) - parameter?.isComposable == true - } - // A local / non-local lambda variable - is UVariable -> { - lambdaParent.isComposable +/** Returns whether this lambda expression is @Composable or not */ +val ULambdaExpression.isComposable: Boolean + get() = + when (val lambdaParent = uastParent) { + // Function call with a lambda parameter + is UCallExpression -> { + val parameter = lambdaParent.getParameterForArgument(this) + parameter?.isComposable == true + } + // A local / non-local lambda variable + is UVariable -> { + lambdaParent.isComposable + } + // Either a new UAST type we haven't handled, or non-Kotlin declarations + else -> false } - // Either a new UAST type we haven't handled, or non-Kotlin declarations - else -> false - } /** - * Helper class that visits parent declarations above the provided [callExpression], until it - * finds a lambda or method. This 'boundary' is used as the indicator for whether this - * [callExpression] can be considered to be inside a Composable body or not. + * Helper class that visits parent declarations above the provided [expression], until it finds a + * lambda or method. This 'boundary' is used as the indicator for whether this [expression] can be + * considered to be inside a Composable body or not. * * @see isComposable * @see parentUElements */ -private class ComposableBodyVisitor( - private val callExpression: UCallExpression -) { - /** - * @return whether the body can be considered Composable or not - */ - fun isComposable(): Boolean = when (val element = parentUElements.last()) { - is UMethod -> element.isComposable - is ULambdaExpression -> element.isComposable - else -> false - } +private class ComposableBodyVisitor(private val expression: UExpression) { + /** @return whether the body can be considered Composable or not */ + fun isComposable(): Boolean = + when (val element = parentUElements.last()) { + is UMethod -> element.isComposable + is ULambdaExpression -> element.isComposable + else -> false + } - /** - * Returns all parent [UElement]s until and including the boundary lambda / method. - */ + /** Returns all parent [UElement]s until and including the boundary lambda / method. */ fun parentUElements() = parentUElements /** * The outermost UElement that corresponds to the surrounding UDeclaration that contains - * [callExpression], with the following special cases: - * - * - if the containing UDeclaration is a local property, we ignore it and search above as - * it still could be created in the context of a Composable body - * - if the containing UDeclaration is an anonymous class (object { }), we ignore it and - * search above as it still could be created in the context of a Composable body + * [expression], with the following special cases: + * - if the containing UDeclaration is a local property, we ignore it and search above as it + * still could be created in the context of a Composable body + * - if the containing UDeclaration is an anonymous class (object { }), we ignore it and search + * above as it still could be created in the context of a Composable body */ private val boundaryUElement by lazy { // The nearest property / function / etc declaration that contains this call expression - var containingDeclaration = callExpression.getContainingDeclaration() + var containingDeclaration = expression.getContainingDeclaration() fun UDeclaration.isLocalProperty() = (sourcePsi as? KtProperty)?.isLocal == true fun UDeclaration.isAnonymousClass() = this is UAnonymousClass @@ -203,7 +232,7 @@ private class ComposableBodyVisitor( val elements = mutableListOf() // Look through containing elements until we find a lambda or a method - for (element in callExpression.withContainingElements) { + for (element in expression.withContainingElements) { elements += element when (element) { // TODO: consider handling the case of a lambda inside an inline function call, @@ -222,12 +251,10 @@ private class ComposableBodyVisitor( } } -/** - * Returns whether this type reference is @Composable or not - */ -private val UTypeReferenceExpression.isComposable: Boolean +/** Returns whether this type reference is @Composable or not */ +val UTypeReferenceExpression.isComposable: Boolean get() { - if (type.hasAnnotation(Composable.javaFqn)) return true + if (type.hasAnnotation(Names.Runtime.Composable.javaFqn)) return true // Annotations on the types of local properties (val foo: @Composable () -> Unit = {}) // are currently not present on the PsiType, we so need to manually check the underlying @@ -235,74 +262,9 @@ private val UTypeReferenceExpression.isComposable: Boolean return (sourcePsi as? KtTypeReference)?.hasComposableAnnotation == true } -/** - * Returns whether this annotated declaration has a Composable annotation - */ +/** Returns whether this annotated declaration has a Composable annotation */ private val KtAnnotated.hasComposableAnnotation: Boolean - get() = annotationEntries.any { - (it.toUElement() as UAnnotation).qualifiedName == Composable.javaFqn - } - -private val RuntimePackageName = Package("androidx.compose.runtime") -private val Composable = Name(RuntimePackageName, "Composable") - -/** - * @return a [PackageName] with a Java-style (separated with `.`) [packageName]. - */ -internal fun Package(packageName: String): PackageName = - PackageName(packageName.split(".")) - -/** - * @return a [Name] with the provided [pkg] and Java-style (separated with `.`) [shortName]. - */ -internal fun Name(pkg: PackageName, shortName: String): Name = - Name(pkg, shortName.split(".")) - -/** - * Represents a qualified package - * - * @property segments the segments representing the package - */ -internal class PackageName internal constructor(internal val segments: List) { - /** - * The Java-style package name for this [Name], separated with `.` - */ - val javaPackageName: String - get() = segments.joinToString(".") -} - -/** - * Represents the qualified name for an element - * - * @property pkg the package for this element - * @property nameSegments the segments representing the element - there can be multiple in the - * case of nested classes. - */ -internal class Name internal constructor( - private val pkg: PackageName, - private val nameSegments: List -) { - /** - * The short name for this [Name] - */ - val shortName: String - get() = nameSegments.last() - - /** - * The Java-style fully qualified name for this [Name], separated with `.` - */ - val javaFqn: String - get() = pkg.segments.joinToString(".", postfix = ".") + - nameSegments.joinToString(".") - - /** - * The [ClassName] for use with kotlinx.metadata. Note that in kotlinx.metadata the actual - * type might be different from the underlying JVM type, for example: - * kotlin/Int -> java/lang/Integer - */ - val kmClassName: ClassName - get() = pkg.segments.joinToString("/", postfix = "/") + - nameSegments.joinToString(".") -} - -private typealias ClassName = String + get() = + annotationEntries.any { + (it.toUElement() as UAnnotation).qualifiedName == Names.Runtime.Composable.javaFqn + } diff --git a/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/KotlinMetadataUtils.kt b/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/KotlinMetadataUtils.kt index 64c6f63f9..af2a32a73 100644 --- a/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/KotlinMetadataUtils.kt +++ b/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/KotlinMetadataUtils.kt @@ -16,6 +16,9 @@ package com.google.accompanist.permissions.lint.util +// FILE COPIED FROM: +// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt + import com.intellij.lang.jvm.annotation.JvmAnnotationArrayValue import com.intellij.lang.jvm.annotation.JvmAnnotationAttributeValue import com.intellij.lang.jvm.annotation.JvmAnnotationConstantValue @@ -24,19 +27,16 @@ import com.intellij.psi.PsiClass import com.intellij.psi.PsiMethod import com.intellij.psi.impl.compiled.ClsMethodImpl import com.intellij.psi.util.ClassUtil -import kotlinx.metadata.KmDeclarationContainer -import kotlinx.metadata.KmFunction -import kotlinx.metadata.jvm.KotlinClassHeader -import kotlinx.metadata.jvm.KotlinClassMetadata -import kotlinx.metadata.jvm.signature - -// FILE COPIED FROM: -// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt +import kotlin.metadata.KmDeclarationContainer +import kotlin.metadata.KmFunction +import kotlin.metadata.jvm.KotlinClassMetadata +import kotlin.metadata.jvm.Metadata +import kotlin.metadata.jvm.signature /** * @return the corresponding [KmFunction] for this [PsiMethod], or `null` if there is no - * corresponding [KmFunction]. This method is only meaningful if this [PsiMethod] represents a - * method defined in bytecode (most often a [ClsMethodImpl]). + * corresponding [KmFunction]. This method is only meaningful if this [PsiMethod] represents a + * method defined in bytecode (most often a [ClsMethodImpl]). */ public fun PsiMethod.toKmFunction(): KmFunction? = containingClass!!.getKmDeclarationContainer()?.findKmFunctionForPsiMethod(this) @@ -46,36 +46,39 @@ public fun PsiMethod.toKmFunction(): KmFunction? = // we need to manually inspect the annotations and work with Cls* (compiled PSI). /** * Returns the [KmDeclarationContainer] using the kotlin.Metadata annotation present on this - * [PsiClass]. Returns null if there is no annotation (not parsing a Kotlin - * class file), the annotation data is for an unsupported version of Kotlin, or if the metadata - * represents a synthetic class. + * [PsiClass]. Returns null if there is no annotation (not parsing a Kotlin class file), the + * annotation data is for an unsupported version of Kotlin, or if the metadata represents a + * synthetic class. */ private fun PsiClass.getKmDeclarationContainer(): KmDeclarationContainer? { - val classKotlinMetadataAnnotation = annotations.find { - // hasQualifiedName() not available on the min version of Lint we compile against - it.qualifiedName == KotlinMetadataFqn - } ?: return null - - val metadata = KotlinClassMetadata.read(classKotlinMetadataAnnotation.toHeader()) - ?: return null + val classKotlinMetadataPsiAnnotation = + annotations.find { + // hasQualifiedName() not available on the min version of Lint we compile against + it.qualifiedName == KotlinMetadataFqn + } ?: return null + + val metadata = + try { + KotlinClassMetadata.readStrict(classKotlinMetadataPsiAnnotation.toMetadataAnnotation()) + } catch (e: Exception) { + // Don't crash if we are trying to parse metadata from a newer version of Kotlin, than + // is + // supported by the bundled version of kotlin-metadata-jvm + return null + } return when (metadata) { - is KotlinClassMetadata.Class -> metadata.toKmClass() - is KotlinClassMetadata.FileFacade -> metadata.toKmPackage() + is KotlinClassMetadata.Class -> metadata.kmClass + is KotlinClassMetadata.FileFacade -> metadata.kmPackage is KotlinClassMetadata.SyntheticClass -> null is KotlinClassMetadata.MultiFileClassFacade -> null - is KotlinClassMetadata.MultiFileClassPart -> metadata.toKmPackage() + is KotlinClassMetadata.MultiFileClassPart -> metadata.kmPackage is KotlinClassMetadata.Unknown -> null } } -/** - * Returns a [KotlinClassHeader] by parsing the attributes of this @kotlin.Metadata annotation. - * - * See: https://github.com/udalov/kotlinx-metadata-examples/blob/master/src/main/java - * /examples/FindKotlinGeneratedMethods.java - */ -private fun PsiAnnotation.toHeader(): KotlinClassHeader { +/** Returns a [Metadata] by parsing the attributes of this @kotlin.Metadata PSI annotation. */ +private fun PsiAnnotation.toMetadataAnnotation(): Metadata { val attributes = attributes.associate { it.attributeName to it.attributeValue } fun JvmAnnotationAttributeValue.parseString(): String = @@ -85,14 +88,10 @@ private fun PsiAnnotation.toHeader(): KotlinClassHeader { (this as JvmAnnotationConstantValue).constantValue as Int fun JvmAnnotationAttributeValue.parseStringArray(): Array = - (this as JvmAnnotationArrayValue).values.map { - it.parseString() - }.toTypedArray() + (this as JvmAnnotationArrayValue).values.map { it.parseString() }.toTypedArray() fun JvmAnnotationAttributeValue.parseIntArray(): IntArray = - (this as JvmAnnotationArrayValue).values.map { - it.parseInt() - }.toTypedArray().toIntArray() + (this as JvmAnnotationArrayValue).values.map { it.parseInt() }.toTypedArray().toIntArray() val kind = attributes["k"]?.parseInt() val metadataVersion = attributes["mv"]?.parseIntArray() @@ -102,28 +101,34 @@ private fun PsiAnnotation.toHeader(): KotlinClassHeader { val packageName = attributes["pn"]?.parseString() val extraInt = attributes["xi"]?.parseInt() - return KotlinClassHeader( - kind, - metadataVersion, - data1, - data2, - extraString, - packageName, - extraInt - ) + return Metadata(kind, metadataVersion, data1, data2, extraString, packageName, extraInt) } /** * @return the corresponding [KmFunction] in [this] for the given [method], matching by name and - * signature. + * signature. */ private fun KmDeclarationContainer.findKmFunctionForPsiMethod(method: PsiMethod): KmFunction? { - // Strip any mangled part of the name in case of inline classes + // Strip any mangled part of the name in case of value / inline classes val expectedName = method.name.substringBefore("-") val expectedSignature = ClassUtil.getAsmMethodSignature(method) + // Since Kotlin 1.6 PSI updates, in some cases what used to be `void` return types are converted + // to `kotlin.Unit`, even though in the actual metadata they are still void. Try to match those + // cases as well + val unitReturnTypeSuffix = "Lkotlin/Unit;" + val expectedSignatureConvertedFromUnitToVoid = + if (expectedSignature.endsWith(unitReturnTypeSuffix)) { + expectedSignature.substringBeforeLast(unitReturnTypeSuffix) + "V" + } else { + expectedSignature + } return functions.find { - it.name == expectedName && it.signature?.desc == expectedSignature + it.name == expectedName && + ( + it.signature?.descriptor == expectedSignature || + it.signature?.descriptor == expectedSignatureConvertedFromUnitToVoid + ) } } diff --git a/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/Names.kt b/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/Names.kt new file mode 100644 index 000000000..073620fa1 --- /dev/null +++ b/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/Names.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.permissions.lint.util + +/** + * File copied from + * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/lint/common/src/main/java/androidx/compose/lint/Names.kt + */ + +import kotlin.metadata.ClassName + +/** Contains common names used for lint checks. */ +object Names { + object Animation { + val PackageName = Package("androidx.compose.animation") + + object Core { + val PackageName = Package("androidx.compose.animation.core") + val Animatable = Name(PackageName, "Animatable") + } + } + + object AnimationCore { + val PackageName = Package("androidx.compose.animation.core") + } + + object Runtime { + val PackageName = Package("androidx.compose.runtime") + + val Composable = Name(PackageName, "Composable") + val CompositionLocal = Name(PackageName, "CompositionLocal") + val DerivedStateOf = Name(PackageName, "derivedStateOf") + val State = Name(PackageName, "State") + val MutableState = Name(PackageName, "MutableState") + val MutableStateOf = Name(PackageName, "mutableStateOf") + val MutableIntStateOf = Name(PackageName, "mutableIntStateOf") + val MutableLongStateOf = Name(PackageName, "mutableLongStateOf") + val MutableFloatStateOf = Name(PackageName, "mutableFloatStateOf") + val MutableDoubleStateOf = Name(PackageName, "mutableDoubleStateOf") + val MutableStateListOf = Name(PackageName, "mutableStateListOf") + val MutableStateMapOf = Name(PackageName, "mutableStateMapOf") + val ProduceState = Name(PackageName, "produceState") + val Remember = Name(PackageName, "remember") + val DisposableEffect = Name(PackageName, "DisposableEffect") + val RememberSaveable = Name(PackageName, "rememberSaveable") + val LaunchedEffect = Name(PackageName, "LaunchedEffect") + val ReusableContent = Name(PackageName, "ReusableContent") + val Key = Name(PackageName, "key") + val StructuralEqualityPolicy = Name(PackageName, "structuralEqualityPolicy") + } + + object Ui { + val PackageName = Package("androidx.compose.ui") + val Composed = Name(PackageName, "composed") + val Modifier = Name(PackageName, "Modifier") + + object Layout { + val PackageName = Package("androidx.compose.ui.layout") + val ParentDataModifier = Name(PackageName, "ParentDataModifier") + } + + object Pointer { + val PackageName = Package(Ui.PackageName, "input.pointer") + val PointerInputScope = Name(PackageName, "PointerInputScope") + val PointerInputScopeModifier = Name(PackageName, "pointerInput") + val AwaitPointerEventScope = Name(PackageName, "awaitPointerEventScope") + } + + object Unit { + val PackageName = Package("androidx.compose.ui.unit") + val Dp = Name(PackageName, "Dp") + } + + object Node { + val PackageName = Package(Ui.PackageName, "node") + val CurrentValueOf = Name(PackageName, "currentValueOf") + } + } + + object UiGraphics { + val PackageName = Package("androidx.compose.ui.graphics") + val Color = Name(PackageName, "Color") + } +} + +/** + * Represents a qualified package + * + * @property segments the segments representing the package + */ +class PackageName internal constructor(internal val segments: List) { + /** The Java-style package name for this [Name], separated with `.` */ + val javaPackageName: String + get() = segments.joinToString(".") +} + +/** + * Represents the qualified name for an element + * + * @property pkg the package for this element + * @property nameSegments the segments representing the element - there can be multiple in the case + * of nested classes. + */ +class Name +internal constructor(private val pkg: PackageName, private val nameSegments: List) { + /** The short name for this [Name] */ + val shortName: String + get() = nameSegments.last() + + /** The Java-style fully qualified name for this [Name], separated with `.` */ + val javaFqn: String + get() = pkg.segments.joinToString(".", postfix = ".") + nameSegments.joinToString(".") + + /** + * The [ClassName] for use with kotlin.metadata. Note that in kotlin.metadata the actual type + * might be different from the underlying JVM type, for example: kotlin/Int -> java/lang/Integer + */ + val kmClassName: ClassName + get() = pkg.segments.joinToString("/", postfix = "/") + nameSegments.joinToString(".") + + /** The [PackageName] of this element. */ + val packageName: PackageName + get() = pkg +} + +/** @return a [PackageName] with a Java-style (separated with `.`) [packageName]. */ +fun Package(packageName: String): PackageName = PackageName(packageName.split(".")) + +/** @return a [PackageName] with a Java-style (separated with `.`) [packageName]. */ +fun Package(packageName: PackageName, shortName: String): PackageName = + PackageName(packageName.segments + shortName.split(".")) + +/** @return a [Name] with the provided [pkg] and Java-style (separated with `.`) [shortName]. */ +fun Name(pkg: PackageName, shortName: String): Name = Name(pkg, shortName.split(".")) diff --git a/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/PsiUtils.kt b/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/PsiUtils.kt new file mode 100644 index 000000000..76ee3ea6f --- /dev/null +++ b/permissions-lint/src/main/java/com/google/accompanist/permissions/lint/util/PsiUtils.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.permissions.lint.util + +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassOwner +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiType +import com.intellij.psi.util.InheritanceUtil + +/** + * File copied from + * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/lint/common/src/main/java/androidx/compose/lint/PsiUtils.kt + */ + +/** Returns whether [this] has [packageName] as its package name. */ +fun PsiMethod.isInPackageName(packageName: PackageName): Boolean { + val actual = (containingFile as? PsiClassOwner)?.packageName + return packageName.javaPackageName == actual +} + +/** Whether this [PsiMethod] returns Unit */ +val PsiMethod.returnsUnit + get() = returnType.isVoidOrUnit + +/** + * Whether this [PsiType] is `void` or [Unit] + * + * In Kotlin 1.6 some expressions now explicitly return [Unit] instead of just being [PsiType.VOID], + * so this returns whether this type is either. + */ +val PsiType?.isVoidOrUnit + get() = this == PsiType.VOID || this?.canonicalText == "kotlin.Unit" + +/** @return whether [this] inherits from [name]. Returns `true` if [this] _is_ directly [name]. */ +fun PsiType.inheritsFrom(name: Name) = InheritanceUtil.isInheritor(this, name.javaFqn) + +/** @return whether [this] inherits from [name]. Returns `true` if [this] _is_ directly [name]. */ +fun PsiClass.inheritsFrom(name: Name) = InheritanceUtil.isInheritor(this, name.javaFqn)