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 * https://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 com.google.accompanist.permissions.lint.util
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 kotlinx.metadata.KmDeclarationContainer
28 import kotlinx.metadata.KmFunction
29 import kotlinx.metadata.jvm.KotlinClassHeader
30 import kotlinx.metadata.jvm.KotlinClassMetadata
31 import kotlinx.metadata.jvm.signature
32
33 // FILE COPIED FROM:
34 // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/lint/common/src/main/java/androidx/compose/lint/KotlinMetadataUtils.kt
35
36 /**
37 * @return the corresponding [KmFunction] for this [PsiMethod], or `null` if there is no
38 * corresponding [KmFunction]. This method is only meaningful if this [PsiMethod] represents a
39 * method defined in bytecode (most often a [ClsMethodImpl]).
40 */
toKmFunctionnull41 public fun PsiMethod.toKmFunction(): KmFunction? =
42 containingClass!!.getKmDeclarationContainer()?.findKmFunctionForPsiMethod(this)
43
44 // TODO: https://youtrack.jetbrains.com/issue/KT-45310
45 // Currently there is no built in support for parsing kotlin metadata from kotlin class files, so
46 // we need to manually inspect the annotations and work with Cls* (compiled PSI).
47 /**
48 * Returns the [KmDeclarationContainer] using the kotlin.Metadata annotation present on this
49 * [PsiClass]. Returns null if there is no annotation (not parsing a Kotlin
50 * class file), the annotation data is for an unsupported version of Kotlin, or if the metadata
51 * represents a synthetic class.
52 */
53 private fun PsiClass.getKmDeclarationContainer(): KmDeclarationContainer? {
54 val classKotlinMetadataAnnotation = annotations.find {
55 // hasQualifiedName() not available on the min version of Lint we compile against
56 it.qualifiedName == KotlinMetadataFqn
57 } ?: return null
58
59 val metadata = KotlinClassMetadata.read(classKotlinMetadataAnnotation.toHeader())
60 ?: return null
61
62 return when (metadata) {
63 is KotlinClassMetadata.Class -> metadata.toKmClass()
64 is KotlinClassMetadata.FileFacade -> metadata.toKmPackage()
65 is KotlinClassMetadata.SyntheticClass -> null
66 is KotlinClassMetadata.MultiFileClassFacade -> null
67 is KotlinClassMetadata.MultiFileClassPart -> metadata.toKmPackage()
68 is KotlinClassMetadata.Unknown -> null
69 }
70 }
71
72 /**
73 * Returns a [KotlinClassHeader] by parsing the attributes of this @kotlin.Metadata annotation.
74 *
75 * See: https://github.com/udalov/kotlinx-metadata-examples/blob/master/src/main/java
76 * /examples/FindKotlinGeneratedMethods.java
77 */
PsiAnnotationnull78 private fun PsiAnnotation.toHeader(): KotlinClassHeader {
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 {
89 it.parseString()
90 }.toTypedArray()
91
92 fun JvmAnnotationAttributeValue.parseIntArray(): IntArray =
93 (this as JvmAnnotationArrayValue).values.map {
94 it.parseInt()
95 }.toTypedArray().toIntArray()
96
97 val kind = attributes["k"]?.parseInt()
98 val metadataVersion = attributes["mv"]?.parseIntArray()
99 val data1 = attributes["d1"]?.parseStringArray()
100 val data2 = attributes["d2"]?.parseStringArray()
101 val extraString = attributes["xs"]?.parseString()
102 val packageName = attributes["pn"]?.parseString()
103 val extraInt = attributes["xi"]?.parseInt()
104
105 return KotlinClassHeader(
106 kind,
107 metadataVersion,
108 data1,
109 data2,
110 extraString,
111 packageName,
112 extraInt
113 )
114 }
115
116 /**
117 * @return the corresponding [KmFunction] in [this] for the given [method], matching by name and
118 * signature.
119 */
findKmFunctionForPsiMethodnull120 private fun KmDeclarationContainer.findKmFunctionForPsiMethod(method: PsiMethod): KmFunction? {
121 // Strip any mangled part of the name in case of inline classes
122 val expectedName = method.name.substringBefore("-")
123 val expectedSignature = ClassUtil.getAsmMethodSignature(method)
124
125 return functions.find {
126 it.name == expectedName && it.signature?.desc == expectedSignature
127 }
128 }
129
130 private const val KotlinMetadataFqn = "kotlin.Metadata"
131