• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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