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