1 /*
2  * Copyright (C) 2019 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.class2nonsdklist;
18 
19 import com.google.common.base.Strings;
20 
21 import java.util.HashSet;
22 import java.util.List;
23 import java.util.Set;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26 import java.util.stream.Collectors;
27 
28 public class ApiResolver {
29     private final List<ApiComponents> mPotentialPublicAlternatives;
30     private final Set<PackageAndClassName> mPublicApiClasses;
31 
32     private static final Pattern LINK_TAG_PATTERN = Pattern.compile("\\{@link ([^\\}]+)\\}");
33     private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code ([^\\}]+)\\}");
34     private static final Integer MIN_SDK_REQUIRING_PUBLIC_ALTERNATIVES = 29;
35 
ApiResolver()36     public ApiResolver() {
37         mPotentialPublicAlternatives = null;
38         mPublicApiClasses = null;
39     }
40 
ApiResolver(Set<String> publicApis)41     public ApiResolver(Set<String> publicApis) {
42         mPotentialPublicAlternatives = publicApis.stream()
43                 .map(api -> {
44                     try {
45                         return ApiComponents.fromDexSignature(api);
46                     } catch (SignatureSyntaxError e) {
47                         throw new RuntimeException("Could not parse public API signature:", e);
48                     }
49                 })
50                 .collect(Collectors.toList());
51         mPublicApiClasses = mPotentialPublicAlternatives.stream()
52                 .map(api -> api.getPackageAndClassName())
53                 .collect(Collectors.toCollection(HashSet::new));
54     }
55 
56     /**
57      * Verify that all public alternatives are valid.
58      *
59      * @param publicAlternativesString String containing public alternative explanations.
60      * @param signature                Signature of the member that has the annotation.
61      */
resolvePublicAlternatives(String publicAlternativesString, String signature, Integer maxSdkVersion)62     public void resolvePublicAlternatives(String publicAlternativesString, String signature,
63                                           Integer maxSdkVersion)
64             throws JavadocLinkSyntaxError, AlternativeNotFoundError,
65                     RequiredAlternativeNotSpecifiedError, MultipleAlternativesFoundWarning {
66         if (Strings.isNullOrEmpty(publicAlternativesString) && maxSdkVersion != null
67                 && maxSdkVersion >= MIN_SDK_REQUIRING_PUBLIC_ALTERNATIVES) {
68             throw new RequiredAlternativeNotSpecifiedError();
69         }
70         if (publicAlternativesString != null && mPotentialPublicAlternatives != null) {
71             // Grab all instances of type {@link foo}
72             Matcher matcher = LINK_TAG_PATTERN.matcher(publicAlternativesString);
73             boolean hasLinkAlternative = false;
74             // Validate all link tags
75             while (matcher.find()) {
76                 hasLinkAlternative = true;
77                 String alternativeString = matcher.group(1);
78                 ApiComponents alternative = ApiComponents.fromLinkTag(alternativeString,
79                         signature);
80                 if (alternative.getMemberName().isEmpty()) {
81                     // Provided class as alternative
82                     if (!mPublicApiClasses.contains(alternative.getPackageAndClassName())) {
83                         throw new ClassAlternativeNotFoundError(alternative);
84                     }
85                 } else if (!mPotentialPublicAlternatives.contains(alternative)) {
86                     // If the link is not a public alternative, it must because the link does not
87                     // contain the method parameter types, e.g. {@link foo.bar.Baz#foo} instead of
88                     // {@link foo.bar.Baz#foo(int)}. If the method name is unique within the class,
89                     // we can handle it.
90                     if (!Strings.isNullOrEmpty(alternative.getMethodParameterTypes())) {
91                         throw new MemberAlternativeNotFoundError(alternative);
92                     }
93                     List<ApiComponents> almostMatches = mPotentialPublicAlternatives.stream()
94                             .filter(api -> api.equalsIgnoringParam(alternative))
95                             .collect(Collectors.toList());
96                     if (almostMatches.size() == 0) {
97                         throw new MemberAlternativeNotFoundError(alternative);
98                     } else if (almostMatches.size() > 1) {
99                         throw new MultipleAlternativesFoundWarning(alternative, almostMatches);
100                     }
101                 }
102             }
103             // No {@link ...} alternatives exist; try looking for {@code ...}
104             if (!hasLinkAlternative) {
105                 if (!CODE_TAG_PATTERN.matcher(publicAlternativesString).find()) {
106                     throw new NoAlternativesSpecifiedError();
107                 }
108             }
109         }
110     }
111 }
112