1 /*
<lambda>null2  * Copyright (C) 2018 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 com.android.tools.metalava
18 
19 import com.android.tools.metalava.doclava1.Issues
20 import com.android.tools.metalava.model.ClassItem
21 import com.android.tools.metalava.model.Codebase
22 import com.android.tools.metalava.model.FieldItem
23 import com.android.tools.metalava.model.Item
24 import com.android.tools.metalava.model.MethodItem
25 import com.android.tools.metalava.model.ParameterItem
26 import com.android.tools.metalava.model.TypeItem
27 import com.android.tools.metalava.model.visitors.ApiVisitor
28 import com.intellij.lang.java.lexer.JavaLexer
29 import org.jetbrains.kotlin.lexer.KtTokens
30 import org.jetbrains.kotlin.psi.KtObjectDeclaration
31 import org.jetbrains.kotlin.psi.KtProperty
32 import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
33 import org.jetbrains.uast.kotlin.KotlinUField
34 
35 // Enforces the interoperability guidelines outlined in
36 //   https://android.github.io/kotlin-guides/interop.html
37 //
38 // Also potentially makes other API suggestions.
39 class KotlinInteropChecks(val reporter: Reporter) {
40     fun check(codebase: Codebase) {
41 
42         codebase.accept(object : ApiVisitor(
43             // Sort by source order such that warnings follow source line number order
44             methodComparator = MethodItem.sourceOrderComparator,
45             fieldComparator = FieldItem.comparator
46         ) {
47             private var isKotlin = false
48 
49             override fun visitClass(cls: ClassItem) {
50                 isKotlin = cls.isKotlin()
51             }
52 
53             override fun visitMethod(method: MethodItem) {
54                 checkMethod(method, isKotlin)
55             }
56 
57             override fun visitField(field: FieldItem) {
58                 checkField(field, isKotlin)
59             }
60         })
61     }
62 
63     fun checkField(field: FieldItem, isKotlin: Boolean = field.isKotlin()) {
64         if (isKotlin) {
65             ensureCompanionFieldJvmField(field)
66         }
67         ensureFieldNameNotKeyword(field)
68     }
69 
70     fun checkMethod(method: MethodItem, isKotlin: Boolean = method.isKotlin()) {
71         if (!method.isConstructor()) {
72             if (isKotlin) {
73                 ensureDefaultParamsHaveJvmOverloads(method)
74                 ensureCompanionJvmStatic(method)
75                 ensureExceptionsDocumented(method)
76             } else {
77                 ensureMethodNameNotKeyword(method)
78                 ensureParameterNamesNotKeywords(method)
79             }
80             ensureLambdaLastParameter(method)
81         }
82     }
83 
84     private fun ensureExceptionsDocumented(method: MethodItem) {
85         if (!method.isKotlin()) {
86             return
87         }
88 
89         val exceptions = method.findThrownExceptions()
90         if (exceptions.isEmpty()) {
91             return
92         }
93         val doc = method.documentation
94         for (exception in exceptions.sortedBy { it.qualifiedName() }) {
95             val checked = !(exception.extends("java.lang.RuntimeException") ||
96                 exception.extends("java.lang.Error"))
97             if (checked) {
98                 val annotation = method.modifiers.findAnnotation("kotlin.jvm.Throws")
99                 if (annotation != null) {
100                     // There can be multiple values
101                     for (attribute in annotation.attributes()) {
102                         for (v in attribute.leafValues()) {
103                             val source = v.toSource()
104                             if (source.endsWith(exception.simpleName() + "::class")) {
105                                 return
106                             }
107                         }
108                     }
109                 }
110                 reporter.report(
111                     Issues.DOCUMENT_EXCEPTIONS, method,
112                     "Method ${method.containingClass().simpleName()}.${method.name()} appears to be throwing ${exception.qualifiedName()}; this should be recorded with a @Throws annotation; see https://android.github.io/kotlin-guides/interop.html#document-exceptions"
113                 )
114             } else {
115                 if (!doc.contains(exception.simpleName())) {
116                     reporter.report(
117                         Issues.DOCUMENT_EXCEPTIONS, method,
118                         "Method ${method.containingClass().simpleName()}.${method.name()} appears to be throwing ${exception.qualifiedName()}; this should be listed in the documentation; see https://android.github.io/kotlin-guides/interop.html#document-exceptions"
119                     )
120                 }
121             }
122         }
123     }
124 
125     private fun ensureCompanionFieldJvmField(field: FieldItem) {
126         val modifiers = field.modifiers
127         if (modifiers.isPublic() && modifiers.isFinal()) {
128             // UAST will inline const fields into the surrounding class, so we have to
129             // dip into Kotlin PSI to figure out if this field was really declared in
130             // a companion object
131             val psi = field.psi()
132             if (psi is KotlinUField) {
133                 val sourcePsi = psi.sourcePsi
134                 if (sourcePsi is KtProperty) {
135                     val companionClassName = sourcePsi.containingClassOrObject?.name
136                     if (companionClassName == "Companion") {
137                         // JvmField cannot be applied to const property (https://github.com/JetBrains/kotlin/blob/dc7b1fbff946d1476cc9652710df85f65664baee/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/checkers/JvmFieldApplicabilityChecker.kt#L46)
138                         if (!modifiers.isConst()) {
139                             if (modifiers.findAnnotation("kotlin.jvm.JvmField") == null) {
140                                 reporter.report(
141                                     Issues.MISSING_JVMSTATIC, field,
142                                     "Companion object constants like ${field.name()} should be marked @JvmField for Java interoperability; see https://developer.android.com/kotlin/interop#companion_constants"
143                                 )
144                             } else if (modifiers.findAnnotation("kotlin.jvm.JvmStatic") != null) {
145                                 reporter.report(
146                                     Issues.MISSING_JVMSTATIC, field,
147                                     "Companion object constants like ${field.name()} should be using @JvmField, not @JvmStatic; see https://developer.android.com/kotlin/interop#companion_constants"
148                                 )
149                             }
150                         }
151                     }
152                 } else if (sourcePsi is KtObjectDeclaration && sourcePsi.isCompanion()) {
153                     // We are checking if we have public properties that we can expect to be constant
154                     // (that is, declared via `val`) but that aren't declared 'const' in a companion
155                     // object that are not annotated with @JvmField or annotated with @JvmStatic
156                     // https://developer.android.com/kotlin/interop#companion_constants
157                     val ktProperties = sourcePsi.declarations.filter { declaration ->
158                         declaration is KtProperty && !declaration.isVar && !declaration.hasModifier(
159                             KtTokens.CONST_KEYWORD
160                         ) && declaration.annotationEntries.filter {
161                                 annotationEntry -> annotationEntry.shortName!!.asString() == "JvmField"
162                         }.isEmpty() }
163                     for (ktProperty in ktProperties) {
164                         if (ktProperty.annotationEntries.filter { annotationEntry -> annotationEntry.shortName!!.asString() == "JvmStatic" }.isEmpty()) {
165                             reporter.report(
166                                 Issues.MISSING_JVMSTATIC, ktProperty,
167                                 "Companion object constants like ${ktProperty.name} should be marked @JvmField for Java interoperability; see https://developer.android.com/kotlin/interop#companion_constants"
168                             )
169                         } else {
170                             reporter.report(
171                                 Issues.MISSING_JVMSTATIC, ktProperty,
172                                 "Companion object constants like ${ktProperty.name} should be using @JvmField, not @JvmStatic; see https://developer.android.com/kotlin/interop#companion_constants"
173                             )
174                         }
175                     }
176                 }
177             }
178         }
179     }
180 
181     private fun ensureLambdaLastParameter(method: MethodItem) {
182         val parameters = method.parameters()
183         if (parameters.size > 1) {
184             // Make sure that SAM-compatible parameters are last
185             val lastIndex = parameters.size - 1
186             if (!isSamCompatible(parameters[lastIndex])) {
187                 for (i in lastIndex - 1 downTo 0) {
188                     val parameter = parameters[i]
189                     if (isSamCompatible(parameter)) {
190                         val message =
191                             "${if (isKotlinLambda(parameter.type())) "lambda" else "SAM-compatible"
192                             } parameters (such as parameter ${i + 1}, \"${parameter.name()}\", in ${
193                             method.containingClass().qualifiedName()}.${method.name()
194                             }) should be last to improve Kotlin interoperability; see " +
195                                 "https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions"
196                         reporter.report(Issues.SAM_SHOULD_BE_LAST, method, message)
197                         break
198                     }
199                 }
200             }
201         }
202     }
203 
204     private fun ensureCompanionJvmStatic(method: MethodItem) {
205         if (method.containingClass().simpleName() == "Companion" && method.isKotlin() && method.modifiers.isPublic()) {
206             if (method.isKotlinProperty()) {
207                 /* Not yet working; can't find the @JvmStatic/@JvmField in the AST
208                     // Only flag the read method, not the write method
209                     if (method.name().startsWith("get")) {
210                         // Find the backing field; *that's* where the @JvmStatic/@JvmField annotations
211                         // are available (but the field itself is not visited since it is typically private
212                         // and therefore not part of the API visitor. Dip into Kotlin PSI to accurately
213                         // find the field name instead of guessing based on getter name.
214                         var field: FieldItem? = null
215                         val psi = method.psi()
216                         if (psi is KotlinUMethod) {
217                             val property = psi.sourcePsi as? KtProperty
218                             if (property != null) {
219                                 val propertyName = property.name
220                                 if (propertyName != null) {
221                                     field = method.containingClass().containingClass()?.findField(propertyName)
222                                 }
223                             }
224                         }
225 
226                         if (field != null) {
227                             if (field.modifiers.findAnnotation("kotlin.jvm.JvmStatic") != null) {
228                                 reporter.report(
229                                     Errors.MISSING_JVMSTATIC, method,
230                                     "Companion object constants should be using @JvmField, not @JvmStatic; see https://developer.android.com/kotlin/interop#companion_constants"
231                                 )
232                             } else if (field.modifiers.findAnnotation("kotlin.jvm.JvmField") == null) {
233                                 reporter.report(
234                                     Errors.MISSING_JVMSTATIC, method,
235                                     "Companion object constants should be marked @JvmField for Java interoperability; see https://developer.android.com/kotlin/interop#companion_constants"
236                                 )
237                             }
238                         }
239                     }
240                     */
241             } else if (method.modifiers.findAnnotation("kotlin.jvm.JvmStatic") == null) {
242                 reporter.report(
243                     Issues.MISSING_JVMSTATIC, method,
244                     "Companion object methods like ${method.name()} should be marked @JvmStatic for Java interoperability; see https://developer.android.com/kotlin/interop#companion_functions"
245                 )
246             }
247         }
248     }
249 
250     private fun ensureFieldNameNotKeyword(field: FieldItem) {
251         checkKotlinKeyword(field.name(), "field", field)
252     }
253 
254     private fun ensureMethodNameNotKeyword(method: MethodItem) {
255         checkKotlinKeyword(method.name(), "method", method)
256     }
257 
258     private fun ensureDefaultParamsHaveJvmOverloads(method: MethodItem) {
259         if (!method.isKotlin()) {
260             // Rule does not apply for Java, e.g. if you specify @DefaultValue
261             // in Java you still don't have the option of adding @JvmOverloads
262             return
263         }
264         if (method.containingClass().isInterface()) {
265             // '@JvmOverloads' annotation cannot be used on interface methods
266             // (https://github.com/JetBrains/kotlin/blob/dc7b1fbff946d1476cc9652710df85f65664baee/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/diagnostics/DefaultErrorMessagesJvm.java#L50)
267             return
268         }
269         val parameters = method.parameters()
270         if (parameters.size <= 1) {
271             // No need for overloads when there is at most one version...
272             return
273         }
274 
275         var haveDefault = false
276         if (parameters.isNotEmpty() && method.isJava()) {
277             // Public java parameter names should also not use Kotlin keywords as names
278             for (parameter in parameters) {
279                 if (parameter.hasDefaultValue()) {
280                     haveDefault = true
281                     break
282                 }
283             }
284         }
285 
286         if (haveDefault && method.modifiers.findAnnotation("kotlin.jvm.JvmOverloads") == null &&
287             // Extension methods and inline functions aren't really useful from Java anyway
288             !method.isExtensionMethod() && !method.modifiers.isInline()
289         ) {
290             reporter.report(
291                 Issues.MISSING_JVMSTATIC, method,
292                 "A Kotlin method with default parameter values should be annotated with @JvmOverloads for better Java interoperability; see https://android.github.io/kotlin-guides/interop.html#function-overloads-for-defaults"
293             )
294         }
295     }
296 
297     private fun ensureParameterNamesNotKeywords(method: MethodItem) {
298         val parameters = method.parameters()
299 
300         if (parameters.isNotEmpty() && method.isJava()) {
301             // Public java parameter names should also not use Kotlin keywords as names
302             for (parameter in parameters) {
303                 val publicName = parameter.publicName() ?: continue
304                 checkKotlinKeyword(publicName, "parameter", parameter)
305             }
306         }
307     }
308 
309     // Don't use Kotlin hard keywords in Java signatures
310     private fun checkKotlinKeyword(name: String, typeLabel: String, item: Item) {
311         if (isKotlinHardKeyword(name)) {
312             reporter.report(
313                 Issues.KOTLIN_KEYWORD, item,
314                 "Avoid $typeLabel names that are Kotlin hard keywords (\"$name\"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords"
315             )
316         } else if (isJavaKeyword(name)) {
317             reporter.report(
318                 Issues.KOTLIN_KEYWORD, item,
319                 "Avoid $typeLabel names that are Java keywords (\"$name\"); this makes it harder to use the API from Java"
320             )
321         }
322     }
323 
324     private fun isSamCompatible(parameter: ParameterItem): Boolean {
325         val type = parameter.type()
326         if (type.primitive) {
327             return false
328         }
329 
330         if (isKotlinLambda(type)) {
331             return true
332         }
333 
334         val cls = type.asClass() ?: return false
335         if (!cls.isInterface()) {
336             return false
337         }
338 
339         if (cls.methods().filter { !it.modifiers.isDefault() }.size != 1) {
340             return false
341         }
342 
343         if (cls.superClass()?.isInterface() == true) {
344             return false
345         }
346 
347         // Some interfaces, while they have a single method are not considered to be SAM that we
348         // want to be the last argument because often it leads to unexpected behavior of the
349         // trailing lambda.
350         when (cls.qualifiedName()) {
351             "java.util.concurrent.Executor",
352             "java.lang.Iterable" -> return false
353         }
354         return true
355     }
356 
357     private fun isKotlinLambda(type: TypeItem) =
358         type.toErasedTypeString() == "kotlin.jvm.functions.Function1"
359 
360     private fun isKotlinHardKeyword(keyword: String): Boolean {
361         // From https://github.com/JetBrains/kotlin/blob/master/core/descriptors/src/org/jetbrains/kotlin/renderer/KeywordStringsGenerated.java
362         when (keyword) {
363             "as",
364             "break",
365             "class",
366             "continue",
367             "do",
368             "else",
369             "false",
370             "for",
371             "fun",
372             "if",
373             "in",
374             "interface",
375             "is",
376             "null",
377             "object",
378             "package",
379             "return",
380             "super",
381             "this",
382             "throw",
383             "true",
384             "try",
385             "typealias",
386             "typeof",
387             "val",
388             "var",
389             "when",
390             "while"
391             -> return true
392         }
393 
394         return false
395     }
396 
397     /** Returns true if the given string is a reserved Java keyword  */
398     private fun isJavaKeyword(keyword: String): Boolean {
399         return JavaLexer.isKeyword(keyword, options.javaLanguageLevel)
400     }
401 }
402