Auto-generate docs based on annotations. am: d85b5c88af
am: 361cf77394

Change-Id: I1aeb80c6769094995055e9c5790ab9cccbd21432
diff --git a/res/assets/templates/macros.cs b/res/assets/templates/macros.cs
index 3c8b4bd..ade9d2e 100644
--- a/res/assets/templates/macros.cs
+++ b/res/assets/templates/macros.cs
@@ -168,6 +168,41 @@
   /each ?><?cs
 /def ?><?cs
 
+# Print output for aux tags that are not "standard" javadoc tags ?><?cs
+def:aux_tag_list(tags) ?><?cs
+  each:tag = tags ?><p><?cs
+      if:tag.kind == "@memberDoc" ?><?cs call:tag_list(tag.commentTags) ?><?cs
+      elif:tag.kind == "@paramDoc" ?><?cs call:tag_list(tag.commentTags) ?><?cs
+      elif:tag.kind == "@returnDoc" ?><?cs call:tag_list(tag.commentTags) ?><?cs
+      elif:tag.kind == "@range" ?><?cs call:dump_range(tag) ?><?cs
+      elif:tag.kind == "@intDef" ?><?cs call:dump_int_def(tag) ?><?cs
+      /if ?><?cs
+  /each ?></p><?cs
+/def ?><?cs
+
+# Print output for @range tags ?><?cs
+def:dump_range(tag) ?><?cs
+  if:tag.from && tag.to ?>Value is between <?cs var:tag.from ?> and <?cs var:tag.to ?> inclusive.<?cs
+  elif:tag.from ?>Value is <?cs var:tag.from ?> or greater.<?cs
+  elif:tag.to ?>Value is <?cs var:tag.to ?> or less.<?cs
+  /if ?><?cs
+/def ?><?cs
+
+# Print output for @intDef tags ?><?cs
+def:dump_int_def(tag) ?><?cs
+  if:tag.flag ?>Value is either <code>0</code> or combination of <?cs
+  else ?>Value is <?cs
+  /if ?><?cs
+  loop:i = #0, subcount(tag.values), #1 ?><?cs
+    with:val = tag.values[i] ?><?cs
+      call:tag_list(val.commentTags) ?><?cs
+      if i == subcount(tag.values) - 2 ?>, or <?cs
+      elif:i < subcount(tag.values) - 2 ?>, <?cs
+      /if ?><?cs
+    /with ?><?cs
+  /loop ?>.<?cs
+/def ?><?cs
+
 # Show the short-form description of something.  These come from shortDescr and deprecated ?><?cs
 def:short_descr(obj) ?><?cs
   if:subcount(obj.deprecated) ?><em><?cs
@@ -290,6 +325,7 @@
 def:description(obj) ?><?cs
   call:deprecated_warning(obj) ?>
   <p><?cs call:tag_list(obj.descr) ?></p><?cs
+  call:aux_tag_list(obj.descrAux) ?><?cs
   if:subcount(obj.annotationdocumentation)?><?cs
     each:annodoc=obj.annotationdocumentation ?>
     <div style="display:block"><?cs var:annodoc.text?></div><?cs
@@ -323,8 +359,9 @@
           <code><?cs var:param.kind ?></code><?cs
           if:string.find(param.comment.0.text, "<!--") != 0
             ?>:<?cs # Do not print if param comment is an HTML comment ?><?cs
-          /if ?>
-          <?cs call:tag_list(param.comment) ?></td>
+          /if ?><?cs
+          call:tag_list(param.comment) ?><?cs
+          call:aux_tag_list(param.commentAux) ?></td>
       </tr><?cs
     /each ?>
     </table><?cs
@@ -333,24 +370,17 @@
   # Print the @return value
   #
   ?><?cs
-  if:subcount(obj.returns) ?>
+  if:subcount(obj.returns) || (subcount(method.returnType) && method.returnType.label != 'void') ?>
     <table class="responsive">
       <tr><th colspan=2>Returns</th></tr>
       <tr>
         <td><code><?cs call:type_link(method.returnType) ?></code></td>
