• 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) = 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