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