| // Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| package com.android.tools.r8.apimodel; |
| |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assume.assumeFalse; |
| |
| import com.android.tools.r8.apimodel.AndroidApiVersionsXmlParser.ParsingException; |
| import com.android.tools.r8.apimodel.AndroidApiVersionsXmlParser.ParsingListener; |
| import com.android.tools.r8.references.ClassReference; |
| import com.android.tools.r8.utils.AndroidApiLevel; |
| import com.android.tools.r8.utils.codeinspector.ClassSubject; |
| import com.android.tools.r8.utils.codeinspector.CodeInspector; |
| import com.android.tools.r8.utils.codeinspector.FieldSubject; |
| import com.google.common.collect.ImmutableMap; |
| import java.io.IOException; |
| import java.nio.file.Path; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| public class AndroidApiVersionsXmlParserChecked { |
| |
| /** |
| * Wrapper of {@link AndroidApiVersionsXmlParser} with sanity checks against an android.jar and |
| * max API. |
| */ |
| public static List<ParsedApiClass> parse( |
| Path xmlPath, Path androidJar, AndroidApiLevel maxApiLevel, boolean ignoreExemptionList) |
| throws ParsingException, IOException { |
| return new AndroidApiVersionsXmlParserChecked(androidJar, maxApiLevel, ignoreExemptionList) |
| .parse(xmlPath); |
| } |
| |
| private final AndroidApiLevel maxApiLevel; |
| private final ParsingListener listener; |
| private final CodeInspector inspector; |
| private final Map<String, Integer> knownMissingClassesInJar; |
| |
| private AndroidApiVersionsXmlParserChecked( |
| Path androidJar, AndroidApiLevel maxApiLevel, boolean ignoreExemptionList) |
| throws IOException { |
| this.maxApiLevel = maxApiLevel; |
| this.listener = createListener(); |
| this.inspector = new CodeInspector(androidJar); |
| this.knownMissingClassesInJar = getKnownMissingClasses(ignoreExemptionList); |
| } |
| |
| /** Also updates the hit count for exemptions. */ |
| private boolean isKnownMissingInJar(String className) { |
| if (knownMissingClassesInJar.containsKey(className)) { |
| knownMissingClassesInJar.put(className, knownMissingClassesInJar.get(className) + 1); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| private AndroidApiVersionsXmlParser.ParsingListener createListener() { |
| return new ParsingListener() { |
| @Override |
| public ProcessingEvent startedProcessingClass(ClassReference reference) { |
| ClassSubject clazz = inspector.clazz(reference); |
| String className = clazz.getOriginalTypeName(); |
| if (clazz.isPresent()) { |
| return ProcessingEvent.CONTINUE; |
| } else { |
| assertTrue( |
| className + " should either be present in jar or marked as an exemption", |
| isKnownMissingInJar(className)); |
| return ProcessingEvent.SKIP; |
| } |
| } |
| |
| @Override |
| public void skippingRemovedClass(ClassReference reference, AndroidApiLevel removedAt) { |
| ClassSubject clazz = inspector.clazz(reference); |
| assumeFalse(reference + " not expected in jar", clazz.isPresent()); |
| assert removedAt.isLessThanOrEqualTo(maxApiLevel) |
| : removedAt + " is newer than max (" + maxApiLevel + ")"; |
| } |
| |
| @Override |
| public ProcessingEvent startedProcessingField(FieldTypelessReference reference) { |
| ClassReference classReference = reference.getHolderClass(); |
| ClassSubject clazz = inspector.clazz(classReference); |
| String className = clazz.getOriginalTypeName(); |
| if (clazz.isPresent()) { |
| FieldSubject field = clazz.uniqueFieldWithOriginalName(reference.getFieldName()); |
| assertTrue(field + " should be present", field.isPresent()); |
| return ProcessingEvent.CONTINUE; |
| } else { |
| assertTrue( |
| className + " should either be present in jar or marked as an exemption", |
| isKnownMissingInJar(className)); |
| // Class is exempted and therefore fields of it is too. |
| return ProcessingEvent.SKIP; |
| } |
| } |
| |
| @Override |
| public void skippingRemovedField( |
| FieldTypelessReference reference, AndroidApiLevel removedAt) { |
| ClassReference classReference = reference.getHolderClass(); |
| ClassSubject clazz = inspector.clazz(classReference); |
| assertTrue(classReference + " should be present", clazz.isPresent()); |
| FieldSubject field = clazz.uniqueFieldWithOriginalName(reference.getFieldName()); |
| assertFalse(field + " should be present", field.isPresent()); |
| assert removedAt.isLessThanOrEqualTo(maxApiLevel) |
| : removedAt + " is newer than max (" + maxApiLevel + ")"; |
| } |
| }; |
| } |
| |
| private List<ParsedApiClass> parse(Path xmlPath) throws ParsingException { |
| List<ParsedApiClass> parsedClasses = AndroidApiVersionsXmlParser.parse(xmlPath, listener); |
| knownMissingClassesInJar.forEach( |
| (exemption, hitCount) -> |
| assertTrue("Unused exemption: " + exemption + "(" + hitCount + ")", hitCount > 0)); |
| return parsedClasses; |
| } |
| |
| private Map<String, Integer> getKnownMissingClasses(boolean ignoreExemptionList) { |
| if (ignoreExemptionList) { |
| return ImmutableMap.of(); |
| } |
| Map<String, Integer> classes = new LinkedHashMap<>(); |
| classes.put("android.test.ActivityInstrumentationTestCase", 0); |
| classes.put("android.test.ActivityInstrumentationTestCase2", 0); |
| classes.put("android.test.ActivityTestCase", 0); |
| classes.put("android.test.ActivityUnitTestCase", 0); |
| classes.put("android.test.AndroidTestCase", 0); |
| classes.put("android.test.AndroidTestRunner", 0); |
| classes.put("android.test.ApplicationTestCase", 0); |
| classes.put("android.test.AssertionFailedError", 0); |
| classes.put("android.test.ComparisonFailure", 0); |
| classes.put("android.test.FlakyTest", 0); |
| classes.put("android.test.InstrumentationTestCase", 0); |
| classes.put("android.test.InstrumentationTestRunner", 0); |
| classes.put("android.test.InstrumentationTestSuite", 0); |
| classes.put("android.test.IsolatedContext", 0); |
| classes.put("android.test.LaunchPerformanceBase", 0); |
| classes.put("android.test.LoaderTestCase", 0); |
| classes.put("android.test.MoreAsserts", 0); |
| classes.put("android.test.PerformanceTestCase", 0); |
| classes.put("android.test.PerformanceTestCase$Intermediates", 0); |
| classes.put("android.test.ProviderTestCase", 0); |
| classes.put("android.test.ProviderTestCase2", 0); |
| classes.put("android.test.RenamingDelegatingContext", 0); |
| classes.put("android.test.RepetitiveTest", 0); |
| classes.put("android.test.ServiceTestCase", 0); |
| classes.put("android.test.SingleLaunchActivityTestCase", 0); |
| classes.put("android.test.SyncBaseInstrumentation", 0); |
| classes.put("android.test.TestSuiteProvider", 0); |
| classes.put("android.test.TouchUtils", 0); |
| classes.put("android.test.UiThreadTest", 0); |
| classes.put("android.test.ViewAsserts", 0); |
| classes.put("android.test.mock.MockAccountManager", 0); |
| classes.put("android.test.mock.MockApplication", 0); |
| classes.put("android.test.mock.MockContentProvider", 0); |
| classes.put("android.test.mock.MockContentResolver", 0); |
| classes.put("android.test.mock.MockContext", 0); |
| classes.put("android.test.mock.MockCursor", 0); |
| classes.put("android.test.mock.MockDialogInterface", 0); |
| classes.put("android.test.mock.MockPackageManager", 0); |
| classes.put("android.test.mock.MockResources", 0); |
| classes.put("android.test.mock.MockService", 0); |
| classes.put("android.test.suitebuilder.TestMethod", 0); |
| classes.put("android.test.suitebuilder.TestSuiteBuilder", 0); |
| classes.put("android.test.suitebuilder.TestSuiteBuilder$FailedToCreateTests", 0); |
| classes.put("android.test.suitebuilder.UnitTestSuiteBuilder", 0); |
| classes.put("android.test.suitebuilder.annotation.LargeTest", 0); |
| classes.put("android.test.suitebuilder.annotation.MediumTest", 0); |
| classes.put("android.test.suitebuilder.annotation.SmallTest", 0); |
| classes.put("android.test.suitebuilder.annotation.Smoke", 0); |
| classes.put("android.test.suitebuilder.annotation.Suppress", 0); |
| classes.put("com.android.internal.util.Predicate", 0); |
| classes.put("junit.framework.Assert", 0); |
| classes.put("junit.framework.AssertionFailedError", 0); |
| classes.put("junit.framework.ComparisonFailure", 0); |
| classes.put("junit.framework.Protectable", 0); |
| classes.put("junit.framework.Test", 0); |
| classes.put("junit.framework.TestCase", 0); |
| classes.put("junit.framework.TestFailure", 0); |
| classes.put("junit.framework.TestListener", 0); |
| classes.put("junit.framework.TestResult", 0); |
| classes.put("junit.framework.TestSuite", 0); |
| classes.put("junit.runner.BaseTestRunner", 0); |
| classes.put("junit.runner.TestSuiteLoader", 0); |
| classes.put("junit.runner.Version", 0); |
| return classes; |
| } |
| } |