1 /*
<lambda>null2  * Copyright 2023 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.android.tools.lint.client.api.UElementHandler
20 import com.android.tools.lint.detector.api.Category
21 import com.android.tools.lint.detector.api.Detector
22 import com.android.tools.lint.detector.api.Implementation
23 import com.android.tools.lint.detector.api.Issue
24 import com.android.tools.lint.detector.api.JavaContext
25 import com.android.tools.lint.detector.api.Scope
26 import com.android.tools.lint.detector.api.Severity
27 import com.android.tools.lint.detector.api.SourceCodeScanner
28 import com.intellij.lang.jvm.types.JvmType
29 import com.intellij.psi.PsiMethod
30 import com.intellij.psi.impl.source.PsiClassReferenceType
31 import org.jetbrains.uast.UClass
32 import org.jetbrains.uast.UElement
33 import org.jetbrains.uast.UImportStatement
34 import org.jetbrains.uast.UObjectLiteralExpression
35 import org.jetbrains.uast.USimpleNameReferenceExpression
36 import org.jetbrains.uast.tryResolve
37 
38 /**
39  * Lint [Detector] that catches patterns that are disallowed in MPP (JS + Native) in common module.
40  * Most Compose modules are compiled only for JVM in AndroidX, so Kotlin compiler doesn't report
41  * these issues.
42  */
43 class CommonModuleIncompatibilityDetector : Detector(), SourceCodeScanner {
44 
45     override fun getApplicableUastTypes(): List<Class<out UElement>> =
46         listOf(
47             UImportStatement::class.java,
48             USimpleNameReferenceExpression::class.java,
49             UClass::class.java,
50             UObjectLiteralExpression::class.java
51         )
52 
53     override fun createUastHandler(context: JavaContext): UElementHandler {
54         if (!context.file.absolutePath.contains(COMMON_MAIN_PATH_PREFIX)) {
55             return UElementHandler.NONE
56         }
57 
58         return object : UElementHandler() {
59             override fun visitImportStatement(node: UImportStatement) {
60                 val reference = node.importReference?.asRenderString() ?: return
61                 val isPlatformImport =
62                     PLATFORM_PACKAGES.any { platformPackage ->
63                         (platformPackage == reference && node.isOnDemand) ||
64                             reference.startsWith("$platformPackage.")
65                     }
66                 if (!isPlatformImport) return
67 
68                 val target = node.importReference!!
69                 context.report(
70                     IMPORT_ISSUE,
71                     target,
72                     context.getLocation(target),
73                     "Platform-dependent import in a common module"
74                 )
75             }
76 
77             override fun visitSimpleNameReferenceExpression(node: USimpleNameReferenceExpression) {
78                 if (node.identifier in RESTRICTED_PROPERTIES) {
79                     val method = node.tryResolve()
80                     if (method !is PsiMethod) return
81 
82                     val fqName = RESTRICTED_PROPERTIES[node.identifier]!!
83                     if (method.name != fqName.shortName) return
84                     if (!method.isInPackageName(fqName.packageName)) return
85 
86                     context.report(
87                         REFERENCE_ISSUE,
88                         node,
89                         context.getLocation(node),
90                         "Platform reference in a common module"
91                     )
92                 }
93             }
94 
95             override fun visitClass(node: UClass) {
96                 val extendsLambda = node.uastSuperTypes.any { it.type.isLambda() }
97                 if (extendsLambda) {
98                     context.report(
99                         EXTENDS_LAMBDA_ISSUE,
100                         node,
101                         context.getLocation(node.nameIdentifier),
102                         "Extending Kotlin lambda interfaces is not allowed in common code"
103                     )
104                 }
105             }
106 
107             override fun visitObjectLiteralExpression(node: UObjectLiteralExpression) {
108                 val extendsLambda = node.declaration.uastSuperTypes.any { it.type.isLambda() }
109                 if (extendsLambda) {
110                     context.report(
111                         EXTENDS_LAMBDA_ISSUE,
112                         node,
113                         context.getLocation(node),
114                         "Extending Kotlin lambda interfaces is not allowed in common code"
115                     )
116                 }
117             }
118         }
119     }
120 
121     companion object {
122         val IMPORT_ISSUE =
123             Issue.create(
124                 id = "PlatformImportInCommonModule",
125                 briefDescription = "Platform-dependent import in a common module",
126                 explanation =
127                     "Common Kotlin module cannot contain references to JVM or Android " +
128                         "classes, as it reduces future portability to other Kotlin targets. Consider " +
129                         "alternative methods allowed in common Kotlin code, or use expect/actual " +
130                         "to reference the platform code instead.",
131                 category = Category.CORRECTNESS,
132                 priority = 5,
133                 severity = Severity.ERROR,
134                 implementation =
135                     Implementation(
136                         CommonModuleIncompatibilityDetector::class.java,
137                         Scope.JAVA_FILE_SCOPE
138                     )
139             )
140 
141         val REFERENCE_ISSUE =
142             Issue.create(
143                 id = "PlatformReferenceInCommonModule",
144                 briefDescription = "Platform-dependent reference in a common module",
145                 explanation =
146                     "Common Kotlin module cannot contain references to JVM or Android " +
147                         "classes, as it reduces future portability to other Kotlin targets. Consider " +
148                         "alternative methods allowed in common Kotlin code, or use expect/actual " +
149                         "to reference the platform code instead.",
150                 category = Category.CORRECTNESS,
151                 priority = 5,
152                 severity = Severity.ERROR,
153                 implementation =
154                     Implementation(
155                         CommonModuleIncompatibilityDetector::class.java,
156                         Scope.JAVA_FILE_SCOPE
157                     )
158             )
159 
160         val EXTENDS_LAMBDA_ISSUE =
161             Issue.create(
162                 id = "ExtendedFunctionNInterface",
163                 briefDescription = "Extending Kotlin FunctionN interfaces in common code",
164                 explanation =
165                     "Common Kotlin module are ported to other Kotlin targets, including JS." +
166                         " Kotlin JS backend does not support extending lambda interfaces. Consider" +
167                         "extending fun interface in common Kotlin code, or use expect/actual instead.",
168                 category = Category.CORRECTNESS,
169                 priority = 5,
170                 severity = Severity.ERROR,
171                 implementation =
172                     Implementation(
173                         CommonModuleIncompatibilityDetector::class.java,
174                         Scope.JAVA_FILE_SCOPE
175                     )
176             )
177 
178         private const val COMMON_MAIN_PATH_PREFIX = "src/commonMain"
179         private val PLATFORM_PACKAGES = listOf("java", "javax", "android")
180         private val RESTRICTED_PROPERTIES =
181             mapOf(
182                 "javaClass" to Name(Package("kotlin.jvm"), "getJavaClass"),
183                 "java" to Name(Package("kotlin.jvm"), "getJavaClass"),
184             )
185     }
186 }
187 
188 private const val FunctionPrefix = "kotlin.jvm.functions.Function"
189 
190 @Suppress("UnstableApiUsage")
isLambdanull191 private fun JvmType.isLambda(): Boolean =
192     (this is PsiClassReferenceType && reference.qualifiedName.startsWith(FunctionPrefix))
193