Home | History | Annotate | Download | only in doclava
      1 /*
      2  * Copyright (C) 2010 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 com.google.clearsilver.jsilver.data.Data;
     20 import com.google.doclava.apicheck.ApiParseException;
     21 
     22 import java.util.ArrayList;
     23 import java.util.Comparator;
     24 import java.util.HashSet;
     25 import java.util.Objects;
     26 
     27 public class FieldInfo extends MemberInfo {
     28   public static final Comparator<FieldInfo> comparator = new Comparator<FieldInfo>() {
     29     public int compare(FieldInfo a, FieldInfo b) {
     30       return a.name().compareTo(b.name());
     31     }
     32   };
     33 
     34   public FieldInfo(String name, ClassInfo containingClass, ClassInfo realContainingClass,
     35       boolean isPublic, boolean isProtected, boolean isPackagePrivate, boolean isPrivate,
     36       boolean isFinal, boolean isStatic, boolean isTransient, boolean isVolatile,
     37       boolean isSynthetic, TypeInfo type, String rawCommentText, Object constantValue,
     38       SourcePositionInfo position, ArrayList<AnnotationInstanceInfo> annotations) {
     39     super(rawCommentText, name, null, containingClass, realContainingClass, isPublic, isProtected,
     40           isPackagePrivate, isPrivate, isFinal, isStatic, isSynthetic, chooseKind(isFinal, isStatic, constantValue),
     41         position, annotations);
     42     mIsTransient = isTransient;
     43     mIsVolatile = isVolatile;
     44     mType = type;
     45     mConstantValue = constantValue;
     46   }
     47 
     48   public FieldInfo cloneForClass(ClassInfo newContainingClass) {
     49     if (newContainingClass == containingClass()) {
     50       return this;
     51     }
     52     return new FieldInfo(name(), newContainingClass, realContainingClass(), isPublic(),
     53         isProtected(), isPackagePrivate(), isPrivate(), isFinal(), isStatic(), isTransient(),
     54         isVolatile(), isSynthetic(), mType, getRawCommentText(), mConstantValue, position(),
     55         annotations());
     56   }
     57 
     58   static String chooseKind(boolean isFinal, boolean isStatic, Object constantValue)
     59   {
     60     return isConstant(isFinal, isStatic, constantValue) ? "constant" : "field";
     61   }
     62 
     63   @Override
     64   public String toString() {
     65     return this.name();
     66   }
     67 
     68   @Override
     69   public boolean equals(Object o) {
     70     if (this == o) {
     71       return true;
     72     } else if (o instanceof FieldInfo) {
     73       final FieldInfo f = (FieldInfo) o;
     74       return mName.equals(f.mName);
     75     } else {
     76       return false;
     77     }
     78   }
     79 
     80   @Override
     81   public int hashCode() {
     82     return mName.hashCode();
     83   }
     84 
     85   public String qualifiedName() {
     86     String parentQName
     87         = (containingClass() != null) ? (containingClass().qualifiedName() + ".") : "";
     88     return parentQName + name();
     89   }
     90 
     91   public TypeInfo type() {
     92     return mType;
     93   }
     94 
     95   public HashSet<String> typeVariables() {
     96     HashSet<String> result = new HashSet<String>();
     97     ClassInfo cl = containingClass();
     98     while (cl != null) {
     99       ArrayList<TypeInfo> types = cl.asTypeInfo().typeArguments();
    100       if (types != null) {
    101         TypeInfo.typeVariables(types, result);
    102       }
    103       cl = cl.containingClass();
    104     }
    105     return result;
    106   }
    107 
    108   static boolean isConstant(boolean isFinal, boolean isStatic, Object constantValue)
    109   {
    110     /*
    111      * Note: There is an ambiguity in the doc API that prevents us
    112      * from distinguishing a constant-null from the lack of a
    113      * constant at all. We err on the side of treating all null
    114      * constantValues as meaning that the field is not a constant,
    115      * since having a static final field assigned to null is both
    116      * unusual and generally pretty useless.
    117      */
    118     return isFinal && isStatic && (constantValue != null);
    119   }
    120 
    121   public boolean isConstant() {
    122     return isConstant(isFinal(), isStatic(), mConstantValue);
    123   }
    124 
    125   public TagInfo[] firstSentenceTags() {
    126     return comment().briefTags();
    127   }
    128 
    129   public TagInfo[] inlineTags() {
    130     return comment().tags();
    131   }
    132 
    133   public Object constantValue() {
    134     return mConstantValue;
    135   }
    136 
    137   public String constantLiteralValue() {
    138     return constantLiteralValue(mConstantValue);
    139   }
    140 
    141   public void setDeprecated(boolean deprecated) {
    142     mDeprecatedKnown = true;
    143     mIsDeprecated = deprecated;
    144   }
    145 
    146   public boolean isDeprecated() {
    147     if (!mDeprecatedKnown) {
    148       boolean commentDeprecated = comment().isDeprecated();
    149       boolean annotationDeprecated = false;
    150       for (AnnotationInstanceInfo annotation : annotations()) {
    151         if (annotation.type().qualifiedName().equals("java.lang.Deprecated")) {
    152           annotationDeprecated = true;
    153           break;
    154         }
    155       }
    156 
    157       // Check to see that the JavaDoc contains @deprecated AND the method is marked as @Deprecated.
    158       // Otherwise, warn.
    159       // Note: We only do this for "included" classes (i.e. those we have source code for); we do
    160       // not have comments for classes from .class files but we do know whether a field is marked
    161       // as @Deprecated.
    162       if (mContainingClass.isIncluded() && !isHiddenOrRemoved()
    163           && commentDeprecated != annotationDeprecated) {
    164         Errors.error(Errors.DEPRECATION_MISMATCH, position(), "Field "
    165             + mContainingClass.qualifiedName() + "." + name()
    166             + ": @Deprecated annotation (" + (annotationDeprecated ? "" : "not ")
    167             + "present) and @deprecated doc tag (" + (commentDeprecated ? "" : "not ")
    168             + "present) do not match");
    169       }
    170 
    171       mIsDeprecated = commentDeprecated | annotationDeprecated;
    172       mDeprecatedKnown = true;
    173     }
    174     return mIsDeprecated;
    175   }
    176 
    177   public static String constantLiteralValue(Object val) {
    178     String str = null;
    179     if (val != null) {
    180       if (val instanceof Boolean || val instanceof Byte || val instanceof Short
    181           || val instanceof Integer) {
    182         str = val.toString();
    183       }
    184       // catch all special values
    185       else if (val instanceof Double) {
    186         str = canonicalizeFloatingPoint(val.toString(), "");
    187       } else if (val instanceof Float) {
    188         str = canonicalizeFloatingPoint(val.toString(), "f");
    189       } else if (val instanceof Long) {
    190         str = val.toString() + "L";
    191       } else if (val instanceof Character) {
    192         str = String.format("\'\\u%04x\'", val);
    193         System.out.println("str=" + str);
    194       } else if (val instanceof String) {
    195         str = "\"" + javaEscapeString((String) val) + "\"";
    196       } else {
    197         str = "<<<<" + val.toString() + ">>>>";
    198       }
    199     }
    200     if (str == null) {
    201       str = "null";
    202     }
    203     return str;
    204   }
    205 
    206   /**
    207    * Returns a canonical string representation of a floating point
    208    * number. The representation is suitable for use as Java source
    209    * code. This method also addresses bug #4428022 in the Sun JDK.
    210    */
    211   private static String canonicalizeFloatingPoint(String val, String suffix) {
    212     if (val.equals("Infinity")) {
    213       return "(1.0" + suffix + "/0.0" + suffix + ")";
    214     } else if (val.equals("-Infinity")) {
    215       return "(-1.0" + suffix + "/0.0" + suffix + ")";
    216     } else if (val.equals("NaN")) {
    217       return "(0.0" + suffix + "/0.0" + suffix + ")";
    218     }
    219 
    220     String str = val.toString();
    221     if (str.indexOf('E') != -1) {
    222       return str + suffix;
    223     }
    224 
    225     // 1.0 is the only case where a trailing "0" is allowed.
    226     // 1.00 is canonicalized as 1.0.
    227     int i = str.length() - 1;
    228     int d = str.indexOf('.');
    229     while (i >= d + 2 && str.charAt(i) == '0') {
    230       str = str.substring(0, i--);
    231     }
    232     return str + suffix;
    233   }
    234 
    235   public static String javaEscapeString(String str) {
    236     String result = "";
    237     final int N = str.length();
    238     for (int i = 0; i < N; i++) {
    239       char c = str.charAt(i);
    240       if (c == '\\') {
    241         result += "\\\\";
    242       } else if (c == '\t') {
    243         result += "\\t";
    244       } else if (c == '\b') {
    245         result += "\\b";
    246       } else if (c == '\r') {
    247         result += "\\r";
    248       } else if (c == '\n') {
    249         result += "\\n";
    250       } else if (c == '\f') {
    251         result += "\\f";
    252       } else if (c == '\'') {
    253         result += "\\'";
    254       } else if (c == '\"') {
    255         result += "\\\"";
    256       } else if (c >= ' ' && c <= '~') {
    257         result += c;
    258       } else {
    259         result += String.format("\\u%04x", new Integer((int) c));
    260       }
    261     }
    262     return result;
    263   }
    264 
    265   public static String javaUnescapeString(String str) throws ApiParseException {
    266     final int N = str.length();
    267     check: {
    268       for (int i=0; i<N; i++) {
    269         final char c = str.charAt(i);
    270         if (c == '\\') {
    271           break check;
    272         }
    273       }
    274       return str;
    275     }
    276 
    277     final StringBuilder buf = new StringBuilder(str.length());
    278     char escaped = 0;
    279     final int START = 0;
    280     final int CHAR1 = 1;
    281     final int CHAR2 = 2;
    282     final int CHAR3 = 3;
    283     final int CHAR4 = 4;
    284     final int ESCAPE = 5;
    285     int state = START;
    286 
    287     for (int i=0; i<N; i++) {
    288       final char c = str.charAt(i);
    289       switch (state) {
    290         case START:
    291           if (c == '\\') {
    292             state = ESCAPE;
    293           } else {
    294             buf.append(c);
    295           }
    296           break;
    297         case ESCAPE:
    298           switch (c) {
    299             case '\\':
    300               buf.append('\\');
    301               state = START;
    302               break;
    303             case 't':
    304               buf.append('\t');
    305               state = START;
    306               break;
    307             case 'b':
    308               buf.append('\b');
    309               state = START;
    310               break;
    311             case 'r':
    312               buf.append('\r');
    313               state = START;
    314               break;
    315             case 'n':
    316               buf.append('\n');
    317               state = START;
    318               break;
    319             case 'f':
    320               buf.append('\f');
    321               state = START;
    322               break;
    323             case '\'':
    324               buf.append('\'');
    325               state = START;
    326               break;
    327             case '\"':
    328               buf.append('\"');
    329               state = START;
    330               break;
    331             case 'u':
    332               state = CHAR1;
    333               escaped = 0;
    334               break;
    335           }
    336           break;
    337         case CHAR1:
    338         case CHAR2:
    339         case CHAR3:
    340         case CHAR4:
    341           escaped <<= 4;
    342           if (c >= '0' && c <= '9') {
    343             escaped |= c - '0';
    344           } else if (c >= 'a' && c <= 'f') {
    345             escaped |= 10 + (c - 'a');
    346           } else if (c >= 'A' && c <= 'F') {
    347             escaped |= 10 + (c - 'A');
    348           } else {
    349             throw new ApiParseException("bad escape sequence: '" + c + "' at pos " + i + " in: \""
    350                 + str + "\"");
    351           }
    352           if (state == CHAR4) {
    353             buf.append(escaped);
    354             state = START;
    355           } else {
    356             state++;
    357           }
    358           break;
    359       }
    360     }
    361     if (state != START) {
    362       throw new ApiParseException("unfinished escape sequence: " + str);
    363     }
    364     return buf.toString();
    365   }
    366 
    367   public void makeHDF(Data data, String base) {
    368     data.setValue(base + ".kind", kind());
    369     type().makeHDF(data, base + ".type");
    370     data.setValue(base + ".name", name());
    371     data.setValue(base + ".href", htmlPage());
    372     data.setValue(base + ".anchor", anchor());
    373     TagInfo.makeHDF(data, base + ".shortDescr", firstSentenceTags());
    374     TagInfo.makeHDF(data, base + ".descr", inlineTags());
    375     TagInfo.makeHDF(data, base + ".descrAux", Doclava.auxSource.fieldAuxTags(this));
    376     TagInfo.makeHDF(data, base + ".deprecated", comment().deprecatedTags());
    377     TagInfo.makeHDF(data, base + ".seeAlso", comment().seeTags());
    378     data.setValue(base + ".since", getSince());
    379     if (isDeprecated()) {
    380       data.setValue(base + ".deprecatedsince", getDeprecatedSince());
    381     }
    382     data.setValue(base + ".final", isFinal() ? "final" : "");
    383     data.setValue(base + ".static", isStatic() ? "static" : "");
    384     if (isPublic()) {
    385       data.setValue(base + ".scope", "public");
    386     } else if (isProtected()) {
    387       data.setValue(base + ".scope", "protected");
    388     } else if (isPackagePrivate()) {
    389       data.setValue(base + ".scope", "");
    390     } else if (isPrivate()) {
    391       data.setValue(base + ".scope", "private");
    392     }
    393     Object val = mConstantValue;
    394     if (val != null) {
    395       String dec = null;
    396       String hex = null;
    397       String str = null;
    398 
    399       if (val instanceof Boolean) {
    400         str = ((Boolean) val).toString();
    401       } else if (val instanceof Byte) {
    402         dec = String.format("%d", val);
    403         hex = String.format("0x%02x", val);
    404       } else if (val instanceof Character) {
    405         dec = String.format("\'%c\'", val);
    406         hex = String.format("0x%04x", val);
    407       } else if (val instanceof Double) {
    408         str = ((Double) val).toString();
    409       } else if (val instanceof Float) {
    410         str = ((Float) val).toString();
    411       } else if (val instanceof Integer) {
    412         dec = String.format("%d", val);
    413         hex = String.format("0x%08x", val);
    414       } else if (val instanceof Long) {
    415         dec = String.format("%d", val);
    416         hex = String.format("0x%016x", val);
    417       } else if (val instanceof Short) {
    418         dec = String.format("%d", val);
    419         hex = String.format("0x%04x", val);
    420       } else if (val instanceof String) {
    421         str = "\"" + ((String) val) + "\"";
    422       } else {
    423         str = "";
    424       }
    425 
    426       if (dec != null && hex != null) {
    427         data.setValue(base + ".constantValue.dec", Doclava.escape(dec));
    428         data.setValue(base + ".constantValue.hex", Doclava.escape(hex));
    429       } else {
    430         data.setValue(base + ".constantValue.str", Doclava.escape(str));
    431         data.setValue(base + ".constantValue.isString", "1");
    432       }
    433     }
    434 
    435     AnnotationInstanceInfo.makeLinkListHDF(
    436       data,
    437       base + ".showAnnotations",
    438       showAnnotations().toArray(new AnnotationInstanceInfo[showAnnotations().size()]));
    439 
    440     setFederatedReferences(data, base);
    441 
    442     Doclava.linter.lintField(this);
    443   }
    444 
    445   @Override
    446   public boolean isExecutable() {
    447     return false;
    448   }
    449 
    450   public boolean isTransient() {
    451     return mIsTransient;
    452   }
    453 
    454   public boolean isVolatile() {
    455     return mIsVolatile;
    456   }
    457 
    458   // Check the declared value with a typed comparison, not a string comparison,
    459   // to accommodate toolchains with different fp -> string conversions.
    460   private boolean valueEquals(FieldInfo other) {
    461     if ((mConstantValue == null) != (other.mConstantValue == null)) {
    462       return false;
    463     }
    464 
    465     // Null values are considered equal
    466     if (mConstantValue == null) {
    467       return true;
    468     }
    469 
    470     return mType.equals(other.mType)
    471         && mConstantValue.equals(other.mConstantValue);
    472   }
    473 
    474   public boolean isConsistent(FieldInfo fInfo) {
    475     boolean consistent = true;
    476     if (!mType.equals(fInfo.mType)) {
    477       Errors.error(Errors.CHANGED_TYPE, fInfo.position(), "Field " + fInfo.qualifiedName()
    478           + " has changed type from " + mType + " to " + fInfo.mType);
    479       consistent = false;
    480     } else if (!this.valueEquals(fInfo)) {
    481       Errors.error(Errors.CHANGED_VALUE, fInfo.position(), "Field " + fInfo.qualifiedName()
    482           + " has changed value from " + mConstantValue + " to " + fInfo.mConstantValue);
    483       consistent = false;
    484     }
    485 
    486     if (!scope().equals(fInfo.scope())) {
    487       Errors.error(Errors.CHANGED_SCOPE, fInfo.position(), "Method " + fInfo.qualifiedName()
    488           + " changed scope from " + this.scope() + " to " + fInfo.scope());
    489       consistent = false;
    490     }
    491 
    492     if (mIsStatic != fInfo.mIsStatic) {
    493       Errors.error(Errors.CHANGED_STATIC, fInfo.position(), "Field " + fInfo.qualifiedName()
    494           + " has changed 'static' qualifier");
    495       consistent = false;
    496     }
    497 
    498     if (!mIsFinal && fInfo.mIsFinal) {
    499       Errors.error(Errors.ADDED_FINAL, fInfo.position(), "Field " + fInfo.qualifiedName()
    500           + " has added 'final' qualifier");
    501       consistent = false;
    502     } else if (mIsFinal && !fInfo.mIsFinal) {
    503       Errors.error(Errors.REMOVED_FINAL, fInfo.position(), "Field " + fInfo.qualifiedName()
    504           + " has removed 'final' qualifier");
    505       consistent = false;
    506     }
    507 
    508     if (mIsTransient != fInfo.mIsTransient) {
    509       Errors.error(Errors.CHANGED_TRANSIENT, fInfo.position(), "Field " + fInfo.qualifiedName()
    510           + " has changed 'transient' qualifier");
    511       consistent = false;
    512     }
    513 
    514     if (mIsVolatile != fInfo.mIsVolatile) {
    515       Errors.error(Errors.CHANGED_VOLATILE, fInfo.position(), "Field " + fInfo.qualifiedName()
    516           + " has changed 'volatile' qualifier");
    517       consistent = false;
    518     }
    519 
    520     if (isDeprecated() != fInfo.isDeprecated()) {
    521       Errors.error(Errors.CHANGED_DEPRECATED, fInfo.position(), "Field " + fInfo.qualifiedName()
    522           + " has changed deprecation state " + isDeprecated() + " --> " + fInfo.isDeprecated());
    523       consistent = false;
    524     }
    525 
    526     return consistent;
    527   }
    528 
    529   public boolean hasValue() {
    530       return mHasValue;
    531   }
    532 
    533   public void setHasValue(boolean hasValue) {
    534       mHasValue = hasValue;
    535   }
    536 
    537   boolean mIsTransient;
    538   boolean mIsVolatile;
    539   boolean mDeprecatedKnown;
    540   boolean mIsDeprecated;
    541   boolean mHasValue;
    542   TypeInfo mType;
    543   Object mConstantValue;
    544 }
    545