• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * 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.model
18 
19 import com.android.tools.metalava.reporter.BaselineKey
20 import com.android.tools.metalava.reporter.FileLocation
21 import com.android.tools.metalava.reporter.Reportable
22 import java.util.concurrent.atomic.AtomicInteger
23 
24 /**
25  * Represents a code element such as a package, a class, a method, a field, a parameter.
26  *
27  * This extra abstraction on top of PSI allows us to more model the API (and customize visibility,
28  * which cannot always be done by looking at a particular piece of code and examining visibility
29  * and @hide/@removed annotations: sometimes package private APIs are unhidden by being used in
30  * public APIs for example.
31  *
32  * The abstraction also lets us back the model by an alternative implementation read from signature
33  * files, to do compatibility checks.
34  */
35 interface Item : Reportable {
36     val codebase: Codebase
37 
38     /** Return the modifiers of this class */
39     @MetalavaApi val modifiers: ModifierList
40 
parentnull41     fun parent(): SelectableItem?
42 
43     /**
44      * Recursive check to see if compatibility checks should be suppressed for this item or any of
45      * its parents (containing class, containing package).
46      */
47     fun isCompatibilitySuppressed(): Boolean {
48         return hasSuppressCompatibilityMetaAnnotation() ||
49             parent()?.isCompatibilitySuppressed() ?: false
50     }
51 
52     /** True if this item has been marked deprecated. */
53     val originallyDeprecated: Boolean
54 
55     /**
56      * True if this item has been marked as deprecated or is a descendant of a non-package item that
57      * has been marked as deprecated.
58      */
59     val effectivelyDeprecated: Boolean
60 
61     /** Visits this element using the given [visitor] */
acceptnull62     fun accept(visitor: ItemVisitor)
63 
64     /**
65      * Mutate the [modifiers] list.
66      *
67      * Provides a [MutableModifierList] of the [modifiers] that can be modified by [mutator]. Once
68      * the mutator exits the [modifiers] will be updated. The [MutableModifierList] must not be
69      * accessed from outside [mutator].
70      */
71     fun mutateModifiers(mutator: MutableModifierList.() -> Unit)
72 
73     /**
74      * The javadoc/KDoc comment for this code element, if any. This is the original content of the
75      * documentation, including lexical tokens to begin, continue and end the comment (such as /+*).
76      * See [ItemDocumentation.fullyQualifiedDocumentation] to look up the documentation with fully
77      * qualified references to classes.
78      */
79     val documentation: ItemDocumentation
80 
81     /**
82      * A rank used for sorting. This allows signature files etc to sort similar items by a natural
83      * order, if non-zero. (Even though in signature files the elements are normally sorted first
84      * logically (constructors, then methods, then fields) and then alphabetically, this lets us
85      * preserve the source ordering for example for overloaded methods of the same name, where it's
86      * not clear that an alphabetical order (of each parameter?) would be preferable.)
87      */
88     val sortingRank: Int
89 
90     /**
91      * Add the given text to the documentation.
92      *
93      * If the [tagSection] is null, add the comment to the initial text block of the description.
94      *
95      * If it is "@return", add the comment to the return value.
96      *
97      * Otherwise, the [tagSection] is taken to be the parameter name, and the comment added as
98      * parameter documentation for the given parameter.
99      */
100     fun appendDocumentation(comment: String, tagSection: String? = null)
101 
102     val isPublic: Boolean
103     val isProtected: Boolean
104     val isInternal: Boolean
105     val isPackagePrivate: Boolean
106     val isPrivate: Boolean
107 
108     /** Calls [equalsToItem]. */
109     override fun equals(other: Any?): Boolean
110 
111     /** Calls [hashCodeForItem]. */
112     override fun hashCode(): Int
113 
114     /** Calls [toStringForItem]. */
115     override fun toString(): String
116 
117     /**
118      * Whether this [Item] is equal to [other].
119      *
120      * This is implemented instead of [equals] because interfaces are not allowed to implement
121      * [equals]. Implementations of this will implement [equals] by calling this.
122      */
123     fun equalsToItem(other: Any?): Boolean
124 
125     /**
126      * Hashcode for this [Item].
127      *
128      * This is implemented instead of [hashCode] because interfaces are not allowed to implement
129      * [hashCode]. Implementations of this will implement [hashCode] by calling this.
130      */
131     fun hashCodeForItem(): Int
132 
133     /** Provides a string representation of the item, suitable for use while debugging. */
134     fun toStringForItem(): String
135 
136     /**
137      * The language in which this was written, or [ItemLanguage.UNKNOWN] if not known, e.g. when
138      * created from a signature file.
139      */
140     val itemLanguage: ItemLanguage
141 
142     /**
143      * Is this element declared in Java (rather than Kotlin) ?
144      *
145      * See [itemLanguage].
146      */
147     fun isJava() = itemLanguage.isJava()
148 
149     /**
150      * Is this element declared in Kotlin (rather than Java) ?
151      *
152      * See [itemLanguage].
153      */
154     fun isKotlin() = itemLanguage.isKotlin()
155 
156     /**
157      * Returns true if this [Item]'s modifier list contains any suppress compatibility
158      * meta-annotations.
159      *
160      * Metalava will suppress compatibility checks for APIs which are within the scope of a
161      * "suppress compatibility" meta-annotation, but they may still be written to API files or stub
162      * JARs.
163      *
164      * "Suppress compatibility" meta-annotations allow Metalava to handle concepts like Jetpack
165      * experimental APIs, where developers can use the [RequiresOptIn] meta-annotation to mark
166      * feature sets with unstable APIs.
167      */
168     fun hasSuppressCompatibilityMetaAnnotation(): Boolean =
169         codebase.annotationManager.hasSuppressCompatibilityMetaAnnotations(modifiers)
170 
171     override val fileLocation: FileLocation
172         get() = FileLocation.UNKNOWN
173 
174     /**
175      * Produces a user visible description of this item, including a label such as "class" or
176      * "field"
177      */
178     fun describe(capitalize: Boolean = false) = describe(this, capitalize)
179 
180     /** Returns the package that contains this item. */
181     fun containingPackage(): PackageItem?
182 
183     /** Returns the class that contains this item. */
184     fun containingClass(): ClassItem?
185 
186     /**
187      * Returns the associated type, if any.
188      *
189      * i.e.
190      * * For a field, property or parameter, this is the type of the variable.
191      * * For a method, it's the return type.
192      * * For classes it's the declared class type, i.e. a class type using the type parameter types
193      *   as the type arguments.
194      * * For type parameters it's a [VariableTypeItem] reference the type parameter.
195      * * For packages and files, it's null.
196      * * For type aliases it's the underlying type for which the alias is an alternative name.
197      */
198     fun type(): TypeItem?
199 
200     /**
201      * Set the type of this.
202      *
203      * The [type] parameter must be of the same concrete type as returned by the [Item.type] method.
204      */
205     fun setType(type: TypeItem)
206 
207     /**
208      * Find the [Item] in [codebase] that corresponds to this item, or `null` if there is no such
209      * item.
210      *
211      * If [superMethods] is true and this is a [MethodItem] then the returned [MethodItem], if any,
212      * could be in a [ClassItem] that does not correspond to the [MethodItem.containingClass], it
213      * could be from a super class or super interface. e.g. if the [codebase] contains something
214      * like:
215      * ```
216      *     public class Super {
217      *         public void method() {...}
218      *     }
219      *     public class Foo extends Super {}
220      * ```
221      *
222      * And this is called on `Foo.method()` then:
223      * * if [superMethods] is false this will return `null`.
224      * * if [superMethods] is true and [duplicate] is false, then this will return `Super.method()`.
225      * * if both [superMethods] and [duplicate] are true then this will return a duplicate of
226      *   `Super.method()` that has been added to `Foo` so it will be essentially `Foo.method()`.
227      *
228      * @param codebase the [Codebase] to search for a corresponding item.
229      * @param superMethods if true and this is a [MethodItem] then this method will search for super
230      *   methods. If this is a [ParameterItem] then the value of this parameter will be passed to
231      *   the [findCorrespondingItemIn] call which is used to find the [MethodItem] corresponding to
232      *   the [ParameterItem.containingCallable].
233      * @param duplicate if true, and this is a [MemberItem] (or [ParameterItem]) then the returned
234      *   [Item], if any, will be in the [ClassItem] that corresponds to the [Item.containingClass].
235      *   This should be `true` if the returned [Item] is going to be compared to the original [Item]
236      *   as the [Item.containingClass] can affect that comparison, e.g. the meaning of certain
237      *   modifiers.
238      */
239     fun findCorrespondingItemIn(
240         codebase: Codebase,
241         superMethods: Boolean = false,
242         duplicate: Boolean = false,
243     ): Item?
244 
245     /**
246      * Get the set of suppressed issues for this [Item].
247      *
248      * These are the values supplied to any of the [SUPPRESS_ANNOTATIONS] on this item. It DOES not
249      * include suppressed issues from the [parent].
250      */
251     override fun suppressedIssues(): Set<String>
252 
253     /** The [BaselineKey] for this. */
254     override val baselineKey
255         get() = BaselineKey.forElementId(baselineElementId())
256 
257     /**
258      * Get the baseline element id from which [baselineKey] is constructed.
259      *
260      * See [BaselineKey.forElementId] for more details.
261      */
262     fun baselineElementId(): String
263 
264     companion object {
265         fun describe(item: Item, capitalize: Boolean = false): String {
266             return when (item) {
267                 is PackageItem -> describe(item, capitalize = capitalize)
268                 is ClassItem -> describe(item, capitalize = capitalize)
269                 is FieldItem -> describe(item, capitalize = capitalize)
270                 is CallableItem ->
271                     describe(
272                         item,
273                         includeParameterNames = false,
274                         includeParameterTypes = true,
275                         capitalize = capitalize
276                     )
277                 is ParameterItem ->
278                     describe(
279                         item,
280                         includeParameterNames = true,
281                         includeParameterTypes = true,
282                         capitalize = capitalize
283                     )
284                 else -> item.toString()
285             }
286         }
287 
288         fun describe(
289             item: CallableItem,
290             includeParameterNames: Boolean = false,
291             includeParameterTypes: Boolean = false,
292             includeReturnValue: Boolean = false,
293             capitalize: Boolean = false
294         ): String {
295             val builder = StringBuilder()
296             if (item.isConstructor()) {
297                 builder.append(if (capitalize) "Constructor" else "constructor")
298             } else {
299                 builder.append(if (capitalize) "Method" else "method")
300             }
301             builder.append(' ')
302             if (includeReturnValue && !item.isConstructor()) {
303                 builder.append(item.returnType().toSimpleType())
304                 builder.append(' ')
305             }
306             appendCallableSignature(builder, item, includeParameterNames, includeParameterTypes)
307             return builder.toString()
308         }
309 
310         fun describe(
311             item: ParameterItem,
312             includeParameterNames: Boolean = false,
313             includeParameterTypes: Boolean = false,
314             capitalize: Boolean = false
315         ): String {
316             val builder = StringBuilder()
317             builder.append(if (capitalize) "Parameter" else "parameter")
318             builder.append(' ')
319             builder.append(item.name())
320             builder.append(" in ")
321             val callable = item.containingCallable()
322             appendCallableSignature(builder, callable, includeParameterNames, includeParameterTypes)
323             return builder.toString()
324         }
325 
326         private fun appendCallableSignature(
327             builder: StringBuilder,
328             item: CallableItem,
329             includeParameterNames: Boolean,
330             includeParameterTypes: Boolean
331         ) {
332             builder.append(item.containingClass().qualifiedName())
333             if (!item.isConstructor()) {
334                 builder.append('.')
335                 builder.append(item.name())
336             }
337             if (includeParameterNames || includeParameterTypes) {
338                 builder.append('(')
339                 var first = true
340                 for (parameter in item.parameters()) {
341                     if (first) {
342                         first = false
343                     } else {
344                         builder.append(',')
345                         if (includeParameterNames && includeParameterTypes) {
346                             builder.append(' ')
347                         }
348                     }
349                     if (includeParameterTypes) {
350                         builder.append(parameter.type().toSimpleType())
351                         if (includeParameterNames) {
352                             builder.append(' ')
353                         }
354                     }
355                     if (includeParameterNames) {
356                         builder.append(parameter.publicName() ?: parameter.name())
357                     }
358                 }
359                 builder.append(')')
360             }
361         }
362 
363         private fun describe(item: FieldItem, capitalize: Boolean = false): String {
364             return if (item.isEnumConstant()) {
365                 "${if (capitalize) "Enum" else "enum"} constant ${item.containingClass().qualifiedName()}.${item.name()}"
366             } else {
367                 "${if (capitalize) "Field" else "field"} ${item.containingClass().qualifiedName()}.${item.name()}"
368             }
369         }
370 
371         private fun describe(item: ClassItem, capitalize: Boolean = false): String {
372             return "${if (capitalize) "Class" else "class"} ${item.qualifiedName()}"
373         }
374 
375         private fun describe(item: PackageItem, capitalize: Boolean = false): String {
376             val suffix = item.qualifiedName().let { if (it.isEmpty()) "<root>" else it }
377             return "${if (capitalize) "Package" else "package"} $suffix"
378         }
379     }
380 }
381 
382 /** Base [Item] implementation that is common to all models. */
383 abstract class DefaultItem(
384     override val codebase: Codebase,
385     final override val fileLocation: FileLocation,
386     final override val itemLanguage: ItemLanguage,
387     modifiers: BaseModifierList,
388     documentationFactory: ItemDocumentationFactory,
389 ) : Item {
390 
391     /**
392      * Create a [ItemDocumentation] appropriate for this [Item].
393      *
394      * The leaking of `this` is safe as the implementations do not access anything that has not been
395      * initialized.
396      */
397     final override val documentation = @Suppress("LeakingThis") documentationFactory(this)
398 
399     /**
400      * The immutable [modifiers].
401      *
402      * The supplied `modifiers` parameter could be either [MutableModifierList] or [ModifierList]
403      * but this requires a [ModifierList] so get one using [BaseModifierList.toImmutable].
404      *
405      * The [ModifierList] that this references is immutable but the [mutateModifiers] method can be
406      * used to change the [ModifierList] to which this refers.
407      */
408     final override var modifiers: ModifierList = modifiers.toImmutable()
409         private set
410 
411     init {
412         if (!modifiers.isDeprecated() && documentation.hasTagSection("@deprecated")) {
<lambda>null413             @Suppress("LeakingThis") mutateModifiers { setDeprecated(true) }
414         }
415     }
416 
417     final override val sortingRank: Int = nextRank.getAndIncrement()
418 
419     final override val originallyDeprecated
420         // Delegate to the [ModifierList.isDeprecated] method so that changes to that will affect
421         // the value of this and [Item.effectivelyDeprecated] which delegates to this.
422         get() = modifiers.isDeprecated()
423 
mutateModifiersnull424     override fun mutateModifiers(mutator: MutableModifierList.() -> Unit) {
425         val mutable = modifiers.toMutable()
426         mutable.mutator()
427         modifiers = mutable.toImmutable()
428     }
429 
430     final override val isPublic: Boolean
431         get() = modifiers.isPublic()
432 
433     final override val isProtected: Boolean
434         get() = modifiers.isProtected()
435 
436     final override val isInternal: Boolean
437         get() = modifiers.getVisibilityLevel() == VisibilityLevel.INTERNAL
438 
439     final override val isPackagePrivate: Boolean
440         get() = modifiers.isPackagePrivate()
441 
442     final override val isPrivate: Boolean
443         get() = modifiers.isPrivate()
444 
445     companion object {
446         private var nextRank = AtomicInteger()
447     }
448 
suppressedIssuesnull449     final override fun suppressedIssues(): Set<String> {
450         return buildSet {
451             for (annotation in modifiers.annotations()) {
452                 val annotationName = annotation.qualifiedName
453                 if (annotationName in SUPPRESS_ANNOTATIONS) {
454                     for (attribute in annotation.attributes) {
455                         // Assumption that all annotations in SUPPRESS_ANNOTATIONS only have
456                         // one attribute such as value/names that is varargs of String
457                         val value = attribute.legacyValue
458                         if (value is AnnotationArrayAttributeValue) {
459                             // Example: @SuppressLint({"RequiresFeature", "AllUpper"})
460                             for (innerValue in value.values) {
461                                 innerValue.value()?.toString()?.let { add(it) }
462                             }
463                         } else {
464                             // Example: @SuppressLint("RequiresFeature")
465                             value.value()?.toString()?.let { add(it) }
466                         }
467                     }
468                 }
469             }
470         }
471     }
472 
appendDocumentationnull473     final override fun appendDocumentation(comment: String, tagSection: String?) {
474         if (comment.isBlank()) {
475             return
476         }
477 
478         // TODO: Figure out if an annotation should go on the return value, or on the method.
479         // For example; threading: on the method, range: on the return value.
480         // TODO: Find a good way to add or append to a given tag (@param <something>, @return, etc)
481 
482         if (this is ParameterItem) {
483             // For parameters, the documentation goes into the surrounding method's documentation!
484             // Find the right parameter location!
485             val parameterName = name()
486             val target = containingCallable()
487             target.appendDocumentation(comment, parameterName)
488             return
489         }
490 
491         documentation.appendDocumentation(comment, tagSection)
492     }
493 
equalsnull494     final override fun equals(other: Any?) = equalsToItem(other)
495 
496     final override fun hashCode() = hashCodeForItem()
497 
498     final override fun toString() = toStringForItem()
499 }
500