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