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