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.text
18 
19 import com.android.tools.metalava.JAVA_LANG_OBJECT
20 import com.android.tools.metalava.JAVA_LANG_PREFIX
21 import com.android.tools.metalava.model.AnnotationItem
22 import com.android.tools.metalava.model.ClassItem
23 import com.android.tools.metalava.model.Item
24 import com.android.tools.metalava.model.MemberItem
25 import com.android.tools.metalava.model.MethodItem
26 import com.android.tools.metalava.model.TypeItem
27 import com.android.tools.metalava.model.TypeParameterItem
28 import com.android.tools.metalava.model.TypeParameterList
29 import com.android.tools.metalava.model.TypeParameterListOwner
30 import java.util.function.Predicate
31 import kotlin.math.min
32 
33 const val ASSUME_TYPE_VARS_EXTEND_OBJECT = false
34 
35 class TextTypeItem(
36     val codebase: TextCodebase,
37     val type: String
38 ) : TypeItem {
39 
toStringnull40     override fun toString(): String = type
41 
42     override fun toErasedTypeString(context: Item?): String {
43         return toTypeString(
44             outerAnnotations = false,
45             innerAnnotations = false,
46             erased = true,
47             kotlinStyleNulls = false,
48             context = context
49         )
50     }
51 
toTypeStringnull52     override fun toTypeString(
53         outerAnnotations: Boolean,
54         innerAnnotations: Boolean,
55         erased: Boolean,
56         kotlinStyleNulls: Boolean,
57         context: Item?,
58         filter: Predicate<Item>?
59     ): String {
60         val typeString = toTypeString(type, outerAnnotations, innerAnnotations, erased, context)
61 
62         if (innerAnnotations && kotlinStyleNulls && !primitive && context != null) {
63             var nullable: Boolean? = AnnotationItem.getImplicitNullness(context)
64 
65             if (nullable == null) {
66                 for (annotation in context.modifiers.annotations()) {
67                     if (annotation.isNullable()) {
68                         nullable = true
69                     } else if (annotation.isNonNull()) {
70                         nullable = false
71                     }
72                 }
73             }
74             when (nullable) {
75                 null -> return "$typeString!"
76                 true -> return "$typeString?"
77                 // else: non-null: nothing to add
78             }
79         }
80         return typeString
81     }
82 
asClassnull83     override fun asClass(): ClassItem? {
84         if (primitive) {
85             return null
86         }
87         val cls = run {
88             val erased = toErasedTypeString()
89             // Also chop off array dimensions
90             val index = erased.indexOf('[')
91             if (index != -1) {
92                 erased.substring(0, index)
93             } else {
94                 erased
95             }
96         }
97         return codebase.getOrCreateClass(cls)
98     }
99 
qualifiedTypeNamenull100     fun qualifiedTypeName(): String = type
101 
102     override fun equals(other: Any?): Boolean {
103         if (this === other) return true
104 
105         return when (other) {
106             // Note: when we support type-use annotations, this is not safe: there could be a string
107             // literal inside which is significant
108             is TextTypeItem -> TypeItem.equalsWithoutSpace(toString(), other.toString())
109             is TypeItem -> {
110                 val thisString = toTypeString()
111                 val otherString = other.toTypeString()
112                 if (TypeItem.equalsWithoutSpace(thisString, otherString)) {
113                     return true
114                 }
115                 if (thisString.startsWith(JAVA_LANG_PREFIX) && thisString.endsWith(otherString) &&
116                     thisString.length == otherString.length + JAVA_LANG_PREFIX.length
117                 ) {
118                     // When reading signature files, it's sometimes ambiguous whether a name
119                     // references a java.lang. implicit class or a type parameter.
120                     return true
121                 }
122 
123                 return false
124             }
125             else -> false
126         }
127     }
128 
hashCodenull129     override fun hashCode(): Int {
130         return qualifiedTypeName().hashCode()
131     }
132 
arrayDimensionsnull133     override fun arrayDimensions(): Int {
134         val type = toErasedTypeString()
135         var dimensions = 0
136         for (c in type) {
137             if (c == '[') {
138                 dimensions++
139             }
140         }
141         return dimensions
142     }
143 
findTypeVariableBoundsnull144     private fun findTypeVariableBounds(typeParameterList: TypeParameterList, name: String): List<ClassItem> {
145         for (p in typeParameterList.typeParameters()) {
146             if (p.simpleName() == name) {
147                 val bounds = p.bounds()
148                 if (bounds.isNotEmpty()) {
149                     return bounds
150                 }
151             }
152         }
153 
154         return emptyList()
155     }
156 
findTypeVariableBoundsnull157     private fun findTypeVariableBounds(context: Item?, name: String): List<ClassItem> {
158         if (context is MethodItem) {
159             val bounds = findTypeVariableBounds(context.typeParameterList(), name)
160             if (bounds.isNotEmpty()) {
161                 return bounds
162             }
163             return findTypeVariableBounds(context.containingClass().typeParameterList(), name)
164         } else if (context is ClassItem) {
165             return findTypeVariableBounds(context.typeParameterList(), name)
166         }
167 
168         return emptyList()
169     }
170 
asTypeParameternull171     override fun asTypeParameter(context: MemberItem?): TypeParameterItem? {
172         return if (isLikelyTypeParameter(toTypeString())) {
173             val typeParameter =
174                 TextTypeParameterItem.create(codebase, context as? TypeParameterListOwner, toTypeString())
175 
176             if (context != null && typeParameter.bounds().isEmpty()) {
177                 val bounds = findTypeVariableBounds(context, typeParameter.simpleName())
178                 if (bounds.isNotEmpty()) {
179                     val filtered = bounds.filter { !it.isJavaLangObject() }
180                     if (filtered.isNotEmpty()) {
181                         return TextTypeParameterItem.create(
182                             codebase,
183                             context as? TypeParameterListOwner,
184                             toTypeString(),
185                             bounds
186                         )
187                     }
188                 }
189             }
190 
191             typeParameter
192         } else {
193             null
194         }
195     }
196 
197     override val primitive: Boolean
198         get() = isPrimitive(type)
199 
typeArgumentClassesnull200     override fun typeArgumentClasses(): List<ClassItem> = codebase.unsupported()
201 
202     override fun convertType(replacementMap: Map<String, String>?, owner: Item?): TypeItem {
203         return TextTypeItem(codebase, convertTypeString(replacementMap))
204     }
205 
markRecentnull206     override fun markRecent() = codebase.unsupported()
207 
208     override fun scrubAnnotations() = codebase.unsupported()
209 
210     companion object {
211         // heuristic to guess if a given type parameter is a type variable
212         fun isLikelyTypeParameter(typeString: String): Boolean {
213             val first = typeString[0]
214             if (!Character.isUpperCase((first)) && first != '_') {
215                 // This rules out primitives which otherwise don't have
216                 return false
217             }
218             for (c in typeString) {
219                 if (c == '.') {
220                     // This rules out qualified class names
221                     return false
222                 }
223                 if (c == ' ' || c == '[' || c == '<') {
224                     return true
225                 }
226                 // I'd like to check for all uppercase here but there are APIs which
227                 // violate this, such as AsyncTask where the type variable names include "Result"
228             }
229 
230             return true
231         }
232 
233         fun toTypeString(
234             type: String,
235             outerAnnotations: Boolean,
236             innerAnnotations: Boolean,
237             erased: Boolean,
238             context: Item? = null
239         ): String {
240             return if (erased) {
241                 val raw = eraseTypeArguments(type)
242                 val concrete = substituteTypeParameters(raw, context)
243                 if (outerAnnotations && innerAnnotations) {
244                     concrete
245                 } else {
246                     eraseAnnotations(concrete, outerAnnotations, innerAnnotations)
247                 }
248             } else {
249                 if (outerAnnotations && innerAnnotations) {
250                     type
251                 } else {
252                     eraseAnnotations(type, outerAnnotations, innerAnnotations)
253                 }
254             }
255         }
256 
257         private fun substituteTypeParameters(s: String, context: Item?): String {
258             if (context is TypeParameterListOwner) {
259                 var end = s.indexOf('[')
260                 if (end == -1) {
261                     end = s.length
262                 }
263                 if (s[0].isUpperCase() && s.lastIndexOf('.', end) == -1) {
264                     val v = s.substring(0, end)
265                     val parameter = context.resolveParameter(v)
266                     if (parameter != null) {
267                         val bounds = parameter.bounds()
268                         if (bounds.isNotEmpty()) {
269                             return bounds.first().qualifiedName() + s.substring(end)
270                         }
271                         @Suppress("ConstantConditionIf")
272                         if (ASSUME_TYPE_VARS_EXTEND_OBJECT) {
273                             return JAVA_LANG_OBJECT + s.substring(end)
274                         }
275                     }
276                 }
277             }
278 
279             return s
280         }
281 
282         fun eraseTypeArguments(s: String): String {
283             val index = s.indexOf('<')
284             if (index != -1) {
285                 var balance = 0
286                 for (i in index..s.length) {
287                     val c = s[i]
288                     if (c == '<') {
289                         balance++
290                     } else if (c == '>') {
291                         balance--
292                         if (balance == 0) {
293                             return if (i == s.length - 1) {
294                                 s.substring(0, index)
295                             } else {
296                                 s.substring(0, index) + s.substring(i + 1)
297                             }
298                         }
299                     }
300                 }
301 
302                 return s.substring(0, index)
303             }
304             return s
305         }
306 
307         /**
308          * Given a type possibly using the Kotlin-style null syntax, strip out any Kotlin-style
309          * null syntax characters, e.g. "String?" -> "String", but make sure not to damage types
310          * like "Set<? extends Number>".
311          */
312         fun stripKotlinNullChars(s: String): String {
313             var found = false
314             var prev = ' '
315             for (c in s) {
316                 if (c == '!' || c == '?' && (prev != '<' && prev != ',' && prev != ' ')) {
317                     found = true
318                     break
319                 }
320                 prev = c
321             }
322 
323             if (!found) {
324                 return s
325             }
326 
327             val sb = StringBuilder(s.length)
328             for (c in s) {
329                 if (c == '!' || c == '?' && (prev != '<' && prev != ',' && prev != ' ')) {
330                     // skip
331                 } else {
332                     sb.append(c)
333                 }
334                 prev = c
335             }
336 
337             return sb.toString()
338         }
339 
340         private fun eraseAnnotations(type: String, outer: Boolean, inner: Boolean): String {
341             if (type.indexOf('@') == -1) {
342                 // If using Kotlin-style null syntax, strip those markers as well
343                 return stripKotlinNullChars(type)
344             }
345 
346             assert(inner || !outer) // Can't supply outer=true,inner=false
347 
348             // Assumption: top level annotations appear first
349             val length = type.length
350             var max = if (!inner)
351                 length
352             else {
353                 val space = type.indexOf(' ')
354                 val generics = type.indexOf('<')
355                 val first = if (space != -1) {
356                     if (generics != -1) {
357                         min(space, generics)
358                     } else {
359                         space
360                     }
361                 } else {
362                     generics
363                 }
364                 if (first != -1) {
365                     first
366                 } else {
367                     length
368                 }
369             }
370 
371             var s = type
372             while (true) {
373                 val index = s.indexOf('@')
374                 if (index == -1 || index >= max) {
375                     break
376                 }
377 
378                 // Find end
379                 val end = findAnnotationEnd(s, index + 1)
380                 val oldLength = s.length
381                 s = s.substring(0, index).trim() + s.substring(end).trim()
382                 val newLength = s.length
383                 val removed = oldLength - newLength
384                 max -= removed
385             }
386 
387             // Sometimes we have a second type after the max, such as
388             // @androidx.annotation.NonNull java.lang.reflect.@androidx.annotation.NonNull TypeVariable<...>
389             for (i in s.indices) {
390                 val c = s[i]
391                 if (Character.isJavaIdentifierPart(c) || c == '.') {
392                     continue
393                 } else if (c == '@') {
394                     // Found embedded annotation within the type
395                     val end = findAnnotationEnd(s, i + 1)
396                     if (end == -1 || end == length) {
397                         break
398                     }
399 
400                     s = s.substring(0, i).trim() + s.substring(end).trim()
401                     break
402                 } else {
403                     break
404                 }
405             }
406 
407             return s
408         }
409 
410         private fun findAnnotationEnd(type: String, start: Int): Int {
411             var index = start
412             val length = type.length
413             var balance = 0
414             while (index < length) {
415                 val c = type[index]
416                 if (c == '(') {
417                     balance++
418                 } else if (c == ')') {
419                     balance--
420                     if (balance == 0) {
421                         return index + 1
422                     }
423                 } else if (c == '.') {
424                 } else if (Character.isJavaIdentifierPart(c)) {
425                 } else if (balance == 0) {
426                     break
427                 }
428                 index++
429             }
430             return index
431         }
432 
433         fun isPrimitive(type: String): Boolean {
434             return when (type) {
435                 "byte", "char", "double", "float", "int", "long", "short", "boolean", "void", "null" -> true
436                 else -> false
437             }
438         }
439     }
440 }