1 /* 2 * Copyright (C) 2017 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.doclava; 18 19 import java.util.ArrayList; 20 import java.util.HashMap; 21 import java.util.List; 22 import java.util.Map; 23 import java.util.regex.Pattern; 24 25 public class AndroidAuxSource implements AuxSource { 26 private static final int TYPE_FIELD = 0; 27 private static final int TYPE_METHOD = 1; 28 private static final int TYPE_PARAM = 2; 29 private static final int TYPE_RETURN = 3; 30 31 @Override classAuxTags(ClassInfo clazz)32 public TagInfo[] classAuxTags(ClassInfo clazz) { 33 if (hasSuppress(clazz.annotations())) return TagInfo.EMPTY_ARRAY; 34 ArrayList<TagInfo> tags = new ArrayList<>(); 35 for (AnnotationInstanceInfo annotation : clazz.annotations()) { 36 // Document system services 37 if (annotation.type().qualifiedNameMatches("android", "annotation.SystemService")) { 38 ArrayList<TagInfo> valueTags = new ArrayList<>(); 39 valueTags 40 .add(new ParsedTagInfo("", "", 41 "{@link android.content.Context#getSystemService(Class)" 42 + " Context.getSystemService(Class)}", 43 null, SourcePositionInfo.UNKNOWN)); 44 valueTags.add(new ParsedTagInfo("", "", 45 "{@code " + clazz.name() + ".class}", null, 46 SourcePositionInfo.UNKNOWN)); 47 48 ClassInfo contextClass = annotation.type().findClass("android.content.Context"); 49 for (AnnotationValueInfo val : annotation.elementValues()) { 50 switch (val.element().name()) { 51 case "value": 52 final String expected = String.valueOf(val.value()); 53 for (FieldInfo field : contextClass.fields()) { 54 if (field.isHiddenOrRemoved()) continue; 55 if (String.valueOf(field.constantValue()).equals(expected)) { 56 valueTags.add(new ParsedTagInfo("", "", 57 "{@link android.content.Context#getSystemService(String)" 58 + " Context.getSystemService(String)}", 59 null, SourcePositionInfo.UNKNOWN)); 60 valueTags.add(new ParsedTagInfo("", "", 61 "{@link android.content.Context#" + field.name() 62 + " Context." + field.name() + "}", 63 null, SourcePositionInfo.UNKNOWN)); 64 } 65 } 66 break; 67 } 68 } 69 70 Map<String, String> args = new HashMap<>(); 71 tags.add(new AuxTagInfo("@service", "@service", SourcePositionInfo.UNKNOWN, args, 72 valueTags.toArray(TagInfo.getArray(valueTags.size())))); 73 } 74 } 75 return tags.toArray(TagInfo.getArray(tags.size())); 76 } 77 78 @Override fieldAuxTags(FieldInfo field)79 public TagInfo[] fieldAuxTags(FieldInfo field) { 80 if (hasSuppress(field)) return TagInfo.EMPTY_ARRAY; 81 return auxTags(TYPE_FIELD, field.annotations(), toString(field.inlineTags())); 82 } 83 84 @Override methodAuxTags(MethodInfo method)85 public TagInfo[] methodAuxTags(MethodInfo method) { 86 if (hasSuppress(method)) return TagInfo.EMPTY_ARRAY; 87 return auxTags(TYPE_METHOD, method.annotations(), toString(method.inlineTags().tags())); 88 } 89 90 @Override paramAuxTags(MethodInfo method, ParameterInfo param, String comment)91 public TagInfo[] paramAuxTags(MethodInfo method, ParameterInfo param, String comment) { 92 if (hasSuppress(method)) return TagInfo.EMPTY_ARRAY; 93 if (hasSuppress(param.annotations())) return TagInfo.EMPTY_ARRAY; 94 return auxTags(TYPE_PARAM, param.annotations(), new String[] { comment }); 95 } 96 97 @Override returnAuxTags(MethodInfo method)98 public TagInfo[] returnAuxTags(MethodInfo method) { 99 if (hasSuppress(method)) return TagInfo.EMPTY_ARRAY; 100 return auxTags(TYPE_RETURN, method.annotations(), toString(method.returnTags().tags())); 101 } 102 auxTags(int type, List<AnnotationInstanceInfo> annotations, String[] comment)103 private static TagInfo[] auxTags(int type, List<AnnotationInstanceInfo> annotations, 104 String[] comment) { 105 ArrayList<TagInfo> tags = new ArrayList<>(); 106 for (AnnotationInstanceInfo annotation : annotations) { 107 // Ignore null-related annotations when docs already mention 108 if (annotation.type().qualifiedNameMatches("android", "annotation.NonNull") 109 || annotation.type().qualifiedNameMatches("android", "annotation.Nullable")) { 110 boolean mentionsNull = false; 111 for (String c : comment) { 112 mentionsNull |= Pattern.compile("\\bnull\\b").matcher(c).find(); 113 } 114 if (mentionsNull) { 115 continue; 116 } 117 } 118 119 // Blindly include docs requested by annotations 120 ParsedTagInfo[] docTags = ParsedTagInfo.EMPTY_ARRAY; 121 switch (type) { 122 case TYPE_METHOD: 123 case TYPE_FIELD: 124 docTags = annotation.type().comment().memberDocTags(); 125 break; 126 case TYPE_PARAM: 127 docTags = annotation.type().comment().paramDocTags(); 128 break; 129 case TYPE_RETURN: 130 docTags = annotation.type().comment().returnDocTags(); 131 break; 132 } 133 for (ParsedTagInfo docTag : docTags) { 134 tags.add(docTag); 135 } 136 137 // Document required permissions 138 if ((type == TYPE_METHOD || type == TYPE_FIELD) 139 && annotation.type().qualifiedNameMatches("android", "annotation.RequiresPermission")) { 140 ArrayList<AnnotationValueInfo> values = null; 141 boolean any = false; 142 for (AnnotationValueInfo val : annotation.elementValues()) { 143 switch (val.element().name()) { 144 case "value": 145 values = new ArrayList<AnnotationValueInfo>(); 146 values.add(val); 147 break; 148 case "allOf": 149 values = (ArrayList<AnnotationValueInfo>) val.value(); 150 break; 151 case "anyOf": 152 any = true; 153 values = (ArrayList<AnnotationValueInfo>) val.value(); 154 break; 155 } 156 } 157 if (values == null || values.isEmpty()) continue; 158 159 ClassInfo permClass = annotation.type().findClass("android.Manifest.permission"); 160 ArrayList<TagInfo> valueTags = new ArrayList<>(); 161 for (AnnotationValueInfo value : values) { 162 final String expected = String.valueOf(value.value()); 163 for (FieldInfo field : permClass.fields()) { 164 if (field.isHiddenOrRemoved()) continue; 165 if (String.valueOf(field.constantValue()).equals(expected)) { 166 valueTags.add(new ParsedTagInfo("", "", 167 "{@link " + permClass.qualifiedName() + "#" + field.name() + "}", null, 168 SourcePositionInfo.UNKNOWN)); 169 } 170 } 171 } 172 173 Map<String, String> args = new HashMap<>(); 174 if (any) args.put("any", "true"); 175 tags.add(new AuxTagInfo("@permission", "@permission", SourcePositionInfo.UNKNOWN, args, 176 valueTags.toArray(TagInfo.getArray(valueTags.size())))); 177 } 178 179 // The remaining annotations below always appear on return docs, and 180 // should not be included in the method body 181 if (type == TYPE_METHOD) continue; 182 183 // Document value ranges 184 if (annotation.type().qualifiedNameMatches("android", "annotation.IntRange") 185 || annotation.type().qualifiedNameMatches("android", "annotation.FloatRange")) { 186 String from = null; 187 String to = null; 188 for (AnnotationValueInfo val : annotation.elementValues()) { 189 switch (val.element().name()) { 190 case "from": from = String.valueOf(val.value()); break; 191 case "to": to = String.valueOf(val.value()); break; 192 } 193 } 194 if (from != null || to != null) { 195 Map<String, String> args = new HashMap<>(); 196 if (from != null) args.put("from", from); 197 if (to != null) args.put("to", to); 198 tags.add(new AuxTagInfo("@range", "@range", SourcePositionInfo.UNKNOWN, args, 199 TagInfo.EMPTY_ARRAY)); 200 } 201 } 202 203 // Document integer values 204 for (AnnotationInstanceInfo inner : annotation.type().annotations()) { 205 if (inner.type().qualifiedNameMatches("android", "annotation.IntDef")) { 206 ArrayList<AnnotationValueInfo> prefixes = null; 207 ArrayList<AnnotationValueInfo> values = null; 208 boolean flag = false; 209 210 for (AnnotationValueInfo val : inner.elementValues()) { 211 switch (val.element().name()) { 212 case "prefix": prefixes = (ArrayList<AnnotationValueInfo>) val.value(); break; 213 case "value": values = (ArrayList<AnnotationValueInfo>) val.value(); break; 214 case "flag": flag = Boolean.parseBoolean(String.valueOf(val.value())); break; 215 } 216 } 217 218 // Sadly we can only generate docs when told about a prefix 219 if (prefixes == null || prefixes.isEmpty()) continue; 220 221 final ClassInfo clazz = annotation.type().containingClass(); 222 final HashMap<String, FieldInfo> candidates = new HashMap<>(); 223 for (FieldInfo field : clazz.fields()) { 224 if (field.isHiddenOrRemoved()) continue; 225 for (AnnotationValueInfo prefix : prefixes) { 226 if (field.name().startsWith(String.valueOf(prefix.value()))) { 227 candidates.put(String.valueOf(field.constantValue()), field); 228 } 229 } 230 } 231 232 ArrayList<TagInfo> valueTags = new ArrayList<>(); 233 for (AnnotationValueInfo value : values) { 234 final String expected = String.valueOf(value.value()); 235 final FieldInfo field = candidates.remove(expected); 236 if (field != null) { 237 valueTags.add(new ParsedTagInfo("", "", 238 "{@link " + clazz.qualifiedName() + "#" + field.name() + "}", null, 239 SourcePositionInfo.UNKNOWN)); 240 } 241 } 242 243 if (!valueTags.isEmpty()) { 244 Map<String, String> args = new HashMap<>(); 245 if (flag) args.put("flag", "true"); 246 tags.add(new AuxTagInfo("@intDef", "@intDef", SourcePositionInfo.UNKNOWN, args, 247 valueTags.toArray(TagInfo.getArray(valueTags.size())))); 248 } 249 } 250 } 251 } 252 return tags.toArray(TagInfo.getArray(tags.size())); 253 } 254 toString(TagInfo[] tags)255 private static String[] toString(TagInfo[] tags) { 256 final String[] res = new String[tags.length]; 257 for (int i = 0; i < res.length; i++) { 258 res[i] = tags[i].text(); 259 } 260 return res; 261 } 262 hasSuppress(MemberInfo member)263 private static boolean hasSuppress(MemberInfo member) { 264 return hasSuppress(member.annotations()) 265 || hasSuppress(member.containingClass().annotations()); 266 } 267 hasSuppress(List<AnnotationInstanceInfo> annotations)268 private static boolean hasSuppress(List<AnnotationInstanceInfo> annotations) { 269 for (AnnotationInstanceInfo annotation : annotations) { 270 if (annotation.type().qualifiedNameMatches("android", "annotation.SuppressAutoDoc")) { 271 return true; 272 } 273 } 274 return false; 275 } 276 } 277