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