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