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 }