• 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         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