-        <td width="100%"><?cs call:tag_list(obj.returns) ?></td>
-      </tr>
-    </table><?cs
-  #
-  # If no return tag found, but there is a return type not 'void', print it
-  #
-  ?><?cs
-  elif:subcount(method.returnType) && method.returnType.label != 'void' ?>
-    <table class="responsive">
-      <tr><th colspan=2>Returns</th></tr>
-      <tr>
-        <td><code><?cs call:type_link(method.returnType) ?></code></td>
-        <td width="100%"><!-- no returns description in source --></td>
+        <td width="100%"><?cs
+        if:subcount(obj.returns) ?><?cs
+          call:tag_list(obj.returns) ?><?cs
+        else ?><!-- no returns description in source --><?cs
+        /if ?><?cs
+        call:aux_tag_list(obj.returnsAux) ?></td>
       </tr>
     </table><?cs
   /if ?><?cs
diff --git a/src/com/google/doclava/AuxUtils.java b/src/com/google/doclava/AuxUtils.java
new file mode 100644
index 0000000..ebed942
--- /dev/null
+++ b/src/com/google/doclava/AuxUtils.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.doclava;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class AuxUtils {
+  public static final int TYPE_METHOD = 0;
+  public static final int TYPE_FIELD = 1;
+  public static final int TYPE_PARAM = 2;
+  public static final int TYPE_RETURN = 3;
+
+  public static TagInfo[] tags(int type, List<AnnotationInstanceInfo> annotations) {
+    ArrayList<TagInfo> tags = new ArrayList<>();
+    for (AnnotationInstanceInfo annotation : annotations) {
+      if (annotation.type().qualifiedName().equals("android.annotation.SuppressAutoDoc")) {
+        return TagInfo.EMPTY_ARRAY;
+      }
+
+      ParsedTagInfo[] docTags = ParsedTagInfo.EMPTY_ARRAY;
+      switch (type) {
+        case TYPE_METHOD:
+        case TYPE_FIELD:
+          docTags = annotation.type().comment().memberDocTags();
+          break;
+        case TYPE_PARAM:
+          docTags = annotation.type().comment().paramDocTags();
+          break;
+        case TYPE_RETURN:
+          docTags = annotation.type().comment().returnDocTags();
+          break;
+      }
+      for (ParsedTagInfo docTag : docTags) {
+        tags.add(docTag);
+      }
+
+      // IntDef below belongs in return docs, not in method body
+      if (type == TYPE_METHOD) continue;
+
+      if (annotation.type().qualifiedName().equals("android.annotation.IntRange")
+          || annotation.type().qualifiedName().equals("android.annotation.FloatRange")) {
+        String from = null;
+        String to = null;
+        for (AnnotationValueInfo val : annotation.elementValues()) {
+          switch (val.element().name()) {
+            case "from": from = String.valueOf(val.value()); break;
+            case "to": to = String.valueOf(val.value()); break;
+          }
+        }
+        if (from != null || to != null) {
+          tags.add(new RangeTagInfo(SourcePositionInfo.UNKNOWN, from, to));
+        }
+      }
+
+      for (AnnotationInstanceInfo inner : annotation.type().annotations()) {
+        if (inner.type().qualifiedName().equals("android.annotation.IntDef")) {
+          ArrayList<AnnotationValueInfo> prefixes = null;
+          ArrayList<AnnotationValueInfo> values = null;
+          boolean flag = false;
+
+          for (AnnotationValueInfo val : inner.elementValues()) {
+            switch (val.element().name()) {
+              case "prefix": prefixes = (ArrayList<AnnotationValueInfo>) val.value(); break;
+              case "value": values = (ArrayList<AnnotationValueInfo>) val.value(); break;
+              case "flag": flag = Boolean.parseBoolean(String.valueOf(val.value())); break;
+            }
+          }
+
+          // Sadly we can only generate docs when told about a prefix
+          if (prefixes == null || prefixes.isEmpty()) continue;
+
+          final ClassInfo clazz = annotation.type().containingClass();
+          final HashMap<String, FieldInfo> candidates = new HashMap<>();
+          for (FieldInfo field : clazz.fields()) {
+            if (field.isHiddenOrRemoved())
+              continue;
+            for (AnnotationValueInfo prefix : prefixes) {
+              if (field.name().startsWith(String.valueOf(prefix.value()))) {
+                candidates.put(String.valueOf(field.constantValue()), field);
+              }
+            }
+          }
+
+          ArrayList<TagInfo> valueTags = new ArrayList<>();
+          for (AnnotationValueInfo value : values) {
+            final String expected = String.valueOf(value.value());
+            final FieldInfo field = candidates.get(expected);
+            if (field != null) {
+              valueTags.add(new ParsedTagInfo("", "",
+                  "{@link " + clazz.qualifiedName() + "#" + field.name() + "}", null,
+                  SourcePositionInfo.UNKNOWN));
+            }
+          }
+
+          if (!valueTags.isEmpty()) {
+            tags.add(new IntDefTagInfo(SourcePositionInfo.UNKNOWN, flag,
+                valueTags.toArray(TagInfo.getArray(valueTags.size()))));
+          }
+        }
+      }
+    }
+    return tags.toArray(TagInfo.getArray(tags.size()));
+  }
+}
diff --git a/src/com/google/doclava/Comment.java b/src/com/google/doclava/Comment.java
index 6abfaad..4ea6027 100644
--- a/src/com/google/doclava/Comment.java
+++ b/src/com/google/doclava/Comment.java
@@ -373,6 +373,12 @@
       mInlineTagsList.add(new SampleTagInfo(name, "@include", text, mBase, pos));
     } else if (name.equals("@apiNote") || name.equals("@implSpec") || name.equals("@implNote")) {
       mTagsList.add(new ParsedTagInfo(name, name, text, mBase, pos));
+    } else if (name.equals("@memberDoc")) {
+      mMemberDocTagsList.add(new ParsedTagInfo("@memberDoc", "@memberDoc", text, mBase, pos));
+    } else if (name.equals("@paramDoc")) {
+      mParamDocTagsList.add(new ParsedTagInfo("@paramDoc", "@paramDoc", text, mBase, pos));
+    } else if (name.equals("@returnDoc")) {
+      mReturnDocTagsList.add(new ParsedTagInfo("@returnDoc", "@returnDoc", text, mBase, pos));
     } else {
       boolean known = KNOWN_TAGS.contains(name);
       if (!known) {
@@ -486,6 +492,21 @@
     return mBriefTags;
   }
 
+  public ParsedTagInfo[] memberDocTags() {
+    init();
+    return mMemberDocTags;
+  }
+
+  public ParsedTagInfo[] paramDocTags() {
+    init();
+    return mParamDocTags;
+  }
+
+  public ParsedTagInfo[] returnDocTags() {
+    init();
+    return mReturnDocTags;
+  }
+
   public boolean isHidden() {
     if (mHidden == null) {
       mHidden = !Doclava.checkLevel(Doclava.SHOW_HIDDEN) &&
@@ -555,6 +576,9 @@
     mUndeprecateTags = mUndeprecateTagsList.toArray(TagInfo.getArray(mUndeprecateTagsList.size()));
     mAttrTags = mAttrTagsList.toArray(AttrTagInfo.getArray(mAttrTagsList.size()));
     mBriefTags = mBriefTagsList.toArray(TagInfo.getArray(mBriefTagsList.size()));
+    mMemberDocTags = mMemberDocTagsList.toArray(ParsedTagInfo.getArray(mMemberDocTagsList.size()));
+    mParamDocTags = mParamDocTagsList.toArray(ParsedTagInfo.getArray(mParamDocTagsList.size()));
+    mReturnDocTags = mReturnDocTagsList.toArray(ParsedTagInfo.getArray(mReturnDocTagsList.size()));
 
     mTagsList = null;
     mParamTagsList = null;
@@ -565,6 +589,9 @@
     mUndeprecateTagsList = null;
     mAttrTagsList = null;
     mBriefTagsList = null;
+    mMemberDocTagsList = null;
+    mParamDocTagsList = null;
+    mReturnDocTagsList = null;
   }
 
   boolean mInitialized;
@@ -587,6 +614,9 @@
   TagInfo[] mDeprecatedTags;
   TagInfo[] mUndeprecateTags;
   AttrTagInfo[] mAttrTags;
+  ParsedTagInfo[] mMemberDocTags;
+  ParsedTagInfo[] mParamDocTags;
+  ParsedTagInfo[] mReturnDocTags;
 
   ArrayList<TagInfo> mInlineTagsList = new ArrayList<TagInfo>();
   ArrayList<TagInfo> mTagsList = new ArrayList<TagInfo>();
@@ -598,6 +628,8 @@
   ArrayList<ParsedTagInfo> mDeprecatedTagsList = new ArrayList<ParsedTagInfo>();
   ArrayList<TagInfo> mUndeprecateTagsList = new ArrayList<TagInfo>();
   ArrayList<AttrTagInfo> mAttrTagsList = new ArrayList<AttrTagInfo>();
-
+  ArrayList<ParsedTagInfo> mMemberDocTagsList = new ArrayList<ParsedTagInfo>();
+  ArrayList<ParsedTagInfo> mParamDocTagsList = new ArrayList<ParsedTagInfo>();
+  ArrayList<ParsedTagInfo> mReturnDocTagsList = new ArrayList<ParsedTagInfo>();
 
 }
diff --git a/src/com/google/doclava/FieldInfo.java b/src/com/google/doclava/FieldInfo.java
index 85c1a85..3b8c676 100644
--- a/src/com/google/doclava/FieldInfo.java
+++ b/src/com/google/doclava/FieldInfo.java
@@ -16,10 +16,11 @@
 
 package com.google.doclava;
 
-import com.google.doclava.apicheck.ApiParseException;
 import com.google.clearsilver.jsilver.data.Data;
-import java.util.Comparator;
+import com.google.doclava.apicheck.ApiParseException;
+
 import java.util.ArrayList;
+import java.util.Comparator;
 
 public class FieldInfo extends MemberInfo {
   public static final Comparator<FieldInfo> comparator = new Comparator<FieldInfo>() {
@@ -330,6 +331,7 @@
     data.setValue(base + ".anchor", anchor());
     TagInfo.makeHDF(data, base + ".shortDescr", firstSentenceTags());
     TagInfo.makeHDF(data, base + ".descr", inlineTags());
+    TagInfo.makeHDF(data, base + ".descrAux", AuxUtils.tags(AuxUtils.TYPE_FIELD, annotations()));
     TagInfo.makeHDF(data, base + ".deprecated", comment().deprecatedTags());
     TagInfo.makeHDF(data, base + ".seeAlso", comment().seeTags());
     data.setValue(base + ".since", getSince());
diff --git a/src/com/google/doclava/IntDefTagInfo.java b/src/com/google/doclava/IntDefTagInfo.java
new file mode 100644
index 0000000..3044557
--- /dev/null
+++ b/src/com/google/doclava/IntDefTagInfo.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+public class IntDefTagInfo extends TagInfo {
+  public static final IntDefTagInfo[] EMPTY_ARRAY = new IntDefTagInfo[0];
+
+  public static IntDefTagInfo[] getArray(int size) {
+      return size == 0 ? EMPTY_ARRAY : new IntDefTagInfo[size];
+  }
+
+  private boolean mFlag;
+  private TagInfo[] mValues;
+
+  IntDefTagInfo(SourcePositionInfo position, boolean flag, TagInfo[] values) {
+    super("@intDef", "@intDef", "", position);
+    mFlag = flag;
+    mValues = values;
+  }
+
+  private TagInfo[] valuesTags() {
+    return mValues;
+  }
+
+  @Override
+  public void makeHDF(Data data, String base) {
+    super.makeHDF(data, base);
+    if (mFlag) {
+      data.setValue(base + ".flag", "true");
+    }
+    TagInfo.makeHDF(data, base + ".values", valuesTags());
+  }
+}
diff --git a/src/com/google/doclava/MethodInfo.java b/src/com/google/doclava/MethodInfo.java
index f45dff4..1e92dff 100644
--- a/src/com/google/doclava/MethodInfo.java
+++ b/src/com/google/doclava/MethodInfo.java
@@ -504,10 +504,13 @@
           }
         }
 
+        // Collect all docs requested by annotations
+        TagInfo[] auxTags = AuxUtils.tags(AuxUtils.TYPE_PARAM, param.annotations());
+
         // Okay, now add the collected parameter information to the method data
         mParamTags[i] =
             new ParamTagInfo("@param", type, name + " " + comment, parent(),
-                position);
+                position, auxTags);
 
         // while we're here, if we find any parameters that are still
         // undocumented at this point, complain. This warning is off by
