• 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_FIELD = 0;
27   private static final int TYPE_METHOD = 1;
28   private static final int TYPE_PARAM = 2;
29   private static final int TYPE_RETURN = 3;
30 
31   @Override
classAuxTags(ClassInfo clazz)32   public TagInfo[] classAuxTags(ClassInfo clazz) {
33     if (hasSuppress(clazz.annotations())) return TagInfo.EMPTY_ARRAY;
34     ArrayList<TagInfo> tags = new ArrayList<>();
35     for (AnnotationInstanceInfo annotation : clazz.annotations()) {
36       // Document system services
37       if (annotation.type().qualifiedNameMatches("android", "annotation.SystemService")) {
38         ArrayList<TagInfo> valueTags = new ArrayList<>();
39         valueTags
40             .add(new ParsedTagInfo("", "",
41                 "{@link android.content.Context#getSystemService(Class)"
42                     + " Context.getSystemService(Class)}",
43                 null, SourcePositionInfo.UNKNOWN));
44         valueTags.add(new ParsedTagInfo("", "",
45             "{@code " + clazz.name() + ".class}", null,
46             SourcePositionInfo.UNKNOWN));
47 
48         ClassInfo contextClass = annotation.type().findClass("android.content.Context");
49         for (AnnotationValueInfo val : annotation.elementValues()) {
50           switch (val.element().name()) {
51             case "value":
52               final String expected = String.valueOf(val.value());
53               for (FieldInfo field : contextClass.fields()) {
54                 if (field.isHiddenOrRemoved()) continue;
55                 if (String.valueOf(field.constantValue()).equals(expected)) {
56                   valueTags.add(new ParsedTagInfo("", "",
57                       "{@link android.content.Context#getSystemService(String)"
58                           + " Context.getSystemService(String)}",
59                       null, SourcePositionInfo.UNKNOWN));
60                   valueTags.add(new ParsedTagInfo("", "",
61                       "{@link android.content.Context#" + field.name()
62                           + " Context." + field.name() + "}",
63                       null, SourcePositionInfo.UNKNOWN));
64                 }
65               }
66               break;
67           }
68         }
69 
70         Map<String, String> args = new HashMap<>();
71         tags.add(new AuxTagInfo("@service", "@service", SourcePositionInfo.UNKNOWN, args,
72             valueTags.toArray(TagInfo.getArray(valueTags.size()))));
73       }
74     }
75     return tags.toArray(TagInfo.getArray(tags.size()));
76   }
77 
78   @Override
fieldAuxTags(FieldInfo field)79   public TagInfo[] fieldAuxTags(FieldInfo field) {
80     if (hasSuppress(field)) return TagInfo.EMPTY_ARRAY;
81     return auxTags(TYPE_FIELD, field.annotations(), toString(field.inlineTags()));
82   }
83 
84   @Override
methodAuxTags(MethodInfo method)85   public TagInfo[] methodAuxTags(MethodInfo method) {
86     if (hasSuppress(method)) return TagInfo.EMPTY_ARRAY;
87     return auxTags(TYPE_METHOD, method.annotations(), toString(method.inlineTags().tags()));
88   }
89 
90   @Override
paramAuxTags(MethodInfo method, ParameterInfo param, String comment)91   public TagInfo[] paramAuxTags(MethodInfo method, ParameterInfo param, String comment) {
92     if (hasSuppress(method)) return TagInfo.EMPTY_ARRAY;
93     if (hasSuppress(param.annotations())) return TagInfo.EMPTY_ARRAY;
94     return auxTags(TYPE_PARAM, param.annotations(), new String[] { comment });
95   }
96 
97   @Override
returnAuxTags(MethodInfo method)98   public TagInfo[] returnAuxTags(MethodInfo method) {
99     if (hasSuppress(method)) return TagInfo.EMPTY_ARRAY;
100     return auxTags(TYPE_RETURN, method.annotations(), toString(method.returnTags().tags()));
101   }
102 
auxTags(int type, List<AnnotationInstanceInfo> annotations, String[] comment)103   private static TagInfo[] auxTags(int type, List<AnnotationInstanceInfo> annotations,
104       String[] comment) {
105     ArrayList<TagInfo> tags = new ArrayList<>();
106     for (AnnotationInstanceInfo annotation : annotations) {
107       // Ignore null-related annotations when docs already mention
108       if (annotation.type().qualifiedNameMatches("android", "annotation.NonNull")
109           || annotation.type().qualifiedNameMatches("android", "annotation.Nullable")) {
110         boolean mentionsNull = false;
111         for (String c : comment) {
112           mentionsNull |= Pattern.compile("\\bnull\\b").matcher(c).find();
113         }
114         if (mentionsNull) {
115           continue;
116         }
117       }
118 
119       // Blindly include docs requested by annotations
120       ParsedTagInfo[] docTags = ParsedTagInfo.EMPTY_ARRAY;
121       switch (type) {
122         case TYPE_METHOD:
123         case TYPE_FIELD:
124           docTags = annotation.type().comment().memberDocTags();
125           break;
126         case TYPE_PARAM:
127           docTags = annotation.type().comment().paramDocTags();
128           break;
129         case TYPE_RETURN:
130           docTags = annotation.type().comment().returnDocTags();
131           break;
132       }
133       for (ParsedTagInfo docTag : docTags) {
134         tags.add(docTag);
135       }
136 
137       // Document required permissions
138       if ((type == TYPE_METHOD || type == TYPE_FIELD)
139           && annotation.type().qualifiedNameMatches("android", "annotation.RequiresPermission")) {
140         ArrayList<AnnotationValueInfo> values = null;
141         boolean any = false;
142         for (AnnotationValueInfo val : annotation.elementValues()) {
143           switch (val.element().name()) {
144             case "value":
145               values = new ArrayList<AnnotationValueInfo>();
146               values.add(val);
147               break;
148             case "allOf":
149               values = (ArrayList<AnnotationValueInfo>) val.value();
150               break;
151             case "anyOf":
152               any = true;
153               values = (ArrayList<AnnotationValueInfo>) val.value();
154               break;
155           }
156         }
157         if (values == null || values.isEmpty()) continue;
158 
159         ClassInfo permClass = annotation.type().findClass("android.Manifest.permission");
160         ArrayList<TagInfo> valueTags = new ArrayList<>();
161         for (AnnotationValueInfo value : values) {
162           final String expected = String.valueOf(value.value());
163           for (FieldInfo field : permClass.fields()) {
164             if (field.isHiddenOrRemoved()) continue;
165             if (String.valueOf(field.constantValue()).equals(expected)) {
166               valueTags.add(new ParsedTagInfo("", "",
167                   "{@link " + permClass.qualifiedName() + "#" + field.name() + "}", null,
168                   SourcePositionInfo.UNKNOWN));
169             }
170           }
171         }
172 
173         Map<String, String> args = new HashMap<>();
174         if (any) args.put("any", "true");
175         tags.add(new AuxTagInfo("@permission", "@permission", SourcePositionInfo.UNKNOWN, args,
176             valueTags.toArray(TagInfo.getArray(valueTags.size()))));
177       }
178 
179       // The remaining annotations below always appear on return docs, and
180       // should not be included in the method body
181       if (type == TYPE_METHOD) continue;
182 
183       // Document value ranges
184       if (annotation.type().qualifiedNameMatches("android", "annotation.IntRange")
185           || annotation.type().qualifiedNameMatches("android", "annotation.FloatRange")) {
186         String from = null;
187         String to = null;
188         for (AnnotationValueInfo val : annotation.elementValues()) {
189           switch (val.element().name()) {
190             case "from": from = String.valueOf(val.value()); break;
191             case "to": to = String.valueOf(val.value()); break;
192           }
193         }
194         if (from != null || to != null) {
195           Map<String, String> args = new HashMap<>();
196           if (from != null) args.put("from", from);
197           if (to != null) args.put("to", to);
198           tags.add(new AuxTagInfo("@range", "@range", SourcePositionInfo.UNKNOWN, args,
199               TagInfo.EMPTY_ARRAY));
200         }
201       }
202 
203       // Document integer values
204       for (AnnotationInstanceInfo inner : annotation.type().annotations()) {
205         if (inner.type().qualifiedNameMatches("android", "annotation.IntDef")) {
206           ArrayList<AnnotationValueInfo> prefixes = null;
207           ArrayList<AnnotationValueInfo> values = null;
208           boolean flag = false;
209 
210           for (AnnotationValueInfo val : inner.elementValues()) {
211             switch (val.element().name()) {
212               case "prefix": prefixes = (ArrayList<AnnotationValueInfo>) val.value(); break;
213               case "value": values = (ArrayList<AnnotationValueInfo>) val.value(); break;
214               case "flag": flag = Boolean.parseBoolean(String.valueOf(val.value())); break;
215             }
216           }
217 
218           // Sadly we can only generate docs when told about a prefix
219           if (prefixes == null || prefixes.isEmpty()) continue;
220 
221           final ClassInfo clazz = annotation.type().containingClass();
222           final HashMap<String, FieldInfo> candidates = new HashMap<>();
223           for (FieldInfo field : clazz.fields()) {
224             if (field.isHiddenOrRemoved()) continue;
225             for (AnnotationValueInfo prefix : prefixes) {
226               if (field.name().startsWith(String.valueOf(prefix.value()))) {
227                 candidates.put(String.valueOf(field.constantValue()), field);
228               }
229             }
230           }
231 
232           ArrayList<TagInfo> valueTags = new ArrayList<>();
233           for (AnnotationValueInfo value : values) {
234             final String expected = String.valueOf(value.value());
235             final FieldInfo field = candidates.remove(expected);
236             if (field != null) {
237               valueTags.add(new ParsedTagInfo("", "",
238                   "{@link " + clazz.qualifiedName() + "#" + field.name() + "}", null,
239                   SourcePositionInfo.UNKNOWN));
240             }
241           }
242 
243           if (!valueTags.isEmpty()) {
244             Map<String, String> args = new HashMap<>();
245             if (flag) args.put("flag", "true");
246             tags.add(new AuxTagInfo("@intDef", "@intDef", SourcePositionInfo.UNKNOWN, args,
247                 valueTags.toArray(TagInfo.getArray(valueTags.size()))));
248           }
249         }
250       }
251     }
252     return tags.toArray(TagInfo.getArray(tags.size()));
253   }
254 
toString(TagInfo[] tags)255   private static String[] toString(TagInfo[] tags) {
256     final String[] res = new String[tags.length];
257     for (int i = 0; i < res.length; i++) {
258       res[i] = tags[i].text();
259     }
260     return res;
261   }
262 
hasSuppress(MemberInfo member)263   private static boolean hasSuppress(MemberInfo member) {
264     return hasSuppress(member.annotations())
265         || hasSuppress(member.containingClass().annotations());
266   }
267 
hasSuppress(List<AnnotationInstanceInfo> annotations)268   private static boolean hasSuppress(List<AnnotationInstanceInfo> annotations) {
269     for (AnnotationInstanceInfo annotation : annotations) {
270       if (annotation.type().qualifiedNameMatches("android", "annotation.SuppressAutoDoc")) {
271         return true;
272       }
273     }
274     return false;
275   }
276 }
277