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