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