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