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 case "fun": 688 modifiers.setFunctional(true); 689 token = tokenizer.requireToken(); 690 break; 691 default: 692 break processModifiers; 693 } 694 } 695 696 if (annotations != null) { 697 modifiers.addAnnotations(annotations); 698 } 699 700 return modifiers; 701 } 702 parseValue(String type, String val)703 private static Object parseValue(String type, String val) { 704 if (val != null) { 705 switch (type) { 706 case "boolean": 707 return "true".equals(val) ? Boolean.TRUE : Boolean.FALSE; 708 case "byte": 709 return Integer.valueOf(val); 710 case "short": 711 return Integer.valueOf(val); 712 case "int": 713 return Integer.valueOf(val); 714 case "long": 715 return Long.valueOf(val.substring(0, val.length() - 1)); 716 case "float": 717 switch (val) { 718 case "(1.0f/0.0f)": 719 case "(1.0f / 0.0f)": 720 return Float.POSITIVE_INFINITY; 721 case "(-1.0f/0.0f)": 722 case "(-1.0f / 0.0f)": 723 return Float.NEGATIVE_INFINITY; 724 case "(0.0f/0.0f)": 725 case "(0.0f / 0.0f)": 726 return Float.NaN; 727 default: 728 return Float.valueOf(val); 729 } 730 case "double": 731 switch (val) { 732 case "(1.0/0.0)": 733 case "(1.0 / 0.0)": 734 return Double.POSITIVE_INFINITY; 735 case "(-1.0/0.0)": 736 case "(-1.0 / 0.0)": 737 return Double.NEGATIVE_INFINITY; 738 case "(0.0/0.0)": 739 case "(0.0 / 0.0)": 740 return Double.NaN; 741 default: 742 return Double.valueOf(val); 743 } 744 case "char": 745 return (char) Integer.parseInt(val); 746 case JAVA_LANG_STRING: 747 case "String": 748 if ("null".equals(val)) { 749 return null; 750 } else { 751 return javaUnescapeString(val.substring(1, val.length() - 1)); 752 } 753 case "null": 754 return null; 755 default: 756 return val; 757 } 758 } 759 return null; 760 } 761 parseProperty(TextCodebase api, Tokenizer tokenizer, TextClassItem cl, String token)762 private static void parseProperty(TextCodebase api, Tokenizer tokenizer, TextClassItem cl, String token) 763 throws ApiParseException { 764 String type; 765 String name; 766 767 // Metalava: including annotations in file now 768 List<String> annotations = getAnnotations(tokenizer, token); 769 token = tokenizer.getCurrent(); 770 771 TextModifiers modifiers = parseModifiers(api, tokenizer, token, null); 772 token = tokenizer.getCurrent(); 773 assertIdent(tokenizer, token); 774 775 Pair<String, List<String>> kotlinTypeSuffix = processKotlinTypeSuffix(api, token, annotations); 776 token = kotlinTypeSuffix.getFirst(); 777 annotations = kotlinTypeSuffix.getSecond(); 778 modifiers.addAnnotations(annotations); 779 type = token; 780 TextTypeItem typeInfo = api.obtainTypeFromString(type); 781 782 token = tokenizer.requireToken(); 783 assertIdent(tokenizer, token); 784 name = token; 785 token = tokenizer.requireToken(); 786 if (!";".equals(token)) { 787 throw new ApiParseException("expected ; found " + token, tokenizer); 788 } 789 790 TextPropertyItem property = new TextPropertyItem(api, name, cl, modifiers, typeInfo, tokenizer.pos()); 791 property.setDeprecated(modifiers.isDeprecated()); 792 cl.addProperty(property); 793 } 794 parseTypeParameterList(TextCodebase codebase, Tokenizer tokenizer)795 private static TypeParameterList parseTypeParameterList(TextCodebase codebase, Tokenizer tokenizer) throws ApiParseException { 796 String token; 797 798 int start = tokenizer.offset() - 1; 799 int balance = 1; 800 while (balance > 0) { 801 token = tokenizer.requireToken(); 802 if (token.equals("<")) { 803 balance++; 804 } else if (token.equals(">")) { 805 balance--; 806 } 807 } 808 809 String typeParameterList = tokenizer.getStringFromOffset(start); 810 if (typeParameterList.isEmpty()) { 811 return TypeParameterList.Companion.getNONE(); 812 } else { 813 return TextTypeParameterList.Companion.create(codebase, null, typeParameterList); 814 } 815 } 816 parseParameterList(TextCodebase api, Tokenizer tokenizer, TextMethodItem method)817 private static void parseParameterList(TextCodebase api, Tokenizer tokenizer, TextMethodItem method) 818 throws ApiParseException { 819 String token = tokenizer.requireToken(); 820 int index = 0; 821 while (true) { 822 if (")".equals(token)) { 823 return; 824 } 825 826 // Each item can be 827 // optional annotations optional-modifiers type-with-use-annotations-and-generics optional-name optional-equals-default-value 828 829 // Used to represent the presence of a default value, instead of showing the entire 830 // default value 831 boolean hasDefaultValue = token.equals("optional"); 832 if (hasDefaultValue) { token = tokenizer.requireToken(); } 833 834 // Metalava: including annotations in file now 835 List<String> annotations = getAnnotations(tokenizer, token); 836 token = tokenizer.getCurrent(); 837 838 TextModifiers modifiers = parseModifiers(api, tokenizer, token, null); 839 token = tokenizer.getCurrent(); 840 841 // Token should now represent the type 842 String type = token; 843 token = tokenizer.requireToken(); 844 if (token.startsWith("@")) { 845 // Type use annotations within the type, which broke up the tokenizer; 846 // put it back together 847 type += " " + token; 848 token = tokenizer.requireToken(); 849 if (token.startsWith("[")) { // TODO: This isn't general purpose; make requireToken smarter! 850 type += " " + token; 851 token = tokenizer.requireToken(); 852 } 853 } 854 855 Pair<String, List<String>> kotlinTypeSuffix = processKotlinTypeSuffix(api, type, annotations); 856 String typeString = kotlinTypeSuffix.getFirst(); 857 annotations = kotlinTypeSuffix.getSecond(); 858 modifiers.addAnnotations(annotations); 859 if (typeString.endsWith("...")) { 860 modifiers.setVarArg(true); 861 } 862 TextTypeItem typeInfo = api.obtainTypeFromString(typeString, 863 (TextClassItem) method.containingClass(), 864 method.typeParameterList()); 865 866 String name; 867 String publicName; 868 if (isIdent(token) && !token.equals("=")) { 869 name = token; 870 publicName = name; 871 token = tokenizer.requireToken(); 872 } else { 873 name = "arg" + (index + 1); 874 publicName = null; 875 } 876 877 String defaultValue = TextParameterItemKt.UNKNOWN_DEFAULT_VALUE; 878 if ("=".equals(token)) { 879 defaultValue = tokenizer.requireToken(true); 880 StringBuilder sb = new StringBuilder(defaultValue); 881 if (defaultValue.equals("{")) { 882 int balance = 1; 883 while (balance > 0) { 884 token = tokenizer.requireToken(false, false); 885 sb.append(token); 886 if (token.equals("{")) { 887 balance++; 888 } else if (token.equals("}")) { 889 balance--; 890 if (balance == 0) { 891 break; 892 } 893 } 894 } 895 token = tokenizer.requireToken(); 896 } else { 897 int balance = defaultValue.equals("(") ? 1 : 0; 898 while (true) { 899 token = tokenizer.requireToken(true, false); 900 if ((token.endsWith(",") || token.endsWith(")")) && balance <= 0) { 901 if (token.length() > 1) { 902 sb.append(token, 0, token.length() - 1); 903 token = Character.toString(token.charAt(token.length() - 1)); 904 } 905 break; 906 } 907 sb.append(token); 908 if (token.equals("(")) { 909 balance++; 910 } else if (token.equals(")")) { 911 balance--; 912 } 913 } 914 } 915 defaultValue = sb.toString(); 916 } 917 918 if (!defaultValue.equals(TextParameterItemKt.UNKNOWN_DEFAULT_VALUE)) { 919 hasDefaultValue = true; 920 } 921 922 if (",".equals(token)) { 923 token = tokenizer.requireToken(); 924 } else if (")".equals(token)) { 925 } else { 926 throw new ApiParseException("expected , or ), found " + token, tokenizer); 927 } 928 929 method.addParameter(new TextParameterItem(api, method, name, publicName, hasDefaultValue, defaultValue, index, 930 typeInfo, modifiers, tokenizer.pos())); 931 if (modifiers.isVarArg()) { 932 method.setVarargs(true); 933 } 934 index++; 935 } 936 } 937 parseDefault(Tokenizer tokenizer, TextMethodItem method)938 private static String parseDefault(Tokenizer tokenizer, TextMethodItem method) 939 throws ApiParseException { 940 StringBuilder sb = new StringBuilder(); 941 while (true) { 942 String token = tokenizer.requireToken(); 943 if (";".equals(token)) { 944 method.setAnnotationDefault(sb.toString()); 945 return token; 946 } else { 947 sb.append(token); 948 } 949 } 950 } 951 parseThrows(Tokenizer tokenizer, TextMethodItem method)952 private static String parseThrows(Tokenizer tokenizer, TextMethodItem method) 953 throws ApiParseException { 954 String token = tokenizer.requireToken(); 955 boolean comma = true; 956 while (true) { 957 if (";".equals(token)) { 958 return token; 959 } else if (",".equals(token)) { 960 if (comma) { 961 throw new ApiParseException("Expected exception, got ','", tokenizer); 962 } 963 comma = true; 964 } else { 965 if (!comma) { 966 throw new ApiParseException("Expected ',' or ';' got " + token, tokenizer); 967 } 968 comma = false; 969 method.addException(token); 970 } 971 token = tokenizer.requireToken(); 972 } 973 } 974 qualifiedName(String pkg, String className)975 private static String qualifiedName(String pkg, String className) { 976 return pkg + "." + className; 977 } 978 isIdent(String token)979 private static boolean isIdent(String token) { 980 return isIdent(token.charAt(0)); 981 } 982 assertIdent(Tokenizer tokenizer, String token)983 private static void assertIdent(Tokenizer tokenizer, String token) throws ApiParseException { 984 if (!isIdent(token.charAt(0))) { 985 throw new ApiParseException("Expected identifier: " + token, tokenizer); 986 } 987 } 988 989 static class Tokenizer { 990 final char[] mBuf; 991 final String mFilename; 992 int mPos; 993 int mLine = 1; 994 Tokenizer(String filename, char[] buf)995 Tokenizer(String filename, char[] buf) { 996 mFilename = filename; 997 mBuf = buf; 998 } 999 pos()1000 SourcePositionInfo pos() { 1001 return new SourcePositionInfo(mFilename, mLine); 1002 } 1003 getLine()1004 public int getLine() { 1005 return mLine; 1006 } 1007 eatWhitespace()1008 boolean eatWhitespace() { 1009 boolean ate = false; 1010 while (mPos < mBuf.length && isSpace(mBuf[mPos])) { 1011 if (mBuf[mPos] == '\n') { 1012 mLine++; 1013 } 1014 mPos++; 1015 ate = true; 1016 } 1017 return ate; 1018 } 1019 eatComment()1020 boolean eatComment() { 1021 if (mPos + 1 < mBuf.length) { 1022 if (mBuf[mPos] == '/' && mBuf[mPos + 1] == '/') { 1023 mPos += 2; 1024 while (mPos < mBuf.length && !isNewline(mBuf[mPos])) { 1025 mPos++; 1026 } 1027 return true; 1028 } 1029 } 1030 return false; 1031 } 1032 eatWhitespaceAndComments()1033 void eatWhitespaceAndComments() { 1034 while (eatWhitespace() || eatComment()) { 1035 } 1036 } 1037 requireToken()1038 String requireToken() throws ApiParseException { 1039 return requireToken(true); 1040 } 1041 requireToken(boolean parenIsSep)1042 String requireToken(boolean parenIsSep) throws ApiParseException { 1043 return requireToken(parenIsSep, true); 1044 } 1045 requireToken(boolean parenIsSep, boolean eatWhitespace)1046 String requireToken(boolean parenIsSep, boolean eatWhitespace) throws ApiParseException { 1047 final String token = getToken(parenIsSep, eatWhitespace); 1048 if (token != null) { 1049 return token; 1050 } else { 1051 throw new ApiParseException("Unexpected end of file", this); 1052 } 1053 } 1054 getToken()1055 String getToken() throws ApiParseException { 1056 return getToken(true); 1057 } 1058 offset()1059 int offset() { 1060 return mPos; 1061 } 1062 getStringFromOffset(int offset)1063 String getStringFromOffset(int offset) { 1064 return new String(mBuf, offset, mPos - offset); 1065 } 1066 getToken(boolean parenIsSep)1067 String getToken(boolean parenIsSep) throws ApiParseException { 1068 return getToken(parenIsSep, true); 1069 } 1070 getCurrent()1071 String getCurrent() { 1072 return mCurrent; 1073 } 1074 1075 private String mCurrent = null; 1076 getToken(boolean parenIsSep, boolean eatWhitespace)1077 String getToken(boolean parenIsSep, boolean eatWhitespace) throws ApiParseException { 1078 if (eatWhitespace) { 1079 eatWhitespaceAndComments(); 1080 } 1081 if (mPos >= mBuf.length) { 1082 return null; 1083 } 1084 final int line = mLine; 1085 final char c = mBuf[mPos]; 1086 final int start = mPos; 1087 mPos++; 1088 if (c == '"') { 1089 final int STATE_BEGIN = 0; 1090 final int STATE_ESCAPE = 1; 1091 int state = STATE_BEGIN; 1092 while (true) { 1093 if (mPos >= mBuf.length) { 1094 throw new ApiParseException("Unexpected end of file for \" starting at " + line, this); 1095 } 1096 final char k = mBuf[mPos]; 1097 if (k == '\n' || k == '\r') { 1098 throw new ApiParseException("Unexpected newline for \" starting at " + line +" in " + mFilename, this); 1099 } 1100 mPos++; 1101 switch (state) { 1102 case STATE_BEGIN: 1103 switch (k) { 1104 case '\\': 1105 state = STATE_ESCAPE; 1106 break; 1107 case '"': 1108 mCurrent = new String(mBuf, start, mPos - start); 1109 return mCurrent; 1110 } 1111 break; 1112 case STATE_ESCAPE: 1113 state = STATE_BEGIN; 1114 break; 1115 } 1116 } 1117 } else if (isSeparator(c, parenIsSep)) { 1118 mCurrent = Character.toString(c); 1119 return mCurrent; 1120 } else { 1121 int genericDepth = 0; 1122 do { 1123 while (mPos < mBuf.length) { 1124 char d = mBuf[mPos]; 1125 if (isSpace(d) || isSeparator(d, parenIsSep)) { 1126 break; 1127 } else if (d == '"') { 1128 // String literal in token: skip the full thing 1129 mPos++; 1130 while (mPos < mBuf.length) { 1131 if (mBuf[mPos] == '"') { 1132 mPos++; 1133 break; 1134 } else if (mBuf[mPos] == '\\') { 1135 mPos++; 1136 } 1137 mPos++; 1138 } 1139 continue; 1140 } 1141 mPos++; 1142 } 1143 if (mPos < mBuf.length) { 1144 if (mBuf[mPos] == '<') { 1145 genericDepth++; 1146 mPos++; 1147 } else if (genericDepth != 0) { 1148 if (mBuf[mPos] == '>') { 1149 genericDepth--; 1150 } 1151 mPos++; 1152 } 1153 } 1154 } while (mPos < mBuf.length 1155 && ((!isSpace(mBuf[mPos]) && !isSeparator(mBuf[mPos], parenIsSep)) || genericDepth != 0)); 1156 if (mPos >= mBuf.length) { 1157 throw new ApiParseException("Unexpected end of file for \" starting at " + line, this); 1158 } 1159 mCurrent = new String(mBuf, start, mPos - start); 1160 return mCurrent; 1161 } 1162 } 1163 1164 @Nullable getFileName()1165 public String getFileName() { 1166 return mFilename; 1167 } 1168 } 1169 isSpace(char c)1170 private static boolean isSpace(char c) { 1171 return c == ' ' || c == '\t' || c == '\n' || c == '\r'; 1172 } 1173 isNewline(char c)1174 private static boolean isNewline(char c) { 1175 return c == '\n' || c == '\r'; 1176 } 1177 isSeparator(char c, boolean parenIsSep)1178 private static boolean isSeparator(char c, boolean parenIsSep) { 1179 if (parenIsSep) { 1180 if (c == '(' || c == ')') { 1181 return true; 1182 } 1183 } 1184 return c == '{' || c == '}' || c == ',' || c == ';' || c == '<' || c == '>'; 1185 } 1186 isIdent(char c)1187 private static boolean isIdent(char c) { 1188 return c != '"' && !isSeparator(c, true); 1189 } 1190 } 1191