blob: 2d958d4e292af0e6da8ad2e6d25233e13d100963 [file] [log] [blame]
/*
* 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.Collections;
import java.util.LinkedList;
import java.util.List;
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<AnnotationInstanceInfo>());
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<TypeInfo>()/*typeParameters*/,
name, null/*signature*/, cl, cl, pub, prot, pkgpriv, false/*isPrivate*/, false/*isFinal*/,
false/*isStatic*/, false/*isSynthetic*/, false/*isAbstract*/, false/*isSynthetic*/,
false/*isNative*/, false/* isDefault */,
false /*isAnnotationElement*/, "constructor", null/*flatSignature*/,
null/*overriddenMethod*/, cl.asTypeInfo(), new ArrayList<ParameterInfo>(),
new ArrayList<ClassInfo>()/*thrownExceptions*/, tokenizer.pos(),
new ArrayList<AnnotationInstanceInfo>()/*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;
boolean def = 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 ("default".equals(token)) {
def = true;
token = tokenizer.requireToken();
}
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<TypeInfo>()/*typeParameters*/,
name, null/*signature*/, cl, cl, pub, prot, pkgpriv, false/*isPrivate*/, fin,
stat, false/*isSynthetic*/, abs/*isAbstract*/, syn, false/*isNative*/, def/*isDefault*/,
false /*isAnnotationElement*/, "method", null/*flatSignature*/, null/*overriddenMethod*/,
Converter.obtainTypeFromString(type), new ArrayList<ParameterInfo>(),
new ArrayList<ClassInfo>()/*thrownExceptions*/, tokenizer.pos(),
new ArrayList<AnnotationInstanceInfo>()/*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<AnnotationInstanceInfo>());
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());
}
// api file does not preserve annotations.
List<AnnotationInstanceInfo> annotations = Collections.emptyList();
method.addParameter(new ParameterInfo(name, type,
Converter.obtainTypeFromString(type),
type.endsWith("..."),
tokenizer.pos(),
annotations));
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;
}
}