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