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