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