1 /*
2 * Copyright 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.lint
18
19 import com.intellij.lang.jvm.annotation.JvmAnnotationArrayValue
20 import com.intellij.lang.jvm.annotation.JvmAnnotationAttributeValue
21 import com.intellij.lang.jvm.annotation.JvmAnnotationConstantValue
22 import com.intellij.psi.PsiAnnotation
23 import com.intellij.psi.PsiClass
24 import com.intellij.psi.PsiMethod
25 import com.intellij.psi.impl.compiled.ClsMethodImpl
26 import com.intellij.psi.util.ClassUtil
27 import kotlin.metadata.KmDeclarationContainer
28 import kotlin.metadata.KmFunction
29 import kotlin.metadata.jvm.KotlinClassMetadata
30 import kotlin.metadata.jvm.Metadata
31 import kotlin.metadata.jvm.signature
32
33 /**
34 * @return the corresponding [KmFunction] for this [PsiMethod], or `null` if there is no
35 * corresponding [KmFunction]. This method is only meaningful if this [PsiMethod] represents a
36 * method defined in bytecode (most often a [ClsMethodImpl]).
37 */
PsiMethodnull38 fun PsiMethod.toKmFunction(): KmFunction? =
39 containingClass!!.getKmDeclarationContainer()?.findKmFunctionForPsiMethod(this)
40
41 // TODO: https://youtrack.jetbrains.com/issue/KT-45310
42 // Currently there is no built in support for parsing kotlin metadata from kotlin class files, so
43 // we need to manually inspect the annotations and work with Cls* (compiled PSI).
44 /**
45 * Returns the [KmDeclarationContainer] using the kotlin.Metadata annotation present on this
46 * [PsiClass]. Returns null if there is no annotation (not parsing a Kotlin class file), the
47 * annotation data is for an unsupported version of Kotlin, or if the metadata represents a
48 * synthetic class.
49 */
50 private fun PsiClass.getKmDeclarationContainer(): KmDeclarationContainer? {
51 val classKotlinMetadataPsiAnnotation =
52 annotations.find {
53 // hasQualifiedName() not available on the min version of Lint we compile against
54 it.qualifiedName == KotlinMetadataFqn
55 } ?: return null
56
57 val metadata =
58 try {
59 KotlinClassMetadata.readStrict(classKotlinMetadataPsiAnnotation.toMetadataAnnotation())
60 } catch (e: Exception) {
61 // Don't crash if we are trying to parse metadata from a newer version of Kotlin, than
62 // is
63 // supported by the bundled version of kotlin-metadata-jvm
64 return null
65 }
66
67 return when (metadata) {
68 is KotlinClassMetadata.Class -> metadata.kmClass
69 is KotlinClassMetadata.FileFacade -> metadata.kmPackage
70 is KotlinClassMetadata.SyntheticClass -> null
71 is KotlinClassMetadata.MultiFileClassFacade -> null
72 is KotlinClassMetadata.MultiFileClassPart -> metadata.kmPackage
73 is KotlinClassMetadata.Unknown -> null
74 }
75 }
76
77 /** Returns a [Metadata] by parsing the attributes of this @kotlin.Metadata PSI annotation. */
PsiAnnotationnull78 private fun PsiAnnotation.toMetadataAnnotation(): Metadata {
79 val attributes = attributes.associate { it.attributeName to it.attributeValue }
80
81 fun JvmAnnotationAttributeValue.parseString(): String =
82 (this as JvmAnnotationConstantValue).constantValue as String
83
84 fun JvmAnnotationAttributeValue.parseInt(): Int =
85 (this as JvmAnnotationConstantValue).constantValue as Int
86
87 fun JvmAnnotationAttributeValue.parseStringArray(): Array<String> =
88 (this as JvmAnnotationArrayValue).values.map { it.parseString() }.toTypedArray()
89
90 fun JvmAnnotationAttributeValue.parseIntArray(): IntArray =
91 (this as JvmAnnotationArrayValue).values.map { it.parseInt() }.toTypedArray().toIntArray()
92
93 val kind = attributes["k"]?.parseInt()
94 val metadataVersion = attributes["mv"]?.parseIntArray()
95 val data1 = attributes["d1"]?.parseStringArray()
96 val data2 = attributes["d2"]?.parseStringArray()
97 val extraString = attributes["xs"]?.parseString()
98 val packageName = attributes["pn"]?.parseString()
99 val extraInt = attributes["xi"]?.parseInt()
100
101 return Metadata(kind, metadataVersion, data1, data2, extraString, packageName, extraInt)
102 }
103
104 /**
105 * @return the corresponding [KmFunction] in [this] for the given [method], matching by name and
106 * signature.
107 */
findKmFunctionForPsiMethodnull108 private fun KmDeclarationContainer.findKmFunctionForPsiMethod(method: PsiMethod): KmFunction? {
109 // Strip any mangled part of the name in case of value / inline classes
110 val expectedName = method.name.substringBefore("-")
111 val expectedSignature = ClassUtil.getAsmMethodSignature(method)
112 // Since Kotlin 1.6 PSI updates, in some cases what used to be `void` return types are converted
113 // to `kotlin.Unit`, even though in the actual metadata they are still void. Try to match those
114 // cases as well
115 val unitReturnTypeSuffix = "Lkotlin/Unit;"
116 val expectedSignatureConvertedFromUnitToVoid =
117 if (expectedSignature.endsWith(unitReturnTypeSuffix)) {
118 expectedSignature.substringBeforeLast(unitReturnTypeSuffix) + "V"
119 } else {
120 expectedSignature
121 }
122
123 return functions.find {
124 it.name == expectedName &&
125 (it.signature?.descriptor == expectedSignature ||
126 it.signature?.descriptor == expectedSignatureConvertedFromUnitToVoid)
127 }
128 }
129
130 private const val KotlinMetadataFqn = "kotlin.Metadata"
131