blob: 3be4cab9ab2abd7484b0ea9056eee6ed31ec3b55 [file] [log] [blame]
/*
* Copyright (C) 2014 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.build.gradle.tasks.annotations;
import static com.android.SdkConstants.AMP_ENTITY;
import static com.android.SdkConstants.APOS_ENTITY;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_VALUE;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAR;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.GT_ENTITY;
import static com.android.SdkConstants.INT_DEF_ANNOTATION;
import static com.android.SdkConstants.LT_ENTITY;
import static com.android.SdkConstants.QUOT_ENTITY;
import static com.android.SdkConstants.STRING_DEF_ANNOTATION;
import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX;
import static com.android.SdkConstants.TYPE_DEF_FLAG_ATTRIBUTE;
import static com.android.SdkConstants.TYPE_DEF_VALUE_ATTRIBUTE;
import static com.android.SdkConstants.VALUE_TRUE;
import static com.android.tools.lint.detector.api.LintUtils.assertionsEnabled;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.utils.XmlUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import com.google.common.xml.XmlEscapers;
import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.ArrayInitializer;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.ConstructorDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.FalseLiteral;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.MemberValuePair;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.NameReference;
import org.eclipse.jdt.internal.compiler.ast.NumberLiteral;
import org.eclipse.jdt.internal.compiler.ast.StringLiteral;
import org.eclipse.jdt.internal.compiler.ast.TrueLiteral;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.impl.ReferenceContext;
import org.eclipse.jdt.internal.compiler.lookup.AnnotationBinding;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope;
import org.eclipse.jdt.internal.compiler.lookup.ElementValuePair;
import org.eclipse.jdt.internal.compiler.lookup.FieldBinding;
import org.eclipse.jdt.internal.compiler.lookup.LocalVariableBinding;
import org.eclipse.jdt.internal.compiler.lookup.MemberTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodScope;
import org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
/**
* Annotation extractor which looks for annotations in parsed compilation units and writes
* the annotations into a format suitable for use by IntelliJ and Android Studio etc;
* it's basically an XML file, organized by package, which lists the signatures for
* fields and methods in classes in the given package, and identifiers method parameters
* by index, and lists the annotations annotated on that element.
* <p>
* This is primarily intended for use in Android libraries such as the support library,
* where you want to use the resource int ({@code StringRes}, {@code DrawableRes}, and so on)
* annotations to indicate what types of id's are expected, or the {@code IntDef} or
* {@code StringDef} annotations to record which specific constants are allowed in int and
* String parameters.
* <p>
* However, the code is also used to extract SDK annotations from the platform, where
* the package names of the annotations differ slightly (and where the nullness annotations
* do not have class retention for example). Therefore, this code contains some extra
* support not needed when extracting annotations in an Android library, such as code
* to skip annotations for any method/field not mentioned in the API database, and code
* to rewrite the android.jar file to insert annotations in the generated bytecode.
* <p>
* TODO:
* - Warn if the {@code @IntDef} annotation is used on a non-int, and similarly if
* {@code @StringDef} is used on a non-string
* - Ignore annotations defined on @hide elements
*/
public class Extractor {
/** Whether to sort annotation attributes (otherwise their declaration order is used) */
private static final boolean SORT_ANNOTATIONS = false;
/** Whether we should include type args like &lt;T*gt; in external annotations signatures */
private static final boolean INCLUDE_TYPE_ARGS = false;
/**
* Whether we should include class-retention annotations into the extracted file;
* we don't need {@code android.support.annotation.Nullable} to be in the extracted XML
* file since it has class retention and will appear in the compiled .jar version of
* the library
*/
private final boolean includeClassRetentionAnnotations;
/**
* Whether we should skip nullable annotations in merged in annotations zip files
* (these are typically from infer nullity, which sometimes is a bit aggressive
* in assuming something should be marked as nullable; see for example issue #66999
* or all the manual removals of findViewById @Nullable return value annotations
*/
private static final boolean INCLUDE_INFERRED_NULLABLE = false;
public static final String ANDROID_ANNOTATIONS_PREFIX = "android.annotation.";
public static final String ANDROID_NULLABLE = "android.annotation.Nullable";
public static final String SUPPORT_NULLABLE = "android.support.annotation.Nullable";
public static final String RESOURCE_TYPE_ANNOTATIONS_SUFFIX = "Res";
public static final String ANDROID_NOTNULL = "android.annotation.NonNull";
public static final String SUPPORT_NOTNULL = "android.support.annotation.NonNull";
public static final String ANDROID_INT_DEF = "android.annotation.IntDef";
public static final String ANDROID_STRING_DEF = "android.annotation.StringDef";
public static final String IDEA_NULLABLE = "org.jetbrains.annotations.Nullable";
public static final String IDEA_NOTNULL = "org.jetbrains.annotations.NotNull";
public static final String IDEA_MAGIC = "org.intellij.lang.annotations.MagicConstant";
public static final String IDEA_CONTRACT = "org.jetbrains.annotations.Contract";
public static final String IDEA_NON_NLS = "org.jetbrains.annotations.NonNls";
public static final String ATTR_VAL = "val";
@NonNull
private final Map<String, AnnotationData> types = Maps.newHashMap();
@NonNull
private final Set<String> irrelevantAnnotations = Sets.newHashSet();
private final File classDir;
@NonNull
private Map<String, Map<String, List<Item>>> itemMap = Maps.newHashMap();
@Nullable
private final ApiDatabase apiFilter;
private final boolean displayInfo;
private Map<String,Integer> stats = Maps.newHashMap();
private int filteredCount;
private int mergedCount;
private Set<CompilationUnitDeclaration> processedFiles = Sets.newHashSetWithExpectedSize(100);
private Set<String> ignoredAnnotations = Sets.newHashSet();
private boolean listIgnored;
private Map<String,Annotation> typedefs;
private List<File> classFiles;
private Map<String,Boolean> sourceRetention;
public Extractor(@Nullable ApiDatabase apiFilter, @Nullable File classDir, boolean displayInfo,
boolean includeClassRetentionAnnotations) {
this.apiFilter = apiFilter;
this.listIgnored = apiFilter != null;
this.classDir = classDir;
this.displayInfo = displayInfo;
this.includeClassRetentionAnnotations = includeClassRetentionAnnotations;
}
public void extractFromProjectSource(Collection<CompilationUnitDeclaration> units) {
TypedefCollector collector = new TypedefCollector(units, false /*requireHide*/,
true /*requireSourceRetention*/);
typedefs = collector.getTypedefs();
classFiles = collector.getNonPublicTypedefClassFiles();
for (CompilationUnitDeclaration unit : units) {
analyze(unit);
}
}
public void removeTypedefClasses() {
if (classDir != null && classFiles != null && !classFiles.isEmpty()) {
int count = 0;
for (File file : classFiles) {
if (!file.isAbsolute()) {
file = new File(classDir, file.getPath());
}
if (file.exists()) {
boolean deleted = file.delete();
if (deleted) {
count++;
} else {
warning("Could not delete typedef class " + file.getPath());
}
}
}
info("Deleted " + count + " typedef annotation classes");
}
}
public void export(@NonNull File output) {
if (itemMap.isEmpty()) {
if (output.exists()) {
//noinspection ResultOfMethodCallIgnored
output.delete();
}
} else if (writeOutputFile(output)) {
writeStats();
info("Annotations written to " + output);
}
}
public void writeStats() {
if (!displayInfo) {
return;
}
if (!stats.isEmpty()) {
List<String> annotations = Lists.newArrayList(stats.keySet());
Collections.sort(annotations, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
int frequency1 = stats.get(s1);
int frequency2 = stats.get(s2);
int delta = frequency2 - frequency1;
if (delta != 0) {
return delta;
}
return s1.compareTo(s2);
}
});
Map<String,String> fqnToName = Maps.newHashMap();
int max = 0;
int count = 0;
for (String fqn : annotations) {
String name = fqn.substring(fqn.lastIndexOf('.') + 1);
fqnToName.put(fqn, name);
max = Math.max(max, name.length());
count += stats.get(fqn);
}
StringBuilder sb = new StringBuilder(200);
sb.append("Extracted ").append(count).append(" Annotations:");
for (String fqn : annotations) {
sb.append('\n');
String name = fqnToName.get(fqn);
for (int i = 0, n = max - name.length() + 1; i < n; i++) {
sb.append(' ');
}
sb.append('@');
sb.append(name);
sb.append(':').append(' ');
sb.append(Integer.toString(stats.get(fqn)));
}
if (sb.length() > 0) {
info(sb.toString());
}
}
if (filteredCount > 0) {
info(filteredCount + " of these were filtered out (not in API database file)");
}
if (mergedCount > 0) {
info(mergedCount + " additional annotations were merged in");
}
}
@SuppressWarnings("UseOfSystemOutOrSystemErr")
void info(final String message) {
if (displayInfo) {
System.out.println(message);
}
}
@SuppressWarnings("UseOfSystemOutOrSystemErr")
static void error(String message) {
System.err.println("Error: " + message);
}
@SuppressWarnings("UseOfSystemOutOrSystemErr")
static void warning(String message) {
System.out.println("Warning: " + message);
}
private void analyze(CompilationUnitDeclaration unit) {
if (processedFiles.contains(unit)) {
// The code to process all roots seems to hit some of the same classes
// repeatedly... so filter these out manually
return;
}
processedFiles.add(unit);
AnnotationVisitor visitor = new AnnotationVisitor();
unit.traverse(visitor, unit.scope);
}
@Nullable
private static ClassScope findClassScope(Scope scope) {
while (scope != null) {
if (scope instanceof ClassScope) {
return (ClassScope)scope;
}
scope = scope.parent;
}
return null;
}
@Nullable
static String getFqn(@NonNull Annotation annotation) {
if (annotation.resolvedType != null) {
return new String(annotation.resolvedType.readableName());
}
return null;
}
@Nullable
private static String getFqn(@NonNull ClassScope scope) {
TypeDeclaration typeDeclaration = scope.referenceType();
if (typeDeclaration != null && typeDeclaration.binding != null) {
return new String(typeDeclaration.binding.readableName());
}
return null;
}
@Nullable
private static String getFqn(@NonNull MethodScope scope) {
ClassScope classScope = findClassScope(scope);
if (classScope != null) {
return getFqn(classScope);
}
return null;
}
@Nullable
private static String getFqn(@NonNull BlockScope scope) {
ClassScope classScope = findClassScope(scope);
if (classScope != null) {
return getFqn(classScope);
}
return null;
}
boolean hasSourceRetention(@NonNull String fqn, @Nullable Annotation annotation) {
if (sourceRetention == null) {
sourceRetention = Maps.newHashMapWithExpectedSize(20);
// The @IntDef and @String annotations have always had source retention,
// and always must (because we can't express fully qualified field references
// in a .class file.)
sourceRetention.put(INT_DEF_ANNOTATION, true);
sourceRetention.put(STRING_DEF_ANNOTATION, true);
// The @Nullable and @NonNull annotations have always had class retention
sourceRetention.put(SUPPORT_NOTNULL, false);
sourceRetention.put(SUPPORT_NULLABLE, false);
// TODO: Look at support library statistics and put the other most
// frequently referenced annotations in here statically
// The resource annotations vary: up until 22.0.1 they had source
// retention but then switched to class retention.
}
Boolean source = sourceRetention.get(fqn);
if (source != null) {
return source;
}
if (annotation == null || annotation.type == null
|| annotation.type.resolvedType == null) {
// Assume it's class retention: that's what nearly all annotations
// currently are. (We do dynamic lookup of unknown ones to allow for
// this version of the Gradle plugin to be able to work on future
// versions of the support library with new annotations, where it's
// possible some annotations need to use source retention.
sourceRetention.put(fqn, false);
return false;
} else if (annotation.type.resolvedType.getAnnotations() != null) {
for (AnnotationBinding binding : annotation.type.resolvedType.getAnnotations()) {
if (hasSourceRetention(binding)) {
sourceRetention.put(fqn, true);
return true;
}
}
}
sourceRetention.put(fqn, false);
return false;
}
@SuppressWarnings("unused")
static boolean hasSourceRetention(@NonNull AnnotationBinding a) {
if (new String(a.getAnnotationType().readableName()).equals("java.lang.annotation.Retention")) {
ElementValuePair[] pairs = a.getElementValuePairs();
if (pairs == null || pairs.length != 1) {
warning("Expected exactly one parameter passed to @Retention");
return false;
}
ElementValuePair pair = pairs[0];
Object value = pair.getValue();
if (value instanceof FieldBinding) {
FieldBinding field = (FieldBinding) value;
if ("SOURCE".equals(new String(field.readableName()))) {
return true;
}
}
}
return false;
}
@SuppressWarnings("unused")
static boolean hasSourceRetention(@NonNull Annotation[] annotations) {
for (Annotation annotation : annotations) {
String typeName = Extractor.getFqn(annotation);
if ("java.lang.annotation.Retention".equals(typeName)) {
MemberValuePair[] pairs = annotation.memberValuePairs();
if (pairs == null || pairs.length != 1) {
warning("Expected exactly one parameter passed to @Retention");
return false;
}
MemberValuePair pair = pairs[0];
Expression value = pair.value;
if (value instanceof NameReference) {
NameReference reference = (NameReference) value;
Binding binding = reference.binding;
if (binding != null) {
if (binding instanceof FieldBinding) {
FieldBinding fb = (FieldBinding) binding;
if ("SOURCE".equals(new String(fb.name)) &&
"java.lang.annotation.RetentionPolicy".equals(
new String(fb.declaringClass.readableName()))) {
return true;
}
}
}
}
}
}
return false;
}
private void addAnnotations(@Nullable Annotation[] annotations, @NonNull Item item) {
if (annotations != null) {
for (Annotation annotation : annotations) {
if (isRelevantAnnotation(annotation)) {
AnnotationData annotationData = createAnnotation(annotation);
if (annotationData != null) {
item.annotations.add(annotationData);
}
}
}
}
}
@Nullable
private AnnotationData createAnnotation(@NonNull Annotation annotation) {
String fqn = getFqn(annotation);
if (fqn == null) {
return null;
}
if (fqn.equals(ANDROID_NULLABLE) || fqn.equals(SUPPORT_NULLABLE)) {
recordStats(fqn);
return new AnnotationData(SUPPORT_NULLABLE);
}
if (fqn.equals(ANDROID_NOTNULL) || fqn.equals(SUPPORT_NOTNULL)) {
recordStats(fqn);
return new AnnotationData(SUPPORT_NOTNULL);
}
if (fqn.startsWith(SUPPORT_ANNOTATIONS_PREFIX)
&& fqn.endsWith(RESOURCE_TYPE_ANNOTATIONS_SUFFIX)) {
recordStats(fqn);
return new AnnotationData(fqn);
} else if (fqn.startsWith(ANDROID_ANNOTATIONS_PREFIX)) {
// System annotations: translate to support library annotations
if (fqn.endsWith(RESOURCE_TYPE_ANNOTATIONS_SUFFIX)) {
// Translate e.g. android.annotation.DrawableRes to
// android.support.annotation.DrawableRes
String resAnnotation = SUPPORT_ANNOTATIONS_PREFIX +
fqn.substring(ANDROID_ANNOTATIONS_PREFIX.length());
if (!includeClassRetentionAnnotations
&& !hasSourceRetention(resAnnotation, null)) {
return null;
}
recordStats(resAnnotation);
return new AnnotationData(resAnnotation);
} else if (isRelevantFrameworkAnnotation(fqn)) {
// Translate other android.annotation annotations into corresponding
// support annotations
String supportAnnotation = SUPPORT_ANNOTATIONS_PREFIX +
fqn.substring(ANDROID_ANNOTATIONS_PREFIX.length());
if (!includeClassRetentionAnnotations
&& !hasSourceRetention(supportAnnotation, null)) {
return null;
}
recordStats(supportAnnotation);
return createData(supportAnnotation, annotation);
}
}
if (fqn.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) {
recordStats(fqn);
return createData(fqn, annotation);
}
if (isMagicConstant(fqn)) {
return types.get(fqn);
}
return null;
}
private void recordStats(String fqn) {
Integer count = stats.get(fqn);
if (count == null) {
count = 0;
}
stats.put(fqn, count + 1);
}
private boolean hasRelevantAnnotations(@Nullable Annotation[] annotations) {
if (annotations == null) {
return false;
}
for (Annotation annotation : annotations) {
if (isRelevantAnnotation(annotation)) {
return true;
}
}
return false;
}
private boolean isRelevantAnnotation(@NonNull Annotation annotation) {
String fqn = getFqn(annotation);
if (fqn == null || fqn.startsWith("java.lang.")) {
return false;
}
if (fqn.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) {
//noinspection PointlessBooleanExpression,ConstantConditions,RedundantIfStatement
if (!includeClassRetentionAnnotations && !hasSourceRetention(fqn, annotation)) {
return false;
}
//noinspection RedundantIfStatement
if (fqn.endsWith(".Keep")) {
// TODO: Extract into a proguard file
return false;
}
return true;
} else if (fqn.startsWith(ANDROID_ANNOTATIONS_PREFIX)) {
return isRelevantFrameworkAnnotation(fqn);
}
if (fqn.equals(ANDROID_NULLABLE) || fqn.equals(ANDROID_NOTNULL)
|| isMagicConstant(fqn)) {
return true;
} else if (fqn.equals(IDEA_CONTRACT)) {
return true;
}
return false;
}
private static boolean isRelevantFrameworkAnnotation(@NonNull String fqn) {
return fqn.startsWith(ANDROID_ANNOTATIONS_PREFIX)
&& !fqn.endsWith(".Widget")
&& !fqn.endsWith(".TargetApi")
&& !fqn.endsWith(".SystemApi")
&& !fqn.endsWith(".SuppressLint")
&& !fqn.endsWith(".SdkConstant");
}
boolean isMagicConstant(String typeName) {
if (irrelevantAnnotations.contains(typeName)
|| typeName.startsWith("java.lang.")) { // @Override, @SuppressWarnings, etc.
return false;
}
if (types.containsKey(typeName) ||
typeName.equals(INT_DEF_ANNOTATION) ||
typeName.equals(STRING_DEF_ANNOTATION) ||
typeName.equals(ANDROID_INT_DEF) ||
typeName.equals(ANDROID_STRING_DEF)) {
return true;
}
Annotation typeDef = typedefs.get(typeName);
// We only support a single level of IntDef type annotations, not arbitrary nesting
if (typeDef != null) {
String fqn = getFqn(typeDef);
if (fqn != null &&
(fqn.equals(INT_DEF_ANNOTATION) ||
fqn.equals(STRING_DEF_ANNOTATION) ||
fqn.equals(ANDROID_INT_DEF) ||
fqn.equals(ANDROID_STRING_DEF))) {
AnnotationData a = createAnnotation(typeDef);
if (a != null) {
types.put(typeName, a);
return true;
}
}
}
irrelevantAnnotations.add(typeName);
return false;
}
private boolean writeOutputFile(File dest) {
try {
FileOutputStream fileOutputStream = new FileOutputStream(dest);
JarOutputStream zos = new JarOutputStream(fileOutputStream);
try {
List<String> sortedPackages = new ArrayList<String>(itemMap.keySet());
Collections.sort(sortedPackages);
for (String pkg : sortedPackages) {
// Note: Using / rather than File.separator: jar lib requires it
String name = pkg.replace('.', '/') + "/annotations.xml";
JarEntry outEntry = new JarEntry(name);
zos.putNextEntry(outEntry);
StringWriter stringWriter = new StringWriter(1000);
PrintWriter writer = new PrintWriter(stringWriter);
try {
writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<root>");
Map<String, List<Item>> classMap = itemMap.get(pkg);
List<String> classes = new ArrayList<String>(classMap.keySet());
Collections.sort(classes);
for (String cls : classes) {
List<Item> items = classMap.get(cls);
Collections.sort(items);
for (Item item : items) {
item.write(writer);
}
}
writer.println("</root>\n");
writer.close();
String xml = stringWriter.toString();
// Validate
if (assertionsEnabled()) {
Document document = checkDocument(pkg, xml, false);
if (document == null) {
error("Could not parse XML document back in for entry " + name
+ ": invalid XML?\n\"\"\"\n" + xml + "\n\"\"\"\n");
return false;
}
}
byte[] bytes = xml.getBytes(Charsets.UTF_8);
zos.write(bytes);
zos.closeEntry();
} finally {
writer.close();
}
}
} finally {
zos.flush();
zos.close();
}
} catch (IOException ioe) {
error(ioe.toString());
return false;
}
return true;
}
private void addItem(@NonNull String fqn, @NonNull Item item) {
// Not part of the API?
if (apiFilter != null && item.isFiltered(apiFilter)) {
if (isListIgnored()) {
info("Skipping API because it is not part of the API file: " + item);
}
filteredCount++;
return;
}
String pkg = getPackage(fqn);
Map<String, List<Item>> classMap = itemMap.get(pkg);
if (classMap == null) {
classMap = Maps.newHashMapWithExpectedSize(100);
itemMap.put(pkg, classMap);
}
List<Item> items = classMap.get(fqn);
if (items == null) {
items = Lists.newArrayList();
classMap.put(fqn, items);
}
items.add(item);
}
private void removeItem(@NonNull String fqn, @NonNull Item item) {
String pkg = getPackage(fqn);
Map<String, List<Item>> classMap = itemMap.get(pkg);
if (classMap != null) {
List<Item> items = classMap.get(fqn);
if (items != null) {
items.remove(item);
if (items.isEmpty()) {
classMap.remove(fqn);
if (classMap.isEmpty()) {
itemMap.remove(pkg);
}
}
}
}
}
@Nullable
private Item findItem(@NonNull String fqn, @NonNull Item item) {
String pkg = getPackage(fqn);
Map<String, List<Item>> classMap = itemMap.get(pkg);
if (classMap == null) {
return null;
}
List<Item> items = classMap.get(fqn);
if (items == null) {
return null;
}
for (Item existing : items) {
if (existing.equals(item)) {
return existing;
}
}
return null;
}
@Nullable
private static Document checkDocument(@NonNull String pkg, @NonNull String xml,
boolean namespaceAware) {
try {
return XmlUtils.parseDocument(xml, namespaceAware);
} catch (SAXException sax) {
warning("Failed to parse document for package " + pkg + ": " + sax.toString());
} catch (Exception e) {
// pass
// This method is deliberately silent; will return null
}
return null;
}
public void mergeExisting(@NonNull File file) {
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null) {
for (File child : files) {
mergeExisting(child);
}
}
} else if (file.isFile()) {
if (file.getPath().endsWith(DOT_JAR)) {
mergeFromJar(file);
} else if (file.getPath().endsWith(DOT_XML)) {
try {
String xml = Files.toString(file, Charsets.UTF_8);
mergeAnnotationsXml(file.getPath(), xml);
} catch (IOException e) {
error("Aborting: I/O problem during transform: " + e.toString());
}
}
}
}
private void mergeFromJar(@NonNull File jar) {
// Reads in an existing annotations jar and merges in entries found there
// with the annotations analyzed from source.
JarInputStream zis = null;
try {
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
FileInputStream fis = new FileInputStream(jar);
zis = new JarInputStream(fis);
ZipEntry entry = zis.getNextEntry();
while (entry != null) {
if (entry.getName().endsWith(".xml")) {
byte[] bytes = ByteStreams.toByteArray(zis);
String xml = new String(bytes, Charsets.UTF_8);
mergeAnnotationsXml(jar.getPath() + ": " + entry, xml);
}
entry = zis.getNextEntry();
}
} catch (IOException e) {
error("Aborting: I/O problem during transform: " + e.toString());
} finally {
//noinspection deprecation
try {
Closeables.close(zis, true /* swallowIOException */);
} catch (IOException e) {
// cannot happen
}
}
}
private void mergeAnnotationsXml(@NonNull String path, @NonNull String xml) {
try {
Document document = XmlUtils.parseDocument(xml, false);
mergeDocument(document);
} catch (Exception e) {
warning("Failed to merge " + path + ": " + e.toString());
if (!(e instanceof IOException)) {
e.printStackTrace();
}
}
}
private void mergeDocument(@NonNull Document document) {
final Pattern XML_SIGNATURE = Pattern.compile(
// Class (FieldName | Type? Name(ArgList) Argnum?)
//"(\\S+) (\\S+|(.*)\\s+(\\S+)\\((.*)\\)( \\d+)?)");
"(\\S+) (\\S+|((.*)\\s+)?(\\S+)\\((.*)\\)( \\d+)?)");
Element root = document.getDocumentElement();
String rootTag = root.getTagName();
assert rootTag.equals("root") : rootTag;
for (Element item : getChildren(root)) {
String signature = item.getAttribute(ATTR_NAME);
if (signature == null || signature.equals("null")) {
continue; // malformed item
}
if (!hasRelevantAnnotations(item)) {
continue;
}
signature = unescapeXml(signature);
if (signature.equals("java.util.Arrays void sort(T[], java.util.Comparator<?) 0")) {
// Incorrect metadata (unbalanced <>'s)
// See IDEA-137385
signature = "java.util.Arrays void sort(T[], java.util.Comparator<?>) 0";
}
Matcher matcher = XML_SIGNATURE.matcher(signature);
if (matcher.matches()) {
String containingClass = matcher.group(1);
if (containingClass == null) {
warning("Could not find class for " + signature);
}
String methodName = matcher.group(5);
if (methodName != null) {
String type = matcher.group(4);
boolean isConstructor = type == null;
String parameters = matcher.group(6);
mergeMethodOrParameter(item, matcher, containingClass, methodName, type,
isConstructor, parameters);
} else {
String fieldName = matcher.group(2);
mergeField(item, containingClass, fieldName);
}
} else {
if (signature.indexOf(' ') != -1 || signature.indexOf('.') == -1) {
warning("No merge match for signature " + signature);
} // else: probably just a class signature, e.g. for @NonNls
}
}
}
@NonNull
private static String unescapeXml(@NonNull String escaped) {
String 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;
}
@NonNull
private static String escapeXml(@NonNull String unescaped) {
return XmlEscapers.xmlAttributeEscaper().escape(unescaped);
}
private void mergeField(Element item, String containingClass, String fieldName) {
if (apiFilter != null &&
!apiFilter.hasField(containingClass, fieldName)) {
if (isListIgnored()) {
info("Skipping imported element because it is not part of the API file: "
+ containingClass + "#" + fieldName);
}
filteredCount++;
} else {
FieldItem fieldItem = new FieldItem(containingClass, fieldName);
Item existing = findItem(containingClass, fieldItem);
if (existing != null) {
mergedCount += mergeAnnotations(item, existing);
} else {
addItem(containingClass, fieldItem);
mergedCount += addAnnotations(item, fieldItem);
}
}
}
private void mergeMethodOrParameter(Element item, Matcher matcher, String containingClass,
String methodName, String type, boolean constructor, String parameters) {
parameters = fixParameterString(parameters);
if (apiFilter != null &&
!apiFilter.hasMethod(containingClass, methodName, parameters)) {
if (isListIgnored()) {
info("Skipping imported element because it is not part of the API file: "
+ containingClass + "#" + methodName + "(" + parameters + ")");
}
filteredCount++;
return;
}
String argNum = matcher.group(7);
if (argNum != null) {
argNum = argNum.trim();
ParameterItem parameterItem = new ParameterItem(containingClass, type,
methodName, parameters, constructor, argNum);
Item existing = findItem(containingClass, parameterItem);
if ("java.util.Calendar".equals(containingClass) && "set".equals(methodName)
&& Integer.parseInt(argNum) > 0) {
// Skip the metadata for Calendar.set(int, int, int+); see
// https://code.google.com/p/android/issues/detail?id=73982
return;
}
if (existing != null) {
mergedCount += mergeAnnotations(item, existing);
} else {
addItem(containingClass, parameterItem);
mergedCount += addAnnotations(item, parameterItem);
}
} else {
MethodItem methodItem = new MethodItem(containingClass, type, methodName,
parameters, constructor);
Item existing = findItem(containingClass, methodItem);
if (existing != null) {
mergedCount += mergeAnnotations(item, existing);
} else {
addItem(containingClass, methodItem);
mergedCount += addAnnotations(item, methodItem);
}
}
}
// 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 static String fixParameterString(String parameters) {
return parameters.replace(" ", " ").replace(", ", ",");
}
private boolean hasRelevantAnnotations(Element item) {
for (Element annotationElement : getChildren(item)) {
if (isRelevantAnnotation(annotationElement)) {
return true;
}
}
return false;
}
private boolean isRelevantAnnotation(Element annotationElement) {
AnnotationData annotation = createAnnotation(annotationElement);
if (annotation == null) {
// Unsupported annotation in import
return false;
}
if (isNullable(annotation.name) || isNonNull(annotation.name)
|| annotation.name.startsWith(ANDROID_ANNOTATIONS_PREFIX)
|| annotation.name.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) {
return true;
} else if (annotation.name.equals(IDEA_CONTRACT)) {
return true;
} else if (annotation.name.equals(IDEA_NON_NLS)) {
return false;
} else {
if (!ignoredAnnotations.contains(annotation.name)) {
ignoredAnnotations.add(annotation.name);
if (isListIgnored()) {
info("(Ignoring merge annotation " + annotation.name + ")");
}
}
}
return false;
}
@NonNull
private static List<Element> getChildren(@NonNull Element element) {
NodeList itemList = element.getChildNodes();
int length = itemList.getLength();
List<Element> result = new ArrayList<Element>(Math.max(5, length / 2 + 1));
for (int i = 0; i < length; i++) {
Node node = itemList.item(i);
if (node.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
result.add((Element) node);
}
return result;
}
private int addAnnotations(Element itemElement, Item item) {
int count = 0;
for (Element annotationElement : getChildren(itemElement)) {
if (!isRelevantAnnotation(annotationElement)) {
continue;
}
AnnotationData annotation = createAnnotation(annotationElement);
item.annotations.add(annotation);
count++;
}
return count;
}
private int mergeAnnotations(Element itemElement, Item item) {
int count = 0;
loop:
for (Element annotationElement : getChildren(itemElement)) {
if (!isRelevantAnnotation(annotationElement)) {
continue;
}
AnnotationData annotation = createAnnotation(annotationElement);
if (annotation == null) {
continue;
}
boolean haveNullable = false;
boolean haveNotNull = false;
for (AnnotationData existing : item.annotations) {
if (isNonNull(existing.name)) {
haveNotNull = true;
}
if (isNullable(existing.name)) {
haveNullable = true;
}
if (existing.equals(annotation)) {
continue loop;
}
}
// Make sure we don't have a conflict between nullable and not nullable
if (isNonNull(annotation.name) && haveNullable ||
isNullable(annotation.name) && haveNotNull) {
warning("Found both @Nullable and @NonNull after import for " + item);
continue;
}
item.annotations.add(annotation);
count++;
}
return count;
}
private static boolean isNonNull(String name) {
return name.equals(IDEA_NOTNULL)
|| name.equals(ANDROID_NOTNULL)
|| name.equals(SUPPORT_NOTNULL);
}
private static boolean isNullable(String name) {
return name.equals(IDEA_NULLABLE)
|| name.equals(ANDROID_NULLABLE)
|| name.equals(SUPPORT_NULLABLE);
}
private AnnotationData createAnnotation(Element annotationElement) {
String tagName = annotationElement.getTagName();
assert tagName.equals("annotation") : tagName;
String name = annotationElement.getAttribute(ATTR_NAME);
assert name != null && !name.isEmpty();
AnnotationData annotation;
if (IDEA_MAGIC.equals(name)) {
List<Element> children = getChildren(annotationElement);
assert children.size() == 1 : children.size();
Element valueElement = children.get(0);
String valName = valueElement.getAttribute(ATTR_NAME);
String value = valueElement.getAttribute(ATTR_VAL);
boolean flagsFromClass = valName.equals("flagsFromClass");
boolean flag = valName.equals("flags") || flagsFromClass;
if (valName.equals("valuesFromClass") || flagsFromClass) {
// Not supported
boolean found = false;
if (value.endsWith(DOT_CLASS)) {
String clsName = value.substring(0, value.length() - DOT_CLASS.length());
StringBuilder sb = new StringBuilder();
sb.append('{');
Field[] reflectionFields = null;
try {
Class<?> cls = Class.forName(clsName);
reflectionFields = cls.getDeclaredFields();
} catch (Exception ignore) {
// Class not available: not a problem. We'll rely on API filter.
// It's mainly used for sorting anyway.
}
if (apiFilter != null) {
// Search in API database
Set<String> fields = apiFilter.getDeclaredIntFields(clsName);
if ("java.util.zip.ZipEntry".equals(clsName)) {
// The metadata says valuesFromClass ZipEntry, and unfortunately
// that class implements ZipConstants and therefore imports a large
// number of irrelevant constants that aren't valid here. Instead,
// only allow these two:
fields = Sets.newHashSet("STORED", "DEFLATED");
}
if (fields != null) {
List<String> sorted = Lists.newArrayList(fields);
Collections.sort(sorted);
if (reflectionFields != null) {
final Map<String,Integer> rank = Maps.newHashMap();
for (int i = 0, n = sorted.size(); i < n; i++) {
rank.put(sorted.get(i), reflectionFields.length + i);
}
for (int i = 0, n = reflectionFields.length; i < n; i++) {
rank.put(reflectionFields[i].getName(), i);
}
Collections.sort(sorted, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
int rank1 = rank.get(o1);
int rank2 = rank.get(o2);
int delta = rank1 - rank2;
if (delta != 0) {
return delta;
}
return o1.compareTo(o2);
}
});
}
boolean first = true;
for (String field : sorted) {
if (first) {
first = false;
} else {
sb.append(',').append(' ');
}
sb.append(clsName).append('.').append(field);
}
found = true;
}
}
// Attempt to sort in reflection order
if (!found && reflectionFields != null && (apiFilter == null || apiFilter.hasClass(clsName))) {
// Attempt with reflection
boolean first = true;
for (Field field : reflectionFields) {
if (field.getType() == Integer.TYPE ||
field.getType() == int.class) {
if (first) {
first = false;
} else {
sb.append(',').append(' ');
}
sb.append(clsName).append('.').append(field.getName());
}
}
}
sb.append('}');
value = sb.toString();
if (sb.length() > 2) { // 2: { }
found = true;
}
}
if (!found) {
return null;
}
}
//noinspection VariableNotUsedInsideIf
if (apiFilter != null) {
value = removeFiltered(value);
while (value.contains(", ,")) {
value = value.replace(", ,",",");
}
if (value.startsWith(", ")) {
value = value.substring(2);
}
}
annotation = new AnnotationData(
valName.equals("stringValues") ? STRING_DEF_ANNOTATION : INT_DEF_ANNOTATION,
TYPE_DEF_VALUE_ATTRIBUTE, value,
flag ? TYPE_DEF_FLAG_ATTRIBUTE : null, flag ? VALUE_TRUE : null);
} else if (STRING_DEF_ANNOTATION.equals(name) || ANDROID_STRING_DEF.equals(name) ||
INT_DEF_ANNOTATION.equals(name) || ANDROID_INT_DEF.equals(name)) {
List<Element> children = getChildren(annotationElement);
Element valueElement = children.get(0);
String valName = valueElement.getAttribute(ATTR_NAME);
assert TYPE_DEF_VALUE_ATTRIBUTE.equals(valName);
String value = valueElement.getAttribute(ATTR_VAL);
boolean flag = false;
if (children.size() == 2) {
valueElement = children.get(1);
assert TYPE_DEF_FLAG_ATTRIBUTE.equals(valueElement.getAttribute(ATTR_NAME));
flag = VALUE_TRUE.equals(valueElement.getAttribute(ATTR_VAL));
}
boolean intDef = INT_DEF_ANNOTATION.equals(name) || ANDROID_INT_DEF.equals(name);
annotation = new AnnotationData(
intDef ? INT_DEF_ANNOTATION : STRING_DEF_ANNOTATION,
TYPE_DEF_VALUE_ATTRIBUTE, value,
flag ? TYPE_DEF_FLAG_ATTRIBUTE : null, flag ? VALUE_TRUE : null);
} else if (IDEA_CONTRACT.equals(name)) {
List<Element> children = getChildren(annotationElement);
assert children.size() == 1 : children.size();
Element valueElement = children.get(0);
String value = valueElement.getAttribute(ATTR_VAL);
annotation = new AnnotationData(name, TYPE_DEF_VALUE_ATTRIBUTE, value, null, null);
} else if (isNonNull(name)) {
annotation = new AnnotationData(SUPPORT_NOTNULL);
} else if (isNullable(name)) {
//noinspection PointlessBooleanExpression,ConstantConditions
if (!INCLUDE_INFERRED_NULLABLE && IDEA_NULLABLE.equals(name)) {
return null;
}
annotation = new AnnotationData(SUPPORT_NULLABLE);
} else {
annotation = new AnnotationData(name, null, null);
}
return annotation;
}
private String removeFiltered(String value) {
assert apiFilter != null;
if (value.startsWith("{")) {
value = value.substring(1);
}
if (value.endsWith("}")) {
value = value.substring(0, value.length() - 1);
}
value = value.trim();
StringBuilder sb = new StringBuilder(value.length());
sb.append('{');
for (String fqn : Splitter.on(',').omitEmptyStrings().trimResults().split(value)) {
fqn = unescapeXml(fqn);
if (fqn.startsWith("\"")) {
continue;
}
int index = fqn.lastIndexOf('.');
String cls = fqn.substring(0, index);
String field = fqn.substring(index + 1);
if (apiFilter.hasField(cls, field)) {
if (sb.length() > 1) { // 0: '{'
sb.append(", ");
}
sb.append(fqn);
} else if (isListIgnored()) {
info("Skipping constant from typedef because it is not part of the SDK: " + fqn);
}
}
sb.append('}');
return escapeXml(sb.toString());
}
private static String getPackage(String fqn) {
// Extract package from the given fqn. Attempts to handle inner classes;
// e.g. "foo.bar.Foo.Bar will return "foo.bar".
int index = 0;
int last = 0;
while (true) {
index = fqn.indexOf('.', index);
if (index == -1) {
break;
}
last = index;
if (index < fqn.length() - 1) {
char next = fqn.charAt(index + 1);
if (Character.isUpperCase(next)) {
break;
}
}
index++;
}
return fqn.substring(0, last);
}
@SuppressWarnings("UnusedDeclaration")
public void setListIgnored(boolean listIgnored) {
this.listIgnored = listIgnored;
}
public boolean isListIgnored() {
return listIgnored;
}
public AnnotationData createData(@NonNull String name, @NonNull Annotation annotation) {
MemberValuePair[] pairs = annotation.memberValuePairs();
if (pairs == null || pairs.length == 0) {
return new AnnotationData(name);
}
return new AnnotationData(name, pairs);
}
private class AnnotationData {
@NonNull
public final String name;
@Nullable
public final String attributeName1;
@Nullable
public final String attributeValue1;
@Nullable
public final String attributeName2;
@Nullable
public final String attributeValue2;
@Nullable
public MemberValuePair[] attributes;
private AnnotationData(@NonNull String name) {
this(name, null, null, null, null);
}
private AnnotationData(@NonNull String name, @Nullable MemberValuePair[] pairs) {
this(name, null, null, null, null);
attributes = pairs;
assert attributes == null || attributes.length > 0;
}
private AnnotationData(@NonNull String name, @Nullable String attributeName,
@Nullable String attributeValue) {
this(name, attributeName, attributeValue, null, null);
}
private AnnotationData(@NonNull String name,
@Nullable String attributeName1, @Nullable String attributeValue1,
@Nullable String attributeName2, @Nullable String attributeValue2) {
this.name = name;
this.attributeName1 = attributeName1;
this.attributeValue1 = attributeValue1;
this.attributeName2 = attributeName2;
this.attributeValue2 = attributeValue2;
}
void write(PrintWriter writer) {
writer.print(" <annotation name=\"");
writer.print(name);
if (attributes != null) {
writer.print("\">");
writer.println();
//noinspection PointlessBooleanExpression,ConstantConditions
if (attributes.length > 1 && SORT_ANNOTATIONS) {
// Ensure that the value attribute is written first
Arrays.sort(attributes, new Comparator<MemberValuePair>() {
private String getName(MemberValuePair pair) {
if (pair.name == null) {
return ATTR_VALUE;
} else {
return new String(pair.name);
}
}
private int rank(MemberValuePair pair) {
return ATTR_VALUE.equals(getName(pair)) ? -1 : 0;
}
@Override
public int compare(MemberValuePair o1, MemberValuePair o2) {
int r1 = rank(o1);
int r2 = rank(o2);
int delta = r1 - r2;
if (delta != 0) {
return delta;
}
return getName(o1).compareTo(getName(o2));
}
});
}
for (MemberValuePair pair : attributes) {
writer.print(" <val name=\"");
if (pair.name != null) {
writer.print(pair.name);
} else {
writer.print(ATTR_VALUE); // default name
}
writer.print("\" val=\"");
writer.print(escapeXml(attributeString(pair.value)));
writer.println("\" />");
}
writer.println(" </annotation>");
} else if (attributeValue1 != null) {
writer.print("\">");
writer.println();
writer.print(" <val name=\"");
writer.print(attributeName1);
writer.print("\" val=\"");
writer.print(escapeXml(attributeValue1));
writer.println("\" />");
if (attributeValue2 != null) {
writer.print(" <val name=\"");
writer.print(attributeName2);
writer.print("\" val=\"");
writer.print(escapeXml(attributeValue2));
writer.println("\" />");
}
writer.println(" </annotation>");
} else {
writer.println("\" />");
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AnnotationData that = (AnnotationData) o;
return name.equals(that.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
private String attributeString(@NonNull Expression value) {
StringBuilder sb = new StringBuilder();
appendExpression(sb, value);
return sb.toString();
}
private boolean appendExpression(@NonNull StringBuilder sb,
@NonNull Expression expression) {
if (expression instanceof ArrayInitializer) {
sb.append('{');
ArrayInitializer initializer = (ArrayInitializer) expression;
boolean first = true;
int initialLength = sb.length();
for (Expression e : initializer.expressions) {
int length = sb.length();
if (first) {
first = false;
} else {
sb.append(", ");
}
boolean appended = appendExpression(sb, e);
if (!appended) {
// trunk off comma if it bailed for some reason (e.g. constant
// filtered out by API etc)
sb.setLength(length);
if (length == initialLength) {
first = true;
}
}
}
sb.append('}');
return true;
} else if (expression instanceof NameReference) {
NameReference reference = (NameReference) expression;
if (reference.binding != null) {
if (reference.binding instanceof FieldBinding) {
FieldBinding fb = (FieldBinding)reference.binding;
if (fb.declaringClass != null) {
if (apiFilter != null &&
!apiFilter.hasField(
new String(fb.declaringClass.readableName()),
new String(fb.name))) {
if (isListIgnored()) {
info("Filtering out typedef constant "
+ new String(fb.declaringClass.readableName()) + "."
+ new String(fb.name) + "");
}
return false;
}
sb.append(fb.declaringClass.readableName());
sb.append('.');
sb.append(fb.name);
} else {
sb.append(reference.binding.readableName());
}
} else {
sb.append(reference.binding.readableName());
}
return true;
} else {
warning("No binding for reference " + reference);
}
return false;
} else if (expression instanceof StringLiteral) {
StringLiteral s = (StringLiteral) expression;
sb.append('"');
sb.append(s.source());
sb.append('"');
return true;
} else if (expression instanceof NumberLiteral) {
NumberLiteral number = (NumberLiteral) expression;
sb.append(number.source());
return true;
} else if (expression instanceof TrueLiteral) {
sb.append(true);
return true;
} else if (expression instanceof FalseLiteral) {
sb.append(false);
return true;
} else if (expression instanceof org.eclipse.jdt.internal.compiler.ast.NullLiteral) {
sb.append("null");
return true;
} else {
// BinaryExpression etc can happen if you put "3 + 4" in as an integer!
if (expression.constant != null) {
if (expression.constant.typeID() == TypeIds.T_int) {
sb.append(expression.constant.intValue());
return true;
} else if (expression.constant.typeID() == TypeIds.T_JavaLangString) {
sb.append('"');
sb.append(expression.constant.stringValue());
sb.append('"');
return true;
} else {
warning("Unexpected type for constant " + expression.constant.toString());
}
} else {
warning("Unexpected annotation expression of type " + expression.getClass() + " and is "
+ expression);
}
}
return false;
}
}
/**
* An item in the XML file: this corresponds to a method, a field, or a method parameter, and
* has an associated set of annotations
*/
private abstract static class Item implements Comparable<Item> {
public final List<AnnotationData> annotations = Lists.newArrayList();
void write(PrintWriter writer) {
if (!isValid() || annotations.isEmpty()) {
return;
}
writer.print(" <item name=\"");
writer.print(getSignature());
writer.println("\">");
for (AnnotationData annotation : annotations) {
annotation.write(writer);
}
writer.print(" </item>");
writer.println();
}
abstract boolean isValid();
abstract boolean isFiltered(@NonNull ApiDatabase database);
abstract String getSignature();
@Override
public int compareTo(@SuppressWarnings("NullableProblems") @NonNull Item item) {
String signature1 = getSignature();
String signature2 = item.getSignature();
// IntelliJ's sorting order is not on the escaped HTML but the original
// signatures, which means android.os.AsyncTask<Params,Progress,Result>
// should appear *after* android.os.AsyncTask.Status, which when the <'s are
// escaped it does not
signature1 = signature1.replace('&', '.');
signature2 = signature2.replace('&', '.');
return signature1.compareTo(signature2);
}
}
private static class ClassItem extends Item {
@NonNull
public final String className;
private ClassItem(@NonNull String containingClass) {
this.className = containingClass;
}
@NonNull
static ClassItem create(@NonNull String classFqn) {
classFqn = ApiDatabase.getRawClass(classFqn);
return new ClassItem(classFqn);
}
@Override
boolean isValid() {
return true;
}
@Override
boolean isFiltered(@NonNull ApiDatabase database) {
return !database.hasClass(className);
}
@Override
String getSignature() {
return escapeXml(className);
}
@Override
public String toString() {
return "Class " + className;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ClassItem that = (ClassItem) o;
return className.equals(that.className);
}
@Override
public int hashCode() {
return className.hashCode();
}
}
private static class FieldItem extends Item {
@NonNull
public final String fieldName;
@NonNull
public final String containingClass;
private FieldItem(@NonNull String containingClass, @NonNull String fieldName) {
this.containingClass = containingClass;
this.fieldName = fieldName;
}
@Nullable
static FieldItem create(String classFqn, FieldBinding field) {
String name = new String(field.name);
return classFqn != null ? new FieldItem(classFqn, name) : null;
}
@Override
boolean isValid() {
return true;
}
@Override
boolean isFiltered(@NonNull ApiDatabase database) {
return !database.hasField(containingClass, fieldName);
}
@Override
String getSignature() {
return escapeXml(containingClass) + ' ' + fieldName;
}
@Override
public String toString() {
return "Field " + containingClass + "#" + fieldName;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FieldItem that = (FieldItem) o;
return containingClass.equals(that.containingClass) &&
fieldName.equals(that.fieldName);
}
@Override
public int hashCode() {
int result = fieldName.hashCode();
result = 31 * result + containingClass.hashCode();
return result;
}
}
private static class MethodItem extends Item {
@NonNull
public final String methodName;
@NonNull
public final String containingClass;
@NonNull
public final String parameterList;
@Nullable
public final String returnType;
public final boolean isConstructor;
private MethodItem(@NonNull String containingClass, @Nullable String returnType,
@NonNull String methodName, @NonNull String parameterList, boolean isConstructor) {
this.containingClass = containingClass;
this.returnType = returnType;
this.methodName = methodName;
this.parameterList = parameterList;
this.isConstructor = isConstructor;
}
@NonNull
public String getName() {
return methodName;
}
@Nullable
static MethodItem create(@Nullable String classFqn,
@NonNull AbstractMethodDeclaration declaration,
@Nullable MethodBinding binding) {
if (classFqn == null || binding == null) {
return null;
}
String returnType = getReturnType(binding);
String methodName = getMethodName(binding);
Argument[] arguments = declaration.arguments;
boolean isVarargs = arguments != null && arguments.length > 0 &&
arguments[arguments.length - 1].isVarArgs();
String parameterList = getParameterList(binding, isVarargs);
if (returnType == null || methodName == null) {
return null;
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (!INCLUDE_TYPE_ARGS) {
classFqn = ApiDatabase.getRawClass(classFqn);
methodName = ApiDatabase.getRawMethod(methodName);
}
return new MethodItem(classFqn, returnType,
methodName, parameterList,
binding.isConstructor());
}
@Override
boolean isValid() {
return true;
}
@Override
String getSignature() {
StringBuilder sb = new StringBuilder(100);
sb.append(escapeXml(containingClass));
sb.append(' ');
if (isConstructor) {
sb.append(escapeXml(methodName));
} else {
assert returnType != null;
sb.append(escapeXml(returnType));
sb.append(' ');
sb.append(escapeXml(methodName));
}
sb.append('(');
// The signature must match *exactly* the formatting used by IDEA,
// since it looks up external annotations in a map by this key.
// Therefore, it is vital that the parameter list uses exactly one
// space after each comma between parameters, and *no* spaces between
// generics variables, e.g. foo(Map<A,B>, int)
// Insert spaces between commas, but not in generics signatures
int balance = 0;
for (int i = 0, n = parameterList.length(); i < n; i++) {
char c = parameterList.charAt(i);
if (c == '<') {
balance++;
sb.append("&lt;");
} else if (c == '>') {
balance--;
sb.append("&gt;");
} else if (c == ',') {
sb.append(',');
if (balance == 0) {
sb.append(' ');
}
} else {
sb.append(c);
}
}
sb.append(')');
return sb.toString();
}
@Override
boolean isFiltered(@NonNull ApiDatabase database) {
return !database.hasMethod(containingClass, methodName, parameterList);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MethodItem that = (MethodItem) o;
return isConstructor == that.isConstructor && containingClass
.equals(that.containingClass) && methodName.equals(that.methodName)
&& parameterList.equals(that.parameterList);
}
@Override
public int hashCode() {
int result = methodName.hashCode();
result = 31 * result + containingClass.hashCode();
result = 31 * result + parameterList.hashCode();
result = 31 * result + (returnType != null ? returnType.hashCode() : 0);
result = 31 * result + (isConstructor ? 1 : 0);
return result;
}
@Override
public String toString() {
return "Method " + containingClass + "#" + methodName;
}
}
@Nullable
private static String getReturnType(MethodBinding binding) {
if (binding.returnType != null) {
return new String(binding.returnType.readableName());
} else if (binding.declaringClass != null) {
assert binding.isConstructor();
return new String(binding.declaringClass.readableName());
}
return null;
}
@Nullable
private static String getMethodName(@NonNull MethodBinding binding) {
if (binding.isConstructor()) {
if (binding.declaringClass != null) {
String classFqn = new String(binding.declaringClass.readableName());
return classFqn.substring(classFqn.lastIndexOf('.') + 1);
}
}
if (binding.selector != null) {
return new String(binding.selector);
}
assert binding.isConstructor();
return null;
}
@NonNull
private static String getParameterList(@NonNull MethodBinding binding, boolean isVarargs) {
// Create compact type signature (no spaces around commas or generics arguments)
StringBuilder sb = new StringBuilder();
TypeBinding[] typeParameters = binding.parameters;
if (typeParameters != null) {
for (int i = 0, n = typeParameters.length; i < n; i++) {
TypeBinding parameter = typeParameters[i];
if (i > 0) {
sb.append(',');
}
String str = fixParameterString(new String(parameter.readableName()));
if (isVarargs && i == n - 1 && str.endsWith("[]")) {
str = str.substring(0, str.length() - 2) + "...";
}
sb.append(str);
}
}
return sb.toString();
}
private static class ParameterItem extends MethodItem {
@NonNull
public String argIndex;
private ParameterItem(@NonNull String containingClass, @Nullable String returnType,
@NonNull String methodName, @NonNull String parameterList, boolean isConstructor,
@NonNull String argIndex) {
super(containingClass, returnType, methodName, parameterList, isConstructor);
this.argIndex = argIndex;
}
@Nullable
static ParameterItem create(AbstractMethodDeclaration methodDeclaration, Argument argument,
String classFqn, MethodBinding methodBinding,
LocalVariableBinding parameterBinding) {
if (classFqn == null || methodBinding == null || parameterBinding == null) {
return null;
}
String methodName = getMethodName(methodBinding);
Argument[] arguments = methodDeclaration.arguments;
boolean isVarargs = arguments != null && arguments.length > 0 &&
arguments[arguments.length - 1].isVarArgs();
String parameterList = getParameterList(methodBinding, isVarargs);
String returnType = getReturnType(methodBinding);
if (methodName == null || returnType == null) {
return null;
}
int index = 0;
boolean found = false;
if (methodDeclaration.arguments != null) {
for (Argument a : methodDeclaration.arguments) {
if (a == argument) {
found = true;
break;
}
index++;
}
}
if (!found) {
return null;
}
String argNum = Integer.toString(index);
//noinspection PointlessBooleanExpression,ConstantConditions
if (!INCLUDE_TYPE_ARGS) {
classFqn = ApiDatabase.getRawClass(classFqn);
methodName = ApiDatabase.getRawMethod(methodName);
}
return new ParameterItem(classFqn, returnType, methodName, parameterList,
methodBinding.isConstructor(), argNum);
}
@Override
String getSignature() {
return super.getSignature() + ' ' + argIndex;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
ParameterItem that = (ParameterItem) o;
return argIndex.equals(that.argIndex);
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + argIndex.hashCode();
return result;
}
@Override
public String toString() {
return "Parameter #" + argIndex + " in " + super.toString();
}
}
class AnnotationVisitor extends ASTVisitor {
@Override
public boolean visit(Argument argument, BlockScope scope) {
Annotation[] annotations = argument.annotations;
if (hasRelevantAnnotations(annotations)) {
ReferenceContext referenceContext = scope.referenceContext();
if (referenceContext instanceof AbstractMethodDeclaration) {
MethodBinding binding = ((AbstractMethodDeclaration) referenceContext).binding;
String fqn = getFqn(scope);
Item item = ParameterItem.create(
(AbstractMethodDeclaration) referenceContext, argument, fqn,
binding, argument.binding);
if (item != null) {
addItem(fqn, item);
addAnnotations(annotations, item);
}
}
}
return false;
}
@Override
public boolean visit(ConstructorDeclaration constructorDeclaration, ClassScope scope) {
Annotation[] annotations = constructorDeclaration.annotations;
if (hasRelevantAnnotations(annotations)) {
MethodBinding constructorBinding = constructorDeclaration.binding;
if (constructorBinding == null) {
return false;
}
String fqn = getFqn(scope);
Item item = MethodItem.create(fqn, constructorDeclaration, constructorBinding);
if (item != null) {
addItem(fqn, item);
addAnnotations(annotations, item);
}
}
Argument[] arguments = constructorDeclaration.arguments;
if (arguments != null) {
for (Argument argument : arguments) {
argument.traverse(this, constructorDeclaration.scope);
}
}
return false;
}
@Override
public boolean visit(FieldDeclaration fieldDeclaration, MethodScope scope) {
Annotation[] annotations = fieldDeclaration.annotations;
if (hasRelevantAnnotations(annotations)) {
FieldBinding fieldBinding = fieldDeclaration.binding;
if (fieldBinding == null) {
return false;
}
String fqn = getFqn(scope);
Item item = FieldItem.create(fqn, fieldBinding);
if (item != null) {
addItem(fqn, item);
addAnnotations(annotations, item);
}
}
return false;
}
@Override
public boolean visit(MethodDeclaration methodDeclaration, ClassScope scope) {
Annotation[] annotations = methodDeclaration.annotations;
if (hasRelevantAnnotations(annotations)) {
MethodBinding methodBinding = methodDeclaration.binding;
if (methodBinding == null) {
return false;
}
String fqn = getFqn(scope);
MethodItem item = MethodItem.create(fqn, methodDeclaration,
methodDeclaration.binding);
if (item != null) {
addItem(fqn, item);
// Deliberately skip findViewById()'s return nullability
// for now; it's true that findViewById can return null,
// but that means all code which does findViewById(R.id.foo).something()
// will be flagged as potentially throwing an NPE, and many developers
// will do this when they *know* that the id exists (in which case
// the method won't return null.)
boolean skipReturnAnnotations = false;
if ("findViewById".equals(item.getName())) {
skipReturnAnnotations = true;
if (item.annotations.isEmpty()) {
// No other annotations so far: just remove it
removeItem(fqn, item);
}
}
if (!skipReturnAnnotations) {
addAnnotations(annotations, item);
}
}
}
Argument[] arguments = methodDeclaration.arguments;
if (arguments != null) {
for (Argument argument : arguments) {
argument.traverse(this, methodDeclaration.scope);
}
}
return false;
}
@Override
public boolean visit(TypeDeclaration localTypeDeclaration, BlockScope scope) {
Annotation[] annotations = localTypeDeclaration.annotations;
if (hasRelevantAnnotations(annotations)) {
SourceTypeBinding binding = localTypeDeclaration.binding;
if (binding == null) {
return true;
}
String fqn = getFqn(scope);
if (fqn == null) {
fqn = new String(localTypeDeclaration.binding.readableName());
}
Item item = ClassItem.create(fqn);
addItem(fqn, item);
addAnnotations(annotations, item);
}
return true;
}
@Override
public boolean visit(TypeDeclaration memberTypeDeclaration, ClassScope scope) {
Annotation[] annotations = memberTypeDeclaration.annotations;
if (hasRelevantAnnotations(annotations)) {
SourceTypeBinding binding = memberTypeDeclaration.binding;
if (binding == null || !(binding instanceof MemberTypeBinding)) {
return true;
}
if (binding.isAnnotationType() || binding.isAnonymousType()) {
return false;
}
String fqn = new String(memberTypeDeclaration.binding.readableName());
Item item = ClassItem.create(fqn);
addItem(fqn, item);
addAnnotations(annotations, item);
}
return true;
}
@Override
public boolean visit(TypeDeclaration typeDeclaration, CompilationUnitScope scope) {
Annotation[] annotations = typeDeclaration.annotations;
if (hasRelevantAnnotations(annotations)) {
SourceTypeBinding binding = typeDeclaration.binding;
if (binding == null) {
return true;
}
String fqn = new String(typeDeclaration.binding.readableName());
Item item = ClassItem.create(fqn);
addItem(fqn, item);
addAnnotations(annotations, item);
}
return true;
}
}
}