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