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