blob: 31c8fa70b0f4de1b9e338a85e10413ef4fa0dd97 [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.metalava.model.text
import com.android.tools.metalava.model.PrimitiveTypeItem
import com.android.tools.metalava.model.TypeParameterItem
import java.util.HashMap
import kotlin.math.min
/** Parses and caches types for a [codebase]. */
internal class TextTypeParser(val codebase: TextCodebase) {
private val typeCache = Cache<String, TextTypeItem>()
/**
* Creates a [TextTypeItem] representing the type of [cl]. Since this is definitely a class
* type, the steps in [obtainTypeFromString] aren't needed.
*/
fun obtainTypeFromClass(cl: TextClassItem): TextTypeItem {
return TextTypeItem(codebase, cl.qualifiedTypeName)
}
/**
* Creates or retrieves from the cache a [TextTypeItem] representing [type], in the context of
* the type parameters from [typeParams], if applicable.
*/
fun obtainTypeFromString(
type: String,
typeParams: List<TypeParameterItem> = emptyList()
): TextTypeItem {
// Only use the cache if there are no type parameters to prevent identically named type
// variables from different contexts being parsed as the same type.
return if (typeParams.isEmpty()) {
typeCache.obtain(type) { parseType(it, typeParams) }
} else {
parseType(type, typeParams)
}
}
/** Converts the [type] to a [TextTypeItem] in the context of the [typeParams]. */
private fun parseType(type: String, typeParams: List<TypeParameterItem>): TextTypeItem {
// TODO(b/300081840): handle annotations
val (unannotated, _) = trimLeadingAnnotations(type)
val (withoutNullability, _) = splitNullabilitySuffix(unannotated)
val trimmed = withoutNullability.trim()
// Figure out what kind of type this is. Start with the simple case: primitive.
return asPrimitive(type, trimmed) ?: parseUnknownType(type, typeParams)
}
/** Temporary method for parsing an unknown kind of type, until [parseType] is complete. */
private fun parseUnknownType(type: String, typeParams: List<TypeParameterItem>): TextTypeItem {
if (typeParams.isNotEmpty() && TextTypeItem.isLikelyTypeParameter(type)) {
// Find the "name" part of the type (before a list of type parameters, array marking,
// or nullability annotation), and see if it is a type parameter name.
// This does not handle when a type variable is in the middle of a type (e.g. List<T>),
// which will be fixed when type parsing is rewritten later.
val length = type.length
var nameEnd = length
for (i in 0 until length) {
val c = type[i]
if (c == '<' || c == '[' || c == '!' || c == '?') {
nameEnd = i
break
}
}
val name =
if (nameEnd == length) {
type
} else {
type.substring(0, nameEnd)
}
// Confirm that it's a type variable
if (typeParams.any { it.simpleName() == name }) {
return TextTypeItem(codebase, type)
}
}
return if (implicitJavaLangType(type)) {
TextTypeItem(codebase, "java.lang.$type")
} else {
TextTypeItem(codebase, type)
}
}
private fun implicitJavaLangType(s: String): Boolean {
if (s.length <= 1) {
return false // Usually a type variable
}
if (s[1] == '[') {
return false // Type variable plus array
}
val dotIndex = s.indexOf('.')
val array = s.indexOf('[')
val generics = s.indexOf('<')
if (array == -1 && generics == -1) {
return dotIndex == -1 && !isPrimitive(s)
}
val typeEnd =
if (array != -1) {
if (generics != -1) {
min(array, generics)
} else {
array
}
} else {
generics
}
// Allow dotted type in generic parameter, e.g. "Iterable<java.io.File>" -> return
// true
return (dotIndex == -1 || dotIndex > typeEnd) &&
!isPrimitive(s.substring(0, typeEnd).trim())
}
/**
* Try parsing [type] as a primitive. This will return a non-null [TextPrimitiveTypeItem] if
* [type] exactly matches a primitive name.
*
* [type] should have annotations and nullability markers stripped, with [original] as the
* complete annotated type. Once annotations are properly handled (b/300081840), preserving
* [original] won't be necessary.
*/
private fun asPrimitive(original: String, type: String): TextPrimitiveTypeItem? {
val kind =
when (type) {
"byte" -> PrimitiveTypeItem.Primitive.BYTE
"char" -> PrimitiveTypeItem.Primitive.CHAR
"double" -> PrimitiveTypeItem.Primitive.DOUBLE
"float" -> PrimitiveTypeItem.Primitive.FLOAT
"int" -> PrimitiveTypeItem.Primitive.INT
"long" -> PrimitiveTypeItem.Primitive.LONG
"short" -> PrimitiveTypeItem.Primitive.SHORT
"boolean" -> PrimitiveTypeItem.Primitive.BOOLEAN
"void" -> PrimitiveTypeItem.Primitive.VOID
else -> return null
}
return TextPrimitiveTypeItem(codebase, original, kind)
}
private class Cache<Key, Value> {
private val cache = HashMap<Key, Value>()
fun obtain(o: Key, make: (Key) -> Value): Value {
var r = cache[o]
if (r == null) {
r = make(o)
cache[o] = r
}
// r must be non-null: either it was cached or created with make
return r!!
}
}
companion object {
/** Whether the string represents a primitive type. */
fun isPrimitive(type: String): Boolean {
return when (type) {
"byte",
"char",
"double",
"float",
"int",
"long",
"short",
"boolean",
"void" -> true
else -> false
}
}
/**
* Splits the Kotlin-style nullability marker off the type string, returning a pair of the
* cleaned type string and the nullability suffix.
*/
fun splitNullabilitySuffix(type: String): Pair<String, String> {
// Don't interpret the wildcard type `?` as a nullability marker.
return if (type.length == 1) {
Pair(type, "")
} else if (type.endsWith("?") || type.endsWith("!")) {
Pair(type.dropLast(1), type.last().toString())
} else {
Pair(type, "")
}
}
/**
* Removes all annotations at the beginning of the type, returning the trimmed type and list
* of annotations.
*/
fun trimLeadingAnnotations(type: String): Pair<String, List<String>> {
val annotations = mutableListOf<String>()
var trimmed = type.trim()
while (trimmed.startsWith('@')) {
val end = findAnnotationEnd(trimmed, 1)
annotations.add(trimmed.substring(0, end).trim())
trimmed = trimmed.substring(end).trim()
}
return Pair(trimmed, annotations)
}
/**
* Removes all annotations at the end of the [type], returning the trimmed type and list of
* annotations. This is for use with arrays where annotations applying to the array type go
* after the component type, for instance `String @A []`. The input [type] should **not**
* include the array suffix (`[]` or `...`).
*/
fun trimTrailingAnnotations(type: String): Pair<String, List<String>> {
// The simple way to implement this would be to work from the end of the string, finding
// `@` and removing annotations from the end. However, it is possible for an annotation
// string to contain an `@`, so this is not a safe way to remove the annotations.
// Instead, this finds all annotations starting from the beginning of the string, then
// works backwards to find which ones are the trailing annotations.
val allAnnotationIndices = mutableListOf<Pair<Int, Int>>()
var trimmed = type.trim()
// First find all annotations, saving the first and last index.
var currIndex = 0
while (currIndex < trimmed.length) {
if (trimmed[currIndex] == '@') {
val endIndex = findAnnotationEnd(trimmed, currIndex + 1)
allAnnotationIndices.add(Pair(currIndex, endIndex))
currIndex = endIndex + 1
} else {
currIndex++
}
}
val annotations = mutableListOf<String>()
// Go through all annotations from the back, seeing if they're at the end of the string.
for ((start, end) in allAnnotationIndices.reversed()) {
// This annotation isn't at the end, so we've hit the last trailing annotation
if (end < trimmed.length) {
break
}
annotations.add(trimmed.substring(start))
// Cut this annotation off, so now the next one can end at the last index.
trimmed = trimmed.substring(0, start).trim()
}
return Pair(trimmed, annotations.reversed())
}
/**
* Given [type] which represents a class, splits the string into the qualified name of the
* class, the type parameter string, and a list of type-use annotations.
*
* For instance, for `java.util.@A @B List<java.lang.@C String>`, returns the triple
* ("java.util.List", "<java.lang.@C String", listOf("@A", "@B")).
*/
fun trimClassAnnotations(type: String): Triple<String, String?, List<String>> {
// The constructed qualified type name
var className = ""
// The part of the type which still needs to be parsed
var remaining = type.trim()
val annotations: MutableList<String> = mutableListOf()
var annotationIndex = remaining.indexOf('@')
var paramIndex = remaining.indexOf('<')
while (annotationIndex != -1) {
// If there's an annotation before the params start, parse the next annotation
if (annotationIndex < paramIndex || paramIndex == -1) {
// Everything before the annotation is part of the class name
className += remaining.substring(0, annotationIndex).trim()
// Find the end of the annotation, add the annotation to the list
val annotationEnd = findAnnotationEnd(remaining, annotationIndex + 1)
annotations.add(remaining.substring(annotationIndex, annotationEnd).trim())
// Set the remaining string to parse
remaining =
if (annotationEnd == remaining.length) {
""
} else {
remaining.substring(annotationEnd).trim()
}
// Reset indices to continue
annotationIndex = remaining.indexOf('@')
paramIndex = remaining.indexOf('<')
} else {
// Params start first, so there are no more annotations breaking up the name.
// All remaining annotations apply to parameters of the class, not the class
// type itself.
break
}
}
// Finalize the class name and find the parameter string
val params: String?
if (paramIndex == -1) {
className += remaining
params = null
} else {
className += remaining.substring(0, paramIndex).trim()
params = remaining.substring(paramIndex)
}
return Triple(className, params, annotations)
}
/**
* Given a string and the index in that string which is the start of an annotation (the
* character _after_ the `@`), returns the index of the end of the annotation.
*/
fun findAnnotationEnd(type: String, start: Int): Int {
var index = start
val length = type.length
var balance = 0
while (index < length) {
val c = type[index]
if (c == '(') {
balance++
} else if (c == ')') {
balance--
if (balance == 0) {
return index + 1
}
} else if (c != '.' && !Character.isJavaIdentifierPart(c) && balance == 0) {
break
}
index++
}
return index
}
/**
* Breaks a string representing type parameters into a list of the type parameter strings.
*
* E.g. `"<A, B, C>"` -> `["A", "B", "C"]` and `"<List<A>, B>"` -> `["List<A>", "B"]`.
*/
fun typeParameterStrings(typeString: String?): List<String> {
return typeParameterStringsWithRemainder(typeString).first
}
/**
* Breaks a string representing type parameters into a list of the type parameter strings,
* and also returns the remainder of the string after the closing ">".
*
* E.g. `"<A, B, C>.Inner"` -> `Pair(["A", "B", "C"], ".Inner")`
*/
fun typeParameterStringsWithRemainder(typeString: String?): Pair<List<String>, String?> {
val s = typeString ?: return Pair(emptyList(), null)
val list = mutableListOf<String>()
var balance = 0
var expect = false
var start = 0
for (i in s.indices) {
val c = s[i]
if (c == '<') {
balance++
expect = balance == 1
} else if (c == '>') {
balance--
if (balance == 1) {
add(list, s, start, i + 1)
start = i + 1
} else if (balance == 0) {
add(list, s, start, i)
return if (i == s.length - 1) {
Pair(list, null)
} else {
Pair(list, s.substring(i + 1))
}
}
} else if (c == ',') {
expect =
if (balance == 1) {
add(list, s, start, i)
true
} else {
false
}
} else if (expect && balance == 1) {
start = i
expect = false
}
}
return Pair(list, null)
}
/**
* Adds the substring of [s] from [from] to [to] to the [list], trimming whitespace from the
* front.
*/
private fun add(list: MutableList<String>, s: String, from: Int, to: Int) {
for (i in from until to) {
if (!Character.isWhitespace(s[i])) {
list.add(s.substring(i, to))
return
}
}
}
}
}