Home | History | Annotate | Download | only in doclava
      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