• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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