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