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