@@ -613,6 +616,7 @@
 
     TagInfo.makeHDF(data, base + ".shortDescr", firstSentenceTags());
     TagInfo.makeHDF(data, base + ".descr", inlineTags());
+    TagInfo.makeHDF(data, base + ".descrAux", AuxUtils.tags(AuxUtils.TYPE_METHOD, annotations()));
     TagInfo.makeHDF(data, base + ".blockTags", blockTags());
     TagInfo.makeHDF(data, base + ".deprecated", deprecatedTags());
     TagInfo.makeHDF(data, base + ".seeAlso", seeTags());
@@ -631,6 +635,7 @@
       data.setValue(base + ".scope", "public");
     }
     TagInfo.makeHDF(data, base + ".returns", returnTags());
+    TagInfo.makeHDF(data, base + ".returnsAux", AuxUtils.tags(AuxUtils.TYPE_RETURN, annotations()));
 
     if (mTypeParameters != null) {
       TypeInfo.makeHDF(data, base + ".generic.typeArguments", mTypeParameters, false);
@@ -654,6 +659,7 @@
       showAnnotations().toArray(new AnnotationInstanceInfo[showAnnotations().size()]));
 
     setFederatedReferences(data, base);
+
   }
 
   public HashSet<String> typeVariables() {
diff --git a/src/com/google/doclava/ParamTagInfo.java b/src/com/google/doclava/ParamTagInfo.java
index d9353c5..c71ce64 100644
--- a/src/com/google/doclava/ParamTagInfo.java
+++ b/src/com/google/doclava/ParamTagInfo.java
@@ -18,8 +18,8 @@
 
 import com.google.clearsilver.jsilver.data.Data;
 
-import java.util.regex.Pattern;
 import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 public class ParamTagInfo extends ParsedTagInfo {
   public static final ParamTagInfo[] EMPTY_ARRAY = new ParamTagInfo[0];
@@ -33,9 +33,16 @@
   private boolean mIsTypeParameter;
   private String mParameterComment;
   private String mParameterName;
+  private TagInfo[] mAuxTags;
 
   ParamTagInfo(String name, String kind, String text, ContainerInfo base, SourcePositionInfo sp) {
+      this(name, kind, text, base, sp, null);
+  }
+
+  ParamTagInfo(String name, String kind, String text, ContainerInfo base, SourcePositionInfo sp,
+      TagInfo[] auxTags) {
     super(name, kind, text, base, sp);
+    mAuxTags = auxTags;
 
     Matcher m = PATTERN.matcher(text);
     if (m.matches()) {
@@ -72,12 +79,17 @@
     return mParameterName;
   }
 
+  public TagInfo[] auxTags() {
+    return mAuxTags;
+  }
+
   @Override
   public void makeHDF(Data data, String base) {
     data.setValue(base + ".name", parameterName());
     data.setValue(base + ".kind", kind());
     data.setValue(base + ".isTypeParameter", isTypeParameter() ? "1" : "0");
     TagInfo.makeHDF(data, base + ".comment", commentTags());
+    TagInfo.makeHDF(data, base + ".commentAux", auxTags());
   }
 
   public static void makeHDF(Data data, String base, ParamTagInfo[] tags) {
diff --git a/src/com/google/doclava/RangeTagInfo.java b/src/com/google/doclava/RangeTagInfo.java
new file mode 100644
index 0000000..8f3be30
--- /dev/null
+++ b/src/com/google/doclava/RangeTagInfo.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+
+public class RangeTagInfo extends TagInfo {
+  public static final RangeTagInfo[] EMPTY_ARRAY = new RangeTagInfo[0];
+
+  public static RangeTagInfo[] getArray(int size) {
+      return size == 0 ? EMPTY_ARRAY : new RangeTagInfo[size];
+  }
+
+  private String mFrom;
+  private String mTo;
+
+  RangeTagInfo(SourcePositionInfo position, String from, String to) {
+    super("@range", "@range", "", position);
+    mFrom = from;
+    mTo = to;
+  }
+
+  @Override
+  public void makeHDF(Data data, String base) {
+    super.makeHDF(data, base);
+    if (mFrom != null) {
+      data.setValue(base + ".from", mFrom);
+    }
+    if (mTo != null) {
+      data.setValue(base + ".to", mTo);
+    }
+  }
+}
diff --git a/src/com/google/doclava/TagInfo.java b/src/com/google/doclava/TagInfo.java
index 7bb640e..f321fa8 100644
--- a/src/com/google/doclava/TagInfo.java
+++ b/src/com/google/doclava/TagInfo.java
@@ -18,6 +18,8 @@
 
 import com.google.clearsilver.jsilver.data.Data;
 
+import java.util.Arrays;
+
 public class TagInfo {
   public static final TagInfo[] EMPTY_ARRAY = new TagInfo[0];
 
@@ -25,6 +27,18 @@
       return size == 0 ? EMPTY_ARRAY : new TagInfo[size];
   }
 
+  public static TagInfo[] append(TagInfo[] list, TagInfo item) {
+    if (item == null) {
+        return list;
+    } if (list == null) {
+      return new TagInfo[] { item };
+    } else {
+      list = Arrays.copyOf(list, list.length + 1);
+      list[list.length - 1] = item;
+      return list;
+    }
+  }
+
   private String mName;
   private String mText;
   private String mKind;