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 AnnotationValueInfo value = null; 191 for (AnnotationValueInfo val : annotation.elementValues()) { 192 switch (val.element().name()) { 193 case "value": 194 value = val; 195 break; 196 } 197 } 198 if (value == null) continue; 199 200 ClassInfo pmClass = annotation.type().findClass("android.content.pm.PackageManager"); 201 ArrayList<TagInfo> valueTags = new ArrayList<>(); 202 final String expected = String.valueOf(value.value()); 203 for (FieldInfo field : pmClass.fields()) { 204 if (field.isHiddenOrRemoved()) continue; 205 if (String.valueOf(field.constantValue()).equals(expected)) { 206 valueTags.add(new ParsedTagInfo("", "", 207 "{@link " + pmClass.qualifiedName() + "#" + field.name() + "}", null, 208 SourcePositionInfo.UNKNOWN)); 209 } 210 } 211 212 valueTags.add(new ParsedTagInfo("", "", 213 "{@link android.content.pm.PackageManager#hasSystemFeature(String)" 214 + " PackageManager.hasSystemFeature(String)}", 215 null, SourcePositionInfo.UNKNOWN)); 216 217 Map<String, String> args = new HashMap<>(); 218 tags.add(new AuxTagInfo("@feature", "@feature", SourcePositionInfo.UNKNOWN, args, 219 valueTags.toArray(TagInfo.getArray(valueTags.size())))); 220 } 221 222 // Document provider columns 223 if ((type == TYPE_FIELD) && annotation.type().qualifiedNameMatches("android", "Column")) { 224 String value = null; 225 boolean readOnly = false; 226 for (AnnotationValueInfo val : annotation.elementValues()) { 227 switch (val.element().name()) { 228 case "value": 229 value = String.valueOf(val.value()); 230 break; 231 case "readOnly": 232 readOnly = Boolean.parseBoolean(String.valueOf(val.value())); 233 break; 234 } 235 } 236 237 ArrayList<TagInfo> valueTags = new ArrayList<>(); 238 valueTags.add(new ParsedTagInfo("", "", 239 "{@link android.content.ContentProvider}", null, SourcePositionInfo.UNKNOWN)); 240 valueTags.add(new ParsedTagInfo("", "", 241 "{@link android.content.ContentValues}", null, SourcePositionInfo.UNKNOWN)); 242 valueTags.add(new ParsedTagInfo("", "", 243 "{@link android.database.Cursor}", null, SourcePositionInfo.UNKNOWN)); 244 245 ClassInfo cursorClass = annotation.type().findClass("android.database.Cursor"); 246 for (FieldInfo field : cursorClass.fields()) { 247 if (field.isHiddenOrRemoved()) continue; 248 if (String.valueOf(field.constantValue()).equals(value)) { 249 valueTags.add(new ParsedTagInfo("", "", 250 "{@link android.database.Cursor#" + field.name() + "}", 251 null, SourcePositionInfo.UNKNOWN)); 252 } 253 } 254 if (valueTags.size() < 4) continue; 255 256 Map<String, String> args = new HashMap<>(); 257 if (readOnly) args.put("readOnly", "true"); 258 tags.add(new AuxTagInfo("@column", "@column", SourcePositionInfo.UNKNOWN, args, 259 valueTags.toArray(TagInfo.getArray(valueTags.size())))); 260 } 261 262 // The remaining annotations below always appear on return docs, and 263 // should not be included in the method body 264 if (type == TYPE_METHOD) continue; 265 266 // Document value ranges 267 if (annotation.type().qualifiedNameMatches("android", "annotation.IntRange") 268 || annotation.type().qualifiedNameMatches("android", "annotation.FloatRange")) { 269 String from = null; 270 String to = null; 271 for (AnnotationValueInfo val : annotation.elementValues()) { 272 switch (val.element().name()) { 273 case "from": from = String.valueOf(val.value()); break; 274 case "to": to = String.valueOf(val.value()); break; 275 } 276 } 277 if (from != null || to != null) { 278 Map<String, String> args = new HashMap<>(); 279 if (from != null) args.put("from", from); 280 if (to != null) args.put("to", to); 281 tags.add(new AuxTagInfo("@range", "@range", SourcePositionInfo.UNKNOWN, args, 282 TagInfo.EMPTY_ARRAY)); 283 } 284 } 285 286 // Document integer values 287 for (AnnotationInstanceInfo inner : annotation.type().annotations()) { 288 boolean intDef = inner.type().qualifiedNameMatches("android", "annotation.IntDef"); 289 boolean stringDef = inner.type().qualifiedNameMatches("android", "annotation.StringDef"); 290 if (intDef || stringDef) { 291 ArrayList<AnnotationValueInfo> prefixes = null; 292 ArrayList<AnnotationValueInfo> suffixes = null; 293 ArrayList<AnnotationValueInfo> values = null; 294 final String kind = intDef ? "@intDef" : "@stringDef"; 295 boolean flag = false; 296 297 for (AnnotationValueInfo val : inner.elementValues()) { 298 switch (val.element().name()) { 299 case "prefix": prefixes = (ArrayList<AnnotationValueInfo>) val.value(); break; 300 case "suffix": suffixes = (ArrayList<AnnotationValueInfo>) val.value(); break; 301 case "value": values = (ArrayList<AnnotationValueInfo>) val.value(); break; 302 case "flag": flag = Boolean.parseBoolean(String.valueOf(val.value())); break; 303 } 304 } 305 306 // Sadly we can only generate docs when told about a prefix/suffix 307 if (prefixes == null) prefixes = new ArrayList<>(); 308 if (suffixes == null) suffixes = new ArrayList<>(); 309 if (prefixes.isEmpty() && suffixes.isEmpty()) continue; 310 311 final ClassInfo clazz = annotation.type().containingClass(); 312 final HashMap<String, FieldInfo> candidates = new HashMap<>(); 313 for (FieldInfo field : clazz.fields()) { 314 if (field.isHiddenOrRemoved()) continue; 315 for (AnnotationValueInfo prefix : prefixes) { 316 if (field.name().startsWith(String.valueOf(prefix.value()))) { 317 candidates.put(String.valueOf(field.constantValue()), field); 318 } 319 } 320 for (AnnotationValueInfo suffix : suffixes) { 321 if (field.name().endsWith(String.valueOf(suffix.value()))) { 322 candidates.put(String.valueOf(field.constantValue()), field); 323 } 324 } 325 } 326 327 ArrayList<TagInfo> valueTags = new ArrayList<>(); 328 for (AnnotationValueInfo value : values) { 329 final String expected = String.valueOf(value.value()); 330 final FieldInfo field = candidates.remove(expected); 331 if (field != null) { 332 valueTags.add(new ParsedTagInfo("", "", 333 "{@link " + clazz.qualifiedName() + "#" + field.name() + "}", null, 334 SourcePositionInfo.UNKNOWN)); 335 } 336 } 337 338 if (!valueTags.isEmpty()) { 339 Map<String, String> args = new HashMap<>(); 340 if (flag) args.put("flag", "true"); 341 tags.add(new AuxTagInfo(kind, kind, SourcePositionInfo.UNKNOWN, args, 342 valueTags.toArray(TagInfo.getArray(valueTags.size())))); 343 } 344 } 345 } 346 } 347 } 348 toString(TagInfo[] tags)349 private static String[] toString(TagInfo[] tags) { 350 final String[] res = new String[tags.length]; 351 for (int i = 0; i < res.length; i++) { 352 res[i] = tags[i].text(); 353 } 354 return res; 355 } 356 hasSuppress(MemberInfo member)357 private static boolean hasSuppress(MemberInfo member) { 358 return hasSuppress(member.annotations()) 359 || hasSuppress(member.containingClass().annotations()); 360 } 361 hasSuppress(List<AnnotationInstanceInfo> annotations)362 private static boolean hasSuppress(List<AnnotationInstanceInfo> annotations) { 363 for (AnnotationInstanceInfo annotation : annotations) { 364 if (annotation.type().qualifiedNameMatches("android", "annotation.SuppressAutoDoc")) { 365 return true; 366 } 367 } 368 return false; 369 } 370 } 371