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