| /* |
| * Copyright 2019 The Bazel Authors. All rights reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.google.devtools.build.android.desugar; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.annotations.UsedReflectively; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableListMultimap; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.LinkedHashMultimap; |
| import com.google.common.collect.Multimap; |
| import com.google.devtools.build.runtime.RunfilesPaths; |
| import com.google.errorprone.annotations.FormatMethod; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.annotation.Annotation; |
| import java.lang.annotation.Documented; |
| import java.lang.annotation.ElementType; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.annotation.Target; |
| import java.lang.invoke.MethodHandle; |
| import java.lang.invoke.MethodHandles; |
| import java.lang.invoke.MethodHandles.Lookup; |
| import java.lang.management.ManagementFactory; |
| import java.lang.management.RuntimeMXBean; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Modifier; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipFile; |
| import org.junit.rules.TemporaryFolder; |
| import org.junit.rules.TestRule; |
| import org.junit.runner.Description; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.model.Statement; |
| import org.objectweb.asm.ClassReader; |
| import org.objectweb.asm.Opcodes; |
| import org.objectweb.asm.tree.ClassNode; |
| |
| /** A JUnit4 Rule that desugars an input jar file and load the transformed jar to JVM. */ |
| public class DesugarRule implements TestRule { |
| |
| /** |
| * Identifies injectable class-literal fields with the specified class to load at runtime and |
| * assign to the field. An injectable class-literal field may have any access modifier (private, |
| * package-private, protected, public). Sample usage: |
| * |
| * <pre><code> |
| * @RunWith(JUnit4.class) |
| * public class DesugarRuleTest { |
| * |
| * @Rule |
| * public final DesugarRule desugarRule = |
| * DesugarRule.builder(this, MethodHandles.lookup()) |
| * .addRuntimeInputs("path/to/my_jar.jar") |
| * .build(); |
| * |
| * @LoadClass("my.package.ClassToDesugar") |
| * private Class<?> classToDesugarClass; |
| * |
| * // ... Test methods ... |
| * } |
| * </code></pre> |
| */ |
| @UsedReflectively |
| @Documented |
| @Target(ElementType.FIELD) |
| @Retention(RetentionPolicy.RUNTIME) |
| public @interface LoadClass { |
| |
| /** |
| * The fully-qualified class name of the class to load. The format agrees with {@link |
| * Class#getName}. |
| */ |
| String value(); |
| |
| /** The round during which its associated jar is being used. */ |
| int round() default 1; |
| } |
| |
| /** |
| * Identifies injectable {@link ZipEntry} fields with a zip entry path. The desugar rule resolves |
| * the requested zip entry at runtime and assign it to the annotated field. An injectable {@link |
| * ZipEntry} field may have any access modifier (private, package-private, protected, public). |
| * Sample usage: |
| * |
| * <pre><code> |
| * @RunWith(JUnit4.class) |
| * public class DesugarRuleTest { |
| * |
| * @Rule |
| * public final DesugarRule desugarRule = |
| * DesugarRule.builder(this, MethodHandles.lookup()) |
| * .addRuntimeInputs("path/to/my_jar.jar") |
| * .build(); |
| * |
| * @LoadZipEntry("my/package/ClassToDesugar.class") |
| * private ZipEntry classToDesugarClassFile; |
| * |
| * // ... Test methods ... |
| * } |
| * </code></pre> |
| */ |
| @UsedReflectively |
| @Documented |
| @Target(ElementType.FIELD) |
| @Retention(RetentionPolicy.RUNTIME) |
| public @interface LoadZipEntry { |
| |
| /** The requested zip entry path name within a zip file. */ |
| String value(); |
| |
| /** The round during which its associated jar is being used. */ |
| int round() default 1; |
| } |
| |
| /** |
| * Identifies injectable {@link ClassNode} fields with a qualified class name. The desugar rule |
| * resolves the requested class at runtime, parse it into a {@link ClassNode} and assign parsed |
| * class node to the annotated field. An injectable {@link ClassNode} field may have any access |
| * modifier (private, package-private, protected, public). Sample usage: |
| * |
| * <pre><code> |
| * @RunWith(JUnit4.class) |
| * public class DesugarRuleTest { |
| * |
| * @Rule |
| * public final DesugarRule desugarRule = |
| * DesugarRule.builder(this, MethodHandles.lookup()) |
| * .addRuntimeInputs("path/to/my_jar.jar") |
| * .build(); |
| * |
| * @LoadClassNode("my.package.ClassToDesugar") |
| * private ClassNode classToDesugarClassFile; |
| * |
| * // ... Test methods ... |
| * } |
| * </code></pre> |
| */ |
| @UsedReflectively |
| @Documented |
| @Target(ElementType.FIELD) |
| @Retention(RetentionPolicy.RUNTIME) |
| public @interface LoadClassNode { |
| |
| /** |
| * The fully-qualified class name of the class to load. The format agrees with {@link |
| * Class#getName}. |
| */ |
| String value(); |
| |
| /** The round during which its associated jar is being used. */ |
| int round() default 1; |
| } |
| |
| private static final Path ANDROID_RUNTIME_JAR_PATH = |
| RunfilesPaths.resolve( |
| "third_party/java/android/android_sdk_linux/platforms/experimental/android_blaze.jar"); |
| |
| private static final Path JACOCO_RUNTIME_PATH = |
| RunfilesPaths.resolve("third_party/java/jacoco/jacoco_agent.jar"); |
| |
| private static final String DEFAULT_OUTPUT_ROOT_PREFIX = "desugared_dump"; |
| |
| private static final ClassLoader baseClassLoader = ClassLoader.getSystemClassLoader().getParent(); |
| |
| /** The transformation record that describes the desugaring of a jar. */ |
| @AutoValue |
| abstract static class JarTransformationRecord { |
| |
| /** |
| * The full runtime path of a pre-transformationRecord jar. |
| * |
| * @see Desugar.DesugarOptions#inputJars for details. |
| */ |
| abstract ImmutableList<Path> inputJars(); |
| |
| /** |
| * The full runtime path of a post-transformationRecord jar (deguared jar). |
| * |
| * @see Desugar.DesugarOptions#inputJars for details. |
| */ |
| abstract ImmutableList<Path> outputJars(); |
| |
| /** @see Desugar.DesugarOptions#classpath for details. */ |
| abstract ImmutableList<Path> classPathEntries(); |
| |
| /** @see Desugar.DesugarOptions#bootclasspath for details. */ |
| abstract ImmutableList<Path> bootClassPathEntries(); |
| |
| /** The remaining command options used for desugaring. */ |
| abstract ImmutableListMultimap<String, String> extraCustomCommandOptions(); |
| |
| /** The factory method of this jar transformation record. */ |
| static JarTransformationRecord create( |
| ImmutableList<Path> inputJars, |
| ImmutableList<Path> outputJars, |
| ImmutableList<Path> classPathEntries, |
| ImmutableList<Path> bootClassPathEntries, |
| ImmutableListMultimap<String, String> extraCustomCommandOptions) { |
| return new AutoValue_DesugarRule_JarTransformationRecord( |
| inputJars, outputJars, classPathEntries, bootClassPathEntries, extraCustomCommandOptions); |
| } |
| |
| final ImmutableList<String> getDesugarFlags() { |
| ImmutableList.Builder<String> args = ImmutableList.builder(); |
| inputJars().forEach(path -> args.add("--input=" + path)); |
| outputJars().forEach(path -> args.add("--output=" + path)); |
| classPathEntries().forEach(path -> args.add("--classpath_entry=" + path)); |
| bootClassPathEntries().forEach(path -> args.add("--bootclasspath_entry=" + path)); |
| extraCustomCommandOptions().forEach((k, v) -> args.add("--" + k + "=" + v)); |
| return args.build(); |
| } |
| |
| final ClassLoader getOutputClassLoader() throws MalformedURLException { |
| List<URL> urls = new ArrayList<>(); |
| for (Path path : Iterables.concat(outputJars(), classPathEntries(), bootClassPathEntries())) { |
| urls.add(path.toUri().toURL()); |
| } |
| return URLClassLoader.newInstance(urls.toArray(new URL[0]), baseClassLoader); |
| } |
| } |
| |
| /** For hosting desugared jar temporarily. */ |
| private final TemporaryFolder temporaryFolder = new TemporaryFolder(); |
| |
| private final Object testInstance; |
| private final MethodHandles.Lookup testInstanceLookup; |
| |
| /** The maximum number of desugar operations, used for testing idempotency. */ |
| private final int maxNumOfTransformations; |
| |
| private final ImmutableList<Field> injectableClassLiterals; |
| private final ImmutableList<Field> injectableAstClassNodes; |
| private final ImmutableList<Field> injectableZipEntries; |
| |
| private final ImmutableList<Path> inputs; |
| private final ImmutableList<Path> classPathEntries; |
| private final ImmutableList<Path> bootClassPathEntries; |
| private final ImmutableListMultimap<String, String> extraCustomCommandOptions; |
| |
| private final List<JarTransformationRecord> jarTransformationRecords; |
| /** The state of the already-created directories to avoid directory re-creation. */ |
| private final Map<String, Path> tempDirs = new HashMap<>(); |
| |
| /** |
| * The entry point to create a {@link DesugarRule}. |
| * |
| * @param testInstance The <code>this</code> reference of the JUnit test class. |
| * @param testInstanceLookup The lookup object from the test class, i.e.<code> |
| * MethodHandles.lookup()</code> |
| */ |
| public static DesugarRuleBuilder builder(Object testInstance, Lookup testInstanceLookup) { |
| return new DesugarRuleBuilder(testInstance, testInstanceLookup); |
| } |
| |
| private DesugarRule( |
| Object testInstance, |
| Lookup testInstanceLookup, |
| int maxNumOfTransformations, |
| ImmutableList<Field> injectableClassLiterals, |
| ImmutableList<Field> injectableAstClassNodes, |
| ImmutableList<Field> injectableZipEntries, |
| ImmutableList<Path> inputJars, |
| ImmutableList<Path> classPathEntries, |
| ImmutableList<Path> bootClassPathEntries, |
| ImmutableListMultimap<String, String> customCommandOptions) { |
| this.testInstance = testInstance; |
| this.testInstanceLookup = testInstanceLookup; |
| this.maxNumOfTransformations = maxNumOfTransformations; |
| this.injectableClassLiterals = injectableClassLiterals; |
| this.injectableAstClassNodes = injectableAstClassNodes; |
| this.injectableZipEntries = injectableZipEntries; |
| this.inputs = inputJars; |
| this.classPathEntries = classPathEntries; |
| this.bootClassPathEntries = bootClassPathEntries; |
| this.extraCustomCommandOptions = customCommandOptions; |
| this.jarTransformationRecords = new ArrayList<>(maxNumOfTransformations); |
| } |
| |
| @Override |
| public Statement apply(Statement base, Description description) { |
| return temporaryFolder.apply( |
| new Statement() { |
| @Override |
| public void evaluate() throws Throwable { |
| ImmutableList<Path> transInputs = inputs; |
| for (int i = 0; i < maxNumOfTransformations; i++) { |
| ImmutableList<Path> transOutputs = |
| getRuntimeOutputPaths( |
| transInputs, |
| temporaryFolder, |
| tempDirs, |
| /* outputRootPrefix= */ DEFAULT_OUTPUT_ROOT_PREFIX + "_" + i); |
| JarTransformationRecord transformationRecord = |
| JarTransformationRecord.create( |
| transInputs, |
| transOutputs, |
| classPathEntries, |
| bootClassPathEntries, |
| extraCustomCommandOptions); |
| Desugar.main(transformationRecord.getDesugarFlags().toArray(new String[0])); |
| |
| jarTransformationRecords.add(transformationRecord); |
| transInputs = transOutputs; |
| } |
| |
| for (Field field : injectableClassLiterals) { |
| Class<?> classLiteral = |
| getClassLiteral( |
| field.getDeclaredAnnotation(LoadClass.class), |
| getInputClassLoader(), |
| jarTransformationRecords); |
| MethodHandle fieldSetter = testInstanceLookup.unreflectSetter(field); |
| fieldSetter.invoke(testInstance, classLiteral); |
| } |
| |
| for (Field field : injectableAstClassNodes) { |
| ClassNode classNode = |
| getAstClassNode( |
| field.getDeclaredAnnotation(LoadClassNode.class), |
| inputs, |
| jarTransformationRecords); |
| MethodHandle fieldSetter = testInstanceLookup.unreflectSetter(field); |
| fieldSetter.invoke(testInstance, classNode); |
| } |
| |
| for (Field field : injectableZipEntries) { |
| ZipEntry zipEntry = getZipEntry(field.getDeclaredAnnotation(LoadZipEntry.class)); |
| MethodHandle fieldSetter = testInstanceLookup.unreflectSetter(field); |
| fieldSetter.invoke(testInstance, zipEntry); |
| } |
| base.evaluate(); |
| } |
| }, |
| description); |
| } |
| |
| private static Class<?> getClassLiteral( |
| LoadClass classLiteralRequestInfo, |
| ClassLoader initialInputClassLoader, |
| List<JarTransformationRecord> jarTransformationRecords) |
| throws Throwable { |
| String qualifiedClassName = classLiteralRequestInfo.value(); |
| int round = classLiteralRequestInfo.round(); |
| ClassLoader outputJarClassLoader = |
| round == 0 |
| ? initialInputClassLoader |
| : jarTransformationRecords.get(round - 1).getOutputClassLoader(); |
| return outputJarClassLoader.loadClass(qualifiedClassName); |
| } |
| |
| private static ClassNode getAstClassNode( |
| LoadClassNode astNodeRequestInfo, |
| ImmutableList<Path> initialInputs, |
| List<JarTransformationRecord> jarTransformationRecords) |
| throws IOException, ClassNotFoundException { |
| String qualifiedClassName = astNodeRequestInfo.value(); |
| String classFileName = qualifiedClassName.replace('.', '/') + ".class"; |
| int round = astNodeRequestInfo.round(); |
| ImmutableList<Path> jars = |
| round == 0 ? initialInputs : jarTransformationRecords.get(round - 1).outputJars(); |
| ClassNode classNode = findClassNode(classFileName, jars); |
| if (classNode == null) { |
| throw new ClassNotFoundException(qualifiedClassName); |
| } |
| return classNode; |
| } |
| |
| private ZipEntry getZipEntry(LoadZipEntry zipEntryRequestInfo) throws IOException { |
| String zipEntryPathName = zipEntryRequestInfo.value(); |
| int round = zipEntryRequestInfo.round(); |
| ImmutableList<Path> jars = |
| round == 0 ? inputs : jarTransformationRecords.get(round - 1).outputJars(); |
| for (Path jar : jars) { |
| ZipFile zipFile = new ZipFile(jar.toFile()); |
| ZipEntry zipEntry = zipFile.getEntry(zipEntryPathName); |
| if (zipEntry != null) { |
| return zipEntry; |
| } |
| } |
| throw new IllegalStateException( |
| String.format("Expected zip entry of (%s) present.", zipEntryPathName)); |
| } |
| |
| private static ClassNode findClassNode(String zipEntryPathName, ImmutableList<Path> jars) |
| throws IOException { |
| for (Path jar : jars) { |
| ZipFile zipFile = new ZipFile(jar.toFile()); |
| ZipEntry zipEntry = zipFile.getEntry(zipEntryPathName); |
| if (zipEntry != null) { |
| try (InputStream inputStream = zipFile.getInputStream(zipEntry)) { |
| ClassReader cr = new ClassReader(inputStream); |
| ClassNode classNode = new ClassNode(Opcodes.ASM7); |
| cr.accept(classNode, 0); |
| return classNode; |
| } |
| } |
| } |
| return null; |
| } |
| |
| private ClassLoader getInputClassLoader() throws MalformedURLException { |
| List<URL> urls = new ArrayList<>(); |
| for (Path path : Iterables.concat(inputs, classPathEntries, bootClassPathEntries)) { |
| urls.add(path.toUri().toURL()); |
| } |
| return URLClassLoader.newInstance(urls.toArray(new URL[0]), baseClassLoader); |
| } |
| |
| private static ImmutableList<Path> getRuntimeOutputPaths( |
| ImmutableList<Path> inputs, |
| TemporaryFolder temporaryFolder, |
| Map<String, Path> tempDirs, |
| String outputRootPrefix) |
| throws IOException { |
| ImmutableList.Builder<Path> outputRuntimePathsBuilder = ImmutableList.builder(); |
| for (Path path : inputs) { |
| String targetDirKey = Paths.get(outputRootPrefix) + "/" + path.getParent(); |
| final Path outputDirPath; |
| if (tempDirs.containsKey(targetDirKey)) { |
| outputDirPath = tempDirs.get(targetDirKey); |
| } else { |
| outputDirPath = Paths.get(temporaryFolder.newFolder(targetDirKey).getPath()); |
| tempDirs.put(targetDirKey, outputDirPath); |
| } |
| outputRuntimePathsBuilder.add(outputDirPath.resolve(path.getFileName())); |
| } |
| return outputRuntimePathsBuilder.build(); |
| } |
| |
| private static ImmutableList<Field> findAllFieldsWithAnnotation( |
| Class<?> testClass, Class<? extends Annotation> annotationClass) { |
| ImmutableList.Builder<Field> fields = ImmutableList.builder(); |
| for (Class<?> currentClass = testClass; |
| currentClass != null; |
| currentClass = currentClass.getSuperclass()) { |
| for (Field field : currentClass.getDeclaredFields()) { |
| if (field.isAnnotationPresent(annotationClass)) { |
| fields.add(field); |
| } |
| } |
| } |
| return fields.build(); |
| } |
| |
| /** The builder class for {@link DesugarRule}. */ |
| public static class DesugarRuleBuilder { |
| |
| private final Object testInstance; |
| private final MethodHandles.Lookup testInstanceLookup; |
| private final ImmutableList<Field> injectableClassLiterals; |
| private final ImmutableList<Field> injectableAstClassNodes; |
| private final ImmutableList<Field> injectableZipEntries; |
| private int maxNumOfTransformations; |
| private final List<Path> inputs = new ArrayList<>(); |
| private final List<Path> classPathEntries = new ArrayList<>(); |
| private final List<Path> bootClassPathEntries = new ArrayList<>(); |
| private final Multimap<String, String> customCommandOptions = LinkedHashMultimap.create(); |
| private final ErrorMessenger errorMessenger = new ErrorMessenger(); |
| |
| DesugarRuleBuilder(Object testInstance, MethodHandles.Lookup testInstanceLookup) { |
| this.testInstance = testInstance; |
| this.testInstanceLookup = testInstanceLookup; |
| Class<?> testClass = testInstance.getClass(); |
| if (testClass != testInstanceLookup.lookupClass()) { |
| errorMessenger.addError( |
| "Expected testInstanceLookup has private access to (%s), but get (%s). Have you" |
| + " passed MethodHandles.lookup() to testInstanceLookup in test class?", |
| testClass, testInstanceLookup.lookupClass()); |
| } |
| if (!testClass.isAnnotationPresent(RunWith.class)) { |
| errorMessenger.addError( |
| "Expected a test instance whose class is annotated with @RunWith. %s", testClass); |
| } |
| |
| injectableClassLiterals = findAllFieldsWithAnnotation(testClass, LoadClass.class); |
| injectableAstClassNodes = findAllFieldsWithAnnotation(testClass, LoadClassNode.class); |
| injectableZipEntries = findAllFieldsWithAnnotation(testClass, LoadZipEntry.class); |
| } |
| |
| public DesugarRuleBuilder enableIterativeTransformation(int maxNumOfTransformations) { |
| this.maxNumOfTransformations = maxNumOfTransformations; |
| return this; |
| } |
| |
| public DesugarRuleBuilder addRuntimeInputs(String... inputJars) { |
| Arrays.stream(inputJars).map(RunfilesPaths::resolve).forEach(this::addInputs); |
| return this; |
| } |
| |
| public DesugarRuleBuilder addInputs(Path... inputJars) { |
| for (Path path : inputJars) { |
| if (!path.toString().endsWith(".jar")) { |
| errorMessenger.addError("Expected a JAR file (*.jar): Actual (%s)", path); |
| } |
| if (!Files.exists(path)) { |
| errorMessenger.addError("File does not exist: %s", path); |
| } |
| inputs.add(path); |
| } |
| return this; |
| } |
| |
| public DesugarRuleBuilder addClasspathEntries(Path... inputJars) { |
| Collections.addAll(classPathEntries, inputJars); |
| return this; |
| } |
| |
| public DesugarRuleBuilder addBootClassPathEntries(Path... inputJars) { |
| Collections.addAll(bootClassPathEntries, inputJars); |
| return this; |
| } |
| |
| /** Format: --<key>=<value> */ |
| public DesugarRuleBuilder addCommandOptions(String key, String value) { |
| customCommandOptions.put(key, value); |
| return this; |
| } |
| |
| private void checkJVMOptions() { |
| RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean(); |
| List<String> arguments = runtimeMxBean.getInputArguments(); |
| if (arguments.stream() |
| .noneMatch(arg -> arg.startsWith("-Djdk.internal.lambda.dumpProxyClasses="))) { |
| errorMessenger.addError( |
| "Expected \"-Djdk.internal.lambda.dumpProxyClasses=$$(mktemp -d)\" in jvm_flags.\n"); |
| } |
| } |
| |
| public DesugarRule build() { |
| checkJVMOptions(); |
| checkInjectableClassLiterals(); |
| checkInjectableAstNodes(); |
| checkInjectableZipEntries(); |
| |
| if (bootClassPathEntries.isEmpty() |
| && !customCommandOptions.containsKey("allow_empty_bootclasspath")) { |
| addCommandOptions("bootclasspath_entry", ANDROID_RUNTIME_JAR_PATH.toString()); |
| } |
| |
| if (errorMessenger.anyError()) { |
| throw new IllegalStateException( |
| String.format( |
| "Invalid Desugar configurations:\n%s\n", errorMessenger.getAllMessages())); |
| } |
| |
| addClasspathEntries(JACOCO_RUNTIME_PATH); |
| |
| return new DesugarRule( |
| testInstance, |
| testInstanceLookup, |
| maxNumOfTransformations, |
| injectableClassLiterals, |
| injectableAstClassNodes, |
| injectableZipEntries, |
| ImmutableList.copyOf(inputs), |
| ImmutableList.copyOf(classPathEntries), |
| ImmutableList.copyOf(bootClassPathEntries), |
| ImmutableListMultimap.copyOf(customCommandOptions)); |
| } |
| |
| private void checkInjectableClassLiterals() { |
| for (Field field : injectableClassLiterals) { |
| if (Modifier.isStatic(field.getModifiers())) { |
| errorMessenger.addError("Expected to be non-static for field (%s)", field); |
| } |
| |
| if (field.getType() != Class.class) { |
| errorMessenger.addError("Expected a class literal type (Class<?>) for field (%s)", field); |
| } |
| |
| LoadClass loadClassAnnotation = field.getDeclaredAnnotation(LoadClass.class); |
| if (loadClassAnnotation.round() < 0 |
| || loadClassAnnotation.round() > maxNumOfTransformations) { |
| errorMessenger.addError( |
| "Expected the round of desugar transformation within [0, %d], where 0 indicates no" |
| + " transformation is used.", |
| maxNumOfTransformations); |
| } |
| } |
| } |
| |
| private void checkInjectableAstNodes() { |
| for (Field field : injectableAstClassNodes) { |
| if (Modifier.isStatic(field.getModifiers())) { |
| errorMessenger.addError("Expected to be non-static for field (%s)", field); |
| } |
| |
| if (field.getType() != ClassNode.class) { |
| errorMessenger.addError( |
| "Expected a field with Type: (%s) but gets (%s)", ClassNode.class.getName(), field); |
| } |
| |
| LoadClassNode astClassNodeInfo = field.getDeclaredAnnotation(LoadClassNode.class); |
| if (astClassNodeInfo.round() < 0 || astClassNodeInfo.round() > maxNumOfTransformations) { |
| errorMessenger.addError( |
| "Expected the round of desugar transformation within [0, %d], where 0 indicates no" |
| + " transformation is used.", |
| maxNumOfTransformations); |
| } |
| } |
| } |
| |
| private void checkInjectableZipEntries() { |
| for (Field field : injectableZipEntries) { |
| if (Modifier.isStatic(field.getModifiers())) { |
| errorMessenger.addError("Expected to be non-static for field (%s)", field); |
| } |
| |
| if (field.getType() != ZipEntry.class) { |
| errorMessenger.addError( |
| "Expected a field with Type: (%s) but gets (%s)", ZipEntry.class.getName(), field); |
| } |
| |
| LoadZipEntry zipEntryInfo = field.getDeclaredAnnotation(LoadZipEntry.class); |
| if (zipEntryInfo.round() < 0 || zipEntryInfo.round() > maxNumOfTransformations) { |
| errorMessenger.addError( |
| "Expected the round of desugar transformation within [0, %d], where 0 indicates no" |
| + " transformation is used.", |
| maxNumOfTransformations); |
| } |
| } |
| } |
| } |
| |
| /** A messenger that manages desugar configuration errors. */ |
| private static class ErrorMessenger { |
| |
| private final List<String> errorMessages = new ArrayList<>(); |
| |
| @FormatMethod |
| ErrorMessenger addError(String recipe, Object... args) { |
| errorMessages.add(String.format(recipe, args)); |
| return this; |
| } |
| |
| boolean anyError() { |
| return !errorMessages.isEmpty(); |
| } |
| |
| List<String> getAllMessages() { |
| return errorMessages; |
| } |
| |
| @Override |
| public String toString() { |
| return getAllMessages().toString(); |
| } |
| } |
| } |