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) || "(1.0f / 0.0f)".equals(val)) { 436 return Float.POSITIVE_INFINITY; 437 } else if ("(-1.0f/0.0f)".equals(val) || "(-1.0f / 0.0f)".equals(val)) { 438 return Float.NEGATIVE_INFINITY; 439 } else if ("(0.0f/0.0f)".equals(val) || "(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) || "(1.0 / 0.0)".equals(val)) { 446 return Double.POSITIVE_INFINITY; 447 } else if ("(-1.0/0.0)".equals(val) || "(-1.0 / 0.0)".equals(val)) { 448 return Double.NEGATIVE_INFINITY; 449 } else if ("(0.0/0.0)".equals(val) || "(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