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 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 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 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 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 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 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 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 // The remaining annotations below always appear on return docs, and 223 // should not be included in the method body 224 if (type == TYPE_METHOD) continue; 225 226 // Document value ranges 227 if (annotation.type().qualifiedNameMatches("android", "annotation.IntRange") 228 || annotation.type().qualifiedNameMatches("android", "annotation.FloatRange")) { 229 String from = null; 230 String to = null; 231 for (AnnotationValueInfo val : annotation.elementValues()) { 232 switch (val.element().name()) { 233 case "from": from = String.valueOf(val.value()); break; 234 case "to": to = String.valueOf(val.value()); break; 235 } 236 } 237 if (from != null || to != null) { 238 Map<String, String> args = new HashMap<>(); 239 if (from != null) args.put("from", from); 240 if (to != null) args.put("to", to); 241 tags.add(new AuxTagInfo("@range", "@range", SourcePositionInfo.UNKNOWN, args, 242 TagInfo.EMPTY_ARRAY)); 243 } 244 } 245 246 // Document integer values 247 for (AnnotationInstanceInfo inner : annotation.type().annotations()) { 248 boolean intDef = inner.type().qualifiedNameMatches("android", "annotation.IntDef"); 249 boolean stringDef = inner.type().qualifiedNameMatches("android", "annotation.StringDef"); 250 if (intDef || stringDef) { 251 ArrayList<AnnotationValueInfo> prefixes = null; 252 ArrayList<AnnotationValueInfo> suffixes = null; 253 ArrayList<AnnotationValueInfo> values = null; 254 final String kind = intDef ? "@intDef" : "@stringDef"; 255 boolean flag = false; 256 257 for (AnnotationValueInfo val : inner.elementValues()) { 258 switch (val.element().name()) { 259 case "prefix": prefixes = (ArrayList<AnnotationValueInfo>) val.value(); break; 260 case "suffix": suffixes = (ArrayList<AnnotationValueInfo>) val.value(); break; 261 case "value": values = (ArrayList<AnnotationValueInfo>) val.value(); break; 262 case "flag": flag = Boolean.parseBoolean(String.valueOf(val.value())); break; 263 } 264 } 265 266 // Sadly we can only generate docs when told about a prefix/suffix 267 if (prefixes == null) prefixes = new ArrayList<>(); 268 if (suffixes == null) suffixes = new ArrayList<>(); 269 if (prefixes.isEmpty() && suffixes.isEmpty()) continue; 270 271 final ClassInfo clazz = annotation.type().containingClass(); 272 final HashMap<String, FieldInfo> candidates = new HashMap<>(); 273 for (FieldInfo field : clazz.fields()) { 274 if (field.isHiddenOrRemoved()) continue; 275 for (AnnotationValueInfo prefix : prefixes) { 276 if (field.name().startsWith(String.valueOf(prefix.value()))) { 277 candidates.put(String.valueOf(field.constantValue()), field); 278 } 279 } 280 for (AnnotationValueInfo suffix : suffixes) { 281 if (field.name().endsWith(String.valueOf(suffix.value()))) { 282 candidates.put(String.valueOf(field.constantValue()), field); 283 } 284 } 285 } 286 287 ArrayList<TagInfo> valueTags = new ArrayList<>(); 288 for (AnnotationValueInfo value : values) { 289 final String expected = String.valueOf(value.value()); 290 final FieldInfo field = candidates.remove(expected); 291 if (field != null) { 292 valueTags.add(new ParsedTagInfo("", "", 293 "{@link " + clazz.qualifiedName() + "#" + field.name() + "}", null, 294 SourcePositionInfo.UNKNOWN)); 295 } 296 } 297 298 if (!valueTags.isEmpty()) { 299 Map<String, String> args = new HashMap<>(); 300 if (flag) args.put("flag", "true"); 301 tags.add(new AuxTagInfo(kind, kind, SourcePositionInfo.UNKNOWN, args, 302 valueTags.toArray(TagInfo.getArray(valueTags.size())))); 303 } 304 } 305 } 306 } 307 } 308 309 private static String[] toString(TagInfo[] tags) { 310 final String[] res = new String[tags.length]; 311 for (int i = 0; i < res.length; i++) { 312 res[i] = tags[i].text(); 313 } 314 return res; 315 } 316 317 private static boolean hasSuppress(MemberInfo member) { 318 return hasSuppress(member.annotations()) 319 || hasSuppress(member.containingClass().annotations()); 320 } 321 322 private static boolean hasSuppress(List<AnnotationInstanceInfo> annotations) { 323 for (AnnotationInstanceInfo annotation : annotations) { 324 if (annotation.type().qualifiedNameMatches("android", "annotation.SuppressAutoDoc")) { 325 return true; 326 } 327 } 328 return false; 329 } 330 } 331