blob: 1aaca8e9974a8802c04cfc931dad0da113a9d5cb [file] [log] [blame]
/*
* Copyright (C) 2017 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.psi
import com.android.SdkConstants.ATTR_VALUE
import com.android.tools.lint.detector.api.ConstantEvaluator
import com.android.tools.metalava.XmlBackedAnnotationItem
import com.android.tools.metalava.model.AnnotationArrayAttributeValue
import com.android.tools.metalava.model.AnnotationAttribute
import com.android.tools.metalava.model.AnnotationAttributeValue
import com.android.tools.metalava.model.AnnotationItem
import com.android.tools.metalava.model.AnnotationSingleAttributeValue
import com.android.tools.metalava.model.ClassItem
import com.android.tools.metalava.model.Codebase
import com.android.tools.metalava.model.Item
import com.android.tools.metalava.model.canonicalizeFloatingPointString
import com.android.tools.metalava.model.javaEscapeString
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiAnnotationMemberValue
import com.intellij.psi.PsiArrayInitializerMemberValue
import com.intellij.psi.PsiBinaryExpression
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiExpression
import com.intellij.psi.PsiField
import com.intellij.psi.PsiLiteral
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiReference
import com.intellij.psi.impl.JavaConstantExpressionEvaluator
import org.jetbrains.kotlin.asJava.elements.KtLightNullabilityAnnotation
class PsiAnnotationItem private constructor(
override val codebase: PsiBasedCodebase,
val psiAnnotation: PsiAnnotation
) : AnnotationItem {
private var attributes: List<AnnotationAttribute>? = null
override fun toString(): String = toSource()
override fun toSource(): String {
val sb = StringBuilder(60)
appendAnnotation(sb, psiAnnotation)
return sb.toString()
}
private fun appendAnnotation(sb: StringBuilder, psiAnnotation: PsiAnnotation) {
val qualifiedName = AnnotationItem.mapName(codebase, psiAnnotation.qualifiedName) ?: return
val attributes = psiAnnotation.parameterList.attributes
if (attributes.isEmpty()) {
sb.append("@$qualifiedName")
return
}
sb.append("@")
sb.append(qualifiedName)
sb.append("(")
if (attributes.size == 1 && (attributes[0].name == null || attributes[0].name == ATTR_VALUE)) {
// Special case: omit "value" if it's the only attribute
appendValue(sb, attributes[0].value)
} else {
var first = true
for (attribute in attributes) {
if (first) {
first = false
} else {
sb.append(", ")
}
sb.append(attribute.name ?: ATTR_VALUE)
sb.append('=')
appendValue(sb, attribute.value)
}
}
sb.append(")")
}
override fun resolve(): ClassItem? {
return codebase.findClass(psiAnnotation.qualifiedName ?: return null)
}
private fun appendValue(sb: StringBuilder, value: PsiAnnotationMemberValue?) {
// Compute annotation string -- we don't just use value.text here
// because that may not use fully qualified names, e.g. the source may say
// @RequiresPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
// and we want to compute
// @android.support.annotation.RequiresPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
when (value) {
null -> sb.append("null")
is PsiLiteral -> sb.append(literalToString(value.value))
is PsiReference -> {
val resolved = value.resolve()
when (resolved) {
is PsiField -> {
val containing = resolved.containingClass
if (containing != null) {
// If it's a field reference, see if it looks like the field is hidden; if
// so, inline the value
val cls = codebase.findOrCreateClass(containing)
val initializer = resolved.initializer
if (initializer != null) {
val fieldItem = cls.findField(resolved.name)
if (fieldItem == null || fieldItem.isHiddenOrRemoved()) {
// Use the literal value instead
val source = getConstantSource(initializer)
if (source != null) {
sb.append(source)
return
}
}
}
containing.qualifiedName?.let {
sb.append(it).append('.')
}
}
sb.append(resolved.name)
}
is PsiClass -> resolved.qualifiedName?.let { sb.append(it) }
else -> {
sb.append(value.text)
}
}
}
is PsiBinaryExpression -> {
appendValue(sb, value.lOperand)
sb.append(' ')
sb.append(value.operationSign.text)
sb.append(' ')
appendValue(sb, value.rOperand)
}
is PsiArrayInitializerMemberValue -> {
sb.append('{')
var first = true
for (initializer in value.initializers) {
if (first) {
first = false
} else {
sb.append(", ")
}
appendValue(sb, initializer)
}
sb.append('}')
}
is PsiAnnotation -> {
appendAnnotation(sb, value)
}
else -> {
if (value is PsiExpression) {
val source = getConstantSource(value)
if (source != null) {
sb.append(source)
return
}
}
sb.append(value.text)
}
}
}
override fun isNonNull(): Boolean {
if (psiAnnotation is KtLightNullabilityAnnotation &&
psiAnnotation.qualifiedName == ""
) {
// Hack/workaround: some UAST annotation nodes do not provide qualified name :=(
return true
}
return super.isNonNull()
}
private fun getConstantSource(value: PsiExpression): String? {
val constant = JavaConstantExpressionEvaluator.computeConstantExpression(value, false)
return when (constant) {
is Int -> "0x${Integer.toHexString(constant)}"
is String -> "\"${javaEscapeString(constant)}\""
is Long -> "${constant}L"
is Boolean -> constant.toString()
is Byte -> Integer.toHexString(constant.toInt())
is Short -> Integer.toHexString(constant.toInt())
is Float -> {
when (constant) {
Float.POSITIVE_INFINITY -> "Float.POSITIVE_INFINITY"
Float.NEGATIVE_INFINITY -> "Float.NEGATIVE_INFINITY"
Float.NaN -> "Float.NaN"
else -> {
"${canonicalizeFloatingPointString(constant.toString())}F"
}
}
}
is Double -> {
when (constant) {
Double.POSITIVE_INFINITY -> "Double.POSITIVE_INFINITY"
Double.NEGATIVE_INFINITY -> "Double.NEGATIVE_INFINITY"
Double.NaN -> "Double.NaN"
else -> {
canonicalizeFloatingPointString(constant.toString())
}
}
}
is Char -> {
"'${javaEscapeString(constant.toString())}'"
}
else -> {
null
}
}
}
private fun literalToString(value: Any?): String {
if (value == null) {
return "null"
}
when (value) {
is Int -> {
return value.toString()
}
is String -> {
return "\"${javaEscapeString(value)}\""
}
is Long -> {
return value.toString() + "L"
}
is Boolean -> {
return value.toString()
}
is Byte -> {
return Integer.toHexString(value.toInt())
}
is Short -> {
return Integer.toHexString(value.toInt())
}
is Float -> {
return when (value) {
Float.POSITIVE_INFINITY -> "(1.0f/0.0f)"
Float.NEGATIVE_INFINITY -> "(-1.0f/0.0f)"
Float.NaN -> "(0.0f/0.0f)"
else -> {
canonicalizeFloatingPointString(value.toString()) + "f"
}
}
}
is Double -> {
return when (value) {
Double.POSITIVE_INFINITY -> "(1.0/0.0)"
Double.NEGATIVE_INFINITY -> "(-1.0/0.0)"
Double.NaN -> "(0.0/0.0)"
else -> {
canonicalizeFloatingPointString(value.toString())
}
}
}
is Char -> {
return String.format("'%s'", javaEscapeString(value.toString()))
}
}
return value.toString()
}
override fun qualifiedName() = AnnotationItem.mapName(codebase, psiAnnotation.qualifiedName)
override fun attributes(): List<AnnotationAttribute> {
if (attributes == null) {
val psiAttributes = psiAnnotation.parameterList.attributes
attributes = if (psiAttributes.isEmpty()) {
emptyList()
} else {
val list = mutableListOf<AnnotationAttribute>()
for (parameter in psiAttributes) {
list.add(
PsiAnnotationAttribute(
codebase,
parameter.name ?: ATTR_VALUE, parameter.value ?: continue
)
)
}
list
}
}
return attributes!!
}
companion object {
fun create(codebase: PsiBasedCodebase, psiAnnotation: PsiAnnotation): PsiAnnotationItem {
return PsiAnnotationItem(codebase, psiAnnotation)
}
fun create(codebase: PsiBasedCodebase, original: PsiAnnotationItem): PsiAnnotationItem {
return PsiAnnotationItem(codebase, original.psiAnnotation)
}
// TODO: Inline this such that instead of constructing XmlBackedAnnotationItem
// and then producing source and parsing it, produce source directly
fun create(
codebase: Codebase,
xmlAnnotation: XmlBackedAnnotationItem,
context: Item? = null
): PsiAnnotationItem {
if (codebase is PsiBasedCodebase) {
return codebase.createAnnotation(xmlAnnotation.toSource(), context)
} else {
codebase.unsupported("Converting to PSI annotation requires PSI codebase")
}
}
}
}
class PsiAnnotationAttribute(
codebase: PsiBasedCodebase,
override val name: String,
psiValue: PsiAnnotationMemberValue
) : AnnotationAttribute {
override val value: AnnotationAttributeValue = PsiAnnotationValue.create(
codebase, psiValue
)
}
abstract class PsiAnnotationValue : AnnotationAttributeValue {
companion object {
fun create(codebase: PsiBasedCodebase, value: PsiAnnotationMemberValue): PsiAnnotationValue {
return if (value is PsiArrayInitializerMemberValue) {
PsiAnnotationArrayAttributeValue(codebase, value)
} else {
PsiAnnotationSingleAttributeValue(codebase, value)
}
}
}
override fun toString(): String = toSource()
}
class PsiAnnotationSingleAttributeValue(
private val codebase: PsiBasedCodebase,
private val psiValue: PsiAnnotationMemberValue
) : PsiAnnotationValue(), AnnotationSingleAttributeValue {
override val valueSource: String = psiValue.text
override val value: Any?
get() {
if (psiValue is PsiLiteral) {
return psiValue.value
}
val value = ConstantEvaluator.evaluate(null, psiValue)
if (value != null) {
return value
}
return psiValue.text
}
override fun value(): Any? = value
override fun toSource(): String = psiValue.text
override fun resolve(): Item? {
if (psiValue is PsiReference) {
val resolved = psiValue.resolve()
when (resolved) {
is PsiField -> return codebase.findField(resolved)
is PsiClass -> return codebase.findOrCreateClass(resolved)
is PsiMethod -> return codebase.findMethod(resolved)
}
}
return null
}
}
class PsiAnnotationArrayAttributeValue(codebase: PsiBasedCodebase, private val value: PsiArrayInitializerMemberValue) :
PsiAnnotationValue(), AnnotationArrayAttributeValue {
override val values = value.initializers.map {
create(codebase, it)
}.toList()
override fun toSource(): String = value.text
}