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