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