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.android.tools.metalava.doclava1; 18 19 import com.android.tools.lint.checks.infrastructure.ClassNameKt; 20 import com.android.tools.metalava.FileFormat; 21 import com.android.tools.metalava.model.AnnotationItem; 22 import com.android.tools.metalava.model.DefaultModifierList; 23 import com.android.tools.metalava.model.TypeParameterList; 24 import com.android.tools.metalava.model.text.TextClassItem; 25 import com.android.tools.metalava.model.text.TextConstructorItem; 26 import com.android.tools.metalava.model.text.TextFieldItem; 27 import com.android.tools.metalava.model.text.TextMethodItem; 28 import com.android.tools.metalava.model.text.TextModifiers; 29 import com.android.tools.metalava.model.text.TextPackageItem; 30 import com.android.tools.metalava.model.text.TextParameterItem; 31 import com.android.tools.metalava.model.text.TextParameterItemKt; 32 import com.android.tools.metalava.model.text.TextPropertyItem; 33 import com.android.tools.metalava.model.text.TextTypeItem; 34 import com.android.tools.metalava.model.text.TextTypeParameterList; 35 import com.google.common.annotations.VisibleForTesting; 36 import com.google.common.io.Files; 37 import kotlin.Pair; 38 import kotlin.text.StringsKt; 39 import org.jetbrains.annotations.Nullable; 40 41 import java.io.File; 42 import java.io.IOException; 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 48 import static com.android.tools.metalava.ConstantsKt.ANDROIDX_NONNULL; 49 import static com.android.tools.metalava.ConstantsKt.ANDROIDX_NULLABLE; 50 import static com.android.tools.metalava.ConstantsKt.JAVA_LANG_ANNOTATION; 51 import static com.android.tools.metalava.ConstantsKt.JAVA_LANG_ENUM; 52 import static com.android.tools.metalava.ConstantsKt.JAVA_LANG_STRING; 53 import static com.android.tools.metalava.model.FieldItemKt.javaUnescapeString; 54 import static kotlin.text.Charsets.UTF_8; 55 56 // 57 // Copied from doclava1, but adapted to metalava's code model (plus tweaks to handle 58 // metalava's richer files, e.g. annotations) 59 // 60 public class ApiFile { parseApi(File file)61 public static TextCodebase parseApi(File file) throws ApiParseException { 62 return parseApi(file, null); 63 } 64 parseApi(File file, Boolean kotlinStyleNulls)65 public static TextCodebase parseApi(File file, 66 Boolean kotlinStyleNulls) throws ApiParseException { 67 try { 68 String apiText = Files.asCharSource(file, UTF_8).read(); 69 return parseApi(file.getPath(), apiText, kotlinStyleNulls); 70 } catch (IOException ex) { 71 throw new ApiParseException("Error reading API file", ex); 72 } 73 } 74 75 @SuppressWarnings("StatementWithEmptyBody") 76 @VisibleForTesting parseApi(String filename, String apiText, Boolean kotlinStyleNulls)77 public static TextCodebase parseApi(String filename, String apiText, 78 Boolean kotlinStyleNulls) throws ApiParseException { 79 FileFormat format = FileFormat.Companion.parseHeader(apiText); 80 if (format.isSignatureFormat()) { 81 if (kotlinStyleNulls == null || !kotlinStyleNulls) { 82 kotlinStyleNulls = format.useKotlinStyleNulls(); 83 } 84 } else if (StringsKt.isBlank(apiText)) { 85 // Signature files are sometimes blank, particularly with show annotations 86 kotlinStyleNulls = false; 87 } else { 88 throw new ApiParseException("Unknown file format of " + filename); 89 } 90 91 if (apiText.contains("/*")) { 92 apiText = ClassNameKt.stripComments(apiText, false); // line comments are used to stash field constants 93 } 94 95 final Tokenizer tokenizer = new Tokenizer(filename, apiText.toCharArray()); 96 final TextCodebase api = new TextCodebase(new File(filename)); 97 api.setDescription("Codebase loaded from " + filename); 98 api.setFormat(format); 99 api.setKotlinStyleNulls(kotlinStyleNulls); 100 101 while (true) { 102 String token = tokenizer.getToken(); 103 if (token == null) { 104 break; 105 } 106 if ("package".equals(token)) { 107 parsePackage(api, tokenizer); 108 } else { 109 throw new ApiParseException("expected package got " + token, tokenizer); 110 } 111 } 112 113 api.postProcess(); 114 115 return api; 116 } 117 parsePackage(TextCodebase api, Tokenizer tokenizer)118 private static void parsePackage(TextCodebase api, Tokenizer tokenizer) 119 throws ApiParseException { 120 String token; 121 String name; 122 TextPackageItem pkg; 123 124 token = tokenizer.requireToken(); 125 126 // Metalava: including annotations in file now 127 List<String> annotations = getAnnotations(tokenizer, token); 128 TextModifiers modifiers = new TextModifiers(api, DefaultModifierList.PUBLIC, null); 129 if (annotations != null) { 130 modifiers.addAnnotations(annotations); 131 } 132 133 token = tokenizer.getCurrent(); 134 135 assertIdent(tokenizer, token); 136 name = token; 137 pkg = new TextPackageItem(api, name, modifiers, tokenizer.pos()); 138 139 token = tokenizer.requireToken(); 140 if (!"{".equals(token)) { 141 throw new ApiParseException("expected '{' got " + token, tokenizer); 142 } 143 while (true) { 144 token = tokenizer.requireToken(); 145 if ("}".equals(token)) { 146 break; 147 } else { 148 parseClass(api, pkg, tokenizer, token); 149 } 150 } 151 api.addPackage(pkg); 152 } 153 parseClass(TextCodebase api, TextPackageItem pkg, Tokenizer tokenizer, String token)154 private static void parseClass(TextCodebase api, TextPackageItem pkg, Tokenizer tokenizer, String token) 155 throws ApiParseException { 156 boolean isInterface = false; 157 boolean isAnnotation = false; 158 boolean isEnum = false; 159 String name; 160 String qualifiedName; 161 String ext = null; 162 TextClassItem cl; 163 164 // Metalava: including annotations in file now 165 List<String> annotations = getAnnotations(tokenizer, token); 166 token = tokenizer.getCurrent(); 167 168 TextModifiers modifiers = parseModifiers(api, tokenizer, token, annotations); 169 token = tokenizer.getCurrent(); 170 171 if ("class".equals(token)) { 172 token = tokenizer.requireToken(); 173 } else if ("interface".equals(token)) { 174 isInterface = true; 175 modifiers.setAbstract(true); 176 token = tokenizer.requireToken(); 177 } else if ("@interface".equals(token)) { 178 // Annotation 179 modifiers.setAbstract(true); 180 isAnnotation = true; 181 token = tokenizer.requireToken(); 182 } else if ("enum".equals(token)) { 183 isEnum = true; 184 modifiers.setFinal(true); 185 modifiers.setStatic(true); 186 ext = JAVA_LANG_ENUM; 187 token = tokenizer.requireToken(); 188 } else { 189 throw new ApiParseException("missing class or interface. got: " + token, tokenizer); 190 } 191 assertIdent(tokenizer, token); 192 name = token; 193 qualifiedName = qualifiedName(pkg.name(), name); 194 final TextTypeItem typeInfo = api.obtainTypeFromString(qualifiedName); 195 // Simple type info excludes the package name (but includes enclosing class names) 196 197 String rawName = name; 198 int variableIndex = rawName.indexOf('<'); 199 if (variableIndex != -1) { 200 rawName = rawName.substring(0, variableIndex); 201 } 202 203 token = tokenizer.requireToken(); 204 205 cl = new TextClassItem(api, tokenizer.pos(), modifiers, isInterface, isEnum, isAnnotation, 206 typeInfo.toErasedTypeString(null), typeInfo.qualifiedTypeName(), 207 rawName, annotations); 208 cl.setContainingPackage(pkg); 209 cl.setTypeInfo(typeInfo); 210 cl.setDeprecated(modifiers.isDeprecated()); 211 if ("extends".equals(token)) { 212 token = tokenizer.requireToken(); 213 assertIdent(tokenizer, token); 214 ext = token; 215 token = tokenizer.requireToken(); 216 } 217 // Resolve superclass after done parsing 218 api.mapClassToSuper(cl, ext); 219 if ("implements".equals(token) || "extends".equals(token) || 220 isInterface && ext != null && !token.equals("{")) { 221 if (!token.equals("implements") && !token.equals("extends")) { 222 api.mapClassToInterface(cl, token); 223 } 224 while (true) { 225 token = tokenizer.requireToken(); 226 if ("{".equals(token)) { 227 break; 228 } else { 229 /// TODO 230 if (!",".equals(token)) { 231 api.mapClassToInterface(cl, token); 232 } 233 } 234 } 235 } 236 if (JAVA_LANG_ENUM.equals(ext)) { 237 cl.setIsEnum(true); 238 // Above we marked all enums as static but for a top level class it's implicit 239 if (!cl.fullName().contains(".")) { 240 cl.getModifiers().setStatic(false); 241 } 242 } else if (isAnnotation) { 243 api.mapClassToInterface(cl, JAVA_LANG_ANNOTATION); 244 } else if (api.implementsInterface(cl, JAVA_LANG_ANNOTATION)) { 245 cl.setIsAnnotationType(true); 246 } 247 if (!"{".equals(token)) { 248 throw new ApiParseException("expected {, was " + token, tokenizer); 249 } 250 token = tokenizer.requireToken(); 251 while (true) { 252 if ("}".equals(token)) { 253 break; 254 } else if ("ctor".equals(token)) { 255 token = tokenizer.requireToken(); 256 parseConstructor(api, tokenizer, cl, token); 257 } else if ("method".equals(token)) { 258 token = tokenizer.requireToken(); 259 parseMethod(api, tokenizer, cl, token); 260 } else if ("field".equals(token)) { 261 token = tokenizer.requireToken(); 262 parseField(api, tokenizer, cl, token, false); 263 } else if ("enum_constant".equals(token)) { 264 token = tokenizer.requireToken(); 265 parseField(api, tokenizer, cl, token, true); 266 } else if ("property".equals(token)) { 267 token = tokenizer.requireToken(); 268 parseProperty(api, tokenizer, cl, token); 269 } else { 270 throw new ApiParseException("expected ctor, enum_constant, field or method", tokenizer); 271 } 272 token = tokenizer.requireToken(); 273 } 274 pkg.addClass(cl); 275 } 276 processKotlinTypeSuffix(TextCodebase api, String type, List<String> annotations)277 private static Pair<String, List<String>> processKotlinTypeSuffix(TextCodebase api, String type, List<String> annotations) throws ApiParseException { 278 if (api.getKotlinStyleNulls()) { 279 if (type.endsWith("?")) { 280 type = type.substring(0, type.length() - 1); 281 annotations = mergeAnnotations(annotations, ANDROIDX_NULLABLE); 282 } else if (type.endsWith("!")) { 283 type = type.substring(0, type.length() - 1); 284 } else if (!type.endsWith("!")) { 285 if (!TextTypeItem.Companion.isPrimitive(type)) { // Don't add nullness on primitive types like void 286 annotations = mergeAnnotations(annotations, ANDROIDX_NONNULL); 287 } 288 } 289 } else if (type.endsWith("?") || type.endsWith("!")) { 290 throw new ApiParseException("Did you forget to supply --input-kotlin-nulls? Found Kotlin-style null type suffix when parser was not configured " + 291 "to interpret signature file that way: " + type); 292 } 293 return new Pair<>(type, annotations); 294 } 295 getAnnotations(Tokenizer tokenizer, String token)296 private static List<String> getAnnotations(Tokenizer tokenizer, String token) throws ApiParseException { 297 List<String> annotations = null; 298 299 while (true) { 300 if (token.startsWith("@")) { 301 // Annotation 302 String annotation = token; 303 if (annotation.indexOf('.') == -1) { 304 // Restore annotations that were shortened on export 305 annotation = AnnotationItem.Companion.unshortenAnnotation(annotation); 306 } 307 token = tokenizer.requireToken(); 308 if (token.equals("(")) { 309 // Annotation arguments; potentially nested 310 int balance = 0; 311 int start = tokenizer.offset() - 1; 312 while (true) { 313 if (token.equals("(")) { 314 balance++; 315 } else if (token.equals(")")) { 316 balance--; 317 if (balance == 0) { 318 break; 319 } 320 } 321 token = tokenizer.requireToken(); 322 } 323 annotation += tokenizer.getStringFromOffset(start); 324 token = tokenizer.requireToken(); 325 } 326 if (annotations == null) { 327 annotations = new ArrayList<>(); 328 } 329 annotations.add(annotation); 330 } else { 331 break; 332 } 333 } 334 335 return annotations; 336 } 337 parseConstructor(TextCodebase api, Tokenizer tokenizer, TextClassItem cl, String token)338 private static void parseConstructor(TextCodebase api, Tokenizer tokenizer, TextClassItem cl, String token) 339 throws ApiParseException { 340 String name; 341 TextConstructorItem method; 342 343 // Metalava: including annotations in file now 344 List<String> annotations = getAnnotations(tokenizer, token); 345 token = tokenizer.getCurrent(); 346 347 TextModifiers modifiers = parseModifiers(api, tokenizer, token, annotations); 348 token = tokenizer.getCurrent(); 349 350 assertIdent(tokenizer, token); 351 name = token.substring(token.lastIndexOf('.') + 1); // For inner classes, strip outer classes from name 352 token = tokenizer.requireToken(); 353 if (!"(".equals(token)) { 354 throw new ApiParseException("expected (", tokenizer); 355 } 356 method = new TextConstructorItem(api, name, cl, modifiers, cl.asTypeInfo(), tokenizer.pos()); 357 method.setDeprecated(modifiers.isDeprecated()); 358 parseParameterList(api, tokenizer, method); 359 token = tokenizer.requireToken(); 360 if ("throws".equals(token)) { 361 token = parseThrows(tokenizer, method); 362 } 363 if (!";".equals(token)) { 364 throw new ApiParseException("expected ; found " + token, tokenizer); 365 } 366 cl.addConstructor(method); 367 } 368 parseMethod(TextCodebase api, Tokenizer tokenizer, TextClassItem cl, String token)369 private static void parseMethod(TextCodebase api, Tokenizer tokenizer, TextClassItem cl, String token) 370 throws ApiParseException { 371 TextTypeItem returnType; 372 String name; 373 TextMethodItem method; 374 TypeParameterList typeParameterList = TypeParameterList.Companion.getNONE(); 375 376 // Metalava: including annotations in file now 377 List<String> annotations = getAnnotations(tokenizer, token); 378 token = tokenizer.getCurrent(); 379 380 TextModifiers modifiers = parseModifiers(api, tokenizer, token, null); 381 token = tokenizer.getCurrent(); 382 383 if ("<".equals(token)) { 384 typeParameterList = parseTypeParameterList(api, tokenizer); 385 token = tokenizer.requireToken(); 386 } 387 assertIdent(tokenizer, token); 388 389 Pair<String, List<String>> kotlinTypeSuffix = processKotlinTypeSuffix(api, token, annotations); 390 token = kotlinTypeSuffix.getFirst(); 391 annotations = kotlinTypeSuffix.getSecond(); 392 modifiers.addAnnotations(annotations); 393 String returnTypeString = token; 394 395 token = tokenizer.requireToken(); 396 397 if (returnTypeString.contains("@") && (returnTypeString.indexOf('<') == -1 || 398 returnTypeString.indexOf('@') < returnTypeString.indexOf('<'))) { 399 returnTypeString += " " + token; 400 token = tokenizer.requireToken(); 401 } 402 while (true) { 403 if (token.contains("@") && (token.indexOf('<') == -1 || 404 token.indexOf('@') < token.indexOf('<'))) { 405 // Type-use annotations in type; keep accumulating 406 returnTypeString += " " + token; 407 token = tokenizer.requireToken(); 408 if (token.startsWith("[")) { // TODO: This isn't general purpose; make requireToken smarter! 409 returnTypeString += " " + token; 410 token = tokenizer.requireToken(); 411 } 412 } else { 413 break; 414 } 415 } 416 417 returnType = api.obtainTypeFromString(returnTypeString, cl, typeParameterList); 418 419 assertIdent(tokenizer, token); 420 name = token; 421 method = new TextMethodItem(api, name, cl, modifiers, returnType, tokenizer.pos()); 422 method.setDeprecated(modifiers.isDeprecated()); 423 if (cl.isInterface() && !modifiers.isDefault() && !modifiers.isStatic()) { 424 modifiers.setAbstract(true); 425 } 426 method.setTypeParameterList(typeParameterList); 427 if (typeParameterList instanceof TextTypeParameterList) { 428 ((TextTypeParameterList) typeParameterList).setOwner(method); 429 } 430 token = tokenizer.requireToken(); 431 if (!"(".equals(token)) { 432 throw new ApiParseException("expected (, was " + token, tokenizer); 433 } 434 parseParameterList(api, tokenizer, method); 435 token = tokenizer.requireToken(); 436 if ("throws".equals(token)) { 437 token = parseThrows(tokenizer, method); 438 } 439 if ("default".equals(token)) { 440 token = parseDefault(tokenizer, method); 441 } 442 if (!";".equals(token)) { 443 throw new ApiParseException("expected ; found " + token, tokenizer); 444 } 445 cl.addMethod(method); 446 } 447 mergeAnnotations(List<String> annotations, String annotation)448 private static List<String> mergeAnnotations(List<String> annotations, String annotation) { 449 if (annotations == null) { 450 annotations = new ArrayList<>(); 451 } 452 // Reverse effect of TypeItem.shortenTypes(...) 453 String qualifiedName = annotation.indexOf('.') == -1 454 ? "@androidx.annotation" + annotation 455 : "@" + annotation; 456 457 annotations.add(qualifiedName); 458 return annotations; 459 } 460 parseField(TextCodebase api, Tokenizer tokenizer, TextClassItem cl, String token, boolean isEnum)461 private static void parseField(TextCodebase api, Tokenizer tokenizer, TextClassItem cl, String token, boolean isEnum) 462 throws ApiParseException { 463 List<String> annotations = getAnnotations(tokenizer, token); 464 token = tokenizer.getCurrent(); 465 466 TextModifiers modifiers = parseModifiers(api, tokenizer, token, null); 467 token = tokenizer.getCurrent(); 468 assertIdent(tokenizer, token); 469 470 Pair<String, List<String>> kotlinTypeSuffix = processKotlinTypeSuffix(api, token, annotations); 471 token = kotlinTypeSuffix.getFirst(); 472 annotations = kotlinTypeSuffix.getSecond(); 473 modifiers.addAnnotations(annotations); 474 475 String type = token; 476 TextTypeItem typeInfo = api.obtainTypeFromString(type); 477 478 token = tokenizer.requireToken(); 479 assertIdent(tokenizer, token); 480 String name = token; 481 token = tokenizer.requireToken(); 482 Object value = null; 483 if ("=".equals(token)) { 484 token = tokenizer.requireToken(false); 485 value = parseValue(type, token); 486 token = tokenizer.requireToken(); 487 } 488 if (!";".equals(token)) { 489 throw new ApiParseException("expected ; found " + token, tokenizer); 490 } 491 TextFieldItem field = new TextFieldItem(api, name, cl, modifiers, typeInfo, value, tokenizer.pos()); 492 field.setDeprecated(modifiers.isDeprecated()); 493 if (isEnum) { 494 cl.addEnumConstant(field); 495 } else { 496 cl.addField(field); 497 } 498 } 499 parseModifiers( TextCodebase api, Tokenizer tokenizer, String token, List<String> annotations)500 private static TextModifiers parseModifiers( 501 TextCodebase api, 502 Tokenizer tokenizer, 503 String token, 504 List<String> annotations) throws ApiParseException { 505 506 TextModifiers modifiers = new TextModifiers(api, 0, null); 507 508 processModifiers: 509 while (true) { 510 switch (token) { 511 case "public": 512 modifiers.setPublic(true); 513 token = tokenizer.requireToken(); 514 break; 515 case "protected": 516 modifiers.setProtected(true); 517 token = tokenizer.requireToken(); 518 break; 519 case "private": 520 modifiers.setPrivate(true); 521 token = tokenizer.requireToken(); 522 break; 523 case "internal": 524 modifiers.setInternal(true); 525 token = tokenizer.requireToken(); 526 break; 527 case "static": 528 modifiers.setStatic(true); 529 token = tokenizer.requireToken(); 530 break; 531 case "final": 532 modifiers.setFinal(true); 533 token = tokenizer.requireToken(); 534 break; 535 case "deprecated": 536 modifiers.setDeprecated(true); 537 token = tokenizer.requireToken(); 538 break; 539 case "abstract": 540 modifiers.setAbstract(true); 541 token = tokenizer.requireToken(); 542 break; 543 case "transient": 544 modifiers.setTransient(true); 545 token = tokenizer.requireToken(); 546 break; 547 case "volatile": 548 modifiers.setVolatile(true); 549 token = tokenizer.requireToken(); 550 break; 551 case "sealed": 552 modifiers.setSealed(true); 553 token = tokenizer.requireToken(); 554 break; 555 case "default": 556 modifiers.setDefault(true); 557 token = tokenizer.requireToken(); 558 break; 559 case "synchronized": 560 modifiers.setSynchronized(true); 561 token = tokenizer.requireToken(); 562 break; 563 case "native": 564 modifiers.setNative(true); 565 token = tokenizer.requireToken(); 566 break; 567 case "strictfp": 568 modifiers.setStrictFp(true); 569 token = tokenizer.requireToken(); 570 break; 571 case "infix": 572 modifiers.setInfix(true); 573 token = tokenizer.requireToken(); 574 break; 575 case "operator": 576 modifiers.setOperator(true); 577 token = tokenizer.requireToken(); 578 break; 579 case "inline": 580 modifiers.setInline(true); 581 token = tokenizer.requireToken(); 582 break; 583 case "suspend": 584 modifiers.setSuspend(true); 585 token = tokenizer.requireToken(); 586 break; 587 case "vararg": 588 modifiers.setVarArg(true); 589 token = tokenizer.requireToken(); 590 break; 591 default: 592 break processModifiers; 593 } 594 } 595 596 if (annotations != null) { 597 modifiers.addAnnotations(annotations); 598 } 599 600 return modifiers; 601 } 602 parseValue(String type, String val)603 private static Object parseValue(String type, String val) { 604 if (val != null) { 605 switch (type) { 606 case "boolean": 607 return "true".equals(val) ? Boolean.TRUE : Boolean.FALSE; 608 case "byte": 609 return Integer.valueOf(val); 610 case "short": 611 return Integer.valueOf(val); 612 case "int": 613 return Integer.valueOf(val); 614 case "long": 615 return Long.valueOf(val.substring(0, val.length() - 1)); 616 case "float": 617 switch (val) { 618 case "(1.0f/0.0f)": 619 case "(1.0f / 0.0f)": 620 return Float.POSITIVE_INFINITY; 621 case "(-1.0f/0.0f)": 622 case "(-1.0f / 0.0f)": 623 return Float.NEGATIVE_INFINITY; 624 case "(0.0f/0.0f)": 625 case "(0.0f / 0.0f)": 626 return Float.NaN; 627 default: 628 return Float.valueOf(val); 629 } 630 case "double": 631 switch (val) { 632 case "(1.0/0.0)": 633 case "(1.0 / 0.0)": 634 return Double.POSITIVE_INFINITY; 635 case "(-1.0/0.0)": 636 case "(-1.0 / 0.0)": 637 return Double.NEGATIVE_INFINITY; 638 case "(0.0/0.0)": 639 case "(0.0 / 0.0)": 640 return Double.NaN; 641 default: 642 return Double.valueOf(val); 643 } 644 case "char": 645 return (char) Integer.parseInt(val); 646 case JAVA_LANG_STRING: 647 case "String": 648 if ("null".equals(val)) { 649 return null; 650 } else { 651 return javaUnescapeString(val.substring(1, val.length() - 1)); 652 } 653 case "null": 654 return null; 655 default: 656 return val; 657 } 658 } 659 return null; 660 } 661 parseProperty(TextCodebase api, Tokenizer tokenizer, TextClassItem cl, String token)662 private static void parseProperty(TextCodebase api, Tokenizer tokenizer, TextClassItem cl, String token) 663 throws ApiParseException { 664 String type; 665 String name; 666 667 // Metalava: including annotations in file now 668 List<String> annotations = getAnnotations(tokenizer, token); 669 token = tokenizer.getCurrent(); 670 671 TextModifiers modifiers = parseModifiers(api, tokenizer, token, null); 672 token = tokenizer.getCurrent(); 673 assertIdent(tokenizer, token); 674 675 Pair<String, List<String>> kotlinTypeSuffix = processKotlinTypeSuffix(api, token, annotations); 676 token = kotlinTypeSuffix.getFirst(); 677 annotations = kotlinTypeSuffix.getSecond(); 678 modifiers.addAnnotations(annotations); 679 type = token; 680 TextTypeItem typeInfo = api.obtainTypeFromString(type); 681 682 token = tokenizer.requireToken(); 683 assertIdent(tokenizer, token); 684 name = token; 685 token = tokenizer.requireToken(); 686 if (!";".equals(token)) { 687 throw new ApiParseException("expected ; found " + token, tokenizer); 688 } 689 690 TextPropertyItem property = new TextPropertyItem(api, name, cl, modifiers, typeInfo, tokenizer.pos()); 691 property.setDeprecated(modifiers.isDeprecated()); 692 cl.addProperty(property); 693 } 694 parseTypeParameterList(TextCodebase codebase, Tokenizer tokenizer)695 private static TypeParameterList parseTypeParameterList(TextCodebase codebase, Tokenizer tokenizer) throws ApiParseException { 696 String token; 697 698 int start = tokenizer.offset() - 1; 699 int balance = 1; 700 while (balance > 0) { 701 token = tokenizer.requireToken(); 702 if (token.equals("<")) { 703 balance++; 704 } else if (token.equals(">")) { 705 balance--; 706 } 707 } 708 709 String typeParameterList = tokenizer.getStringFromOffset(start); 710 if (typeParameterList.isEmpty()) { 711 return TypeParameterList.Companion.getNONE(); 712 } else { 713 return TextTypeParameterList.Companion.create(codebase, null, typeParameterList); 714 } 715 } 716 parseParameterList(TextCodebase api, Tokenizer tokenizer, TextMethodItem method)717 private static void parseParameterList(TextCodebase api, Tokenizer tokenizer, TextMethodItem method) 718 throws ApiParseException { 719 String token = tokenizer.requireToken(); 720 int index = 0; 721 while (true) { 722 if (")".equals(token)) { 723 return; 724 } 725 726 // Each item can be 727 // annotations optional-modifiers type-with-use-annotations-and-generics optional-name optional-equals-default-value 728 729 // Metalava: including annotations in file now 730 List<String> annotations = getAnnotations(tokenizer, token); 731 token = tokenizer.getCurrent(); 732 733 TextModifiers modifiers = parseModifiers(api, tokenizer, token, null); 734 token = tokenizer.getCurrent(); 735 736 // Token should now represent the type 737 String type = token; 738 token = tokenizer.requireToken(); 739 if (token.startsWith("@")) { 740 // Type use annotations within the type, which broke up the tokenizer; 741 // put it back together 742 type += " " + token; 743 token = tokenizer.requireToken(); 744 if (token.startsWith("[")) { // TODO: This isn't general purpose; make requireToken smarter! 745 type += " " + token; 746 token = tokenizer.requireToken(); 747 } 748 } 749 750 Pair<String, List<String>> kotlinTypeSuffix = processKotlinTypeSuffix(api, type, annotations); 751 String typeString = kotlinTypeSuffix.getFirst(); 752 annotations = kotlinTypeSuffix.getSecond(); 753 modifiers.addAnnotations(annotations); 754 if (typeString.endsWith("...")) { 755 modifiers.setVarArg(true); 756 } 757 TextTypeItem typeInfo = api.obtainTypeFromString(typeString, 758 (TextClassItem) method.containingClass(), 759 method.typeParameterList()); 760 761 String name; 762 String publicName; 763 if (isIdent(token) && !token.equals("=")) { 764 name = token; 765 publicName = name; 766 token = tokenizer.requireToken(); 767 } else { 768 name = "arg" + (index + 1); 769 publicName = null; 770 } 771 772 String defaultValue = TextParameterItemKt.NO_DEFAULT_VALUE; 773 if ("=".equals(token)) { 774 defaultValue = tokenizer.requireToken(true); 775 StringBuilder sb = new StringBuilder(defaultValue); 776 if (defaultValue.equals("{")) { 777 int balance = 1; 778 while (balance > 0) { 779 token = tokenizer.requireToken(false, false); 780 sb.append(token); 781 if (token.equals("{")) { 782 balance++; 783 } else if (token.equals("}")) { 784 balance--; 785 if (balance == 0) { 786 break; 787 } 788 } 789 } 790 token = tokenizer.requireToken(); 791 } else { 792 int balance = defaultValue.equals("(") ? 1 : 0; 793 while (true) { 794 token = tokenizer.requireToken(true, false); 795 if (token.endsWith(",") || token.endsWith(")") && balance <= 0) { 796 if (token.length() > 1) { 797 sb.append(token, 0, token.length() - 1); 798 token = Character.toString(token.charAt(token.length() - 1)); 799 } 800 break; 801 } 802 sb.append(token); 803 if (token.equals("(")) { 804 balance++; 805 } else if (token.equals(")")) { 806 balance--; 807 } 808 } 809 } 810 defaultValue = sb.toString(); 811 } 812 813 if (",".equals(token)) { 814 token = tokenizer.requireToken(); 815 } else if (")".equals(token)) { 816 } else { 817 throw new ApiParseException("expected , or ), found " + token, tokenizer); 818 } 819 820 method.addParameter(new TextParameterItem(api, method, name, publicName, defaultValue, index, 821 typeInfo, modifiers, tokenizer.pos())); 822 if (modifiers.isVarArg()) { 823 method.setVarargs(true); 824 } 825 index++; 826 } 827 } 828 parseDefault(Tokenizer tokenizer, TextMethodItem method)829 private static String parseDefault(Tokenizer tokenizer, TextMethodItem method) 830 throws ApiParseException { 831 StringBuilder sb = new StringBuilder(); 832 while (true) { 833 String token = tokenizer.requireToken(); 834 if (";".equals(token)) { 835 method.setAnnotationDefault(sb.toString()); 836 return token; 837 } else { 838 sb.append(token); 839 } 840 } 841 } 842 parseThrows(Tokenizer tokenizer, TextMethodItem method)843 private static String parseThrows(Tokenizer tokenizer, TextMethodItem method) 844 throws ApiParseException { 845 String token = tokenizer.requireToken(); 846 boolean comma = true; 847 while (true) { 848 if (";".equals(token)) { 849 return token; 850 } else if (",".equals(token)) { 851 if (comma) { 852 throw new ApiParseException("Expected exception, got ','", tokenizer); 853 } 854 comma = true; 855 } else { 856 if (!comma) { 857 throw new ApiParseException("Expected ',' or ';' got " + token, tokenizer); 858 } 859 comma = false; 860 method.addException(token); 861 } 862 token = tokenizer.requireToken(); 863 } 864 } 865 qualifiedName(String pkg, String className)866 private static String qualifiedName(String pkg, String className) { 867 return pkg + "." + className; 868 } 869 isIdent(String token)870 private static boolean isIdent(String token) { 871 return isIdent(token.charAt(0)); 872 } 873 assertIdent(Tokenizer tokenizer, String token)874 private static void assertIdent(Tokenizer tokenizer, String token) throws ApiParseException { 875 if (!isIdent(token.charAt(0))) { 876 throw new ApiParseException("Expected identifier: " + token, tokenizer); 877 } 878 } 879 880 static class Tokenizer { 881 final char[] mBuf; 882 final String mFilename; 883 int mPos; 884 int mLine = 1; 885 Tokenizer(String filename, char[] buf)886 Tokenizer(String filename, char[] buf) { 887 mFilename = filename; 888 mBuf = buf; 889 } 890 pos()891 SourcePositionInfo pos() { 892 return new SourcePositionInfo(mFilename, mLine, 0); 893 } 894 getLine()895 public int getLine() { 896 return mLine; 897 } 898 eatWhitespace()899 boolean eatWhitespace() { 900 boolean ate = false; 901 while (mPos < mBuf.length && isSpace(mBuf[mPos])) { 902 if (mBuf[mPos] == '\n') { 903 mLine++; 904 } 905 mPos++; 906 ate = true; 907 } 908 return ate; 909 } 910 eatComment()911 boolean eatComment() { 912 if (mPos + 1 < mBuf.length) { 913 if (mBuf[mPos] == '/' && mBuf[mPos + 1] == '/') { 914 mPos += 2; 915 while (mPos < mBuf.length && !isNewline(mBuf[mPos])) { 916 mPos++; 917 } 918 return true; 919 } 920 } 921 return false; 922 } 923 eatWhitespaceAndComments()924 void eatWhitespaceAndComments() { 925 while (eatWhitespace() || eatComment()) { 926 } 927 } 928 requireToken()929 String requireToken() throws ApiParseException { 930 return requireToken(true); 931 } 932 requireToken(boolean parenIsSep)933 String requireToken(boolean parenIsSep) throws ApiParseException { 934 return requireToken(parenIsSep, true); 935 } 936 requireToken(boolean parenIsSep, boolean eatWhitespace)937 String requireToken(boolean parenIsSep, boolean eatWhitespace) throws ApiParseException { 938 final String token = getToken(parenIsSep, eatWhitespace); 939 if (token != null) { 940 return token; 941 } else { 942 throw new ApiParseException("Unexpected end of file", mLine); 943 } 944 } 945 getToken()946 String getToken() throws ApiParseException { 947 return getToken(true); 948 } 949 offset()950 int offset() { 951 return mPos; 952 } 953 getStringFromOffset(int offset)954 String getStringFromOffset(int offset) { 955 return new String(mBuf, offset, mPos - offset); 956 } 957 getToken(boolean parenIsSep)958 String getToken(boolean parenIsSep) throws ApiParseException { 959 return getToken(parenIsSep, true); 960 } 961 getCurrent()962 String getCurrent() { 963 return mCurrent; 964 } 965 966 private String mCurrent = null; 967 getToken(boolean parenIsSep, boolean eatWhitespace)968 String getToken(boolean parenIsSep, boolean eatWhitespace) throws ApiParseException { 969 if (eatWhitespace) { 970 eatWhitespaceAndComments(); 971 } 972 if (mPos >= mBuf.length) { 973 return null; 974 } 975 final int line = mLine; 976 final char c = mBuf[mPos]; 977 final int start = mPos; 978 mPos++; 979 if (c == '"') { 980 final int STATE_BEGIN = 0; 981 final int STATE_ESCAPE = 1; 982 int state = STATE_BEGIN; 983 while (true) { 984 if (mPos >= mBuf.length) { 985 throw new ApiParseException("Unexpected end of file for \" starting at " + line, mLine); 986 } 987 final char k = mBuf[mPos]; 988 if (k == '\n' || k == '\r') { 989 throw new ApiParseException("Unexpected newline for \" starting at " + line + " in " + mFilename, mLine); 990 } 991 mPos++; 992 switch (state) { 993 case STATE_BEGIN: 994 switch (k) { 995 case '\\': 996 state = STATE_ESCAPE; 997 mPos++; 998 break; 999 case '"': 1000 mCurrent = new String(mBuf, start, mPos - start); 1001 return mCurrent; 1002 } 1003 case STATE_ESCAPE: 1004 state = STATE_BEGIN; 1005 break; 1006 } 1007 } 1008 } else if (isSeparator(c, parenIsSep)) { 1009 mCurrent = Character.toString(c); 1010 return mCurrent; 1011 } else { 1012 int genericDepth = 0; 1013 do { 1014 while (mPos < mBuf.length) { 1015 char d = mBuf[mPos]; 1016 if (isSpace(d) || isSeparator(d, parenIsSep)) { 1017 break; 1018 } else if (d == '"') { 1019 // String literal in token: skip the full thing 1020 mPos++; 1021 while (mPos < mBuf.length) { 1022 if (mBuf[mPos] == '"') { 1023 mPos++; 1024 break; 1025 } else if (mBuf[mPos] == '\\') { 1026 mPos++; 1027 } 1028 mPos++; 1029 } 1030 continue; 1031 } 1032 mPos++; 1033 } 1034 if (mPos < mBuf.length) { 1035 if (mBuf[mPos] == '<') { 1036 genericDepth++; 1037 mPos++; 1038 } else if (genericDepth != 0) { 1039 if (mBuf[mPos] == '>') { 1040 genericDepth--; 1041 } 1042 mPos++; 1043 } 1044 } 1045 } while (mPos < mBuf.length 1046 && ((!isSpace(mBuf[mPos]) && !isSeparator(mBuf[mPos], parenIsSep)) || genericDepth != 0)); 1047 if (mPos >= mBuf.length) { 1048 throw new ApiParseException("Unexpected end of file for \" starting at " + line, mLine); 1049 } 1050 mCurrent = new String(mBuf, start, mPos - start); 1051 return mCurrent; 1052 } 1053 } 1054 1055 @Nullable getFileName()1056 public String getFileName() { 1057 return mFilename; 1058 } 1059 } 1060 isSpace(char c)1061 private static boolean isSpace(char c) { 1062 return c == ' ' || c == '\t' || c == '\n' || c == '\r'; 1063 } 1064 isNewline(char c)1065 private static boolean isNewline(char c) { 1066 return c == '\n' || c == '\r'; 1067 } 1068 isSeparator(char c, boolean parenIsSep)1069 private static boolean isSeparator(char c, boolean parenIsSep) { 1070 if (parenIsSep) { 1071 if (c == '(' || c == ')') { 1072 return true; 1073 } 1074 } 1075 return c == '{' || c == '}' || c == ',' || c == ';' || c == '<' || c == '>'; 1076 } 1077 isIdent(char c)1078 private static boolean isIdent(char c) { 1079 return c != '"' && !isSeparator(c, true); 1080 } 1081 } 1082