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