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