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