• 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.model.ClassItem
20 import com.android.tools.metalava.model.FieldItem
21 import com.android.tools.metalava.model.Item
22 import com.android.tools.metalava.model.JVM_STATIC
23 import com.android.tools.metalava.model.MethodItem
24 import com.android.tools.metalava.model.ParameterItem
25 import com.android.tools.metalava.model.PropertyItem
26 import com.android.tools.metalava.model.psi.PsiEnvironmentManager
27 import com.android.tools.metalava.reporter.Issues
28 import com.android.tools.metalava.reporter.Reporter
29 import com.intellij.psi.util.PsiUtil
30 
31 // Enforces the interoperability guidelines outlined in
32 //   https://android.github.io/kotlin-guides/interop.html
33 //
34 // Also potentially makes other API suggestions.
35 class KotlinInteropChecks(val reporter: Reporter) {
36 
37     @Suppress("DEPRECATION")
38     private val javaLanguageLevel =
39         PsiEnvironmentManager.javaLanguageLevelFromString(options.javaLanguageLevelAsString)
40 
checkFieldnull41     fun checkField(field: FieldItem, isKotlin: Boolean = field.isKotlin()) {
42         ensureFieldNameNotKeyword(field)
43     }
44 
checkMethodnull45     fun checkMethod(method: MethodItem, isKotlin: Boolean = method.isKotlin()) {
46         if (isKotlin) {
47             ensureDefaultParamsHaveJvmOverloads(method)
48             ensureCompanionJvmStatic(method)
49             ensureExceptionsDocumented(method)
50         } else {
51             ensureMethodNameNotKeyword(method)
52             ensureParameterNamesNotKeywords(method)
53             ensureLambdaLastParameter(method)
54         }
55     }
56 
checkClassnull57     fun checkClass(cls: ClassItem, isKotlin: Boolean = cls.isKotlin()) {
58         if (isKotlin) {
59             disallowValueClasses(cls)
60         }
61     }
62 
checkPropertynull63     fun checkProperty(property: PropertyItem) {
64         ensureCompanionJvmField(property)
65     }
66 
ensureExceptionsDocumentednull67     private fun ensureExceptionsDocumented(method: MethodItem) {
68         if (!method.isKotlin()) {
69             return
70         }
71 
72         val exceptions = method.body.findThrownExceptions()
73         if (exceptions.isEmpty()) {
74             return
75         }
76         val doc =
77             method.documentation.text.ifEmpty { method.property?.documentation?.text.orEmpty() }
78         for (exception in exceptions.sortedBy { it.qualifiedName() }) {
79             val checked =
80                 !(exception.extends("java.lang.RuntimeException") ||
81                     exception.extends("java.lang.Error"))
82             if (checked) {
83                 val annotation = method.modifiers.findAnnotation("kotlin.jvm.Throws")
84                 if (annotation != null) {
85                     // There can be multiple values
86                     for (attribute in annotation.attributes) {
87                         for (v in attribute.leafValues()) {
88                             val source = v.toSource()
89                             if (source.endsWith(exception.simpleName() + "::class")) {
90                                 return
91                             }
92                         }
93                     }
94                 }
95                 reporter.report(
96                     Issues.DOCUMENT_EXCEPTIONS,
97                     method,
98                     "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"
99                 )
100             } else {
101                 if (!doc.contains(exception.simpleName())) {
102                     reporter.report(
103                         Issues.DOCUMENT_EXCEPTIONS,
104                         method,
105                         "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"
106                     )
107                 }
108             }
109         }
110     }
111 
ensureLambdaLastParameternull112     private fun ensureLambdaLastParameter(method: MethodItem) {
113         val parameters = method.parameters()
114         if (parameters.size > 1) {
115             // Make sure that SAM-compatible parameters are last
116             val lastIndex = parameters.size - 1
117             if (!isSamCompatible(parameters[lastIndex])) {
118                 for (i in lastIndex - 1 downTo 0) {
119                     val parameter = parameters[i]
120                     if (isSamCompatible(parameter)) {
121                         val message =
122                             "SAM-compatible parameters (such as parameter ${i + 1}, " +
123                                 "\"${parameter.name()}\", in ${
124                                 method.containingClass().qualifiedName()}.${method.name()
125                                 }) should be last to improve Kotlin interoperability; see " +
126                                 "https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions"
127                         reporter.report(Issues.SAM_SHOULD_BE_LAST, method, message)
128                         break
129                     }
130                 }
131             }
132         }
133     }
134 
ensureCompanionJvmStaticnull135     private fun ensureCompanionJvmStatic(method: MethodItem) {
136         if (
137             method.containingClass().simpleName() == "Companion" &&
138                 // Many properties will be checked through [ensureCompanionJvmField]. If this method
139                 // is not a property or its property can't use @JvmField, it should use @JvmStatic.
140                 method.property?.canHaveJvmField() != true &&
141                 method.modifiers.findAnnotation(JVM_STATIC) == null &&
142                 method.property?.modifiers?.findAnnotation(JVM_STATIC) == null
143         ) {
144             reporter.report(
145                 Issues.MISSING_JVMSTATIC,
146                 method,
147                 "Companion object methods like ${method.name()} should be marked @JvmStatic for Java interoperability; see https://developer.android.com/kotlin/interop#companion_functions"
148             )
149         }
150     }
151 
152     /**
153      * Warn if companion constants are not marked with @JvmField.
154      *
155      * Properties that we can expect to be constant (that is, declared via `val`, so they don't have
156      * a setter) but that aren't declared 'const' in a companion object should have @JvmField, and
157      * not have @JvmStatic.
158      *
159      * See https://developer.android.com/kotlin/interop#companion_constants
160      */
ensureCompanionJvmFieldnull161     private fun ensureCompanionJvmField(property: PropertyItem) {
162         if (property.containingClass().modifiers.isCompanion() && property.canHaveJvmField()) {
163             if (property.modifiers.findAnnotation(JVM_STATIC) != null) {
164                 reporter.report(
165                     Issues.MISSING_JVMSTATIC,
166                     property,
167                     "Companion object constants like ${property.name()} should be using @JvmField, not @JvmStatic; see https://developer.android.com/kotlin/interop#companion_constants"
168                 )
169             } else if (property.modifiers.findAnnotation("kotlin.jvm.JvmField") == null) {
170                 reporter.report(
171                     Issues.MISSING_JVMSTATIC,
172                     property,
173                     "Companion object constants like ${property.name()} should be marked @JvmField for Java interoperability; see https://developer.android.com/kotlin/interop#companion_constants"
174                 )
175             }
176         }
177     }
178 
179     /**
180      * Whether the property (assumed to be a companion property) is allowed to be have @JvmField.
181      *
182      * If it can't be annotated with @JvmField, it should use @JvmStatic for its accessors instead.
183      */
PropertyItemnull184     private fun PropertyItem.canHaveJvmField(): Boolean {
185         val companionContainer = containingClass().containingClass()
186         return !modifiers.isConst() &&
187             setter == null &&
188             // @JvmField can only be used on interface companion properties in limited situations --
189             // all the companion properties must be public and constant, so adding more properties
190             // might mean @JvmField would no longer be allowed even if it was originally. Because of
191             // this, don't suggest using @JvmField for interface companion properties.
192             // https://github.com/Kotlin/KEEP/blob/master/proposals/jvm-field-annotation-in-interface-companion.md
193             containingClass().containingClass()?.isInterface() != true &&
194             // @JvmField can only be used when the property has a backing field. The backing
195             // field is present on the containing class of the companion.
196             companionContainer?.findField(name()) != null &&
197             // The compiler does not allow @JvmField on value class type properties.
198             !type().isValueClassType()
199     }
200 
ensureFieldNameNotKeywordnull201     private fun ensureFieldNameNotKeyword(field: FieldItem) {
202         checkKotlinKeyword(field.name(), "field", field)
203     }
204 
ensureMethodNameNotKeywordnull205     private fun ensureMethodNameNotKeyword(method: MethodItem) {
206         checkKotlinKeyword(method.name(), "method", method)
207     }
208 
ensureDefaultParamsHaveJvmOverloadsnull209     private fun ensureDefaultParamsHaveJvmOverloads(method: MethodItem) {
210         if (!method.isKotlin()) {
211             // Rule does not apply for Java, e.g. if you specify @DefaultValue
212             // in Java you still don't have the option of adding @JvmOverloads
213             return
214         }
215         if (method.containingClass().isInterface()) {
216             // '@JvmOverloads' annotation cannot be used on interface methods
217             // (https://github.com/JetBrains/kotlin/blob/dc7b1fbff946d1476cc9652710df85f65664baee/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/diagnostics/DefaultErrorMessagesJvm.java#L50)
218             return
219         }
220         val parameters = method.parameters()
221         if (parameters.size <= 1) {
222             // No need for overloads when there is at most one version...
223             return
224         }
225 
226         var haveDefault = false
227         for (parameter in parameters) {
228             if (parameter.hasDefaultValue()) {
229                 haveDefault = true
230                 break
231             }
232         }
233 
234         if (
235             haveDefault &&
236                 method.modifiers.findAnnotation("kotlin.jvm.JvmOverloads") == null &&
237                 // Extension methods and inline functions aren't really useful from Java anyway
238                 !method.isExtensionMethod() &&
239                 !method.modifiers.isInline() &&
240                 // Methods marked @JvmSynthetic are hidden from java, overloads not useful
241                 !method.modifiers.hasJvmSyntheticAnnotation()
242         ) {
243             reporter.report(
244                 Issues.MISSING_JVMSTATIC,
245                 method,
246                 "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"
247             )
248         }
249     }
250 
ensureParameterNamesNotKeywordsnull251     private fun ensureParameterNamesNotKeywords(method: MethodItem) {
252         val parameters = method.parameters()
253 
254         if (parameters.isNotEmpty() && method.isJava()) {
255             // Public java parameter names should also not use Kotlin keywords as names
256             for (parameter in parameters) {
257                 val publicName = parameter.publicName() ?: continue
258                 checkKotlinKeyword(publicName, "parameter", parameter)
259             }
260         }
261     }
262 
263     // Don't use Kotlin hard keywords in Java signatures
checkKotlinKeywordnull264     private fun checkKotlinKeyword(name: String, typeLabel: String, item: Item) {
265         if (isKotlinHardKeyword(name)) {
266             reporter.report(
267                 Issues.KOTLIN_KEYWORD,
268                 item,
269                 "Avoid $typeLabel names that are Kotlin hard keywords (\"$name\"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords"
270             )
271         } else if (isJavaKeyword(name)) {
272             reporter.report(
273                 Issues.KOTLIN_KEYWORD,
274                 item,
275                 "Avoid $typeLabel names that are Java keywords (\"$name\"); this makes it harder to use the API from Java"
276             )
277         }
278     }
279 
280     /**
281      * @return whether [parameter] can be invoked by Kotlin callers using SAM conversion. This does
282      *   not check TextParameterItem, as there is missing metadata (such as whether the type is
283      *   defined in Kotlin source or not, which can affect SAM conversion).
284      */
isSamCompatiblenull285     private fun isSamCompatible(parameter: ParameterItem): Boolean {
286         val cls = parameter.type().asClass()
287         // Some interfaces, while they have a single method are not considered to be SAM that we
288         // want to be the last argument because often it leads to unexpected behavior of the
289         // trailing lambda.
290         when (cls?.qualifiedName()) {
291             "java.util.concurrent.Executor",
292             "java.lang.Iterable" -> return false
293         }
294 
295         return parameter.isSamCompatibleOrKotlinLambda()
296     }
297 
disallowValueClassesnull298     private fun disallowValueClasses(cls: ClassItem) {
299         if (cls.modifiers.isValue()) {
300             reporter.report(
301                 Issues.VALUE_CLASS_DEFINITION,
302                 cls,
303                 "Value classes should not be public in APIs targeting Java clients."
304             )
305         }
306     }
307 
isKotlinHardKeywordnull308     private fun isKotlinHardKeyword(keyword: String): Boolean {
309         // From
310         // https://github.com/JetBrains/kotlin/blob/master/core/descriptors/src/org/jetbrains/kotlin/renderer/KeywordStringsGenerated.java
311         when (keyword) {
312             "as",
313             "break",
314             "class",
315             "continue",
316             "do",
317             "else",
318             "false",
319             "for",
320             "fun",
321             "if",
322             "in",
323             "interface",
324             "is",
325             "null",
326             "object",
327             "package",
328             "return",
329             "super",
330             "this",
331             "throw",
332             "true",
333             "try",
334             "typealias",
335             "typeof",
336             "val",
337             "var",
338             "when",
339             "while" -> return true
340         }
341 
342         return false
343     }
344 
345     /** Returns true if the given string is a reserved Java keyword */
isJavaKeywordnull346     private fun isJavaKeyword(keyword: String): Boolean {
347         return PsiUtil.isKeyword(keyword, javaLanguageLevel)
348     }
349 }
350