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