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