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