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                 default:
688                     break processModifiers;
689             }
690         }
691 
692         if (annotations != null) {
693             modifiers.addAnnotations(annotations);
694         }
695 
696         return modifiers;
697     }
698 
parseValue(String type, String val)699     private static Object parseValue(String type, String val) {
700         if (val != null) {
701             switch (type) {
702                 case "boolean":
703                     return "true".equals(val) ? Boolean.TRUE : Boolean.FALSE;
704                 case "byte":
705                     return Integer.valueOf(val);
706                 case "short":
707                     return Integer.valueOf(val);
708                 case "int":
709                     return Integer.valueOf(val);
710                 case "long":
711                     return Long.valueOf(val.substring(0, val.length() - 1));
712                 case "float":
713                     switch (val) {
714                         case "(1.0f/0.0f)":
715                         case "(1.0f / 0.0f)":
716                             return Float.POSITIVE_INFINITY;
717                         case "(-1.0f/0.0f)":
718                         case "(-1.0f / 0.0f)":
719                             return Float.NEGATIVE_INFINITY;
720                         case "(0.0f/0.0f)":
721                         case "(0.0f / 0.0f)":
722                             return Float.NaN;
723                         default:
724                             return Float.valueOf(val);
725                     }
726                 case "double":
727                     switch (val) {
728                         case "(1.0/0.0)":
729                         case "(1.0 / 0.0)":
730                             return Double.POSITIVE_INFINITY;
731                         case "(-1.0/0.0)":
732                         case "(-1.0 / 0.0)":
733                             return Double.NEGATIVE_INFINITY;
734                         case "(0.0/0.0)":
735                         case "(0.0 / 0.0)":
736                             return Double.NaN;
737                         default:
738                             return Double.valueOf(val);
739                     }
740                 case "char":
741                     return (char) Integer.parseInt(val);
742                 case JAVA_LANG_STRING:
743                 case "String":
744                     if ("null".equals(val)) {
745                         return null;
746                     } else {
747                         return javaUnescapeString(val.substring(1, val.length() - 1));
748                     }
749                 case "null":
750                     return null;
751                 default:
752                     return val;
753             }
754         }
755         return null;
756     }
757 
parseProperty(TextCodebase api, Tokenizer tokenizer, TextClassItem cl, String token)758     private static void parseProperty(TextCodebase api, Tokenizer tokenizer, TextClassItem cl, String token)
759         throws ApiParseException {
760         String type;
761         String name;
762 
763         // Metalava: including annotations in file now
764         List<String> annotations = getAnnotations(tokenizer, token);
765         token = tokenizer.getCurrent();
766 
767         TextModifiers modifiers = parseModifiers(api, tokenizer, token, null);
768         token = tokenizer.getCurrent();
769         assertIdent(tokenizer, token);
770 
771         Pair<String, List<String>> kotlinTypeSuffix = processKotlinTypeSuffix(api, token, annotations);
772         token = kotlinTypeSuffix.getFirst();
773         annotations = kotlinTypeSuffix.getSecond();
774         modifiers.addAnnotations(annotations);
775         type = token;
776         TextTypeItem typeInfo = api.obtainTypeFromString(type);
777 
778         token = tokenizer.requireToken();
779         assertIdent(tokenizer, token);
780         name = token;
781         token = tokenizer.requireToken();
782         if (!";".equals(token)) {
783             throw new ApiParseException("expected ; found " + token, tokenizer);
784         }
785 
786         TextPropertyItem property = new TextPropertyItem(api, name, cl, modifiers, typeInfo, tokenizer.pos());
787         property.setDeprecated(modifiers.isDeprecated());
788         cl.addProperty(property);
789     }
790 
parseTypeParameterList(TextCodebase codebase, Tokenizer tokenizer)791     private static TypeParameterList parseTypeParameterList(TextCodebase codebase, Tokenizer tokenizer) throws ApiParseException {
792         String token;
793 
794         int start = tokenizer.offset() - 1;
795         int balance = 1;
796         while (balance > 0) {
797             token = tokenizer.requireToken();
798             if (token.equals("<")) {
799                 balance++;
800             } else if (token.equals(">")) {
801                 balance--;
802             }
803         }
804 
805         String typeParameterList = tokenizer.getStringFromOffset(start);
806         if (typeParameterList.isEmpty()) {
807             return TypeParameterList.Companion.getNONE();
808         } else {
809             return TextTypeParameterList.Companion.create(codebase, null, typeParameterList);
810         }
811     }
812 
parseParameterList(TextCodebase api, Tokenizer tokenizer, TextMethodItem method)813     private static void parseParameterList(TextCodebase api, Tokenizer tokenizer, TextMethodItem method)
814                                            throws ApiParseException {
815         String token = tokenizer.requireToken();
816         int index = 0;
817         while (true) {
818             if (")".equals(token)) {
819                 return;
820             }
821 
822             // Each item can be
823             // annotations optional-modifiers type-with-use-annotations-and-generics optional-name optional-equals-default-value
824 
825             // Metalava: including annotations in file now
826             List<String> annotations = getAnnotations(tokenizer, token);
827             token = tokenizer.getCurrent();
828 
829             TextModifiers modifiers = parseModifiers(api, tokenizer, token, null);
830             token = tokenizer.getCurrent();
831 
832             // Token should now represent the type
833             String type = token;
834             token = tokenizer.requireToken();
835             if (token.startsWith("@")) {
836                 // Type use annotations within the type, which broke up the tokenizer;
837                 // put it back together
838                 type += " " + token;
839                 token = tokenizer.requireToken();
840                 if (token.startsWith("[")) { // TODO: This isn't general purpose; make requireToken smarter!
841                     type += " " + token;
842                     token = tokenizer.requireToken();
843                 }
844             }
845 
846             Pair<String, List<String>> kotlinTypeSuffix = processKotlinTypeSuffix(api, type, annotations);
847             String typeString = kotlinTypeSuffix.getFirst();
848             annotations = kotlinTypeSuffix.getSecond();
849             modifiers.addAnnotations(annotations);
850             if (typeString.endsWith("...")) {
851                 modifiers.setVarArg(true);
852             }
853             TextTypeItem typeInfo = api.obtainTypeFromString(typeString,
854                 (TextClassItem) method.containingClass(),
855                 method.typeParameterList());
856 
857             String name;
858             String publicName;
859             if (isIdent(token) && !token.equals("=")) {
860                 name = token;
861                 publicName = name;
862                 token = tokenizer.requireToken();
863             } else {
864                 name = "arg" + (index + 1);
865                 publicName = null;
866             }
867 
868             String defaultValue = TextParameterItemKt.NO_DEFAULT_VALUE;
869             if ("=".equals(token)) {
870                 defaultValue = tokenizer.requireToken(true);
871                 StringBuilder sb = new StringBuilder(defaultValue);
872                 if (defaultValue.equals("{")) {
873                     int balance = 1;
874                     while (balance > 0) {
875                         token = tokenizer.requireToken(false, false);
876                         sb.append(token);
877                         if (token.equals("{")) {
878                             balance++;
879                         } else if (token.equals("}")) {
880                             balance--;
881                             if (balance == 0) {
882                                 break;
883                             }
884                         }
885                     }
886                     token = tokenizer.requireToken();
887                 } else {
888                     int balance = defaultValue.equals("(") ? 1 : 0;
889                     while (true) {
890                         token = tokenizer.requireToken(true, false);
891                         if ((token.endsWith(",") || token.endsWith(")")) && balance <= 0) {
892                             if (token.length() > 1) {
893                                 sb.append(token, 0, token.length() - 1);
894                                 token = Character.toString(token.charAt(token.length() - 1));
895                             }
896                             break;
897                         }
898                         sb.append(token);
899                         if (token.equals("(")) {
900                             balance++;
901                         } else if (token.equals(")")) {
902                             balance--;
903                         }
904                     }
905                 }
906                 defaultValue = sb.toString();
907             }
908 
909             if (",".equals(token)) {
910                 token = tokenizer.requireToken();
911             } else if (")".equals(token)) {
912             } else {
913                 throw new ApiParseException("expected , or ), found " + token, tokenizer);
914             }
915 
916             method.addParameter(new TextParameterItem(api, method, name, publicName, defaultValue, index,
917                 typeInfo, modifiers, tokenizer.pos()));
918             if (modifiers.isVarArg()) {
919                 method.setVarargs(true);
920             }
921             index++;
922         }
923     }
924 
parseDefault(Tokenizer tokenizer, TextMethodItem method)925     private static String parseDefault(Tokenizer tokenizer, TextMethodItem method)
926         throws ApiParseException {
927         StringBuilder sb = new StringBuilder();
928         while (true) {
929             String token = tokenizer.requireToken();
930             if (";".equals(token)) {
931                 method.setAnnotationDefault(sb.toString());
932                 return token;
933             } else {
934                 sb.append(token);
935             }
936         }
937     }
938 
parseThrows(Tokenizer tokenizer, TextMethodItem method)939     private static String parseThrows(Tokenizer tokenizer, TextMethodItem method)
940         throws ApiParseException {
941         String token = tokenizer.requireToken();
942         boolean comma = true;
943         while (true) {
944             if (";".equals(token)) {
945                 return token;
946             } else if (",".equals(token)) {
947                 if (comma) {
948                     throw new ApiParseException("Expected exception, got ','", tokenizer);
949                 }
950                 comma = true;
951             } else {
952                 if (!comma) {
953                     throw new ApiParseException("Expected ',' or ';' got " + token, tokenizer);
954                 }
955                 comma = false;
956                 method.addException(token);
957             }
958             token = tokenizer.requireToken();
959         }
960     }
961 
qualifiedName(String pkg, String className)962     private static String qualifiedName(String pkg, String className) {
963         return pkg + "." + className;
964     }
965 
isIdent(String token)966     private static boolean isIdent(String token) {
967         return isIdent(token.charAt(0));
968     }
969 
assertIdent(Tokenizer tokenizer, String token)970     private static void assertIdent(Tokenizer tokenizer, String token) throws ApiParseException {
971         if (!isIdent(token.charAt(0))) {
972             throw new ApiParseException("Expected identifier: " + token, tokenizer);
973         }
974     }
975 
976     static class Tokenizer {
977         final char[] mBuf;
978         final String mFilename;
979         int mPos;
980         int mLine = 1;
981 
Tokenizer(String filename, char[] buf)982         Tokenizer(String filename, char[] buf) {
983             mFilename = filename;
984             mBuf = buf;
985         }
986 
pos()987         SourcePositionInfo pos() {
988             return new SourcePositionInfo(mFilename, mLine);
989         }
990 
getLine()991         public int getLine() {
992             return mLine;
993         }
994 
eatWhitespace()995         boolean eatWhitespace() {
996             boolean ate = false;
997             while (mPos < mBuf.length && isSpace(mBuf[mPos])) {
998                 if (mBuf[mPos] == '\n') {
999                     mLine++;
1000                 }
1001                 mPos++;
1002                 ate = true;
1003             }
1004             return ate;
1005         }
1006 
eatComment()1007         boolean eatComment() {
1008             if (mPos + 1 < mBuf.length) {
1009                 if (mBuf[mPos] == '/' && mBuf[mPos + 1] == '/') {
1010                     mPos += 2;
1011                     while (mPos < mBuf.length && !isNewline(mBuf[mPos])) {
1012                         mPos++;
1013                     }
1014                     return true;
1015                 }
1016             }
1017             return false;
1018         }
1019 
eatWhitespaceAndComments()1020         void eatWhitespaceAndComments() {
1021             while (eatWhitespace() || eatComment()) {
1022             }
1023         }
1024 
requireToken()1025         String requireToken() throws ApiParseException {
1026             return requireToken(true);
1027         }
1028 
requireToken(boolean parenIsSep)1029         String requireToken(boolean parenIsSep) throws ApiParseException {
1030             return requireToken(parenIsSep, true);
1031         }
1032 
requireToken(boolean parenIsSep, boolean eatWhitespace)1033         String requireToken(boolean parenIsSep, boolean eatWhitespace) throws ApiParseException {
1034             final String token = getToken(parenIsSep, eatWhitespace);
1035             if (token != null) {
1036                 return token;
1037             } else {
1038                 throw new ApiParseException("Unexpected end of file", this);
1039             }
1040         }
1041 
getToken()1042         String getToken() throws ApiParseException {
1043             return getToken(true);
1044         }
1045 
offset()1046         int offset() {
1047             return mPos;
1048         }
1049 
getStringFromOffset(int offset)1050         String getStringFromOffset(int offset) {
1051             return new String(mBuf, offset, mPos - offset);
1052         }
1053 
getToken(boolean parenIsSep)1054         String getToken(boolean parenIsSep) throws ApiParseException {
1055             return getToken(parenIsSep, true);
1056         }
1057 
getCurrent()1058         String getCurrent() {
1059             return mCurrent;
1060         }
1061 
1062         private String mCurrent = null;
1063 
getToken(boolean parenIsSep, boolean eatWhitespace)1064         String getToken(boolean parenIsSep, boolean eatWhitespace) throws ApiParseException {
1065             if (eatWhitespace) {
1066                 eatWhitespaceAndComments();
1067             }
1068             if (mPos >= mBuf.length) {
1069                 return null;
1070             }
1071             final int line = mLine;
1072             final char c = mBuf[mPos];
1073             final int start = mPos;
1074             mPos++;
1075             if (c == '"') {
1076                 final int STATE_BEGIN = 0;
1077                 final int STATE_ESCAPE = 1;
1078                 int state = STATE_BEGIN;
1079                 while (true) {
1080                     if (mPos >= mBuf.length) {
1081                         throw new ApiParseException("Unexpected end of file for \" starting at " + line, this);
1082                     }
1083                     final char k = mBuf[mPos];
1084                     if (k == '\n' || k == '\r') {
1085                         throw new ApiParseException("Unexpected newline for \" starting at " + line +" in " + mFilename, this);
1086                     }
1087                     mPos++;
1088                     switch (state) {
1089                         case STATE_BEGIN:
1090                             switch (k) {
1091                                 case '\\':
1092                                     state = STATE_ESCAPE;
1093                                     break;
1094                                 case '"':
1095                                     mCurrent = new String(mBuf, start, mPos - start);
1096                                     return mCurrent;
1097                             }
1098                             break;
1099                         case STATE_ESCAPE:
1100                             state = STATE_BEGIN;
1101                             break;
1102                     }
1103                 }
1104             } else if (isSeparator(c, parenIsSep)) {
1105                 mCurrent = Character.toString(c);
1106                 return mCurrent;
1107             } else {
1108                 int genericDepth = 0;
1109                 do {
1110                     while (mPos < mBuf.length) {
1111                         char d = mBuf[mPos];
1112                         if (isSpace(d) || isSeparator(d, parenIsSep)) {
1113                             break;
1114                         } else if (d == '"') {
1115                             // String literal in token: skip the full thing
1116                             mPos++;
1117                             while (mPos < mBuf.length) {
1118                                 if (mBuf[mPos] == '"') {
1119                                     mPos++;
1120                                     break;
1121                                 } else if (mBuf[mPos] == '\\') {
1122                                     mPos++;
1123                                 }
1124                                 mPos++;
1125                             }
1126                             continue;
1127                         }
1128                         mPos++;
1129                     }
1130                     if (mPos < mBuf.length) {
1131                         if (mBuf[mPos] == '<') {
1132                             genericDepth++;
1133                             mPos++;
1134                         } else if (genericDepth != 0) {
1135                             if (mBuf[mPos] == '>') {
1136                                 genericDepth--;
1137                             }
1138                             mPos++;
1139                         }
1140                     }
1141                 } while (mPos < mBuf.length
1142                     && ((!isSpace(mBuf[mPos]) && !isSeparator(mBuf[mPos], parenIsSep)) || genericDepth != 0));
1143                 if (mPos >= mBuf.length) {
1144                     throw new ApiParseException("Unexpected end of file for \" starting at " + line, this);
1145                 }
1146                 mCurrent = new String(mBuf, start, mPos - start);
1147                 return mCurrent;
1148             }
1149         }
1150 
1151         @Nullable
getFileName()1152         public String getFileName() {
1153             return mFilename;
1154         }
1155     }
1156 
isSpace(char c)1157     private static boolean isSpace(char c) {
1158         return c == ' ' || c == '\t' || c == '\n' || c == '\r';
1159     }
1160 
isNewline(char c)1161     private static boolean isNewline(char c) {
1162         return c == '\n' || c == '\r';
1163     }
1164 
isSeparator(char c, boolean parenIsSep)1165     private static boolean isSeparator(char c, boolean parenIsSep) {
1166         if (parenIsSep) {
1167             if (c == '(' || c == ')') {
1168                 return true;
1169             }
1170         }
1171         return c == '{' || c == '}' || c == ',' || c == ';' || c == '<' || c == '>';
1172     }
1173 
isIdent(char c)1174     private static boolean isIdent(char c) {
1175         return c != '"' && !isSeparator(c, true);
1176     }
1177 }
1178