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.resources.ResourceType 20 import com.android.resources.ResourceType.AAPT 21 import com.android.resources.ResourceType.ANIM 22 import com.android.resources.ResourceType.ANIMATOR 23 import com.android.resources.ResourceType.ARRAY 24 import com.android.resources.ResourceType.ATTR 25 import com.android.resources.ResourceType.BOOL 26 import com.android.resources.ResourceType.COLOR 27 import com.android.resources.ResourceType.DIMEN 28 import com.android.resources.ResourceType.DRAWABLE 29 import com.android.resources.ResourceType.FONT 30 import com.android.resources.ResourceType.FRACTION 31 import com.android.resources.ResourceType.ID 32 import com.android.resources.ResourceType.INTEGER 33 import com.android.resources.ResourceType.INTERPOLATOR 34 import com.android.resources.ResourceType.LAYOUT 35 import com.android.resources.ResourceType.MENU 36 import com.android.resources.ResourceType.MIPMAP 37 import com.android.resources.ResourceType.NAVIGATION 38 import com.android.resources.ResourceType.PLURALS 39 import com.android.resources.ResourceType.PUBLIC 40 import com.android.resources.ResourceType.RAW 41 import com.android.resources.ResourceType.SAMPLE_DATA 42 import com.android.resources.ResourceType.STRING 43 import com.android.resources.ResourceType.STYLE 44 import com.android.resources.ResourceType.STYLEABLE 45 import com.android.resources.ResourceType.STYLE_ITEM 46 import com.android.resources.ResourceType.TRANSITION 47 import com.android.resources.ResourceType.XML 48 import com.android.sdklib.SdkVersionInfo 49 import com.android.tools.metalava.Issues.ABSTRACT_INNER 50 import com.android.tools.metalava.Issues.ACRONYM_NAME 51 import com.android.tools.metalava.Issues.ACTION_VALUE 52 import com.android.tools.metalava.Issues.ALL_UPPER 53 import com.android.tools.metalava.Issues.ANDROID_URI 54 import com.android.tools.metalava.Issues.ARRAY_RETURN 55 import com.android.tools.metalava.Issues.ASYNC_SUFFIX_FUTURE 56 import com.android.tools.metalava.Issues.AUTO_BOXING 57 import com.android.tools.metalava.Issues.BAD_FUTURE 58 import com.android.tools.metalava.Issues.BANNED_THROW 59 import com.android.tools.metalava.Issues.BUILDER_SET_STYLE 60 import com.android.tools.metalava.Issues.CALLBACK_INTERFACE 61 import com.android.tools.metalava.Issues.CALLBACK_METHOD_NAME 62 import com.android.tools.metalava.Issues.CALLBACK_NAME 63 import com.android.tools.metalava.Issues.COMMON_ARGS_FIRST 64 import com.android.tools.metalava.Issues.COMPILE_TIME_CONSTANT 65 import com.android.tools.metalava.Issues.CONCRETE_COLLECTION 66 import com.android.tools.metalava.Issues.CONFIG_FIELD_NAME 67 import com.android.tools.metalava.Issues.CONSISTENT_ARGUMENT_ORDER 68 import com.android.tools.metalava.Issues.CONTEXT_FIRST 69 import com.android.tools.metalava.Issues.CONTEXT_NAME_SUFFIX 70 import com.android.tools.metalava.Issues.ENDS_WITH_IMPL 71 import com.android.tools.metalava.Issues.ENUM 72 import com.android.tools.metalava.Issues.EQUALS_AND_HASH_CODE 73 import com.android.tools.metalava.Issues.EXCEPTION_NAME 74 import com.android.tools.metalava.Issues.EXECUTOR_REGISTRATION 75 import com.android.tools.metalava.Issues.EXTENDS_ERROR 76 import com.android.tools.metalava.Issues.FORBIDDEN_SUPER_CLASS 77 import com.android.tools.metalava.Issues.FRACTION_FLOAT 78 import com.android.tools.metalava.Issues.GENERIC_CALLBACKS 79 import com.android.tools.metalava.Issues.GENERIC_EXCEPTION 80 import com.android.tools.metalava.Issues.GETTER_ON_BUILDER 81 import com.android.tools.metalava.Issues.GETTER_SETTER_NAMES 82 import com.android.tools.metalava.Issues.HEAVY_BIT_SET 83 import com.android.tools.metalava.Issues.INTENT_BUILDER_NAME 84 import com.android.tools.metalava.Issues.INTENT_NAME 85 import com.android.tools.metalava.Issues.INTERFACE_CONSTANT 86 import com.android.tools.metalava.Issues.INTERNAL_CLASSES 87 import com.android.tools.metalava.Issues.INTERNAL_FIELD 88 import com.android.tools.metalava.Issues.INVALID_NULLABILITY_OVERRIDE 89 import com.android.tools.metalava.Issues.Issue 90 import com.android.tools.metalava.Issues.KOTLIN_OPERATOR 91 import com.android.tools.metalava.Issues.LISTENER_INTERFACE 92 import com.android.tools.metalava.Issues.LISTENER_LAST 93 import com.android.tools.metalava.Issues.MANAGER_CONSTRUCTOR 94 import com.android.tools.metalava.Issues.MANAGER_LOOKUP 95 import com.android.tools.metalava.Issues.MENTIONS_GOOGLE 96 import com.android.tools.metalava.Issues.METHOD_NAME_TENSE 97 import com.android.tools.metalava.Issues.METHOD_NAME_UNITS 98 import com.android.tools.metalava.Issues.MIN_MAX_CONSTANT 99 import com.android.tools.metalava.Issues.MISSING_BUILD_METHOD 100 import com.android.tools.metalava.Issues.MISSING_GETTER_MATCHING_BUILDER 101 import com.android.tools.metalava.Issues.MISSING_NULLABILITY 102 import com.android.tools.metalava.Issues.MUTABLE_BARE_FIELD 103 import com.android.tools.metalava.Issues.NOT_CLOSEABLE 104 import com.android.tools.metalava.Issues.NO_BYTE_OR_SHORT 105 import com.android.tools.metalava.Issues.NO_CLONE 106 import com.android.tools.metalava.Issues.NO_SETTINGS_PROVIDER 107 import com.android.tools.metalava.Issues.NULLABLE_COLLECTION 108 import com.android.tools.metalava.Issues.ON_NAME_EXPECTED 109 import com.android.tools.metalava.Issues.OPTIONAL_BUILDER_CONSTRUCTOR_ARGUMENT 110 import com.android.tools.metalava.Issues.OVERLAPPING_CONSTANTS 111 import com.android.tools.metalava.Issues.PACKAGE_LAYERING 112 import com.android.tools.metalava.Issues.PAIRED_REGISTRATION 113 import com.android.tools.metalava.Issues.PARCELABLE_LIST 114 import com.android.tools.metalava.Issues.PARCEL_CONSTRUCTOR 115 import com.android.tools.metalava.Issues.PARCEL_CREATOR 116 import com.android.tools.metalava.Issues.PARCEL_NOT_FINAL 117 import com.android.tools.metalava.Issues.PERCENTAGE_INT 118 import com.android.tools.metalava.Issues.PROTECTED_MEMBER 119 import com.android.tools.metalava.Issues.PUBLIC_TYPEDEF 120 import com.android.tools.metalava.Issues.RAW_AIDL 121 import com.android.tools.metalava.Issues.REGISTRATION_NAME 122 import com.android.tools.metalava.Issues.RESOURCE_FIELD_NAME 123 import com.android.tools.metalava.Issues.RESOURCE_STYLE_FIELD_NAME 124 import com.android.tools.metalava.Issues.RESOURCE_VALUE_FIELD_NAME 125 import com.android.tools.metalava.Issues.RETHROW_REMOTE_EXCEPTION 126 import com.android.tools.metalava.Issues.SERVICE_NAME 127 import com.android.tools.metalava.Issues.SETTER_RETURNS_THIS 128 import com.android.tools.metalava.Issues.SINGLETON_CONSTRUCTOR 129 import com.android.tools.metalava.Issues.SINGLE_METHOD_INTERFACE 130 import com.android.tools.metalava.Issues.SINGULAR_CALLBACK 131 import com.android.tools.metalava.Issues.START_WITH_LOWER 132 import com.android.tools.metalava.Issues.START_WITH_UPPER 133 import com.android.tools.metalava.Issues.STATIC_FINAL_BUILDER 134 import com.android.tools.metalava.Issues.STATIC_UTILS 135 import com.android.tools.metalava.Issues.STREAM_FILES 136 import com.android.tools.metalava.Issues.TOP_LEVEL_BUILDER 137 import com.android.tools.metalava.Issues.UNIQUE_KOTLIN_OPERATOR 138 import com.android.tools.metalava.Issues.USER_HANDLE 139 import com.android.tools.metalava.Issues.USER_HANDLE_NAME 140 import com.android.tools.metalava.Issues.USE_ICU 141 import com.android.tools.metalava.Issues.USE_PARCEL_FILE_DESCRIPTOR 142 import com.android.tools.metalava.Issues.VISIBLY_SYNCHRONIZED 143 import com.android.tools.metalava.model.AnnotationItem 144 import com.android.tools.metalava.model.AnnotationItem.Companion.getImplicitNullness 145 import com.android.tools.metalava.model.ClassItem 146 import com.android.tools.metalava.model.Codebase 147 import com.android.tools.metalava.model.ConstructorItem 148 import com.android.tools.metalava.model.FieldItem 149 import com.android.tools.metalava.model.Item 150 import com.android.tools.metalava.model.MemberItem 151 import com.android.tools.metalava.model.MethodItem 152 import com.android.tools.metalava.model.PackageItem 153 import com.android.tools.metalava.model.ParameterItem 154 import com.android.tools.metalava.model.SetMinSdkVersion 155 import com.android.tools.metalava.model.TypeItem 156 import com.android.tools.metalava.model.psi.PsiMethodItem 157 import com.android.tools.metalava.model.psi.PsiTypeItem 158 import com.android.tools.metalava.model.visitors.ApiVisitor 159 import com.intellij.psi.JavaRecursiveElementVisitor 160 import com.intellij.psi.PsiClassObjectAccessExpression 161 import com.intellij.psi.PsiElement 162 import com.intellij.psi.PsiSynchronizedStatement 163 import com.intellij.psi.PsiThisExpression 164 import org.jetbrains.uast.UCallExpression 165 import org.jetbrains.uast.UClassLiteralExpression 166 import org.jetbrains.uast.UMethod 167 import org.jetbrains.uast.UQualifiedReferenceExpression 168 import org.jetbrains.uast.UThisExpression 169 import org.jetbrains.uast.visitor.AbstractUastVisitor 170 import java.util.Locale 171 import java.util.function.Predicate 172 173 /** 174 * The [ApiLint] analyzer checks the API against a known set of preferred API practices 175 * by the Android API council. 176 */ 177 class ApiLint(private val codebase: Codebase, private val oldCodebase: Codebase?, private val reporter: Reporter) : ApiVisitor( 178 // Sort by source order such that warnings follow source line number order 179 methodComparator = MethodItem.sourceOrderComparator, 180 fieldComparator = FieldItem.comparator, 181 ignoreShown = options.showUnannotated, 182 // No need to check "for stubs only APIs" (== "implicit" APIs) 183 includeApisForStubPurposes = false 184 ) { 185 private fun report(id: Issue, item: Item, message: String, element: PsiElement? = null) { 186 // Don't flag api warnings on deprecated APIs; these are obviously already known to 187 // be problematic. 188 if (item.deprecated) { 189 return 190 } 191 192 if (item is ParameterItem && item.containingMethod().deprecated) { 193 return 194 } 195 196 // With show annotations we might be flagging API that is filtered out: hide these here 197 val testItem = if (item is ParameterItem) item.containingMethod() else item 198 if (!filterEmit.test(testItem)) { 199 return 200 } 201 202 reporter.report(id, item, message, element) 203 } 204 205 private fun check() { 206 if (oldCodebase != null) { 207 // Only check the new APIs 208 CodebaseComparator().compare( 209 object : ComparisonVisitor() { 210 override fun added(new: Item) { 211 new.accept(this@ApiLint) 212 } 213 }, 214 oldCodebase, codebase, filterReference 215 ) 216 } else { 217 // No previous codebase to compare with: visit the whole thing 218 codebase.accept(this) 219 } 220 } 221 222 override fun skip(item: Item): Boolean { 223 return super.skip(item) || 224 item is ClassItem && !isInteresting(item) || 225 item is MethodItem && !isInteresting(item.containingClass()) || 226 item is FieldItem && !isInteresting(item.containingClass()) 227 } 228 229 private val kotlinInterop = KotlinInteropChecks(reporter) 230 231 override fun visitClass(cls: ClassItem) { 232 val methods = cls.filteredMethods(filterReference).asSequence() 233 val fields = cls.filteredFields(filterReference, showUnannotated).asSequence() 234 val constructors = cls.filteredConstructors(filterReference) 235 val superClass = cls.filteredSuperclass(filterReference) 236 val interfaces = cls.filteredInterfaceTypes(filterReference).asSequence() 237 val allMethods = methods.asSequence() + constructors.asSequence() 238 checkClass(cls, methods, constructors, allMethods, fields, superClass, interfaces) 239 } 240 241 override fun visitMethod(method: MethodItem) { 242 checkMethod(method, filterReference) 243 val returnType = method.returnType() 244 if (returnType != null) { 245 checkType(returnType, method) 246 checkNullableCollections(returnType, method) 247 checkMethodSuffixListenableFutureReturn(returnType, method) 248 } 249 for (parameter in method.parameters()) { 250 checkType(parameter.type(), parameter) 251 } 252 kotlinInterop.checkMethod(method) 253 } 254 255 override fun visitField(field: FieldItem) { 256 checkField(field) 257 checkType(field.type(), field) 258 kotlinInterop.checkField(field) 259 } 260 261 private fun checkType(type: TypeItem, item: Item) { 262 val typeString = type.toTypeString() 263 checkPfd(typeString, item) 264 checkNumbers(typeString, item) 265 checkCollections(type, item) 266 checkCollectionsOverArrays(type, typeString, item) 267 checkBoxed(type, item) 268 checkIcu(type, typeString, item) 269 checkBitSet(type, typeString, item) 270 checkHasNullability(item) 271 checkUri(typeString, item) 272 checkFutures(typeString, item) 273 } 274 275 private fun checkClass( 276 cls: ClassItem, 277 methods: Sequence<MethodItem>, 278 constructors: Sequence<ConstructorItem>, 279 methodsAndConstructors: Sequence<MethodItem>, 280 fields: Sequence<FieldItem>, 281 superClass: ClassItem?, 282 interfaces: Sequence<TypeItem> 283 ) { 284 checkEquals(methods) 285 checkEnums(cls) 286 checkClassNames(cls) 287 checkCallbacks(cls) 288 checkListeners(cls, methods) 289 checkParcelable(cls, methods, constructors, fields) 290 checkRegistrationMethods(cls, methods) 291 checkHelperClasses(cls, methods, fields) 292 checkBuilder(cls, methods, constructors, superClass) 293 checkAidl(cls, superClass, interfaces) 294 checkInternal(cls) 295 checkLayering(cls, methodsAndConstructors, fields) 296 checkBooleans(methods) 297 checkFlags(fields) 298 checkGoogle(cls, methods, fields) 299 checkManager(cls, methods, constructors) 300 checkStaticUtils(cls, methods, constructors, fields) 301 checkCallbackHandlers(cls, methodsAndConstructors, superClass) 302 checkGenericCallbacks(cls, methods, constructors, fields) 303 checkResourceNames(cls, fields) 304 checkFiles(methodsAndConstructors) 305 checkManagerList(cls, methods) 306 checkAbstractInner(cls) 307 checkError(cls, superClass) 308 checkCloseable(cls, methods) 309 checkNotKotlinOperator(methods) 310 checkUserHandle(cls, methods) 311 checkParams(cls) 312 checkSingleton(cls, methods, constructors) 313 checkExtends(cls) 314 checkTypedef(cls) 315 316 // TODO: Not yet working 317 // checkOverloadArgs(cls, methods) 318 } 319 320 private fun checkField( 321 field: FieldItem 322 ) { 323 val modifiers = field.modifiers 324 if (modifiers.isStatic() && modifiers.isFinal()) { 325 checkConstantNames(field) 326 checkActions(field) 327 checkIntentExtras(field) 328 } 329 checkProtected(field) 330 checkServices(field) 331 checkFieldName(field) 332 checkSettingKeys(field) 333 checkNullableCollections(field.type(), field) 334 } 335 336 private fun checkMethod( 337 method: MethodItem, 338 filterReference: Predicate<Item> 339 ) { 340 if (!method.isConstructor()) { 341 checkMethodNames(method) 342 checkProtected(method) 343 checkSynchronized(method) 344 checkIntentBuilder(method) 345 checkUnits(method) 346 checkTense(method) 347 checkClone(method) 348 checkCallbackOrListenerMethod(method) 349 } 350 checkExceptions(method, filterReference) 351 checkContextFirst(method) 352 checkListenerLast(method) 353 } 354 355 private fun checkEnums(cls: ClassItem) { 356 if (cls.isEnum()) { 357 report(ENUM, cls, "Enums are discouraged in Android APIs") 358 } 359 } 360 361 private fun checkMethodNames(method: MethodItem) { 362 // Existing violations 363 val containing = method.containingClass().qualifiedName() 364 if (containing.startsWith("android.opengl") || 365 containing.startsWith("android.renderscript") || 366 containing.startsWith("android.database.sqlite.") || 367 containing == "android.system.OsConstants" 368 ) { 369 return 370 } 371 372 val name = if (method.isKotlin() && method.name().contains("-")) { 373 // Kotlin renames certain methods in binary, e.g. fun foo(bar: Bar) where Bar is an 374 // inline class becomes foo-HASHCODE. We only want to consider the original name for 375 // this API lint check 376 method.name().substringBefore("-") 377 } else { 378 method.name() 379 } 380 val first = name[0] 381 382 when { 383 first !in 'a'..'z' -> report(START_WITH_LOWER, method, "Method name must start with lowercase char: $name") 384 hasAcronyms(name) -> { 385 report( 386 ACRONYM_NAME, method, 387 "Acronyms should not be capitalized in method names: was `$name`, should this be `${decapitalizeAcronyms( 388 name 389 )}`?" 390 ) 391 } 392 } 393 } 394 395 private fun checkClassNames(cls: ClassItem) { 396 // Existing violations 397 val qualifiedName = cls.qualifiedName() 398 if (qualifiedName.startsWith("android.opengl") || 399 qualifiedName.startsWith("android.renderscript") || 400 qualifiedName.startsWith("android.database.sqlite.") || 401 qualifiedName.startsWith("android.R.") 402 ) { 403 return 404 } 405 406 val name = cls.simpleName() 407 val first = name[0] 408 when { 409 first !in 'A'..'Z' -> { 410 report( 411 START_WITH_UPPER, cls, 412 "Class must start with uppercase char: $name" 413 ) 414 } 415 hasAcronyms(name) -> { 416 report( 417 ACRONYM_NAME, cls, 418 "Acronyms should not be capitalized in class names: was `$name`, should this be `${decapitalizeAcronyms( 419 name 420 )}`?" 421 ) 422 } 423 name.endsWith("Impl") -> { 424 report( 425 ENDS_WITH_IMPL, cls, 426 "Don't expose your implementation details: `$name` ends with `Impl`" 427 ) 428 } 429 } 430 } 431 432 private fun checkConstantNames(field: FieldItem) { 433 // Skip this check on Kotlin 434 if (field.isKotlin()) { 435 return 436 } 437 438 // Existing violations 439 val qualified = field.containingClass().qualifiedName() 440 if (qualified.startsWith("android.os.Build") || 441 qualified == "android.system.OsConstants" || 442 qualified == "android.media.MediaCodecInfo" || 443 qualified.startsWith("android.opengl.") || 444 qualified.startsWith("android.R.") 445 ) { 446 return 447 } 448 449 val name = field.name() 450 if (!constantNamePattern.matches(name)) { 451 val suggested = SdkVersionInfo.camelCaseToUnderlines(name).uppercase(Locale.US) 452 report( 453 ALL_UPPER, field, 454 "Constant field names must be named with only upper case characters: `$qualified#$name`, should be `$suggested`?" 455 ) 456 } else if ((name.startsWith("MIN_") || name.startsWith("MAX_")) && !field.type().isString()) { 457 report( 458 MIN_MAX_CONSTANT, field, 459 "If min/max could change in future, make them dynamic methods: $qualified#$name" 460 ) 461 } else if ((field.type().primitive || field.type().isString()) && field.initialValue(true) == null) { 462 report( 463 COMPILE_TIME_CONSTANT, field, 464 "All constants must be defined at compile time: $qualified#$name" 465 ) 466 } 467 } 468 469 private fun checkCallbacks(cls: ClassItem) { 470 // Existing violations 471 val qualified = cls.qualifiedName() 472 if (qualified == "android.speech.tts.SynthesisCallback") { 473 return 474 } 475 476 val name = cls.simpleName() 477 when { 478 name.endsWith("Callbacks") -> { 479 report( 480 SINGULAR_CALLBACK, cls, 481 "Callback class names should be singular: $name" 482 ) 483 } 484 name.endsWith("Observer") -> { 485 val prefix = name.removeSuffix("Observer") 486 report( 487 CALLBACK_NAME, cls, 488 "Class should be named ${prefix}Callback" 489 ) 490 } 491 name.endsWith("Callback") -> { 492 if (cls.isInterface()) { 493 report( 494 CALLBACK_INTERFACE, cls, 495 "Callbacks must be abstract class instead of interface to enable extension in future API levels: $name" 496 ) 497 } 498 } 499 } 500 } 501 502 private fun checkCallbackOrListenerMethod(method: MethodItem) { 503 if (method.isConstructor() || method.modifiers.isStatic() || method.modifiers.isFinal()) { 504 return 505 } 506 val cls = method.containingClass() 507 508 // These are not listeners or callbacks despite their name. 509 when { 510 cls.modifiers.isFinal() -> return 511 cls.qualifiedName() == "android.telephony.ims.ImsCallSessionListener" -> return 512 } 513 514 val containingClassSimpleName = cls.simpleName() 515 val kind = when { 516 containingClassSimpleName.endsWith("Callback") -> "Callback" 517 containingClassSimpleName.endsWith("Listener") -> "Listener" 518 else -> return 519 } 520 val methodName = method.name() 521 522 if (!onCallbackNamePattern.matches(methodName)) { 523 report( 524 CALLBACK_METHOD_NAME, method, 525 "$kind method names must follow the on<Something> style: $methodName" 526 ) 527 } 528 529 for (parameter in method.parameters()) { 530 // We require nonnull collections as parameters to callback methods 531 checkNullableCollections(parameter.type(), parameter) 532 } 533 } 534 535 private fun checkListeners(cls: ClassItem, methods: Sequence<MethodItem>) { 536 val name = cls.simpleName() 537 if (name.endsWith("Listener")) { 538 if (cls.isClass()) { 539 report( 540 LISTENER_INTERFACE, cls, 541 "Listeners should be an interface, or otherwise renamed Callback: $name" 542 ) 543 } else { 544 if (methods.count() == 1) { 545 val method = methods.first() 546 val methodName = method.name() 547 if (methodName.startsWith("On") && 548 !("${methodName}Listener").equals(cls.simpleName(), ignoreCase = true) 549 ) { 550 report( 551 SINGLE_METHOD_INTERFACE, cls, 552 "Single listener method name must match class name" 553 ) 554 } 555 } 556 } 557 } 558 } 559 560 private fun checkGenericCallbacks( 561 cls: ClassItem, 562 methods: Sequence<MethodItem>, 563 constructors: Sequence<ConstructorItem>, 564 fields: Sequence<FieldItem> 565 ) { 566 val simpleName = cls.simpleName() 567 if (!simpleName.endsWith("Callback") && !simpleName.endsWith("Listener")) return 568 569 // The following checks for an interface or abstract class of the same shape as 570 // OutcomeReceiver, i.e. two methods, both with the "on" prefix for callbacks, one of 571 // them taking a Throwable or subclass. 572 if (constructors.any { !it.isImplicitConstructor() }) return 573 if (fields.any()) return 574 if (methods.count() != 2) return 575 576 fun isSingleParamCallbackMethod(method: MethodItem) = 577 method.parameters().size == 1 && 578 method.name().startsWith("on") && 579 !method.parameters().first().type().primitive && 580 method.returnType()?.toTypeString() == Void.TYPE.name 581 582 if (!methods.all(::isSingleParamCallbackMethod)) return 583 584 fun TypeItem.extendsThrowable() = asClass()?.extends(JAVA_LANG_THROWABLE) ?: false 585 fun isErrorMethod(method: MethodItem) = 586 method.name().run { startsWith("onError") || startsWith("onFail") } && 587 method.parameters().first().type().extendsThrowable() 588 589 if (methods.count(::isErrorMethod) == 1) { 590 report( 591 GENERIC_CALLBACKS, 592 cls, 593 "${cls.fullName()} can be replaced with OutcomeReceiver<R,E> (platform)" + 594 " or suspend fun / ListenableFuture (AndroidX)." 595 ) 596 } 597 } 598 599 private fun checkActions(field: FieldItem) { 600 val name = field.name() 601 if (name.startsWith("EXTRA_") || name == "SERVICE_INTERFACE" || name == "PROVIDER_INTERFACE") { 602 return 603 } 604 if (!field.type().isString()) { 605 return 606 } 607 val value = field.initialValue(true) as? String ?: return 608 if (!(name.contains("_ACTION") || name.contains("ACTION_") || value.contains(".action."))) { 609 return 610 } 611 val className = field.containingClass().qualifiedName() 612 when (className) { 613 "android.Manifest.permission" -> return 614 } 615 if (!name.startsWith("ACTION_")) { 616 report( 617 INTENT_NAME, field, 618 "Intent action constant name must be ACTION_FOO: $name" 619 ) 620 return 621 } 622 val prefix = when (className) { 623 "android.content.Intent" -> "android.intent.action" 624 "android.provider.Settings" -> "android.settings" 625 "android.app.admin.DevicePolicyManager", "android.app.admin.DeviceAdminReceiver" -> "android.app.action" 626 else -> field.containingClass().containingPackage().qualifiedName() + ".action" 627 } 628 val expected = prefix + "." + name.substring(7) 629 if (value != expected) { 630 report( 631 ACTION_VALUE, field, 632 "Inconsistent action value; expected `$expected`, was `$value`" 633 ) 634 } 635 } 636 637 private fun checkIntentExtras(field: FieldItem) { 638 val className = field.containingClass().qualifiedName() 639 if (className == "android.app.Notification" || className == "android.appwidget.AppWidgetManager") { 640 return 641 } 642 643 val name = field.name() 644 if (name.startsWith("ACTION_") || !field.type().isString()) { 645 return 646 } 647 val value = field.initialValue(true) as? String ?: return 648 if (!(name.contains("_EXTRA") || name.contains("EXTRA_") || value.contains(".extra"))) { 649 return 650 } 651 if (!name.startsWith("EXTRA_")) { 652 report( 653 INTENT_NAME, field, 654 "Intent extra constant name must be EXTRA_FOO: $name" 655 ) 656 return 657 } 658 659 val packageName = field.containingClass().containingPackage().qualifiedName() 660 val prefix = when { 661 className == "android.content.Intent" -> "android.intent.extra" 662 packageName == "android.app.admin" -> "android.app.extra" 663 else -> "$packageName.extra" 664 } 665 val expected = prefix + "." + name.substring(6) 666 if (value != expected) { 667 report( 668 ACTION_VALUE, field, 669 "Inconsistent extra value; expected `$expected`, was `$value`" 670 ) 671 } 672 } 673 674 private fun checkEquals(methods: Sequence<MethodItem>) { 675 var equalsMethod: MethodItem? = null 676 var hashCodeMethod: MethodItem? = null 677 678 for (method in methods) { 679 if (isEqualsMethod(method)) { 680 equalsMethod = method 681 } else if (isHashCodeMethod(method)) { 682 hashCodeMethod = method 683 } 684 } 685 if ((equalsMethod == null) != (hashCodeMethod == null)) { 686 val method = equalsMethod ?: hashCodeMethod!! 687 report( 688 EQUALS_AND_HASH_CODE, method, 689 "Must override both equals and hashCode; missing one in ${method.containingClass().qualifiedName()}" 690 ) 691 } 692 } 693 694 private fun isEqualsMethod(method: MethodItem): Boolean { 695 return method.name() == "equals" && method.parameters().size == 1 && 696 method.parameters()[0].type().isJavaLangObject() && 697 !method.modifiers.isStatic() 698 } 699 700 private fun isHashCodeMethod(method: MethodItem): Boolean { 701 return method.name() == "hashCode" && method.parameters().isEmpty() && 702 !method.modifiers.isStatic() 703 } 704 705 private fun checkParcelable( 706 cls: ClassItem, 707 methods: Sequence<MethodItem>, 708 constructors: Sequence<MethodItem>, 709 fields: Sequence<FieldItem> 710 ) { 711 if (!cls.implements("android.os.Parcelable")) { 712 return 713 } 714 715 if (fields.none { it.name() == "CREATOR" }) { 716 report( 717 PARCEL_CREATOR, cls, 718 "Parcelable requires a `CREATOR` field; missing in ${cls.qualifiedName()}" 719 ) 720 } 721 if (methods.none { it.name() == "writeToParcel" }) { 722 report( 723 PARCEL_CREATOR, cls, 724 "Parcelable requires `void writeToParcel(Parcel, int)`; missing in ${cls.qualifiedName()}" 725 ) 726 } 727 if (methods.none { it.name() == "describeContents" }) { 728 report( 729 PARCEL_CREATOR, cls, 730 "Parcelable requires `public int describeContents()`; missing in ${cls.qualifiedName()}" 731 ) 732 } 733 734 if (!cls.modifiers.isFinal()) { 735 report( 736 PARCEL_NOT_FINAL, cls, 737 "Parcelable classes must be final: ${cls.qualifiedName()} is not final" 738 ) 739 } 740 741 val parcelConstructor = constructors.firstOrNull { 742 val parameters = it.parameters() 743 parameters.size == 1 && parameters[0].type().toTypeString() == "android.os.Parcel" 744 } 745 746 if (parcelConstructor != null) { 747 report( 748 PARCEL_CONSTRUCTOR, parcelConstructor, 749 "Parcelable inflation is exposed through CREATOR, not raw constructors, in ${cls.qualifiedName()}" 750 ) 751 } 752 } 753 754 private fun checkProtected(member: MemberItem) { 755 val modifiers = member.modifiers 756 if (modifiers.isProtected()) { 757 if (member.name() == "finalize" && member is MethodItem && member.parameters().isEmpty()) { 758 return 759 } 760 761 report( 762 PROTECTED_MEMBER, member, 763 "Protected ${if (member is MethodItem) "methods" else "fields"} not allowed; must be public: ${member.describe()}}" 764 ) 765 } 766 } 767 768 private fun checkFieldName(field: FieldItem) { 769 val className = field.containingClass().qualifiedName() 770 val modifiers = field.modifiers 771 if (!modifiers.isFinal()) { 772 if (className !in classesWithBareFields && 773 !className.endsWith("LayoutParams") && 774 !className.startsWith("android.util.Mutable") 775 ) { 776 report( 777 MUTABLE_BARE_FIELD, field, 778 "Bare field ${field.name()} must be marked final, or moved behind accessors if mutable" 779 ) 780 } 781 } 782 if (!modifiers.isStatic()) { 783 if (!fieldNamePattern.matches(field.name())) { 784 report( 785 START_WITH_LOWER, field, 786 "Non-static field ${field.name()} must be named using fooBar style" 787 ) 788 } 789 } 790 if (internalNamePattern.matches(field.name())) { 791 report( 792 INTERNAL_FIELD, field, 793 "Internal field ${field.name()} must not be exposed" 794 ) 795 } 796 if (constantNamePattern.matches(field.name()) && field.isJava()) { 797 if (!modifiers.isStatic() || !modifiers.isFinal()) { 798 report( 799 ALL_UPPER, field, 800 "Constant ${field.name()} must be marked static final" 801 ) 802 } 803 } 804 } 805 806 private fun checkSettingKeys(field: FieldItem) { 807 val className = field.containingClass().qualifiedName() 808 val modifiers = field.modifiers 809 val type = field.type() 810 811 if (modifiers.isFinal() && modifiers.isStatic() && type.isString() && className in settingsKeyClasses) { 812 report( 813 NO_SETTINGS_PROVIDER, field, 814 "New setting keys are not allowed (Field: ${field.name()}); use getters/setters in relevant manager class" 815 ) 816 } 817 } 818 819 private fun checkRegistrationMethods(cls: ClassItem, methods: Sequence<MethodItem>) { 820 /** Make sure that there is a corresponding method */ 821 fun ensureMatched(cls: ClassItem, methods: Sequence<MethodItem>, method: MethodItem, name: String) { 822 if (method.superMethods().isNotEmpty()) return // Do not report for override methods 823 for (candidate in methods) { 824 if (candidate.name() == name) { 825 return 826 } 827 } 828 829 report( 830 PAIRED_REGISTRATION, method, 831 "Found ${method.name()} but not $name in ${cls.qualifiedName()}" 832 ) 833 } 834 835 for (method in methods) { 836 val name = method.name() 837 // the python version looks for any substring, but that includes a lot of other stuff, like plurals 838 if (name.endsWith("Callback")) { 839 if (name.startsWith("register")) { 840 val unregister = "unregister" + name.substring(8) // "register".length 841 ensureMatched(cls, methods, method, unregister) 842 } else if (name.startsWith("unregister")) { 843 val unregister = "register" + name.substring(10) // "unregister".length 844 ensureMatched(cls, methods, method, unregister) 845 } 846 if (name.startsWith("add") || name.startsWith("remove")) { 847 report( 848 REGISTRATION_NAME, method, 849 "Callback methods should be named register/unregister; was $name" 850 ) 851 } 852 } else if (name.endsWith("Listener")) { 853 if (name.startsWith("add")) { 854 val unregister = "remove" + name.substring(3) // "add".length 855 ensureMatched(cls, methods, method, unregister) 856 } else if (name.startsWith("remove") && !name.startsWith("removeAll")) { 857 val unregister = "add" + name.substring(6) // "remove".length 858 ensureMatched(cls, methods, method, unregister) 859 } 860 if (name.startsWith("register") || name.startsWith("unregister")) { 861 report( 862 REGISTRATION_NAME, method, 863 "Listener methods should be named add/remove; was $name" 864 ) 865 } 866 } 867 } 868 } 869 870 private fun checkSynchronized(method: MethodItem) { 871 fun reportError(method: MethodItem, psi: PsiElement? = null) { 872 val message = StringBuilder("Internal locks must not be exposed") 873 if (psi != null) { 874 message.append(" (synchronizing on this or class is still externally observable)") 875 } 876 message.append(": ") 877 message.append(method.describe()) 878 report(VISIBLY_SYNCHRONIZED, method, message.toString(), psi) 879 } 880 881 if (method.modifiers.isSynchronized()) { 882 reportError(method) 883 } else if (method is PsiMethodItem) { 884 val psiMethod = method.psiMethod 885 if (psiMethod is UMethod) { 886 psiMethod.accept(object : AbstractUastVisitor() { 887 override fun afterVisitCallExpression(node: UCallExpression) { 888 super.afterVisitCallExpression(node) 889 890 if (node.methodName == "synchronized" && node.receiver == null) { 891 val arg = node.valueArguments.firstOrNull() 892 if (arg is UThisExpression || 893 arg is UClassLiteralExpression || 894 arg is UQualifiedReferenceExpression && arg.receiver is UClassLiteralExpression 895 ) { 896 reportError(method, arg.sourcePsi ?: node.sourcePsi ?: node.javaPsi) 897 } 898 } 899 } 900 }) 901 } else { 902 psiMethod.body?.accept(object : JavaRecursiveElementVisitor() { 903 override fun visitSynchronizedStatement(statement: PsiSynchronizedStatement) { 904 super.visitSynchronizedStatement(statement) 905 906 val lock = statement.lockExpression 907 if (lock == null || lock is PsiThisExpression || 908 // locking on any class is visible 909 lock is PsiClassObjectAccessExpression 910 ) { 911 reportError(method, lock ?: statement) 912 } 913 } 914 }) 915 } 916 } 917 } 918 919 private fun checkIntentBuilder(method: MethodItem) { 920 if (method.returnType()?.toTypeString() == "android.content.Intent") { 921 val name = method.name() 922 if (name.startsWith("create") && name.endsWith("Intent")) { 923 return 924 } 925 if (method.containingClass().simpleName() == "Intent") { 926 return 927 } 928 929 report( 930 INTENT_BUILDER_NAME, method, 931 "Methods creating an Intent should be named `create<Foo>Intent()`, was `$name`" 932 ) 933 } 934 } 935 936 private fun checkHelperClasses(cls: ClassItem, methods: Sequence<MethodItem>, fields: Sequence<FieldItem>) { 937 fun ensureFieldValue(fields: Sequence<FieldItem>, fieldName: String, fieldValue: String) { 938 fields.firstOrNull { it.name() == fieldName }?.let { field -> 939 if (field.initialValue(true) != fieldValue) { 940 report( 941 INTERFACE_CONSTANT, field, 942 "Inconsistent interface constant; expected '$fieldValue'`" 943 ) 944 } 945 } 946 } 947 948 fun ensureContextNameSuffix(cls: ClassItem, suffix: String) { 949 if (!cls.simpleName().endsWith(suffix)) { 950 report( 951 CONTEXT_NAME_SUFFIX, cls, 952 "Inconsistent class name; should be `<Foo>$suffix`, was `${cls.simpleName()}`" 953 ) 954 } 955 } 956 957 var testMethods = false 958 959 when { 960 cls.extends("android.app.Service") -> { 961 testMethods = true 962 ensureContextNameSuffix(cls, "Service") 963 ensureFieldValue(fields, "SERVICE_INTERFACE", cls.qualifiedName()) 964 } 965 cls.extends("android.content.ContentProvider") -> { 966 testMethods = true 967 ensureContextNameSuffix(cls, "Provider") 968 ensureFieldValue(fields, "PROVIDER_INTERFACE", cls.qualifiedName()) 969 } 970 cls.extends("android.content.BroadcastReceiver") -> { 971 testMethods = true 972 ensureContextNameSuffix(cls, "Receiver") 973 } 974 cls.extends("android.app.Activity") -> { 975 testMethods = true 976 ensureContextNameSuffix(cls, "Activity") 977 } 978 } 979 980 if (testMethods) { 981 for (method in methods) { 982 val modifiers = method.modifiers 983 if (modifiers.isFinal() || modifiers.isStatic()) { 984 continue 985 } 986 val name = method.name() 987 if (!onCallbackNamePattern.matches(name)) { 988 val message = 989 if (modifiers.isAbstract()) { 990 "Methods implemented by developers should follow the on<Something> style, was `$name`" 991 } else { 992 "If implemented by developer, should follow the on<Something> style; otherwise consider marking final" 993 } 994 report(ON_NAME_EXPECTED, method, message) 995 } 996 } 997 } 998 } 999 1000 private fun checkBuilder( 1001 cls: ClassItem, 1002 methods: Sequence<MethodItem>, 1003 constructors: Sequence<ConstructorItem>, 1004 superClass: ClassItem? 1005 ) { 1006 if (!cls.simpleName().endsWith("Builder")) { 1007 return 1008 } 1009 if (superClass != null && !superClass.isJavaLangObject()) { 1010 return 1011 } 1012 if (cls.isTopLevelClass()) { 1013 report( 1014 TOP_LEVEL_BUILDER, cls, 1015 "Builder should be defined as inner class: ${cls.qualifiedName()}" 1016 ) 1017 } 1018 if (!cls.modifiers.isFinal()) { 1019 report( 1020 STATIC_FINAL_BUILDER, cls, 1021 "Builder must be final: ${cls.qualifiedName()}" 1022 ) 1023 } 1024 if (!cls.modifiers.isStatic() && !cls.isTopLevelClass()) { 1025 report( 1026 STATIC_FINAL_BUILDER, cls, 1027 "Builder must be static: ${cls.qualifiedName()}" 1028 ) 1029 } 1030 for (constructor in constructors) { 1031 for (arg in constructor.parameters()) { 1032 if (arg.modifiers.isNullable()) { 1033 report( 1034 OPTIONAL_BUILDER_CONSTRUCTOR_ARGUMENT, arg, 1035 "Builder constructor arguments must be mandatory (i.e. not @Nullable): ${arg.describe()}" 1036 ) 1037 } 1038 } 1039 } 1040 // Maps each setter to a list of potential getters that would satisfy it. 1041 val expectedGetters = mutableListOf<Pair<Item, Set<String>>>() 1042 var builtType: TypeItem? = null 1043 val clsType = cls.toType() 1044 1045 for (method in methods) { 1046 val name = method.name() 1047 if (name == "build") { 1048 builtType = method.type() 1049 continue 1050 } else if (name.startsWith("get") || name.startsWith("is")) { 1051 report( 1052 GETTER_ON_BUILDER, method, 1053 "Getter should be on the built object, not the builder: ${method.describe()}" 1054 ) 1055 } else if (name.startsWith("set") || name.startsWith("add") || name.startsWith("clear")) { 1056 val returnType = method.returnType() 1057 if (returnType != null) { 1058 val returnsClassType = if ( 1059 returnType is PsiTypeItem && clsType is PsiTypeItem 1060 ) { 1061 clsType.isAssignableFromWithoutUnboxing(returnType) 1062 } else { 1063 // fallback to a limited text based check 1064 val returnTypeBounds = returnType 1065 .asTypeParameter(context = method) 1066 ?.typeBounds()?.map { 1067 it.toTypeString() 1068 } ?: emptyList() 1069 returnTypeBounds.contains(clsType.toTypeString()) || returnType == clsType 1070 } 1071 if (!returnsClassType) { 1072 report( 1073 SETTER_RETURNS_THIS, method, 1074 "Methods must return the builder object (return type " + 1075 "$clsType instead of $returnType): ${method.describe()}" 1076 ) 1077 } 1078 } 1079 1080 if (method.modifiers.isNullable()) { 1081 report( 1082 SETTER_RETURNS_THIS, method, 1083 "Builder setter must be @NonNull: ${method.describe()}" 1084 ) 1085 } 1086 val isBool = when (method.parameters().firstOrNull()?.type()?.toTypeString()) { 1087 "boolean", "java.lang.Boolean" -> true 1088 else -> false 1089 } 1090 val allowedGetters: Set<String>? = if (isBool && name.startsWith("set")) { 1091 val pattern = goodBooleanGetterSetterPrefixes.match( 1092 name, GetterSetterPattern::setter 1093 )!! 1094 setOf("${pattern.getter}${name.removePrefix(pattern.setter)}") 1095 } else { 1096 when { 1097 name.startsWith("set") -> listOf(name.removePrefix("set")) 1098 name.startsWith("add") -> { 1099 val nameWithoutPrefix = name.removePrefix("add") 1100 when { 1101 name.endsWith("s") -> { 1102 // If the name ends with s, it may already be a plural. If the 1103 // add method accepts a single value, it is called addFoo() and 1104 // getFoos() is right. If an add method accepts a collection, it 1105 // is called addFoos() and getFoos() is right. So we allow both. 1106 listOf(nameWithoutPrefix, "${nameWithoutPrefix}es") 1107 } 1108 name.endsWith("sh") || name.endsWith("ch") || name.endsWith("x") || 1109 name.endsWith("z") -> listOf("${nameWithoutPrefix}es") 1110 name.endsWith("y") && 1111 name[name.length - 2] !in listOf('a', 'e', 'i', 'o', 'u') 1112 -> { 1113 listOf("${nameWithoutPrefix.removeSuffix("y")}ies") 1114 } 1115 else -> listOf("${nameWithoutPrefix}s") 1116 } 1117 } 1118 else -> null 1119 }?.map { "get$it" }?.toSet() 1120 } 1121 allowedGetters?.let { expectedGetters.add(method to it) } 1122 } else { 1123 report( 1124 BUILDER_SET_STYLE, method, 1125 "Builder methods names should use setFoo() / addFoo() / clearFoo() style: ${method.describe()}" 1126 ) 1127 } 1128 } 1129 if (builtType == null) { 1130 report( 1131 MISSING_BUILD_METHOD, cls, 1132 "${cls.qualifiedName()} does not declare a `build()` method, but builder classes are expected to" 1133 ) 1134 } 1135 builtType?.asClass()?.let { builtClass -> 1136 val builtMethods = builtClass.filteredMethods(filterReference, includeSuperClassMethods = true).map { it.name() }.toSet() 1137 for ((setter, expectedGetterNames) in expectedGetters) { 1138 if (builtMethods.intersect(expectedGetterNames).isEmpty()) { 1139 val expectedGetterCalls = expectedGetterNames.map { "$it()" } 1140 val errorString = if (expectedGetterCalls.size == 1) { 1141 "${builtClass.qualifiedName()} does not declare a " + 1142 "`${expectedGetterCalls.first()}` method matching " + 1143 setter.describe() 1144 } else { 1145 "${builtClass.qualifiedName()} does not declare a getter method " + 1146 "matching ${setter.describe()} (expected one of: " + 1147 "$expectedGetterCalls)" 1148 } 1149 report(MISSING_GETTER_MATCHING_BUILDER, setter, errorString) 1150 } 1151 } 1152 } 1153 } 1154 1155 private fun checkAidl(cls: ClassItem, superClass: ClassItem?, interfaces: Sequence<TypeItem>) { 1156 // Instead of ClassItem.implements() and .extends() which performs hierarchy 1157 // searches, here we only want to flag directly extending or implementing: 1158 val extendsBinder = superClass?.qualifiedName() == "android.os.Binder" 1159 val implementsIInterface = interfaces.any { it.toTypeString() == "android.os.IInterface" } 1160 if (extendsBinder || implementsIInterface) { 1161 val problem = if (extendsBinder) { 1162 "extends Binder" 1163 } else { 1164 "implements IInterface" 1165 } 1166 report( 1167 RAW_AIDL, cls, 1168 "Raw AIDL interfaces must not be exposed: ${cls.simpleName()} $problem" 1169 ) 1170 } 1171 } 1172 1173 private fun checkInternal(cls: ClassItem) { 1174 if (cls.qualifiedName().startsWith("com.android.")) { 1175 report( 1176 INTERNAL_CLASSES, cls, 1177 "Internal classes must not be exposed" 1178 ) 1179 } 1180 } 1181 1182 private fun checkLayering( 1183 cls: ClassItem, 1184 methodsAndConstructors: Sequence<MethodItem>, 1185 fields: Sequence<FieldItem> 1186 ) { 1187 fun packageRank(pkg: PackageItem): Int { 1188 return when (pkg.qualifiedName()) { 1189 "android.service", 1190 "android.accessibilityservice", 1191 "android.inputmethodservice", 1192 "android.printservice", 1193 "android.appwidget", 1194 "android.webkit", 1195 "android.preference", 1196 "android.gesture", 1197 "android.print" -> 10 1198 1199 "android.app" -> 20 1200 "android.widget" -> 30 1201 "android.view" -> 40 1202 "android.animation" -> 50 1203 "android.provider" -> 60 1204 1205 "android.content", 1206 "android.graphics.drawable" -> 70 1207 1208 "android.database" -> 80 1209 "android.text" -> 90 1210 "android.graphics" -> 100 1211 "android.os" -> 110 1212 "android.util" -> 120 1213 else -> -1 1214 } 1215 } 1216 1217 fun getTypePackage(type: TypeItem?): PackageItem? { 1218 return if (type == null || type.primitive) { 1219 null 1220 } else { 1221 type.asClass()?.containingPackage() 1222 } 1223 } 1224 1225 fun getTypeRank(type: TypeItem?): Int { 1226 type ?: return -1 1227 val pkg = getTypePackage(type) ?: return -1 1228 return packageRank(pkg) 1229 } 1230 1231 val classPackage = cls.containingPackage() 1232 val classRank = packageRank(classPackage) 1233 if (classRank == -1) { 1234 return 1235 } 1236 for (field in fields) { 1237 val fieldTypeRank = getTypeRank(field.type()) 1238 if (fieldTypeRank != -1 && fieldTypeRank < classRank) { 1239 report( 1240 PACKAGE_LAYERING, cls, 1241 "Field type `${field.type().toTypeString()}` violates package layering: nothing in `$classPackage` should depend on `${getTypePackage( 1242 field.type() 1243 )}`" 1244 ) 1245 } 1246 } 1247 1248 for (method in methodsAndConstructors) { 1249 val returnType = method.returnType() 1250 if (returnType != null) { // not a constructor 1251 val returnTypeRank = getTypeRank(returnType) 1252 if (returnTypeRank != -1 && returnTypeRank < classRank) { 1253 report( 1254 PACKAGE_LAYERING, cls, 1255 "Method return type `${returnType.toTypeString()}` violates package layering: nothing in `$classPackage` should depend on `${getTypePackage( 1256 returnType 1257 )}`" 1258 ) 1259 } 1260 } 1261 1262 for (parameter in method.parameters()) { 1263 val parameterTypeRank = getTypeRank(parameter.type()) 1264 if (parameterTypeRank != -1 && parameterTypeRank < classRank) { 1265 report( 1266 PACKAGE_LAYERING, cls, 1267 "Method parameter type `${parameter.type().toTypeString()}` violates package layering: nothing in `$classPackage` should depend on `${getTypePackage( 1268 parameter.type() 1269 )}`" 1270 ) 1271 } 1272 } 1273 } 1274 } 1275 1276 private fun checkBooleans(methods: Sequence<MethodItem>) { 1277 /* 1278 Correct: 1279 1280 void setVisible(boolean visible); 1281 boolean isVisible(); 1282 1283 void setHasTransientState(boolean hasTransientState); 1284 boolean hasTransientState(); 1285 1286 void setCanRecord(boolean canRecord); 1287 boolean canRecord(); 1288 1289 void setShouldFitWidth(boolean shouldFitWidth); 1290 boolean shouldFitWidth(); 1291 1292 void setWiFiRoamingSettingEnabled(boolean enabled) 1293 boolean isWiFiRoamingSettingEnabled() 1294 */ 1295 1296 fun errorIfExists(methods: Sequence<MethodItem>, trigger: String, expected: String, actual: String) { 1297 for (method in methods) { 1298 if (method.name() == actual) { 1299 report( 1300 GETTER_SETTER_NAMES, method, 1301 "Symmetric method for `$trigger` must be named `$expected`; was `$actual`" 1302 ) 1303 } 1304 } 1305 } 1306 1307 fun isGetter(method: MethodItem): Boolean { 1308 val returnType = method.returnType() ?: return false 1309 return method.parameters().isEmpty() && returnType.primitive && returnType.toTypeString() == "boolean" 1310 } 1311 1312 fun isSetter(method: MethodItem): Boolean { 1313 return method.parameters().size == 1 && method.parameters()[0].type().toTypeString() == "boolean" 1314 } 1315 1316 for (method in methods) { 1317 val name = method.name() 1318 if (isGetter(method)) { 1319 val pattern = goodBooleanGetterSetterPrefixes.match(name, GetterSetterPattern::getter) ?: continue 1320 val target = name.substring(pattern.getter.length) 1321 val expectedSetter = "${pattern.setter}$target" 1322 1323 badBooleanSetterPrefixes.forEach { 1324 val actualSetter = "${it}$target" 1325 if (actualSetter != expectedSetter) { 1326 errorIfExists(methods, name, expectedSetter, actualSetter) 1327 } 1328 } 1329 } else if (isSetter(method)) { 1330 val pattern = goodBooleanGetterSetterPrefixes.match(name, GetterSetterPattern::setter) ?: continue 1331 val target = name.substring(pattern.setter.length) 1332 val expectedGetter = "${pattern.getter}$target" 1333 1334 badBooleanGetterPrefixes.forEach { 1335 val actualGetter = "${it}$target" 1336 if (actualGetter != expectedGetter) { 1337 errorIfExists(methods, name, expectedGetter, actualGetter) 1338 } 1339 } 1340 } 1341 } 1342 } 1343 1344 private fun checkCollections( 1345 type: TypeItem, 1346 item: Item 1347 ) { 1348 if (type.primitive) { 1349 return 1350 } 1351 1352 when (type.asClass()?.qualifiedName()) { 1353 "java.util.Vector", 1354 "java.util.LinkedList", 1355 "java.util.ArrayList", 1356 "java.util.Stack", 1357 "java.util.HashMap", 1358 "java.util.HashSet", 1359 "android.util.ArraySet", 1360 "android.util.ArrayMap" -> { 1361 if (item.containingClass()?.qualifiedName() == "android.os.Bundle") { 1362 return 1363 } 1364 val where = when (item) { 1365 is MethodItem -> "Return type" 1366 is FieldItem -> "Field type" 1367 else -> "Parameter type" 1368 } 1369 val erased = type.toErasedTypeString() 1370 report( 1371 CONCRETE_COLLECTION, item, 1372 "$where is concrete collection (`$erased`); must be higher-level interface" 1373 ) 1374 } 1375 } 1376 } 1377 1378 fun Item.containingClass(): ClassItem? { 1379 return when (this) { 1380 is MemberItem -> this.containingClass() 1381 is ParameterItem -> this.containingMethod().containingClass() 1382 is ClassItem -> this 1383 else -> null 1384 } 1385 } 1386 1387 private fun checkNullableCollections(type: TypeItem, item: Item) { 1388 if (type.primitive) return 1389 if (!item.modifiers.isNullable()) return 1390 val typeAsClass = type.asClass() ?: return 1391 1392 val superItem: Item? = when (item) { 1393 is MethodItem -> item.findPredicateSuperMethod(filterReference) 1394 is ParameterItem -> item.containingMethod().findPredicateSuperMethod(filterReference) 1395 ?.parameters()?.find { it.parameterIndex == item.parameterIndex } 1396 else -> null 1397 } 1398 1399 if (superItem?.modifiers?.isNullable() == true) { 1400 return 1401 } 1402 1403 if (type.isArray() || 1404 typeAsClass.extendsOrImplements("java.util.Collection") || 1405 typeAsClass.extendsOrImplements("kotlin.collections.Collection") || 1406 typeAsClass.extendsOrImplements("java.util.Map") || 1407 typeAsClass.extendsOrImplements("kotlin.collections.Map") || 1408 typeAsClass.qualifiedName() == "android.os.Bundle" || 1409 typeAsClass.qualifiedName() == "android.os.PersistableBundle" 1410 ) { 1411 val where = when (item) { 1412 is MethodItem -> "Return type of ${item.describe()}" 1413 else -> "Type of ${item.describe()}" 1414 } 1415 1416 val erased = type.toErasedTypeString(item) 1417 report( 1418 NULLABLE_COLLECTION, item, 1419 "$where is a nullable collection (`$erased`); must be non-null" 1420 ) 1421 } 1422 } 1423 1424 private fun checkFlags(fields: Sequence<FieldItem>) { 1425 var known: MutableMap<String, Int>? = null 1426 var valueToFlag: MutableMap<Int?, String>? = null 1427 for (field in fields) { 1428 val name = field.name() 1429 val index = name.indexOf("FLAG_") 1430 if (index != -1) { 1431 val value = field.initialValue() as? Int ?: continue 1432 val scope = name.substring(0, index) 1433 val prev = known?.get(scope) ?: 0 1434 if (known != null && (prev and value) != 0) { 1435 val prevName = valueToFlag?.get(prev) 1436 report( 1437 OVERLAPPING_CONSTANTS, field, 1438 "Found overlapping flag constant values: `$name` with value $value (0x${Integer.toHexString( 1439 value 1440 )}) and overlapping flag value $prev (0x${Integer.toHexString(prev)}) from `$prevName`" 1441 ) 1442 } 1443 if (known == null) { 1444 known = mutableMapOf() 1445 } 1446 known[scope] = value 1447 if (valueToFlag == null) { 1448 valueToFlag = mutableMapOf() 1449 } 1450 valueToFlag[value] = name 1451 } 1452 } 1453 } 1454 1455 private fun checkExceptions(method: MethodItem, filterReference: Predicate<Item>) { 1456 for (exception in method.filteredThrowsTypes(filterReference)) { 1457 if (isUncheckedException(exception)) { 1458 report( 1459 BANNED_THROW, method, 1460 "Methods must not throw unchecked exceptions" 1461 ) 1462 } else { 1463 when (val qualifiedName = exception.qualifiedName()) { 1464 "java.lang.Exception", 1465 "java.lang.Throwable", 1466 "java.lang.Error" -> { 1467 report( 1468 GENERIC_EXCEPTION, method, 1469 "Methods must not throw generic exceptions (`$qualifiedName`)" 1470 ) 1471 } 1472 "android.os.RemoteException" -> { 1473 when (method.containingClass().qualifiedName()) { 1474 "android.content.ContentProviderClient", 1475 "android.os.Binder", 1476 "android.os.IBinder" -> { 1477 // exceptions 1478 } 1479 else -> { 1480 report( 1481 RETHROW_REMOTE_EXCEPTION, method, 1482 "Methods calling system APIs should rethrow `RemoteException` as `RuntimeException` (but do not list it in the throws clause)" 1483 ) 1484 } 1485 } 1486 } 1487 } 1488 } 1489 } 1490 } 1491 1492 /** 1493 * Unchecked exceptions are subclasses of RuntimeException or Error. These are not 1494 * checked by the compiler, and it is against API guidelines to put them in the 'throws'. 1495 * See https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html 1496 */ 1497 private fun isUncheckedException(exception: ClassItem): Boolean { 1498 val superNames = exception.allSuperClasses().map { 1499 it.qualifiedName() 1500 } 1501 return superNames.any { 1502 it == "java.lang.RuntimeException" || it == "java.lang.Error" 1503 } 1504 } 1505 1506 private fun checkGoogle(cls: ClassItem, methods: Sequence<MethodItem>, fields: Sequence<FieldItem>) { 1507 fun checkName(name: String, item: Item) { 1508 if (name.contains("Google", ignoreCase = true)) { 1509 report( 1510 MENTIONS_GOOGLE, item, 1511 "Must never reference Google (`$name`)" 1512 ) 1513 } 1514 } 1515 1516 checkName(cls.simpleName(), cls) 1517 for (method in methods) { 1518 checkName(method.name(), method) 1519 } 1520 for (field in fields) { 1521 checkName(field.name(), field) 1522 } 1523 } 1524 1525 private fun checkBitSet(type: TypeItem, typeString: String, item: Item) { 1526 if (typeString.startsWith("java.util.BitSet") && 1527 type.asClass()?.qualifiedName() == "java.util.BitSet" 1528 ) { 1529 report( 1530 HEAVY_BIT_SET, item, 1531 "Type must not be heavy BitSet (${item.describe()})" 1532 ) 1533 } 1534 } 1535 1536 private fun checkManager(cls: ClassItem, methods: Sequence<MethodItem>, constructors: Sequence<ConstructorItem>) { 1537 if (!cls.simpleName().endsWith("Manager")) { 1538 return 1539 } 1540 for (method in constructors) { 1541 method.modifiers.isPublic() 1542 method.modifiers.isPrivate() 1543 report( 1544 MANAGER_CONSTRUCTOR, method, 1545 "Managers must always be obtained from Context; no direct constructors" 1546 ) 1547 } 1548 for (method in methods) { 1549 if (method.returnType()?.asClass() == cls) { 1550 report( 1551 MANAGER_LOOKUP, method, 1552 "Managers must always be obtained from Context (`${method.name()}`)" 1553 ) 1554 } 1555 } 1556 } 1557 1558 private fun checkHasNullability(item: Item) { 1559 if (!item.requiresNullnessInfo()) return 1560 if (!item.hasNullnessInfo() && getImplicitNullness(item) == null) { 1561 val type = item.type() 1562 val inherited = when (item) { 1563 is ParameterItem -> item.containingMethod().inheritedMethod 1564 is FieldItem -> item.inheritedField 1565 is MethodItem -> item.inheritedMethod 1566 else -> false 1567 } 1568 if (inherited) { 1569 return // Do not enforce nullability on inherited items (non-overridden) 1570 } 1571 if (type != null && type.isTypeParameter()) { 1572 // Generic types should have declarations of nullability set at the site of where 1573 // the type is set, so that for Foo<T>, T does not need to specify nullability, but 1574 // for Foo<Bar>, Bar does. 1575 return // Do not enforce nullability for generics 1576 } 1577 if (item is MethodItem && item.isKotlinProperty()) { 1578 return // kotlinc doesn't add nullability https://youtrack.jetbrains.com/issue/KT-45771 1579 } 1580 val where = when (item) { 1581 is ParameterItem -> "parameter `${item.name()}` in method `${item.parent()?.name()}`" 1582 is FieldItem -> { 1583 if (item.isKotlin()) { 1584 if (item.name() == "INSTANCE") { 1585 // Kotlin compiler is not marking it with a nullability annotation 1586 // https://youtrack.jetbrains.com/issue/KT-33226 1587 return 1588 } 1589 if (item.modifiers.isCompanion()) { 1590 // Kotlin compiler is not marking it with a nullability annotation 1591 // https://youtrack.jetbrains.com/issue/KT-33314 1592 return 1593 } 1594 } 1595 "field `${item.name()}` in class `${item.parent()}`" 1596 } 1597 1598 is ConstructorItem -> "constructor `${item.name()}` return" 1599 is MethodItem -> { 1600 // For methods requiresNullnessInfo and hasNullnessInfo considers both parameters and return, 1601 // only warn about non-annotated returns here as parameters will get visited individually. 1602 if (item.isConstructor() || item.returnType()?.primitive == true) return 1603 if (item.modifiers.hasNullnessInfo()) return 1604 "method `${item.name()}` return" 1605 } 1606 else -> throw IllegalStateException("Unexpected item type: $item") 1607 } 1608 report(MISSING_NULLABILITY, item, "Missing nullability on $where") 1609 } else { 1610 when (item) { 1611 is ParameterItem -> { 1612 // We don't enforce this check on constructor params 1613 if (item.containingMethod().isConstructor()) return 1614 if (item.modifiers.isNonNull()) { 1615 if (anySuperParameterLacksNullnessInfo(item)) { 1616 report(INVALID_NULLABILITY_OVERRIDE, item, "Invalid nullability on parameter `${item.name()}` in method `${item.parent()?.name()}`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.") 1617 } else if (anySuperParameterIsNullable(item)) { 1618 report(INVALID_NULLABILITY_OVERRIDE, item, "Invalid nullability on parameter `${item.name()}` in method `${item.parent()?.name()}`. Parameters of overrides cannot be NonNull if super parameter is Nullable.") 1619 } 1620 } 1621 } 1622 is MethodItem -> { 1623 // We don't enforce this check on constructors 1624 if (item.isConstructor()) return 1625 if (item.modifiers.isNullable()) { 1626 if (anySuperMethodLacksNullnessInfo(item)) { 1627 report(INVALID_NULLABILITY_OVERRIDE, item, "Invalid nullability on method `${item.name()}` return. Overrides of unannotated super method cannot be Nullable.") 1628 } else if (anySuperMethodIsNonNull(item)) { 1629 report(INVALID_NULLABILITY_OVERRIDE, item, "Invalid nullability on method `${item.name()}` return. Overrides of NonNull methods cannot be Nullable.") 1630 } 1631 } 1632 } 1633 } 1634 } 1635 } 1636 1637 private fun anySuperMethodIsNonNull(method: MethodItem): Boolean { 1638 return method.superMethods().any { superMethod -> 1639 superMethod.modifiers.isNonNull() && 1640 // Disable check for generics 1641 superMethod.returnType()?.isTypeParameter() != true 1642 } 1643 } 1644 1645 private fun anySuperParameterIsNullable(parameter: ParameterItem): Boolean { 1646 val supers = parameter.containingMethod().superMethods() 1647 return supers.all { superMethod -> 1648 // Disable check for generics 1649 superMethod.parameters().none { 1650 it.type().isTypeParameter() 1651 } 1652 } && supers.any { superMethod -> 1653 superMethod.parameters().firstOrNull { param -> 1654 parameter.parameterIndex == param.parameterIndex 1655 }?.modifiers?.isNullable() ?: false 1656 } 1657 } 1658 1659 private fun anySuperMethodLacksNullnessInfo(method: MethodItem): Boolean { 1660 return method.superMethods().any { superMethod -> 1661 !superMethod.hasNullnessInfo() && 1662 // Disable check for generics 1663 superMethod.returnType()?.isTypeParameter() != true 1664 } 1665 } 1666 1667 private fun anySuperParameterLacksNullnessInfo(parameter: ParameterItem): Boolean { 1668 val supers = parameter.containingMethod().superMethods() 1669 return supers.all { superMethod -> 1670 // Disable check for generics 1671 superMethod.parameters().none { 1672 it.type().isTypeParameter() 1673 } 1674 } && supers.any { superMethod -> 1675 !( 1676 superMethod.parameters().firstOrNull { param -> 1677 parameter.parameterIndex == param.parameterIndex 1678 }?.hasNullnessInfo() ?: true 1679 ) 1680 } 1681 } 1682 1683 private fun checkBoxed(type: TypeItem, item: Item) { 1684 fun isBoxType(qualifiedName: String): Boolean { 1685 return when (qualifiedName) { 1686 "java.lang.Number", 1687 "java.lang.Byte", 1688 "java.lang.Double", 1689 "java.lang.Float", 1690 "java.lang.Integer", 1691 "java.lang.Long", 1692 "java.lang.Short", 1693 "java.lang.Boolean" -> 1694 true 1695 else -> 1696 false 1697 } 1698 } 1699 1700 val qualifiedName = type.asClass()?.qualifiedName() ?: return 1701 if (isBoxType(qualifiedName)) { 1702 report( 1703 AUTO_BOXING, item, 1704 "Must avoid boxed primitives (`$qualifiedName`)" 1705 ) 1706 } 1707 } 1708 1709 private fun checkStaticUtils( 1710 cls: ClassItem, 1711 methods: Sequence<MethodItem>, 1712 constructors: Sequence<ConstructorItem>, 1713 fields: Sequence<FieldItem> 1714 ) { 1715 if (!cls.isClass()) { 1716 return 1717 } 1718 1719 val hasDefaultConstructor = cls.hasImplicitDefaultConstructor() || run { 1720 if (constructors.count() == 1) { 1721 val constructor = constructors.first() 1722 constructor.parameters().isEmpty() && constructor.modifiers.isPublic() 1723 } else { 1724 false 1725 } 1726 } 1727 1728 if (hasDefaultConstructor) { 1729 val qualifiedName = cls.qualifiedName() 1730 if (qualifiedName.startsWith("android.opengl.") || 1731 qualifiedName.startsWith("android.R.") || 1732 qualifiedName == "android.R" 1733 ) { 1734 return 1735 } 1736 1737 if (methods.none() && fields.none()) { 1738 return 1739 } 1740 1741 if (methods.none { !it.modifiers.isStatic() } && 1742 fields.none { !it.modifiers.isStatic() } 1743 ) { 1744 report( 1745 STATIC_UTILS, cls, 1746 "Fully-static utility classes must not have constructor" 1747 ) 1748 } 1749 } 1750 } 1751 1752 private fun checkOverloadArgs(cls: ClassItem, methods: Sequence<MethodItem>) { 1753 if (cls.qualifiedName().startsWith("android.opengl")) { 1754 return 1755 } 1756 1757 val overloads = mutableMapOf<String, MutableList<MethodItem>>() 1758 for (method in methods) { 1759 if (!method.deprecated) { 1760 val name = method.name() 1761 val list = overloads[name] ?: run { 1762 val new = mutableListOf<MethodItem>() 1763 overloads[name] = new 1764 new 1765 } 1766 list.add(method) 1767 } 1768 } 1769 1770 // Look for arguments common across all overloads 1771 fun cluster(args: List<ParameterItem>): MutableSet<String> { 1772 val count = mutableMapOf<String, Int>() 1773 val res = mutableSetOf<String>() 1774 for (parameter in args) { 1775 val a = parameter.type().toTypeString() 1776 val currCount = count[a] ?: 1 1777 res.add("$a#$currCount") 1778 count[a] = currCount + 1 1779 } 1780 return res 1781 } 1782 1783 for ((_, methodList) in overloads.entries) { 1784 if (methodList.size <= 1) { 1785 continue 1786 } 1787 1788 val commonArgs = cluster(methodList[0].parameters()) 1789 for (m in methodList) { 1790 val clustered = cluster(m.parameters()) 1791 commonArgs.removeAll(clustered) 1792 } 1793 if (commonArgs.isEmpty()) { 1794 continue 1795 } 1796 1797 // Require that all common arguments are present at the start of the signature 1798 var lockedSig: List<ParameterItem>? = null 1799 val commonArgCount = commonArgs.size 1800 for (m in methodList) { 1801 val sig = m.parameters().subList(0, commonArgCount) 1802 val cluster = cluster(sig) 1803 if (!cluster.containsAll(commonArgs)) { 1804 report( 1805 COMMON_ARGS_FIRST, m, 1806 "Expected common arguments ${commonArgs.joinToString()}} at beginning of overloaded method ${m.describe()}" 1807 ) 1808 } else if (lockedSig == null) { 1809 lockedSig = sig 1810 } else if (lockedSig != sig) { 1811 report( 1812 CONSISTENT_ARGUMENT_ORDER, m, 1813 "Expected consistent argument ordering between overloads: ${lockedSig.joinToString()}}" 1814 ) 1815 } 1816 } 1817 } 1818 } 1819 1820 private fun checkCallbackHandlers( 1821 cls: ClassItem, 1822 methodsAndConstructors: Sequence<MethodItem>, 1823 superClass: ClassItem? 1824 ) { 1825 fun packageContainsSegment(packageName: String?, segment: String): Boolean { 1826 packageName ?: return false 1827 return ( 1828 packageName.contains(segment) && 1829 (packageName.contains(".$segment.") || packageName.endsWith(".$segment")) 1830 ) 1831 } 1832 1833 fun skipPackage(packageName: String?): Boolean { 1834 packageName ?: return false 1835 for (segment in uiPackageParts) { 1836 if (packageContainsSegment(packageName, segment)) { 1837 return true 1838 } 1839 } 1840 1841 return false 1842 } 1843 1844 // Ignore UI packages which assume main thread 1845 val classPackage = cls.containingPackage().qualifiedName() 1846 val extendsPackage = superClass?.containingPackage()?.qualifiedName() 1847 1848 if (skipPackage(classPackage) || skipPackage(extendsPackage)) { 1849 return 1850 } 1851 1852 // Ignore UI classes which assume main thread 1853 if (packageContainsSegment(classPackage, "app") || 1854 packageContainsSegment(extendsPackage, "app") 1855 ) { 1856 val fullName = cls.fullName() 1857 if (fullName.contains("ActionBar") || 1858 fullName.contains("Dialog") || 1859 fullName.contains("Application") || 1860 fullName.contains("Activity") || 1861 fullName.contains("Fragment") || 1862 fullName.contains("Loader") 1863 ) { 1864 return 1865 } 1866 } 1867 if (packageContainsSegment(classPackage, "content") || 1868 packageContainsSegment(extendsPackage, "content") 1869 ) { 1870 val fullName = cls.fullName() 1871 if (fullName.contains("Loader")) { 1872 return 1873 } 1874 } 1875 1876 val found = mutableMapOf<String, MethodItem>() 1877 val byName = mutableMapOf<String, MutableList<MethodItem>>() 1878 for (method in methodsAndConstructors) { 1879 val name = method.name() 1880 if (name.startsWith("unregister")) { 1881 continue 1882 } 1883 if (name.startsWith("remove")) { 1884 continue 1885 } 1886 if (name.startsWith("on") && onCallbackNamePattern.matches(name)) { 1887 continue 1888 } 1889 1890 val list = byName[name] ?: run { 1891 val new = mutableListOf<MethodItem>() 1892 byName[name] = new 1893 new 1894 } 1895 list.add(method) 1896 1897 for (parameter in method.parameters()) { 1898 val type = parameter.type().toTypeString() 1899 if (type.endsWith("Listener") || 1900 type.endsWith("Callback") || 1901 type.endsWith("Callbacks") 1902 ) { 1903 found[name] = method 1904 } 1905 } 1906 } 1907 1908 for (f in found.values) { 1909 var takesExec = false 1910 1911 // TODO: apilint computed takes_handler but did not use it; should we add more checks or conditions? 1912 // var takesHandler = false 1913 1914 val name = f.name() 1915 for (method in byName[name]!!) { 1916 // if (method.parameters().any { it.type().toTypeString() == "android.os.Handler" }) { 1917 // takesHandler = true 1918 // } 1919 if (method.parameters().any { it.type().toTypeString() == "java.util.concurrent.Executor" }) { 1920 takesExec = true 1921 } 1922 } 1923 if (!takesExec) { 1924 report( 1925 EXECUTOR_REGISTRATION, f, 1926 "Registration methods should have overload that accepts delivery Executor: `$name`" 1927 ) 1928 } 1929 } 1930 } 1931 1932 private fun checkContextFirst(method: MethodItem) { 1933 val parameters = method.parameters() 1934 if (parameters.size > 1 && parameters[0].type().toTypeString() != "android.content.Context") { 1935 for (i in 1 until parameters.size) { 1936 val p = parameters[i] 1937 if (p.type().toTypeString() == "android.content.Context") { 1938 report( 1939 CONTEXT_FIRST, p, 1940 "Context is distinct, so it must be the first argument (method `${method.name()}`)" 1941 ) 1942 } 1943 } 1944 } 1945 if (parameters.size > 1 && parameters[0].type().toTypeString() != "android.content.ContentResolver") { 1946 for (i in 1 until parameters.size) { 1947 val p = parameters[i] 1948 if (p.type().toTypeString() == "android.content.ContentResolver") { 1949 report( 1950 CONTEXT_FIRST, p, 1951 "ContentResolver is distinct, so it must be the first argument (method `${method.name()}`)" 1952 ) 1953 } 1954 } 1955 } 1956 } 1957 1958 private fun checkListenerLast(method: MethodItem) { 1959 val name = method.name() 1960 if (name.contains("Listener") || name.contains("Callback")) { 1961 return 1962 } 1963 1964 val parameters = method.parameters() 1965 if (parameters.size > 1) { 1966 var found = false 1967 for (parameter in parameters) { 1968 val type = parameter.type().toTypeString() 1969 if (type.endsWith("Callback") || type.endsWith("Callbacks") || type.endsWith("Listener")) { 1970 found = true 1971 } else if (found) { 1972 report( 1973 LISTENER_LAST, parameter, 1974 "Listeners should always be at end of argument list (method `${method.name()}`)" 1975 ) 1976 } 1977 } 1978 } 1979 } 1980 1981 private fun checkResourceNames(cls: ClassItem, fields: Sequence<FieldItem>) { 1982 if (!cls.qualifiedName().startsWith("android.R.")) { 1983 return 1984 } 1985 1986 val resourceType = ResourceType.fromClassName(cls.simpleName()) ?: return 1987 when (resourceType) { 1988 ANIM, 1989 ANIMATOR, 1990 COLOR, 1991 DIMEN, 1992 DRAWABLE, 1993 FONT, 1994 INTERPOLATOR, 1995 LAYOUT, 1996 MENU, 1997 MIPMAP, 1998 NAVIGATION, 1999 PLURALS, 2000 RAW, 2001 STRING, 2002 TRANSITION, 2003 XML -> { 2004 // Resources defined by files are foo_bar_baz 2005 // Note: it's surprising that dimen, plurals and string are in this list since 2006 // they are value resources, not file resources, but keeping api lint compatibility 2007 // for now. 2008 2009 for (field in fields) { 2010 val name = field.name() 2011 if (name.startsWith("config_")) { 2012 if (!configFieldPattern.matches(name)) { 2013 report( 2014 CONFIG_FIELD_NAME, field, 2015 "Expected config name to be in the `config_fooBarBaz` style, was `$name`" 2016 ) 2017 } 2018 continue 2019 } 2020 if (!resourceFileFieldPattern.matches(name)) { 2021 report( 2022 RESOURCE_FIELD_NAME, field, 2023 "Expected resource name in `${cls.qualifiedName()}` to be in the `foo_bar_baz` style, was `$name`" 2024 ) 2025 } 2026 } 2027 } 2028 2029 ARRAY, 2030 ATTR, 2031 BOOL, 2032 FRACTION, 2033 ID, 2034 INTEGER -> { 2035 // Resources defined inside files are fooBarBaz 2036 for (field in fields) { 2037 val name = field.name() 2038 if (name.startsWith("config_") && configFieldPattern.matches(name)) { 2039 continue 2040 } 2041 if (name.startsWith("layout_") && layoutFieldPattern.matches(name)) { 2042 continue 2043 } 2044 if (name.startsWith("state_") && stateFieldPattern.matches(name)) { 2045 continue 2046 } 2047 if (resourceValueFieldPattern.matches(name)) { 2048 continue 2049 } 2050 report( 2051 RESOURCE_VALUE_FIELD_NAME, field, 2052 "Expected resource name in `${cls.qualifiedName()}` to be in the `fooBarBaz` style, was `$name`" 2053 ) 2054 } 2055 } 2056 2057 STYLE -> { 2058 for (field in fields) { 2059 val name = field.name() 2060 if (!styleFieldPattern.matches(name)) { 2061 report( 2062 RESOURCE_STYLE_FIELD_NAME, field, 2063 "Expected resource name in `${cls.qualifiedName()}` to be in the `FooBar_Baz` style, was `$name`" 2064 ) 2065 } 2066 } 2067 } 2068 2069 STYLEABLE, // appears as R class but name check is implicitly done as part of style class check 2070 // DECLARE_STYLEABLE, 2071 STYLE_ITEM, 2072 PUBLIC, 2073 SAMPLE_DATA, 2074 AAPT -> { 2075 // no-op; these are resource "types" in XML but not present as R classes 2076 // Listed here explicitly to force compiler error as new resource types 2077 // are added. 2078 } 2079 } 2080 } 2081 2082 private fun checkFiles(methodsAndConstructors: Sequence<MethodItem>) { 2083 var hasFile: MutableSet<MethodItem>? = null 2084 var hasStream: MutableSet<String>? = null 2085 for (method in methodsAndConstructors) { 2086 for (parameter in method.parameters()) { 2087 when (parameter.type().toTypeString()) { 2088 "java.io.File" -> { 2089 val set = hasFile ?: run { 2090 val new = mutableSetOf<MethodItem>() 2091 hasFile = new 2092 new 2093 } 2094 set.add(method) 2095 } 2096 "java.io.FileDescriptor", 2097 "android.os.ParcelFileDescriptor", 2098 "java.io.InputStream", 2099 "java.io.OutputStream" -> { 2100 val set = hasStream ?: run { 2101 val new = mutableSetOf<String>() 2102 hasStream = new 2103 new 2104 } 2105 set.add(method.name()) 2106 } 2107 } 2108 } 2109 } 2110 val files = hasFile 2111 if (files != null) { 2112 val streams = hasStream 2113 for (method in files) { 2114 if (streams == null || !streams.contains(method.name())) { 2115 report( 2116 STREAM_FILES, method, 2117 "Methods accepting `File` should also accept `FileDescriptor` or streams: ${method.describe()}" 2118 ) 2119 } 2120 } 2121 } 2122 } 2123 2124 private fun checkManagerList(cls: ClassItem, methods: Sequence<MethodItem>) { 2125 if (!cls.simpleName().endsWith("Manager")) { 2126 return 2127 } 2128 for (method in methods) { 2129 val returnType = method.returnType() ?: continue 2130 if (returnType.primitive) { 2131 return 2132 } 2133 val type = returnType.toTypeString() 2134 if (type.startsWith("android.") && returnType.isArray()) { 2135 report( 2136 PARCELABLE_LIST, method, 2137 "Methods should return `List<? extends Parcelable>` instead of `Parcelable[]` to support `ParceledListSlice` under the hood: ${method.describe()}" 2138 ) 2139 } 2140 } 2141 } 2142 2143 private fun checkAbstractInner(cls: ClassItem) { 2144 if (!cls.isTopLevelClass() && cls.isClass() && cls.modifiers.isAbstract() && !cls.modifiers.isStatic()) { 2145 report( 2146 ABSTRACT_INNER, cls, 2147 "Abstract inner classes should be static to improve testability: ${cls.describe()}" 2148 ) 2149 } 2150 } 2151 2152 private fun checkError(cls: ClassItem, superClass: ClassItem?) { 2153 superClass ?: return 2154 if (superClass.simpleName().endsWith("Error")) { 2155 report( 2156 EXTENDS_ERROR, cls, 2157 "Trouble must be reported through an `Exception`, not an `Error` (`${cls.simpleName()}` extends `${superClass.simpleName()}`)" 2158 ) 2159 } 2160 if (superClass.simpleName().endsWith("Exception") && !cls.simpleName().endsWith("Exception")) { 2161 report( 2162 EXCEPTION_NAME, cls, 2163 "Exceptions must be named `FooException`, was `${cls.simpleName()}`" 2164 ) 2165 } 2166 } 2167 2168 private fun checkUnits(method: MethodItem) { 2169 val returnType = method.returnType() ?: return 2170 var type = returnType.toTypeString() 2171 val name = method.name() 2172 if (type == "int" || type == "long" || type == "short") { 2173 if (badUnits.any { name.endsWith(it.key) }) { 2174 val badUnit = badUnits.keys.find { name.endsWith(it) } 2175 val value = badUnits[badUnit] 2176 report( 2177 METHOD_NAME_UNITS, method, 2178 "Expected method name units to be `$value`, was `$badUnit` in `$name`" 2179 ) 2180 } 2181 } else if (type == "void") { 2182 if (method.parameters().size != 1) { 2183 return 2184 } 2185 type = method.parameters()[0].type().toTypeString() 2186 } 2187 if (name.endsWith("Fraction") && (type == "int" || type == "long" || type == "short")) { 2188 report( 2189 FRACTION_FLOAT, method, 2190 "Fractions must use floats, was `$type` in `$name`" 2191 ) 2192 } else if (name.endsWith("Percentage") && (type == "float" || type == "double")) { 2193 report( 2194 PERCENTAGE_INT, method, 2195 "Percentage must use ints, was `$type` in `$name`" 2196 ) 2197 } 2198 } 2199 2200 private fun checkCloseable(cls: ClassItem, methods: Sequence<MethodItem>) { 2201 // AutoClosable has been added in API 19, so libraries with minSdkVersion <19 cannot use it. If the version 2202 // is not set, then keep the check enabled. 2203 val minSdkVersion = codebase.getMinSdkVersion() 2204 if (minSdkVersion is SetMinSdkVersion && minSdkVersion.value < 19) { 2205 return 2206 } 2207 2208 val foundMethods = methods.filter { method -> 2209 when (method.name()) { 2210 "close", "release", "destroy", "finish", "finalize", "disconnect", "shutdown", "stop", "free", "quit" -> true 2211 else -> false 2212 } 2213 } 2214 if (foundMethods.iterator().hasNext() && !cls.implements("java.lang.AutoCloseable")) { // includes java.io.Closeable 2215 val foundMethodsDescriptions = foundMethods.joinToString { method -> "${method.name()}()" } 2216 report( 2217 NOT_CLOSEABLE, cls, 2218 "Classes that release resources ($foundMethodsDescriptions) should implement AutoClosable and CloseGuard: ${cls.describe()}" 2219 ) 2220 } 2221 } 2222 2223 private fun checkNotKotlinOperator(methods: Sequence<MethodItem>) { 2224 fun flagKotlinOperator(method: MethodItem, message: String) { 2225 if (method.isKotlin()) { 2226 report( 2227 KOTLIN_OPERATOR, method, 2228 "Note that adding the `operator` keyword would allow calling this method using operator syntax" 2229 ) 2230 } else { 2231 report( 2232 KOTLIN_OPERATOR, method, 2233 "$message (this is usually desirable; just make sure it makes sense for this type of object)" 2234 ) 2235 } 2236 } 2237 2238 for (method in methods) { 2239 if (method.modifiers.isStatic() || method.modifiers.isOperator() || method.superMethods().isNotEmpty()) { 2240 continue 2241 } 2242 when (val name = method.name()) { 2243 // https://kotlinlang.org/docs/reference/operator-overloading.html#unary-prefix-operators 2244 "unaryPlus", "unaryMinus", "not" -> { 2245 if (method.parameters().isEmpty()) { 2246 flagKotlinOperator( 2247 method, "Method can be invoked as a unary operator from Kotlin: `$name`" 2248 ) 2249 } 2250 } 2251 // https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements 2252 "inc", "dec" -> { 2253 if (method.parameters().isEmpty() && method.returnType()?.toTypeString() != "void") { 2254 flagKotlinOperator( 2255 method, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin: `$name`" 2256 ) 2257 } 2258 } 2259 // https://kotlinlang.org/docs/reference/operator-overloading.html#arithmetic 2260 "plus", "minus", "times", "div", "rem", "mod", "rangeTo" -> { 2261 if (method.parameters().size == 1) { 2262 flagKotlinOperator( 2263 method, "Method can be invoked as a binary operator from Kotlin: `$name`" 2264 ) 2265 } 2266 val assignName = name + "Assign" 2267 2268 if (methods.any { 2269 it.name() == assignName && 2270 it.parameters().size == 1 && 2271 it.returnType()?.toTypeString() == "void" 2272 } 2273 ) { 2274 report( 2275 UNIQUE_KOTLIN_OPERATOR, method, 2276 "Only one of `$name` and `${name}Assign` methods should be present for Kotlin" 2277 ) 2278 } 2279 } 2280 // https://kotlinlang.org/docs/reference/operator-overloading.html#in 2281 "contains" -> { 2282 if (method.parameters().size == 1 && method.returnType()?.toTypeString() == "boolean") { 2283 flagKotlinOperator( 2284 method, "Method can be invoked as a \"in\" operator from Kotlin: `$name`" 2285 ) 2286 } 2287 } 2288 // https://kotlinlang.org/docs/reference/operator-overloading.html#indexed 2289 "get" -> { 2290 if (method.parameters().isNotEmpty()) { 2291 flagKotlinOperator( 2292 method, "Method can be invoked with an indexing operator from Kotlin: `$name`" 2293 ) 2294 } 2295 } 2296 // https://kotlinlang.org/docs/reference/operator-overloading.html#indexed 2297 "set" -> { 2298 if (method.parameters().size > 1) { 2299 flagKotlinOperator( 2300 method, "Method can be invoked with an indexing operator from Kotlin: `$name`" 2301 ) 2302 } 2303 } 2304 // https://kotlinlang.org/docs/reference/operator-overloading.html#invoke 2305 "invoke" -> { 2306 if (method.parameters().size > 1) { 2307 flagKotlinOperator( 2308 method, "Method can be invoked with function call syntax from Kotlin: `$name`" 2309 ) 2310 } 2311 } 2312 // https://kotlinlang.org/docs/reference/operator-overloading.html#assignments 2313 "plusAssign", "minusAssign", "timesAssign", "divAssign", "remAssign", "modAssign" -> { 2314 if (method.parameters().size == 1 && method.returnType()?.toTypeString() == "void") { 2315 flagKotlinOperator( 2316 method, "Method can be invoked as a compound assignment operator from Kotlin: `$name`" 2317 ) 2318 } 2319 } 2320 } 2321 } 2322 } 2323 2324 private fun checkCollectionsOverArrays(type: TypeItem, typeString: String, item: Item) { 2325 if (!type.isArray() || (item is ParameterItem && item.isVarArgs())) { 2326 return 2327 } 2328 2329 when (typeString) { 2330 "java.lang.String[]", 2331 "byte[]", 2332 "short[]", 2333 "int[]", 2334 "long[]", 2335 "float[]", 2336 "double[]", 2337 "boolean[]", 2338 "char[]" -> { 2339 return 2340 } 2341 else -> { 2342 val action = when (item) { 2343 is MethodItem -> { 2344 if (item.name() == "values" && item.containingClass().isEnum()) { 2345 return 2346 } 2347 if (item.containingClass().extends("java.lang.annotation.Annotation")) { 2348 // Annotation are allowed to use arrays 2349 return 2350 } 2351 "Method should return" 2352 } 2353 is FieldItem -> "Field should be" 2354 else -> "Method parameter should be" 2355 } 2356 val component = type.asClass()?.simpleName() ?: "" 2357 report( 2358 ARRAY_RETURN, item, 2359 "$action Collection<$component> (or subclass) instead of raw array; was `$typeString`" 2360 ) 2361 } 2362 } 2363 } 2364 2365 private fun checkUserHandle(cls: ClassItem, methods: Sequence<MethodItem>) { 2366 val qualifiedName = cls.qualifiedName() 2367 if (qualifiedName == "android.app.admin.DeviceAdminReceiver" || 2368 qualifiedName == "android.content.pm.LauncherApps" || 2369 qualifiedName == "android.os.UserHandle" || 2370 qualifiedName == "android.os.UserManager" 2371 ) { 2372 return 2373 } 2374 2375 for (method in methods) { 2376 val parameters = method.parameters() 2377 if (parameters.isEmpty()) { 2378 continue 2379 } 2380 val name = method.name() 2381 if (name.startsWith("on") && onCallbackNamePattern.matches(name)) { 2382 continue 2383 } 2384 val hasArg = parameters.any { it.type().toTypeString() == "android.os.UserHandle" } 2385 if (!hasArg) { 2386 continue 2387 } 2388 if (qualifiedName.endsWith("Manager")) { 2389 report( 2390 USER_HANDLE, method, 2391 "When a method overload is needed to target a specific " + 2392 "UserHandle, callers should be directed to use " + 2393 "Context.createPackageContextAsUser() and re-obtain the relevant " + 2394 "Manager, and no new API should be added" 2395 ) 2396 } else if (!(name.endsWith("AsUser") || name.endsWith("ForUser"))) { 2397 report( 2398 USER_HANDLE_NAME, method, 2399 "Method taking UserHandle should be named `doFooAsUser` or `queryFooForUser`, was `$name`" 2400 ) 2401 } 2402 } 2403 } 2404 2405 private fun checkParams(cls: ClassItem) { 2406 val qualifiedName = cls.qualifiedName() 2407 for (suffix in badParameterClassNames) { 2408 if (qualifiedName.endsWith(suffix) && !( 2409 ( 2410 qualifiedName.endsWith("Params") || 2411 qualifiedName == "android.app.ActivityOptions" || 2412 qualifiedName == "android.app.BroadcastOptions" || 2413 qualifiedName == "android.os.Bundle" || 2414 qualifiedName == "android.os.BaseBundle" || 2415 qualifiedName == "android.os.PersistableBundle" 2416 ) 2417 ) 2418 ) { 2419 report( 2420 USER_HANDLE_NAME, cls, 2421 "Classes holding a set of parameters should be called `FooParams`, was `${cls.simpleName()}`" 2422 ) 2423 } 2424 } 2425 } 2426 2427 private fun checkServices(field: FieldItem) { 2428 val type = field.type() 2429 if (!type.isString() || !field.modifiers.isFinal() || !field.modifiers.isStatic() || 2430 field.containingClass().qualifiedName() != "android.content.Context" 2431 ) { 2432 return 2433 } 2434 val name = field.name() 2435 val endsWithService = name.endsWith("_SERVICE") 2436 val value = field.initialValue(requireConstant = true) as? String 2437 2438 if (value == null) { 2439 val mustEndInService = 2440 if (!endsWithService) " and its name must end with `_SERVICE`" else "" 2441 2442 report( 2443 SERVICE_NAME, field, 2444 "Non-constant service constant `$name`. Must be static," + 2445 " final and initialized with a String literal$mustEndInService." 2446 ) 2447 return 2448 } 2449 2450 if (name.endsWith("_MANAGER_SERVICE")) { 2451 report( 2452 SERVICE_NAME, field, 2453 "Inconsistent service constant name; expected " + 2454 "`${name.removeSuffix("_MANAGER_SERVICE")}_SERVICE`, was `$name`" 2455 ) 2456 } else if (endsWithService) { 2457 val service = name.substring(0, name.length - "_SERVICE".length).lowercase(Locale.US) 2458 if (service != value) { 2459 report( 2460 SERVICE_NAME, field, 2461 "Inconsistent service value; expected `$service`, was `$value` (Note: Do not" + 2462 " change the name of already released services, which will break tools" + 2463 " using `adb shell dumpsys`." + 2464 " Instead add `@SuppressLint(\"${SERVICE_NAME.name}\"))`" 2465 ) 2466 } 2467 } else { 2468 val valueUpper = value.uppercase(Locale.US) 2469 report( 2470 SERVICE_NAME, field, 2471 "Inconsistent service constant name;" + 2472 " expected `${valueUpper}_SERVICE`, was `$name`" 2473 ) 2474 } 2475 } 2476 2477 private fun checkTense(method: MethodItem) { 2478 val name = method.name() 2479 if (name.endsWith("Enable")) { 2480 if (method.containingClass().qualifiedName().startsWith("android.opengl")) { 2481 return 2482 } 2483 report( 2484 METHOD_NAME_TENSE, method, 2485 "Unexpected tense; probably meant `enabled`, was `$name`" 2486 ) 2487 } 2488 } 2489 2490 private fun checkIcu(type: TypeItem, typeString: String, item: Item) { 2491 if (type.primitive) { 2492 return 2493 } 2494 // ICU types have been added in API 24, so libraries with minSdkVersion <24 cannot use them. 2495 // If the version is not set, then keep the check enabled. 2496 val minSdkVersion = codebase.getMinSdkVersion() 2497 if (minSdkVersion is SetMinSdkVersion && minSdkVersion.value < 24) { 2498 return 2499 } 2500 val better = when (typeString) { 2501 "java.util.TimeZone" -> "android.icu.util.TimeZone" 2502 "java.util.Calendar" -> "android.icu.util.Calendar" 2503 "java.util.Locale" -> "android.icu.util.ULocale" 2504 "java.util.ResourceBundle" -> "android.icu.util.UResourceBundle" 2505 "java.util.SimpleTimeZone" -> "android.icu.util.SimpleTimeZone" 2506 "java.util.StringTokenizer" -> "android.icu.util.StringTokenizer" 2507 "java.util.GregorianCalendar" -> "android.icu.util.GregorianCalendar" 2508 "java.lang.Character" -> "android.icu.lang.UCharacter" 2509 "java.text.BreakIterator" -> "android.icu.text.BreakIterator" 2510 "java.text.Collator" -> "android.icu.text.Collator" 2511 "java.text.DecimalFormatSymbols" -> "android.icu.text.DecimalFormatSymbols" 2512 "java.text.NumberFormat" -> "android.icu.text.NumberFormat" 2513 "java.text.DateFormatSymbols" -> "android.icu.text.DateFormatSymbols" 2514 "java.text.DateFormat" -> "android.icu.text.DateFormat" 2515 "java.text.SimpleDateFormat" -> "android.icu.text.SimpleDateFormat" 2516 "java.text.MessageFormat" -> "android.icu.text.MessageFormat" 2517 "java.text.DecimalFormat" -> "android.icu.text.DecimalFormat" 2518 else -> return 2519 } 2520 report( 2521 USE_ICU, item, 2522 "Type `$typeString` should be replaced with richer ICU type `$better`" 2523 ) 2524 } 2525 2526 private fun checkClone(method: MethodItem) { 2527 if (method.name() == "clone" && method.parameters().isEmpty()) { 2528 report( 2529 NO_CLONE, method, 2530 "Provide an explicit copy constructor instead of implementing `clone()`" 2531 ) 2532 } 2533 } 2534 2535 private fun checkPfd(type: String, item: Item) { 2536 if (item.containingClass()?.qualifiedName() in lowLevelFileClassNames || 2537 isServiceDumpMethod(item) 2538 ) { 2539 return 2540 } 2541 2542 if (type == "java.io.FileDescriptor") { 2543 report( 2544 USE_PARCEL_FILE_DESCRIPTOR, item, 2545 "Must use ParcelFileDescriptor instead of FileDescriptor in ${item.describe()}" 2546 ) 2547 } else if (type == "int" && item is MethodItem) { 2548 val name = item.name() 2549 if (name.contains("Fd") || name.contains("FD") || name.contains("FileDescriptor", ignoreCase = true)) { 2550 report( 2551 USE_PARCEL_FILE_DESCRIPTOR, item, 2552 "Must use ParcelFileDescriptor instead of FileDescriptor in ${item.describe()}" 2553 ) 2554 } 2555 } 2556 } 2557 2558 private fun checkNumbers(type: String, item: Item) { 2559 if (type == "short" || type == "byte") { 2560 report( 2561 NO_BYTE_OR_SHORT, item, 2562 "Should avoid odd sized primitives; use `int` instead of `$type` in ${item.describe()}" 2563 ) 2564 } 2565 } 2566 2567 private fun checkSingleton( 2568 cls: ClassItem, 2569 methods: Sequence<MethodItem>, 2570 constructors: Sequence<ConstructorItem> 2571 ) { 2572 if (constructors.none()) { 2573 return 2574 } 2575 if (methods.any { it.name().startsWith("get") && it.name().endsWith("Instance") && it.modifiers.isStatic() }) { 2576 for (constructor in constructors) { 2577 report( 2578 SINGLETON_CONSTRUCTOR, constructor, 2579 "Singleton classes should use `getInstance()` methods: `${cls.simpleName()}`" 2580 ) 2581 } 2582 } 2583 } 2584 2585 private fun checkExtends(cls: ClassItem) { 2586 // Call cls.superClass().extends() instead of cls.extends() since extends returns true for self 2587 val superCls = cls.superClass() ?: return 2588 if (superCls.extends("android.os.AsyncTask")) { 2589 report( 2590 FORBIDDEN_SUPER_CLASS, cls, 2591 "${cls.simpleName()} should not extend `AsyncTask`. AsyncTask is an implementation detail. Expose a listener or, in androidx, a `ListenableFuture` API instead" 2592 ) 2593 } 2594 if (superCls.extends("android.app.Activity")) { 2595 report( 2596 FORBIDDEN_SUPER_CLASS, cls, 2597 "${cls.simpleName()} should not extend `Activity`. Activity subclasses are impossible to compose. Expose a composable API instead." 2598 ) 2599 } 2600 badFutureTypes.firstOrNull { cls.extendsOrImplements(it) }?.let { 2601 val extendOrImplement = if (cls.extends(it)) "extend" else "implement" 2602 report( 2603 BAD_FUTURE, cls, 2604 "${cls.simpleName()} should not $extendOrImplement `$it`." + 2605 " In AndroidX, use (but do not extend) ListenableFuture. In platform, use a combination of OutcomeReceiver<R,E>, Executor, and CancellationSignal`." 2606 ) 2607 } 2608 } 2609 2610 private fun checkTypedef(cls: ClassItem) { 2611 if (cls.isAnnotationType()) { 2612 cls.modifiers.annotations().firstOrNull { it.isTypeDefAnnotation() }?.let { 2613 report(PUBLIC_TYPEDEF, cls, "Don't expose ${AnnotationItem.simpleName(it)}: ${cls.simpleName()} must be hidden.") 2614 } 2615 } 2616 } 2617 2618 private fun checkUri(typeString: String, item: Item) { 2619 badUriTypes.firstOrNull { typeString.contains(it) }?.let { 2620 report( 2621 ANDROID_URI, item, "Use android.net.Uri instead of $it (${item.describe()})" 2622 ) 2623 } 2624 } 2625 2626 private fun checkFutures(typeString: String, item: Item) { 2627 badFutureTypes.firstOrNull { typeString.contains(it) }?.let { 2628 report( 2629 BAD_FUTURE, item, 2630 "Use ListenableFuture (library), " + 2631 "or a combination of OutcomeReceiver<R,E>, Executor, and CancellationSignal (platform) instead of $it (${item.describe()})" 2632 ) 2633 } 2634 } 2635 2636 private fun checkMethodSuffixListenableFutureReturn(type: TypeItem, method: MethodItem) { 2637 if (type.toTypeString().contains(listenableFuture) && 2638 !method.isConstructor() && 2639 !method.name().endsWith("Async") 2640 ) { 2641 report( 2642 ASYNC_SUFFIX_FUTURE, 2643 method, 2644 "Methods returning $listenableFuture should have a suffix *Async to " + 2645 "reserve unmodified name for a suspend function" 2646 ) 2647 } 2648 } 2649 2650 private fun isInteresting(cls: ClassItem): Boolean { 2651 val name = cls.qualifiedName() 2652 for (prefix in options.checkApiIgnorePrefix) { 2653 if (name.startsWith(prefix)) { 2654 return false 2655 } 2656 } 2657 return true 2658 } 2659 2660 companion object { 2661 2662 private data class GetterSetterPattern(val getter: String, val setter: String) 2663 private val goodBooleanGetterSetterPrefixes = listOf( 2664 GetterSetterPattern("has", "setHas"), 2665 GetterSetterPattern("can", "setCan"), 2666 GetterSetterPattern("should", "setShould"), 2667 GetterSetterPattern("is", "set") 2668 ) 2669 private fun List<GetterSetterPattern>.match( 2670 name: String, 2671 prop: (GetterSetterPattern) -> String 2672 ) = firstOrNull { 2673 name.startsWith(prop(it)) && name.getOrNull(prop(it).length)?.let { charAfterPrefix -> 2674 charAfterPrefix.isUpperCase() || charAfterPrefix.isDigit() 2675 } ?: false 2676 } 2677 2678 private val badBooleanGetterPrefixes = listOf("isHas", "isCan", "isShould", "get", "is") 2679 private val badBooleanSetterPrefixes = listOf("setIs", "set") 2680 2681 private val badParameterClassNames = listOf( 2682 "Param", "Parameter", "Parameters", "Args", "Arg", "Argument", "Arguments", "Options", "Bundle" 2683 ) 2684 2685 private val badUriTypes = listOf("java.net.URL", "java.net.URI", "android.net.URL") 2686 2687 private val badFutureTypes = listOf( 2688 "java.util.concurrent.CompletableFuture", 2689 "java.util.concurrent.Future" 2690 ) 2691 2692 private val listenableFuture = "com.google.common.util.concurrent.ListenableFuture" 2693 2694 /** 2695 * Classes for manipulating file descriptors directly, where using ParcelFileDescriptor 2696 * isn't required 2697 */ 2698 private val lowLevelFileClassNames = listOf( 2699 "android.os.FileUtils", 2700 "android.system.Os", 2701 "android.net.util.SocketUtils", 2702 "android.os.NativeHandle", 2703 "android.os.ParcelFileDescriptor" 2704 ) 2705 2706 /** 2707 * Classes which already use bare fields extensively, and bare fields are thus allowed for 2708 * consistency with existing API surface. 2709 */ 2710 private val classesWithBareFields = listOf( 2711 "android.app.ActivityManager.RecentTaskInfo", 2712 "android.app.Notification", 2713 "android.content.pm.ActivityInfo", 2714 "android.content.pm.ApplicationInfo", 2715 "android.content.pm.ComponentInfo", 2716 "android.content.pm.ResolveInfo", 2717 "android.content.pm.FeatureGroupInfo", 2718 "android.content.pm.InstrumentationInfo", 2719 "android.content.pm.PackageInfo", 2720 "android.content.pm.PackageItemInfo", 2721 "android.content.res.Configuration", 2722 "android.graphics.BitmapFactory.Options", 2723 "android.os.Message", 2724 "android.system.StructPollfd" 2725 ) 2726 2727 /** 2728 * Classes containing setting provider keys. 2729 */ 2730 private val settingsKeyClasses = listOf( 2731 "android.provider.Settings.Global", 2732 "android.provider.Settings.Secure", 2733 "android.provider.Settings.System" 2734 ) 2735 2736 private val badUnits = mapOf( 2737 "Ns" to "Nanos", 2738 "Ms" to "Millis or Micros", 2739 "Sec" to "Seconds", 2740 "Secs" to "Seconds", 2741 "Hr" to "Hours", 2742 "Hrs" to "Hours", 2743 "Mo" to "Months", 2744 "Mos" to "Months", 2745 "Yr" to "Years", 2746 "Yrs" to "Years", 2747 "Byte" to "Bytes", 2748 "Space" to "Bytes" 2749 ) 2750 private val uiPackageParts = listOf( 2751 "animation", 2752 "view", 2753 "graphics", 2754 "transition", 2755 "widget", 2756 "webkit" 2757 ) 2758 2759 private val constantNamePattern = Regex("[A-Z0-9_]+") 2760 private val internalNamePattern = Regex("[ms][A-Z0-9].*") 2761 private val fieldNamePattern = Regex("[a-z].*") 2762 private val onCallbackNamePattern = Regex("on[A-Z][a-z0-9][a-zA-Z0-9]*") 2763 private val configFieldPattern = Regex("config_[a-z][a-zA-Z0-9]*") 2764 private val layoutFieldPattern = Regex("layout_[a-z][a-zA-Z0-9]*") 2765 private val stateFieldPattern = Regex("state_[a-z_]+") 2766 private val resourceFileFieldPattern = Regex("[a-z0-9_]+") 2767 private val resourceValueFieldPattern = Regex("[a-z][a-zA-Z0-9]*") 2768 private val styleFieldPattern = Regex("[A-Z][A-Za-z0-9]+(_[A-Z][A-Za-z0-9]+?)*") 2769 2770 private val acronymPattern2 = Regex("([A-Z]){2,}") 2771 private val acronymPattern3 = Regex("([A-Z]){3,}") 2772 2773 private val serviceDumpMethodParameterTypes = 2774 listOf("java.io.FileDescriptor", "java.io.PrintWriter", "java.lang.String[]") 2775 2776 private fun isServiceDumpMethod(item: Item) = when (item) { 2777 is MethodItem -> isServiceDumpMethod(item) 2778 is ParameterItem -> isServiceDumpMethod(item.containingMethod()) 2779 else -> false 2780 } 2781 2782 private fun isServiceDumpMethod(item: MethodItem) = item.name() == "dump" && 2783 item.containingClass().extends("android.app.Service") && 2784 item.parameters().map { it.type().toTypeString() } == serviceDumpMethodParameterTypes 2785 2786 private fun hasAcronyms(name: String): Boolean { 2787 // Require 3 capitals, or 2 if it's at the end of a word. 2788 val result = acronymPattern2.find(name) ?: return false 2789 return result.range.first == name.length - 2 || acronymPattern3.find(name) != null 2790 } 2791 2792 private fun getFirstAcronym(name: String): String? { 2793 // Require 3 capitals, or 2 if it's at the end of a word. 2794 val result = acronymPattern2.find(name) ?: return null 2795 if (result.range.first == name.length - 2) { 2796 return name.substring(name.length - 2) 2797 } 2798 val result2 = acronymPattern3.find(name) 2799 return if (result2 != null) { 2800 name.substring(result2.range.first, result2.range.last + 1) 2801 } else { 2802 null 2803 } 2804 } 2805 2806 /** for something like "HTMLWriter", returns "HtmlWriter" */ 2807 private fun decapitalizeAcronyms(name: String): String { 2808 var s = name 2809 2810 if (s.none { it.isLowerCase() }) { 2811 // The entire thing is capitalized. If so, just perform 2812 // normal capitalization, but try dropping _'s. 2813 return SdkVersionInfo.underlinesToCamelCase(s.lowercase(Locale.US)) 2814 .replaceFirstChar { 2815 if (it.isLowerCase()) { 2816 it.titlecase(Locale.getDefault()) 2817 } else { 2818 it.toString() 2819 } 2820 } 2821 } 2822 2823 while (true) { 2824 val acronym = getFirstAcronym(s) ?: return s 2825 val index = s.indexOf(acronym) 2826 if (index == -1) { 2827 return s 2828 } 2829 // The last character, if not the end of the string, is probably the beginning of the 2830 // next word so capitalize it 2831 s = if (index == s.length - acronym.length) { 2832 // acronym at the end of the word word 2833 val decapitalized = acronym[0] + acronym.substring(1).lowercase(Locale.US) 2834 s.replace(acronym, decapitalized) 2835 } else { 2836 val replacement = acronym[0] + acronym.substring( 2837 1, 2838 acronym.length - 1 2839 ).lowercase(Locale.US) + acronym[acronym.length - 1] 2840 s.replace(acronym, replacement) 2841 } 2842 } 2843 } 2844 2845 fun check(codebase: Codebase, oldCodebase: Codebase?, reporter: Reporter) { 2846 ApiLint(codebase, oldCodebase, reporter).check() 2847 } 2848 } 2849 } 2850 2851 internal const val DefaultLintErrorMessage = """ 2852 ************************************************************ 2853 Your API changes are triggering API Lint warnings or errors. 2854 To make these errors go away, fix the code according to the 2855 error and/or warning messages above. 2856 2857 If it's not possible to do so, there are two workarounds: 2858 2859 1. Suppress the issues with @Suppress("<id>") / @SuppressWarnings("<id>") 2860 2. Update the baseline passed into metalava 2861 ************************************************************ 2862 """ 2863