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.AUTO_BOXING 56 import com.android.tools.metalava.Issues.BAD_FUTURE 57 import com.android.tools.metalava.Issues.BANNED_THROW 58 import com.android.tools.metalava.Issues.BUILDER_SET_STYLE 59 import com.android.tools.metalava.Issues.CALLBACK_INTERFACE 60 import com.android.tools.metalava.Issues.CALLBACK_METHOD_NAME 61 import com.android.tools.metalava.Issues.CALLBACK_NAME 62 import com.android.tools.metalava.Issues.COMMON_ARGS_FIRST 63 import com.android.tools.metalava.Issues.COMPILE_TIME_CONSTANT 64 import com.android.tools.metalava.Issues.CONCRETE_COLLECTION 65 import com.android.tools.metalava.Issues.CONFIG_FIELD_NAME 66 import com.android.tools.metalava.Issues.CONSISTENT_ARGUMENT_ORDER 67 import com.android.tools.metalava.Issues.CONTEXT_FIRST 68 import com.android.tools.metalava.Issues.CONTEXT_NAME_SUFFIX 69 import com.android.tools.metalava.Issues.ENDS_WITH_IMPL 70 import com.android.tools.metalava.Issues.ENUM 71 import com.android.tools.metalava.Issues.EQUALS_AND_HASH_CODE 72 import com.android.tools.metalava.Issues.EXCEPTION_NAME 73 import com.android.tools.metalava.Issues.EXECUTOR_REGISTRATION 74 import com.android.tools.metalava.Issues.EXTENDS_ERROR 75 import com.android.tools.metalava.Issues.FORBIDDEN_SUPER_CLASS 76 import com.android.tools.metalava.Issues.FRACTION_FLOAT 77 import com.android.tools.metalava.Issues.GENERIC_EXCEPTION 78 import com.android.tools.metalava.Issues.GETTER_ON_BUILDER 79 import com.android.tools.metalava.Issues.GETTER_SETTER_NAMES 80 import com.android.tools.metalava.Issues.HEAVY_BIT_SET 81 import com.android.tools.metalava.Issues.ILLEGAL_STATE_EXCEPTION 82 import com.android.tools.metalava.Issues.INTENT_BUILDER_NAME 83 import com.android.tools.metalava.Issues.INTENT_NAME 84 import com.android.tools.metalava.Issues.INTERFACE_CONSTANT 85 import com.android.tools.metalava.Issues.INTERNAL_CLASSES 86 import com.android.tools.metalava.Issues.INTERNAL_FIELD 87 import com.android.tools.metalava.Issues.Issue 88 import com.android.tools.metalava.Issues.KOTLIN_OPERATOR 89 import com.android.tools.metalava.Issues.LISTENER_INTERFACE 90 import com.android.tools.metalava.Issues.LISTENER_LAST 91 import com.android.tools.metalava.Issues.MANAGER_CONSTRUCTOR 92 import com.android.tools.metalava.Issues.MANAGER_LOOKUP 93 import com.android.tools.metalava.Issues.MENTIONS_GOOGLE 94 import com.android.tools.metalava.Issues.METHOD_NAME_TENSE 95 import com.android.tools.metalava.Issues.METHOD_NAME_UNITS 96 import com.android.tools.metalava.Issues.MIN_MAX_CONSTANT 97 import com.android.tools.metalava.Issues.MISSING_BUILD_METHOD 98 import com.android.tools.metalava.Issues.MISSING_GETTER_MATCHING_BUILDER 99 import com.android.tools.metalava.Issues.MISSING_NULLABILITY 100 import com.android.tools.metalava.Issues.MUTABLE_BARE_FIELD 101 import com.android.tools.metalava.Issues.NOT_CLOSEABLE 102 import com.android.tools.metalava.Issues.NO_BYTE_OR_SHORT 103 import com.android.tools.metalava.Issues.NO_CLONE 104 import com.android.tools.metalava.Issues.NO_SETTINGS_PROVIDER 105 import com.android.tools.metalava.Issues.NULLABLE_COLLECTION 106 import com.android.tools.metalava.Issues.ON_NAME_EXPECTED 107 import com.android.tools.metalava.Issues.OPTIONAL_BUILDER_CONSTRUCTOR_ARGUMENT 108 import com.android.tools.metalava.Issues.OVERLAPPING_CONSTANTS 109 import com.android.tools.metalava.Issues.PACKAGE_LAYERING 110 import com.android.tools.metalava.Issues.PAIRED_REGISTRATION 111 import com.android.tools.metalava.Issues.PARCELABLE_LIST 112 import com.android.tools.metalava.Issues.PARCEL_CONSTRUCTOR 113 import com.android.tools.metalava.Issues.PARCEL_CREATOR 114 import com.android.tools.metalava.Issues.PARCEL_NOT_FINAL 115 import com.android.tools.metalava.Issues.PERCENTAGE_INT 116 import com.android.tools.metalava.Issues.PROTECTED_MEMBER 117 import com.android.tools.metalava.Issues.PUBLIC_TYPEDEF 118 import com.android.tools.metalava.Issues.RAW_AIDL 119 import com.android.tools.metalava.Issues.REGISTRATION_NAME 120 import com.android.tools.metalava.Issues.RESOURCE_FIELD_NAME 121 import com.android.tools.metalava.Issues.RESOURCE_STYLE_FIELD_NAME 122 import com.android.tools.metalava.Issues.RESOURCE_VALUE_FIELD_NAME 123 import com.android.tools.metalava.Issues.RETHROW_REMOTE_EXCEPTION 124 import com.android.tools.metalava.Issues.SERVICE_NAME 125 import com.android.tools.metalava.Issues.SETTER_RETURNS_THIS 126 import com.android.tools.metalava.Issues.SINGLETON_CONSTRUCTOR 127 import com.android.tools.metalava.Issues.SINGLE_METHOD_INTERFACE 128 import com.android.tools.metalava.Issues.SINGULAR_CALLBACK 129 import com.android.tools.metalava.Issues.START_WITH_LOWER 130 import com.android.tools.metalava.Issues.START_WITH_UPPER 131 import com.android.tools.metalava.Issues.STATIC_FINAL_BUILDER 132 import com.android.tools.metalava.Issues.STATIC_UTILS 133 import com.android.tools.metalava.Issues.STREAM_FILES 134 import com.android.tools.metalava.Issues.TOP_LEVEL_BUILDER 135 import com.android.tools.metalava.Issues.UNIQUE_KOTLIN_OPERATOR 136 import com.android.tools.metalava.Issues.USER_HANDLE 137 import com.android.tools.metalava.Issues.USER_HANDLE_NAME 138 import com.android.tools.metalava.Issues.USE_ICU 139 import com.android.tools.metalava.Issues.USE_PARCEL_FILE_DESCRIPTOR 140 import com.android.tools.metalava.Issues.VISIBLY_SYNCHRONIZED 141 import com.android.tools.metalava.model.AnnotationItem 142 import com.android.tools.metalava.model.AnnotationItem.Companion.getImplicitNullness 143 import com.android.tools.metalava.model.ClassItem 144 import com.android.tools.metalava.model.Codebase 145 import com.android.tools.metalava.model.ConstructorItem 146 import com.android.tools.metalava.model.FieldItem 147 import com.android.tools.metalava.model.Item 148 import com.android.tools.metalava.model.MemberItem 149 import com.android.tools.metalava.model.MethodItem 150 import com.android.tools.metalava.model.PackageItem 151 import com.android.tools.metalava.model.ParameterItem 152 import com.android.tools.metalava.model.SetMinSdkVersion 153 import com.android.tools.metalava.model.TypeItem 154 import com.android.tools.metalava.model.psi.PsiMethodItem 155 import com.android.tools.metalava.model.visitors.ApiVisitor 156 import com.intellij.psi.JavaRecursiveElementVisitor 157 import com.intellij.psi.PsiClassObjectAccessExpression 158 import com.intellij.psi.PsiElement 159 import com.intellij.psi.PsiSynchronizedStatement 160 import com.intellij.psi.PsiThisExpression 161 import org.jetbrains.uast.UCallExpression 162 import org.jetbrains.uast.UClassLiteralExpression 163 import org.jetbrains.uast.UMethod 164 import org.jetbrains.uast.UQualifiedReferenceExpression 165 import org.jetbrains.uast.UThisExpression 166 import org.jetbrains.uast.visitor.AbstractUastVisitor 167 import java.util.Locale 168 import java.util.function.Predicate 169 170 /** 171 * The [ApiLint] analyzer checks the API against a known set of preferred API practices 172 * by the Android API council. 173 */ 174 class ApiLint(private val codebase: Codebase, private val oldCodebase: Codebase?, private val reporter: Reporter) : ApiVisitor( 175 // Sort by source order such that warnings follow source line number order 176 methodComparator = MethodItem.sourceOrderComparator, 177 fieldComparator = FieldItem.comparator, 178 ignoreShown = options.showUnannotated, 179 // No need to check "for stubs only APIs" (== "implicit" APIs) 180 includeApisForStubPurposes = false 181 ) { 182 private fun report(id: Issue, item: Item, message: String, element: PsiElement? = null) { 183 // Don't flag api warnings on deprecated APIs; these are obviously already known to 184 // be problematic. 185 if (item.deprecated) { 186 return 187 } 188 189 if (item is ParameterItem && item.containingMethod().deprecated) { 190 return 191 } 192 193 // With show annotations we might be flagging API that is filtered out: hide these here 194 val testItem = if (item is ParameterItem) item.containingMethod() else item 195 if (!filterEmit.test(testItem)) { 196 return 197 } 198 199 reporter.report(id, item, message, element) 200 } 201 202 private fun check() { 203 if (oldCodebase != null) { 204 // Only check the new APIs 205 CodebaseComparator().compare(object : ComparisonVisitor() { 206 override fun added(new: Item) { 207 new.accept(this@ApiLint) 208 } 209 }, oldCodebase, codebase, filterReference) 210 } else { 211 // No previous codebase to compare with: visit the whole thing 212 codebase.accept(this) 213 } 214 } 215 216 override fun skip(item: Item): Boolean { 217 return super.skip(item) || 218 item is ClassItem && !isInteresting(item) || 219 item is MethodItem && !isInteresting(item.containingClass()) || 220 item is FieldItem && !isInteresting(item.containingClass()) 221 } 222 223 private val kotlinInterop = KotlinInteropChecks(reporter) 224 225 override fun visitClass(cls: ClassItem) { 226 val methods = cls.filteredMethods(filterReference).asSequence() 227 val fields = cls.filteredFields(filterReference, showUnannotated).asSequence() 228 val constructors = cls.filteredConstructors(filterReference) 229 val superClass = cls.filteredSuperclass(filterReference) 230 val interfaces = cls.filteredInterfaceTypes(filterReference).asSequence() 231 val allMethods = methods.asSequence() + constructors.asSequence() 232 checkClass( 233 cls, methods, constructors, allMethods, fields, superClass, interfaces, 234 filterReference 235 ) 236 } 237 238 override fun visitMethod(method: MethodItem) { 239 checkMethod(method, filterReference) 240 val returnType = method.returnType() 241 if (returnType != null) { 242 checkType(returnType, method) 243 checkNullableCollections(returnType, method) 244 } 245 for (parameter in method.parameters()) { 246 checkType(parameter.type(), parameter) 247 } 248 kotlinInterop.checkMethod(method) 249 } 250 251 override fun visitField(field: FieldItem) { 252 checkField(field) 253 checkType(field.type(), field) 254 kotlinInterop.checkField(field) 255 } 256 257 private fun checkType(type: TypeItem, item: Item) { 258 val typeString = type.toTypeString() 259 checkPfd(typeString, item) 260 checkNumbers(typeString, item) 261 checkCollections(type, item) 262 checkCollectionsOverArrays(type, typeString, item) 263 checkBoxed(type, item) 264 checkIcu(type, typeString, item) 265 checkBitSet(type, typeString, item) 266 checkHasNullability(item) 267 checkUri(typeString, item) 268 checkFutures(typeString, item) 269 } 270 271 private fun checkClass( 272 cls: ClassItem, 273 methods: Sequence<MethodItem>, 274 constructors: Sequence<ConstructorItem>, 275 methodsAndConstructors: Sequence<MethodItem>, 276 fields: Sequence<FieldItem>, 277 superClass: ClassItem?, 278 interfaces: Sequence<TypeItem>, 279 filterReference: Predicate<Item> 280 ) { 281 checkEquals(methods) 282 checkEnums(cls) 283 checkClassNames(cls) 284 checkCallbacks(cls) 285 checkListeners(cls, methods) 286 checkParcelable(cls, methods, constructors, fields) 287 checkRegistrationMethods(cls, methods) 288 checkHelperClasses(cls, methods, fields) 289 checkBuilder(cls, methods, constructors, superClass) 290 checkAidl(cls, superClass, interfaces) 291 checkInternal(cls) 292 checkLayering(cls, methodsAndConstructors, fields) 293 checkBooleans(methods) 294 checkFlags(fields) 295 checkGoogle(cls, methods, fields) 296 checkManager(cls, methods, constructors) 297 checkStaticUtils(cls, methods, constructors, fields) 298 checkCallbackHandlers(cls, methodsAndConstructors, superClass) 299 checkResourceNames(cls, fields) 300 checkFiles(methodsAndConstructors) 301 checkManagerList(cls, methods) 302 checkAbstractInner(cls) 303 checkRuntimeExceptions(methodsAndConstructors, filterReference) 304 checkError(cls, superClass) 305 checkCloseable(cls, methods) 306 checkNotKotlinOperator(methods) 307 checkUserHandle(cls, methods) 308 checkParams(cls) 309 checkSingleton(cls, methods, constructors) 310 checkExtends(cls) 311 checkTypedef(cls) 312 313 // TODO: Not yet working 314 // checkOverloadArgs(cls, methods) 315 } 316 317 private fun checkField( 318 field: FieldItem 319 ) { 320 val modifiers = field.modifiers 321 if (modifiers.isStatic() && modifiers.isFinal()) { 322 checkConstantNames(field) 323 checkActions(field) 324 checkIntentExtras(field) 325 } 326 checkProtected(field) 327 checkServices(field) 328 checkFieldName(field) 329 checkSettingKeys(field) 330 checkNullableCollections(field.type(), field) 331 } 332 333 private fun checkMethod( 334 method: MethodItem, 335 filterReference: Predicate<Item> 336 ) { 337 if (!method.isConstructor()) { 338 checkMethodNames(method) 339 checkProtected(method) 340 checkSynchronized(method) 341 checkIntentBuilder(method) 342 checkUnits(method) 343 checkTense(method) 344 checkClone(method) 345 checkCallbackOrListenerMethod(method) 346 } 347 checkExceptions(method, filterReference) 348 checkContextFirst(method) 349 checkListenerLast(method) 350 } 351 352 private fun checkEnums(cls: ClassItem) { 353 /* 354 def verify_enums(clazz): 355 """Enums are bad, mmkay?""" 356 if "extends java.lang.Enum" in clazz.raw: 357 error(clazz, None, "F5", "Enums are not allowed") 358 */ 359 if (cls.isEnum()) { 360 report(ENUM, cls, "Enums are discouraged in Android APIs") 361 } 362 } 363 364 private fun checkMethodNames(method: MethodItem) { 365 /* 366 def verify_method_names(clazz): 367 """Try catching malformed method names, like Foo() or getMTU().""" 368 if clazz.fullname.startswith("android.opengl"): return 369 if clazz.fullname.startswith("android.renderscript"): return 370 if clazz.fullname == "android.system.OsConstants": return 371 372 for m in clazz.methods: 373 if re.search("[A-Z]{2,}", m.name) is not None: 374 warn(clazz, m, "S1", "Method names with acronyms should be getMtu() instead of getMTU()") 375 if re.match("[^a-z]", m.name): 376 error(clazz, m, "S1", "Method name must start with lowercase char") 377 */ 378 379 // Existing violations 380 val containing = method.containingClass().qualifiedName() 381 if (containing.startsWith("android.opengl") || 382 containing.startsWith("android.renderscript") || 383 containing.startsWith("android.database.sqlite.") || 384 containing == "android.system.OsConstants" 385 ) { 386 return 387 } 388 389 val name = if (method.isKotlin() && method.name().contains("-")) { 390 // Kotlin renames certain methods in binary, e.g. fun foo(bar: Bar) where Bar is an 391 // inline class becomes foo-HASHCODE. We only want to consider the original name for 392 // this API lint check 393 method.name().substringBefore("-") 394 } else { 395 method.name() 396 } 397 val first = name[0] 398 399 when { 400 first !in 'a'..'z' -> report(START_WITH_LOWER, method, "Method name must start with lowercase char: $name") 401 hasAcronyms(name) -> { 402 report( 403 ACRONYM_NAME, method, 404 "Acronyms should not be capitalized in method names: was `$name`, should this be `${decapitalizeAcronyms( 405 name 406 )}`?" 407 ) 408 } 409 } 410 } 411 412 private fun checkClassNames(cls: ClassItem) { 413 /* 414 def verify_class_names(clazz): 415 """Try catching malformed class names like myMtp or MTPUser.""" 416 if clazz.fullname.startswith("android.opengl"): return 417 if clazz.fullname.startswith("android.renderscript"): return 418 if re.match("android\.R\.[a-z]+", clazz.fullname): return 419 420 if re.search("[A-Z]{2,}", clazz.name) is not None: 421 warn(clazz, None, "S1", "Class names with acronyms should be Mtp not MTP") 422 if re.match("[^A-Z]", clazz.name): 423 error(clazz, None, "S1", "Class must start with uppercase char") 424 if clazz.name.endswith("Impl"): 425 error(clazz, None, None, "Don't expose your implementation details") 426 */ 427 428 // Existing violations 429 val qualifiedName = cls.qualifiedName() 430 if (qualifiedName.startsWith("android.opengl") || 431 qualifiedName.startsWith("android.renderscript") || 432 qualifiedName.startsWith("android.database.sqlite.") || 433 qualifiedName.startsWith("android.R.") 434 ) { 435 return 436 } 437 438 val name = cls.simpleName() 439 val first = name[0] 440 when { 441 first !in 'A'..'Z' -> { 442 report( 443 START_WITH_UPPER, cls, 444 "Class must start with uppercase char: $name" 445 ) 446 } 447 hasAcronyms(name) -> { 448 report( 449 ACRONYM_NAME, cls, 450 "Acronyms should not be capitalized in class names: was `$name`, should this be `${decapitalizeAcronyms( 451 name 452 )}`?" 453 ) 454 } 455 name.endsWith("Impl") -> { 456 report( 457 ENDS_WITH_IMPL, cls, 458 "Don't expose your implementation details: `$name` ends with `Impl`" 459 ) 460 } 461 } 462 } 463 464 private fun checkConstantNames(field: FieldItem) { 465 /* 466 def verify_constants(clazz): 467 """All static final constants must be FOO_NAME style.""" 468 if re.match("android\.R\.[a-z]+", clazz.fullname): return 469 if clazz.fullname.startswith("android.os.Build"): return 470 if clazz.fullname == "android.system.OsConstants": return 471 472 req = ["java.lang.String","byte","short","int","long","float","double","boolean","char"] 473 for f in clazz.fields: 474 if "static" in f.split and "final" in f.split: 475 if re.match("[A-Z0-9_]+", f.name) is None: 476 error(clazz, f, "C2", "Constant field names must be FOO_NAME") 477 if f.typ != "java.lang.String": 478 if f.name.startswith("MIN_") or f.name.startswith("MAX_"): 479 warn(clazz, f, "C8", "If min/max could change in future, make them dynamic methods") 480 if f.typ in req and f.value is None: 481 error(clazz, f, None, "All constants must be defined at compile time") 482 */ 483 484 // Skip this check on Kotlin 485 if (field.isKotlin()) { 486 return 487 } 488 489 // Existing violations 490 val qualified = field.containingClass().qualifiedName() 491 if (qualified.startsWith("android.os.Build") || 492 qualified == "android.system.OsConstants" || 493 qualified == "android.media.MediaCodecInfo" || 494 qualified.startsWith("android.opengl.") || 495 qualified.startsWith("android.R.") 496 ) { 497 return 498 } 499 500 val name = field.name() 501 if (!constantNamePattern.matches(name)) { 502 val suggested = SdkVersionInfo.camelCaseToUnderlines(name).toUpperCase(Locale.US) 503 report( 504 ALL_UPPER, field, 505 "Constant field names must be named with only upper case characters: `$qualified#$name`, should be `$suggested`?" 506 ) 507 } else if ((name.startsWith("MIN_") || name.startsWith("MAX_")) && !field.type().isString()) { 508 report( 509 MIN_MAX_CONSTANT, field, 510 "If min/max could change in future, make them dynamic methods: $qualified#$name" 511 ) 512 } else if ((field.type().primitive || field.type().isString()) && field.initialValue(true) == null) { 513 report( 514 COMPILE_TIME_CONSTANT, field, 515 "All constants must be defined at compile time: $qualified#$name" 516 ) 517 } 518 } 519 520 private fun checkCallbacks(cls: ClassItem) { 521 /* 522 def verify_callbacks(clazz): 523 """Verify Callback classes. 524 All callback classes must be abstract. 525 All methods must follow onFoo() naming style.""" 526 if clazz.fullname == "android.speech.tts.SynthesisCallback": return 527 528 if clazz.name.endswith("Callbacks"): 529 error(clazz, None, "L1", "Callback class names should be singular") 530 if clazz.name.endswith("Observer"): 531 warn(clazz, None, "L1", "Class should be named FooCallback") 532 533 if clazz.name.endswith("Callback"): 534 if "interface" in clazz.split: 535 error(clazz, None, "CL3", "Callbacks must be abstract class to enable extension in future API levels") 536 537 for m in clazz.methods: 538 if not re.match("on[A-Z][a-z]*", m.name): 539 error(clazz, m, "L1", "Callback method names must be onFoo() style") 540 541 ) 542 */ 543 544 // Existing violations 545 val qualified = cls.qualifiedName() 546 if (qualified == "android.speech.tts.SynthesisCallback") { 547 return 548 } 549 550 val name = cls.simpleName() 551 when { 552 name.endsWith("Callbacks") -> { 553 report( 554 SINGULAR_CALLBACK, cls, 555 "Callback class names should be singular: $name" 556 ) 557 } 558 name.endsWith("Observer") -> { 559 val prefix = name.removeSuffix("Observer") 560 report( 561 CALLBACK_NAME, cls, 562 "Class should be named ${prefix}Callback" 563 ) 564 } 565 name.endsWith("Callback") -> { 566 if (cls.isInterface()) { 567 report( 568 CALLBACK_INTERFACE, cls, 569 "Callbacks must be abstract class instead of interface to enable extension in future API levels: $name" 570 ) 571 } 572 } 573 } 574 } 575 576 private fun checkCallbackOrListenerMethod(method: MethodItem) { 577 if (method.isConstructor() || method.modifiers.isStatic() || method.modifiers.isFinal()) { 578 return 579 } 580 val cls = method.containingClass() 581 582 // These are not listeners or callbacks despite their name. 583 when { 584 cls.modifiers.isFinal() -> return 585 cls.qualifiedName() == "android.telephony.ims.ImsCallSessionListener" -> return 586 } 587 588 val containingClassSimpleName = cls.simpleName() 589 val kind = when { 590 containingClassSimpleName.endsWith("Callback") -> "Callback" 591 containingClassSimpleName.endsWith("Listener") -> "Listener" 592 else -> return 593 } 594 val methodName = method.name() 595 596 if (!onCallbackNamePattern.matches(methodName)) { 597 report( 598 CALLBACK_METHOD_NAME, method, 599 "$kind method names must follow the on<Something> style: $methodName" 600 ) 601 } 602 603 for (parameter in method.parameters()) { 604 // We require nonnull collections as parameters to callback methods 605 checkNullableCollections(parameter.type(), parameter) 606 } 607 } 608 609 private fun checkListeners(cls: ClassItem, methods: Sequence<MethodItem>) { 610 /* 611 def verify_listeners(clazz): 612 """Verify Listener classes. 613 All Listener classes must be interface. 614 All methods must follow onFoo() naming style. 615 If only a single method, it must match class name: 616 interface OnFooListener { void onFoo() }""" 617 618 if clazz.name.endswith("Listener"): 619 if " abstract class " in clazz.raw: 620 error(clazz, None, "L1", "Listeners should be an interface, or otherwise renamed Callback") 621 622 for m in clazz.methods: 623 if not re.match("on[A-Z][a-z]*", m.name): 624 error(clazz, m, "L1", "Listener method names must be onFoo() style") 625 626 if len(clazz.methods) == 1 and clazz.name.startswith("On"): 627 m = clazz.methods[0] 628 if (m.name + "Listener").lower() != clazz.name.lower(): 629 error(clazz, m, "L1", "Single listener method name must match class name") 630 */ 631 632 val name = cls.simpleName() 633 if (name.endsWith("Listener")) { 634 if (cls.isClass()) { 635 report( 636 LISTENER_INTERFACE, cls, 637 "Listeners should be an interface, or otherwise renamed Callback: $name" 638 ) 639 } else { 640 if (methods.count() == 1) { 641 val method = methods.first() 642 val methodName = method.name() 643 if (methodName.startsWith("On") && 644 !("${methodName}Listener").equals(cls.simpleName(), ignoreCase = true) 645 ) { 646 report( 647 SINGLE_METHOD_INTERFACE, cls, 648 "Single listener method name must match class name" 649 ) 650 } 651 } 652 } 653 } 654 } 655 656 private fun checkActions(field: FieldItem) { 657 /* 658 def verify_actions(clazz): 659 """Verify intent actions. 660 All action names must be named ACTION_FOO. 661 All action values must be scoped by package and match name: 662 package android.foo { 663 String ACTION_BAR = "android.foo.action.BAR"; 664 }""" 665 for f in clazz.fields: 666 if f.value is None: continue 667 if f.name.startswith("EXTRA_"): continue 668 if f.name == "SERVICE_INTERFACE" or f.name == "PROVIDER_INTERFACE": continue 669 if "INTERACTION" in f.name: continue 670 671 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String": 672 if "_ACTION" in f.name or "ACTION_" in f.name or ".action." in f.value.lower(): 673 if not f.name.startswith("ACTION_"): 674 error(clazz, f, "C3", "Intent action constant name must be ACTION_FOO") 675 else: 676 if clazz.fullname == "android.content.Intent": 677 prefix = "android.intent.action" 678 elif clazz.fullname == "android.provider.Settings": 679 prefix = "android.settings" 680 elif clazz.fullname == "android.app.admin.DevicePolicyManager" or clazz.fullname == "android.app.admin.DeviceAdminReceiver": 681 prefix = "android.app.action" 682 else: 683 prefix = clazz.pkg.name + ".action" 684 expected = prefix + "." + f.name[7:] 685 if f.value != expected: 686 error(clazz, f, "C4", "Inconsistent action value; expected '%s'" % (expected)) 687 */ 688 689 val name = field.name() 690 if (name.startsWith("EXTRA_") || name == "SERVICE_INTERFACE" || name == "PROVIDER_INTERFACE") { 691 return 692 } 693 if (!field.type().isString()) { 694 return 695 } 696 val value = field.initialValue(true) as? String ?: return 697 if (!(name.contains("_ACTION") || name.contains("ACTION_") || value.contains(".action."))) { 698 return 699 } 700 val className = field.containingClass().qualifiedName() 701 when (className) { 702 "android.Manifest.permission" -> return 703 } 704 if (!name.startsWith("ACTION_")) { 705 report( 706 INTENT_NAME, field, 707 "Intent action constant name must be ACTION_FOO: $name" 708 ) 709 return 710 } 711 val prefix = when (className) { 712 "android.content.Intent" -> "android.intent.action" 713 "android.provider.Settings" -> "android.settings" 714 "android.app.admin.DevicePolicyManager", "android.app.admin.DeviceAdminReceiver" -> "android.app.action" 715 else -> field.containingClass().containingPackage().qualifiedName() + ".action" 716 } 717 val expected = prefix + "." + name.substring(7) 718 if (value != expected) { 719 report( 720 ACTION_VALUE, field, 721 "Inconsistent action value; expected `$expected`, was `$value`" 722 ) 723 } 724 } 725 726 private fun checkIntentExtras(field: FieldItem) { 727 /* 728 def verify_extras(clazz): 729 """Verify intent extras. 730 All extra names must be named EXTRA_FOO. 731 All extra values must be scoped by package and match name: 732 package android.foo { 733 String EXTRA_BAR = "android.foo.extra.BAR"; 734 }""" 735 if clazz.fullname == "android.app.Notification": return 736 if clazz.fullname == "android.appwidget.AppWidgetManager": return 737 738 for f in clazz.fields: 739 if f.value is None: continue 740 if f.name.startswith("ACTION_"): continue 741 742 if "static" in f.split and "final" in f.split and f.typ == "java.lang.String": 743 if "_EXTRA" in f.name or "EXTRA_" in f.name or ".extra" in f.value.lower(): 744 if not f.name.startswith("EXTRA_"): 745 error(clazz, f, "C3", "Intent extra must be EXTRA_FOO") 746 else: 747 if clazz.pkg.name == "android.content" and clazz.name == "Intent": 748 prefix = "android.intent.extra" 749 elif clazz.pkg.name == "android.app.admin": 750 prefix = "android.app.extra" 751 else: 752 prefix = clazz.pkg.name + ".extra" 753 expected = prefix + "." + f.name[6:] 754 if f.value != expected: 755 error(clazz, f, "C4", "Inconsistent extra value; expected '%s'" % (expected)) 756 757 758 */ 759 val className = field.containingClass().qualifiedName() 760 if (className == "android.app.Notification" || className == "android.appwidget.AppWidgetManager") { 761 return 762 } 763 764 val name = field.name() 765 if (name.startsWith("ACTION_") || !field.type().isString()) { 766 return 767 } 768 val value = field.initialValue(true) as? String ?: return 769 if (!(name.contains("_EXTRA") || name.contains("EXTRA_") || value.contains(".extra"))) { 770 return 771 } 772 if (!name.startsWith("EXTRA_")) { 773 report( 774 INTENT_NAME, field, 775 "Intent extra constant name must be EXTRA_FOO: $name" 776 ) 777 return 778 } 779 780 val packageName = field.containingClass().containingPackage().qualifiedName() 781 val prefix = when { 782 className == "android.content.Intent" -> "android.intent.extra" 783 packageName == "android.app.admin" -> "android.app.extra" 784 else -> "$packageName.extra" 785 } 786 val expected = prefix + "." + name.substring(6) 787 if (value != expected) { 788 report( 789 ACTION_VALUE, field, 790 "Inconsistent extra value; expected `$expected`, was `$value`" 791 ) 792 } 793 } 794 795 private fun checkEquals(methods: Sequence<MethodItem>) { 796 /* 797 def verify_equals(clazz): 798 """Verify that equals() and hashCode() must be overridden together.""" 799 eq = False 800 hc = False 801 for m in clazz.methods: 802 if " static " in m.raw: continue 803 if "boolean equals(java.lang.Object)" in m.raw: eq = True 804 if "int hashCode()" in m.raw: hc = True 805 if eq != hc: 806 error(clazz, None, "M8", "Must override both equals and hashCode; missing one") 807 */ 808 var equalsMethod: MethodItem? = null 809 var hashCodeMethod: MethodItem? = null 810 811 for (method in methods) { 812 if (isEqualsMethod(method)) { 813 equalsMethod = method 814 } else if (isHashCodeMethod(method)) { 815 hashCodeMethod = method 816 } 817 } 818 if ((equalsMethod == null) != (hashCodeMethod == null)) { 819 val method = equalsMethod ?: hashCodeMethod!! 820 report( 821 EQUALS_AND_HASH_CODE, method, 822 "Must override both equals and hashCode; missing one in ${method.containingClass().qualifiedName()}" 823 ) 824 } 825 } 826 827 private fun isEqualsMethod(method: MethodItem): Boolean { 828 return method.name() == "equals" && method.parameters().size == 1 && 829 method.parameters()[0].type().isJavaLangObject() && 830 !method.modifiers.isStatic() 831 } 832 833 private fun isHashCodeMethod(method: MethodItem): Boolean { 834 return method.name() == "hashCode" && method.parameters().isEmpty() && 835 !method.modifiers.isStatic() 836 } 837 838 private fun checkParcelable( 839 cls: ClassItem, 840 methods: Sequence<MethodItem>, 841 constructors: Sequence<MethodItem>, 842 fields: Sequence<FieldItem> 843 ) { 844 /* 845 def verify_parcelable(clazz): 846 """Verify that Parcelable objects aren't hiding required bits.""" 847 if "implements android.os.Parcelable" in clazz.raw: 848 creator = [ i for i in clazz.fields if i.name == "CREATOR" ] 849 write = [ i for i in clazz.methods if i.name == "writeToParcel" ] 850 describe = [ i for i in clazz.methods if i.name == "describeContents" ] 851 852 if len(creator) == 0 or len(write) == 0 or len(describe) == 0: 853 error(clazz, None, "FW3", "Parcelable requires CREATOR, writeToParcel, and describeContents; missing one") 854 855 if ((" final class " not in clazz.raw) and 856 (" final deprecated class " not in clazz.raw)): 857 error(clazz, None, "FW8", "Parcelable classes must be final") 858 859 for c in clazz.ctors: 860 if c.args == ["android.os.Parcel"]: 861 error(clazz, c, "FW3", "Parcelable inflation is exposed through CREATOR, not raw constructors") 862 */ 863 864 if (!cls.implements("android.os.Parcelable")) { 865 return 866 } 867 868 if (fields.none { it.name() == "CREATOR" }) { 869 report( 870 PARCEL_CREATOR, cls, 871 "Parcelable requires a `CREATOR` field; missing in ${cls.qualifiedName()}" 872 ) 873 } 874 if (methods.none { it.name() == "writeToParcel" }) { 875 report( 876 PARCEL_CREATOR, cls, 877 "Parcelable requires `void writeToParcel(Parcel, int)`; missing in ${cls.qualifiedName()}" 878 ) 879 } 880 if (methods.none { it.name() == "describeContents" }) { 881 report( 882 PARCEL_CREATOR, cls, 883 "Parcelable requires `public int describeContents()`; missing in ${cls.qualifiedName()}" 884 ) 885 } 886 887 if (!cls.modifiers.isFinal()) { 888 report( 889 PARCEL_NOT_FINAL, cls, 890 "Parcelable classes must be final: ${cls.qualifiedName()} is not final" 891 ) 892 } 893 894 val parcelConstructor = constructors.firstOrNull { 895 val parameters = it.parameters() 896 parameters.size == 1 && parameters[0].type().toTypeString() == "android.os.Parcel" 897 } 898 899 if (parcelConstructor != null) { 900 report( 901 PARCEL_CONSTRUCTOR, parcelConstructor, 902 "Parcelable inflation is exposed through CREATOR, not raw constructors, in ${cls.qualifiedName()}" 903 ) 904 } 905 } 906 907 private fun checkProtected(member: MemberItem) { 908 /* 909 def verify_protected(clazz): 910 """Verify that no protected methods or fields are allowed.""" 911 for m in clazz.methods: 912 if m.name == "finalize": continue 913 if "protected" in m.split: 914 error(clazz, m, "M7", "Protected methods not allowed; must be public") 915 for f in clazz.fields: 916 if "protected" in f.split: 917 error(clazz, f, "M7", "Protected fields not allowed; must be public") 918 */ 919 val modifiers = member.modifiers 920 if (modifiers.isProtected()) { 921 if (member.name() == "finalize" && member is MethodItem && member.parameters().isEmpty()) { 922 return 923 } 924 925 report( 926 PROTECTED_MEMBER, member, 927 "Protected ${if (member is MethodItem) "methods" else "fields"} not allowed; must be public: ${member.describe()}}" 928 ) 929 } 930 } 931 932 private fun checkFieldName(field: FieldItem) { 933 /* 934 def verify_fields(clazz): 935 """Verify that all exposed fields are final. 936 Exposed fields must follow myName style. 937 Catch internal mFoo objects being exposed.""" 938 939 IGNORE_BARE_FIELDS = [ 940 "android.app.ActivityManager.RecentTaskInfo", 941 "android.app.Notification", 942 "android.content.pm.ActivityInfo", 943 "android.content.pm.ApplicationInfo", 944 "android.content.pm.ComponentInfo", 945 "android.content.pm.ResolveInfo", 946 "android.content.pm.FeatureGroupInfo", 947 "android.content.pm.InstrumentationInfo", 948 "android.content.pm.PackageInfo", 949 "android.content.pm.PackageItemInfo", 950 "android.content.res.Configuration", 951 "android.graphics.BitmapFactory.Options", 952 "android.os.Message", 953 "android.system.StructPollfd", 954 ] 955 956 for f in clazz.fields: 957 if not "final" in f.split: 958 if clazz.fullname in IGNORE_BARE_FIELDS: 959 pass 960 elif clazz.fullname.endswith("LayoutParams"): 961 pass 962 elif clazz.fullname.startswith("android.util.Mutable"): 963 pass 964 else: 965 error(clazz, f, "F2", "Bare fields must be marked final, or add accessors if mutable") 966 967 if "static" not in f.split and "property" not in f.split: 968 if not re.match("[a-z]([a-zA-Z]+)?", f.name): 969 error(clazz, f, "S1", "Non-static fields must be named using myField style") 970 971 if re.match("[ms][A-Z]", f.name): 972 error(clazz, f, "F1", "Internal objects must not be exposed") 973 974 if re.match("[A-Z_]+", f.name): 975 if "static" not in f.split or "final" not in f.split: 976 error(clazz, f, "C2", "Constants must be marked static final") 977 */ 978 val className = field.containingClass().qualifiedName() 979 val modifiers = field.modifiers 980 if (!modifiers.isFinal()) { 981 if (className !in classesWithBareFields && 982 !className.endsWith("LayoutParams") && 983 !className.startsWith("android.util.Mutable")) { 984 report(MUTABLE_BARE_FIELD, field, 985 "Bare field ${field.name()} must be marked final, or moved behind accessors if mutable") 986 } 987 } 988 if (!modifiers.isStatic()) { 989 if (!fieldNamePattern.matches(field.name())) { 990 report(START_WITH_LOWER, field, 991 "Non-static field ${field.name()} must be named using fooBar style") 992 } 993 } 994 if (internalNamePattern.matches(field.name())) { 995 report(INTERNAL_FIELD, field, 996 "Internal field ${field.name()} must not be exposed") 997 } 998 if (constantNamePattern.matches(field.name()) && field.isJava()) { 999 if (!modifiers.isStatic() || !modifiers.isFinal()) { 1000 report(ALL_UPPER, field, 1001 "Constant ${field.name()} must be marked static final") 1002 } 1003 } 1004 } 1005 1006 private fun checkSettingKeys(field: FieldItem) { 1007 val className = field.containingClass().qualifiedName() 1008 val modifiers = field.modifiers 1009 val type = field.type() 1010 1011 if (modifiers.isFinal() && modifiers.isStatic() && type.isString() && className in settingsKeyClasses) { 1012 report(NO_SETTINGS_PROVIDER, field, 1013 "New setting keys are not allowed (Field: ${field.name()}); use getters/setters in relevant manager class") 1014 } 1015 } 1016 1017 private fun checkRegistrationMethods(cls: ClassItem, methods: Sequence<MethodItem>) { 1018 /* 1019 def verify_register(clazz): 1020 """Verify parity of registration methods. 1021 Callback objects use register/unregister methods. 1022 Listener objects use add/remove methods.""" 1023 methods = [ m.name for m in clazz.methods ] 1024 for m in clazz.methods: 1025 if "Callback" in m.raw: 1026 if m.name.startswith("register"): 1027 other = "unregister" + m.name[8:] 1028 if other not in methods: 1029 error(clazz, m, "L2", "Missing unregister method") 1030 if m.name.startswith("unregister"): 1031 other = "register" + m.name[10:] 1032 if other not in methods: 1033 error(clazz, m, "L2", "Missing register method") 1034 1035 if m.name.startswith("add") or m.name.startswith("remove"): 1036 error(clazz, m, "L3", "Callback methods should be named register/unregister") 1037 1038 if "Listener" in m.raw: 1039 if m.name.startswith("add"): 1040 other = "remove" + m.name[3:] 1041 if other not in methods: 1042 error(clazz, m, "L2", "Missing remove method") 1043 if m.name.startswith("remove") and not m.name.startswith("removeAll"): 1044 other = "add" + m.name[6:] 1045 if other not in methods: 1046 error(clazz, m, "L2", "Missing add method") 1047 1048 if m.name.startswith("register") or m.name.startswith("unregister"): 1049 error(clazz, m, "L3", "Listener methods should be named add/remove") 1050 */ 1051 1052 /** Make sure that there is a corresponding method */ 1053 fun ensureMatched(cls: ClassItem, methods: Sequence<MethodItem>, method: MethodItem, name: String) { 1054 if (method.superMethods().isNotEmpty()) return // Do not report for override methods 1055 for (candidate in methods) { 1056 if (candidate.name() == name) { 1057 return 1058 } 1059 } 1060 1061 report( 1062 PAIRED_REGISTRATION, method, 1063 "Found ${method.name()} but not $name in ${cls.qualifiedName()}" 1064 ) 1065 } 1066 1067 for (method in methods) { 1068 val name = method.name() 1069 // the python version looks for any substring, but that includes a lot of other stuff, like plurals 1070 if (name.endsWith("Callback")) { 1071 if (name.startsWith("register")) { 1072 val unregister = "unregister" + name.substring(8) // "register".length 1073 ensureMatched(cls, methods, method, unregister) 1074 } else if (name.startsWith("unregister")) { 1075 val unregister = "register" + name.substring(10) // "unregister".length 1076 ensureMatched(cls, methods, method, unregister) 1077 } 1078 if (name.startsWith("add") || name.startsWith("remove")) { 1079 report( 1080 REGISTRATION_NAME, method, 1081 "Callback methods should be named register/unregister; was $name" 1082 ) 1083 } 1084 } else if (name.endsWith("Listener")) { 1085 if (name.startsWith("add")) { 1086 val unregister = "remove" + name.substring(3) // "add".length 1087 ensureMatched(cls, methods, method, unregister) 1088 } else if (name.startsWith("remove") && !name.startsWith("removeAll")) { 1089 val unregister = "add" + name.substring(6) // "remove".length 1090 ensureMatched(cls, methods, method, unregister) 1091 } 1092 if (name.startsWith("register") || name.startsWith("unregister")) { 1093 report( 1094 REGISTRATION_NAME, method, 1095 "Listener methods should be named add/remove; was $name" 1096 ) 1097 } 1098 } 1099 } 1100 } 1101 1102 private fun checkSynchronized(method: MethodItem) { 1103 /* 1104 def verify_sync(clazz): 1105 """Verify synchronized methods aren't exposed.""" 1106 for m in clazz.methods: 1107 if "synchronized" in m.split: 1108 error(clazz, m, "M5", "Internal locks must not be exposed") 1109 */ 1110 1111 fun reportError(method: MethodItem, psi: PsiElement? = null) { 1112 val message = StringBuilder("Internal locks must not be exposed") 1113 if (psi != null) { 1114 message.append(" (synchronizing on this or class is still externally observable)") 1115 } 1116 message.append(": ") 1117 message.append(method.describe()) 1118 report(VISIBLY_SYNCHRONIZED, method, message.toString(), psi) 1119 } 1120 1121 if (method.modifiers.isSynchronized()) { 1122 reportError(method) 1123 } else if (method is PsiMethodItem) { 1124 val psiMethod = method.psiMethod 1125 if (psiMethod is UMethod) { 1126 psiMethod.accept(object : AbstractUastVisitor() { 1127 override fun afterVisitCallExpression(node: UCallExpression) { 1128 super.afterVisitCallExpression(node) 1129 1130 if (node.methodName == "synchronized" && node.receiver == null) { 1131 val arg = node.valueArguments.firstOrNull() 1132 if (arg is UThisExpression || 1133 arg is UClassLiteralExpression || 1134 arg is UQualifiedReferenceExpression && arg.receiver is UClassLiteralExpression 1135 ) { 1136 reportError(method, arg.sourcePsi ?: node.sourcePsi ?: node.javaPsi) 1137 } 1138 } 1139 } 1140 }) 1141 } else { 1142 psiMethod.body?.accept(object : JavaRecursiveElementVisitor() { 1143 override fun visitSynchronizedStatement(statement: PsiSynchronizedStatement) { 1144 super.visitSynchronizedStatement(statement) 1145 1146 val lock = statement.lockExpression 1147 if (lock == null || lock is PsiThisExpression || 1148 // locking on any class is visible 1149 lock is PsiClassObjectAccessExpression 1150 ) { 1151 reportError(method, lock ?: statement) 1152 } 1153 } 1154 }) 1155 } 1156 } 1157 } 1158 1159 private fun checkIntentBuilder(method: MethodItem) { 1160 /* 1161 def verify_intent_builder(clazz): 1162 """Verify that Intent builders are createFooIntent() style.""" 1163 if clazz.name == "Intent": return 1164 1165 for m in clazz.methods: 1166 if m.typ == "android.content.Intent": 1167 if m.name.startswith("create") and m.name.endswith("Intent"): 1168 pass 1169 else: 1170 warn(clazz, m, "FW1", "Methods creating an Intent should be named createFooIntent()") 1171 */ 1172 if (method.returnType()?.toTypeString() == "android.content.Intent") { 1173 val name = method.name() 1174 if (name.startsWith("create") && name.endsWith("Intent")) { 1175 return 1176 } 1177 if (method.containingClass().simpleName() == "Intent") { 1178 return 1179 } 1180 1181 report( 1182 INTENT_BUILDER_NAME, method, 1183 "Methods creating an Intent should be named `create<Foo>Intent()`, was `$name`" 1184 ) 1185 } 1186 } 1187 1188 private fun checkHelperClasses(cls: ClassItem, methods: Sequence<MethodItem>, fields: Sequence<FieldItem>) { 1189 /* 1190 def verify_helper_classes(clazz): 1191 """Verify that helper classes are named consistently with what they extend. 1192 All developer extendable methods should be named onFoo().""" 1193 test_methods = False 1194 if "extends android.app.Service" in clazz.raw: 1195 test_methods = True 1196 if not clazz.name.endswith("Service"): 1197 error(clazz, None, "CL4", "Inconsistent class name; should be FooService") 1198 1199 found = False 1200 for f in clazz.fields: 1201 if f.name == "SERVICE_INTERFACE": 1202 found = True 1203 if f.value != clazz.fullname: 1204 error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname)) 1205 1206 if "extends android.content.ContentProvider" in clazz.raw: 1207 test_methods = True 1208 if not clazz.name.endswith("Provider"): 1209 error(clazz, None, "CL4", "Inconsistent class name; should be FooProvider") 1210 1211 found = False 1212 for f in clazz.fields: 1213 if f.name == "PROVIDER_INTERFACE": 1214 found = True 1215 if f.value != clazz.fullname: 1216 error(clazz, f, "C4", "Inconsistent interface constant; expected '%s'" % (clazz.fullname)) 1217 1218 if "extends android.content.BroadcastReceiver" in clazz.raw: 1219 test_methods = True 1220 if not clazz.name.endswith("Receiver"): 1221 error(clazz, None, "CL4", "Inconsistent class name; should be FooReceiver") 1222 1223 if "extends android.app.Activity" in clazz.raw: 1224 test_methods = True 1225 if not clazz.name.endswith("Activity"): 1226 error(clazz, None, "CL4", "Inconsistent class name; should be FooActivity") 1227 1228 if test_methods: 1229 for m in clazz.methods: 1230 if "final" in m.split: continue 1231 // Note: This regex seems wrong: 1232 if not re.match("on[A-Z]", m.name): 1233 if "abstract" in m.split: 1234 warn(clazz, m, None, "Methods implemented by developers should be named onFoo()") 1235 else: 1236 warn(clazz, m, None, "If implemented by developer, should be named onFoo(); otherwise consider marking final") 1237 1238 */ 1239 1240 fun ensureFieldValue(fields: Sequence<FieldItem>, fieldName: String, fieldValue: String) { 1241 fields.firstOrNull { it.name() == fieldName }?.let { field -> 1242 if (field.initialValue(true) != fieldValue) { 1243 report( 1244 INTERFACE_CONSTANT, field, 1245 "Inconsistent interface constant; expected '$fieldValue'`" 1246 ) 1247 } 1248 } 1249 } 1250 1251 fun ensureContextNameSuffix(cls: ClassItem, suffix: String) { 1252 if (!cls.simpleName().endsWith(suffix)) { 1253 report( 1254 CONTEXT_NAME_SUFFIX, cls, 1255 "Inconsistent class name; should be `<Foo>$suffix`, was `${cls.simpleName()}`" 1256 ) 1257 } 1258 } 1259 1260 var testMethods = false 1261 1262 when { 1263 cls.extends("android.app.Service") -> { 1264 testMethods = true 1265 ensureContextNameSuffix(cls, "Service") 1266 ensureFieldValue(fields, "SERVICE_INTERFACE", cls.qualifiedName()) 1267 } 1268 cls.extends("android.content.ContentProvider") -> { 1269 testMethods = true 1270 ensureContextNameSuffix(cls, "Provider") 1271 ensureFieldValue(fields, "PROVIDER_INTERFACE", cls.qualifiedName()) 1272 } 1273 cls.extends("android.content.BroadcastReceiver") -> { 1274 testMethods = true 1275 ensureContextNameSuffix(cls, "Receiver") 1276 } 1277 cls.extends("android.app.Activity") -> { 1278 testMethods = true 1279 ensureContextNameSuffix(cls, "Activity") 1280 } 1281 } 1282 1283 if (testMethods) { 1284 for (method in methods) { 1285 val modifiers = method.modifiers 1286 if (modifiers.isFinal() || modifiers.isStatic()) { 1287 continue 1288 } 1289 val name = method.name() 1290 if (!onCallbackNamePattern.matches(name)) { 1291 val message = 1292 if (modifiers.isAbstract()) { 1293 "Methods implemented by developers should follow the on<Something> style, was `$name`" 1294 } else { 1295 "If implemented by developer, should follow the on<Something> style; otherwise consider marking final" 1296 } 1297 report(ON_NAME_EXPECTED, method, message) 1298 } 1299 } 1300 } 1301 } 1302 1303 private fun checkBuilder( 1304 cls: ClassItem, 1305 methods: Sequence<MethodItem>, 1306 constructors: Sequence<ConstructorItem>, 1307 superClass: ClassItem? 1308 ) { 1309 /* 1310 def verify_builder(clazz): 1311 """Verify builder classes. 1312 Methods should return the builder to enable chaining.""" 1313 if " extends " in clazz.raw: return 1314 if not clazz.name.endswith("Builder"): return 1315 1316 if clazz.name != "Builder": 1317 warn(clazz, None, None, "Builder should be defined as inner class") 1318 1319 has_build = False 1320 for m in clazz.methods: 1321 if m.name == "build": 1322 has_build = True 1323 continue 1324 1325 if m.name.startswith("get"): continue 1326 if m.name.startswith("clear"): continue 1327 1328 if m.name.startswith("with"): 1329 warn(clazz, m, None, "Builder methods names should use setFoo() style") 1330 1331 if m.name.startswith("set"): 1332 if not m.typ.endswith(clazz.fullname): 1333 warn(clazz, m, "M4", "Methods must return the builder object") 1334 1335 if not has_build: 1336 warn(clazz, None, None, "Missing build() method") 1337 */ 1338 if (!cls.simpleName().endsWith("Builder")) { 1339 return 1340 } 1341 if (superClass != null && !superClass.isJavaLangObject()) { 1342 return 1343 } 1344 if (cls.isTopLevelClass()) { 1345 report( 1346 TOP_LEVEL_BUILDER, cls, 1347 "Builder should be defined as inner class: ${cls.qualifiedName()}" 1348 ) 1349 } 1350 if (!cls.modifiers.isFinal()) { 1351 report( 1352 STATIC_FINAL_BUILDER, cls, 1353 "Builder must be final: ${cls.qualifiedName()}" 1354 ) 1355 } 1356 if (!cls.modifiers.isStatic() && !cls.isTopLevelClass()) { 1357 report( 1358 STATIC_FINAL_BUILDER, cls, 1359 "Builder must be static: ${cls.qualifiedName()}" 1360 ) 1361 } 1362 for (constructor in constructors) { 1363 for (arg in constructor.parameters()) { 1364 if (arg.modifiers.isNullable()) { 1365 report( 1366 OPTIONAL_BUILDER_CONSTRUCTOR_ARGUMENT, arg, 1367 "Builder constructor arguments must be mandatory (i.e. not @Nullable): ${arg.describe()}" 1368 ) 1369 } 1370 } 1371 } 1372 // Maps each setter to a list of potential getters that would satisfy it. 1373 val expectedGetters = mutableListOf<Pair<Item, Set<String>>>() 1374 var builtType: TypeItem? = null 1375 val clsType = cls.toType().toTypeString() 1376 1377 for (method in methods) { 1378 val name = method.name() 1379 if (name == "build") { 1380 builtType = method.type() 1381 continue 1382 } else if (name.startsWith("get") || name.startsWith("is")) { 1383 report( 1384 GETTER_ON_BUILDER, method, 1385 "Getter should be on the built object, not the builder: ${method.describe()}" 1386 ) 1387 } else if (name.startsWith("set") || name.startsWith("add") || name.startsWith("clear")) { 1388 val returnType = method.returnType()?.toTypeString() ?: "" 1389 val returnTypeBounds = method.returnType()?.asTypeParameter(context = method)?.bounds()?.map { 1390 it.toType().toTypeString() 1391 } ?: listOf() 1392 1393 if (returnType != clsType && !returnTypeBounds.contains(clsType)) { 1394 report( 1395 SETTER_RETURNS_THIS, method, 1396 "Methods must return the builder object (return type $clsType instead of $returnType): ${method.describe()}" 1397 ) 1398 } 1399 if (method.modifiers.isNullable()) { 1400 report( 1401 SETTER_RETURNS_THIS, method, 1402 "Builder setter must be @NonNull: ${method.describe()}" 1403 ) 1404 } 1405 val isBool = when (method.parameters().firstOrNull()?.type()?.toTypeString()) { 1406 "boolean", "java.lang.Boolean" -> true 1407 else -> false 1408 } 1409 val allowedGetters: Set<String>? = if (isBool && name.startsWith("set")) { 1410 val pattern = goodBooleanGetterSetterPrefixes.match( 1411 name, GetterSetterPattern::setter)!! 1412 setOf("${pattern.getter}${name.removePrefix(pattern.setter)}") 1413 } else { 1414 when { 1415 name.startsWith("set") -> listOf(name.removePrefix("set")) 1416 name.startsWith("add") -> { 1417 val nameWithoutPrefix = name.removePrefix("add") 1418 when { 1419 name.endsWith("s") -> { 1420 // If the name ends with s, it may already be a plural. If the 1421 // add method accepts a single value, it is called addFoo() and 1422 // getFoos() is right. If an add method accepts a collection, it 1423 // is called addFoos() and getFoos() is right. So we allow both. 1424 listOf(nameWithoutPrefix, "${nameWithoutPrefix}es") 1425 } 1426 name.endsWith("sh") || name.endsWith("ch") || name.endsWith("x") || 1427 name.endsWith("z") -> listOf("${nameWithoutPrefix}es") 1428 name.endsWith("y") && 1429 name[name.length - 2] !in listOf('a', 'e', 'i', 'o', 'u') 1430 -> { 1431 listOf("${nameWithoutPrefix.removeSuffix("y")}ies") 1432 } 1433 else -> listOf("${nameWithoutPrefix}s") 1434 } 1435 } 1436 else -> null 1437 }?.map { "get$it" }?.toSet() 1438 } 1439 allowedGetters?.let { expectedGetters.add(method to it) } 1440 } else { 1441 report( 1442 BUILDER_SET_STYLE, method, 1443 "Builder methods names should use setFoo() / addFoo() / clearFoo() style: ${method.describe()}" 1444 ) 1445 } 1446 } 1447 if (builtType == null) { 1448 report( 1449 MISSING_BUILD_METHOD, cls, 1450 "${cls.qualifiedName()} does not declare a `build()` method, but builder classes are expected to" 1451 ) 1452 } 1453 builtType?.asClass()?.let { builtClass -> 1454 val builtMethods = builtClass.filteredMethods(filterReference, includeSuperClassMethods = true).map { it.name() }.toSet() 1455 for ((setter, expectedGetterNames) in expectedGetters) { 1456 if (builtMethods.intersect(expectedGetterNames).isEmpty()) { 1457 val expectedGetterCalls = expectedGetterNames.map { "$it()" } 1458 val errorString = if (expectedGetterCalls.size == 1) { 1459 "${builtClass.qualifiedName()} does not declare a " + 1460 "`${expectedGetterCalls.first()}` method matching " + 1461 "${setter.describe()}" 1462 } else { 1463 "${builtClass.qualifiedName()} does not declare a getter method " + 1464 "matching ${setter.describe()} (expected one of: " + 1465 "$expectedGetterCalls)" 1466 } 1467 report(MISSING_GETTER_MATCHING_BUILDER, setter, errorString) 1468 } 1469 } 1470 } 1471 } 1472 1473 private fun checkAidl(cls: ClassItem, superClass: ClassItem?, interfaces: Sequence<TypeItem>) { 1474 /* 1475 def verify_aidl(clazz): 1476 """Catch people exposing raw AIDL.""" 1477 if "extends android.os.Binder" in clazz.raw or "implements android.os.IInterface" in clazz.raw: 1478 error(clazz, None, None, "Raw AIDL interfaces must not be exposed") 1479 */ 1480 1481 // Instead of ClassItem.implements() and .extends() which performs hierarchy 1482 // searches, here we only want to flag directly extending or implementing: 1483 val extendsBinder = superClass?.qualifiedName() == "android.os.Binder" 1484 val implementsIInterface = interfaces.any { it.toTypeString() == "android.os.IInterface" } 1485 if (extendsBinder || implementsIInterface) { 1486 val problem = if (extendsBinder) { 1487 "extends Binder" 1488 } else { 1489 "implements IInterface" 1490 } 1491 report( 1492 RAW_AIDL, cls, 1493 "Raw AIDL interfaces must not be exposed: ${cls.simpleName()} $problem" 1494 ) 1495 } 1496 } 1497 1498 private fun checkInternal(cls: ClassItem) { 1499 /* 1500 def verify_internal(clazz): 1501 """Catch people exposing internal classes.""" 1502 if clazz.pkg.name.startswith("com.android"): 1503 error(clazz, None, None, "Internal classes must not be exposed") 1504 */ 1505 1506 if (cls.qualifiedName().startsWith("com.android.")) { 1507 report( 1508 INTERNAL_CLASSES, cls, 1509 "Internal classes must not be exposed" 1510 ) 1511 } 1512 } 1513 1514 private fun checkLayering( 1515 cls: ClassItem, 1516 methodsAndConstructors: Sequence<MethodItem>, 1517 fields: Sequence<FieldItem> 1518 ) { 1519 /* 1520 def verify_layering(clazz): 1521 """Catch package layering violations. 1522 For example, something in android.os depending on android.app.""" 1523 ranking = [ 1524 ["android.service","android.accessibilityservice","android.inputmethodservice","android.printservice","android.appwidget","android.webkit","android.preference","android.gesture","android.print"], 1525 "android.app", 1526 "android.widget", 1527 "android.view", 1528 "android.animation", 1529 "android.provider", 1530 ["android.content","android.graphics.drawable"], 1531 "android.database", 1532 "android.text", 1533 "android.graphics", 1534 "android.os", 1535 "android.util" 1536 ] 1537 1538 def rank(p): 1539 for i in range(len(ranking)): 1540 if isinstance(ranking[i], list): 1541 for j in ranking[i]: 1542 if p.startswith(j): return i 1543 else: 1544 if p.startswith(ranking[i]): return i 1545 1546 cr = rank(clazz.pkg.name) 1547 if cr is None: return 1548 1549 for f in clazz.fields: 1550 ir = rank(f.typ) 1551 if ir and ir < cr: 1552 warn(clazz, f, "FW6", "Field type violates package layering") 1553 1554 for m in clazz.methods: 1555 ir = rank(m.typ) 1556 if ir and ir < cr: 1557 warn(clazz, m, "FW6", "Method return type violates package layering") 1558 for arg in m.args: 1559 ir = rank(arg) 1560 if ir and ir < cr: 1561 warn(clazz, m, "FW6", "Method argument type violates package layering") 1562 1563 */ 1564 1565 fun packageRank(pkg: PackageItem): Int { 1566 return when (pkg.qualifiedName()) { 1567 "android.service", 1568 "android.accessibilityservice", 1569 "android.inputmethodservice", 1570 "android.printservice", 1571 "android.appwidget", 1572 "android.webkit", 1573 "android.preference", 1574 "android.gesture", 1575 "android.print" -> 10 1576 1577 "android.app" -> 20 1578 "android.widget" -> 30 1579 "android.view" -> 40 1580 "android.animation" -> 50 1581 "android.provider" -> 60 1582 1583 "android.content", 1584 "android.graphics.drawable" -> 70 1585 1586 "android.database" -> 80 1587 "android.text" -> 90 1588 "android.graphics" -> 100 1589 "android.os" -> 110 1590 "android.util" -> 120 1591 else -> -1 1592 } 1593 } 1594 1595 fun getTypePackage(type: TypeItem?): PackageItem? { 1596 return if (type == null || type.primitive) { 1597 null 1598 } else { 1599 type.asClass()?.containingPackage() 1600 } 1601 } 1602 1603 fun getTypeRank(type: TypeItem?): Int { 1604 type ?: return -1 1605 val pkg = getTypePackage(type) ?: return -1 1606 return packageRank(pkg) 1607 } 1608 1609 val classPackage = cls.containingPackage() 1610 val classRank = packageRank(classPackage) 1611 if (classRank == -1) { 1612 return 1613 } 1614 for (field in fields) { 1615 val fieldTypeRank = getTypeRank(field.type()) 1616 if (fieldTypeRank != -1 && fieldTypeRank < classRank) { 1617 report( 1618 PACKAGE_LAYERING, cls, 1619 "Field type `${field.type().toTypeString()}` violates package layering: nothing in `$classPackage` should depend on `${getTypePackage( 1620 field.type() 1621 )}`" 1622 ) 1623 } 1624 } 1625 1626 for (method in methodsAndConstructors) { 1627 val returnType = method.returnType() 1628 if (returnType != null) { // not a constructor 1629 val returnTypeRank = getTypeRank(returnType) 1630 if (returnTypeRank != -1 && returnTypeRank < classRank) { 1631 report( 1632 PACKAGE_LAYERING, cls, 1633 "Method return type `${returnType.toTypeString()}` violates package layering: nothing in `$classPackage` should depend on `${getTypePackage( 1634 returnType 1635 )}`" 1636 ) 1637 } 1638 } 1639 1640 for (parameter in method.parameters()) { 1641 val parameterTypeRank = getTypeRank(parameter.type()) 1642 if (parameterTypeRank != -1 && parameterTypeRank < classRank) { 1643 report( 1644 PACKAGE_LAYERING, cls, 1645 "Method parameter type `${parameter.type().toTypeString()}` violates package layering: nothing in `$classPackage` should depend on `${getTypePackage( 1646 parameter.type() 1647 )}`" 1648 ) 1649 } 1650 } 1651 } 1652 } 1653 1654 private fun checkBooleans(methods: Sequence<MethodItem>) { 1655 /* 1656 Correct: 1657 1658 void setVisible(boolean visible); 1659 boolean isVisible(); 1660 1661 void setHasTransientState(boolean hasTransientState); 1662 boolean hasTransientState(); 1663 1664 void setCanRecord(boolean canRecord); 1665 boolean canRecord(); 1666 1667 void setShouldFitWidth(boolean shouldFitWidth); 1668 boolean shouldFitWidth(); 1669 1670 void setWiFiRoamingSettingEnabled(boolean enabled) 1671 boolean isWiFiRoamingSettingEnabled() 1672 */ 1673 1674 fun errorIfExists(methods: Sequence<MethodItem>, trigger: String, expected: String, actual: String) { 1675 for (method in methods) { 1676 if (method.name() == actual) { 1677 report( 1678 GETTER_SETTER_NAMES, method, 1679 "Symmetric method for `$trigger` must be named `$expected`; was `$actual`" 1680 ) 1681 } 1682 } 1683 } 1684 1685 fun isGetter(method: MethodItem): Boolean { 1686 val returnType = method.returnType() ?: return false 1687 return method.parameters().isEmpty() && returnType.primitive && returnType.toTypeString() == "boolean" 1688 } 1689 1690 fun isSetter(method: MethodItem): Boolean { 1691 return method.parameters().size == 1 && method.parameters()[0].type().toTypeString() == "boolean" 1692 } 1693 1694 for (method in methods) { 1695 val name = method.name() 1696 if (isGetter(method)) { 1697 val pattern = goodBooleanGetterSetterPrefixes.match(name, GetterSetterPattern::getter) ?: continue 1698 val target = name.substring(pattern.getter.length) 1699 val expectedSetter = "${pattern.setter}$target" 1700 1701 badBooleanSetterPrefixes.forEach { 1702 val actualSetter = "${it}$target" 1703 if (actualSetter != expectedSetter) { 1704 errorIfExists(methods, name, expectedSetter, actualSetter) 1705 } 1706 } 1707 } else if (isSetter(method)) { 1708 val pattern = goodBooleanGetterSetterPrefixes.match(name, GetterSetterPattern::setter) ?: continue 1709 val target = name.substring(pattern.setter.length) 1710 val expectedGetter = "${pattern.getter}$target" 1711 1712 badBooleanGetterPrefixes.forEach { 1713 val actualGetter = "${it}$target" 1714 if (actualGetter != expectedGetter) { 1715 errorIfExists(methods, name, expectedGetter, actualGetter) 1716 } 1717 } 1718 } 1719 } 1720 } 1721 1722 private fun checkCollections( 1723 type: TypeItem, 1724 item: Item 1725 ) { 1726 /* 1727 def verify_collections(clazz): 1728 """Verifies that collection types are interfaces.""" 1729 if clazz.fullname == "android.os.Bundle": return 1730 1731 bad = ["java.util.Vector", "java.util.LinkedList", "java.util.ArrayList", "java.util.Stack", 1732 "java.util.HashMap", "java.util.HashSet", "android.util.ArraySet", "android.util.ArrayMap"] 1733 for m in clazz.methods: 1734 if m.typ in bad: 1735 error(clazz, m, "CL2", "Return type is concrete collection; must be higher-level interface") 1736 for arg in m.args: 1737 if arg in bad: 1738 error(clazz, m, "CL2", "Argument is concrete collection; must be higher-level interface") 1739 */ 1740 1741 if (type.primitive) { 1742 return 1743 } 1744 1745 when (type.asClass()?.qualifiedName()) { 1746 "java.util.Vector", 1747 "java.util.LinkedList", 1748 "java.util.ArrayList", 1749 "java.util.Stack", 1750 "java.util.HashMap", 1751 "java.util.HashSet", 1752 "android.util.ArraySet", 1753 "android.util.ArrayMap" -> { 1754 if (item.containingClass()?.qualifiedName() == "android.os.Bundle") { 1755 return 1756 } 1757 val where = when (item) { 1758 is MethodItem -> "Return type" 1759 is FieldItem -> "Field type" 1760 else -> "Parameter type" 1761 } 1762 val erased = type.toErasedTypeString() 1763 report( 1764 CONCRETE_COLLECTION, item, 1765 "$where is concrete collection (`$erased`); must be higher-level interface" 1766 ) 1767 } 1768 } 1769 } 1770 1771 fun Item.containingClass(): ClassItem? { 1772 return when (this) { 1773 is MemberItem -> this.containingClass() 1774 is ParameterItem -> this.containingMethod().containingClass() 1775 is ClassItem -> this 1776 else -> null 1777 } 1778 } 1779 1780 private fun checkNullableCollections(type: TypeItem, item: Item) { 1781 if (type.primitive) return 1782 if (!item.modifiers.isNullable()) return 1783 val typeAsClass = type.asClass() ?: return 1784 1785 val superItem: Item? = when (item) { 1786 is MethodItem -> item.findPredicateSuperMethod(filterReference) 1787 is ParameterItem -> item.containingMethod().findPredicateSuperMethod(filterReference) 1788 ?.parameters()?.find { it.parameterIndex == item.parameterIndex } 1789 else -> null 1790 } 1791 1792 if (superItem?.modifiers?.isNullable() == true) { 1793 return 1794 } 1795 1796 if (type.isArray() || 1797 typeAsClass.extendsOrImplements("java.util.Collection") || 1798 typeAsClass.extendsOrImplements("kotlin.collections.Collection") || 1799 typeAsClass.extendsOrImplements("java.util.Map") || 1800 typeAsClass.extendsOrImplements("kotlin.collections.Map") || 1801 typeAsClass.qualifiedName() == "android.os.Bundle" || 1802 typeAsClass.qualifiedName() == "android.os.PersistableBundle") { 1803 val where = when (item) { 1804 is MethodItem -> "Return type of ${item.describe()}" 1805 else -> "Type of ${item.describe()}" 1806 } 1807 1808 val erased = type.toErasedTypeString(item) 1809 report( 1810 NULLABLE_COLLECTION, item, 1811 "$where is a nullable collection (`$erased`); must be non-null" 1812 ) 1813 } 1814 } 1815 1816 private fun checkFlags(fields: Sequence<FieldItem>) { 1817 /* 1818 def verify_flags(clazz): 1819 """Verifies that flags are non-overlapping.""" 1820 known = collections.defaultdict(int) 1821 for f in clazz.fields: 1822 if "FLAG_" in f.name: 1823 try: 1824 val = int(f.value) 1825 except: 1826 continue 1827 1828 scope = f.name[0:f.name.index("FLAG_")] 1829 if val & known[scope]: 1830 warn(clazz, f, "C1", "Found overlapping flag constant value") 1831 known[scope] |= val 1832 1833 */ 1834 var known: MutableMap<String, Int>? = null 1835 var valueToFlag: MutableMap<Int?, String>? = null 1836 for (field in fields) { 1837 val name = field.name() 1838 val index = name.indexOf("FLAG_") 1839 if (index != -1) { 1840 val value = field.initialValue() as? Int ?: continue 1841 val scope = name.substring(0, index) 1842 val prev = known?.get(scope) ?: 0 1843 if (known != null && (prev and value) != 0) { 1844 val prevName = valueToFlag?.get(prev) 1845 report( 1846 OVERLAPPING_CONSTANTS, field, 1847 "Found overlapping flag constant values: `$name` with value $value (0x${Integer.toHexString( 1848 value 1849 )}) and overlapping flag value $prev (0x${Integer.toHexString(prev)}) from `$prevName`" 1850 ) 1851 } 1852 if (known == null) { 1853 known = mutableMapOf() 1854 } 1855 known[scope] = value 1856 if (valueToFlag == null) { 1857 valueToFlag = mutableMapOf() 1858 } 1859 valueToFlag[value] = name 1860 } 1861 } 1862 } 1863 1864 private fun checkExceptions(method: MethodItem, filterReference: Predicate<Item>) { 1865 /* 1866 def verify_exception(clazz): 1867 """Verifies that methods don't throw generic exceptions.""" 1868 for m in clazz.methods: 1869 for t in m.throws: 1870 if t in ["java.lang.Exception", "java.lang.Throwable", "java.lang.Error"]: 1871 error(clazz, m, "S1", "Methods must not throw generic exceptions") 1872 1873 if t in ["android.os.RemoteException"]: 1874 if clazz.name == "android.content.ContentProviderClient": continue 1875 if clazz.name == "android.os.Binder": continue 1876 if clazz.name == "android.os.IBinder": continue 1877 1878 error(clazz, m, "FW9", "Methods calling into system server should rethrow RemoteException as RuntimeException") 1879 1880 if len(m.args) == 0 and t in ["java.lang.IllegalArgumentException", "java.lang.NullPointerException"]: 1881 warn(clazz, m, "S1", "Methods taking no arguments should throw IllegalStateException") 1882 */ 1883 for (exception in method.filteredThrowsTypes(filterReference)) { 1884 when (val qualifiedName = exception.qualifiedName()) { 1885 "java.lang.Exception", 1886 "java.lang.Throwable", 1887 "java.lang.Error" -> { 1888 report( 1889 GENERIC_EXCEPTION, method, 1890 "Methods must not throw generic exceptions (`$qualifiedName`)" 1891 ) 1892 } 1893 "android.os.RemoteException" -> { 1894 when (method.containingClass().qualifiedName()) { 1895 "android.content.ContentProviderClient", 1896 "android.os.Binder", 1897 "android.os.IBinder" -> { 1898 // exceptions 1899 } 1900 else -> { 1901 report( 1902 RETHROW_REMOTE_EXCEPTION, method, 1903 "Methods calling system APIs should rethrow `RemoteException` as `RuntimeException` (but do not list it in the throws clause)" 1904 ) 1905 } 1906 } 1907 } 1908 "java.lang.IllegalArgumentException", 1909 "java.lang.NullPointerException" -> { 1910 if (method.parameters().isEmpty()) { 1911 report( 1912 ILLEGAL_STATE_EXCEPTION, method, 1913 "Methods taking no arguments should throw `IllegalStateException` instead of `$qualifiedName`" 1914 ) 1915 } 1916 } 1917 } 1918 } 1919 } 1920 1921 private fun checkGoogle(cls: ClassItem, methods: Sequence<MethodItem>, fields: Sequence<FieldItem>) { 1922 /* 1923 def verify_google(clazz): 1924 """Verifies that APIs never reference Google.""" 1925 1926 if re.search("google", clazz.raw, re.IGNORECASE): 1927 error(clazz, None, None, "Must never reference Google") 1928 1929 test = [] 1930 test.extend(clazz.ctors) 1931 test.extend(clazz.fields) 1932 test.extend(clazz.methods) 1933 1934 for t in test: 1935 if re.search("google", t.raw, re.IGNORECASE): 1936 error(clazz, t, None, "Must never reference Google") 1937 */ 1938 1939 fun checkName(name: String, item: Item) { 1940 if (name.contains("Google", ignoreCase = true)) { 1941 report( 1942 MENTIONS_GOOGLE, item, 1943 "Must never reference Google (`$name`)" 1944 ) 1945 } 1946 } 1947 1948 checkName(cls.simpleName(), cls) 1949 for (method in methods) { 1950 checkName(method.name(), method) 1951 } 1952 for (field in fields) { 1953 checkName(field.name(), field) 1954 } 1955 } 1956 1957 private fun checkBitSet(type: TypeItem, typeString: String, item: Item) { 1958 if (typeString.startsWith("java.util.BitSet") && 1959 type.asClass()?.qualifiedName() == "java.util.BitSet" 1960 ) { 1961 report( 1962 HEAVY_BIT_SET, item, 1963 "Type must not be heavy BitSet (${item.describe()})" 1964 ) 1965 } 1966 } 1967 1968 private fun checkManager(cls: ClassItem, methods: Sequence<MethodItem>, constructors: Sequence<ConstructorItem>) { 1969 /* 1970 def verify_manager(clazz): 1971 """Verifies that FooManager is only obtained from Context.""" 1972 1973 if not clazz.name.endswith("Manager"): return 1974 1975 for c in clazz.ctors: 1976 error(clazz, c, None, "Managers must always be obtained from Context; no direct constructors") 1977 1978 for m in clazz.methods: 1979 if m.typ == clazz.fullname: 1980 error(clazz, m, None, "Managers must always be obtained from Context") 1981 1982 */ 1983 if (!cls.simpleName().endsWith("Manager")) { 1984 return 1985 } 1986 for (method in constructors) { 1987 method.modifiers.isPublic() 1988 method.modifiers.isPrivate() 1989 report( 1990 MANAGER_CONSTRUCTOR, method, 1991 "Managers must always be obtained from Context; no direct constructors" 1992 ) 1993 } 1994 for (method in methods) { 1995 if (method.returnType()?.asClass() == cls) { 1996 report( 1997 MANAGER_LOOKUP, method, 1998 "Managers must always be obtained from Context (`${method.name()}`)" 1999 ) 2000 } 2001 } 2002 } 2003 2004 private fun checkHasNullability(item: Item) { 2005 if (item.requiresNullnessInfo() && !item.hasNullnessInfo() && 2006 getImplicitNullness(item) == null) { 2007 val type = item.type() 2008 val inherited = when (item) { 2009 is ParameterItem -> item.containingMethod().inheritedMethod 2010 is FieldItem -> item.inheritedField 2011 is MethodItem -> item.inheritedMethod 2012 else -> false 2013 } 2014 if (inherited) { 2015 return // Do not enforce nullability on inherited items (non-overridden) 2016 } 2017 if (type != null && type.isTypeParameter()) { 2018 // Generic types should have declarations of nullability set at the site of where 2019 // the type is set, so that for Foo<T>, T does not need to specify nullability, but 2020 // for Foo<Bar>, Bar does. 2021 return // Do not enforce nullability for generics 2022 } 2023 if (item is MethodItem && item.isKotlinProperty()) { 2024 return // kotlinc doesn't add nullability https://youtrack.jetbrains.com/issue/KT-45771 2025 } 2026 val where = when (item) { 2027 is ParameterItem -> "parameter `${item.name()}` in method `${item.parent()?.name()}`" 2028 is FieldItem -> { 2029 if (item.isKotlin()) { 2030 if (item.name() == "INSTANCE") { 2031 // Kotlin compiler is not marking it with a nullability annotation 2032 // https://youtrack.jetbrains.com/issue/KT-33226 2033 return 2034 } 2035 if (item.modifiers.isCompanion()) { 2036 // Kotlin compiler is not marking it with a nullability annotation 2037 // https://youtrack.jetbrains.com/issue/KT-33314 2038 return 2039 } 2040 } 2041 "field `${item.name()}` in class `${item.parent()}`" 2042 } 2043 2044 is ConstructorItem -> "constructor `${item.name()}` return" 2045 is MethodItem -> { 2046 // For methods requiresNullnessInfo and hasNullnessInfo considers both parameters and return, 2047 // only warn about non-annotated returns here as parameters will get visited individually. 2048 if (item.isConstructor() || item.returnType()?.primitive == true) return 2049 if (item.modifiers.hasNullnessInfo()) return 2050 "method `${item.name()}` return" 2051 } 2052 else -> throw IllegalStateException("Unexpected item type: $item") 2053 } 2054 report(MISSING_NULLABILITY, item, "Missing nullability on $where") 2055 } 2056 } 2057 2058 private fun checkBoxed(type: TypeItem, item: Item) { 2059 /* 2060 def verify_boxed(clazz): 2061 """Verifies that methods avoid boxed primitives.""" 2062 2063 boxed = ["java.lang.Number","java.lang.Byte","java.lang.Double","java.lang.Float","java.lang.Integer","java.lang.Long","java.lang.Short"] 2064 2065 for c in clazz.ctors: 2066 for arg in c.args: 2067 if arg in boxed: 2068 error(clazz, c, "M11", "Must avoid boxed primitives") 2069 2070 for f in clazz.fields: 2071 if f.typ in boxed: 2072 error(clazz, f, "M11", "Must avoid boxed primitives") 2073 2074 for m in clazz.methods: 2075 if m.typ in boxed: 2076 error(clazz, m, "M11", "Must avoid boxed primitives") 2077 for arg in m.args: 2078 if arg in boxed: 2079 error(clazz, m, "M11", "Must avoid boxed primitives") 2080 */ 2081 2082 fun isBoxType(qualifiedName: String): Boolean { 2083 return when (qualifiedName) { 2084 "java.lang.Number", 2085 "java.lang.Byte", 2086 "java.lang.Double", 2087 "java.lang.Float", 2088 "java.lang.Integer", 2089 "java.lang.Long", 2090 "java.lang.Short", 2091 "java.lang.Boolean" -> 2092 true 2093 else -> 2094 false 2095 } 2096 } 2097 2098 val qualifiedName = type.asClass()?.qualifiedName() ?: return 2099 if (isBoxType(qualifiedName)) { 2100 report( 2101 AUTO_BOXING, item, 2102 "Must avoid boxed primitives (`$qualifiedName`)" 2103 ) 2104 } 2105 } 2106 2107 private fun checkStaticUtils( 2108 cls: ClassItem, 2109 methods: Sequence<MethodItem>, 2110 constructors: Sequence<ConstructorItem>, 2111 fields: Sequence<FieldItem> 2112 ) { 2113 /* 2114 def verify_static_utils(clazz): 2115 """Verifies that helper classes can't be constructed.""" 2116 if clazz.fullname.startswith("android.opengl"): return 2117 if clazz.fullname.startswith("android.R"): return 2118 2119 # Only care about classes with default constructors 2120 if len(clazz.ctors) == 1 and len(clazz.ctors[0].args) == 0: 2121 test = [] 2122 test.extend(clazz.fields) 2123 test.extend(clazz.methods) 2124 2125 if len(test) == 0: return 2126 for t in test: 2127 if "static" not in t.split: 2128 return 2129 2130 error(clazz, None, None, "Fully-static utility classes must not have constructor") 2131 */ 2132 if (!cls.isClass()) { 2133 return 2134 } 2135 2136 val hasDefaultConstructor = cls.hasImplicitDefaultConstructor() || run { 2137 if (constructors.count() == 1) { 2138 val constructor = constructors.first() 2139 constructor.parameters().isEmpty() && constructor.modifiers.isPublic() 2140 } else { 2141 false 2142 } 2143 } 2144 2145 if (hasDefaultConstructor) { 2146 val qualifiedName = cls.qualifiedName() 2147 if (qualifiedName.startsWith("android.opengl.") || 2148 qualifiedName.startsWith("android.R.") || 2149 qualifiedName == "android.R" 2150 ) { 2151 return 2152 } 2153 2154 if (methods.none() && fields.none()) { 2155 return 2156 } 2157 2158 if (methods.none { !it.modifiers.isStatic() } && 2159 fields.none { !it.modifiers.isStatic() }) { 2160 report( 2161 STATIC_UTILS, cls, 2162 "Fully-static utility classes must not have constructor" 2163 ) 2164 } 2165 } 2166 } 2167 2168 private fun checkOverloadArgs(cls: ClassItem, methods: Sequence<MethodItem>) { 2169 /* 2170 def verify_overload_args(clazz): 2171 """Verifies that method overloads add new arguments at the end.""" 2172 if clazz.fullname.startswith("android.opengl"): return 2173 2174 overloads = collections.defaultdict(list) 2175 for m in clazz.methods: 2176 if "deprecated" in m.split: continue 2177 overloads[m.name].append(m) 2178 2179 for name, methods in overloads.items(): 2180 if len(methods) <= 1: continue 2181 2182 # Look for arguments common across all overloads 2183 def cluster(args): 2184 count = collections.defaultdict(int) 2185 res = set() 2186 for i in range(len(args)): 2187 a = args[i] 2188 res.add("%s#%d" % (a, count[a])) 2189 count[a] += 1 2190 return res 2191 2192 common_args = cluster(methods[0].args) 2193 for m in methods: 2194 common_args = common_args & cluster(m.args) 2195 2196 if len(common_args) == 0: continue 2197 2198 # Require that all common arguments are present at start of signature 2199 locked_sig = None 2200 for m in methods: 2201 sig = m.args[0:len(common_args)] 2202 if not common_args.issubset(cluster(sig)): 2203 warn(clazz, m, "M2", "Expected common arguments [%s] at beginning of overloaded method" % (", ".join(common_args))) 2204 elif not locked_sig: 2205 locked_sig = sig 2206 elif locked_sig != sig: 2207 error(clazz, m, "M2", "Expected consistent argument ordering between overloads: %s..." % (", ".join(locked_sig))) 2208 */ 2209 2210 if (cls.qualifiedName().startsWith("android.opengl")) { 2211 return 2212 } 2213 2214 val overloads = mutableMapOf<String, MutableList<MethodItem>>() 2215 for (method in methods) { 2216 if (!method.deprecated) { 2217 val name = method.name() 2218 val list = overloads[name] ?: run { 2219 val new = mutableListOf<MethodItem>() 2220 overloads[name] = new 2221 new 2222 } 2223 list.add(method) 2224 } 2225 } 2226 2227 // Look for arguments common across all overloads 2228 fun cluster(args: List<ParameterItem>): MutableSet<String> { 2229 val count = mutableMapOf<String, Int>() 2230 val res = mutableSetOf<String>() 2231 for (parameter in args) { 2232 val a = parameter.type().toTypeString() 2233 val currCount = count[a] ?: 1 2234 res.add("$a#$currCount") 2235 count[a] = currCount + 1 2236 } 2237 return res 2238 } 2239 2240 for ((_, methodList) in overloads.entries) { 2241 if (methodList.size <= 1) { 2242 continue 2243 } 2244 2245 val commonArgs = cluster(methodList[0].parameters()) 2246 for (m in methodList) { 2247 val clustered = cluster(m.parameters()) 2248 commonArgs.removeAll(clustered) 2249 } 2250 if (commonArgs.isEmpty()) { 2251 continue 2252 } 2253 2254 // Require that all common arguments are present at the start of the signature 2255 var lockedSig: List<ParameterItem>? = null 2256 val commonArgCount = commonArgs.size 2257 for (m in methodList) { 2258 val sig = m.parameters().subList(0, commonArgCount) 2259 val cluster = cluster(sig) 2260 if (!cluster.containsAll(commonArgs)) { 2261 report( 2262 COMMON_ARGS_FIRST, m, 2263 "Expected common arguments ${commonArgs.joinToString()}} at beginning of overloaded method ${m.describe()}" 2264 ) 2265 } else if (lockedSig == null) { 2266 lockedSig = sig 2267 } else if (lockedSig != sig) { 2268 report( 2269 CONSISTENT_ARGUMENT_ORDER, m, 2270 "Expected consistent argument ordering between overloads: ${lockedSig.joinToString()}}" 2271 ) 2272 } 2273 } 2274 } 2275 } 2276 2277 private fun checkCallbackHandlers( 2278 cls: ClassItem, 2279 methodsAndConstructors: Sequence<MethodItem>, 2280 superClass: ClassItem? 2281 ) { 2282 /* 2283 def verify_callback_handlers(clazz): 2284 """Verifies that methods adding listener/callback have overload 2285 for specifying delivery thread.""" 2286 2287 # Ignore UI packages which assume main thread 2288 skip = [ 2289 "animation", 2290 "view", 2291 "graphics", 2292 "transition", 2293 "widget", 2294 "webkit", 2295 ] 2296 for s in skip: 2297 if s in clazz.pkg.name_path: return 2298 if s in clazz.extends_path: return 2299 2300 # Ignore UI classes which assume main thread 2301 if "app" in clazz.pkg.name_path or "app" in clazz.extends_path: 2302 for s in ["ActionBar","Dialog","Application","Activity","Fragment","Loader"]: 2303 if s in clazz.fullname: return 2304 if "content" in clazz.pkg.name_path or "content" in clazz.extends_path: 2305 for s in ["Loader"]: 2306 if s in clazz.fullname: return 2307 2308 found = {} 2309 by_name = collections.defaultdict(list) 2310 examine = clazz.ctors + clazz.methods 2311 for m in examine: 2312 if m.name.startswith("unregister"): continue 2313 if m.name.startswith("remove"): continue 2314 if re.match("on[A-Z]+", m.name): continue 2315 2316 by_name[m.name].append(m) 2317 2318 for a in m.args: 2319 if a.endswith("Listener") or a.endswith("Callback") or a.endswith("Callbacks"): 2320 found[m.name] = m 2321 2322 for f in found.values(): 2323 takes_handler = False 2324 takes_exec = False 2325 for m in by_name[f.name]: 2326 if "android.os.Handler" in m.args: 2327 takes_handler = True 2328 if "java.util.concurrent.Executor" in m.args: 2329 takes_exec = True 2330 if not takes_exec: 2331 warn(clazz, f, "L1", "Registration methods should have overload that accepts delivery Executor") 2332 2333 */ 2334 2335 // Note: In the above we compute takes_handler but it's not used; is this an incomplete 2336 // check? 2337 2338 fun packageContainsSegment(packageName: String?, segment: String): Boolean { 2339 packageName ?: return false 2340 return (packageName.contains(segment) && 2341 (packageName.contains(".$segment.") || packageName.endsWith(".$segment"))) 2342 } 2343 2344 fun skipPackage(packageName: String?): Boolean { 2345 packageName ?: return false 2346 for (segment in uiPackageParts) { 2347 if (packageContainsSegment(packageName, segment)) { 2348 return true 2349 } 2350 } 2351 2352 return false 2353 } 2354 2355 // Ignore UI packages which assume main thread 2356 val classPackage = cls.containingPackage().qualifiedName() 2357 val extendsPackage = superClass?.containingPackage()?.qualifiedName() 2358 2359 if (skipPackage(classPackage) || skipPackage(extendsPackage)) { 2360 return 2361 } 2362 2363 // Ignore UI classes which assume main thread 2364 if (packageContainsSegment(classPackage, "app") || 2365 packageContainsSegment(extendsPackage, "app") 2366 ) { 2367 val fullName = cls.fullName() 2368 if (fullName.contains("ActionBar") || 2369 fullName.contains("Dialog") || 2370 fullName.contains("Application") || 2371 fullName.contains("Activity") || 2372 fullName.contains("Fragment") || 2373 fullName.contains("Loader") 2374 ) { 2375 return 2376 } 2377 } 2378 if (packageContainsSegment(classPackage, "content") || 2379 packageContainsSegment(extendsPackage, "content") 2380 ) { 2381 val fullName = cls.fullName() 2382 if (fullName.contains("Loader")) { 2383 return 2384 } 2385 } 2386 2387 val found = mutableMapOf<String, MethodItem>() 2388 val byName = mutableMapOf<String, MutableList<MethodItem>>() 2389 for (method in methodsAndConstructors) { 2390 val name = method.name() 2391 if (name.startsWith("unregister")) { 2392 continue 2393 } 2394 if (name.startsWith("remove")) { 2395 continue 2396 } 2397 if (name.startsWith("on") && onCallbackNamePattern.matches(name)) { 2398 continue 2399 } 2400 2401 val list = byName[name] ?: run { 2402 val new = mutableListOf<MethodItem>() 2403 byName[name] = new 2404 new 2405 } 2406 list.add(method) 2407 2408 for (parameter in method.parameters()) { 2409 val type = parameter.type().toTypeString() 2410 if (type.endsWith("Listener") || 2411 type.endsWith("Callback") || 2412 type.endsWith("Callbacks") 2413 ) { 2414 found[name] = method 2415 } 2416 } 2417 } 2418 2419 for (f in found.values) { 2420 var takesExec = false 2421 2422 // TODO: apilint computed takes_handler but did not use it; should we add more checks or conditions? 2423 // var takesHandler = false 2424 2425 val name = f.name() 2426 for (method in byName[name]!!) { 2427 // if (method.parameters().any { it.type().toTypeString() == "android.os.Handler" }) { 2428 // takesHandler = true 2429 // } 2430 if (method.parameters().any { it.type().toTypeString() == "java.util.concurrent.Executor" }) { 2431 takesExec = true 2432 } 2433 } 2434 if (!takesExec) { 2435 report( 2436 EXECUTOR_REGISTRATION, f, 2437 "Registration methods should have overload that accepts delivery Executor: `$name`" 2438 ) 2439 } 2440 } 2441 } 2442 2443 private fun checkContextFirst(method: MethodItem) { 2444 /* 2445 def verify_context_first(clazz): 2446 """Verifies that methods accepting a Context keep it the first argument.""" 2447 examine = clazz.ctors + clazz.methods 2448 for m in examine: 2449 if len(m.args) > 1 and m.args[0] != "android.content.Context": 2450 if "android.content.Context" in m.args[1:]: 2451 error(clazz, m, "M3", "Context is distinct, so it must be the first argument") 2452 if len(m.args) > 1 and m.args[0] != "android.content.ContentResolver": 2453 if "android.content.ContentResolver" in m.args[1:]: 2454 error(clazz, m, "M3", "ContentResolver is distinct, so it must be the first argument") 2455 */ 2456 val parameters = method.parameters() 2457 if (parameters.size > 1 && parameters[0].type().toTypeString() != "android.content.Context") { 2458 for (i in 1 until parameters.size) { 2459 val p = parameters[i] 2460 if (p.type().toTypeString() == "android.content.Context") { 2461 report( 2462 CONTEXT_FIRST, p, 2463 "Context is distinct, so it must be the first argument (method `${method.name()}`)" 2464 ) 2465 } 2466 } 2467 } 2468 if (parameters.size > 1 && parameters[0].type().toTypeString() != "android.content.ContentResolver") { 2469 for (i in 1 until parameters.size) { 2470 val p = parameters[i] 2471 if (p.type().toTypeString() == "android.content.ContentResolver") { 2472 report( 2473 CONTEXT_FIRST, p, 2474 "ContentResolver is distinct, so it must be the first argument (method `${method.name()}`)" 2475 ) 2476 } 2477 } 2478 } 2479 } 2480 2481 private fun checkListenerLast(method: MethodItem) { 2482 /* 2483 def verify_listener_last(clazz): 2484 """Verifies that methods accepting a Listener or Callback keep them as last arguments.""" 2485 examine = clazz.ctors + clazz.methods 2486 for m in examine: 2487 if "Listener" in m.name or "Callback" in m.name: continue 2488 found = False 2489 for a in m.args: 2490 if a.endswith("Callback") or a.endswith("Callbacks") or a.endswith("Listener"): 2491 found = True 2492 elif found: 2493 warn(clazz, m, "M3", "Listeners should always be at end of argument list") 2494 */ 2495 2496 val name = method.name() 2497 if (name.contains("Listener") || name.contains("Callback")) { 2498 return 2499 } 2500 2501 val parameters = method.parameters() 2502 if (parameters.size > 1) { 2503 var found = false 2504 for (parameter in parameters) { 2505 val type = parameter.type().toTypeString() 2506 if (type.endsWith("Callback") || type.endsWith("Callbacks") || type.endsWith("Listener")) { 2507 found = true 2508 } else if (found) { 2509 report( 2510 LISTENER_LAST, parameter, 2511 "Listeners should always be at end of argument list (method `${method.name()}`)" 2512 ) 2513 } 2514 } 2515 } 2516 } 2517 2518 private fun checkResourceNames(cls: ClassItem, fields: Sequence<FieldItem>) { 2519 /* 2520 def verify_resource_names(clazz): 2521 """Verifies that resource names have consistent case.""" 2522 if not re.match("android\.R\.[a-z]+", clazz.fullname): return 2523 2524 # Resources defined by files are foo_bar_baz 2525 if clazz.name in ["anim","animator","color","dimen","drawable","interpolator","layout","transition","menu","mipmap","string","plurals","raw","xml"]: 2526 for f in clazz.fields: 2527 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue 2528 if f.name.startswith("config_"): 2529 error(clazz, f, None, "Expected config name to be config_fooBarBaz style") 2530 2531 if re.match("[a-z1-9_]+$", f.name): continue 2532 error(clazz, f, None, "Expected resource name in this class to be foo_bar_baz style") 2533 2534 # Resources defined inside files are fooBarBaz 2535 if clazz.name in ["array","attr","id","bool","fraction","integer"]: 2536 for f in clazz.fields: 2537 if re.match("config_[a-z][a-zA-Z1-9]*$", f.name): continue 2538 if re.match("layout_[a-z][a-zA-Z1-9]*$", f.name): continue 2539 if re.match("state_[a-z_]*$", f.name): continue 2540 2541 if re.match("[a-z][a-zA-Z1-9]*$", f.name): continue 2542 error(clazz, f, "C7", "Expected resource name in this class to be fooBarBaz style") 2543 2544 # Styles are FooBar_Baz 2545 if clazz.name in ["style"]: 2546 for f in clazz.fields: 2547 if re.match("[A-Z][A-Za-z1-9]+(_[A-Z][A-Za-z1-9]+?)*$", f.name): continue 2548 error(clazz, f, "C7", "Expected resource name in this class to be FooBar_Baz style") 2549 */ 2550 if (!cls.qualifiedName().startsWith("android.R.")) { 2551 return 2552 } 2553 2554 val resourceType = ResourceType.fromClassName(cls.simpleName()) ?: return 2555 when (resourceType) { 2556 ANIM, 2557 ANIMATOR, 2558 COLOR, 2559 DIMEN, 2560 DRAWABLE, 2561 FONT, 2562 INTERPOLATOR, 2563 LAYOUT, 2564 MENU, 2565 MIPMAP, 2566 NAVIGATION, 2567 PLURALS, 2568 RAW, 2569 STRING, 2570 TRANSITION, 2571 XML -> { 2572 // Resources defined by files are foo_bar_baz 2573 // Note: it's surprising that dimen, plurals and string are in this list since 2574 // they are value resources, not file resources, but keeping api lint compatibility 2575 // for now. 2576 2577 for (field in fields) { 2578 val name = field.name() 2579 if (name.startsWith("config_")) { 2580 if (!configFieldPattern.matches(name)) { 2581 report( 2582 CONFIG_FIELD_NAME, field, 2583 "Expected config name to be in the `config_fooBarBaz` style, was `$name`" 2584 ) 2585 } 2586 continue 2587 } 2588 if (!resourceFileFieldPattern.matches(name)) { 2589 report( 2590 RESOURCE_FIELD_NAME, field, 2591 "Expected resource name in `${cls.qualifiedName()}` to be in the `foo_bar_baz` style, was `$name`" 2592 ) 2593 } 2594 } 2595 } 2596 2597 ARRAY, 2598 ATTR, 2599 BOOL, 2600 FRACTION, 2601 ID, 2602 INTEGER -> { 2603 // Resources defined inside files are fooBarBaz 2604 for (field in fields) { 2605 val name = field.name() 2606 if (name.startsWith("config_") && configFieldPattern.matches(name)) { 2607 continue 2608 } 2609 if (name.startsWith("layout_") && layoutFieldPattern.matches(name)) { 2610 continue 2611 } 2612 if (name.startsWith("state_") && stateFieldPattern.matches(name)) { 2613 continue 2614 } 2615 if (resourceValueFieldPattern.matches(name)) { 2616 continue 2617 } 2618 report( 2619 RESOURCE_VALUE_FIELD_NAME, field, 2620 "Expected resource name in `${cls.qualifiedName()}` to be in the `fooBarBaz` style, was `$name`" 2621 ) 2622 } 2623 } 2624 2625 STYLE -> { 2626 for (field in fields) { 2627 val name = field.name() 2628 if (!styleFieldPattern.matches(name)) { 2629 report( 2630 RESOURCE_STYLE_FIELD_NAME, field, 2631 "Expected resource name in `${cls.qualifiedName()}` to be in the `FooBar_Baz` style, was `$name`" 2632 ) 2633 } 2634 } 2635 } 2636 2637 STYLEABLE, // appears as R class but name check is implicitly done as part of style class check 2638 // DECLARE_STYLEABLE, 2639 STYLE_ITEM, 2640 PUBLIC, 2641 SAMPLE_DATA, 2642 AAPT -> { 2643 // no-op; these are resource "types" in XML but not present as R classes 2644 // Listed here explicitly to force compiler error as new resource types 2645 // are added. 2646 } 2647 } 2648 } 2649 2650 private fun checkFiles(methodsAndConstructors: Sequence<MethodItem>) { 2651 /* 2652 def verify_files(clazz): 2653 """Verifies that methods accepting File also accept streams.""" 2654 2655 has_file = set() 2656 has_stream = set() 2657 2658 test = [] 2659 test.extend(clazz.ctors) 2660 test.extend(clazz.methods) 2661 2662 for m in test: 2663 if "java.io.File" in m.args: 2664 has_file.add(m) 2665 if "java.io.FileDescriptor" in m.args or "android.os.ParcelFileDescriptor" in m.args or "java.io.InputStream" in m.args or "java.io.OutputStream" in m.args: 2666 has_stream.add(m.name) 2667 2668 for m in has_file: 2669 if m.name not in has_stream: 2670 warn(clazz, m, "M10", "Methods accepting File should also accept FileDescriptor or streams") 2671 */ 2672 2673 var hasFile: MutableSet<MethodItem>? = null 2674 var hasStream: MutableSet<String>? = null 2675 for (method in methodsAndConstructors) { 2676 for (parameter in method.parameters()) { 2677 when (parameter.type().toTypeString()) { 2678 "java.io.File" -> { 2679 val set = hasFile ?: run { 2680 val new = mutableSetOf<MethodItem>() 2681 hasFile = new 2682 new 2683 } 2684 set.add(method) 2685 } 2686 "java.io.FileDescriptor", 2687 "android.os.ParcelFileDescriptor", 2688 "java.io.InputStream", 2689 "java.io.OutputStream" -> { 2690 val set = hasStream ?: run { 2691 val new = mutableSetOf<String>() 2692 hasStream = new 2693 new 2694 } 2695 set.add(method.name()) 2696 } 2697 } 2698 } 2699 } 2700 val files = hasFile 2701 if (files != null) { 2702 val streams = hasStream 2703 for (method in files) { 2704 if (streams == null || !streams.contains(method.name())) { 2705 report( 2706 STREAM_FILES, method, 2707 "Methods accepting `File` should also accept `FileDescriptor` or streams: ${method.describe()}" 2708 ) 2709 } 2710 } 2711 } 2712 } 2713 2714 private fun checkManagerList(cls: ClassItem, methods: Sequence<MethodItem>) { 2715 /* 2716 def verify_manager_list(clazz): 2717 """Verifies that managers return List<? extends Parcelable> instead of arrays.""" 2718 2719 if not clazz.name.endswith("Manager"): return 2720 2721 for m in clazz.methods: 2722 if m.typ.startswith("android.") and m.typ.endswith("[]"): 2723 warn(clazz, m, None, "Methods should return List<? extends Parcelable> instead of Parcelable[] to support ParceledListSlice under the hood") 2724 */ 2725 if (!cls.simpleName().endsWith("Manager")) { 2726 return 2727 } 2728 for (method in methods) { 2729 val returnType = method.returnType() ?: continue 2730 if (returnType.primitive) { 2731 return 2732 } 2733 val type = returnType.toTypeString() 2734 if (type.startsWith("android.") && returnType.isArray()) { 2735 report( 2736 PARCELABLE_LIST, method, 2737 "Methods should return `List<? extends Parcelable>` instead of `Parcelable[]` to support `ParceledListSlice` under the hood: ${method.describe()}" 2738 ) 2739 } 2740 } 2741 } 2742 2743 private fun checkAbstractInner(cls: ClassItem) { 2744 /* 2745 def verify_abstract_inner(clazz): 2746 """Verifies that abstract inner classes are static.""" 2747 2748 if re.match(".+?\.[A-Z][^\.]+\.[A-Z]", clazz.fullname): 2749 if " abstract " in clazz.raw and " static " not in clazz.raw: 2750 warn(clazz, None, None, "Abstract inner classes should be static to improve testability") 2751 */ 2752 if (!cls.isTopLevelClass() && cls.isClass() && cls.modifiers.isAbstract() && !cls.modifiers.isStatic()) { 2753 report( 2754 ABSTRACT_INNER, cls, 2755 "Abstract inner classes should be static to improve testability: ${cls.describe()}" 2756 ) 2757 } 2758 } 2759 2760 private fun checkRuntimeExceptions( 2761 methodsAndConstructors: Sequence<MethodItem>, 2762 filterReference: Predicate<Item> 2763 ) { 2764 /* 2765 def verify_runtime_exceptions(clazz): 2766 """Verifies that runtime exceptions aren't listed in throws.""" 2767 2768 banned = [ 2769 "java.lang.NullPointerException", 2770 "java.lang.ClassCastException", 2771 "java.lang.IndexOutOfBoundsException", 2772 "java.lang.reflect.UndeclaredThrowableException", 2773 "java.lang.reflect.MalformedParametersException", 2774 "java.lang.reflect.MalformedParameterizedTypeException", 2775 "java.lang.invoke.WrongMethodTypeException", 2776 "java.lang.EnumConstantNotPresentException", 2777 "java.lang.IllegalMonitorStateException", 2778 "java.lang.SecurityException", 2779 "java.lang.UnsupportedOperationException", 2780 "java.lang.annotation.AnnotationTypeMismatchException", 2781 "java.lang.annotation.IncompleteAnnotationException", 2782 "java.lang.TypeNotPresentException", 2783 "java.lang.IllegalStateException", 2784 "java.lang.ArithmeticException", 2785 "java.lang.IllegalArgumentException", 2786 "java.lang.ArrayStoreException", 2787 "java.lang.NegativeArraySizeException", 2788 "java.util.MissingResourceException", 2789 "java.util.EmptyStackException", 2790 "java.util.concurrent.CompletionException", 2791 "java.util.concurrent.RejectedExecutionException", 2792 "java.util.IllformedLocaleException", 2793 "java.util.ConcurrentModificationException", 2794 "java.util.NoSuchElementException", 2795 "java.io.UncheckedIOException", 2796 "java.time.DateTimeException", 2797 "java.security.ProviderException", 2798 "java.nio.BufferUnderflowException", 2799 "java.nio.BufferOverflowException", 2800 ] 2801 2802 examine = clazz.ctors + clazz.methods 2803 for m in examine: 2804 for t in m.throws: 2805 if t in banned: 2806 error(clazz, m, None, "Methods must not mention RuntimeException subclasses in throws clauses") 2807 2808 */ 2809 for (method in methodsAndConstructors) { 2810 if (method.synthetic) { 2811 continue 2812 } 2813 for (throws in method.filteredThrowsTypes(filterReference)) { 2814 when (throws.qualifiedName()) { 2815 "java.lang.NullPointerException", 2816 "java.lang.ClassCastException", 2817 "java.lang.IndexOutOfBoundsException", 2818 "java.lang.reflect.UndeclaredThrowableException", 2819 "java.lang.reflect.MalformedParametersException", 2820 "java.lang.reflect.MalformedParameterizedTypeException", 2821 "java.lang.invoke.WrongMethodTypeException", 2822 "java.lang.EnumConstantNotPresentException", 2823 "java.lang.IllegalMonitorStateException", 2824 "java.lang.SecurityException", 2825 "java.lang.UnsupportedOperationException", 2826 "java.lang.annotation.AnnotationTypeMismatchException", 2827 "java.lang.annotation.IncompleteAnnotationException", 2828 "java.lang.TypeNotPresentException", 2829 "java.lang.IllegalStateException", 2830 "java.lang.ArithmeticException", 2831 "java.lang.IllegalArgumentException", 2832 "java.lang.ArrayStoreException", 2833 "java.lang.NegativeArraySizeException", 2834 "java.util.MissingResourceException", 2835 "java.util.EmptyStackException", 2836 "java.util.concurrent.CompletionException", 2837 "java.util.concurrent.RejectedExecutionException", 2838 "java.util.IllformedLocaleException", 2839 "java.util.ConcurrentModificationException", 2840 "java.util.NoSuchElementException", 2841 "java.io.UncheckedIOException", 2842 "java.time.DateTimeException", 2843 "java.security.ProviderException", 2844 "java.nio.BufferUnderflowException", 2845 "java.nio.BufferOverflowException" -> { 2846 report( 2847 BANNED_THROW, method, 2848 "Methods must not mention RuntimeException subclasses in throws clauses (was `${throws.qualifiedName()}`)" 2849 ) 2850 } 2851 } 2852 } 2853 } 2854 } 2855 2856 private fun checkError(cls: ClassItem, superClass: ClassItem?) { 2857 /* 2858 def verify_error(clazz): 2859 """Verifies that we always use Exception instead of Error.""" 2860 if not clazz.extends: return 2861 if clazz.extends.endswith("Error"): 2862 error(clazz, None, None, "Trouble must be reported through an Exception, not Error") 2863 if clazz.extends.endswith("Exception") and not clazz.name.endswith("Exception"): 2864 error(clazz, None, None, "Exceptions must be named FooException") 2865 */ 2866 superClass ?: return 2867 if (superClass.simpleName().endsWith("Error")) { 2868 report( 2869 EXTENDS_ERROR, cls, 2870 "Trouble must be reported through an `Exception`, not an `Error` (`${cls.simpleName()}` extends `${superClass.simpleName()}`)" 2871 ) 2872 } 2873 if (superClass.simpleName().endsWith("Exception") && !cls.simpleName().endsWith("Exception")) { 2874 report( 2875 EXCEPTION_NAME, cls, 2876 "Exceptions must be named `FooException`, was `${cls.simpleName()}`" 2877 ) 2878 } 2879 } 2880 2881 private fun checkUnits(method: MethodItem) { 2882 val returnType = method.returnType() ?: return 2883 var type = returnType.toTypeString() 2884 val name = method.name() 2885 if (type == "int" || type == "long" || type == "short") { 2886 if (badUnits.any { name.endsWith(it.key) }) { 2887 val badUnit = badUnits.keys.find { name.endsWith(it) } 2888 val value = badUnits[badUnit] 2889 report( 2890 METHOD_NAME_UNITS, method, 2891 "Expected method name units to be `$value`, was `$badUnit` in `$name`" 2892 ) 2893 } 2894 } else if (type == "void") { 2895 if (method.parameters().size != 1) { 2896 return 2897 } 2898 type = method.parameters()[0].type().toTypeString() 2899 } 2900 if (name.endsWith("Fraction") && (type == "int" || type == "long" || type == "short")) { 2901 report( 2902 FRACTION_FLOAT, method, 2903 "Fractions must use floats, was `$type` in `$name`" 2904 ) 2905 } else if (name.endsWith("Percentage") && (type == "float" || type == "double")) { 2906 report( 2907 PERCENTAGE_INT, method, 2908 "Percentage must use ints, was `$type` in `$name`" 2909 ) 2910 } 2911 } 2912 2913 private fun checkCloseable(cls: ClassItem, methods: Sequence<MethodItem>) { 2914 /* 2915 def verify_closable(clazz): 2916 """Verifies that classes are AutoClosable.""" 2917 if "implements java.lang.AutoCloseable" in clazz.raw: return 2918 if "implements java.io.Closeable" in clazz.raw: return 2919 2920 for m in clazz.methods: 2921 if len(m.args) > 0: continue 2922 if m.name in ["close","release","destroy","finish","finalize","disconnect","shutdown","stop","free","quit"]: 2923 warn(clazz, m, None, "Classes that release resources should implement AutoClosable and CloseGuard") 2924 return 2925 */ 2926 // AutoClosable has been added in API 19, so libraries with minSdkVersion <19 cannot use it. If the version 2927 // is not set, then keep the check enabled. 2928 val minSdkVersion = codebase.getMinSdkVersion() 2929 if (minSdkVersion is SetMinSdkVersion && minSdkVersion.value < 19) { 2930 return 2931 } 2932 2933 val foundMethods = methods.filter { method -> 2934 when (method.name()) { 2935 "close", "release", "destroy", "finish", "finalize", "disconnect", "shutdown", "stop", "free", "quit" -> true 2936 else -> false 2937 } 2938 } 2939 if (foundMethods.iterator().hasNext() && !cls.implements("java.lang.AutoCloseable")) { // includes java.io.Closeable 2940 val foundMethodsDescriptions = foundMethods.joinToString { method -> "${method.name()}()" } 2941 report( 2942 NOT_CLOSEABLE, cls, 2943 "Classes that release resources ($foundMethodsDescriptions) should implement AutoClosable and CloseGuard: ${cls.describe()}" 2944 ) 2945 } 2946 } 2947 2948 private fun checkNotKotlinOperator(methods: Sequence<MethodItem>) { 2949 /* 2950 def verify_method_name_not_kotlin_operator(clazz): 2951 """Warn about method names which become operators in Kotlin.""" 2952 2953 binary = set() 2954 2955 def unique_binary_op(m, op): 2956 if op in binary: 2957 error(clazz, m, None, "Only one of '{0}' and '{0}Assign' methods should be present for Kotlin".format(op)) 2958 binary.add(op) 2959 2960 for m in clazz.methods: 2961 if 'static' in m.split: 2962 continue 2963 2964 # https://kotlinlang.org/docs/reference/operator-overloading.html#unary-prefix-operators 2965 if m.name in ["unaryPlus", "unaryMinus", "not"] and len(m.args) == 0: 2966 warn(clazz, m, None, "Method can be invoked as a unary operator from Kotlin") 2967 2968 # https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements 2969 if m.name in ["inc", "dec"] and len(m.args) == 0 and m.typ != "void": 2970 # This only applies if the return type is the same or a subtype of the enclosing class, but we have no 2971 # practical way of checking that relationship here. 2972 warn(clazz, m, None, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin") 2973 2974 # https://kotlinlang.org/docs/reference/operator-overloading.html#arithmetic 2975 if m.name in ["plus", "minus", "times", "div", "rem", "mod", "rangeTo"] and len(m.args) == 1: 2976 warn(clazz, m, None, "Method can be invoked as a binary operator from Kotlin") 2977 unique_binary_op(m, m.name) 2978 2979 # https://kotlinlang.org/docs/reference/operator-overloading.html#in 2980 if m.name == "contains" and len(m.args) == 1 and m.typ == "boolean": 2981 warn(clazz, m, None, "Method can be invoked as a "in" operator from Kotlin") 2982 2983 # https://kotlinlang.org/docs/reference/operator-overloading.html#indexed 2984 if (m.name == "get" and len(m.args) > 0) or (m.name == "set" and len(m.args) > 1): 2985 warn(clazz, m, None, "Method can be invoked with an indexing operator from Kotlin") 2986 2987 # https://kotlinlang.org/docs/reference/operator-overloading.html#invoke 2988 if m.name == "invoke": 2989 warn(clazz, m, None, "Method can be invoked with function call syntax from Kotlin") 2990 2991 # https://kotlinlang.org/docs/reference/operator-overloading.html#assignments 2992 if m.name in ["plusAssign", "minusAssign", "timesAssign", "divAssign", "remAssign", "modAssign"] \ 2993 and len(m.args) == 1 \ 2994 and m.typ == "void": 2995 warn(clazz, m, None, "Method can be invoked as a compound assignment operator from Kotlin") 2996 unique_binary_op(m, m.name[:-6]) # Remove "Assign" suffix 2997 2998 */ 2999 3000 fun flagKotlinOperator(method: MethodItem, message: String) { 3001 if (method.isKotlin()) { 3002 report( 3003 KOTLIN_OPERATOR, method, 3004 "Note that adding the `operator` keyword would allow calling this method using operator syntax") 3005 } else { 3006 report( 3007 KOTLIN_OPERATOR, method, 3008 "$message (this is usually desirable; just make sure it makes sense for this type of object)" 3009 ) 3010 } 3011 } 3012 3013 for (method in methods) { 3014 if (method.modifiers.isStatic() || method.modifiers.isOperator() || method.superMethods().isNotEmpty()) { 3015 continue 3016 } 3017 when (val name = method.name()) { 3018 // https://kotlinlang.org/docs/reference/operator-overloading.html#unary-prefix-operators 3019 "unaryPlus", "unaryMinus", "not" -> { 3020 if (method.parameters().isEmpty()) { 3021 flagKotlinOperator( 3022 method, "Method can be invoked as a unary operator from Kotlin: `$name`" 3023 ) 3024 } 3025 } 3026 // https://kotlinlang.org/docs/reference/operator-overloading.html#increments-and-decrements 3027 "inc", "dec" -> { 3028 if (method.parameters().isEmpty() && method.returnType()?.toTypeString() != "void") { 3029 flagKotlinOperator( 3030 method, "Method can be invoked as a pre/postfix inc/decrement operator from Kotlin: `$name`" 3031 ) 3032 } 3033 } 3034 // https://kotlinlang.org/docs/reference/operator-overloading.html#arithmetic 3035 "plus", "minus", "times", "div", "rem", "mod", "rangeTo" -> { 3036 if (method.parameters().size == 1) { 3037 flagKotlinOperator( 3038 method, "Method can be invoked as a binary operator from Kotlin: `$name`" 3039 ) 3040 } 3041 val assignName = name + "Assign" 3042 3043 if (methods.any { 3044 it.name() == assignName && 3045 it.parameters().size == 1 && 3046 it.returnType()?.toTypeString() == "void" 3047 }) { 3048 report( 3049 UNIQUE_KOTLIN_OPERATOR, method, 3050 "Only one of `$name` and `${name}Assign` methods should be present for Kotlin" 3051 ) 3052 } 3053 } 3054 // https://kotlinlang.org/docs/reference/operator-overloading.html#in 3055 "contains" -> { 3056 if (method.parameters().size == 1 && method.returnType()?.toTypeString() == "boolean") { 3057 flagKotlinOperator( 3058 method, "Method can be invoked as a \"in\" operator from Kotlin: `$name`" 3059 ) 3060 } 3061 } 3062 // https://kotlinlang.org/docs/reference/operator-overloading.html#indexed 3063 "get" -> { 3064 if (method.parameters().isNotEmpty()) { 3065 flagKotlinOperator( 3066 method, "Method can be invoked with an indexing operator from Kotlin: `$name`" 3067 ) 3068 } 3069 } 3070 // https://kotlinlang.org/docs/reference/operator-overloading.html#indexed 3071 "set" -> { 3072 if (method.parameters().size > 1) { 3073 flagKotlinOperator( 3074 method, "Method can be invoked with an indexing operator from Kotlin: `$name`" 3075 ) 3076 } 3077 } 3078 // https://kotlinlang.org/docs/reference/operator-overloading.html#invoke 3079 "invoke" -> { 3080 if (method.parameters().size > 1) { 3081 flagKotlinOperator( 3082 method, "Method can be invoked with function call syntax from Kotlin: `$name`" 3083 ) 3084 } 3085 } 3086 // https://kotlinlang.org/docs/reference/operator-overloading.html#assignments 3087 "plusAssign", "minusAssign", "timesAssign", "divAssign", "remAssign", "modAssign" -> { 3088 if (method.parameters().size == 1 && method.returnType()?.toTypeString() == "void") { 3089 flagKotlinOperator( 3090 method, "Method can be invoked as a compound assignment operator from Kotlin: `$name`" 3091 ) 3092 } 3093 } 3094 } 3095 } 3096 } 3097 3098 private fun checkCollectionsOverArrays(type: TypeItem, typeString: String, item: Item) { 3099 /* 3100 def verify_collections_over_arrays(clazz): 3101 """Warn that [] should be Collections.""" 3102 3103 safe = ["java.lang.String[]","byte[]","short[]","int[]","long[]","float[]","double[]","boolean[]","char[]"] 3104 for m in clazz.methods: 3105 if m.typ.endswith("[]") and m.typ not in safe: 3106 warn(clazz, m, None, "Method should return Collection<> (or subclass) instead of raw array") 3107 for arg in m.args: 3108 if arg.endswith("[]") and arg not in safe: 3109 warn(clazz, m, None, "Method argument should be Collection<> (or subclass) instead of raw array") 3110 3111 */ 3112 3113 if (!type.isArray() || (item is ParameterItem && item.isVarArgs())) { 3114 return 3115 } 3116 3117 when (typeString) { 3118 "java.lang.String[]", 3119 "byte[]", 3120 "short[]", 3121 "int[]", 3122 "long[]", 3123 "float[]", 3124 "double[]", 3125 "boolean[]", 3126 "char[]" -> { 3127 return 3128 } 3129 else -> { 3130 val action = when (item) { 3131 is MethodItem -> { 3132 if (item.name() == "values" && item.containingClass().isEnum()) { 3133 return 3134 } 3135 if (item.containingClass().extends("java.lang.annotation.Annotation")) { 3136 // Annotation are allowed to use arrays 3137 return 3138 } 3139 "Method should return" 3140 } 3141 is FieldItem -> "Field should be" 3142 else -> "Method parameter should be" 3143 } 3144 val component = type.asClass()?.simpleName() ?: "" 3145 report( 3146 ARRAY_RETURN, item, 3147 "$action Collection<$component> (or subclass) instead of raw array; was `$typeString`" 3148 ) 3149 } 3150 } 3151 } 3152 3153 private fun checkUserHandle(cls: ClassItem, methods: Sequence<MethodItem>) { 3154 /* 3155 def verify_user_handle(clazz): 3156 """Methods taking UserHandle should be ForUser or AsUser.""" 3157 if clazz.name.endswith("Listener") or clazz.name.endswith("Callback") or clazz.name.endswith("Callbacks"): return 3158 if clazz.fullname == "android.app.admin.DeviceAdminReceiver": return 3159 if clazz.fullname == "android.content.pm.LauncherApps": return 3160 if clazz.fullname == "android.os.UserHandle": return 3161 if clazz.fullname == "android.os.UserManager": return 3162 3163 for m in clazz.methods: 3164 if re.match("on[A-Z]+", m.name): continue 3165 3166 has_arg = "android.os.UserHandle" in m.args 3167 has_name = m.name.endswith("AsUser") or m.name.endswith("ForUser") 3168 3169 if clazz.fullname.endswith("Manager") and has_arg: 3170 warn(clazz, m, None, "When a method overload is needed to target a specific " 3171 "UserHandle, callers should be directed to use " 3172 "Context.createPackageContextAsUser() and re-obtain the relevant " 3173 "Manager, and no new API should be added") 3174 elif has_arg and not has_name: 3175 warn(clazz, m, None, "Method taking UserHandle should be named 'doFooAsUser' " 3176 "or 'queryFooForUser'") 3177 3178 */ 3179 val qualifiedName = cls.qualifiedName() 3180 if (qualifiedName == "android.app.admin.DeviceAdminReceiver" || 3181 qualifiedName == "android.content.pm.LauncherApps" || 3182 qualifiedName == "android.os.UserHandle" || 3183 qualifiedName == "android.os.UserManager" 3184 ) { 3185 return 3186 } 3187 3188 for (method in methods) { 3189 val parameters = method.parameters() 3190 if (parameters.isEmpty()) { 3191 continue 3192 } 3193 val name = method.name() 3194 if (name.startsWith("on") && onCallbackNamePattern.matches(name)) { 3195 continue 3196 } 3197 val hasArg = parameters.any { it.type().toTypeString() == "android.os.UserHandle" } 3198 if (!hasArg) { 3199 continue 3200 } 3201 if (qualifiedName.endsWith("Manager")) { 3202 report( 3203 USER_HANDLE, method, 3204 "When a method overload is needed to target a specific " + 3205 "UserHandle, callers should be directed to use " + 3206 "Context.createPackageContextAsUser() and re-obtain the relevant " + 3207 "Manager, and no new API should be added" 3208 ) 3209 } else if (!(name.endsWith("AsUser") || name.endsWith("ForUser"))) { 3210 report( 3211 USER_HANDLE_NAME, method, 3212 "Method taking UserHandle should be named `doFooAsUser` or `queryFooForUser`, was `$name`" 3213 ) 3214 } 3215 } 3216 } 3217 3218 private fun checkParams(cls: ClassItem) { 3219 /* 3220 def verify_params(clazz): 3221 """Parameter classes should be 'Params'.""" 3222 if clazz.name.endswith("Params"): return 3223 if clazz.fullname == "android.app.ActivityOptions": return 3224 if clazz.fullname == "android.app.BroadcastOptions": return 3225 if clazz.fullname == "android.os.Bundle": return 3226 if clazz.fullname == "android.os.BaseBundle": return 3227 if clazz.fullname == "android.os.PersistableBundle": return 3228 3229 bad = ["Param","Parameter","Parameters","Args","Arg","Argument","Arguments","Options","Bundle"] 3230 for b in bad: 3231 if clazz.name.endswith(b): 3232 error(clazz, None, None, "Classes holding a set of parameters should be called 'FooParams'") 3233 */ 3234 3235 val qualifiedName = cls.qualifiedName() 3236 for (suffix in badParameterClassNames) { 3237 if (qualifiedName.endsWith(suffix) && !((qualifiedName.endsWith("Params") || 3238 qualifiedName == "android.app.ActivityOptions" || 3239 qualifiedName == "android.app.BroadcastOptions" || 3240 qualifiedName == "android.os.Bundle" || 3241 qualifiedName == "android.os.BaseBundle" || 3242 qualifiedName == "android.os.PersistableBundle")) 3243 ) { 3244 report( 3245 USER_HANDLE_NAME, cls, 3246 "Classes holding a set of parameters should be called `FooParams`, was `${cls.simpleName()}`" 3247 ) 3248 } 3249 } 3250 } 3251 3252 private fun checkServices(field: FieldItem) { 3253 /* 3254 def verify_services(clazz): 3255 """Service name should be FOO_BAR_SERVICE = 'foo_bar'.""" 3256 if clazz.fullname != "android.content.Context": return 3257 3258 for f in clazz.fields: 3259 if f.typ != "java.lang.String": continue 3260 found = re.match(r"([A-Z_]+)_SERVICE", f.name) 3261 if found: 3262 expected = found.group(1).lower() 3263 if f.value != expected: 3264 error(clazz, f, "C4", "Inconsistent service value; expected '%s'" % (expected)) 3265 */ 3266 val type = field.type() 3267 if (!type.isString() || !field.modifiers.isFinal() || !field.modifiers.isStatic() || 3268 field.containingClass().qualifiedName() != "android.content.Context") { 3269 return 3270 } 3271 val name = field.name() 3272 val endsWithService = name.endsWith("_SERVICE") 3273 val value = field.initialValue(requireConstant = true) as? String 3274 3275 if (value == null) { 3276 val mustEndInService = 3277 if (!endsWithService) " and its name must end with `_SERVICE`" else "" 3278 3279 report( 3280 SERVICE_NAME, field, "Non-constant service constant `$name`. Must be static," + 3281 " final and initialized with a String literal$mustEndInService." 3282 ) 3283 return 3284 } 3285 3286 if (name.endsWith("_MANAGER_SERVICE")) { 3287 report( 3288 SERVICE_NAME, field, 3289 "Inconsistent service constant name; expected " + 3290 "`${name.removeSuffix("_MANAGER_SERVICE")}_SERVICE`, was `$name`" 3291 ) 3292 } else if (endsWithService) { 3293 val service = name.substring(0, name.length - "_SERVICE".length).toLowerCase(Locale.US) 3294 if (service != value) { 3295 report( 3296 SERVICE_NAME, field, 3297 "Inconsistent service value; expected `$service`, was `$value` (Note: Do not" + 3298 " change the name of already released services, which will break tools" + 3299 " using `adb shell dumpsys`." + 3300 " Instead add `@SuppressLint(\"${SERVICE_NAME.name}\"))`" 3301 ) 3302 } 3303 } else { 3304 val valueUpper = value.toUpperCase(Locale.US) 3305 report( 3306 SERVICE_NAME, field, "Inconsistent service constant name;" + 3307 " expected `${valueUpper}_SERVICE`, was `$name`" 3308 ) 3309 } 3310 } 3311 3312 private fun checkTense(method: MethodItem) { 3313 /* 3314 def verify_tense(clazz): 3315 """Verify tenses of method names.""" 3316 if clazz.fullname.startswith("android.opengl"): return 3317 3318 for m in clazz.methods: 3319 if m.name.endswith("Enable"): 3320 warn(clazz, m, None, "Unexpected tense; probably meant 'enabled'") 3321 */ 3322 val name = method.name() 3323 if (name.endsWith("Enable")) { 3324 if (method.containingClass().qualifiedName().startsWith("android.opengl")) { 3325 return 3326 } 3327 report( 3328 METHOD_NAME_TENSE, method, 3329 "Unexpected tense; probably meant `enabled`, was `$name`" 3330 ) 3331 } 3332 } 3333 3334 private fun checkIcu(type: TypeItem, typeString: String, item: Item) { 3335 /* 3336 def verify_icu(clazz): 3337 """Verifies that richer ICU replacements are used.""" 3338 better = { 3339 "java.util.TimeZone": "android.icu.util.TimeZone", 3340 "java.util.Calendar": "android.icu.util.Calendar", 3341 "java.util.Locale": "android.icu.util.ULocale", 3342 "java.util.ResourceBundle": "android.icu.util.UResourceBundle", 3343 "java.util.SimpleTimeZone": "android.icu.util.SimpleTimeZone", 3344 "java.util.StringTokenizer": "android.icu.util.StringTokenizer", 3345 "java.util.GregorianCalendar": "android.icu.util.GregorianCalendar", 3346 "java.lang.Character": "android.icu.lang.UCharacter", 3347 "java.text.BreakIterator": "android.icu.text.BreakIterator", 3348 "java.text.Collator": "android.icu.text.Collator", 3349 "java.text.DecimalFormatSymbols": "android.icu.text.DecimalFormatSymbols", 3350 "java.text.NumberFormat": "android.icu.text.NumberFormat", 3351 "java.text.DateFormatSymbols": "android.icu.text.DateFormatSymbols", 3352 "java.text.DateFormat": "android.icu.text.DateFormat", 3353 "java.text.SimpleDateFormat": "android.icu.text.SimpleDateFormat", 3354 "java.text.MessageFormat": "android.icu.text.MessageFormat", 3355 "java.text.DecimalFormat": "android.icu.text.DecimalFormat", 3356 } 3357 3358 for m in clazz.ctors + clazz.methods: 3359 types = [] 3360 types.extend(m.typ) 3361 types.extend(m.args) 3362 for arg in types: 3363 if arg in better: 3364 warn(clazz, m, None, "Type %s should be replaced with richer ICU type %s" % (arg, better[arg])) 3365 */ 3366 if (type.primitive) { 3367 return 3368 } 3369 // ICU types have been added in API 24, so libraries with minSdkVersion <24 cannot use them. 3370 // If the version is not set, then keep the check enabled. 3371 val minSdkVersion = codebase.getMinSdkVersion() 3372 if (minSdkVersion is SetMinSdkVersion && minSdkVersion.value < 24) { 3373 return 3374 } 3375 val better = when (typeString) { 3376 "java.util.TimeZone" -> "android.icu.util.TimeZone" 3377 "java.util.Calendar" -> "android.icu.util.Calendar" 3378 "java.util.Locale" -> "android.icu.util.ULocale" 3379 "java.util.ResourceBundle" -> "android.icu.util.UResourceBundle" 3380 "java.util.SimpleTimeZone" -> "android.icu.util.SimpleTimeZone" 3381 "java.util.StringTokenizer" -> "android.icu.util.StringTokenizer" 3382 "java.util.GregorianCalendar" -> "android.icu.util.GregorianCalendar" 3383 "java.lang.Character" -> "android.icu.lang.UCharacter" 3384 "java.text.BreakIterator" -> "android.icu.text.BreakIterator" 3385 "java.text.Collator" -> "android.icu.text.Collator" 3386 "java.text.DecimalFormatSymbols" -> "android.icu.text.DecimalFormatSymbols" 3387 "java.text.NumberFormat" -> "android.icu.text.NumberFormat" 3388 "java.text.DateFormatSymbols" -> "android.icu.text.DateFormatSymbols" 3389 "java.text.DateFormat" -> "android.icu.text.DateFormat" 3390 "java.text.SimpleDateFormat" -> "android.icu.text.SimpleDateFormat" 3391 "java.text.MessageFormat" -> "android.icu.text.MessageFormat" 3392 "java.text.DecimalFormat" -> "android.icu.text.DecimalFormat" 3393 else -> return 3394 } 3395 report( 3396 USE_ICU, item, 3397 "Type `$typeString` should be replaced with richer ICU type `$better`" 3398 ) 3399 } 3400 3401 private fun checkClone(method: MethodItem) { 3402 /* 3403 def verify_clone(clazz): 3404 """Verify that clone() isn't implemented; see EJ page 61.""" 3405 for m in clazz.methods: 3406 if m.name == "clone": 3407 error(clazz, m, None, "Provide an explicit copy constructor instead of implementing clone()") 3408 */ 3409 if (method.name() == "clone" && method.parameters().isEmpty()) { 3410 report( 3411 NO_CLONE, method, 3412 "Provide an explicit copy constructor instead of implementing `clone()`" 3413 ) 3414 } 3415 } 3416 3417 private fun checkPfd(type: String, item: Item) { 3418 /* 3419 def verify_pfd(clazz): 3420 """Verify that android APIs use PFD over FD.""" 3421 examine = clazz.ctors + clazz.methods 3422 for m in examine: 3423 if m.typ == "java.io.FileDescriptor": 3424 error(clazz, m, "FW11", "Must use ParcelFileDescriptor") 3425 if m.typ == "int": 3426 if "Fd" in m.name or "FD" in m.name or "FileDescriptor" in m.name: 3427 error(clazz, m, "FW11", "Must use ParcelFileDescriptor") 3428 for arg in m.args: 3429 if arg == "java.io.FileDescriptor": 3430 error(clazz, m, "FW11", "Must use ParcelFileDescriptor") 3431 3432 for f in clazz.fields: 3433 if f.typ == "java.io.FileDescriptor": 3434 error(clazz, f, "FW11", "Must use ParcelFileDescriptor") 3435 3436 */ 3437 if (item.containingClass()?.qualifiedName() in lowLevelFileClassNames || 3438 isServiceDumpMethod(item)) { 3439 return 3440 } 3441 3442 if (type == "java.io.FileDescriptor") { 3443 report( 3444 USE_PARCEL_FILE_DESCRIPTOR, item, 3445 "Must use ParcelFileDescriptor instead of FileDescriptor in ${item.describe()}" 3446 ) 3447 } else if (type == "int" && item is MethodItem) { 3448 val name = item.name() 3449 if (name.contains("Fd") || name.contains("FD") || name.contains("FileDescriptor", ignoreCase = true)) { 3450 report( 3451 USE_PARCEL_FILE_DESCRIPTOR, item, 3452 "Must use ParcelFileDescriptor instead of FileDescriptor in ${item.describe()}" 3453 ) 3454 } 3455 } 3456 } 3457 3458 private fun checkNumbers(type: String, item: Item) { 3459 /* 3460 def verify_numbers(clazz): 3461 """Discourage small numbers types like short and byte.""" 3462 3463 discouraged = ["short","byte"] 3464 3465 for c in clazz.ctors: 3466 for arg in c.args: 3467 if arg in discouraged: 3468 warn(clazz, c, "FW12", "Should avoid odd sized primitives; use int instead") 3469 3470 for f in clazz.fields: 3471 if f.typ in discouraged: 3472 warn(clazz, f, "FW12", "Should avoid odd sized primitives; use int instead") 3473 3474 for m in clazz.methods: 3475 if m.typ in discouraged: 3476 warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead") 3477 for arg in m.args: 3478 if arg in discouraged: 3479 warn(clazz, m, "FW12", "Should avoid odd sized primitives; use int instead") 3480 */ 3481 if (type == "short" || type == "byte") { 3482 report( 3483 NO_BYTE_OR_SHORT, item, 3484 "Should avoid odd sized primitives; use `int` instead of `$type` in ${item.describe()}" 3485 ) 3486 } 3487 } 3488 3489 private fun checkSingleton( 3490 cls: ClassItem, 3491 methods: Sequence<MethodItem>, 3492 constructors: Sequence<ConstructorItem> 3493 ) { 3494 /* 3495 def verify_singleton(clazz): 3496 """Catch singleton objects with constructors.""" 3497 3498 singleton = False 3499 for m in clazz.methods: 3500 if m.name.startswith("get") and m.name.endswith("Instance") and " static " in m.raw: 3501 singleton = True 3502 3503 if singleton: 3504 for c in clazz.ctors: 3505 error(clazz, c, None, "Singleton classes should use getInstance() methods") 3506 */ 3507 if (constructors.none()) { 3508 return 3509 } 3510 if (methods.any { it.name().startsWith("get") && it.name().endsWith("Instance") && it.modifiers.isStatic() }) { 3511 for (constructor in constructors) { 3512 report( 3513 SINGLETON_CONSTRUCTOR, constructor, 3514 "Singleton classes should use `getInstance()` methods: `${cls.simpleName()}`" 3515 ) 3516 } 3517 } 3518 } 3519 3520 private fun checkExtends(cls: ClassItem) { 3521 // Call cls.superClass().extends() instead of cls.extends() since extends returns true for self 3522 val superCls = cls.superClass() ?: return 3523 if (superCls.extends("android.os.AsyncTask")) { 3524 report( 3525 FORBIDDEN_SUPER_CLASS, cls, 3526 "${cls.simpleName()} should not extend `AsyncTask`. AsyncTask is an implementation detail. Expose a listener or, in androidx, a `ListenableFuture` API instead" 3527 ) 3528 } 3529 if (superCls.extends("android.app.Activity")) { 3530 report( 3531 FORBIDDEN_SUPER_CLASS, cls, 3532 "${cls.simpleName()} should not extend `Activity`. Activity subclasses are impossible to compose. Expose a composable API instead." 3533 ) 3534 } 3535 badFutureTypes.firstOrNull { cls.extendsOrImplements(it) }?.let { 3536 val extendOrImplement = if (cls.extends(it)) "extend" else "implement" 3537 report( 3538 BAD_FUTURE, cls, "${cls.simpleName()} should not $extendOrImplement `$it`." + 3539 " In AndroidX, use (but do not extend) ListenableFuture. In platform, use a combination of Consumer<T>, Executor, and CancellationSignal`." 3540 ) 3541 } 3542 } 3543 3544 private fun checkTypedef(cls: ClassItem) { 3545 /* 3546 def verify_intdef(clazz): 3547 """intdefs must be @hide, because the constant names cannot be stored in 3548 the stubs (only the values are, which is not useful)""" 3549 if "@interface" not in clazz.split: 3550 return 3551 if "@IntDef" in clazz.annotations or "@LongDef" in clazz.annotations: 3552 error(clazz, None, None, "@IntDef and @LongDef annotations must be @hide") 3553 */ 3554 if (cls.isAnnotationType()) { 3555 cls.modifiers.annotations().firstOrNull { it.isTypeDefAnnotation() }?.let { 3556 report(PUBLIC_TYPEDEF, cls, "Don't expose ${AnnotationItem.simpleName(it)}: ${cls.simpleName()} must be hidden.") 3557 } 3558 } 3559 } 3560 3561 private fun checkUri(typeString: String, item: Item) { 3562 /* 3563 def verify_uris(clazz): 3564 bad = ["java.net.URL", "java.net.URI", "android.net.URL"] 3565 3566 for f in clazz.fields: 3567 if f.typ in bad: 3568 error(clazz, f, None, "Field must be android.net.Uri instead of " + f.typ) 3569 3570 for m in clazz.methods + clazz.ctors: 3571 if m.typ in bad: 3572 error(clazz, m, None, "Must return android.net.Uri instead of " + m.typ) 3573 for arg in m.args: 3574 if arg in bad: 3575 error(clazz, m, None, "Argument must take android.net.Uri instead of " + arg) 3576 */ 3577 badUriTypes.firstOrNull { typeString.contains(it) }?.let { 3578 report( 3579 ANDROID_URI, item, "Use android.net.Uri instead of $it (${item.describe()})" 3580 ) 3581 } 3582 } 3583 3584 private fun checkFutures(typeString: String, item: Item) { 3585 badFutureTypes.firstOrNull { typeString.contains(it) }?.let { 3586 report( 3587 BAD_FUTURE, item, "Use ListenableFuture (library), " + 3588 "or a combination of Consumer<T>, Executor, and CancellationSignal (platform) instead of $it (${item.describe()})" 3589 ) 3590 } 3591 } 3592 3593 private fun isInteresting(cls: ClassItem): Boolean { 3594 val name = cls.qualifiedName() 3595 for (prefix in options.checkApiIgnorePrefix) { 3596 if (name.startsWith(prefix)) { 3597 return false 3598 } 3599 } 3600 return true 3601 } 3602 3603 companion object { 3604 3605 private data class GetterSetterPattern(val getter: String, val setter: String) 3606 private val goodBooleanGetterSetterPrefixes = listOf( 3607 GetterSetterPattern("has", "setHas"), 3608 GetterSetterPattern("can", "setCan"), 3609 GetterSetterPattern("should", "setShould"), 3610 GetterSetterPattern("is", "set") 3611 ) 3612 private fun List<GetterSetterPattern>.match( 3613 name: String, 3614 prop: (GetterSetterPattern) -> String 3615 ) = firstOrNull { 3616 name.startsWith(prop(it)) && name.getOrNull(prop(it).length)?.isUpperCase() ?: false 3617 } 3618 3619 private val badBooleanGetterPrefixes = listOf("isHas", "isCan", "isShould", "get", "is") 3620 private val badBooleanSetterPrefixes = listOf("setIs", "set") 3621 3622 private val badParameterClassNames = listOf( 3623 "Param", "Parameter", "Parameters", "Args", "Arg", "Argument", "Arguments", "Options", "Bundle" 3624 ) 3625 3626 private val badUriTypes = listOf("java.net.URL", "java.net.URI", "android.net.URL") 3627 3628 private val badFutureTypes = listOf( 3629 "java.util.concurrent.CompletableFuture", 3630 "java.util.concurrent.Future" 3631 ) 3632 3633 /** 3634 * Classes for manipulating file descriptors directly, where using ParcelFileDescriptor 3635 * isn't required 3636 */ 3637 private val lowLevelFileClassNames = listOf( 3638 "android.os.FileUtils", 3639 "android.system.Os", 3640 "android.net.util.SocketUtils", 3641 "android.os.NativeHandle", 3642 "android.os.ParcelFileDescriptor" 3643 ) 3644 3645 /** 3646 * Classes which already use bare fields extensively, and bare fields are thus allowed for 3647 * consistency with existing API surface. 3648 */ 3649 private val classesWithBareFields = listOf( 3650 "android.app.ActivityManager.RecentTaskInfo", 3651 "android.app.Notification", 3652 "android.content.pm.ActivityInfo", 3653 "android.content.pm.ApplicationInfo", 3654 "android.content.pm.ComponentInfo", 3655 "android.content.pm.ResolveInfo", 3656 "android.content.pm.FeatureGroupInfo", 3657 "android.content.pm.InstrumentationInfo", 3658 "android.content.pm.PackageInfo", 3659 "android.content.pm.PackageItemInfo", 3660 "android.content.res.Configuration", 3661 "android.graphics.BitmapFactory.Options", 3662 "android.os.Message", 3663 "android.system.StructPollfd" 3664 ) 3665 3666 /** 3667 * Classes containing setting provider keys. 3668 */ 3669 private val settingsKeyClasses = listOf( 3670 "android.provider.Settings.Global", 3671 "android.provider.Settings.Secure", 3672 "android.provider.Settings.System" 3673 ) 3674 3675 private val badUnits = mapOf( 3676 "Ns" to "Nanos", 3677 "Ms" to "Millis or Micros", 3678 "Sec" to "Seconds", 3679 "Secs" to "Seconds", 3680 "Hr" to "Hours", 3681 "Hrs" to "Hours", 3682 "Mo" to "Months", 3683 "Mos" to "Months", 3684 "Yr" to "Years", 3685 "Yrs" to "Years", 3686 "Byte" to "Bytes", 3687 "Space" to "Bytes" 3688 ) 3689 private val uiPackageParts = listOf( 3690 "animation", 3691 "view", 3692 "graphics", 3693 "transition", 3694 "widget", 3695 "webkit" 3696 ) 3697 3698 private val constantNamePattern = Regex("[A-Z0-9_]+") 3699 private val internalNamePattern = Regex("[ms][A-Z0-9].*") 3700 private val fieldNamePattern = Regex("[a-z].*") 3701 private val onCallbackNamePattern = Regex("on[A-Z][a-z0-9][a-zA-Z0-9]*") 3702 private val configFieldPattern = Regex("config_[a-z][a-zA-Z0-9]*") 3703 private val layoutFieldPattern = Regex("layout_[a-z][a-zA-Z0-9]*") 3704 private val stateFieldPattern = Regex("state_[a-z_]+") 3705 private val resourceFileFieldPattern = Regex("[a-z0-9_]+") 3706 private val resourceValueFieldPattern = Regex("[a-z][a-zA-Z0-9]*") 3707 private val styleFieldPattern = Regex("[A-Z][A-Za-z0-9]+(_[A-Z][A-Za-z0-9]+?)*") 3708 3709 private val acronymPattern2 = Regex("([A-Z]){2,}") 3710 private val acronymPattern3 = Regex("([A-Z]){3,}") 3711 3712 private val serviceDumpMethodParameterTypes = 3713 listOf("java.io.FileDescriptor", "java.io.PrintWriter", "java.lang.String[]") 3714 3715 private fun isServiceDumpMethod(item: Item) = when (item) { 3716 is MethodItem -> isServiceDumpMethod(item) 3717 is ParameterItem -> isServiceDumpMethod(item.containingMethod()) 3718 else -> false 3719 } 3720 3721 private fun isServiceDumpMethod(item: MethodItem) = item.name() == "dump" && 3722 item.containingClass().extends("android.app.Service") && 3723 item.parameters().map { it.type().toTypeString() } == serviceDumpMethodParameterTypes 3724 3725 private fun hasAcronyms(name: String): Boolean { 3726 // Require 3 capitals, or 2 if it's at the end of a word. 3727 val result = acronymPattern2.find(name) ?: return false 3728 return result.range.first == name.length - 2 || acronymPattern3.find(name) != null 3729 } 3730 3731 private fun getFirstAcronym(name: String): String? { 3732 // Require 3 capitals, or 2 if it's at the end of a word. 3733 val result = acronymPattern2.find(name) ?: return null 3734 if (result.range.first == name.length - 2) { 3735 return name.substring(name.length - 2) 3736 } 3737 val result2 = acronymPattern3.find(name) 3738 return if (result2 != null) { 3739 name.substring(result2.range.first, result2.range.last + 1) 3740 } else { 3741 null 3742 } 3743 } 3744 3745 /** for something like "HTMLWriter", returns "HtmlWriter" */ 3746 private fun decapitalizeAcronyms(name: String): String { 3747 var s = name 3748 3749 if (s.none { it.isLowerCase() }) { 3750 // The entire thing is capitalized. If so, just perform 3751 // normal capitalization, but try dropping _'s. 3752 return SdkVersionInfo.underlinesToCamelCase(s.toLowerCase(Locale.US)).capitalize() 3753 } 3754 3755 while (true) { 3756 val acronym = getFirstAcronym(s) ?: return s 3757 val index = s.indexOf(acronym) 3758 if (index == -1) { 3759 return s 3760 } 3761 // The last character, if not the end of the string, is probably the beginning of the 3762 // next word so capitalize it 3763 s = if (index == s.length - acronym.length) { 3764 // acronym at the end of the word word 3765 val decapitalized = acronym[0] + acronym.substring(1).toLowerCase(Locale.US) 3766 s.replace(acronym, decapitalized) 3767 } else { 3768 val replacement = acronym[0] + acronym.substring( 3769 1, 3770 acronym.length - 1 3771 ).toLowerCase(Locale.US) + acronym[acronym.length - 1] 3772 s.replace(acronym, replacement) 3773 } 3774 } 3775 } 3776 3777 fun check(codebase: Codebase, oldCodebase: Codebase?, reporter: Reporter) { 3778 ApiLint(codebase, oldCodebase, reporter).check() 3779 } 3780 } 3781 } 3782 3783 internal const val DefaultLintErrorMessage = """ 3784 ************************************************************ 3785 Your API changes are triggering API Lint warnings or errors. 3786 To make these errors go away, fix the code according to the 3787 error and/or warning messages above. 3788 3789 If it's not possible to do so, there are two workarounds: 3790 3791 1. Suppress the issues with @Suppress("<id>") / @SuppressWarnings("<id>") 3792 2. Update the baseline passed into metalava 3793 ************************************************************ 3794 """