Home | History | Annotate | Download | only in apicheck
      1 /*
      2  * Copyright (C) 2011 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.apicheck;
     18 
     19 import com.google.doclava.AnnotationInstanceInfo;
     20 import com.google.doclava.ClassInfo;
     21 import com.google.doclava.Converter;
     22 import com.google.doclava.FieldInfo;
     23 import com.google.doclava.MethodInfo;
     24 import com.google.doclava.PackageInfo;
     25 import com.google.doclava.ParameterInfo;
     26 import com.google.doclava.SourcePositionInfo;
     27 import com.google.doclava.TypeInfo;
     28 
     29 import java.io.IOException;
     30 import java.io.InputStream;
     31 import java.util.ArrayList;
     32 import java.util.LinkedList;
     33 
     34 class ApiFile {
     35 
     36   public static ApiInfo parseApi(String filename, InputStream stream) throws ApiParseException {
     37     final int CHUNK = 1024*1024;
     38     int hint = 0;
     39     try {
     40       hint = stream.available() + CHUNK;
     41     } catch (IOException ex) {
     42     }
     43     if (hint < CHUNK) {
     44       hint = CHUNK;
     45     }
     46     byte[] buf = new byte[hint];
     47     int size = 0;
     48 
     49     try {
     50       while (true) {
     51         if (size == buf.length) {
     52           byte[] tmp = new byte[buf.length+CHUNK];
     53           System.arraycopy(buf, 0, tmp, 0, buf.length);
     54           buf = tmp;
     55         }
     56         int amt = stream.read(buf, size, (buf.length-size));
     57         if (amt < 0) {
     58           break;
     59         } else {
     60           size += amt;
     61         }
     62       }
     63     } catch (IOException ex) {
     64       throw new ApiParseException("Error reading API file", ex);
     65     }
     66 
     67     final Tokenizer tokenizer = new Tokenizer(filename, (new String(buf, 0, size)).toCharArray());
     68     final ApiInfo api = new ApiInfo();
     69 
     70     while (true) {
     71       String token = tokenizer.getToken();
     72       if (token == null) {
     73         break;
     74       }
     75       if ("package".equals(token)) {
     76         parsePackage(api, tokenizer);
     77       } else {
     78         throw new ApiParseException("expected package got " + token, tokenizer.getLine());
     79       }
     80     }
     81 
     82     api.resolveSuperclasses();
     83     api.resolveInterfaces();
     84 
     85     return api;
     86   }
     87 
     88   private static void parsePackage(ApiInfo api, Tokenizer tokenizer)
     89       throws ApiParseException {
     90     String token;
     91     String name;
     92     PackageInfo pkg;
     93 
     94     token = tokenizer.requireToken();
     95     assertIdent(tokenizer, token);
     96     name = token;
     97     pkg = new PackageInfo(name, tokenizer.pos());
     98     token = tokenizer.requireToken();
     99     if (!"{".equals(token)) {
    100       throw new ApiParseException("expected '{' got " + token, tokenizer.getLine());
    101     }
    102     while (true) {
    103       token = tokenizer.requireToken();
    104       if ("}".equals(token)) {
    105         break;
    106       } else {
    107         parseClass(api, pkg, tokenizer, token);
    108       }
    109     }
    110     api.addPackage(pkg);
    111   }
    112 
    113   private static void parseClass(ApiInfo api, PackageInfo pkg, Tokenizer tokenizer, String token)
    114       throws ApiParseException {
    115     boolean pub = false;
    116     boolean prot = false;
    117     boolean pkgpriv = false;
    118     boolean stat = false;
    119     boolean fin = false;
    120     boolean abs = false;
    121     boolean dep = false;
    122     boolean iface;
    123     String name;
    124     String qname;
    125     String ext = null;
    126     ClassInfo cl;
    127 
    128     if ("public".equals(token)) {
    129       pub = true;
    130       token = tokenizer.requireToken();
    131     } else if ("protected".equals(token)) {
    132       prot = true;
    133       token = tokenizer.requireToken();
    134     } else {
    135       pkgpriv = true;
    136     }
    137     if ("static".equals(token)) {
    138       stat = true;
    139       token = tokenizer.requireToken();
    140     }
    141     if ("final".equals(token)) {
    142       fin = true;
    143       token = tokenizer.requireToken();
    144     }
    145     if ("abstract".equals(token)) {
    146       abs = true;
    147       token = tokenizer.requireToken();
    148     }
    149     if ("deprecated".equals(token)) {
    150       dep = true;
    151       token = tokenizer.requireToken();
    152     }
    153     if ("class".equals(token)) {
    154       iface = false;
    155       token = tokenizer.requireToken();
    156     } else if ("interface".equals(token)) {
    157       iface = true;
    158       token = tokenizer.requireToken();
    159     } else {
    160       throw new ApiParseException("missing class or interface. got: " + token, tokenizer.getLine());
    161     }
    162     assertIdent(tokenizer, token);
    163     name = token;
    164     token = tokenizer.requireToken();
    165     qname = qualifiedName(pkg.name(), name, null);
    166     cl = new ClassInfo(null/*classDoc*/, ""/*rawCommentText*/, tokenizer.pos(), pub, prot,
    167         pkgpriv, false/*isPrivate*/, stat, iface, abs, true/*isOrdinaryClass*/,
    168         false/*isException*/, false/*isError*/, false/*isEnum*/, false/*isAnnotation*/,
    169         fin, false/*isIncluded*/, name, qname, null/*qualifiedTypeName*/, false/*isPrimitive*/);
    170     cl.setDeprecated(dep);
    171     if ("extends".equals(token)) {
    172       token = tokenizer.requireToken();
    173       assertIdent(tokenizer, token);
    174       ext = token;
    175       token = tokenizer.requireToken();
    176     }
    177     // Resolve superclass after done parsing
    178     api.mapClassToSuper(cl, ext);
    179     final TypeInfo typeInfo = Converter.obtainTypeFromString(qname) ;
    180     cl.setTypeInfo(typeInfo);
    181     cl.setAnnotations(new ArrayList<AnnotationInstanceInfo>());
    182     if ("implements".equals(token)) {
    183       while (true) {
    184         token = tokenizer.requireToken();
    185         if ("{".equals(token)) {
    186           break;
    187         } else {
    188           /// TODO
    189           if (!",".equals(token)) {
    190             api.mapClassToInterface(cl, token);
    191           }
    192         }
    193       }
    194     }
    195     if (!"{".equals(token)) {
    196       throw new ApiParseException("expected {", tokenizer.getLine());
    197     }
    198     token = tokenizer.requireToken();
    199     while (true) {
    200       if ("}".equals(token)) {
    201         break;
    202       } else if ("ctor".equals(token)) {
    203         token = tokenizer.requireToken();
    204         parseConstructor(tokenizer, cl, token);
    205       } else if ("method".equals(token)) {
    206         token = tokenizer.requireToken();
    207         parseMethod(tokenizer, cl, token);
    208       } else if ("field".equals(token)) {
    209         token = tokenizer.requireToken();
    210         parseField(tokenizer, cl, token, false);
    211       } else if ("enum_constant".equals(token)) {
    212         token = tokenizer.requireToken();
    213         parseField(tokenizer, cl, token, true);
    214       } else {
    215         throw new ApiParseException("expected ctor, enum_constant, field or method", tokenizer.getLine());
    216       }
    217       token = tokenizer.requireToken();
    218     }
    219     pkg.addClass(cl);
    220   }
    221 
    222   private static void parseConstructor(Tokenizer tokenizer, ClassInfo cl, String token)
    223       throws ApiParseException {
    224     boolean pub = false;
    225     boolean prot = false;
    226     boolean pkgpriv = false;
    227     boolean dep = false;
    228     String name;
    229     MethodInfo method;
    230 
    231     if ("public".equals(token)) {
    232       pub = true;
    233       token = tokenizer.requireToken();
    234     } else if ("protected".equals(token)) {
    235       prot = true;
    236       token = tokenizer.requireToken();
    237     } else {
    238       pkgpriv = true;
    239     }
    240     if ("deprecated".equals(token)) {
    241       dep = true;
    242       token = tokenizer.requireToken();
    243     }
    244     assertIdent(tokenizer, token);
    245     name = token;
    246     token = tokenizer.requireToken();
    247     if (!"(".equals(token)) {
    248       throw new ApiParseException("expected (", tokenizer.getLine());
    249     }
    250     //method = new MethodInfo(name, cl.qualifiedName(), false/*static*/, false/*final*/, dep,
    251     //    pub ? "public" : "protected", tokenizer.pos(), cl);
    252     method = new MethodInfo(""/*rawCommentText*/, new ArrayList<TypeInfo>()/*typeParameters*/,
    253         name, null/*signature*/, cl, cl, pub, prot, pkgpriv, false/*isPrivate*/, false/*isFinal*/,
    254         false/*isStatic*/, false/*isSynthetic*/, false/*isAbstract*/, false/*isSynthetic*/,
    255         false/*isNative*/,
    256         false /*isAnnotationElement*/, "constructor", null/*flatSignature*/,
    257         null/*overriddenMethod*/, cl.asTypeInfo(), new ArrayList<ParameterInfo>(),
    258         new ArrayList<ClassInfo>()/*thrownExceptions*/, tokenizer.pos(),
    259         new ArrayList<AnnotationInstanceInfo>()/*annotations*/);
    260     method.setDeprecated(dep);
    261     token = tokenizer.requireToken();
    262     parseParameterList(tokenizer, method, token);
    263     token = tokenizer.requireToken();
    264     if ("throws".equals(token)) {
    265       token = parseThrows(tokenizer, method);
    266     }
    267     if (!";".equals(token)) {
    268       throw new ApiParseException("expected ; found " + token, tokenizer.getLine());
    269     }
    270     cl.addConstructor(method);
    271   }
    272 
    273   private static void parseMethod(Tokenizer tokenizer, ClassInfo cl, String token)
    274       throws ApiParseException {
    275     boolean pub = false;
    276     boolean prot = false;
    277     boolean pkgpriv = false;
    278     boolean stat = false;
    279     boolean fin = false;
    280     boolean abs = false;
    281     boolean dep = false;
    282     boolean syn = false;
    283     String type;
    284     String name;
    285     String ext = null;
    286     MethodInfo method;
    287 
    288     if ("public".equals(token)) {
    289       pub = true;
    290       token = tokenizer.requireToken();
    291     } else if ("protected".equals(token)) {
    292       prot = true;
    293       token = tokenizer.requireToken();
    294     } else {
    295       pkgpriv = true;
    296     }
    297     if ("static".equals(token)) {
    298       stat = true;
    299       token = tokenizer.requireToken();
    300     }
    301     if ("final".equals(token)) {
    302       fin = true;
    303       token = tokenizer.requireToken();
    304     }
    305     if ("abstract".equals(token)) {
    306       abs = true;
    307       token = tokenizer.requireToken();
    308     }
    309     if ("deprecated".equals(token)) {
    310       dep = true;
    311       token = tokenizer.requireToken();
    312     }
    313     if ("synchronized".equals(token)) {
    314       syn = true;
    315       token = tokenizer.requireToken();
    316     }
    317     assertIdent(tokenizer, token);
    318     type = token;
    319     token = tokenizer.requireToken();
    320     assertIdent(tokenizer, token);
    321     name = token;
    322     method = new MethodInfo(""/*rawCommentText*/, new ArrayList<TypeInfo>()/*typeParameters*/,
    323         name, null/*signature*/, cl, cl, pub, prot, pkgpriv, false/*isPrivate*/, fin,
    324         stat, false/*isSynthetic*/, abs/*isAbstract*/, syn, false/*isNative*/,
    325         false /*isAnnotationElement*/, "method", null/*flatSignature*/, null/*overriddenMethod*/,
    326         Converter.obtainTypeFromString(type), new ArrayList<ParameterInfo>(),
    327         new ArrayList<ClassInfo>()/*thrownExceptions*/, tokenizer.pos(),
    328         new ArrayList<AnnotationInstanceInfo>()/*annotations*/);
    329     method.setDeprecated(dep);
    330     token = tokenizer.requireToken();
    331     if (!"(".equals(token)) {
    332       throw new ApiParseException("expected (", tokenizer.getLine());
    333     }
    334     token = tokenizer.requireToken();
    335     parseParameterList(tokenizer, method, token);
    336     token = tokenizer.requireToken();
    337     if ("throws".equals(token)) {
    338       token = parseThrows(tokenizer, method);
    339     }
    340     if (!";".equals(token)) {
    341       throw new ApiParseException("expected ; found " + token, tokenizer.getLine());
    342     }
    343     cl.addMethod(method);
    344   }
    345 
    346   private static void parseField(Tokenizer tokenizer, ClassInfo cl, String token, boolean isEnum)
    347       throws ApiParseException {
    348     boolean pub = false;
    349     boolean prot = false;
    350     boolean pkgpriv = false;
    351     boolean stat = false;
    352     boolean fin = false;
    353     boolean dep = false;
    354     boolean trans = false;
    355     boolean vol = false;
    356     String type;
    357     String name;
    358     String val = null;
    359     Object v;
    360     FieldInfo field;
    361 
    362     if ("public".equals(token)) {
    363       pub = true;
    364       token = tokenizer.requireToken();
    365     } else if ("protected".equals(token)) {
    366       prot = true;
    367       token = tokenizer.requireToken();
    368     } else {
    369       pkgpriv = true;
    370     }
    371     if ("static".equals(token)) {
    372       stat = true;
    373       token = tokenizer.requireToken();
    374     }
    375     if ("final".equals(token)) {
    376       fin = true;
    377       token = tokenizer.requireToken();
    378     }
    379     if ("deprecated".equals(token)) {
    380       dep = true;
    381       token = tokenizer.requireToken();
    382     }
    383     if ("transient".equals(token)) {
    384       trans = true;
    385       token = tokenizer.requireToken();
    386     }
    387     if ("volatile".equals(token)) {
    388       vol = true;
    389       token = tokenizer.requireToken();
    390     }
    391     assertIdent(tokenizer, token);
    392     type = token;
    393     token = tokenizer.requireToken();
    394     assertIdent(tokenizer, token);
    395     name = token;
    396     token = tokenizer.requireToken();
    397     if ("=".equals(token)) {
    398       token = tokenizer.requireToken(false);
    399       val = token;
    400       token = tokenizer.requireToken();
    401     }
    402     if (!";".equals(token)) {
    403       throw new ApiParseException("expected ; found " + token, tokenizer.getLine());
    404     }
    405     try {
    406       v = parseValue(type, val);
    407     } catch (ApiParseException ex) {
    408       ex.line = tokenizer.getLine();
    409       throw ex;
    410     }
    411     field = new FieldInfo(name, cl, cl, pub, prot, pkgpriv, false/*isPrivate*/, fin, stat,
    412         trans, vol, false, Converter.obtainTypeFromString(type), "", v, tokenizer.pos(),
    413         new ArrayList<AnnotationInstanceInfo>());
    414     field.setDeprecated(dep);
    415     if (isEnum) {
    416       cl.addEnumConstant(field);
    417     } else {
    418       cl.addField(field);
    419     }
    420   }
    421 
    422   public static Object parseValue(String type, String val) throws ApiParseException {
    423     if (val != null) {
    424       if ("boolean".equals(type)) {
    425         return "true".equals(val) ? Boolean.TRUE : Boolean.FALSE;
    426       } else if ("byte".equals(type)) {
    427         return Integer.valueOf(val);
    428       } else if ("short".equals(type)) {
    429         return Integer.valueOf(val);
    430       } else if ("int".equals(type)) {
    431         return Integer.valueOf(val);
    432       } else if ("long".equals(type)) {
    433         return Long.valueOf(val.substring(0, val.length()-1));
    434       } else if ("float".equals(type)) {
    435         if ("(1.0f/0.0f)".equals(val)) {
    436           return Float.POSITIVE_INFINITY;
    437         } else if ("(-1.0f/0.0f)".equals(val)) {
    438           return Float.NEGATIVE_INFINITY;
    439         } else if ("(0.0f/0.0f)".equals(val)) {
    440           return Float.NaN;
    441         } else {
    442           return Float.valueOf(val);
    443         }
    444       } else if ("double".equals(type)) {
    445         if ("(1.0/0.0)".equals(val)) {
    446           return Double.POSITIVE_INFINITY;
    447         } else if ("(-1.0/0.0)".equals(val)) {
    448           return Double.NEGATIVE_INFINITY;
    449         } else if ("(0.0/0.0)".equals(val)) {
    450           return Double.NaN;
    451         } else {
    452           return Double.valueOf(val);
    453         }
    454       } else if ("char".equals(type)) {
    455         return new Integer((char)Integer.parseInt(val));
    456       } else if ("java.lang.String".equals(type)) {
    457         if ("null".equals(val)) {
    458           return null;
    459         } else {
    460           return FieldInfo.javaUnescapeString(val.substring(1, val.length()-1));
    461         }
    462       }
    463     }
    464     if ("null".equals(val)) {
    465       return null;
    466     } else {
    467       return val;
    468     }
    469   }
    470 
    471   private static void parseParameterList(Tokenizer tokenizer, AbstractMethodInfo method,
    472       String token) throws ApiParseException {
    473     while (true) {
    474       if (")".equals(token)) {
    475         return;
    476       }
    477 
    478       String type = token;
    479       String name = null;
    480       token = tokenizer.requireToken();
    481       if (isIdent(token)) {
    482         name = token;
    483         token = tokenizer.requireToken();
    484       }
    485       if (",".equals(token)) {
    486         token = tokenizer.requireToken();
    487       } else if (")".equals(token)) {
    488       } else {
    489         throw new ApiParseException("expected , found " + token, tokenizer.getLine());
    490       }
    491       method.addParameter(new ParameterInfo(name, type,
    492             Converter.obtainTypeFromString(type),
    493             type.endsWith("..."),
    494             tokenizer.pos()));
    495       if (type.endsWith("...")) {
    496         method.setVarargs(true);
    497       }
    498     }
    499   }
    500 
    501   private static String parseThrows(Tokenizer tokenizer, AbstractMethodInfo method)
    502       throws ApiParseException {
    503     String token = tokenizer.requireToken();
    504     boolean comma = true;
    505     while (true) {
    506       if (";".equals(token)) {
    507         return token;
    508       } else if (",".equals(token)) {
    509         if (comma) {
    510           throw new ApiParseException("Expected exception, got ','", tokenizer.getLine());
    511         }
    512         comma = true;
    513       } else {
    514         if (!comma) {
    515           throw new ApiParseException("Expected ',' or ';' got " + token, tokenizer.getLine());
    516         }
    517         comma = false;
    518         method.addException(token);
    519       }
    520       token = tokenizer.requireToken();
    521     }
    522   }
    523 
    524   private static String qualifiedName(String pkg, String className, ClassInfo parent) {
    525     String parentQName = (parent != null) ? (parent.qualifiedName() + ".") : "";
    526     return pkg + "." + parentQName + className;
    527   }
    528 
    529   public static boolean isIdent(String token) {
    530     return isident(token.charAt(0));
    531   }
    532 
    533   public static void assertIdent(Tokenizer tokenizer, String token) throws ApiParseException {
    534     if (!isident(token.charAt(0))) {
    535       throw new ApiParseException("Expected identifier: " + token, tokenizer.getLine());
    536     }
    537   }
    538 
    539   static class Tokenizer {
    540     char[] mBuf;
    541     String mFilename;
    542     int mPos;
    543     int mLine = 1;
    544     Tokenizer(String filename, char[] buf) {
    545       mFilename = filename;
    546       mBuf = buf;
    547     }
    548 
    549     public SourcePositionInfo pos() {
    550       return new SourcePositionInfo(mFilename, mLine, 0);
    551     }
    552 
    553     public int getLine() {
    554       return mLine;
    555     }
    556 
    557     boolean eatWhitespace() {
    558       boolean ate = false;
    559       while (mPos < mBuf.length && isspace(mBuf[mPos])) {
    560         if (mBuf[mPos] == '\n') {
    561           mLine++;
    562         }
    563         mPos++;
    564         ate = true;
    565       }
    566       return ate;
    567     }
    568 
    569     boolean eatComment() {
    570       if (mPos+1 < mBuf.length) {
    571         if (mBuf[mPos] == '/' && mBuf[mPos+1] == '/') {
    572           mPos += 2;
    573           while (mPos < mBuf.length && !isnewline(mBuf[mPos])) {
    574             mPos++;
    575           }
    576           return true;
    577         }
    578       }
    579       return false;
    580     }
    581 
    582     void eatWhitespaceAndComments() {
    583       while (eatWhitespace() || eatComment()) {
    584       }
    585     }
    586 
    587     public String requireToken() throws ApiParseException {
    588       return requireToken(true);
    589     }
    590 
    591     public String requireToken(boolean parenIsSep) throws ApiParseException {
    592       final String token = getToken(parenIsSep);
    593       if (token != null) {
    594         return token;
    595       } else {
    596         throw new ApiParseException("Unexpected end of file", mLine);
    597       }
    598     }
    599 
    600     public String getToken() throws ApiParseException {
    601       return getToken(true);
    602     }
    603 
    604     public String getToken(boolean parenIsSep) throws ApiParseException {
    605       eatWhitespaceAndComments();
    606       if (mPos >= mBuf.length) {
    607         return null;
    608       }
    609       final int line = mLine;
    610       final char c = mBuf[mPos];
    611       final int start = mPos;
    612       mPos++;
    613       if (c == '"') {
    614         final int STATE_BEGIN = 0;
    615         final int STATE_ESCAPE = 1;
    616         int state = STATE_BEGIN;
    617         while (true) {
    618           if (mPos >= mBuf.length) {
    619             throw new ApiParseException("Unexpected end of file for \" starting at " + line, mLine);
    620           }
    621           final char k = mBuf[mPos];
    622           if (k == '\n' || k == '\r') {
    623             throw new ApiParseException("Unexpected newline for \" starting at " + line, mLine);
    624           }
    625           mPos++;
    626           switch (state) {
    627             case STATE_BEGIN:
    628               switch (k) {
    629                 case '\\':
    630                   state = STATE_ESCAPE;
    631                   mPos++;
    632                   break;
    633                 case '"':
    634                   return new String(mBuf, start, mPos-start);
    635               }
    636             case STATE_ESCAPE:
    637               state = STATE_BEGIN;
    638               break;
    639           }
    640         }
    641       } else if (issep(c, parenIsSep)) {
    642         return "" + c;
    643       } else {
    644         int genericDepth = 0;
    645         do {
    646           while (mPos < mBuf.length && !isspace(mBuf[mPos]) && !issep(mBuf[mPos], parenIsSep)) {
    647             mPos++;
    648           }
    649           if (mPos < mBuf.length) {
    650             if (mBuf[mPos] == '<') {
    651               genericDepth++;
    652               mPos++;
    653             } else if (mBuf[mPos] == '>') {
    654               genericDepth--;
    655               mPos++;
    656             } else if (genericDepth != 0) {
    657               mPos++;
    658             }
    659           }
    660         } while (mPos < mBuf.length
    661             && ((!isspace(mBuf[mPos]) && !issep(mBuf[mPos], parenIsSep)) || genericDepth != 0));
    662         if (mPos >= mBuf.length) {
    663           throw new ApiParseException("Unexpected end of file for \" starting at " + line, mLine);
    664         }
    665         return new String(mBuf, start, mPos-start);
    666       }
    667     }
    668   }
    669 
    670   static boolean isspace(char c) {
    671     return c == ' ' || c == '\t' || c == '\n' || c == '\r';
    672   }
    673 
    674   static boolean isnewline(char c) {
    675     return c == '\n' || c == '\r';
    676   }
    677 
    678   static boolean issep(char c, boolean parenIsSep) {
    679     if (parenIsSep) {
    680       if (c == '(' || c == ')') {
    681         return true;
    682       }
    683     }
    684     return c == '{' || c == '}' || c == ',' || c == ';' || c == '<' || c == '>';
    685   }
    686 
    687   static boolean isident(char c) {
    688     if (c == '"' || issep(c, true)) {
    689       return false;
    690     }
    691     return true;
    692   }
    693 }
    694 
    695