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_CLASS = 0; 27 private static final int TYPE_FIELD = 1; 28 private static final int TYPE_METHOD = 2; 29 private static final int TYPE_PARAM = 3; 30 private static final int TYPE_RETURN = 4; 31 32 @Override classAuxTags(ClassInfo clazz)33 public TagInfo[] classAuxTags(ClassInfo clazz) { 34 if (hasSuppress(clazz.annotations())) return TagInfo.EMPTY_ARRAY; 35 ArrayList<TagInfo> tags = new ArrayList<>(); 36 for (AnnotationInstanceInfo annotation : clazz.annotations()) { 37 // Document system services 38 if (annotation.type().qualifiedNameMatches("android", "annotation.SystemService")) { 39 ArrayList<TagInfo> valueTags = new ArrayList<>(); 40 valueTags 41 .add(new ParsedTagInfo("", "", 42 "{@link android.content.Context#getSystemService(Class)" 43 + " Context.getSystemService(Class)}", 44 null, SourcePositionInfo.UNKNOWN)); 45 valueTags.add(new ParsedTagInfo("", "", 46 "{@code " + clazz.name() + ".class}", null, 47 SourcePositionInfo.UNKNOWN)); 48 49 ClassInfo contextClass = annotation.type().findClass("android.content.Context"); 50 for (AnnotationValueInfo val : annotation.elementValues()) { 51 switch (val.element().name()) { 52 case "value": 53 final String expected = String.valueOf(val.value()); 54 for (FieldInfo field : contextClass.fields()) { 55 if (field.isHiddenOrRemoved()) continue; 56 if (String.valueOf(field.constantValue()).equals(expected)) { 57 valueTags.add(new ParsedTagInfo("", "", 58 "{@link android.content.Context#getSystemService(String)" 59 + " Context.getSystemService(String)}", 60 null, SourcePositionInfo.UNKNOWN)); 61 valueTags.add(new ParsedTagInfo("", "", 62 "{@link android.content.Context#" + field.name() 63 + " Context." + field.name() + "}", 64 null, SourcePositionInfo.UNKNOWN)); 65 } 66 } 67 break; 68 } 69 } 70 71 Map<String, String> args = new HashMap<>(); 72 tags.add(new AuxTagInfo("@service", "@service", SourcePositionInfo.UNKNOWN, args, 73 valueTags.toArray(TagInfo.getArray(valueTags.size())))); 74 } 75 } 76 auxTags(TYPE_CLASS, clazz.annotations(), toString(clazz.inlineTags()), tags); 77 return tags.toArray(TagInfo.getArray(tags.size())); 78 } 79 80 @Override fieldAuxTags(FieldInfo field)81 public TagInfo[] fieldAuxTags(FieldInfo field) { 82 if (hasSuppress(field)) return TagInfo.EMPTY_ARRAY; 83 return auxTags(TYPE_FIELD, field.annotations(), toString(field.inlineTags())); 84 } 85 86 @Override methodAuxTags(MethodInfo method)87 public TagInfo[] methodAuxTags(MethodInfo method) { 88 if (hasSuppress(method)) return TagInfo.EMPTY_ARRAY; 89 return auxTags(TYPE_METHOD, method.annotations(), toString(method.inlineTags().tags())); 90 } 91 92 @Override paramAuxTags(MethodInfo method, ParameterInfo param, String comment)93 public TagInfo[] paramAuxTags(MethodInfo method, ParameterInfo param, String comment) { 94 if (hasSuppress(method)) return TagInfo.EMPTY_ARRAY; 95 if (hasSuppress(param.annotations())) return TagInfo.EMPTY_ARRAY; 96 return auxTags(TYPE_PARAM, param.annotations(), new String[] { comment }); 97 } 98 99 @Override returnAuxTags(MethodInfo method)100 public TagInfo[] returnAuxTags(MethodInfo method) { 101 if (hasSuppress(method)) return TagInfo.EMPTY_ARRAY; 102 return auxTags(TYPE_RETURN, method.annotations(), toString(method.returnTags().tags())); 103 } 104 auxTags(int type, List<AnnotationInstanceInfo> annotations, String[] comment)105 private static TagInfo[] auxTags(int type, List<AnnotationInstanceInfo> annotations, 106 String[] comment) { 107 ArrayList<TagInfo> tags = new ArrayList<>(); 108 auxTags(type, annotations, comment, tags); 109 return tags.toArray(TagInfo.getArray(tags.size())); 110 } 111 auxTags(int type, List<AnnotationInstanceInfo> annotations, String[] comment, ArrayList<TagInfo> tags)112 private static void auxTags(int type, List<AnnotationInstanceInfo> annotations, 113 String[] comment, ArrayList<TagInfo> tags) { 114 for (AnnotationInstanceInfo annotation : annotations) { 115 // Ignore null-related annotations when docs already mention 116 if (annotation.type().qualifiedNameMatches("android", "annotation.NonNull") 117 || annotation.type().qualifiedNameMatches("android", "annotation.Nullable")) { 118 boolean mentionsNull = false; 119 for (String c : comment) { 120 mentionsNull |= Pattern.compile("\\bnull\\b").matcher(c).find(); 121 } 122 if (mentionsNull) { 123 continue; 124 } 125 } 126 127 // Blindly include docs requested by annotations 128 ParsedTagInfo[] docTags = ParsedTagInfo.EMPTY_ARRAY; 129 switch (type) { 130 case TYPE_METHOD: 131 case TYPE_FIELD: 132 case TYPE_CLASS: 133 docTags = annotation.type().comment().memberDocTags(); 134 break; 135 case TYPE_PARAM: 136 docTags = annotation.type().comment().paramDocTags(); 137 break; 138 case TYPE_RETURN: 139 docTags = annotation.type().comment().returnDocTags(); 140 break; 141 } 142 for (ParsedTagInfo docTag : docTags) { 143 tags.add(docTag); 144 } 145 146 // Document required permissions 147 if ((type == TYPE_CLASS || type == TYPE_METHOD || type == TYPE_FIELD) 148 && annotation.type().qualifiedNameMatches("android", "annotation.RequiresPermission")) { 149 ArrayList<AnnotationValueInfo> values = new ArrayList<>(); 150 boolean any = false; 151 for (AnnotationValueInfo val : annotation.elementValues()) { 152 switch (val.element().name()) { 153 case "value": 154 values.add(val); 155 break; 156 case "allOf": 157 values = (ArrayList<AnnotationValueInfo>) val.value(); 158 break; 159 case "anyOf": 160 any = true; 161 values = (ArrayList<AnnotationValueInfo>) val.value(); 162 break; 163 } 164 } 165 if (values.isEmpty()) continue; 166 167 ClassInfo permClass = annotation.type().findClass("android.Manifest.permission"); 168 ArrayList<TagInfo> valueTags = new ArrayList<>(); 169 for (AnnotationValueInfo value : values) { 170 final String expected = String.valueOf(value.value()); 171 for (FieldInfo field : permClass.fields()) { 172 if (field.isHiddenOrRemoved()) continue; 173 if (String.valueOf(field.constantValue()).equals(expected)) { 174 valueTags.add(new ParsedTagInfo("", "", 175 "{@link " + permClass.qualifiedName() + "#" + field.name() + "}", null, 176 SourcePositionInfo.UNKNOWN)); 177 } 178 } 179 } 180 181 Map<String, String> args = new HashMap<>(); 182 if (any) args.put("any", "true"); 183 tags.add(new AuxTagInfo("@permission", "@permission", SourcePositionInfo.UNKNOWN, args, 184 valueTags.toArray(TagInfo.getArray(valueTags.size())))); 185 } 186 187 // Document required features 188 if ((type == TYPE_CLASS || type == TYPE_METHOD || type == TYPE_FIELD) 189 && annotation.type().qualifiedNameMatches("android", "annotation.RequiresFeature")) { 190 ArrayList<AnnotationValueInfo> values = new ArrayList<>(); 191 boolean any = false; 192 for (AnnotationValueInfo val : annotation.elementValues()) { 193 switch (val.element().name()) { 194 case "value": 195 values.add(val); 196 break; 197 case "allOf": 198 values = (ArrayList<AnnotationValueInfo>) val.value(); 199 break; 200 case "anyOf": 201 any = true; 202 values = (ArrayList<AnnotationValueInfo>) val.value(); 203 break; 204 } 205 } 206 if (values.isEmpty()) continue; 207 208 ClassInfo pmClass = annotation.type().findClass("android.content.pm.PackageManager"); 209 ArrayList<TagInfo> valueTags = new ArrayList<>(); 210 for (AnnotationValueInfo value : values) { 211 final String expected = String.valueOf(value.value()); 212 for (FieldInfo field : pmClass.fields()) { 213 if (field.isHiddenOrRemoved()) continue; 214 if (String.valueOf(field.constantValue()).equals(expected)) { 215 valueTags.add(new ParsedTagInfo("", "", 216 "{@link " + pmClass.qualifiedName() + "#" + field.name() + "}", null, 217 SourcePositionInfo.UNKNOWN)); 218 } 219 } 220 } 221 222 valueTags.add(new ParsedTagInfo("", "", 223 "{@link android.content.pm.PackageManager#hasSystemFeature(String)" 224 + " PackageManager.hasSystemFeature(String)}", 225 null, SourcePositionInfo.UNKNOWN)); 226 227 Map<String, String> args = new HashMap<>(); 228 if (any) args.put("any", "true"); 229 tags.add(new AuxTagInfo("@feature", "@feature", SourcePositionInfo.UNKNOWN, args, 230 valueTags.toArray(TagInfo.getArray(valueTags.size())))); 231 } 232 233 // Document provider columns 234 if ((type == TYPE_FIELD) && annotation.type().qualifiedNameMatches("android", "Column")) { 235 String value = null; 236 boolean readOnly = false; 237 for (AnnotationValueInfo val : annotation.elementValues()) { 238 switch (val.element().name()) { 239 case "value": 240 value = String.valueOf(val.value()); 241 break; 242 case "readOnly": 243 readOnly = Boolean.parseBoolean(String.valueOf(val.value())); 244 break; 245 } 246 } 247 248 ArrayList<TagInfo> valueTags = new ArrayList<>(); 249 valueTags.add(new ParsedTagInfo("", "", 250 "{@link android.content.ContentProvider}", null, SourcePositionInfo.UNKNOWN)); 251 valueTags.add(new ParsedTagInfo("", "", 252 "{@link android.content.ContentValues}", null, SourcePositionInfo.UNKNOWN)); 253 valueTags.add(new ParsedTagInfo("", "", 254 "{@link android.database.Cursor}", null, SourcePositionInfo.UNKNOWN)); 255 256 ClassInfo cursorClass = annotation.type().findClass("android.database.Cursor"); 257 for (FieldInfo field : cursorClass.fields()) { 258 if (field.isHiddenOrRemoved()) continue; 259 if (String.valueOf(field.constantValue()).equals(value)) { 260 valueTags.add(new ParsedTagInfo("", "", 261 "{@link android.database.Cursor#" + field.name() + "}", 262 null, SourcePositionInfo.UNKNOWN)); 263 } 264 } 265 if (valueTags.size() < 4) continue; 266 267 Map<String, String> args = new HashMap<>(); 268 if (readOnly) args.put("readOnly", "true"); 269 tags.add(new AuxTagInfo("@column", "@column", SourcePositionInfo.UNKNOWN, args, 270 valueTags.toArray(TagInfo.getArray(valueTags.size())))); 271 } 272 273 // The remaining annotations below always appear on return docs, and 274 // should not be included in the method body 275 if (type == TYPE_METHOD) continue; 276 277 // Document value ranges 278 if (annotation.type().qualifiedNameMatches("android", "annotation.IntRange") 279 || annotation.type().qualifiedNameMatches("android", "annotation.FloatRange")) { 280 String from = null; 281 String to = null; 282 for (AnnotationValueInfo val : annotation.elementValues()) { 283 switch (val.element().name()) { 284 case "from": from = String.valueOf(val.value()); break; 285 case "to": to = String.valueOf(val.value()); break; 286 } 287 } 288 if (from != null || to != null) { 289 Map<String, String> args = new HashMap<>(); 290 if (from != null) args.put("from", from); 291 if (to != null) args.put("to", to); 292 tags.add(new AuxTagInfo("@range", "@range", SourcePositionInfo.UNKNOWN, args, 293 TagInfo.EMPTY_ARRAY)); 294 } 295 } 296 297 // Document integer values 298 for (AnnotationInstanceInfo inner : annotation.type().annotations()) { 299 boolean intDef = inner.type().qualifiedNameMatches("android", "annotation.IntDef"); 300 boolean stringDef = inner.type().qualifiedNameMatches("android", "annotation.StringDef"); 301 if (intDef || stringDef) { 302 ArrayList<AnnotationValueInfo> prefixes = null; 303 ArrayList<AnnotationValueInfo> suffixes = null; 304 ArrayList<AnnotationValueInfo> values = null; 305 final String kind = intDef ? "@intDef" : "@stringDef"; 306 boolean flag = false; 307 308 for (AnnotationValueInfo val : inner.elementValues()) { 309 switch (val.element().name()) { 310 case "prefix": prefixes = (ArrayList<AnnotationValueInfo>) val.value(); break; 311 case "suffix": suffixes = (ArrayList<AnnotationValueInfo>) val.value(); break; 312 case "value": values = (ArrayList<AnnotationValueInfo>) val.value(); break; 313 case "flag": flag = Boolean.parseBoolean(String.valueOf(val.value())); break; 314 } 315 } 316 317 // Sadly we can only generate docs when told about a prefix/suffix 318 if (prefixes == null) prefixes = new ArrayList<>(); 319 if (suffixes == null) suffixes = new ArrayList<>(); 320 if (prefixes.isEmpty() && suffixes.isEmpty()) continue; 321 322 final ClassInfo clazz = annotation.type().containingClass(); 323 final HashMap<String, FieldInfo> candidates = new HashMap<>(); 324 for (FieldInfo field : clazz.fields()) { 325 if (field.isHiddenOrRemoved()) continue; 326 for (AnnotationValueInfo prefix : prefixes) { 327 if (field.name().startsWith(String.valueOf(prefix.value()))) { 328 candidates.put(String.valueOf(field.constantValue()), field); 329 } 330 } 331 for (AnnotationValueInfo suffix : suffixes) { 332 if (field.name().endsWith(String.valueOf(suffix.value()))) { 333 candidates.put(String.valueOf(field.constantValue()), field); 334 } 335 } 336 } 337 338 ArrayList<TagInfo> valueTags = new ArrayList<>(); 339 for (AnnotationValueInfo value : values) { 340 final String expected = String.valueOf(value.value()); 341 final FieldInfo field = candidates.remove(expected); 342 if (field != null) { 343 valueTags.add(new ParsedTagInfo("", "", 344 "{@link " + clazz.qualifiedName() + "#" + field.name() + "}", null, 345 SourcePositionInfo.UNKNOWN)); 346 } 347 } 348 349 if (!valueTags.isEmpty()) { 350 Map<String, String> args = new HashMap<>(); 351 if (flag) args.put("flag", "true"); 352 tags.add(new AuxTagInfo(kind, kind, SourcePositionInfo.UNKNOWN, args, 353 valueTags.toArray(TagInfo.getArray(valueTags.size())))); 354 } 355 } 356 } 357 } 358 } 359 toString(TagInfo[] tags)360 private static String[] toString(TagInfo[] tags) { 361 final String[] res = new String[tags.length]; 362 for (int i = 0; i < res.length; i++) { 363 res[i] = tags[i].text(); 364 } 365 return res; 366 } 367 hasSuppress(MemberInfo member)368 private static boolean hasSuppress(MemberInfo member) { 369 return hasSuppress(member.annotations()) 370 || hasSuppress(member.containingClass().annotations()); 371 } 372 hasSuppress(List<AnnotationInstanceInfo> annotations)373 private static boolean hasSuppress(List<AnnotationInstanceInfo> annotations) { 374 for (AnnotationInstanceInfo annotation : annotations) { 375 if (annotation.type().qualifiedNameMatches("android", "annotation.SuppressAutoDoc")) { 376 return true; 377 } 378 } 379 return false; 380 } 381 } 382