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