1 /*
<lambda>null2 * Copyright (C) 2017 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.tools.metalava.apilevels.internalDesc
20 import com.android.tools.metalava.manifest.Manifest
21 import com.android.tools.metalava.manifest.emptyManifest
22 import com.android.tools.metalava.model.ANDROIDX_REQUIRES_PERMISSION
23 import com.android.tools.metalava.model.ANDROID_ANNOTATION_PREFIX
24 import com.android.tools.metalava.model.ANDROID_DEPRECATED_FOR_SDK
25 import com.android.tools.metalava.model.ANDROID_SYSTEM_API
26 import com.android.tools.metalava.model.ANNOTATION_ATTR_VALUE
27 import com.android.tools.metalava.model.AnnotationAttributeValue
28 import com.android.tools.metalava.model.AnnotationItem
29 import com.android.tools.metalava.model.BaseItemVisitor
30 import com.android.tools.metalava.model.BaseTypeVisitor
31 import com.android.tools.metalava.model.CallableItem
32 import com.android.tools.metalava.model.ClassItem
33 import com.android.tools.metalava.model.ClassOrigin
34 import com.android.tools.metalava.model.ClassTypeItem
35 import com.android.tools.metalava.model.Codebase
36 import com.android.tools.metalava.model.FieldItem
37 import com.android.tools.metalava.model.FilterPredicate
38 import com.android.tools.metalava.model.Item
39 import com.android.tools.metalava.model.JAVA_LANG_DEPRECATED
40 import com.android.tools.metalava.model.MethodItem
41 import com.android.tools.metalava.model.PackageList
42 import com.android.tools.metalava.model.ParameterItem
43 import com.android.tools.metalava.model.PropertyItem
44 import com.android.tools.metalava.model.SelectableItem
45 import com.android.tools.metalava.model.TypeItem
46 import com.android.tools.metalava.model.TypeParameterList
47 import com.android.tools.metalava.model.VariableTypeItem
48 import com.android.tools.metalava.model.annotation.AnnotationFilter
49 import com.android.tools.metalava.model.source.SourceParser
50 import com.android.tools.metalava.model.visitors.ApiPredicate
51 import com.android.tools.metalava.model.visitors.ApiVisitor
52 import com.android.tools.metalava.reporter.Issues
53 import com.android.tools.metalava.reporter.Reporter
54 import java.io.File
55 import java.util.Locale
56
57 /**
58 * The [ApiAnalyzer] is responsible for walking over the various classes and members and compute
59 * visibility etc. of the APIs
60 */
61 class ApiAnalyzer(
62 private val sourceParser: SourceParser,
63 /** The code to analyze */
64 private val codebase: Codebase,
65 private val reporter: Reporter,
66 private val config: Config = Config(),
67 ) {
68
69 data class Config(
70 val manifest: Manifest = emptyManifest,
71
72 /** Packages to exclude/hide */
73 val hidePackages: List<String> = emptyList(),
74
75 /**
76 * Packages that we should skip generating even if not hidden; typically only used by tests
77 */
78 val skipEmitPackages: List<String> = emptyList(),
79
80 /**
81 * External annotation files that contain non-inclusion annotations which will appear in the
82 * generated API.
83 *
84 * These will be merged into the codebase.
85 */
86 val mergeQualifierAnnotations: List<File> = emptyList(),
87
88 /**
89 * External annotation files that contain annotations which affect inclusion of items in the
90 * API.
91 *
92 * These will be merged into the codebase.
93 */
94 val mergeInclusionAnnotations: List<File> = emptyList(),
95
96 /** The filter for all the show annotations. */
97 val allShowAnnotations: AnnotationFilter = AnnotationFilter.emptyFilter(),
98
99 /** Configuration for any [ApiPredicate] instances this needs to create. */
100 val apiPredicateConfig: ApiPredicate.Config = ApiPredicate.Config()
101 )
102
103 /** All packages in the API */
104 private val packages: PackageList = codebase.getPackages()
105
106 fun computeApi() {
107 if (codebase.trustedApi()) {
108 // The codebase is already an API; no consistency checks to be performed
109 return
110 }
111
112 skipEmitPackages()
113 // Suppress kotlin file facade classes with no public api
114 hideEmptyKotlinFileFacadeClasses()
115
116 // Propagate visibility down into individual elements -- if a class is hidden,
117 // then the methods and fields are hidden etc
118 propagateHiddenRemovedAndDocOnly()
119 }
120
121 fun generateInheritedStubs(filterEmit: FilterPredicate, filterReference: FilterPredicate) {
122 // When analyzing libraries we may discover some new classes during traversal; these aren't
123 // part of the API but may be super classes or interfaces; these will then be added into the
124 // package class lists, which could trigger a concurrent modification, so create a snapshot
125 // of the class list and iterate over it:
126 val allClasses = packages.allClasses().toList()
127
128 val visited = mutableSetOf<ClassItem>()
129 allClasses.forEach { generateInheritedStubs(it, filterEmit, filterReference, visited) }
130 }
131
132 private fun generateInheritedStubs(
133 cls: ClassItem,
134 filterEmit: FilterPredicate,
135 filterReference: FilterPredicate,
136 visited: MutableSet<ClassItem>,
137 ) {
138 // If it is not a class, i.e. an interface, etc., then return.
139 if (!cls.isClass()) return
140
141 // If already visited this class then ignore it. Otherwise, remember that this was visited.
142 if (cls in visited) return
143 visited += cls
144
145 // If it has no super class then ignore it.
146 val superClass = cls.superClass() ?: return
147
148 // If the class is not going to be emitted then do not inherit any methods into it.
149 if (!filterEmit.test(cls)) return
150
151 // Make sure that the super class has inherited the stubs and interfaces.
152 generateInheritedStubs(superClass, filterEmit, filterReference, visited)
153
154 val allSuperClasses = cls.allSuperClasses()
155 val hiddenSuperClasses =
156 allSuperClasses.filter { !filterReference.test(it) && !it.isJavaLangObject() }
157
158 if (hiddenSuperClasses.none()) { // not missing any implementation methods
159 return
160 }
161
162 addInheritedStubsFrom(cls, hiddenSuperClasses, allSuperClasses, filterEmit, filterReference)
163 addInheritedInterfacesFrom(cls, hiddenSuperClasses, filterReference)
164 }
165
166 private fun addInheritedInterfacesFrom(
167 cls: ClassItem,
168 hiddenSuperClasses: Sequence<ClassItem>,
169 filterReference: FilterPredicate
170 ) {
171 var interfaceTypes: MutableList<ClassTypeItem>? = null
172 var interfaceTypeClasses: MutableList<ClassItem>? = null
173 for (hiddenSuperClass in hiddenSuperClasses) {
174 for (hiddenInterface in hiddenSuperClass.interfaceTypes()) {
175 val hiddenInterfaceClass = hiddenInterface.asClass()
176 if (filterReference.test(hiddenInterfaceClass ?: continue)) {
177 if (interfaceTypes == null) {
178 interfaceTypes = cls.interfaceTypes().toMutableList()
179 interfaceTypeClasses =
180 interfaceTypes.mapNotNull { it.asClass() }.toMutableList()
181 if (cls.isInterface()) {
182 cls.superClass()?.let { interfaceTypeClasses.add(it) }
183 }
184 cls.setInterfaceTypes(interfaceTypes)
185 }
186 if (interfaceTypeClasses!!.any { it == hiddenInterfaceClass }) {
187 continue
188 }
189
190 interfaceTypeClasses.add(hiddenInterfaceClass)
191
192 if (hiddenInterfaceClass.hasTypeVariables()) {
193 val mapping = cls.mapTypeVariables(hiddenSuperClass)
194 if (mapping.isNotEmpty()) {
195 val mappedType = hiddenInterface.convertType(mapping)
196 interfaceTypes.add(mappedType)
197 continue
198 }
199 }
200
201 interfaceTypes.add(hiddenInterface)
202 }
203 }
204 }
205 }
206
207 private fun addInheritedStubsFrom(
208 cls: ClassItem,
209 hiddenSuperClasses: Sequence<ClassItem>,
210 superClasses: Sequence<ClassItem>,
211 filterEmit: FilterPredicate,
212 filterReference: FilterPredicate
213 ) {
214 // Also generate stubs for any methods we would have inherited from abstract parents
215 // All methods from super classes that (1) aren't overridden in this class already, and
216 // (2) are overriding some method that is in a public interface accessible from this class.
217 val interfaces: Set<TypeItem> = cls.allInterfaceTypes(filterReference).toSet()
218
219 // Note that we can't just call method.superMethods() to and see whether any of their
220 // containing classes are among our target APIs because it's possible that the super class
221 // doesn't actually implement the interface, but still provides a matching signature for the
222 // interface. Instead, we'll look through all of our interface methods and look for
223 // potential overrides.
224 val inheritableMethods = MethodItemSet()
225 for (interfaceType in interfaces) {
226 val interfaceClass = interfaceType.asClass() ?: continue
227 for (method in interfaceClass.methods()) {
228 inheritableMethods.add(method)
229 }
230 }
231
232 // Also add in any abstract methods from public super classes
233 val publicSuperClasses =
234 superClasses.filter { filterEmit.test(it) && !it.isJavaLangObject() }
235 for (superClass in publicSuperClasses) {
236 for (method in superClass.methods()) {
237 if (!method.modifiers.isAbstract() || !method.modifiers.isPublicOrProtected()) {
238 continue
239 }
240 inheritableMethods.add(method)
241 }
242 }
243
244 // Also add in any concrete public methods from hidden super classes
245 for (superClass in hiddenSuperClasses) {
246 // Determine if there is a non-hidden class between the superClass and this class.
247 // If non-hidden classes are found, don't include the methods for this hiddenSuperClass,
248 // as it will already have been included in a previous super class
249 val includeHiddenSuperClassMethods =
250 !cls.allSuperClasses()
251 // Search from this class up to, but not including the superClass.
252 .takeWhile { currentClass -> currentClass != superClass }
253 // Find any class that is not hidden.
254 .any { currentClass -> !hiddenSuperClasses.contains(currentClass) }
255
256 if (!includeHiddenSuperClassMethods) {
257 continue
258 }
259
260 for (method in superClass.methods()) {
261 if (method.modifiers.isAbstract() || !method.modifiers.isPublic()) {
262 continue
263 }
264
265 if (method.hasHiddenType(filterReference)) {
266 continue
267 }
268
269 inheritableMethods.add(method)
270 }
271 }
272
273 // Find all methods that are inherited from these classes into our class (making sure that
274 // we don't have duplicates, e.g. a method defined by one inherited class and then
275 // overridden by another closer one). map from method name to super methods overriding our
276 // interfaces
277 val inheritedMethods = MethodItemSet()
278
279 for (superClass in hiddenSuperClasses) {
280 for (method in superClass.methods()) {
281 val modifiers = method.modifiers
282 if (!modifiers.isPrivate() && !modifiers.isAbstract()) {
283 if (inheritableMethods.containsMatchingMethod(method)) {
284 inheritedMethods.add(method)
285 }
286 }
287 }
288 }
289
290 // Remove any methods that are overriding any of our existing methods
291 for (method in cls.methods()) {
292 inheritedMethods.removeMatchingMethods(method)
293 }
294
295 // Next remove any overrides among the remaining super methods (e.g. one method from a
296 // hidden parent is overriding another method from a more distant hidden parent).
297 inheritedMethods.values.forEach { methods ->
298 if (methods.size >= 2) {
299 for (candidate in ArrayList(methods)) {
300 for (superMethod in candidate.allSuperMethods()) {
301 methods.remove(superMethod)
302 }
303 }
304 }
305 }
306
307 // Add all the existing methods in the class to the set of existing methods.
308 val existingMethods = MethodItemSet()
309 for (method in cls.methods()) {
310 existingMethods.add(method)
311 }
312
313 // We're now left with concrete methods in hidden parents that are implementing methods in
314 // public interfaces that are listed in this class. Create stubs for them:
315 inheritedMethods.values.flatten().forEach {
316 // Copy the method from the hidden class that is not part of the API into the class that
317 // is part of the API.
318 val method = it.duplicate(cls)
319 /* Insert comment marker: This is useful for debugging purposes but doesn't
320 belong in the stub
321 method.documentation = "// Inlined stub from hidden parent class ${it.containingClass().qualifiedName()}\n" +
322 method.documentation
323 */
324
325 // If we already have an override of this method, do not add it to the methods list
326 if (existingMethods.containsMatchingMethod(method)) {
327 return@forEach
328 }
329
330 val runtimeDesc = it.internalDesc()
331 val stubDesc = method.internalDesc()
332 if (filterEmit.test(method) && runtimeDesc != stubDesc) {
333 // This is problematic primarily for the platform where we use stubs, and the
334 // generated method in the android.jar won't actually exist at runtime.
335 // While we don't use stubs in AndroidX, this can still cause compat issues because
336 // the current.txt (which will show the equivalent of stubDesc) won't actually match
337 // the ABI of the library (because call sites will reference runtimeDesc).
338 reporter.report(
339 Issues.INHERIT_CHANGES_SIGNATURE,
340 it,
341 "Explicitly override $it in $cls, or hide it in ${it.containingClass()};" +
342 " it cannot be implicitly inherited as API from the hidden super class" +
343 " because that would change its erased signature from $runtimeDesc to" +
344 " $stubDesc, and cause failures at runtime.",
345 )
346 }
347
348 cls.addMethod(method)
349
350 // Make sure that the same method is not added from multiple super classes.
351 existingMethods.add(method)
352 }
353 }
354
355 /** Apply package filters listed in [Options.skipEmitPackages] */
356 private fun skipEmitPackages() {
357 for (pkgName in config.skipEmitPackages) {
358 val pkg = codebase.findPackage(pkgName) ?: continue
359 pkg.emit = false
360 }
361 }
362
363 /** If a file facade class has no public members, don't add it to the api */
364 private fun hideEmptyKotlinFileFacadeClasses() {
365 codebase.getPackages().allClasses().forEach { cls ->
366 if (
367 cls.isFileFacade() &&
368 // a facade class needs to be emitted if it has any top-level fun/prop to emit
369 cls.members().none { member ->
370 // a member needs to be emitted if
371 // 1) it doesn't have a hide annotation;
372 // 2) it is either public or has a show annotation;
373 // 3) it is not `expect`
374 !member.hasHideAnnotation() &&
375 (member.isPublic || member.hasShowAnnotation()) &&
376 !member.modifiers.isExpect()
377 }
378 ) {
379 cls.emit = false
380 }
381 }
382 }
383
384 /**
385 * Merge in external qualifier annotations (i.e. ones intended to be included in the API written
386 * from all configured sources).
387 */
388 fun mergeExternalQualifierAnnotations() {
389 val mergeQualifierAnnotations = config.mergeQualifierAnnotations
390 if (mergeQualifierAnnotations.isNotEmpty()) {
391 AnnotationsMerger(sourceParser, codebase, reporter)
392 .mergeQualifierAnnotationsFromFiles(mergeQualifierAnnotations)
393 }
394 }
395
396 /** Merge in external show/hide annotations from all configured sources */
397 fun mergeExternalInclusionAnnotations() {
398 val mergeInclusionAnnotations = config.mergeInclusionAnnotations
399 if (mergeInclusionAnnotations.isNotEmpty()) {
400 AnnotationsMerger(sourceParser, codebase, reporter)
401 .mergeInclusionAnnotationsFromFiles(mergeInclusionAnnotations)
402 }
403 }
404
405 /**
406 * Propagate the hidden flag down into individual elements -- if a class is hidden, then the
407 * methods and fields are hidden etc
408 */
409 private fun propagateHiddenRemovedAndDocOnly() {
410 // Create a visitor to propagate hidden and docOnly from the containing package onto the top
411 // level classes and then propagate them, and removed status, down onto the nested classes
412 // and members.
413 val visitor =
414 object :
415 BaseItemVisitor(
416 preserveClassNesting = true,
417 // Only SelectableItems can have variantSelectors.
418 visitParameterItems = false,
419 ) {
420 override fun visitSelectableItem(item: SelectableItem) {
421 item.variantSelectors.inheritInto()
422 }
423 }
424
425 codebase.accept(visitor)
426 }
427
428 private fun checkSystemPermissions(method: MethodItem) {
429 val annotation = method.modifiers.findAnnotation(ANDROIDX_REQUIRES_PERMISSION)
430 var hasAnnotation = false
431
432 if (annotation != null) {
433 hasAnnotation = true
434 for (attribute in annotation.attributes) {
435 var values: List<AnnotationAttributeValue>? = null
436 var any = false
437 when (attribute.name) {
438 "value",
439 "allOf" -> {
440 values = attribute.leafValues()
441 }
442 "anyOf" -> {
443 any = true
444 values = attribute.leafValues()
445 }
446 }
447
448 values ?: continue
449
450 val system = ArrayList<String>()
451 val nonSystem = ArrayList<String>()
452 val missing = ArrayList<String>()
453 for (value in values) {
454 val perm = (value.value() ?: value.toSource()).toString()
455 val level = config.manifest.getPermissionLevel(perm)
456 if (level == null) {
457 if (any) {
458 missing.add(perm)
459 continue
460 }
461
462 reporter.report(
463 Issues.REQUIRES_PERMISSION,
464 method,
465 "Permission '$perm' is not defined by manifest ${config.manifest}."
466 )
467 continue
468 }
469 if (
470 level.contains("normal") ||
471 level.contains("dangerous") ||
472 level.contains("ephemeral")
473 ) {
474 nonSystem.add(perm)
475 } else {
476 system.add(perm)
477 }
478 }
479 if (any && missing.size == values.size) {
480 reporter.report(
481 Issues.REQUIRES_PERMISSION,
482 method,
483 "None of the permissions ${missing.joinToString()} are defined by manifest " +
484 "${config.manifest}."
485 )
486 }
487
488 if (system.isEmpty() && nonSystem.isEmpty()) {
489 hasAnnotation = false
490 } else if (any && nonSystem.isNotEmpty() || !any && system.isEmpty()) {
491 reporter.report(
492 Issues.REQUIRES_PERMISSION,
493 method,
494 "Method '" +
495 method.name() +
496 "' must be protected with a system permission; it currently" +
497 " allows non-system callers holding " +
498 nonSystem.toString()
499 )
500 }
501 }
502 }
503
504 if (!hasAnnotation) {
505 reporter.report(
506 Issues.REQUIRES_PERMISSION,
507 method,
508 "Method '" + method.name() + "' must be protected with a system permission."
509 )
510 }
511 }
512
513 fun performChecks() {
514 if (codebase.trustedApi()) {
515 // The codebase is already an API; no consistency checks to be performed
516 return
517 }
518
519 val checkSystemApi =
520 !reporter.isSuppressed(Issues.REQUIRES_PERMISSION) &&
521 config.allShowAnnotations.matches(ANDROID_SYSTEM_API) &&
522 !config.manifest.isEmpty()
523 val checkHiddenShowAnnotations =
524 !reporter.isSuppressed(Issues.UNHIDDEN_SYSTEM_API) &&
525 config.allShowAnnotations.isNotEmpty()
526
527 codebase.accept(
528 object :
529 ApiVisitor(
530 apiPredicateConfig = @Suppress("DEPRECATION") options.apiPredicateConfig,
531 ) {
532 override fun visitParameter(parameter: ParameterItem) {
533 checkTypeReferencesHidden(parameter, parameter.type())
534 }
535
536 /**
537 * Visit all [SelectableItem]s, i.e. all [Item]s apart from [ParameterItem]s.
538 *
539 * None of the checks in this apply to [ParameterItem]. The deprecation checks do
540 * not apply as there is no way to provide an `@deprecation` tag in Javadoc for
541 * parameters. The unhidden showability annotation check ('UnhiddemSystemApi`) does
542 * not apply as you cannot annotate a [ParameterItem] with a showability annotation.
543 */
544 override fun visitSelectableItem(item: SelectableItem) {
545 if (
546 item.originallyDeprecated &&
547 !item.documentationContainsDeprecated() &&
548 // Don't warn about this in Kotlin; the Kotlin deprecation annotation
549 // includes deprecation
550 // messages (unlike java.lang.Deprecated which has no attributes).
551 // Instead, these
552 // are added to the documentation by the [DocAnalyzer].
553 !item.isKotlin() &&
554 // @DeprecatedForSdk will show up as an alias for @Deprecated, but it's
555 // correct
556 // and expected to *not* combine this with @deprecated in the text;
557 // here,
558 // the text comes from an annotation attribute.
559 item.modifiers.isAnnotatedWith(JAVA_LANG_DEPRECATED)
560 ) {
561 reporter.report(
562 Issues.DEPRECATION_MISMATCH,
563 item,
564 "${item.toString().capitalize()}: @Deprecated annotation (present) and @deprecated doc tag (not present) do not match"
565 )
566 // TODO: Check opposite (doc tag but no annotation)
567 } else {
568 val deprecatedForSdk =
569 item.modifiers.findAnnotation(ANDROID_DEPRECATED_FOR_SDK)
570 if (deprecatedForSdk != null) {
571 if (item.documentation.hasTagSection("@deprecated")) {
572 reporter.report(
573 Issues.DEPRECATION_MISMATCH,
574 item,
575 "${item.toString().capitalize()}: Documentation contains `@deprecated` which implies this API is fully deprecated, not just @DeprecatedForSdk"
576 )
577 } else {
578 val value = deprecatedForSdk.findAttribute(ANNOTATION_ATTR_VALUE)
579 val message = value?.legacyValue?.value()?.toString() ?: ""
580 item.appendDocumentation(message, "@deprecated")
581 }
582 }
583 }
584
585 if (
586 checkHiddenShowAnnotations &&
587 item.hasShowAnnotation() &&
588 !item.originallyHidden &&
589 !item.showability.showNonRecursive()
590 ) {
591 item.modifiers
592 .annotations()
593 // Find the first show annotation. Just because item.hasShowAnnotation()
594 // is true does not mean that there must be one show annotation as a
595 // revert annotation could be treated as a show annotation on one item
596 // and a hide annotation on another but is neither a show or hide
597 // annotation.
598 .firstOrNull(AnnotationItem::isShowAnnotation)
599 // All show annotations must have a non-null string otherwise they
600 // would not have been matched.
601 ?.qualifiedName
602 ?.removePrefix(ANDROID_ANNOTATION_PREFIX)
603 ?.let { annotationName ->
604 reporter.report(
605 Issues.UNHIDDEN_SYSTEM_API,
606 item,
607 "@$annotationName APIs must also be marked @hide: ${item.describe()}"
608 )
609 }
610 }
611 }
612
613 override fun visitClass(cls: ClassItem) {
614 if (checkSystemApi) {
615 // Look for Android @SystemApi exposed outside the normal SDK; we require
616 // that they're protected with a system permission.
617 // Also flag @SystemApi apis not annotated with @hide.
618
619 // This class is a system service if it's annotated with @SystemService,
620 // or if it's android.content.pm.PackageManager
621 if (
622 cls.modifiers.isAnnotatedWith("android.annotation.SystemService") ||
623 cls.qualifiedName() == "android.content.pm.PackageManager"
624 ) {
625 // Check permissions on system services
626 for (method in cls.filteredMethods(filterEmit)) {
627 checkSystemPermissions(method)
628 }
629 }
630 }
631 }
632
633 override fun visitField(field: FieldItem) {
634 checkTypeReferencesHidden(field, field.type())
635 }
636
637 override fun visitProperty(property: PropertyItem) {
638 checkTypeReferencesHidden(property, property.type())
639 }
640
641 override fun visitMethod(method: MethodItem) {
642 checkTypeReferencesHidden(
643 method,
644 method.returnType()
645 ) // returnType is nullable only for constructors
646 }
647
648 /** Check that the type doesn't refer to any hidden classes. */
649 private fun checkTypeReferencesHidden(item: Item, type: TypeItem) {
650 type.accept(
651 object : BaseTypeVisitor() {
652 override fun visitClassType(classType: ClassTypeItem) {
653 val cls = classType.asClass() ?: return
654 if (
655 !filterReference.test(cls) &&
656 cls.origin != ClassOrigin.CLASS_PATH
657 ) {
658 reporter.report(
659 Issues.HIDDEN_TYPE_PARAMETER,
660 item,
661 "${item.toString().capitalize()} references hidden type $classType."
662 )
663 }
664 }
665 }
666 )
667 }
668 }
669 )
670 }
671
672 // TODO: Switch to visitor iteration
673 fun handleStripping() {
674 val notStrippable = HashSet<ClassItem>(5000)
675
676 val filter = ApiPredicate(config = config.apiPredicateConfig.copy(ignoreShown = true))
677
678 // If a class is public or protected, not hidden, not imported and marked as included,
679 // then we can't strip it
680 val allTopLevelClasses = codebase.getPackages().allTopLevelClasses().toList()
681 allTopLevelClasses
682 .filter { it.isApiCandidate() && it.emit && !it.hidden() }
683 .forEach { cantStripThis(it, filter, notStrippable, it, "self") }
684
685 // complain about anything that looks includeable but is not supposed to
686 // be written, e.g. hidden things
687 for (cl in notStrippable) {
688 if (!cl.isHiddenOrRemoved()) {
689 val publiclyConstructable =
690 !cl.modifiers.isSealed() && cl.constructors().any { it.isApiCandidate() }
691 for (m in cl.methods()) {
692 if (!m.isApiCandidate()) {
693 if (publiclyConstructable && m.modifiers.isAbstract()) {
694 reporter.report(
695 Issues.HIDDEN_ABSTRACT_METHOD,
696 m,
697 "${m.name()} cannot be hidden and abstract when " +
698 "${cl.simpleName()} has a visible constructor, in case a " +
699 "third-party attempts to subclass it."
700 )
701 }
702 continue
703 }
704 if (m.isHiddenOrRemoved()) {
705 reporter.report(
706 Issues.UNAVAILABLE_SYMBOL,
707 m,
708 "Reference to unavailable method " + m.name()
709 )
710 } else if (m.originallyDeprecated) {
711 // don't bother reporting deprecated methods unless they are public and
712 // explicitly marked as deprecated.
713 reporter.report(
714 Issues.DEPRECATED,
715 m,
716 "Method " + cl.qualifiedName() + "." + m.name() + " is deprecated"
717 )
718 }
719
720 checkTypeReferencesHiddenOrDeprecated(m.returnType(), m, cl, "Return type")
721 for (p in m.parameters()) {
722 checkTypeReferencesHiddenOrDeprecated(p.type(), m, cl, "Parameter")
723 }
724 }
725
726 if (!cl.effectivelyDeprecated) {
727 val s = cl.superClass()
728 if (s?.effectivelyDeprecated == true) {
729 reporter.report(
730 Issues.EXTENDS_DEPRECATED,
731 cl,
732 "Extending deprecated super class $s from ${cl.qualifiedName()}: this class should also be deprecated"
733 )
734 }
735
736 for (t in cl.interfaceTypes()) {
737 if (t.asClass()?.effectivelyDeprecated == true) {
738 reporter.report(
739 Issues.EXTENDS_DEPRECATED,
740 cl,
741 "Implementing interface of deprecated type $t in ${cl.qualifiedName()}: this class should also be deprecated"
742 )
743 }
744 }
745 }
746 } else if (cl.originallyDeprecated) {
747 // not hidden, but deprecated
748 reporter.report(Issues.DEPRECATED, cl, "Class ${cl.qualifiedName()} is deprecated")
749 }
750 }
751 }
752
753 private fun cantStripThis(
754 cl: ClassItem,
755 filter: FilterPredicate,
756 notStrippable: MutableSet<ClassItem>,
757 from: Item,
758 usage: String
759 ) {
760 if (cl.origin == ClassOrigin.CLASS_PATH) {
761 return
762 }
763
764 if (cl.isHiddenOrRemoved() || cl.isPackagePrivate && !cl.isApiCandidate()) {
765 reporter.report(
766 Issues.REFERENCES_HIDDEN,
767 from,
768 "Class ${cl.qualifiedName()} is ${if (cl.isHiddenOrRemoved()) "hidden" else "not public"} but was referenced ($usage) from public ${from.describe(
769 false
770 )}"
771 )
772 }
773
774 if (!notStrippable.add(cl)) {
775 // slight optimization: if it already contains cl, it already contains
776 // all of cl's parents
777 return
778 }
779
780 // can't strip any public fields or their generics
781 for (field in cl.fields()) {
782 if (!filter.test(field)) {
783 continue
784 }
785 cantStripThis(field.type(), field, filter, notStrippable, "in field type")
786 }
787 // can't strip any of the type's generics
788 cantStripThis(cl.typeParameterList, filter, notStrippable, cl)
789 // can't strip any of the annotation elements
790 // cantStripThis(cl.annotationElements(), notStrippable);
791 // take care of methods
792 cantStripThis(cl.methods(), filter, notStrippable)
793 cantStripThis(cl.constructors(), filter, notStrippable)
794 // blow the outer class open if this is an inner class
795 val containingClass = cl.containingClass()
796 if (containingClass != null) {
797 cantStripThis(containingClass, filter, notStrippable, cl, "as containing class")
798 }
799 // all visible inner classes will be included in stubs
800 cl.nestedClasses()
801 .filter { it.isApiCandidate() }
802 .forEach { cantStripThis(it, filter, notStrippable, cl, "as nested class") }
803 // blow open super class and interfaces
804 // TODO: Consider using val superClass = cl.filteredSuperclass(filter)
805 val superItems = cl.allInterfaces().toMutableSet()
806 cl.superClass()?.let { superClass -> superItems.add(superClass) }
807
808 for (superItem in superItems) {
809 // allInterfaces includes cl itself if cl is an interface
810 if (superItem.isHiddenOrRemoved() && superItem != cl) {
811 // cl is a public class declared as extending a hidden superclass.
812 // this is not a desired practice, but it's happened, so we deal
813 // with it by finding the first super class which passes checkLevel for purposes of
814 // generating the doc & stub information, and proceeding normally.
815 if (superItem.origin != ClassOrigin.CLASS_PATH) {
816 reporter.report(
817 Issues.HIDDEN_SUPERCLASS,
818 cl,
819 "Public class " +
820 cl.qualifiedName() +
821 " stripped of unavailable superclass " +
822 superItem.qualifiedName()
823 )
824 }
825 } else {
826 // doclava would also mark the package private super classes as unhidden, but that's
827 // not
828 // right (this was just done for its stub handling)
829 // cantStripThis(superClass, filter, notStrippable, stubImportPackages, cl, "as
830 // super class")
831
832 if (superItem.isPrivate && superItem.origin != ClassOrigin.CLASS_PATH) {
833 reporter.report(
834 Issues.PRIVATE_SUPERCLASS,
835 cl,
836 "Public class " +
837 cl.qualifiedName() +
838 " extends private class " +
839 superItem.qualifiedName()
840 )
841 }
842 }
843 }
844 }
845
846 private fun cantStripThis(
847 callables: List<CallableItem>,
848 filter: FilterPredicate,
849 notStrippable: MutableSet<ClassItem>,
850 ) {
851 // for each callable, blow open the parameters, throws and return types. also blow open
852 // their generics
853 for (callable in callables) {
854 if (!filter.test(callable)) {
855 continue
856 }
857 cantStripThis(callable.typeParameterList, filter, notStrippable, callable)
858 for (parameter in callable.parameters()) {
859 cantStripThis(
860 parameter.type(),
861 parameter,
862 filter,
863 notStrippable,
864 "in parameter type"
865 )
866 }
867 for (thrown in callable.throwsTypes()) {
868 if (thrown is VariableTypeItem) continue
869 val classItem = thrown.erasedClass ?: continue
870 cantStripThis(classItem, filter, notStrippable, callable, "as exception")
871 }
872 cantStripThis(callable.returnType(), callable, filter, notStrippable, "in return type")
873 }
874 }
875
876 private fun cantStripThis(
877 typeParameterList: TypeParameterList,
878 filter: FilterPredicate,
879 notStrippable: MutableSet<ClassItem>,
880 context: Item
881 ) {
882 for (typeParameter in typeParameterList) {
883 for (bound in typeParameter.typeBounds()) {
884 cantStripThis(bound, context, filter, notStrippable, "as type parameter")
885 }
886 }
887 }
888
889 private fun cantStripThis(
890 type: TypeItem,
891 context: Item,
892 filter: FilterPredicate,
893 notStrippable: MutableSet<ClassItem>,
894 usage: String,
895 ) {
896 type.accept(
897 object : BaseTypeVisitor() {
898 override fun visitClassType(classType: ClassTypeItem) {
899 val asClass = classType.asClass() ?: return
900 cantStripThis(asClass, filter, notStrippable, context, usage)
901 }
902 }
903 )
904 }
905
906 /**
907 * Checks if the type (method parameter or return type) references a hidden or deprecated class.
908 */
909 private fun checkTypeReferencesHiddenOrDeprecated(
910 type: TypeItem,
911 containingMethod: MethodItem,
912 containingClass: ClassItem,
913 usage: String
914 ) {
915 if (!containingMethod.effectivelyDeprecated) {
916 type.accept(
917 object : BaseTypeVisitor() {
918 override fun visitClassType(classType: ClassTypeItem) {
919 if (classType.asClass()?.effectivelyDeprecated == true) {
920 reporter.report(
921 Issues.REFERENCES_DEPRECATED,
922 containingMethod,
923 "$usage references deprecated type $classType in ${containingClass.qualifiedName()}.${containingMethod.name()}(): this method should also be deprecated"
924 )
925 }
926 }
927 }
928 )
929 }
930
931 val hiddenClasses = findHiddenClasses(type)
932 val typeClassName = (type as? ClassTypeItem)?.qualifiedName
933 for (hiddenClass in hiddenClasses) {
934 if (hiddenClass.origin == ClassOrigin.CLASS_PATH) continue
935 if (hiddenClass.qualifiedName() == typeClassName) {
936 // The type itself is hidden
937 reporter.report(
938 Issues.UNAVAILABLE_SYMBOL,
939 containingMethod,
940 "$usage of unavailable type $type in ${containingClass.qualifiedName()}.${containingMethod.name()}()"
941 )
942 } else {
943 // The type contains a hidden type
944 reporter.report(
945 Issues.HIDDEN_TYPE_PARAMETER,
946 containingMethod,
947 "$usage uses type parameter of unavailable type $type in ${containingClass.qualifiedName()}.${containingMethod.name()}()"
948 )
949 }
950 }
951 }
952
953 /**
954 * Find references to hidden classes.
955 *
956 * This finds hidden classes that are used by public parts of the API in order to ensure the API
957 * is self-consistent and does not reference classes that are not included in the stubs. Any
958 * such references cause an error to be reported.
959 *
960 * A reference to an imported class is not treated as an error, even though imported classes are
961 * hidden from the stub generation. That is because imported classes are, by definition,
962 * excluded from the set of classes for which stubs are required.
963 *
964 * @param ti the type information to examine for references to hidden classes.
965 * @return all references to hidden classes referenced by the type
966 */
967 private fun findHiddenClasses(ti: TypeItem): Set<ClassItem> {
968 val hiddenClasses = mutableSetOf<ClassItem>()
969 ti.accept(
970 object : BaseTypeVisitor() {
971 override fun visitClassType(classType: ClassTypeItem) {
972 val asClass = classType.asClass() ?: return
973 if (asClass.isHiddenOrRemoved()) {
974 hiddenClasses.add(asClass)
975 }
976 }
977 }
978 )
979 return hiddenClasses
980 }
981 }
982
Stringnull983 private fun String.capitalize(): String {
984 return this.replaceFirstChar {
985 if (it.isLowerCase()) {
986 it.titlecase(Locale.getDefault())
987 } else {
988 it.toString()
989 }
990 }
991 }
992
993 /** Returns true if this item is public or protected and so a candidate for inclusion in an API. */
SelectableItemnull994 private fun SelectableItem.isApiCandidate(): Boolean {
995 return !isHiddenOrRemoved() && (modifiers.isPublic() || modifiers.isProtected())
996 }
997
998 /**
999 * Whether documentation for the [Item] has the `@deprecated` tag -- for inherited methods, this
1000 * also looks at any inherited documentation.
1001 */
Itemnull1002 private fun Item.documentationContainsDeprecated(): Boolean {
1003 val text = documentation.text
1004 if (text.contains("@deprecated")) return true
1005 if (this is MethodItem && (text == "" || text.contains("@inheritDoc"))) {
1006 return superMethods().any { it.documentationContainsDeprecated() }
1007 }
1008 return false
1009 }
1010
1011 /**
1012 * A set of [MethodItem]s.
1013 *
1014 * This is implemented as a [MutableMap] from the [MethodItem.name] to the list of [MethodItem]s
1015 * with that name.
1016 */
1017 private typealias MethodItemSet = HashMap<String, MutableList<MethodItem>>
1018
1019 /**
1020 * Add a method to the set.
1021 *
1022 * This does not check to see if the [MethodItem] exists already so it is possible that it will
1023 * contain duplicate methods.
1024 */
addnull1025 private fun MethodItemSet.add(method: MethodItem) {
1026 val name = method.name()
1027 val list = computeIfAbsent(name) { mutableListOf() }
1028 list.add(method)
1029 }
1030
1031 /**
1032 * Check to see whether the set contains a method that matches [method] as determined by
1033 * [MethodItem.matches].
1034 */
containsMatchingMethodnull1035 private fun MethodItemSet.containsMatchingMethod(method: MethodItem): Boolean {
1036 val name = method.name()
1037 val list = this[name] ?: return false
1038 for (existing in list) {
1039 if (method.matches(existing)) {
1040 return true
1041 }
1042 }
1043 return false
1044 }
1045
1046 /** Remove any method that matches [method] as determined by [MethodItem.matches]. */
MethodItemSetnull1047 private fun MethodItemSet.removeMatchingMethods(method: MethodItem) {
1048 val name = method.name()
1049 val list = this[name] ?: return
1050 val iterator = list.listIterator()
1051 while (iterator.hasNext()) {
1052 val existing = iterator.next()
1053 if (method.matches(existing)) {
1054 iterator.remove()
1055 }
1056 }
1057 }
1058