summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorColin Cross <ccross@android.com>2017-04-28 21:36:31 +0000
committerandroid-build-merger <android-build-merger@google.com>2017-04-28 21:36:31 +0000
commit11c90c2309a059e17841db2c02e51621f14e7813 (patch)
tree03534af441228307207a8cdd82e4af966e930be5
parentaba105bab9aca9d0f7354ccf116f8defaf02669a (diff)
parentc18767262aeedcfe0648a08635082e472985cc77 (diff)
downloaddesugar-11c90c2309a059e17841db2c02e51621f14e7813.tar.gz
Merge remote-tracking branch 'aosp/upstream-master' into master am: 0949aa80fc am: dd6801b47d
am: c18767262a Change-Id: Ief3f8a403b589a017dfc1e6db335fe777c7cef51
-rw-r--r--java/com/google/devtools/build/android/desugar/ClassReaderFactory.java10
-rw-r--r--java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java300
-rw-r--r--java/com/google/devtools/build/android/desugar/Desugar.java216
-rw-r--r--java/com/google/devtools/build/android/desugar/GeneratedClassStore.java49
-rw-r--r--java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java313
-rw-r--r--java/com/google/devtools/build/android/desugar/Java7Compatibility.java2
-rw-r--r--java/com/google/devtools/build/android/desugar/LambdaClassFixer.java42
-rw-r--r--java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java146
-rw-r--r--java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java276
-rw-r--r--java/com/google/devtools/common/options/IsolatedOptionsData.java99
-rw-r--r--java/com/google/devtools/common/options/Option.java3
-rw-r--r--java/com/google/devtools/common/options/OptionsBase.java41
-rw-r--r--java/com/google/devtools/common/options/OptionsParser.java163
-rw-r--r--java/com/google/devtools/common/options/OptionsParserImpl.java45
-rw-r--r--java/com/google/devtools/common/options/UsesOnlyCoreTypes.java57
15 files changed, 1594 insertions, 168 deletions
diff --git a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
index 2b44d76..bae5251 100644
--- a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
+++ b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java
@@ -28,7 +28,7 @@ class ClassReaderFactory {
}
/**
- * Returns a reader for the given/internal/Class$Name if the class is defined in the wrapped Jar
+ * Returns a reader for the given/internal/Class$Name if the class is defined in the wrapped input
* and {@code null} otherwise. For simplicity this method turns checked into runtime exceptions
* under the assumption that all classes have already been read once when this method is called.
*/
@@ -50,4 +50,12 @@ class ClassReaderFactory {
return null;
}
+
+ /**
+ * Returns {@code true} if the given given/internal/Class$Name is defined in the wrapped input.
+ */
+ public boolean isKnown(String internalClassName) {
+ String filename = rewriter.unprefix(internalClassName) + ".class";
+ return indexedInputs.getInputFileProvider(filename) != null;
+ }
}
diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
new file mode 100644
index 0000000..8ad5dc2
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
@@ -0,0 +1,300 @@
+// Copyright 2017 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 static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableList;
+import java.util.HashSet;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+/**
+ * Fixer of classes that extend interfaces with default methods to declare any missing methods
+ * explicitly and call the corresponding companion method generated by {@link InterfaceDesugaring}.
+ */
+public class DefaultMethodClassFixer extends ClassVisitor {
+
+ private final ClassReaderFactory classpath;
+ private final ClassReaderFactory bootclasspath;
+ private final HashSet<String> instanceMethods = new HashSet<>();
+ private final HashSet<String> seenInterfaces = new HashSet<>();
+
+ private boolean isInterface;
+ private ImmutableList<String> interfaces;
+ private String superName;
+
+ public DefaultMethodClassFixer(ClassVisitor dest, ClassReaderFactory classpath,
+ ClassReaderFactory bootclasspath) {
+ super(Opcodes.ASM5, dest);
+ this.classpath = classpath;
+ this.bootclasspath = bootclasspath;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ checkState(this.interfaces == null);
+ isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE);
+ checkArgument(superName != null || "java/lang/Object".equals(name), // ASM promises this
+ "Type without superclass: %s", name);
+ this.interfaces = ImmutableList.copyOf(interfaces);
+ this.superName = superName;
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public void visitEnd() {
+ if (!isInterface && defaultMethodsDefined(interfaces)) {
+ // Inherited methods take precedence over default methods, so visit all superclasses and
+ // figure out what methods they declare before stubbing in any missing default methods.
+ recordInheritedMethods();
+ stubMissingDefaultMethods(interfaces);
+ }
+ super.visitEnd();
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ // Keep track of instance methods implemented in this class for later.
+ if (!isInterface) {
+ recordIfInstanceMethod(access, name, desc);
+ }
+ return super.visitMethod(access, name, desc, signature, exceptions);
+ }
+
+ private void recordInheritedMethods() {
+ InstanceMethodRecorder recorder = new InstanceMethodRecorder();
+ String internalName = superName;
+ while (internalName != null) {
+ ClassReader bytecode = bootclasspath.readIfKnown(internalName);
+ if (bytecode == null) {
+ bytecode = checkNotNull(classpath.readIfKnown(internalName),
+ "Superclass not found: %s", internalName);
+ }
+ bytecode.accept(recorder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
+ internalName = bytecode.getSuperName();
+ }
+ }
+
+ private void recordIfInstanceMethod(int access, String name, String desc) {
+ if (BitFlags.noneSet(access, Opcodes.ACC_STATIC)) {
+ // Record all declared instance methods, including abstract, bridge, and native methods, as
+ // they all take precedence over default methods.
+ instanceMethods.add(name + ":" + desc);
+ }
+ }
+
+ /**
+ * Recursively searches the given interfaces for default methods not implemented by this class
+ * directly. If this method returns true we need to think about stubbing missing default methods.
+ */
+ private boolean defaultMethodsDefined(ImmutableList<String> interfaces) {
+ for (String implemented : interfaces) {
+ ClassReader bytecode = classpath.readIfKnown(implemented);
+ if (bytecode != null && !bootclasspath.isKnown(implemented)) {
+ // Class in classpath and bootclasspath is a bad idea but in any event, assume the
+ // bootclasspath will take precedence like in a classloader.
+ // We can skip code attributes as we just need to find default methods to stub.
+ DefaultMethodFinder finder = new DefaultMethodFinder();
+ bytecode.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
+ if (finder.foundDefaultMethods()) {
+ return true;
+ }
+ }
+ // Else interface isn't on the classpath, which indicates incomplete classpaths. For now
+ // we'll just assume the missing interfaces don't declare default methods but if they do
+ // we'll end up with concrete classes that don't implement an abstract method, which can
+ // cause runtime failures. The classpath needs to be fixed in this case.
+ }
+ return false;
+ }
+
+ private void stubMissingDefaultMethods(ImmutableList<String> interfaces) {
+ for (String implemented : interfaces) {
+ if (!seenInterfaces.add(implemented)) {
+ // Skip: a superclass already implements this interface, or we've seen it here
+ continue;
+ }
+ ClassReader bytecode = classpath.readIfKnown(implemented);
+ if (bytecode != null && !bootclasspath.isKnown(implemented)) {
+ // Class in classpath and bootclasspath is a bad idea but in any event, assume the
+ // bootclasspath will take precedence like in a classloader.
+ // We can skip code attributes as we just need to find default methods to stub.
+ bytecode.accept(new DefaultMethodStubber(), ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
+ }
+ }
+ }
+
+ /**
+ * Visitor for interfaces that produces delegates in the class visited by the outer
+ * {@link DefaultMethodClassFixer} for every default method encountered.
+ */
+ public class DefaultMethodStubber extends ClassVisitor {
+
+ @SuppressWarnings("hiding") private ImmutableList<String> interfaces;
+ private String interfaceName;
+
+ public DefaultMethodStubber() {
+ super(Opcodes.ASM5);
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
+ checkState(this.interfaces == null);
+ this.interfaces = ImmutableList.copyOf(interfaces);
+ interfaceName = name;
+ }
+
+ @Override
+ public void visitEnd() {
+ stubMissingDefaultMethods(this.interfaces);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ if (BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE)
+ && !instanceMethods.contains(name + ":" + desc)) {
+ // Add this method to the class we're desugaring and stub in a body to call the default
+ // implementation in the interface's companion class. ijar omits these methods when setting
+ // ACC_SYNTHETIC modifier, so don't. Don't do this for bridge methods, which we handle
+ // separately.
+ // Signatures can be wrong, e.g., when type variables are introduced, instantiated, or
+ // refined in the class we're processing, so drop them.
+ MethodVisitor stubMethod =
+ DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions);
+
+ int slot = 0;
+ stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver
+ Type neededType = Type.getMethodType(desc);
+ for (Type arg : neededType.getArgumentTypes()) {
+ stubMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
+ slot += arg.getSize();
+ }
+ stubMethod.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ interfaceName + InterfaceDesugaring.COMPANION_SUFFIX,
+ name,
+ InterfaceDesugaring.companionDefaultMethodDescriptor(interfaceName, desc),
+ /*itf*/ false);
+ stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+ stubMethod.visitMaxs(0, 0); // rely on class writer to compute these
+ stubMethod.visitEnd();
+ }
+ return null; // we don't care about the actual code in these methods
+ }
+ }
+
+ /**
+ * Visitor for interfaces that recursively searches interfaces for default method declarations.
+ */
+ public class DefaultMethodFinder extends ClassVisitor {
+
+ @SuppressWarnings("hiding") private ImmutableList<String> interfaces;
+ private boolean found;
+
+ public DefaultMethodFinder() {
+ super(Opcodes.ASM5);
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
+ checkState(this.interfaces == null);
+ this.interfaces = ImmutableList.copyOf(interfaces);
+ }
+
+ public boolean foundDefaultMethods() {
+ return found;
+ }
+
+ @Override
+ public void visitEnd() {
+ if (!found) {
+ found = defaultMethodsDefined(this.interfaces);
+ }
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ if (BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE)
+ && !instanceMethods.contains(name + ":" + desc)) {
+ // Found a default method we're not ignoring (instanceMethods at this point contains methods
+ // the top-level visited class implements itself).
+ found = true;
+ }
+ return null; // we don't care about the actual code in these methods
+ }
+ }
+
+ private class InstanceMethodRecorder extends ClassVisitor {
+
+ public InstanceMethodRecorder() {
+ super(Opcodes.ASM5);
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ checkArgument(BitFlags.noneSet(access, Opcodes.ACC_INTERFACE));
+ for (String inheritedInterface : interfaces) {
+ // No point copying default methods that we'll also copy for a superclass. Note we may
+ // be processing a class in the bootclasspath, in which case the interfaces must also
+ // be in the bootclasspath and we can skip those as well. Also note this is best-effort,
+ // since these interfaces may extend other interfaces that we're not recording here.
+ seenInterfaces.add(inheritedInterface);
+ }
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ recordIfInstanceMethod(access, name, desc);
+ return null;
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java
index 797cbc4..c4528ae 100644
--- a/java/com/google/devtools/build/android/desugar/Desugar.java
+++ b/java/com/google/devtools/build/android/desugar/Desugar.java
@@ -14,6 +14,7 @@
package com.google.devtools.build.android.desugar;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
@@ -22,6 +23,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
+import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import com.google.devtools.build.android.Converters.ExistingPathConverter;
import com.google.devtools.build.android.Converters.PathConverter;
@@ -31,6 +33,7 @@ import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParser.OptionUsageRestrictions;
import com.google.errorprone.annotations.MustBeClosed;
+import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
@@ -42,9 +45,12 @@ import java.nio.file.attribute.BasicFileAttributes;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.annotation.Nullable;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.tree.ClassNode;
/**
* Command-line tool to desugar Java 8 constructs that dx doesn't know what to do with, in
@@ -146,6 +152,25 @@ class Desugar {
public int minSdkVersion;
@Option(
+ name = "desugar_interface_method_bodies_if_needed",
+ defaultValue = "true",
+ category = "misc",
+ help =
+ "Rewrites default and static methods in interfaces if --min_sdk_version < 24. This "
+ + "only works correctly if subclasses of rewritten interfaces as well as uses of "
+ + "static interface methods are run through this tool as well."
+ )
+ public boolean desugarInterfaceMethodBodiesIfNeeded;
+
+ @Option(
+ name = "desugar_try_with_resources_if_needed",
+ defaultValue = "false",
+ category = "misc",
+ help = "Rewrites try-with-resources statements if --min_sdk_version < 19."
+ )
+ public boolean desugarTryWithResourcesIfNeeded;
+
+ @Option(
name = "copy_bridges_from_classpath",
defaultValue = "false",
category = "misc",
@@ -167,6 +192,11 @@ class Desugar {
private final Path dumpDirectory;
private final CoreLibraryRewriter rewriter;
private final LambdaClassMaker lambdas;
+ private final GeneratedClassStore store;
+ /** The counter to record the times of try-with-resources desugaring is invoked. */
+ private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger();
+
+ private final boolean outputJava7;
private final boolean allowDefaultMethods;
private final boolean allowCallsToObjectsNonNull;
/** An instance of Desugar is expected to be used ONLY ONCE */
@@ -177,7 +207,10 @@ class Desugar {
this.dumpDirectory = dumpDirectory;
this.rewriter = new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : "");
this.lambdas = new LambdaClassMaker(dumpDirectory);
- this.allowDefaultMethods = options.minSdkVersion >= 24;
+ this.store = new GeneratedClassStore();
+ this.outputJava7 = options.minSdkVersion < 24;
+ this.allowDefaultMethods =
+ options.desugarInterfaceMethodBodiesIfNeeded || options.minSdkVersion >= 24;
this.allowCallsToObjectsNonNull = options.minSdkVersion >= 19;
this.used = false;
}
@@ -187,27 +220,33 @@ class Desugar {
this.used = true;
try (Closer closer = Closer.create()) {
- IndexedInputs indexedClasspath =
- new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath));
+ IndexedInputs indexedBootclasspath =
+ new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath));
// Use a classloader that as much as possible uses the provided bootclasspath instead of
// the tool's system classloader. Unfortunately we can't do that for java. classes.
ClassLoader bootclassloader =
options.bootclasspath.isEmpty()
? new ThrowingClassLoader()
- : new HeaderClassLoader(
- new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath)),
- rewriter,
- new ThrowingClassLoader());
+ : new HeaderClassLoader(indexedBootclasspath, rewriter, new ThrowingClassLoader());
+ IndexedInputs indexedClasspath =
+ new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath));
// Process each input separately
for (InputOutputPair inputOutputPair : toInputOutputPairs(options)) {
- desugarOneInput(inputOutputPair, indexedClasspath, bootclassloader);
+ desugarOneInput(
+ inputOutputPair,
+ indexedClasspath,
+ bootclassloader,
+ new ClassReaderFactory(indexedBootclasspath, rewriter));
}
}
}
private void desugarOneInput(
- InputOutputPair inputOutputPair, IndexedInputs indexedClasspath, ClassLoader bootclassloader)
+ InputOutputPair inputOutputPair,
+ IndexedInputs indexedClasspath,
+ ClassLoader bootclassloader,
+ ClassReaderFactory bootclasspathReader)
throws Exception {
Path inputPath = inputOutputPair.getInput();
Path outputPath = inputOutputPair.getOutput();
@@ -227,24 +266,62 @@ class Desugar {
ClassLoader loader =
new HeaderClassLoader(indexedClasspathAndInputFiles, rewriter, bootclassloader);
- ClassReaderFactory readerFactory =
- new ClassReaderFactory(
- (options.copyBridgesFromClasspath && !allowDefaultMethods)
- ? indexedClasspathAndInputFiles
- : indexedInputFiles,
- rewriter);
+ ClassReaderFactory classpathReader = null;
+ ClassReaderFactory bridgeMethodReader = null;
+ if (outputJava7) {
+ classpathReader = new ClassReaderFactory(indexedClasspathAndInputFiles, rewriter);
+ if (options.copyBridgesFromClasspath) {
+ bridgeMethodReader = classpathReader;
+ } else {
+ bridgeMethodReader = new ClassReaderFactory(indexedInputFiles, rewriter);
+ }
+ }
ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder();
desugarClassesInInput(
- inputFiles, outputFileProvider, loader, readerFactory, interfaceLambdaMethodCollector);
+ inputFiles,
+ outputFileProvider,
+ loader,
+ classpathReader,
+ bootclasspathReader,
+ interfaceLambdaMethodCollector);
desugarAndWriteDumpedLambdaClassesToOutput(
- outputFileProvider, loader, readerFactory, interfaceLambdaMethodCollector);
+ outputFileProvider,
+ loader,
+ classpathReader,
+ bootclasspathReader,
+ interfaceLambdaMethodCollector.build(),
+ bridgeMethodReader);
+
+ desugarAndWriteGeneratedClasses(outputFileProvider);
+ copyThrowableExtensionClass(outputFileProvider);
}
- ImmutableMap<Path, LambdaInfo> leftBehind = lambdas.drain();
- checkState(leftBehind.isEmpty(), "Didn't process %s", leftBehind);
+ ImmutableMap<Path, LambdaInfo> lambdasLeftBehind = lambdas.drain();
+ checkState(lambdasLeftBehind.isEmpty(), "Didn't process %s", lambdasLeftBehind);
+ ImmutableMap<String, ClassNode> generatedLeftBehind = store.drain();
+ checkState(generatedLeftBehind.isEmpty(), "Didn't process %s", generatedLeftBehind.keySet());
+ }
+
+ private void copyThrowableExtensionClass(OutputFileProvider outputFileProvider) {
+ if (!outputJava7 || !options.desugarTryWithResourcesIfNeeded) {
+ // try-with-resources statements are okay in the output jar.
+ return;
+ }
+ if (this.numOfTryWithResourcesInvoked.get() <= 0) {
+ // the try-with-resources desugaring pass does nothing, so no need to copy these class files.
+ return;
+ }
+ for (String className :
+ TryWithResourcesRewriter.THROWABLE_EXT_CLASS_INTERNAL_NAMES_WITH_CLASS_EXT) {
+ try (InputStream stream = Desugar.class.getClassLoader().getResourceAsStream(className)) {
+ outputFileProvider.write(className, ByteStreams.toByteArray(stream));
+ } catch (IOException e) {
+ throw new IOError(e);
+ }
+ }
}
/** Desugar the classes that are in the inputs specified in the command line arguments. */
@@ -252,7 +329,8 @@ class Desugar {
InputFileProvider inputFiles,
OutputFileProvider outputFileProvider,
ClassLoader loader,
- ClassReaderFactory readerFactory,
+ @Nullable ClassReaderFactory classpathReader,
+ ClassReaderFactory bootclasspathReader,
Builder<String> interfaceLambdaMethodCollector)
throws IOException {
for (String filename : inputFiles) {
@@ -262,11 +340,14 @@ class Desugar {
// any danger of accidentally uncompressed resources ending up in an .apk.
if (filename.endsWith(".class")) {
ClassReader reader = rewriter.reader(content);
- UnprefixingClassWriter writer =
- rewriter.writer(ClassWriter.COMPUTE_MAXS /*for bridge methods*/);
+ UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor =
createClassVisitorsForClassesInInputs(
- loader, readerFactory, interfaceLambdaMethodCollector, writer);
+ loader,
+ classpathReader,
+ bootclasspathReader,
+ interfaceLambdaMethodCollector,
+ writer);
reader.accept(visitor, 0);
outputFileProvider.write(filename, writer.toByteArray());
@@ -284,10 +365,11 @@ class Desugar {
private void desugarAndWriteDumpedLambdaClassesToOutput(
OutputFileProvider outputFileProvider,
ClassLoader loader,
- ClassReaderFactory readerFactory,
- Builder<String> interfaceLambdaMethodCollector)
+ @Nullable ClassReaderFactory classpathReader,
+ ClassReaderFactory bootclasspathReader,
+ ImmutableSet<String> interfaceLambdaMethods,
+ @Nullable ClassReaderFactory bridgeMethodReader)
throws IOException {
- ImmutableSet<String> interfaceLambdaMethods = interfaceLambdaMethodCollector.build();
checkState(
!allowDefaultMethods || interfaceLambdaMethods.isEmpty(),
"Desugaring with default methods enabled moved interface lambdas");
@@ -307,7 +389,13 @@ class Desugar {
rewriter.writer(ClassWriter.COMPUTE_MAXS /*for invoking bridges*/);
ClassVisitor visitor =
createClassVisitorsForDumpedLambdaClasses(
- loader, readerFactory, interfaceLambdaMethods, lambdaClass.getValue(), writer);
+ loader,
+ classpathReader,
+ bootclasspathReader,
+ interfaceLambdaMethods,
+ bridgeMethodReader,
+ lambdaClass.getValue(),
+ writer);
reader.accept(visitor, 0);
String filename =
rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class";
@@ -316,26 +404,58 @@ class Desugar {
}
}
+ private void desugarAndWriteGeneratedClasses(OutputFileProvider outputFileProvider)
+ throws IOException {
+ // Write out any classes we generated along the way
+ ImmutableMap<String, ClassNode> generatedClasses = store.drain();
+ checkState(
+ generatedClasses.isEmpty() || (allowDefaultMethods && outputJava7),
+ "Didn't expect generated classes but got %s",
+ generatedClasses.keySet());
+ for (Map.Entry<String, ClassNode> generated : generatedClasses.entrySet()) {
+ UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS);
+ // checkState above implies that we want Java 7 .class files, so send through that visitor.
+ // Don't need a ClassReaderFactory b/c static interface methods should've been moved.
+ ClassVisitor visitor = new Java7Compatibility(writer, (ClassReaderFactory) null);
+ generated.getValue().accept(visitor);
+ String filename = rewriter.unprefix(generated.getKey()) + ".class";
+ outputFileProvider.write(filename, writer.toByteArray());
+ }
+ }
+
/**
* Create the class visitors for the lambda classes that are generated on the fly. If no new class
* visitors are not generated, then the passed-in {@code writer} will be returned.
*/
private ClassVisitor createClassVisitorsForDumpedLambdaClasses(
ClassLoader loader,
- ClassReaderFactory readerFactory,
+ @Nullable ClassReaderFactory classpathReader,
+ ClassReaderFactory bootclasspathReader,
ImmutableSet<String> interfaceLambdaMethods,
+ @Nullable ClassReaderFactory bridgeMethodReader,
LambdaInfo lambdaClass,
UnprefixingClassWriter writer) {
- ClassVisitor visitor = writer;
+ ClassVisitor visitor = checkNotNull(writer);
- if (!allowDefaultMethods) {
+ if (outputJava7) {
// null ClassReaderFactory b/c we don't expect to need it for lambda classes
visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null);
+ if (options.desugarTryWithResourcesIfNeeded) {
+ visitor = new TryWithResourcesRewriter(visitor, loader, numOfTryWithResourcesInvoked);
+ }
+ if (options.desugarInterfaceMethodBodiesIfNeeded) {
+ visitor = new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader);
+ visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store);
+ }
}
-
visitor =
new LambdaClassFixer(
- visitor, lambdaClass, readerFactory, interfaceLambdaMethods, allowDefaultMethods);
+ visitor,
+ lambdaClass,
+ bridgeMethodReader,
+ interfaceLambdaMethods,
+ allowDefaultMethods,
+ outputJava7);
// Send lambda classes through desugaring to make sure there's no invokedynamic
// instructions in generated lambda classes (checkState below will fail)
visitor = new LambdaDesugaring(visitor, loader, lambdas, null, allowDefaultMethods);
@@ -357,17 +477,23 @@ class Desugar {
*/
private ClassVisitor createClassVisitorsForClassesInInputs(
ClassLoader loader,
- ClassReaderFactory readerFactory,
+ @Nullable ClassReaderFactory classpathReader,
+ ClassReaderFactory bootclasspathReader,
Builder<String> interfaceLambdaMethodCollector,
UnprefixingClassWriter writer) {
- checkArgument(writer != null, "The class writer cannot be null");
- ClassVisitor visitor = writer;
+ ClassVisitor visitor = checkNotNull(writer);
if (!options.onlyDesugarJavac9ForLint) {
- if (!allowDefaultMethods) {
- visitor = new Java7Compatibility(visitor, readerFactory);
+ if (outputJava7) {
+ visitor = new Java7Compatibility(visitor, classpathReader);
+ if (options.desugarTryWithResourcesIfNeeded) {
+ visitor = new TryWithResourcesRewriter(visitor, loader, numOfTryWithResourcesInvoked);
+ }
+ if (options.desugarInterfaceMethodBodiesIfNeeded) {
+ visitor = new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader);
+ visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store);
+ }
}
-
visitor =
new LambdaDesugaring(
visitor, loader, lambdas, interfaceLambdaMethodCollector, allowDefaultMethods);
@@ -382,7 +508,6 @@ class Desugar {
return visitor;
}
-
public static void main(String[] args) throws Exception {
// It is important that this method is called first. See its javadoc.
Path dumpDirectory = createAndRegisterLambdaDumpDirectory();
@@ -440,7 +565,8 @@ class Desugar {
final ImmutableList.Builder<InputOutputPair> ioPairListbuilder = ImmutableList.builder();
for (Iterator<Path> inputIt = options.inputJars.iterator(),
outputIt = options.outputJars.iterator();
- inputIt.hasNext();) {
+ inputIt.hasNext();
+ ) {
ioPairListbuilder.add(InputOutputPair.create(inputIt.next(), outputIt.next()));
}
return ioPairListbuilder.build();
@@ -498,8 +624,7 @@ class Desugar {
/** Transform a Path to an {@link OutputFileProvider} */
@MustBeClosed
- private static OutputFileProvider toOutputFileProvider(Path path)
- throws IOException {
+ private static OutputFileProvider toOutputFileProvider(Path path) throws IOException {
if (Files.isDirectory(path)) {
return new DirectoryOutputFileProvider(path);
} else {
@@ -509,8 +634,7 @@ class Desugar {
/** Transform a Path to an InputFileProvider that needs to be closed by the caller. */
@MustBeClosed
- private static InputFileProvider toInputFileProvider(Path path)
- throws IOException {
+ private static InputFileProvider toInputFileProvider(Path path) throws IOException {
if (Files.isDirectory(path)) {
return new DirectoryInputFileProvider(path);
} else {
@@ -532,9 +656,7 @@ class Desugar {
return builder.build();
}
- /**
- * Pair input and output.
- */
+ /** Pair input and output. */
@AutoValue
abstract static class InputOutputPair {
diff --git a/java/com/google/devtools/build/android/desugar/GeneratedClassStore.java b/java/com/google/devtools/build/android/desugar/GeneratedClassStore.java
new file mode 100644
index 0000000..bf376a4
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/GeneratedClassStore.java
@@ -0,0 +1,49 @@
+// Copyright 2017 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 static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.tree.ClassNode;
+
+/**
+ * Simple wrapper around a map that holds generated classes so they can be processed later.
+ */
+class GeneratedClassStore {
+
+ /** Map from internal names to generated classes with deterministic iteration order. */
+ private final Map<String, ClassNode> classes = new LinkedHashMap<>();
+
+ /**
+ * Adds a class for the given internal name. It's the caller's responsibility to {@link
+ * ClassVisitor#visit} the returned object to initialize the desired class, and to avoid
+ * confusion, this method throws if the class had already been present.
+ */
+ public ClassVisitor add(String internalClassName) {
+ ClassNode result = new ClassNode();
+ checkState(
+ classes.put(internalClassName, result) == null, "Already present: %s", internalClassName);
+ return result;
+ }
+
+ public ImmutableMap<String, ClassNode> drain() {
+ ImmutableMap<String, ClassNode> result = ImmutableMap.copyOf(classes);
+ classes.clear();
+ return result;
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
new file mode 100644
index 0000000..974c90b
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
@@ -0,0 +1,313 @@
+// Copyright 2017 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 static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import javax.annotation.Nullable;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.TypePath;
+
+/**
+ * Visitor that moves methods with bodies from interfaces into a companion class and rewrites
+ * call sites accordingly (which is only needed for static interface methods). Default methods
+ * are kept as abstract methods with all their annotations.
+ *
+ * <p>Any necessary companion classes will be added to the given {@link GeneratedClassStore}. It's
+ * the caller's responsibility to write those out.
+ *
+ * <p>Relies on {@link DefaultMethodClassFixer} to stub in method bodies for moved default methods.
+ * Assumes that lambdas are already desugared. Ignores bridge methods, which are handled specially.
+ */
+class InterfaceDesugaring extends ClassVisitor {
+
+ static final String COMPANION_SUFFIX = "$$CC";
+
+ private final ClassReaderFactory bootclasspath;
+ private final GeneratedClassStore store;
+
+ private String internalName;
+ private int bytecodeVersion;
+ private int accessFlags;
+ @Nullable private ClassVisitor companion;
+
+ public InterfaceDesugaring(ClassVisitor dest, ClassReaderFactory bootclasspath,
+ GeneratedClassStore store) {
+ super(Opcodes.ASM5, dest);
+ this.bootclasspath = bootclasspath;
+ this.store = store;
+ }
+
+ @Override
+ public void visit(
+ int version,
+ int access,
+ String name,
+ String signature,
+ String superName,
+ String[] interfaces) {
+ companion = null;
+ internalName = name;
+ bytecodeVersion = version;
+ accessFlags = access;
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public void visitEnd() {
+ if (companion != null) {
+ companion.visitEnd();
+ }
+ super.visitEnd();
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ MethodVisitor result;
+ if (BitFlags.isSet(accessFlags, Opcodes.ACC_INTERFACE)
+ && BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_BRIDGE)
+ && !"<clinit>".equals(name)) {
+ checkArgument(BitFlags.noneSet(access, Opcodes.ACC_NATIVE), "Forbidden per JLS ch 9.4");
+
+ boolean isLambdaBody =
+ name.startsWith("lambda$") && BitFlags.isSet(access, Opcodes.ACC_SYNTHETIC);
+ if (isLambdaBody) {
+ access &= ~Opcodes.ACC_PUBLIC; // undo visibility change from LambdaDesugaring
+ // Rename lambda method to reflect the new owner. Not doing so confuses LambdaDesugaring
+ // if it's run over this class again.
+ name += COMPANION_SUFFIX;
+ }
+ if (BitFlags.isSet(access, Opcodes.ACC_STATIC)) {
+ // Completely move static interface methods, which requires rewriting call sites
+ result =
+ companion()
+ .visitMethod(access & ~Opcodes.ACC_PRIVATE, name, desc, signature, exceptions);
+ } else {
+ MethodVisitor abstractDest;
+ if (isLambdaBody) {
+ // Completely move lambda bodies, which requires rewriting call sites
+ access &= ~Opcodes.ACC_PRIVATE;
+ abstractDest = null;
+ } else {
+ // Make default methods abstract but move their implementation into a static method with
+ // corresponding signature. Doesn't require callsite rewriting but implementing classes
+ // may need to implement default methods explicitly.
+ checkArgument(BitFlags.noneSet(access, Opcodes.ACC_PRIVATE),
+ "Unexpected private interface method %s.%s : %s", name, internalName, desc);
+ abstractDest = super.visitMethod(
+ access | Opcodes.ACC_ABSTRACT, name, desc, signature, exceptions);
+ }
+
+ // TODO(b/37110951): adjust signature with explicit receiver type, which may be generic
+ MethodVisitor codeDest =
+ companion()
+ .visitMethod(
+ access | Opcodes.ACC_STATIC,
+ name,
+ companionDefaultMethodDescriptor(internalName, desc),
+ (String) null, // drop signature, since given one doesn't include the new param
+ exceptions);
+
+ result = abstractDest != null ? new MultiplexAnnotations(codeDest, abstractDest) : codeDest;
+ }
+ } else {
+ result = super.visitMethod(access, name, desc, signature, exceptions);
+ }
+ return result != null ? new InterfaceInvocationRewriter(result) : null;
+ }
+
+ /**
+ * Returns the descriptor of a static method for an instance method with the given receiver and
+ * description, simply by pre-pending the given descriptor's parameter list with the given
+ * receiver type.
+ */
+ static String companionDefaultMethodDescriptor(String interfaceName, String desc) {
+ Type type = Type.getMethodType(desc);
+ Type[] companionArgs = new Type[type.getArgumentTypes().length + 1];
+ companionArgs[0] = Type.getObjectType(interfaceName);
+ System.arraycopy(type.getArgumentTypes(), 0, companionArgs, 1, type.getArgumentTypes().length);
+ return Type.getMethodDescriptor(type.getReturnType(), companionArgs);
+ }
+
+ private ClassVisitor companion() {
+ if (companion == null) {
+ checkState(BitFlags.isSet(accessFlags, Opcodes.ACC_INTERFACE));
+ String companionName = internalName + COMPANION_SUFFIX;
+
+ companion = store.add(companionName);
+ companion.visit(
+ bytecodeVersion,
+ // Companion class must be public so moved methods can be called from anywhere
+ (accessFlags | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC) & ~Opcodes.ACC_INTERFACE,
+ companionName,
+ (String) null, // signature
+ "java/lang/Object",
+ new String[0]);
+ }
+ return companion;
+ }
+
+ /**
+ * Rewriter for calls to static interface methods and super calls to default methods, unless
+ * they're part of the bootclasspath, as well as all lambda body methods. Keeps calls to
+ * interface methods declared in the bootclasspath as-is (but note that these would presumably
+ * fail on devices without those methods).
+ */
+ private class InterfaceInvocationRewriter extends MethodVisitor {
+
+ public InterfaceInvocationRewriter(MethodVisitor dest) {
+ super(Opcodes.ASM5, dest);
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ // Assume that any static interface methods on the classpath are moved
+ if (itf) {
+ if (name.startsWith("lambda$")) {
+ // Redirect lambda invocations to completely remove all lambda methods from interfaces.
+ checkArgument(!owner.endsWith(COMPANION_SUFFIX),
+ "%s shouldn't consider %s an interface", internalName, owner);
+ checkArgument(!bootclasspath.isKnown(owner)); // must be in current input
+ if (opcode == Opcodes.INVOKEINTERFACE) {
+ opcode = Opcodes.INVOKESTATIC;
+ desc = companionDefaultMethodDescriptor(owner, desc);
+ } else {
+ checkArgument(opcode == Opcodes.INVOKESTATIC,
+ "Unexpected opcode %s to invoke %s.%s", opcode, owner, name);
+ }
+ // Reflect that InterfaceDesugaring moves and renames the lambda body method
+ owner += COMPANION_SUFFIX;
+ name += COMPANION_SUFFIX;
+ checkState(name.equals(LambdaDesugaring.uniqueInPackage(owner, name)),
+ "Unexpected lambda body method name %s for %s", name, owner);
+ itf = false;
+ } else if ((opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)
+ && !bootclasspath.isKnown(owner)) {
+ checkArgument(!owner.endsWith(COMPANION_SUFFIX),
+ "%s shouldn't consider %s an interface", internalName, owner);
+ if (opcode == Opcodes.INVOKESPECIAL) {
+ // Turn Interface.super.m() into Interface$$CC.m(receiver)
+ opcode = Opcodes.INVOKESTATIC;
+ desc = companionDefaultMethodDescriptor(owner, desc);
+ }
+ owner += COMPANION_SUFFIX;
+ itf = false;
+ }
+ }
+ super.visitMethodInsn(opcode, owner, name, desc, itf);
+ }
+ }
+
+ /**
+ * Method visitor that behaves like a passthrough but additionally duplicates all annotations
+ * into a second given {@link MethodVisitor}.
+ */
+ private static class MultiplexAnnotations extends MethodVisitor {
+
+ private final MethodVisitor annotationOnlyDest;
+
+ public MultiplexAnnotations(@Nullable MethodVisitor dest, MethodVisitor annotationOnlyDest) {
+ super(Opcodes.ASM5, dest);
+ this.annotationOnlyDest = annotationOnlyDest;
+ }
+
+ @Override
+ public void visitParameter(String name, int access) {
+ super.visitParameter(name, access);
+ annotationOnlyDest.visitParameter(name, access);
+ }
+
+ @Override
+ public AnnotationVisitor visitTypeAnnotation(
+ int typeRef, TypePath typePath, String desc, boolean visible) {
+ AnnotationVisitor dest = super.visitTypeAnnotation(typeRef, typePath, desc, visible);
+ AnnotationVisitor annoDest =
+ annotationOnlyDest.visitTypeAnnotation(typeRef, typePath, desc, visible);
+ return new MultiplexAnnotationVisitor(dest, annoDest);
+ }
+
+ @Override
+ public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
+ AnnotationVisitor dest = super.visitParameterAnnotation(parameter, desc, visible);
+ AnnotationVisitor annoDest =
+ annotationOnlyDest.visitParameterAnnotation(parameter, desc, visible);
+ return new MultiplexAnnotationVisitor(dest, annoDest);
+ }
+ }
+
+ /**
+ * Annotation visitor that recursively passes the visited annotations to any number of given
+ * {@link AnnotationVisitor}s.
+ */
+ private static class MultiplexAnnotationVisitor extends AnnotationVisitor {
+
+ private final AnnotationVisitor[] moreDestinations;
+
+ public MultiplexAnnotationVisitor(@Nullable AnnotationVisitor dest,
+ AnnotationVisitor... moreDestinations) {
+ super(Opcodes.ASM5, dest);
+ this.moreDestinations = moreDestinations;
+ }
+
+ @Override
+ public void visit(String name, Object value) {
+ super.visit(name, value);
+ for (AnnotationVisitor dest : moreDestinations) {
+ dest.visit(name, value);
+ }
+ }
+
+ @Override
+ public void visitEnum(String name, String desc, String value) {
+ super.visitEnum(name, desc, value);
+ for (AnnotationVisitor dest : moreDestinations) {
+ dest.visitEnum(name, desc, value);
+ }
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String name, String desc) {
+ AnnotationVisitor[] subVisitors = new AnnotationVisitor[moreDestinations.length];
+ AnnotationVisitor dest = super.visitAnnotation(name, desc);
+ for (int i = 0; i < subVisitors.length; ++i) {
+ subVisitors[i] = moreDestinations[i].visitAnnotation(name, desc);
+ }
+ return new MultiplexAnnotationVisitor(dest, subVisitors);
+ }
+
+ @Override
+ public AnnotationVisitor visitArray(String name) {
+ AnnotationVisitor[] subVisitors = new AnnotationVisitor[moreDestinations.length];
+ AnnotationVisitor dest = super.visitArray(name);
+ for (int i = 0; i < subVisitors.length; ++i) {
+ subVisitors[i] = moreDestinations[i].visitArray(name);
+ }
+ return new MultiplexAnnotationVisitor(dest, subVisitors);
+ }
+
+ @Override
+ public void visitEnd() {
+ super.visitEnd();
+ for (AnnotationVisitor dest : moreDestinations) {
+ dest.visitEnd();
+ }
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
index cc3fe14..55f82b3 100644
--- a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
+++ b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
@@ -27,7 +27,7 @@ import org.objectweb.asm.Opcodes;
import org.objectweb.asm.TypePath;
/**
- * Visitor that ensures bytecode version <= 51 (Java 7) and that throws if it default or static
+ * Visitor that ensures bytecode version <= 51 (Java 7) and that throws if it sees default or static
* interface methods (i.e., non-abstract interface methods), which don't exist in Java 7.
*/
public class Java7Compatibility extends ClassVisitor {
diff --git a/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
index dea6339..5e50fc8 100644
--- a/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
+++ b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java
@@ -49,6 +49,7 @@ class LambdaClassFixer extends ClassVisitor {
private final ClassReaderFactory factory;
private final ImmutableSet<String> interfaceLambdaMethods;
private final boolean allowDefaultMethods;
+ private final boolean copyBridgeMethods;
private final HashSet<String> implementedMethods = new HashSet<>();
private final LinkedHashSet<String> methodsToMoveIn = new LinkedHashSet<>();
@@ -61,14 +62,17 @@ class LambdaClassFixer extends ClassVisitor {
private String desc;
private String signature;
-
public LambdaClassFixer(ClassVisitor dest, LambdaInfo lambdaInfo, ClassReaderFactory factory,
- ImmutableSet<String> interfaceLambdaMethods, boolean allowDefaultMethods) {
+ ImmutableSet<String> interfaceLambdaMethods, boolean allowDefaultMethods,
+ boolean copyBridgeMethods) {
super(Opcodes.ASM5, dest);
+ checkArgument(!allowDefaultMethods || interfaceLambdaMethods.isEmpty());
+ checkArgument(allowDefaultMethods || copyBridgeMethods);
this.lambdaInfo = lambdaInfo;
this.factory = factory;
this.interfaceLambdaMethods = interfaceLambdaMethods;
this.allowDefaultMethods = allowDefaultMethods;
+ this.copyBridgeMethods = copyBridgeMethods;
}
@Override
@@ -181,7 +185,7 @@ class LambdaClassFixer extends ClassVisitor {
}
copyRewrittenLambdaMethods();
- if (!allowDefaultMethods) {
+ if (copyBridgeMethods) {
copyBridgeMethods(interfaces);
}
super.visitEnd();
@@ -200,6 +204,7 @@ class LambdaClassFixer extends ClassVisitor {
CopyOneMethod copier = new CopyOneMethod(methodName);
// TODO(kmb): Set source file attribute for lambda classes so lambda debug info makes sense
bytecode.accept(copier, ClassReader.SKIP_DEBUG);
+ checkState(copier.copied(), "Didn't find %s", rewritten);
}
}
@@ -232,15 +237,16 @@ class LambdaClassFixer extends ClassVisitor {
owner = getInternalName();
itf = false; // owner was interface but is now a class
methodsToMoveIn.add(method);
- } else {
- if (originalInternalName.equals(owner)) {
- // Reflect renaming of lambda classes
- owner = getInternalName();
- }
- if (name.startsWith("lambda$")) {
- // Reflect renaming of lambda$ instance methods to avoid accidental overrides
- name = LambdaDesugaring.uniqueInPackage(owner, name);
- }
+ } else if (originalInternalName.equals(owner)) {
+ // Reflect renaming of lambda classes
+ owner = getInternalName();
+ }
+
+ if (name.startsWith("lambda$")) {
+ // Reflect renaming of lambda$ instance methods in LambdaDesugaring. Do this even if we'll
+ // move the method into the lambda class we're processing so the renaming done in
+ // LambdaDesugaring doesn't kick in if the class were desugared a second time.
+ name = LambdaDesugaring.uniqueInPackage(owner, name);
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
@@ -326,8 +332,9 @@ class LambdaClassFixer extends ClassVisitor {
// Only copy bridge methods--hand-written default methods are not supported--and only if
// we haven't seen the method already.
if (implementedMethods.add(name + ":" + desc)) {
- return new AvoidJacocoInit(
- LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions));
+ MethodVisitor result =
+ LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions);
+ return allowDefaultMethods ? result : new AvoidJacocoInit(result);
}
}
return null;
@@ -347,9 +354,14 @@ class LambdaClassFixer extends ClassVisitor {
public CopyOneMethod(String methodName) {
// No delegate visitor; instead we'll add methods to the outer class's delegate where needed
super(Opcodes.ASM5);
+ checkState(!allowDefaultMethods, "Couldn't copy interface lambda bodies");
this.methodName = methodName;
}
+ public boolean copied() {
+ return copied > 0;
+ }
+
@Override
public void visit(
int version,
@@ -367,6 +379,8 @@ class LambdaClassFixer extends ClassVisitor {
if (name.equals(methodName)) {
checkState(copied == 0, "Found unexpected second method %s with descriptor %s", name, desc);
++copied;
+ // Rename for consistency with what we do in LambdaClassMethodRewriter
+ name = LambdaDesugaring.uniqueInPackage(getInternalName(), name);
return new AvoidJacocoInit(
LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions));
}
diff --git a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
new file mode 100644
index 0000000..2429d2f
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java
@@ -0,0 +1,146 @@
+// Copyright 2017 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 static org.objectweb.asm.Opcodes.ASM5;
+import static org.objectweb.asm.Opcodes.INVOKESTATIC;
+import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
+
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+
+/**
+ * Desugar try-with-resources. This class visitor intercepts calls to the following methods, and
+ * redirect them to ThrowableExtension.
+ * <li>{@code Throwable.addSuppressed(Throwable)}
+ * <li>{@code Throwable.getSuppressed()}
+ * <li>{@code Throwable.printStackTrace()}
+ * <li>{@code Throwable.printStackTrace(PrintStream)}
+ * <li>{@code Throwable.printStackTrace(PringWriter)}
+ */
+public class TryWithResourcesRewriter extends ClassVisitor {
+
+ private static final String RUNTIME_PACKAGE_INTERNAL_NAME =
+ "com/google/devtools/build/android/desugar/runtime";
+
+ static final String THROWABLE_EXTENSION_INTERNAL_NAME =
+ RUNTIME_PACKAGE_INTERNAL_NAME + '/' + "ThrowableExtension";
+
+ /** The extension classes for java.lang.Throwable. */
+ static final ImmutableSet<String> THROWABLE_EXT_CLASS_INTERNAL_NAMES =
+ ImmutableSet.of(
+ THROWABLE_EXTENSION_INTERNAL_NAME,
+ THROWABLE_EXTENSION_INTERNAL_NAME + "$AbstractDesugaringStrategy",
+ THROWABLE_EXTENSION_INTERNAL_NAME + "$MimicDesugaringStrategy",
+ THROWABLE_EXTENSION_INTERNAL_NAME + "$NullDesugaringStrategy",
+ THROWABLE_EXTENSION_INTERNAL_NAME + "$ReuseDesugaringStrategy");
+
+ /** The extension classes for java.lang.Throwable. All the names end with ".class" */
+ static final ImmutableSet<String> THROWABLE_EXT_CLASS_INTERNAL_NAMES_WITH_CLASS_EXT =
+ FluentIterable.from(THROWABLE_EXT_CLASS_INTERNAL_NAMES)
+ .transform(
+ new Function<String, String>() {
+ @Override
+ public String apply(String s) {
+ return s + ".class";
+ }
+ })
+ .toSet();
+
+ static final ImmutableMultimap<String, String> TARGET_METHODS =
+ ImmutableMultimap.<String, String>builder()
+ .put("addSuppressed", "(Ljava/lang/Throwable;)V")
+ .put("getSuppressed", "()[Ljava/lang/Throwable;")
+ .put("printStackTrace", "()V")
+ .put("printStackTrace", "(Ljava/io/PrintStream;)V")
+ .put("printStackTrace", "(Ljava/io/PrintWriter;)V")
+ .build();
+
+ static final ImmutableMap<String, String> METHOD_DESC_MAP =
+ ImmutableMap.<String, String>builder()
+ .put("(Ljava/lang/Throwable;)V", "(Ljava/lang/Throwable;Ljava/lang/Throwable;)V")
+ .put("()[Ljava/lang/Throwable;", "(Ljava/lang/Throwable;)[Ljava/lang/Throwable;")
+ .put("()V", "(Ljava/lang/Throwable;)V")
+ .put("(Ljava/io/PrintStream;)V", "(Ljava/lang/Throwable;Ljava/io/PrintStream;)V")
+ .put("(Ljava/io/PrintWriter;)V", "(Ljava/lang/Throwable;Ljava/io/PrintWriter;)V")
+ .build();
+
+ private final ClassLoader classLoader;
+
+ private final AtomicInteger numOfTryWithResourcesInvoked;
+
+ public TryWithResourcesRewriter(
+ ClassVisitor classVisitor,
+ ClassLoader classLoader,
+ AtomicInteger numOfTryWithResourcesInvoked) {
+ super(ASM5, classVisitor);
+ this.classLoader = classLoader;
+ this.numOfTryWithResourcesInvoked = numOfTryWithResourcesInvoked;
+ }
+
+ @Override
+ public MethodVisitor visitMethod(
+ int access, String name, String desc, String signature, String[] exceptions) {
+ MethodVisitor visitor = super.cv.visitMethod(access, name, desc, signature, exceptions);
+ return visitor == null || THROWABLE_EXT_CLASS_INTERNAL_NAMES.contains(name)
+ ? visitor
+ : new TryWithResourceVisitor(visitor, classLoader);
+ }
+
+ private class TryWithResourceVisitor extends MethodVisitor {
+
+ private final ClassLoader classLoader;
+
+ public TryWithResourceVisitor(MethodVisitor methodVisitor, ClassLoader classLoader) {
+ super(ASM5, methodVisitor);
+ this.classLoader = classLoader;
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+ if (!isMethodCallTargeted(opcode, owner, name, desc)) {
+ super.visitMethodInsn(opcode, owner, name, desc, itf);
+ return;
+ }
+ numOfTryWithResourcesInvoked.incrementAndGet();
+ super.visitMethodInsn(
+ INVOKESTATIC, THROWABLE_EXTENSION_INTERNAL_NAME, name, METHOD_DESC_MAP.get(desc), false);
+ }
+
+ private boolean isMethodCallTargeted(int opcode, String owner, String name, String desc) {
+ if (opcode != INVOKEVIRTUAL) {
+ return false;
+ }
+ if (!TARGET_METHODS.containsEntry(name, desc)) {
+ return false;
+ }
+ if (owner.equals("java/lang/Throwable")) {
+ return true; // early return, for performance.
+ }
+ try {
+ Class<?> throwableClass = classLoader.loadClass("java.lang.Throwable");
+ Class<?> klass = classLoader.loadClass(owner.replace('/', '.'));
+ return throwableClass.isAssignableFrom(klass);
+ } catch (ClassNotFoundException e) {
+ throw new AssertionError(e);
+ }
+ }
+ }
+}
diff --git a/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java b/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java
new file mode 100644
index 0000000..3581fe8
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java
@@ -0,0 +1,276 @@
+// Copyright 2017 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.runtime;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.WeakHashMap;
+
+/**
+ * This is an extension class for java.lang.Throwable. It emulates the methods
+ * addSuppressed(Throwable) and getSuppressed(), so the language feature try-with-resources can be
+ * used on Android devices whose API level is below 19.
+ *
+ * <p>Note that the Desugar should avoid desugaring this class.
+ */
+public class ThrowableExtension {
+
+ static final AbstractDesugaringStrategy STRATEGY;
+ /**
+ * This property allows users to change the desugared behavior of try-with-resources at runtime.
+ * If its value is {@code true}, then {@link MimicDesugaringStrategy} will NOT be used, and {@link
+ * NullDesugaringStrategy} is used instead.
+ *
+ * <p>Note: this property is ONLY used when the API level on the device is below 19.
+ */
+ public static final String SYSTEM_PROPERTY_TWR_DISABLE_MIMIC =
+ "com.google.devtools.build.android.desugar.runtime.twr_disable_mimic";
+
+ static {
+ AbstractDesugaringStrategy strategy;
+ try {
+ Integer apiLevel = readApiLevelFromBuildVersion();
+ if (apiLevel != null && apiLevel.intValue() >= 19) {
+ strategy = new ReuseDesugaringStrategy();
+ } else if (useMimicStrategy()) {
+ strategy = new MimicDesugaringStrategy();
+ } else {
+ strategy = new NullDesugaringStrategy();
+ }
+ } catch (Throwable e) {
+ // This catchall block is intentionally created to avoid anything unexpected, so that
+ // the desugared app will continue running in case of exceptions.
+ System.err.println(
+ "An error has occured when initializing the try-with-resources desuguring strategy. "
+ + "The default strategy "
+ + NullDesugaringStrategy.class.getName()
+ + "will be used. The error is: ");
+ e.printStackTrace(System.err);
+ strategy = new NullDesugaringStrategy();
+ }
+ STRATEGY = strategy;
+ }
+
+ public static AbstractDesugaringStrategy getStrategy() {
+ return STRATEGY;
+ }
+
+ public static void addSuppressed(Throwable receiver, Throwable suppressed) {
+ STRATEGY.addSuppressed(receiver, suppressed);
+ }
+
+ public static Throwable[] getSuppressed(Throwable receiver) {
+ return STRATEGY.getSuppressed(receiver);
+ }
+
+ public static void printStackTrace(Throwable receiver) {
+ STRATEGY.printStackTrace(receiver);
+ }
+
+ public static void printStackTrace(Throwable receiver, PrintWriter writer) {
+ STRATEGY.printStackTrace(receiver, writer);
+ }
+
+ public static void printStackTrace(Throwable receiver, PrintStream stream) {
+ STRATEGY.printStackTrace(receiver, stream);
+ }
+
+ private static boolean useMimicStrategy() {
+ return !Boolean.getBoolean(SYSTEM_PROPERTY_TWR_DISABLE_MIMIC);
+ }
+
+ private static final String ANDROID_OS_BUILD_VERSION = "android.os.Build$VERSION";
+
+ /**
+ * Get the API level from {@link android.os.Build.VERSION} via reflection. The reason to use
+ * relection is to avoid dependency on {@link android.os.Build.VERSION}. The advantage of doing
+ * this is that even when you desugar a jar twice, and Desugars sees this class, there is no need
+ * to put {@link android.os.Build.VERSION} on the classpath.
+ *
+ * <p>Another reason of doing this is that it does not introduce any additional dependency into
+ * the input jars.
+ *
+ * @return The API level of the current device. If it is {@code null}, then it means there was an
+ * exception.
+ */
+ private static Integer readApiLevelFromBuildVersion() {
+ try {
+ Class<?> buildVersionClass = Class.forName(ANDROID_OS_BUILD_VERSION);
+ Field field = buildVersionClass.getField("SDK_INT");
+ return (Integer) field.get(null);
+ } catch (Exception e) {
+ System.err.println(
+ "Failed to retrieve value from "
+ + ANDROID_OS_BUILD_VERSION
+ + ".SDK_INT due to the following exception.");
+ e.printStackTrace(System.err);
+ return null;
+ }
+ }
+
+ /**
+ * The strategy to desugar try-with-resources statements. A strategy handles the behavior of an
+ * exception in terms of suppressed exceptions and stack trace printing.
+ */
+ abstract static class AbstractDesugaringStrategy {
+
+ protected static final Throwable[] EMPTY_THROWABLE_ARRAY = new Throwable[0];
+
+ public abstract void addSuppressed(Throwable receiver, Throwable suppressed);
+
+ public abstract Throwable[] getSuppressed(Throwable receiver);
+
+ public abstract void printStackTrace(Throwable receiver);
+
+ public abstract void printStackTrace(Throwable receiver, PrintStream stream);
+
+ public abstract void printStackTrace(Throwable receiver, PrintWriter writer);
+ }
+
+ /** This strategy just delegates all the method calls to java.lang.Throwable. */
+ static class ReuseDesugaringStrategy extends AbstractDesugaringStrategy {
+
+ @Override
+ public void addSuppressed(Throwable receiver, Throwable suppressed) {
+ receiver.addSuppressed(suppressed);
+ }
+
+ @Override
+ public Throwable[] getSuppressed(Throwable receiver) {
+ return receiver.getSuppressed();
+ }
+
+ @Override
+ public void printStackTrace(Throwable receiver) {
+ receiver.printStackTrace();
+ }
+
+ @Override
+ public void printStackTrace(Throwable receiver, PrintStream stream) {
+ receiver.printStackTrace(stream);
+ }
+
+ @Override
+ public void printStackTrace(Throwable receiver, PrintWriter writer) {
+ receiver.printStackTrace(writer);
+ }
+ }
+
+ /** This strategy mimics the behavior of suppressed exceptions with a map. */
+ static class MimicDesugaringStrategy extends AbstractDesugaringStrategy {
+
+ public static final String SUPPRESSED_PREFIX = "Suppressed: ";
+ private final WeakHashMap<Throwable, List<Throwable>> map = new WeakHashMap<>();
+
+ /**
+ * Suppress an exception. If the exception to be suppressed is {@receiver} or {@null}, an
+ * exception will be thrown.
+ *
+ * @param receiver
+ * @param suppressed
+ */
+ @Override
+ public void addSuppressed(Throwable receiver, Throwable suppressed) {
+ if (suppressed == receiver) {
+ throw new IllegalArgumentException("Self suppression is not allowed.", suppressed);
+ }
+ if (suppressed == null) {
+ throw new NullPointerException("The suppressed exception cannot be null.");
+ }
+ synchronized (this) {
+ List<Throwable> list = map.get(receiver);
+ if (list == null) {
+ list = new ArrayList<>(1);
+ map.put(receiver, list);
+ }
+ list.add(suppressed);
+ }
+ }
+
+ @Override
+ public synchronized Throwable[] getSuppressed(Throwable receiver) {
+ List<Throwable> list = map.get(receiver);
+ if (list == null || list.isEmpty()) {
+ return EMPTY_THROWABLE_ARRAY;
+ }
+ return list.toArray(new Throwable[0]);
+ }
+
+ /**
+ * Print the stack trace for the parameter {@code receiver}. Note that it is deliberate to NOT
+ * reuse the implementation {@code MimicDesugaringStrategy.printStackTrace(Throwable,
+ * PrintStream)}, because we are not sure whether the developer prints the stack trace to a
+ * different stream other than System.err. Therefore, it is a caveat that the stack traces of
+ * {@code receiver} and its suppressed exceptions are printed in two different streams.
+ */
+ @Override
+ public synchronized void printStackTrace(Throwable receiver) {
+ receiver.printStackTrace();
+ for (Throwable suppressed : getSuppressed(receiver)) {
+ System.err.print(SUPPRESSED_PREFIX);
+ suppressed.printStackTrace();
+ }
+ }
+
+ @Override
+ public synchronized void printStackTrace(Throwable receiver, PrintStream stream) {
+ receiver.printStackTrace(stream);
+ for (Throwable suppressed : getSuppressed(receiver)) {
+ stream.print(SUPPRESSED_PREFIX);
+ suppressed.printStackTrace(stream);
+ }
+ }
+
+ @Override
+ public synchronized void printStackTrace(Throwable receiver, PrintWriter writer) {
+ receiver.printStackTrace(writer);
+ for (Throwable suppressed : getSuppressed(receiver)) {
+ writer.print(SUPPRESSED_PREFIX);
+ suppressed.printStackTrace(writer);
+ }
+ }
+ }
+
+ /** This strategy ignores all suppressed exceptions, which is how retrolambda does. */
+ static class NullDesugaringStrategy extends AbstractDesugaringStrategy {
+
+ @Override
+ public void addSuppressed(Throwable receiver, Throwable suppressed) {
+ // Do nothing. The suppressed exception is discarded.
+ }
+
+ @Override
+ public Throwable[] getSuppressed(Throwable receiver) {
+ return EMPTY_THROWABLE_ARRAY;
+ }
+
+ @Override
+ public void printStackTrace(Throwable receiver) {
+ receiver.printStackTrace();
+ }
+
+ @Override
+ public void printStackTrace(Throwable receiver, PrintStream stream) {
+ receiver.printStackTrace(stream);
+ }
+
+ @Override
+ public void printStackTrace(Throwable receiver, PrintWriter writer) {
+ receiver.printStackTrace(writer);
+ }
+ }
+}
diff --git a/java/com/google/devtools/common/options/IsolatedOptionsData.java b/java/com/google/devtools/common/options/IsolatedOptionsData.java
index 0dc787c..81820c4 100644
--- a/java/com/google/devtools/common/options/IsolatedOptionsData.java
+++ b/java/com/google/devtools/common/options/IsolatedOptionsData.java
@@ -17,6 +17,7 @@ package com.google.devtools.common.options;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Ordering;
+import com.google.devtools.common.options.OptionsParser.ConstructionException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@@ -93,6 +94,16 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
*/
private final ImmutableMap<Field, Boolean> allowMultiple;
+ /**
+ * Mapping from each options class to whether or not it has the {@link UsesOnlyCoreTypes}
+ * annotation (unordered).
+ */
+ private final ImmutableMap<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes;
+
+ /** These categories used to indicate OptionUsageRestrictions, but no longer. */
+ private static final ImmutableList<String> DEPRECATED_CATEGORIES = ImmutableList.of(
+ "undocumented", "hidden", "internal");
+
private IsolatedOptionsData(
Map<Class<? extends OptionsBase>,
Constructor<?>> optionsClasses,
@@ -101,7 +112,8 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
Map<Class<? extends OptionsBase>, ImmutableList<Field>> allOptionsFields,
Map<Field, Object> optionDefaults,
Map<Field, Converter<?>> converters,
- Map<Field, Boolean> allowMultiple) {
+ Map<Field, Boolean> allowMultiple,
+ Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes) {
this.optionsClasses = ImmutableMap.copyOf(optionsClasses);
this.nameToField = ImmutableMap.copyOf(nameToField);
this.abbrevToField = ImmutableMap.copyOf(abbrevToField);
@@ -110,6 +122,7 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
this.optionDefaults = Collections.unmodifiableMap(optionDefaults);
this.converters = ImmutableMap.copyOf(converters);
this.allowMultiple = ImmutableMap.copyOf(allowMultiple);
+ this.usesOnlyCoreTypes = ImmutableMap.copyOf(usesOnlyCoreTypes);
}
protected IsolatedOptionsData(IsolatedOptionsData other) {
@@ -120,7 +133,8 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
other.allOptionsFields,
other.optionDefaults,
other.converters,
- other.allowMultiple);
+ other.allowMultiple,
+ other.usesOnlyCoreTypes);
}
/**
@@ -173,6 +187,10 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
return allowMultiple.get(field);
}
+ public boolean getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass) {
+ return usesOnlyCoreTypes.get(optionsClass);
+ }
+
/**
* For an option that does not use {@link Option#allowMultiple}, returns its type. For an option
* that does use it, asserts that the type is a {@code List<T>} and returns its element type
@@ -183,11 +201,11 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
if (annotation.allowMultiple()) {
// If the type isn't a List<T>, this is an error in the option's declaration.
if (!(fieldType instanceof ParameterizedType)) {
- throw new AssertionError("Type of multiple occurrence option must be a List<...>");
+ throw new ConstructionException("Type of multiple occurrence option must be a List<...>");
}
ParameterizedType pfieldType = (ParameterizedType) fieldType;
if (pfieldType.getRawType() != List.class) {
- throw new AssertionError("Type of multiple occurrence option must be a List<...>");
+ throw new ConstructionException("Type of multiple occurrence option must be a List<...>");
}
fieldType = pfieldType.getActualTypeArguments()[0];
}
@@ -234,7 +252,7 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
Type type = getFieldSingularType(optionField, annotation);
Converter<?> converter = Converters.DEFAULT_CONVERTERS.get(type);
if (converter == null) {
- throw new AssertionError(
+ throw new ConstructionException(
"No converter found for "
+ type
+ "; possible fix: add "
@@ -251,7 +269,7 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
} catch (Exception e) {
// This indicates an error in the Converter, and should be discovered the first time it is
// used.
- throw new AssertionError(e);
+ throw new ConstructionException(e);
}
}
@@ -336,9 +354,11 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
Map<String, String> booleanAliasMap,
String optionName) {
// Check that the negating alias does not conflict with existing flags.
+ checkForCollisions(nameToFieldMap, "no_" + optionName, "boolean option alias");
checkForCollisions(nameToFieldMap, "no" + optionName, "boolean option alias");
// Record that the boolean option takes up additional namespace for its negating alias.
+ booleanAliasMap.put("no_" + optionName, optionName);
booleanAliasMap.put("no" + optionName, optionName);
}
@@ -361,6 +381,8 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
// Maps the negated boolean flag aliases to the original option name.
Map<String, String> booleanAliasMap = new HashMap<>();
+ Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypesBuilder = new HashMap<>();
+
// Read all Option annotations:
for (Class<? extends OptionsBase> parsedOptionsClass : classes) {
try {
@@ -378,7 +400,13 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
Option annotation = field.getAnnotation(Option.class);
String optionName = annotation.name();
if (optionName == null) {
- throw new AssertionError("Option cannot have a null name");
+ throw new ConstructionException("Option cannot have a null name");
+ }
+
+ if (DEPRECATED_CATEGORIES.contains(annotation.category())) {
+ throw new ConstructionException(
+ "Documentation level is no longer read from the option category. Category \""
+ + annotation.category() + "\" in option \"" + optionName + "\" is disallowed.");
}
Type fieldType = getFieldSingularType(field, annotation);
@@ -389,14 +417,14 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
if (converter == Converter.class) {
Converter<?> actualConverter = Converters.DEFAULT_CONVERTERS.get(fieldType);
if (actualConverter == null) {
- throw new AssertionError("Cannot find converter for field of type "
+ throw new ConstructionException("Cannot find converter for field of type "
+ field.getType() + " named " + field.getName()
+ " in class " + field.getDeclaringClass().getName());
}
converter = actualConverter.getClass();
}
if (Modifier.isAbstract(converter.getModifiers())) {
- throw new AssertionError("The converter type " + converter
+ throw new ConstructionException("The converter type " + converter
+ " must be a concrete type");
}
Type converterResultType;
@@ -404,8 +432,8 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
Method convertMethod = converter.getMethod("convert", String.class);
converterResultType = GenericTypeHelper.getActualReturnType(converter, convertMethod);
} catch (NoSuchMethodException e) {
- throw new AssertionError("A known converter object doesn't implement the convert"
- + " method");
+ throw new ConstructionException(
+ "A known converter object doesn't implement the convert method");
}
if (annotation.allowMultiple()) {
@@ -413,22 +441,33 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
Type elementType =
((ParameterizedType) converterResultType).getActualTypeArguments()[0];
if (!GenericTypeHelper.isAssignableFrom(fieldType, elementType)) {
- throw new AssertionError("If the converter return type of a multiple occurrence " +
- "option is a list, then the type of list elements (" + fieldType + ") must be " +
- "assignable from the converter list element type (" + elementType + ")");
+ throw new ConstructionException(
+ "If the converter return type of a multiple occurrence option is a list, then "
+ + "the type of list elements ("
+ + fieldType
+ + ") must be assignable from the converter list element type ("
+ + elementType
+ + ")");
}
} else {
if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) {
- throw new AssertionError("Type of list elements (" + fieldType +
- ") for multiple occurrence option must be assignable from the converter " +
- "return type (" + converterResultType + ")");
+ throw new ConstructionException(
+ "Type of list elements ("
+ + fieldType
+ + ") for multiple occurrence option must be assignable from the converter "
+ + "return type ("
+ + converterResultType
+ + ")");
}
}
} else {
if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) {
- throw new AssertionError("Type of field (" + fieldType +
- ") must be assignable from the converter " +
- "return type (" + converterResultType + ")");
+ throw new ConstructionException(
+ "Type of field ("
+ + fieldType
+ + ") must be assignable from the converter return type ("
+ + converterResultType
+ + ")");
}
}
@@ -461,7 +500,24 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
convertersBuilder.put(field, findConverter(field));
allowMultipleBuilder.put(field, annotation.allowMultiple());
+
+ }
+
+ boolean usesOnlyCoreTypes = parsedOptionsClass.isAnnotationPresent(UsesOnlyCoreTypes.class);
+ if (usesOnlyCoreTypes) {
+ // Validate that @UsesOnlyCoreTypes was used correctly.
+ for (Field field : fields) {
+ // The classes in coreTypes are all final. But even if they weren't, we only want to check
+ // for exact matches; subclasses would not be considered core types.
+ if (!UsesOnlyCoreTypes.CORE_TYPES.contains(field.getType())) {
+ throw new ConstructionException(
+ "Options class '" + parsedOptionsClass.getName() + "' is marked as "
+ + "@UsesOnlyCoreTypes, but field '" + field.getName()
+ + "' has type '" + field.getType().getName() + "'");
+ }
+ }
}
+ usesOnlyCoreTypesBuilder.put(parsedOptionsClass, usesOnlyCoreTypes);
}
return new IsolatedOptionsData(
@@ -471,7 +527,8 @@ public class IsolatedOptionsData extends OpaqueOptionsData {
allOptionsFieldsBuilder,
optionDefaultsBuilder,
convertersBuilder,
- allowMultipleBuilder);
+ allowMultipleBuilder,
+ usesOnlyCoreTypesBuilder);
}
}
diff --git a/java/com/google/devtools/common/options/Option.java b/java/com/google/devtools/common/options/Option.java
index c43966c..e040046 100644
--- a/java/com/google/devtools/common/options/Option.java
+++ b/java/com/google/devtools/common/options/Option.java
@@ -88,9 +88,6 @@ public @interface Option {
*/
String category() default "misc";
- // TODO(b/37353610) the old convention was to include documentation level in the category(),
- // which is still permitted for backwards compatibility. This field should be used for any new
- // options, as the old category use will be removed.
/**
* Options have multiple uses, some flags, some not. For user-visible flags, they are
* "documented," but otherwise, there are 3 types of undocumented options.
diff --git a/java/com/google/devtools/common/options/OptionsBase.java b/java/com/google/devtools/common/options/OptionsBase.java
index 0ca9e44..315efe8 100644
--- a/java/com/google/devtools/common/options/OptionsBase.java
+++ b/java/com/google/devtools/common/options/OptionsBase.java
@@ -16,15 +16,15 @@ package com.google.devtools.common.options;
import com.google.common.escape.CharEscaperBuilder;
import com.google.common.escape.Escaper;
-
+import java.lang.reflect.Field;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
- * Base class for all options classes. Extend this class, adding public
- * instance fields annotated with @Option. Then you can create instances
- * either programmatically:
+ * Base class for all options classes. Extend this class, adding public instance fields annotated
+ * with {@link Option}. Then you can create instances either programmatically:
*
* <pre>
* X x = Options.getDefaults(X.class);
@@ -40,11 +40,10 @@ import java.util.Map.Entry;
* X x = parser.getOptions(X.class);
* </pre>
*
- * <p>Subclasses of OptionsBase <b>must</b> be constructed reflectively,
- * i.e. using not {@code new MyOptions}, but one of the two methods above
- * instead. (Direct construction creates an empty instance, not containing
- * default values. This leads to surprising behavior and often
- * NullPointerExceptions, etc.)
+ * <p>Subclasses of {@code OptionsBase} <i>must</i> be constructed reflectively, i.e. using not
+ * {@code new MyOptions()}, but one of the above methods instead. (Direct construction creates an
+ * empty instance, not containing default values. This leads to surprising behavior and often {@code
+ * NullPointerExceptions}, etc.)
*/
public abstract class OptionsBase {
@@ -61,13 +60,25 @@ public abstract class OptionsBase {
}
/**
- * Returns this options object in the form of a (new) mapping from option
- * names, including inherited ones, to option values. If the public fields
- * are mutated, this will be reflected in subsequent calls to {@code asMap}.
- * Mutation of this map by the caller does not affect this options object.
+ * Returns a mapping from option names to values, for each option on this object, including
+ * inherited ones. The mapping is a copy, so subsequent mutations to it or to this object are
+ * independent. Entries are sorted alphabetically.
*/
- public final Map<String, Object> asMap() {
- return OptionsParserImpl.optionsAsMap(this);
+ public final <O extends OptionsBase> Map<String, Object> asMap() {
+ // Generic O is needed to tell the type system that the toMap() call is safe.
+ // The casts are safe because "this" is an instance of "getClass()"
+ // which subclasses OptionsBase.
+ @SuppressWarnings("unchecked")
+ O castThis = (O) this;
+ @SuppressWarnings("unchecked")
+ Class<O> castClass = (Class<O>) getClass();
+
+ Map<String, Object> map = new LinkedHashMap<>();
+ for (Map.Entry<Field, Object> entry : OptionsParser.toMap(castClass, castThis).entrySet()) {
+ String name = entry.getKey().getAnnotation(Option.class).name();
+ map.put(name, entry.getValue());
+ }
+ return map;
}
@Override
diff --git a/java/com/google/devtools/common/options/OptionsParser.java b/java/com/google/devtools/common/options/OptionsParser.java
index 9574a90..c6bd002 100644
--- a/java/com/google/devtools/common/options/OptionsParser.java
+++ b/java/com/google/devtools/common/options/OptionsParser.java
@@ -21,13 +21,17 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
import com.google.common.escape.Escaper;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.FileSystem;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
@@ -444,8 +448,7 @@ public class OptionsParser implements OptionsProvider {
}
private OptionUsageRestrictions optionUsageRestrictions() {
- Option option = field.getAnnotation(Option.class);
- return OptionsParser.documentationLevel(option);
+ return field.getAnnotation(Option.class).optionUsageRestrictions();
}
public boolean isDocumented() {
@@ -556,12 +559,12 @@ public class OptionsParser implements OptionsProvider {
if (description == null) {
description = "Options category '" + category + "'";
}
- if (documentationLevel(option) == OptionUsageRestrictions.DOCUMENTED) {
+ if (option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) {
desc.append("\n").append(description).append(":\n");
}
}
- if (documentationLevel(option) == OptionUsageRestrictions.DOCUMENTED) {
+ if (option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) {
OptionsUsage.getUsage(optionField, desc, helpVerbosity, impl.getOptionsData());
}
}
@@ -594,8 +597,8 @@ public class OptionsParser implements OptionsProvider {
for (Field optionField : allFields) {
Option option = optionField.getAnnotation(Option.class);
String category = option.category();
- OptionUsageRestrictions level = documentationLevel(option);
- if (!category.equals(prevCategory) && level == OptionUsageRestrictions.DOCUMENTED) {
+ if (!category.equals(prevCategory)
+ && option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) {
String description = categoryDescriptions.get(category);
if (description == null) {
description = "Options category '" + category + "'";
@@ -608,7 +611,7 @@ public class OptionsParser implements OptionsProvider {
prevCategory = category;
}
- if (level == OptionUsageRestrictions.DOCUMENTED) {
+ if (option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) {
OptionsUsage.getUsageHtml(optionField, desc, escaper, impl.getOptionsData());
}
}
@@ -642,7 +645,7 @@ public class OptionsParser implements OptionsProvider {
});
for (Field optionField : allFields) {
Option option = optionField.getAnnotation(Option.class);
- if (documentationLevel(option) == OptionUsageRestrictions.DOCUMENTED) {
+ if (option.optionUsageRestrictions() == OptionUsageRestrictions.DOCUMENTED) {
OptionsUsage.getCompletion(optionField, desc);
}
}
@@ -674,30 +677,6 @@ public class OptionsParser implements OptionsProvider {
return impl.getOptionValueDescription(name);
}
- @Deprecated
- // TODO(b/37353610) the old convention was to include documentation level in the category(),
- // which is still permitted for backwards compatibility. The enum field should be used for any new
- // options, as the old category, and this function, will be removed.
- public static OptionUsageRestrictions documentationLevel(Option option) {
- // Until all options use the new documentationLabel attribute of an option, only rely on it if
- // it is not set to the default value.
- if (option.optionUsageRestrictions() != OptionUsageRestrictions.DOCUMENTED) {
- return option.optionUsageRestrictions();
- }
-
- // Otherwise, continue reading from the category.
- String category = option.category();
- if ("undocumented".equals(category)) {
- return OptionUsageRestrictions.UNDOCUMENTED;
- } else if ("hidden".equals(category)) {
- return OptionUsageRestrictions.HIDDEN;
- } else if ("internal".equals(category)) {
- return OptionUsageRestrictions.INTERNAL;
- } else {
- return OptionUsageRestrictions.DOCUMENTED;
- }
- }
-
/**
* A convenience method, equivalent to
* {@code parse(OptionPriority.COMMAND_LINE, null, Arrays.asList(args))}.
@@ -806,4 +785,124 @@ public class OptionsParser implements OptionsProvider {
public List<String> canonicalize() {
return impl.asCanonicalizedList();
}
+
+ /** Returns all options fields of the given options class, in alphabetic order. */
+ public static Collection<Field> getFields(Class<? extends OptionsBase> optionsClass) {
+ OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass);
+ return data.getFieldsForClass(optionsClass);
+ }
+
+ /**
+ * Returns whether the given options class uses only the core types listed in {@link
+ * OptionsBase#coreTypes}. These are guaranteed to be deeply immutable and serializable.
+ */
+ public static boolean getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass) {
+ OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass);
+ return data.getUsesOnlyCoreTypes(optionsClass);
+ }
+
+ /**
+ * Returns a mapping from each option {@link Field} in {@code optionsClass} (including inherited
+ * ones) to its value in {@code options}.
+ *
+ * <p>The map is a mutable copy; changing the map won't affect {@code options} and vice versa.
+ * The map entries appear sorted alphabetically by option name.
+ *
+ * If {@code options} is an instance of a subclass of {@code optionsClass}, any options defined
+ * by the subclass are not included in the map.
+ *
+ * @throws IllegalArgumentException if {@code options} is not an instance of {@code optionsClass}
+ */
+ public static <O extends OptionsBase> Map<Field, Object> toMap(Class<O> optionsClass, O options) {
+ OptionsData data = getOptionsDataInternal(optionsClass);
+ // Alphabetized due to getFieldsForClass()'s order.
+ Map<Field, Object> map = new LinkedHashMap<>();
+ for (Field field : data.getFieldsForClass(optionsClass)) {
+ try {
+ map.put(field, field.get(options));
+ } catch (IllegalAccessException e) {
+ // All options fields of options classes should be public.
+ throw new IllegalStateException(e);
+ } catch (IllegalArgumentException e) {
+ // This would indicate an inconsistency in the cached OptionsData.
+ throw new IllegalStateException(e);
+ }
+ }
+ return map;
+ }
+
+ /**
+ * Given a mapping as returned by {@link #toMap}, and the options class it that its entries
+ * correspond to, this constructs the corresponding instance of the options class.
+ *
+ * @throws IllegalArgumentException if {@code map} does not contain exactly the fields of {@code
+ * optionsClass}, with values of the appropriate type
+ */
+ public static <O extends OptionsBase> O fromMap(Class<O> optionsClass, Map<Field, Object> map) {
+ // Instantiate the options class.
+ OptionsData data = getOptionsDataInternal(optionsClass);
+ O optionsInstance;
+ try {
+ Constructor<O> constructor = data.getConstructor(optionsClass);
+ Preconditions.checkNotNull(constructor, "No options class constructor available");
+ optionsInstance = constructor.newInstance();
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("Error while instantiating options class", e);
+ }
+
+ List<Field> fields = data.getFieldsForClass(optionsClass);
+ // Ensure all fields are covered, no extraneous fields.
+ validateFieldsSets(new LinkedHashSet<>(fields), new LinkedHashSet<>(map.keySet()));
+ // Populate the instance.
+ for (Field field : fields) {
+ // Non-null as per above check.
+ Object value = map.get(field);
+ try {
+ field.set(optionsInstance, value);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException(e);
+ }
+ // May also throw IllegalArgumentException if map value is ill typed.
+ }
+ return optionsInstance;
+ }
+
+ /**
+ * Raises a pretty {@link IllegalArgumentException} if the two sets of fields are not equal.
+ *
+ * <p>The entries in {@code fieldsFromMap} may be ill formed by being null or lacking an {@link
+ * Option} annotation. (This isn't done for {@code fieldsFromClass} because they come from an
+ * {@link OptionsData} object.)
+ */
+ private static void validateFieldsSets(
+ LinkedHashSet<Field> fieldsFromClass, LinkedHashSet<Field> fieldsFromMap) {
+ if (!fieldsFromClass.equals(fieldsFromMap)) {
+ List<String> extraNamesFromClass = new ArrayList<>();
+ List<String> extraNamesFromMap = new ArrayList<>();
+ for (Field field : fieldsFromClass) {
+ if (!fieldsFromMap.contains(field)) {
+ extraNamesFromClass.add("'" + field.getAnnotation(Option.class).name() + "'");
+ }
+ }
+ for (Field field : fieldsFromMap) {
+ // Extra validation on the map keys since they don't come from OptionsData.
+ if (!fieldsFromClass.contains(field)) {
+ if (field == null) {
+ extraNamesFromMap.add("<null field>");
+ } else {
+ Option annotation = field.getAnnotation(Option.class);
+ if (annotation == null) {
+ extraNamesFromMap.add("<non-Option field>");
+ } else {
+ extraNamesFromMap.add("'" + annotation.name() + "'");
+ }
+ }
+ }
+ }
+ throw new IllegalArgumentException(
+ "Map keys do not match fields of options class; extra map keys: {"
+ + Joiner.on(", ").join(extraNamesFromMap) + "}; extra options class options: {"
+ + Joiner.on(", ").join(extraNamesFromClass) + "}");
+ }
+ }
}
diff --git a/java/com/google/devtools/common/options/OptionsParserImpl.java b/java/com/google/devtools/common/options/OptionsParserImpl.java
index 5c635fb..a4d905a 100644
--- a/java/com/google/devtools/common/options/OptionsParserImpl.java
+++ b/java/com/google/devtools/common/options/OptionsParserImpl.java
@@ -85,7 +85,7 @@ class OptionsParserImpl {
private final List<String> warnings = new ArrayList<>();
private boolean allowSingleDashLongOptions = false;
-
+
private ArgsPreProcessor argsPreProcessor =
new ArgsPreProcessor() {
@Override
@@ -93,7 +93,7 @@ class OptionsParserImpl {
return args;
}
};
-
+
/**
* Create a new parser object
*/
@@ -112,32 +112,13 @@ class OptionsParserImpl {
void setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions) {
this.allowSingleDashLongOptions = allowSingleDashLongOptions;
}
-
+
/** Sets the ArgsPreProcessor for manipulations of the options before parsing. */
void setArgsPreProcessor(ArgsPreProcessor preProcessor) {
this.argsPreProcessor = Preconditions.checkNotNull(preProcessor);
}
/**
- * The implementation of {@link OptionsBase#asMap}.
- */
- static Map<String, Object> optionsAsMap(OptionsBase optionsInstance) {
- Class<? extends OptionsBase> optionsClass = optionsInstance.getClass();
- OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass);
- Map<String, Object> map = new HashMap<>();
- for (Field field : data.getFieldsForClass(optionsClass)) {
- try {
- String name = field.getAnnotation(Option.class).name();
- Object value = field.get(optionsInstance);
- map.put(name, value);
- } catch (IllegalAccessException e) {
- throw new IllegalStateException(e); // unreachable
- }
- }
- return map;
- }
-
- /**
* Implements {@link OptionsParser#asListOfUnparsedOptions()}.
*/
List<UnparsedOptionValueDescription> asListOfUnparsedOptions() {
@@ -639,18 +620,14 @@ class OptionsParserImpl {
// Look for a "no"-prefixed option name: "no<optionName>".
if (field == null && name.startsWith("no")) {
// Give a nice error if someone is using the deprecated --no_ prefix.
- // Note: With this check in place, is impossible to specify "--no_foo" for a flag named
- // "--_foo", if a --foo flag also exists, since that'll be interpreted as the "no_"
- // negating prefix for "--foo". Let that be a warning to anyone wanting to make flags that
- // start with underscores.
// TODO(Bazel-team): Remove the --no_ check when sufficient time has passed for users of
// that feature to have stopped using it.
- if (name.startsWith("no_") && optionsData.getFieldFromName(name.substring(3)) != null) {
- throw new OptionsParsingException(
- "'no_' prefixes are no longer accepted, --no<flag> is an accepted alternative.",
- name.substring(3));
- }
name = name.substring(2);
+ if (name.startsWith("_") && optionsData.getFieldFromName(name.substring(1)) != null) {
+ name = name.substring(1);
+ warnings.add("Option '" + name + "' is specified using the deprecated --no_ prefix. "
+ + "Use --no without the underscore instead.");
+ }
field = optionsData.getFieldFromName(name);
booleanValue = false;
if (field != null) {
@@ -674,7 +651,7 @@ class OptionsParserImpl {
Option option = field == null ? null : field.getAnnotation(Option.class);
if (option == null
- || OptionsParser.documentationLevel(option) == OptionUsageRestrictions.INTERNAL) {
+ || option.optionUsageRestrictions() == OptionUsageRestrictions.INTERNAL) {
// This also covers internal options, which are treated as if they did not exist.
throw new OptionsParsingException("Unrecognized option: " + arg, arg);
}
@@ -707,8 +684,8 @@ class OptionsParserImpl {
return null;
}
optionsInstance = constructor.newInstance();
- } catch (Exception e) {
- throw new IllegalStateException(e);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("Error while instantiating options class", e);
}
// Set the fields
diff --git a/java/com/google/devtools/common/options/UsesOnlyCoreTypes.java b/java/com/google/devtools/common/options/UsesOnlyCoreTypes.java
new file mode 100644
index 0000000..6f2714f
--- /dev/null
+++ b/java/com/google/devtools/common/options/UsesOnlyCoreTypes.java
@@ -0,0 +1,57 @@
+// Copyright 2017 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.common.options;
+
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+
+/**
+ * Applied to an {@link OptionsBase} subclass to indicate that all of its options fields have types
+ * chosen from {@link #coreTypes}. Any subclasses of the class to which it's applied must also
+ * satisfy the same property.
+ *
+ * <p>Options classes with this annotation are serializable and deeply immutable, except that the
+ * fields of the options class can be reassigned (although this is bad practice).
+ *
+ * <p>Note that {@link Option#allowMultiple} is not allowed for options in classes with this
+ * annotation, since their type is {@link List}.
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface UsesOnlyCoreTypes {
+
+ /**
+ * These are the core options field types. They all have default converters, are deeply immutable,
+ * and are serializable.
+ *
+ * Lists are not considered core types, so {@link Option#allowMultiple} options are not permitted.
+ */
+ public static final ImmutableList<Class<?>> CORE_TYPES = ImmutableList.of(
+ // 1:1 correspondence with Converters.DEFAULT_CONVERTERS.
+ String.class,
+ int.class,
+ long.class,
+ double.class,
+ boolean.class,
+ TriState.class,
+ Void.class
+ );
+}