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