| /* |
| * 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 |
| |
| import com.android.SdkConstants.AMP_ENTITY |
| import com.android.SdkConstants.APOS_ENTITY |
| import com.android.SdkConstants.ATTR_NAME |
| import com.android.SdkConstants.DOT_CLASS |
| import com.android.SdkConstants.DOT_JAR |
| import com.android.SdkConstants.DOT_XML |
| import com.android.SdkConstants.DOT_ZIP |
| import com.android.SdkConstants.GT_ENTITY |
| import com.android.SdkConstants.INT_DEF_ANNOTATION |
| import com.android.SdkConstants.LT_ENTITY |
| import com.android.SdkConstants.QUOT_ENTITY |
| import com.android.SdkConstants.STRING_DEF_ANNOTATION |
| import com.android.SdkConstants.TYPE_DEF_FLAG_ATTRIBUTE |
| import com.android.SdkConstants.TYPE_DEF_VALUE_ATTRIBUTE |
| import com.android.SdkConstants.VALUE_TRUE |
| import com.android.tools.lint.annotations.Extractor.ANDROID_INT_DEF |
| import com.android.tools.lint.annotations.Extractor.ANDROID_NOTNULL |
| import com.android.tools.lint.annotations.Extractor.ANDROID_NULLABLE |
| import com.android.tools.lint.annotations.Extractor.ANDROID_STRING_DEF |
| import com.android.tools.lint.annotations.Extractor.ATTR_PURE |
| import com.android.tools.lint.annotations.Extractor.ATTR_VAL |
| import com.android.tools.lint.annotations.Extractor.IDEA_CONTRACT |
| import com.android.tools.lint.annotations.Extractor.IDEA_MAGIC |
| import com.android.tools.lint.annotations.Extractor.IDEA_NOTNULL |
| import com.android.tools.lint.annotations.Extractor.IDEA_NULLABLE |
| import com.android.tools.lint.annotations.Extractor.SUPPORT_NOTNULL |
| import com.android.tools.lint.annotations.Extractor.SUPPORT_NULLABLE |
| import com.android.tools.lint.checks.AnnotationDetector |
| import com.android.tools.lint.detector.api.getChildren |
| 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.AnnotationTarget |
| import com.android.tools.metalava.model.ClassItem |
| import com.android.tools.metalava.model.Codebase |
| import com.android.tools.metalava.model.DefaultAnnotationItem |
| import com.android.tools.metalava.model.DefaultAnnotationValue |
| import com.android.tools.metalava.model.Item |
| import com.android.tools.metalava.model.MethodItem |
| import com.android.tools.metalava.model.ModifierList |
| import com.android.tools.metalava.model.TypeItem |
| import com.android.tools.metalava.model.parseDocument |
| import com.android.tools.metalava.model.psi.PsiAnnotationItem |
| import com.android.tools.metalava.model.psi.PsiBasedCodebase |
| import com.android.tools.metalava.model.psi.PsiTypeItem |
| import com.android.tools.metalava.model.text.ApiFile |
| import com.android.tools.metalava.model.text.ApiParseException |
| import com.android.tools.metalava.model.visitors.ApiVisitor |
| import com.google.common.io.ByteStreams |
| import com.google.common.io.Closeables |
| import com.google.common.io.Files |
| import org.w3c.dom.Document |
| import org.w3c.dom.Element |
| import org.xml.sax.SAXParseException |
| import java.io.File |
| import java.io.FileInputStream |
| import java.io.IOException |
| import java.lang.reflect.Field |
| import java.util.jar.JarInputStream |
| import java.util.regex.Pattern |
| import java.util.zip.ZipEntry |
| import kotlin.text.Charsets.UTF_8 |
| |
| /** Merges annotations into classes already registered in the given [Codebase] */ |
| class AnnotationsMerger( |
| private val codebase: Codebase |
| ) { |
| |
| /** Merge annotations which will appear in the output API. */ |
| fun mergeQualifierAnnotations(files: List<File>) { |
| mergeAll( |
| files, |
| ::mergeQualifierAnnotationsFromFile, |
| ::mergeAndValidateQualifierAnnotationsFromJavaStubsCodebase |
| ) |
| } |
| |
| /** Merge annotations which control what is included in the output API. */ |
| fun mergeInclusionAnnotations(files: List<File>) { |
| mergeAll( |
| files, |
| { |
| throw DriverException( |
| "External inclusion annotations files must be .java, found ${it.path}" |
| ) |
| }, |
| ::mergeInclusionAnnotationsFromCodebase |
| ) |
| } |
| |
| private fun mergeAll( |
| mergeAnnotations: List<File>, |
| mergeFile: (File) -> Unit, |
| mergeJavaStubsCodebase: (PsiBasedCodebase) -> Unit |
| ) { |
| val javaStubFiles = mutableListOf<File>() |
| mergeAnnotations.forEach { |
| mergeFileOrDir(it, mergeFile, javaStubFiles) |
| } |
| if (javaStubFiles.isNotEmpty()) { |
| // Set up class path to contain our main sources such that we can |
| // resolve types in the stubs |
| val roots = mutableListOf<File>() |
| extractRoots(options.sources, roots) |
| roots.addAll(options.sourcePath) |
| val javaStubsCodebase = parseSources( |
| javaStubFiles, |
| "Codebase loaded from stubs", |
| sourcePath = roots, |
| classpath = options.classpath |
| ) |
| mergeJavaStubsCodebase(javaStubsCodebase) |
| } |
| } |
| |
| /** |
| * Merges annotations from `file`, or from all the files under it if `file` is a directory. |
| * All files apart from Java stub files are merged using [mergeFile]. Java stub files are not |
| * merged by this method, instead they are added to [javaStubFiles] and should be merged later |
| * (so that all the Java stubs can be loaded as a single codebase). |
| */ |
| private fun mergeFileOrDir( |
| file: File, |
| mergeFile: (File) -> Unit, |
| javaStubFiles: MutableList<File> |
| ) { |
| if (file.isDirectory) { |
| val files = file.listFiles() |
| if (files != null) { |
| for (child in files) { |
| mergeFileOrDir(child, mergeFile, javaStubFiles) |
| } |
| } |
| } else if (file.isFile) { |
| if (file.path.endsWith(".java")) { |
| javaStubFiles.add(file) |
| } else { |
| mergeFile(file) |
| } |
| } |
| } |
| |
| private fun mergeQualifierAnnotationsFromFile(file: File) { |
| if (file.path.endsWith(DOT_JAR) || file.path.endsWith(DOT_ZIP)) { |
| mergeFromJar(file) |
| } else if (file.path.endsWith(DOT_XML)) { |
| try { |
| val xml = Files.asCharSource(file, UTF_8).read() |
| mergeAnnotationsXml(file.path, xml) |
| } catch (e: IOException) { |
| error("I/O problem during transform: $e") |
| } |
| } else if (file.path.endsWith(".txt") || |
| file.path.endsWith(".signatures") || |
| file.path.endsWith(".api") |
| ) { |
| try { |
| // .txt: Old style signature files |
| // Others: new signature files (e.g. kotlin-style nullness info) |
| mergeAnnotationsSignatureFile(file.path) |
| } catch (e: IOException) { |
| error("I/O problem during transform: $e") |
| } |
| } |
| } |
| |
| private fun mergeFromJar(jar: File) { |
| // Reads in an existing annotations jar and merges in entries found there |
| // with the annotations analyzed from source. |
| var zis: JarInputStream? = null |
| try { |
| val fis = FileInputStream(jar) |
| zis = JarInputStream(fis) |
| var entry: ZipEntry? = zis.nextEntry |
| while (entry != null) { |
| if (entry.name.endsWith(".xml")) { |
| val bytes = ByteStreams.toByteArray(zis) |
| val xml = String(bytes, UTF_8) |
| mergeAnnotationsXml(jar.path + ": " + entry, xml) |
| } |
| entry = zis.nextEntry |
| } |
| } catch (e: IOException) { |
| error("I/O problem during transform: $e") |
| } finally { |
| try { |
| Closeables.close(zis, true /* swallowIOException */) |
| } catch (e: IOException) { |
| // cannot happen |
| } |
| } |
| } |
| |
| private fun mergeAnnotationsXml(path: String, xml: String) { |
| try { |
| val document = parseDocument(xml, false) |
| mergeDocument(document) |
| } catch (e: Exception) { |
| var message = "Failed to merge $path: $e" |
| if (e is SAXParseException) { |
| message = "Line ${e.lineNumber}:${e.columnNumber}: $message" |
| } |
| error(message) |
| if (e !is IOException) { |
| e.printStackTrace() |
| } |
| } |
| } |
| |
| private fun mergeAnnotationsSignatureFile(path: String) { |
| try { |
| val signatureCodebase = ApiFile.parseApi(File(path), options.inputKotlinStyleNulls) |
| signatureCodebase.description = "Signature files for annotation merger: loaded from $path" |
| mergeQualifierAnnotationsFromCodebase(signatureCodebase) |
| } catch (ex: ApiParseException) { |
| val message = "Unable to parse signature file $path: ${ex.message}" |
| throw DriverException(message) |
| } |
| } |
| |
| private fun mergeAndValidateQualifierAnnotationsFromJavaStubsCodebase(javaStubsCodebase: PsiBasedCodebase) { |
| mergeQualifierAnnotationsFromCodebase(javaStubsCodebase) |
| if (options.validateNullabilityFromMergedStubs) { |
| options.nullabilityAnnotationsValidator?.validateAll( |
| codebase, |
| javaStubsCodebase.getTopLevelClassesFromSource().map(ClassItem::qualifiedName) |
| ) |
| } |
| } |
| |
| private fun mergeQualifierAnnotationsFromCodebase(externalCodebase: Codebase) { |
| val visitor = object : ComparisonVisitor() { |
| override fun compare(old: Item, new: Item) { |
| val newModifiers = new.modifiers |
| for (annotation in old.modifiers.annotations()) { |
| mergeAnnotation(annotation, newModifiers, new) |
| } |
| old.type()?.let { |
| mergeTypeAnnotations(it, new) |
| } |
| } |
| |
| override fun removed(old: Item, from: Item?) { |
| reporter.report(Issues.UNMATCHED_MERGE_ANNOTATION, old, "qualifier annotations were given for $old but no matching item was found") |
| } |
| |
| private fun mergeAnnotation( |
| annotation: AnnotationItem, |
| newModifiers: ModifierList, |
| new: Item |
| ) { |
| var addAnnotation = false |
| if (annotation.isNullnessAnnotation()) { |
| if (!newModifiers.hasNullnessInfo()) { |
| addAnnotation = true |
| } |
| } else { |
| // TODO: Check for other incompatibilities than nullness? |
| val qualifiedName = annotation.qualifiedName ?: return |
| if (newModifiers.findAnnotation(qualifiedName) == null) { |
| addAnnotation = true |
| } |
| } |
| |
| if (addAnnotation) { |
| // Don't map annotation names - this would turn newly non null back into non null |
| new.mutableModifiers().addAnnotation( |
| new.codebase.createAnnotation( |
| annotation.toSource(showDefaultAttrs = false), |
| new, |
| mapName = false |
| ) |
| ) |
| } |
| } |
| |
| private fun mergeTypeAnnotations( |
| typeItem: TypeItem, |
| new: Item |
| ) { |
| val type = (typeItem as? PsiTypeItem)?.psiType ?: return |
| val typeAnnotations = type.annotations |
| if (typeAnnotations.isNotEmpty()) { |
| for (annotation in typeAnnotations) { |
| val codebase = new.codebase as PsiBasedCodebase |
| val annotationItem = PsiAnnotationItem.create(codebase, annotation) |
| mergeAnnotation(annotationItem, new.modifiers, new) |
| } |
| } |
| } |
| } |
| |
| CodebaseComparator().compare( |
| visitor, externalCodebase, codebase |
| ) |
| } |
| |
| private fun mergeInclusionAnnotationsFromCodebase(externalCodebase: Codebase) { |
| val showAnnotations = options.showAnnotations |
| val hideAnnotations = options.hideAnnotations |
| val hideMetaAnnotations = options.hideMetaAnnotations |
| if (showAnnotations.isNotEmpty() || hideAnnotations.isNotEmpty() || hideMetaAnnotations.isNotEmpty()) { |
| val visitor = object : ComparisonVisitor() { |
| override fun compare(old: Item, new: Item) { |
| // Transfer any show/hide annotations from the external to the main codebase. |
| for (annotation in old.modifiers.annotations()) { |
| val qualifiedName = annotation.qualifiedName ?: continue |
| if ((showAnnotations.matches(annotation) || hideAnnotations.matches(annotation) || hideMetaAnnotations.contains(qualifiedName)) && |
| new.modifiers.findAnnotation(qualifiedName) == null |
| ) { |
| new.mutableModifiers().addAnnotation(annotation) |
| } |
| } |
| // The hidden field in the main codebase is already initialized. So if the |
| // element is hidden in the external codebase, hide it in the main codebase too. |
| if (old.hidden) { |
| new.hidden = true |
| } |
| if (old.originallyHidden) { |
| new.originallyHidden = true |
| } |
| } |
| } |
| CodebaseComparator().compare(visitor, externalCodebase, codebase) |
| } |
| } |
| |
| internal fun error(message: String) { |
| // TODO: Integrate with metalava error facility |
| options.stderr.println("Error: $message") |
| } |
| |
| internal fun warning(message: String) { |
| if (options.verbose) { |
| options.stdout.println("Warning: $message") |
| } |
| } |
| |
| @Suppress("PrivatePropertyName") |
| private val XML_SIGNATURE: Pattern = Pattern.compile( |
| // Class (FieldName | Type? Name(ArgList) Argnum?) |
| // "(\\S+) (\\S+|(.*)\\s+(\\S+)\\((.*)\\)( \\d+)?)"); |
| "(\\S+) (\\S+|((.*)\\s+)?(\\S+)\\((.*)\\)( \\d+)?)" |
| ) |
| |
| private fun mergeDocument(document: Document) { |
| |
| val root = document.documentElement |
| val rootTag = root.tagName |
| assert(rootTag == "root") { rootTag } |
| |
| for (item in getChildren(root)) { |
| var signature: String? = item.getAttribute(ATTR_NAME) |
| if (signature == null || signature == "null") { |
| continue // malformed item |
| } |
| |
| signature = unescapeXml(signature) |
| if (signature == "java.util.Calendar int get(int)") { |
| // https://youtrack.jetbrains.com/issue/IDEA-137385 |
| continue |
| } else if (signature == "java.util.Calendar void set(int, int, int) 1" || |
| signature == "java.util.Calendar void set(int, int, int, int, int) 1" || |
| signature == "java.util.Calendar void set(int, int, int, int, int, int) 1" |
| ) { |
| // http://b.android.com/76090 |
| continue |
| } |
| |
| val matcher = XML_SIGNATURE.matcher(signature) |
| if (matcher.matches()) { |
| val containingClass = matcher.group(1) |
| if (containingClass == null) { |
| warning("Could not find class for $signature") |
| continue |
| } |
| |
| val classItem = codebase.findClass(containingClass) |
| if (classItem == null) { |
| // Well known exceptions from IntelliJ's external annotations |
| // we won't complain loudly about |
| if (wellKnownIgnoredImport(containingClass)) { |
| continue |
| } |
| |
| warning("Could not find class $containingClass; omitting annotation from merge") |
| continue |
| } |
| |
| val methodName = matcher.group(5) |
| if (methodName != null) { |
| val parameters = matcher.group(6) |
| val parameterIndex = |
| if (matcher.group(7) != null) { |
| Integer.parseInt(matcher.group(7).trim()) |
| } else { |
| -1 |
| } |
| mergeMethodOrParameter(item, containingClass, classItem, methodName, parameterIndex, parameters) |
| } else { |
| val fieldName = matcher.group(2) |
| mergeField(item, containingClass, classItem, fieldName) |
| } |
| } else if (signature.indexOf(' ') == -1 && signature.indexOf('.') != -1) { |
| // Must be just a class |
| val containingClass = signature |
| val classItem = codebase.findClass(containingClass) |
| if (classItem == null) { |
| if (wellKnownIgnoredImport(containingClass)) { |
| continue |
| } |
| |
| warning("Could not find class $containingClass; omitting annotation from merge") |
| continue |
| } |
| |
| mergeAnnotations(item, classItem) |
| } else { |
| warning("No merge match for signature $signature") |
| } |
| } |
| } |
| |
| private fun wellKnownIgnoredImport(containingClass: String): Boolean { |
| if (containingClass.startsWith("javax.swing.") || |
| containingClass.startsWith("javax.naming.") || |
| containingClass.startsWith("java.awt.") || |
| containingClass.startsWith("org.jdom.") |
| ) { |
| return true |
| } |
| return false |
| } |
| |
| // The parameter declaration used in XML files should not have duplicated spaces, |
| // and there should be no space after commas (we can't however strip out all spaces, |
| // since for example the spaces around the "extends" keyword needs to be there in |
| // types like Map<String,? extends Number> |
| private fun fixParameterString(parameters: String): String { |
| return parameters.replace(" ", " ").replace(", ", ",").replace("?super", "? super ") |
| .replace("?extends", "? extends ") |
| } |
| |
| private fun mergeMethodOrParameter( |
| item: Element, |
| containingClass: String, |
| classItem: ClassItem, |
| methodName: String, |
| parameterIndex: Int, |
| parameters: String |
| ) { |
| @Suppress("NAME_SHADOWING") |
| val parameters = fixParameterString(parameters) |
| |
| val methodItem: MethodItem? = classItem.findMethod(methodName, parameters) |
| if (methodItem == null) { |
| if (wellKnownIgnoredImport(containingClass)) { |
| return |
| } |
| |
| warning("Could not find method $methodName($parameters) in $containingClass; omitting annotation from merge") |
| return |
| } |
| |
| if (parameterIndex != -1) { |
| val parameterItem = methodItem.parameters()[parameterIndex] |
| |
| if ("java.util.Calendar" == containingClass && "set" == methodName && |
| parameterIndex > 0 |
| ) { |
| // Skip the metadata for Calendar.set(int, int, int+); see |
| // https://code.google.com/p/android/issues/detail?id=73982 |
| return |
| } |
| |
| mergeAnnotations(item, parameterItem) |
| } else { |
| // Annotation on the method itself |
| mergeAnnotations(item, methodItem) |
| } |
| } |
| |
| private fun mergeField(item: Element, containingClass: String, classItem: ClassItem, fieldName: String) { |
| |
| val fieldItem = classItem.findField(fieldName) |
| if (fieldItem == null) { |
| if (wellKnownIgnoredImport(containingClass)) { |
| return |
| } |
| |
| warning("Could not find field $fieldName in $containingClass; omitting annotation from merge") |
| return |
| } |
| |
| mergeAnnotations(item, fieldItem) |
| } |
| |
| private fun getAnnotationName(element: Element): String { |
| val tagName = element.tagName |
| assert(tagName == "annotation") { tagName } |
| |
| val qualifiedName = element.getAttribute(ATTR_NAME) |
| assert(qualifiedName != null && qualifiedName.isNotEmpty()) |
| return qualifiedName |
| } |
| |
| private fun mergeAnnotations(xmlElement: Element, item: Item) { |
| loop@ for (annotationElement in getChildren(xmlElement)) { |
| val originalName = getAnnotationName(annotationElement) |
| val qualifiedName = AnnotationItem.mapName(codebase, originalName) ?: originalName |
| if (hasNullnessConflicts(item, qualifiedName)) { |
| continue@loop |
| } |
| |
| val annotationItem = createAnnotation(annotationElement) ?: continue |
| item.mutableModifiers().addAnnotation(annotationItem) |
| } |
| } |
| |
| private fun hasNullnessConflicts( |
| item: Item, |
| qualifiedName: String |
| ): Boolean { |
| var haveNullable = false |
| var haveNotNull = false |
| for (existing in item.modifiers.annotations()) { |
| val name = existing.qualifiedName ?: continue |
| if (isNonNull(name)) { |
| haveNotNull = true |
| } |
| if (isNullable(name)) { |
| haveNullable = true |
| } |
| if (name == qualifiedName) { |
| return true |
| } |
| } |
| |
| // Make sure we don't have a conflict between nullable and not nullable |
| if (isNonNull(qualifiedName) && haveNullable || isNullable(qualifiedName) && haveNotNull) { |
| warning("Found both @Nullable and @NonNull after import for $item") |
| return true |
| } |
| return false |
| } |
| |
| /** Reads in annotation data from an XML item (using IntelliJ IDE's external annotations XML format) and |
| * creates a corresponding [AnnotationItem], performing some "translations" in the process (e.g. mapping |
| * from IntelliJ annotations like `org.jetbrains.annotations.Nullable` to `android.support.annotation.Nullable`. */ |
| private fun createAnnotation(annotationElement: Element): AnnotationItem? { |
| val tagName = annotationElement.tagName |
| assert(tagName == "annotation") { tagName } |
| val name = annotationElement.getAttribute(ATTR_NAME) |
| assert(name != null && name.isNotEmpty()) |
| when { |
| name == "org.jetbrains.annotations.Range" -> { |
| val children = getChildren(annotationElement) |
| assert(children.size == 2) { children.size } |
| val valueElement1 = children[0] |
| val valueElement2 = children[1] |
| val valName1 = valueElement1.getAttribute(ATTR_NAME) |
| val value1 = valueElement1.getAttribute(ATTR_VAL) |
| val valName2 = valueElement2.getAttribute(ATTR_NAME) |
| val value2 = valueElement2.getAttribute(ATTR_VAL) |
| return PsiAnnotationItem.create( |
| codebase, |
| XmlBackedAnnotationItem( |
| codebase, AnnotationDetector.INT_RANGE_ANNOTATION.newName(), |
| listOf( |
| // Add "L" suffix to ensure that we don't for example interpret "-1" as |
| // an integer -1 and then end up recording it as "ffffffff" instead of -1L |
| XmlBackedAnnotationAttribute( |
| valName1, |
| value1 + (if (value1.last().isDigit()) "L" else "") |
| ), |
| XmlBackedAnnotationAttribute( |
| valName2, |
| value2 + (if (value2.last().isDigit()) "L" else "") |
| ) |
| ) |
| ) |
| ) |
| } |
| name == IDEA_MAGIC -> { |
| val children = getChildren(annotationElement) |
| assert(children.size == 1) { children.size } |
| val valueElement = children[0] |
| val valName = valueElement.getAttribute(ATTR_NAME) |
| var value = valueElement.getAttribute(ATTR_VAL) |
| val flagsFromClass = valName == "flagsFromClass" |
| val flag = valName == "flags" || flagsFromClass |
| if (valName == "valuesFromClass" || flagsFromClass) { |
| // Not supported |
| var found = false |
| if (value.endsWith(DOT_CLASS)) { |
| val clsName = value.substring(0, value.length - DOT_CLASS.length) |
| val sb = StringBuilder() |
| sb.append('{') |
| |
| var reflectionFields: Array<Field>? = null |
| try { |
| val cls = Class.forName(clsName) |
| reflectionFields = cls.declaredFields |
| } catch (ignore: Exception) { |
| // Class not available: not a problem. We'll rely on API filter. |
| // It's mainly used for sorting anyway. |
| } |
| |
| // Attempt to sort in reflection order |
| if (!found && reflectionFields != null) { |
| val filterEmit = ApiVisitor().filterEmit |
| |
| // Attempt with reflection |
| var first = true |
| for (field in reflectionFields) { |
| if (field.type == Integer.TYPE || field.type == Int::class.javaPrimitiveType) { |
| // Make sure this field is included in our API too |
| val fieldItem = codebase.findClass(clsName)?.findField(field.name) |
| if (fieldItem == null || !filterEmit.test(fieldItem)) { |
| continue |
| } |
| |
| if (first) { |
| first = false |
| } else { |
| sb.append(',').append(' ') |
| } |
| sb.append(clsName).append('.').append(field.name) |
| } |
| } |
| } |
| sb.append('}') |
| value = sb.toString() |
| if (sb.length > 2) { // 2: { } |
| found = true |
| } |
| } |
| |
| if (!found) { |
| return null |
| } |
| } |
| |
| val attributes = mutableListOf<XmlBackedAnnotationAttribute>() |
| attributes.add(XmlBackedAnnotationAttribute(TYPE_DEF_VALUE_ATTRIBUTE, value)) |
| if (flag) { |
| attributes.add(XmlBackedAnnotationAttribute(TYPE_DEF_FLAG_ATTRIBUTE, VALUE_TRUE)) |
| } |
| return PsiAnnotationItem.create( |
| codebase, |
| XmlBackedAnnotationItem( |
| codebase, |
| if (valName == "stringValues") STRING_DEF_ANNOTATION.newName() else INT_DEF_ANNOTATION.newName(), |
| attributes |
| ) |
| ) |
| } |
| |
| name == STRING_DEF_ANNOTATION.oldName() || |
| name == STRING_DEF_ANNOTATION.newName() || |
| name == ANDROID_STRING_DEF || |
| name == INT_DEF_ANNOTATION.oldName() || |
| name == INT_DEF_ANNOTATION.newName() || |
| name == ANDROID_INT_DEF -> { |
| |
| val attributes = mutableListOf<XmlBackedAnnotationAttribute>() |
| val parseChild: (Element) -> Unit = { child: Element -> |
| val elementName = child.getAttribute(ATTR_NAME) |
| val value = child.getAttribute(ATTR_VAL) |
| when (elementName) { |
| TYPE_DEF_VALUE_ATTRIBUTE -> { |
| attributes.add(XmlBackedAnnotationAttribute(TYPE_DEF_VALUE_ATTRIBUTE, value)) |
| } |
| TYPE_DEF_FLAG_ATTRIBUTE -> { |
| if (VALUE_TRUE == value) { |
| attributes.add(XmlBackedAnnotationAttribute(TYPE_DEF_FLAG_ATTRIBUTE, VALUE_TRUE)) |
| } |
| } |
| else -> { error("Unrecognized element: " + elementName) } |
| } |
| } |
| val children = getChildren(annotationElement) |
| parseChild(children[0]) |
| if (children.size == 2) { |
| parseChild(children[1]) |
| } |
| val intDef = INT_DEF_ANNOTATION.oldName() == name || |
| INT_DEF_ANNOTATION.newName() == name || |
| ANDROID_INT_DEF == name |
| return PsiAnnotationItem.create( |
| codebase, |
| XmlBackedAnnotationItem( |
| codebase, |
| if (intDef) INT_DEF_ANNOTATION.newName() else STRING_DEF_ANNOTATION.newName(), attributes |
| ) |
| ) |
| } |
| |
| name == IDEA_CONTRACT -> { |
| val children = getChildren(annotationElement) |
| val valueElement = children[0] |
| val value = valueElement.getAttribute(ATTR_VAL) |
| val pure = valueElement.getAttribute(ATTR_PURE) |
| return if (pure != null && pure.isNotEmpty()) { |
| PsiAnnotationItem.create( |
| codebase, |
| XmlBackedAnnotationItem( |
| codebase, name, |
| listOf( |
| XmlBackedAnnotationAttribute(TYPE_DEF_VALUE_ATTRIBUTE, value), |
| XmlBackedAnnotationAttribute(ATTR_PURE, pure) |
| ) |
| ) |
| ) |
| } else { |
| PsiAnnotationItem.create( |
| codebase, |
| XmlBackedAnnotationItem( |
| codebase, name, |
| listOf(XmlBackedAnnotationAttribute(TYPE_DEF_VALUE_ATTRIBUTE, value)) |
| ) |
| ) |
| } |
| } |
| |
| isNonNull(name) -> return codebase.createAnnotation("@$ANDROIDX_NONNULL") |
| |
| isNullable(name) -> return codebase.createAnnotation("@$ANDROIDX_NULLABLE") |
| |
| else -> { |
| val children = getChildren(annotationElement) |
| if (children.isEmpty()) { |
| return codebase.createAnnotation("@$name") |
| } |
| val attributes = mutableListOf<XmlBackedAnnotationAttribute>() |
| for (valueElement in children) { |
| attributes.add( |
| XmlBackedAnnotationAttribute( |
| valueElement.getAttribute(ATTR_NAME) ?: continue, |
| valueElement.getAttribute(ATTR_VAL) ?: continue |
| ) |
| ) |
| } |
| return PsiAnnotationItem.create(codebase, XmlBackedAnnotationItem(codebase, name, attributes)) |
| } |
| } |
| } |
| |
| private fun isNonNull(name: String): Boolean { |
| return name == IDEA_NOTNULL || |
| name == ANDROID_NOTNULL || |
| name == ANDROIDX_NONNULL || |
| name == SUPPORT_NOTNULL |
| } |
| |
| private fun isNullable(name: String): Boolean { |
| return name == IDEA_NULLABLE || |
| name == ANDROID_NULLABLE || |
| name == ANDROIDX_NULLABLE || |
| name == SUPPORT_NULLABLE |
| } |
| |
| private fun unescapeXml(escaped: String): String { |
| var workingString = escaped.replace(QUOT_ENTITY, "\"") |
| workingString = workingString.replace(LT_ENTITY, "<") |
| workingString = workingString.replace(GT_ENTITY, ">") |
| workingString = workingString.replace(APOS_ENTITY, "'") |
| workingString = workingString.replace(AMP_ENTITY, "&") |
| |
| return workingString |
| } |
| } |
| |
| // TODO: Replace with usage of DefaultAnnotationValue? |
| data class XmlBackedAnnotationAttribute( |
| override val name: String, |
| private val valueLiteral: String |
| ) : AnnotationAttribute { |
| override val value: AnnotationAttributeValue = DefaultAnnotationValue.create(valueLiteral) |
| |
| override fun toString(): String { |
| return "$name=$valueLiteral" |
| } |
| } |
| |
| // TODO: Replace with usage of DefaultAnnotationAttribute? |
| class XmlBackedAnnotationItem( |
| codebase: Codebase, |
| override val originalName: String, |
| override val attributes: List<XmlBackedAnnotationAttribute> = emptyList() |
| ) : DefaultAnnotationItem(codebase) { |
| override val qualifiedName: String? = AnnotationItem.mapName(codebase, originalName) |
| |
| override fun toSource(target: AnnotationTarget, showDefaultAttrs: Boolean): String { |
| val qualifiedName = AnnotationItem.mapName(codebase, qualifiedName, null, target) ?: return "" |
| |
| if (attributes.isEmpty()) { |
| return "@$qualifiedName" |
| } |
| |
| val sb = StringBuilder(30) |
| sb.append("@") |
| sb.append(qualifiedName) |
| sb.append("(") |
| attributes.joinTo(sb) |
| sb.append(")") |
| |
| return sb.toString() |
| } |
| } |