/* * Copyright (C) 2011 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.apicheck; import com.google.doclava.AnnotationInstanceInfo; import com.google.doclava.ClassInfo; import com.google.doclava.Converter; import com.google.doclava.FieldInfo; import com.google.doclava.MethodInfo; import com.google.doclava.PackageInfo; import com.google.doclava.ParameterInfo; import com.google.doclava.SourcePositionInfo; import com.google.doclava.TypeInfo; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.LinkedList; class ApiFile { public static ApiInfo parseApi(String filename, InputStream stream) throws ApiParseException { final int CHUNK = 1024*1024; int hint = 0; try { hint = stream.available() + CHUNK; } catch (IOException ex) { } if (hint < CHUNK) { hint = CHUNK; } byte[] buf = new byte[hint]; int size = 0; try { while (true) { if (size == buf.length) { byte[] tmp = new byte[buf.length+CHUNK]; System.arraycopy(buf, 0, tmp, 0, buf.length); buf = tmp; } int amt = stream.read(buf, size, (buf.length-size)); if (amt < 0) { break; } else { size += amt; } } } catch (IOException ex) { throw new ApiParseException("Error reading API file", ex); } final Tokenizer tokenizer = new Tokenizer(filename, (new String(buf, 0, size)).toCharArray()); final ApiInfo api = new ApiInfo(); while (true) { String token = tokenizer.getToken(); if (token == null) { break; } if ("package".equals(token)) { parsePackage(api, tokenizer); } else { throw new ApiParseException("expected package got " + token, tokenizer.getLine()); } } api.resolveSuperclasses(); api.resolveInterfaces(); return api; } private static void parsePackage(ApiInfo api, Tokenizer tokenizer) throws ApiParseException { String token; String name; PackageInfo pkg; token = tokenizer.requireToken(); assertIdent(tokenizer, token); name = token; pkg = new PackageInfo(name, tokenizer.pos()); token = tokenizer.requireToken(); if (!"{".equals(token)) { throw new ApiParseException("expected '{' got " + token, tokenizer.getLine()); } while (true) { token = tokenizer.requireToken(); if ("}".equals(token)) { break; } else { parseClass(api, pkg, tokenizer, token); } } api.addPackage(pkg); } private static void parseClass(ApiInfo api, PackageInfo pkg, Tokenizer tokenizer, String token) throws ApiParseException { boolean pub = false; boolean prot = false; boolean pkgpriv = false; boolean stat = false; boolean fin = false; boolean abs = false; boolean dep = false; boolean iface; String name; String qname; String ext = null; ClassInfo cl; if ("public".equals(token)) { pub = true; token = tokenizer.requireToken(); } else if ("protected".equals(token)) { prot = true; token = tokenizer.requireToken(); } else { pkgpriv = true; } if ("static".equals(token)) { stat = true; token = tokenizer.requireToken(); } if ("final".equals(token)) { fin = true; token = tokenizer.requireToken(); } if ("abstract".equals(token)) { abs = true; token = tokenizer.requireToken(); } if ("deprecated".equals(token)) { dep = true; token = tokenizer.requireToken(); } if ("class".equals(token)) { iface = false; token = tokenizer.requireToken(); } else if ("interface".equals(token)) { iface = true; token = tokenizer.requireToken(); } else { throw new ApiParseException("missing class or interface. got: " + token, tokenizer.getLine()); } assertIdent(tokenizer, token); name = token; token = tokenizer.requireToken(); qname = qualifiedName(pkg.name(), name, null); cl = new ClassInfo(null/*classDoc*/, ""/*rawCommentText*/, tokenizer.pos(), pub, prot, pkgpriv, false/*isPrivate*/, stat, iface, abs, true/*isOrdinaryClass*/, false/*isException*/, false/*isError*/, false/*isEnum*/, false/*isAnnotation*/, fin, false/*isIncluded*/, name, qname, null/*qualifiedTypeName*/, false/*isPrimitive*/); cl.setDeprecated(dep); if ("extends".equals(token)) { token = tokenizer.requireToken(); assertIdent(tokenizer, token); ext = token; token = tokenizer.requireToken(); } // Resolve superclass after done parsing api.mapClassToSuper(cl, ext); final TypeInfo typeInfo = Converter.obtainTypeFromString(qname) ; cl.setTypeInfo(typeInfo); cl.setAnnotations(new ArrayList()); if ("implements".equals(token)) { while (true) { token = tokenizer.requireToken(); if ("{".equals(token)) { break; } else { /// TODO if (!",".equals(token)) { api.mapClassToInterface(cl, token); } } } } if (!"{".equals(token)) { throw new ApiParseException("expected {", tokenizer.getLine()); } token = tokenizer.requireToken(); while (true) { if ("}".equals(token)) { break; } else if ("ctor".equals(token)) { token = tokenizer.requireToken(); parseConstructor(tokenizer, cl, token); } else if ("method".equals(token)) { token = tokenizer.requireToken(); parseMethod(tokenizer, cl, token); } else if ("field".equals(token)) { token = tokenizer.requireToken(); parseField(tokenizer, cl, token, false); } else if ("enum_constant".equals(token)) { token = tokenizer.requireToken(); parseField(tokenizer, cl, token, true); } else { throw new ApiParseException("expected ctor, enum_constant, field or method", tokenizer.getLine()); } token = tokenizer.requireToken(); } pkg.addClass(cl); } private static void parseConstructor(Tokenizer tokenizer, ClassInfo cl, String token) throws ApiParseException { boolean pub = false; boolean prot = false; boolean pkgpriv = false; boolean dep = false; String name; MethodInfo method; if ("public".equals(token)) { pub = true; token = tokenizer.requireToken(); } else if ("protected".equals(token)) { prot = true; token = tokenizer.requireToken(); } else { pkgpriv = true; } if ("deprecated".equals(token)) { dep = true; token = tokenizer.requireToken(); } assertIdent(tokenizer, token); name = token; token = tokenizer.requireToken(); if (!"(".equals(token)) { throw new ApiParseException("expected (", tokenizer.getLine()); } //method = new MethodInfo(name, cl.qualifiedName(), false/*static*/, false/*final*/, dep, // pub ? "public" : "protected", tokenizer.pos(), cl); method = new MethodInfo(""/*rawCommentText*/, new ArrayList()/*typeParameters*/, name, null/*signature*/, cl, cl, pub, prot, pkgpriv, false/*isPrivate*/, false/*isFinal*/, false/*isStatic*/, false/*isSynthetic*/, false/*isAbstract*/, false/*isSynthetic*/, false/*isNative*/, false /*isAnnotationElement*/, "constructor", null/*flatSignature*/, null/*overriddenMethod*/, cl.asTypeInfo(), new ArrayList(), new ArrayList()/*thrownExceptions*/, tokenizer.pos(), new ArrayList()/*annotations*/); method.setDeprecated(dep); token = tokenizer.requireToken(); parseParameterList(tokenizer, method, token); token = tokenizer.requireToken(); if ("throws".equals(token)) { token = parseThrows(tokenizer, method); } if (!";".equals(token)) { throw new ApiParseException("expected ; found " + token, tokenizer.getLine()); } cl.addConstructor(method); } private static void parseMethod(Tokenizer tokenizer, ClassInfo cl, String token) throws ApiParseException { boolean pub = false; boolean prot = false; boolean pkgpriv = false; boolean stat = false; boolean fin = false; boolean abs = false; boolean dep = false; boolean syn = false; String type; String name; String ext = null; MethodInfo method; if ("public".equals(token)) { pub = true; token = tokenizer.requireToken(); } else if ("protected".equals(token)) { prot = true; token = tokenizer.requireToken(); } else { pkgpriv = true; } if ("static".equals(token)) { stat = true; token = tokenizer.requireToken(); } if ("final".equals(token)) { fin = true; token = tokenizer.requireToken(); } if ("abstract".equals(token)) { abs = true; token = tokenizer.requireToken(); } if ("deprecated".equals(token)) { dep = true; token = tokenizer.requireToken(); } if ("synchronized".equals(token)) { syn = true; token = tokenizer.requireToken(); } assertIdent(tokenizer, token); type = token; token = tokenizer.requireToken(); assertIdent(tokenizer, token); name = token; method = new MethodInfo(""/*rawCommentText*/, new ArrayList()/*typeParameters*/, name, null/*signature*/, cl, cl, pub, prot, pkgpriv, false/*isPrivate*/, fin, stat, false/*isSynthetic*/, abs/*isAbstract*/, syn, false/*isNative*/, false /*isAnnotationElement*/, "method", null/*flatSignature*/, null/*overriddenMethod*/, Converter.obtainTypeFromString(type), new ArrayList(), new ArrayList()/*thrownExceptions*/, tokenizer.pos(), new ArrayList()/*annotations*/); method.setDeprecated(dep); token = tokenizer.requireToken(); if (!"(".equals(token)) { throw new ApiParseException("expected (", tokenizer.getLine()); } token = tokenizer.requireToken(); parseParameterList(tokenizer, method, token); token = tokenizer.requireToken(); if ("throws".equals(token)) { token = parseThrows(tokenizer, method); } if (!";".equals(token)) { throw new ApiParseException("expected ; found " + token, tokenizer.getLine()); } cl.addMethod(method); } private static void parseField(Tokenizer tokenizer, ClassInfo cl, String token, boolean isEnum) throws ApiParseException { boolean pub = false; boolean prot = false; boolean pkgpriv = false; boolean stat = false; boolean fin = false; boolean dep = false; boolean trans = false; boolean vol = false; String type; String name; String val = null; Object v; FieldInfo field; if ("public".equals(token)) { pub = true; token = tokenizer.requireToken(); } else if ("protected".equals(token)) { prot = true; token = tokenizer.requireToken(); } else { pkgpriv = true; } if ("static".equals(token)) { stat = true; token = tokenizer.requireToken(); } if ("final".equals(token)) { fin = true; token = tokenizer.requireToken(); } if ("deprecated".equals(token)) { dep = true; token = tokenizer.requireToken(); } if ("transient".equals(token)) { trans = true; token = tokenizer.requireToken(); } if ("volatile".equals(token)) { vol = true; token = tokenizer.requireToken(); } assertIdent(tokenizer, token); type = token; token = tokenizer.requireToken(); assertIdent(tokenizer, token); name = token; token = tokenizer.requireToken(); if ("=".equals(token)) { token = tokenizer.requireToken(false); val = token; token = tokenizer.requireToken(); } if (!";".equals(token)) { throw new ApiParseException("expected ; found " + token, tokenizer.getLine()); } try { v = parseValue(type, val); } catch (ApiParseException ex) { ex.line = tokenizer.getLine(); throw ex; } field = new FieldInfo(name, cl, cl, pub, prot, pkgpriv, false/*isPrivate*/, fin, stat, trans, vol, false, Converter.obtainTypeFromString(type), "", v, tokenizer.pos(), new ArrayList()); field.setDeprecated(dep); if (isEnum) { cl.addEnumConstant(field); } else { cl.addField(field); } } public static Object parseValue(String type, String val) throws ApiParseException { if (val != null) { if ("boolean".equals(type)) { return "true".equals(val) ? Boolean.TRUE : Boolean.FALSE; } else if ("byte".equals(type)) { return Integer.valueOf(val); } else if ("short".equals(type)) { return Integer.valueOf(val); } else if ("int".equals(type)) { return Integer.valueOf(val); } else if ("long".equals(type)) { return Long.valueOf(val.substring(0, val.length()-1)); } else if ("float".equals(type)) { if ("(1.0f/0.0f)".equals(val) || "(1.0f / 0.0f)".equals(val)) { return Float.POSITIVE_INFINITY; } else if ("(-1.0f/0.0f)".equals(val) || "(-1.0f / 0.0f)".equals(val)) { return Float.NEGATIVE_INFINITY; } else if ("(0.0f/0.0f)".equals(val) || "(0.0f / 0.0f)".equals(val)) { return Float.NaN; } else { return Float.valueOf(val); } } else if ("double".equals(type)) { if ("(1.0/0.0)".equals(val) || "(1.0 / 0.0)".equals(val)) { return Double.POSITIVE_INFINITY; } else if ("(-1.0/0.0)".equals(val) || "(-1.0 / 0.0)".equals(val)) { return Double.NEGATIVE_INFINITY; } else if ("(0.0/0.0)".equals(val) || "(0.0 / 0.0)".equals(val)) { return Double.NaN; } else { return Double.valueOf(val); } } else if ("char".equals(type)) { return new Integer((char)Integer.parseInt(val)); } else if ("java.lang.String".equals(type)) { if ("null".equals(val)) { return null; } else { return FieldInfo.javaUnescapeString(val.substring(1, val.length()-1)); } } } if ("null".equals(val)) { return null; } else { return val; } } private static void parseParameterList(Tokenizer tokenizer, AbstractMethodInfo method, String token) throws ApiParseException { while (true) { if (")".equals(token)) { return; } String type = token; String name = null; token = tokenizer.requireToken(); if (isIdent(token)) { name = token; token = tokenizer.requireToken(); } if (",".equals(token)) { token = tokenizer.requireToken(); } else if (")".equals(token)) { } else { throw new ApiParseException("expected , found " + token, tokenizer.getLine()); } method.addParameter(new ParameterInfo(name, type, Converter.obtainTypeFromString(type), type.endsWith("..."), tokenizer.pos())); if (type.endsWith("...")) { method.setVarargs(true); } } } private static String parseThrows(Tokenizer tokenizer, AbstractMethodInfo method) throws ApiParseException { String token = tokenizer.requireToken(); boolean comma = true; while (true) { if (";".equals(token)) { return token; } else if (",".equals(token)) { if (comma) { throw new ApiParseException("Expected exception, got ','", tokenizer.getLine()); } comma = true; } else { if (!comma) { throw new ApiParseException("Expected ',' or ';' got " + token, tokenizer.getLine()); } comma = false; method.addException(token); } token = tokenizer.requireToken(); } } private static String qualifiedName(String pkg, String className, ClassInfo parent) { String parentQName = (parent != null) ? (parent.qualifiedName() + ".") : ""; return pkg + "." + parentQName + className; } public static boolean isIdent(String token) { return isident(token.charAt(0)); } public static void assertIdent(Tokenizer tokenizer, String token) throws ApiParseException { if (!isident(token.charAt(0))) { throw new ApiParseException("Expected identifier: " + token, tokenizer.getLine()); } } static class Tokenizer { char[] mBuf; String mFilename; int mPos; int mLine = 1; Tokenizer(String filename, char[] buf) { mFilename = filename; mBuf = buf; } public SourcePositionInfo pos() { return new SourcePositionInfo(mFilename, mLine, 0); } public int getLine() { return mLine; } boolean eatWhitespace() { boolean ate = false; while (mPos < mBuf.length && isspace(mBuf[mPos])) { if (mBuf[mPos] == '\n') { mLine++; } mPos++; ate = true; } return ate; } boolean eatComment() { if (mPos+1 < mBuf.length) { if (mBuf[mPos] == '/' && mBuf[mPos+1] == '/') { mPos += 2; while (mPos < mBuf.length && !isnewline(mBuf[mPos])) { mPos++; } return true; } } return false; } void eatWhitespaceAndComments() { while (eatWhitespace() || eatComment()) { } } public String requireToken() throws ApiParseException { return requireToken(true); } public String requireToken(boolean parenIsSep) throws ApiParseException { final String token = getToken(parenIsSep); if (token != null) { return token; } else { throw new ApiParseException("Unexpected end of file", mLine); } } public String getToken() throws ApiParseException { return getToken(true); } public String getToken(boolean parenIsSep) throws ApiParseException { eatWhitespaceAndComments(); if (mPos >= mBuf.length) { return null; } final int line = mLine; final char c = mBuf[mPos]; final int start = mPos; mPos++; if (c == '"') { final int STATE_BEGIN = 0; final int STATE_ESCAPE = 1; int state = STATE_BEGIN; while (true) { if (mPos >= mBuf.length) { throw new ApiParseException("Unexpected end of file for \" starting at " + line, mLine); } final char k = mBuf[mPos]; if (k == '\n' || k == '\r') { throw new ApiParseException("Unexpected newline for \" starting at " + line, mLine); } mPos++; switch (state) { case STATE_BEGIN: switch (k) { case '\\': state = STATE_ESCAPE; mPos++; break; case '"': return new String(mBuf, start, mPos-start); } case STATE_ESCAPE: state = STATE_BEGIN; break; } } } else if (issep(c, parenIsSep)) { return "" + c; } else { int genericDepth = 0; do { while (mPos < mBuf.length && !isspace(mBuf[mPos]) && !issep(mBuf[mPos], parenIsSep)) { mPos++; } if (mPos < mBuf.length) { if (mBuf[mPos] == '<') { genericDepth++; mPos++; } else if (mBuf[mPos] == '>') { genericDepth--; mPos++; } else if (genericDepth != 0) { mPos++; } } } while (mPos < mBuf.length && ((!isspace(mBuf[mPos]) && !issep(mBuf[mPos], parenIsSep)) || genericDepth != 0)); if (mPos >= mBuf.length) { throw new ApiParseException("Unexpected end of file for \" starting at " + line, mLine); } return new String(mBuf, start, mPos-start); } } } static boolean isspace(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; } static boolean isnewline(char c) { return c == '\n' || c == '\r'; } static boolean issep(char c, boolean parenIsSep) { if (parenIsSep) { if (c == '(' || c == ')') { return true; } } return c == '{' || c == '}' || c == ',' || c == ';' || c == '<' || c == '>'; } static boolean isident(char c) { if (c == '"' || issep(c, true)) { return false; } return true; } }