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